尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue学生管理系统前端工程化实战指南

Vue学生管理系统前端工程化实战指南 简介本资源是一套基于Vue框架开发的学生管理系统前端源码面向前端初学者与Vue进阶开发者提供完整的单页面应用SPA实践案例助力掌握组件化开发、路由管理、状态交互及前后端分离架构设计。压缩包共284个文件总计34.19MB涵盖99个JavaScript逻辑文件、51个Vue组件、52张JPG界面素材、20个JSON配置项、8个CSS样式文件及13个Markdown文档说明结构清晰、模块分工明确其中Vue组件构成核心视图层JS文件实现业务交互CSS与图片保障界面呈现Markdown文档辅助理解项目结构与使用方式。目前已有344人学习下载适合用于课程设计、毕业项目参考或Vue技术栈系统性训练。源码配套stuManga-back-end后端项目可快速部署调试同时包含LICENSE开源协议支持合规学习与二次开发。1. 为什么一个“学生管理系统前端”要用 Vue 而不是原生 JS 或 jQuery当你接到「开发一个学生管理系统前端」的需求时第一反应可能是这不就是增删改查表格展示用 HTML AJAX 模板字符串不就能跑起来但现实很快会打脸——当教务老师要求“点击学号自动带出班级和专业信息”“导出 Excel 时保留当前筛选条件”“换肤功能支持夜间模式切换”“在 Chrome 和 Edge 上布局完全一致”原生方案的维护成本会指数级上升。Vue 的响应式数据绑定、组件化隔离、路由懒加载和状态管理能力恰恰是这类中等复杂度业务系统的“刚性基础设施”。它不追求极致性能如游戏或高频交易也不堆砌过度抽象如微前端架构而是用声明式语法把「表单校验逻辑」「分页请求状态同步」「权限按钮显隐控制」这些重复劳动封装成可复用、可测试、可协作的单元。本文面向已掌握 HTML/CSS/JS 基础、正准备用 Vue 实战落地的学生管理系统开发者从零开始构建一个可部署、可扩展、符合企业级前端工程规范的源码结构重点解决“怎么组织目录”“哪些组件必须拆”“API 请求如何统一管理”“权限控制怎么嵌入路由”这四个高频卡点。2. 用 Vue CLI 创建最小可运行骨架并初始化核心目录结构2.1 创建项目并选择关键依赖项使用 Vue CLI 5.0推荐 Node.js 18.x初始化项目避免手动配置 Webpack 复杂度。执行命令时明确指定包管理器和预设选项确保团队环境一致性npm init vuelatest # 交互式选择 # ✔ Project name: … student-mgmt-frontend # ✔ Add TypeScript? … No # ✔ Add JSX Support? … No # ✔ Add Vue Router for Single Page Application development? … Yes # ✔ Add Pinia for state management? … Yes # ✔ Add Vitest for Unit Testing? … No初期可跳过 # ✔ Add Cypress for both Unit and End-to-End Testing? … No # ✔ Add ESLint for code quality? … Yes选 Standard 风格 # ✔ Add Prettier for code formatting? … Yes提示不选 TypeScript 并非否定其价值而是降低新手理解门槛Pinia 必选因 Vue 3 官方推荐且比 Vuex 更轻量Router 必选学生管理系统必然存在「学生列表」「课程管理」「成绩录入」等多视图切换场景。生成后进入项目目录安装额外必需依赖cd student-mgmt-frontend npm install axios element-plus dayjs nprogressaxios统一 HTTP 客户端替代 Vue 内置的fetch支持请求拦截、响应拦截、取消重复请求element-plus基于 Vue 3 的 UI 组件库提供 Table、Form、Dialog、Pagination 等学生管理系统高频组件dayjs轻量日期处理库比 Moment.js 小 90%用于格式化入学时间、成绩录入时间nprogress顶部进度条提升用户等待感知尤其在列表加载、表单提交时。2.2 重构 src 目录为领域驱动结构默认 Vue CLI 目录扁平但学生管理系统需支撑未来扩展如加入教师端、管理员端必须按业务域划分。删除默认components/新建以下结构src/ ├── api/ # 所有 API 请求封装按模块组织 │ ├── student.ts # 学生相关接口list, create, update, delete │ ├── course.ts # 课程相关接口list, assignToClass │ ├── score.ts # 成绩相关接口batchImport, exportExcel │ └── auth.ts # 登录、权限获取接口 ├── assets/ # 静态资源图标、字体、样式变量 │ └── styles/ │ ├── variables.scss # 全局 SCSS 变量主题色、间距、圆角 │ └── reset.scss # 基础样式重置 ├── components/ # 可复用业务组件非 UI 原子组件 │ ├── table/ # 封装带搜索、分页、操作列的通用表格 │ │ └── BaseTable.vue │ ├── form/ # 封装带校验、重置、提交的通用表单 │ │ └── BaseForm.vue │ └── layout/ # 侧边栏、顶部导航、主内容区布局组件 ├── router/ # 路由配置含权限守卫 │ └── index.ts ├── stores/ # Pinia 状态管理按模块拆分 │ ├── student.ts # 学生列表数据、筛选条件、当前页码 │ ├── user.ts # 当前登录用户信息、角色权限 │ └── app.ts # 全局状态如 loading 状态、主题模式 ├── utils/ # 工具函数非业务逻辑 │ ├── request.ts # axios 实例封装含拦截器 │ ├── auth.ts # Token 存储与校验工具 │ └── excel.ts # Excel 导出辅助函数配合 SheetJS ├── views/ # 页面级组件对应路由 │ ├── Login.vue # 登录页无 Layout 包裹 │ ├── Layout.vue # 主布局含 Sidebar Header Main │ ├── StudentList.vue # 学生列表页使用 BaseTable │ └── StudentEdit.vue # 学生编辑页使用 BaseForm └── main.ts # 入口文件注册插件、挂载应用注意api/下每个文件只导出该模块的请求函数不包含业务逻辑stores/中user.ts必须在应用启动时从 localStorage 初始化否则刷新页面后权限丢失router/index.ts需在beforeEach守卫中检查user.role是否匹配路由meta.requiresAuth和meta.permission。2.3 配置 Axios 实例实现请求统一管理src/utils/request.ts是整个系统网络层的核心必须处理 Token 注入、错误统一提示、Loading 状态联动import axios from axios import NProgress from nprogress import { useUserStore } from /stores/user // 创建 axios 实例 const service axios.create({ baseURL: import.meta.env.VUE_APP_BASE_API || /api, // 通过 .env 文件配置 timeout: 10000, headers: { Content-Type: application/json } }) // 请求拦截器 service.interceptors.request.use( config { NProgress.start() // 开始进度条 const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error { NProgress.done() return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { NProgress.done() // 结束进度条 const { code, data, message } response.data if (code 200) { return data // 直接返回业务数据无需 .data.data } else if (code 401) { // Token 过期清空用户状态并跳转登录 const userStore useUserStore() userStore.logout() window.location.href /login return Promise.reject(new Error(登录已过期)) } else { ElMessage.error(message || 请求失败) return Promise.reject(new Error(message)) } }, error { NProgress.done() ElMessage.error(网络连接异常请检查网络) return Promise.reject(error) } ) export default service逻辑说明baseURL使用环境变量便于开发/api代理、测试https://test-api.com、生产https://prod-api.com三套地址切换response.data解构时假设后端返回标准{ code: 200, data: {}, message: }格式若实际格式不同此处需调整ElMessage来自 Element Plus需在main.ts中全局注册。3. 实现学生列表页BaseTable 组件封装与分页请求联动3.1 编写可复用的 BaseTable 组件学生列表页核心是表格但直接在StudentList.vue中写el-table会导致代码冗余每页都要写列定义、分页器、搜索框。抽取为components/table/BaseTable.vue通过props接收列配置和数据源!-- src/components/table/BaseTable.vue -- template div classbase-table !-- 搜索区域 -- div classsearch-bar v-ifsearchConfig el-form :modelsearchForm :inlinetrue submit.preventhandleSearch el-form-item v-foritem in searchConfig :keyitem.prop :labelitem.label component :isitem.component v-modelsearchForm[item.prop] v-binditem.props || {} / /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form /div !-- 表格主体 -- el-table :datatableData :loadingloading stylewidth: 100% el-table-column v-forcol in columns :keycol.prop :propcol.prop :labelcol.label :widthcol.width :formattercol.formatter template #default{ row } slot :namecol.prop :rowrow{{ row[col.prop] }}/slot /template /el-table-column el-table-column label操作 width180 fixedright template #default{ row } slot nameactions :rowrow / /template /el-table-column /el-table !-- 分页器 -- div classpagination v-ifpaginationConfig el-pagination v-model:current-pagepaginationConfig.currentPage v-model:page-sizepaginationConfig.pageSize :page-sizes[10, 20, 50, 100] layouttotal, sizes, prev, pager, next, jumper :totalpaginationConfig.total size-changehandleSizeChange current-changehandleCurrentChange / /div /div /template script setup langts import { ref, watch } from vue import type { PropType } from vue interface Column { prop: string label: string width?: string formatter?: (row: any, column: any, cellValue: any) string } interface SearchItem { prop: string label: string component: string // el-input | el-select | el-date-picker props?: Recordstring, any } interface PaginationConfig { currentPage: number pageSize: number total: number } const props defineProps({ columns: { type: Array as PropTypeColumn[], required: true }, tableData: { type: Array as PropTypeany[], default: () [] }, loading: { type: Boolean, default: false }, searchConfig: { type: Array as PropTypeSearchItem[], default: () [] }, paginationConfig: { type: Object as PropTypePaginationConfig, default: () ({ currentPage: 1, pageSize: 10, total: 0 }) } }) const emit defineEmits([search, size-change, current-change, reset]) const searchForm refRecordstring, any({}) // 初始化 searchForm确保所有搜索字段都有初始值 watch(() props.searchConfig, (newConfig) { if (newConfig newConfig.length) { searchForm.value {} newConfig.forEach(item { searchForm.value[item.prop] }) } }, { immediate: true }) const handleSearch () { emit(search, { ...searchForm.value }) } const handleReset () { searchForm.value {} emit(reset) } const handleSizeChange (val: number) { emit(size-change, val) } const handleCurrentChange (val: number) { emit(current-change, val) } /script style scoped .search-bar { margin-bottom: 16px; } .pagination { margin-top: 16px; text-align: right; } /style参数说明columns定义表格列支持formatter自定义渲染searchConfig描述搜索表单项输入框、下拉框、日期选择器component字段决定渲染哪个 Element Plus 组件paginationConfig传入分页参数emit触发事件供父组件监听v-model在el-input等组件上自动绑定无需手动input。3.2 在 StudentList.vue 中集成 BaseTable 并实现分页请求views/StudentList.vue不再写重复的表格逻辑专注业务组装!-- src/views/StudentList.vue -- template div classstudent-list base-table :columnscolumns :table-datastudentList :loadingloading :search-configsearchConfig :pagination-configpagination searchhandleSearch size-changehandleSizeChange current-changehandleCurrentChange resethandleReset !-- 自定义“性别”列显示 -- template #gender{ row } {{ row.gender 1 ? 男 : row.gender 0 ? 女 : 未知 }} /template !-- 自定义“操作”列 -- template #actions{ row } el-button sizesmall clickhandleEdit(row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(row)删除/el-button /template /base-table /div /template script setup langts import { ref, onMounted } from vue import BaseTable from /components/table/BaseTable.vue import { useStudentStore } from /stores/student import { getStudentList } from /api/student import type { StudentItem } from /api/student // 列配置 const columns [ { prop: id, label: 学号, width: 100 }, { prop: name, label: 姓名, width: 120 }, { prop: gender, label: 性别, width: 80 }, { prop: class_name, label: 班级, width: 150 }, { prop: major, label: 专业, width: 180 }, { prop: enrollment_date, label: 入学时间, width: 140 } ] // 搜索配置 const searchConfig [ { prop: name, label: 姓名, component: el-input }, { prop: class_id, label: 班级, component: el-select, props: { options: [] } }, // 后续从 API 获取 { prop: major, label: 专业, component: el-input } ] // 分页状态 const pagination ref({ currentPage: 1, pageSize: 10, total: 0 }) const studentList refStudentItem[]([]) const loading ref(false) const studentStore useStudentStore() onMounted(() { fetchStudentList() }) const fetchStudentList async () { loading.value true try { const res await getStudentList({ page: pagination.value.currentPage, size: pagination.value.pageSize, ...studentStore.searchParams // 合并搜索条件 }) studentList.value res.list pagination.value.total res.total } catch (err) { console.error(err) } finally { loading.value false } } const handleSearch (params: Recordstring, any) { studentStore.setSearchParams(params) pagination.value.currentPage 1 fetchStudentList() } const handleSizeChange (size: number) { pagination.value.pageSize size pagination.value.currentPage 1 fetchStudentList() } const handleCurrentChange (page: number) { pagination.value.currentPage page fetchStudentList() } const handleReset () { studentStore.resetSearchParams() pagination.value.currentPage 1 fetchStudentList() } const handleEdit (row: StudentItem) { // 跳转到编辑页携带 studentId router.push(/student/edit/${row.id}) } const handleDelete async (row: StudentItem) { ElMessageBox.confirm(确定删除学生 ${row.name}, 提示, { confirmButtonText: 确定, cancelButtonText: 取消, type: warning }).then(async () { try { await deleteStudent(row.id) // 调用 API 删除 ElMessage.success(删除成功) fetchStudentList() // 刷新列表 } catch (err) { ElMessage.error(删除失败) } }) } /script关键点getStudentList接口返回{ list: [], total: 123 }studentStore.searchParams是 Pinia 中存储的搜索条件对象handleSearch更新条件后重置页码为 1handleEdit使用 Vue Router 编程式导航路径/student/edit/:id需在router/index.ts中配置动态路由handleDelete使用 Element Plus 的ElMessageBox提供确认弹窗增强用户操作安全感。4. 路由权限控制与登录态持久化实现4.1 设计路由元信息与权限守卫逻辑学生管理系统需区分「游客」「学生」「教师」「管理员」角色不同角色看到的菜单和可访问页面不同。在router/index.ts中为每个路由添加meta字段// src/router/index.ts import { createRouter, createWebHistory } from vue-router import { useUserStore } from /stores/user const routes [ { path: /login, name: Login, component: () import(/views/Login.vue), meta: { requiresAuth: false } // 不需要登录 }, { path: /, name: Layout, component: () import(/components/layout/Layout.vue), meta: { requiresAuth: true }, // 需要登录 children: [ { path: , name: StudentList, component: () import(/views/StudentList.vue), meta: { title: 学生管理, permission: [admin, teacher] // 仅 admin 和 teacher 可见 } }, { path: course, name: CourseList, component: () import(/views/CourseList.vue), meta: { title: 课程管理, permission: [admin] // 仅 admin 可见 } }, { path: score, name: ScoreList, component: () import(/views/ScoreList.vue), meta: { title: 成绩管理, permission: [admin, teacher] } } ] } ] const router createRouter({ history: createWebHistory(), routes }) // 全局前置守卫 router.beforeEach(async (to, from, next) { const userStore useUserStore() // 未登录且目标页需要认证 if (to.meta.requiresAuth !userStore.token) { next({ name: Login }) return } // 已登录但无权限 if (to.meta.permission !userStore.hasPermission(to.meta.permission)) { next({ name: StudentList }) // 跳转到首页 return } // 登录页已登录则跳转首页 if (to.name Login userStore.token) { next({ name: StudentList }) return } next() }) export default routerhasPermission方法在stores/user.ts中实现return to.meta.permission.some(role this.roles.includes(role))roles字段由登录成功后从后端返回的user_info.roles数组赋值token存储在localStorage同时在userStore的actions.logout()中清除。4.2 构建登录页并实现 Token 持久化views/Login.vue需完成表单校验、API 调用、Token 存储、跳转三件事!-- src/views/Login.vue -- template div classlogin-container el-card classlogin-card shadownever h2 classtitle学生管理系统/h2 el-form :modelloginForm :rulesrules refformRef label-width80px el-form-item label用户名 propusername el-input v-modelloginForm.username placeholder请输入用户名 / /el-form-item el-form-item label密码 proppassword el-input v-modelloginForm.password typepassword placeholder请输入密码 / /el-form-item el-form-item el-button typeprimary stylewidth: 100% clickhandleSubmit :loadingloading 登录 /el-button /el-form-item /el-form /el-card /div /template script setup langts import { ref, reactive } from vue import { ElMessage } from element-plus import { useRouter } from vue-router import { useUserStore } from /stores/user import { login } from /api/auth const router useRouter() const userStore useUserStore() const loading ref(false) const loginForm reactive({ username: , password: }) const rules { username: [{ required: true, message: 请输入用户名, trigger: blur }], password: [{ required: true, message: 请输入密码, trigger: blur }] } const formRef ref() const handleSubmit async () { if (!formRef.value) return await (formRef.value as any).validate(async (valid: boolean) { if (valid) { loading.value true try { const res await login(loginForm.username, loginForm.password) // 存储 token 和用户信息 localStorage.setItem(token, res.token) userStore.setUserInfo(res.user) ElMessage.success(登录成功) router.push(/) // 跳转首页 } catch (err) { ElMessage.error(登录失败 (err as Error).message) } finally { loading.value false } } }) } /script style scoped .login-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .login-card { width: 400px; padding: 30px; } .title { text-align: center; margin-bottom: 20px; color: #333; } /style注意loginAPI 返回token和user对象含id,name,roles等字段userStore.setUserInfo()方法内部会将roles数组存入this.roles供hasPermission使用localStorage.setItem(token, ...)是持久化关键浏览器关闭后仍有效router.push(/)触发beforeEach守卫自动校验权限。5. 生产环境打包优化与常见布局异常排查技巧5.1 配置 Vite 构建参数提升首屏加载速度Vue CLI 默认使用 Webpack但 Vue 3 项目更推荐迁移到 Vite本项目若用 Vue CLI 5.0其底层已支持 Vite 模式。在vite.config.ts中进行针对性优化import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : resolve(__dirname, src) } }, build: { target: es2015, // 兼容 IE11若无需 IE 可设为 modules outDir: dist, sourcemap: false, // 生产环境关闭 source map rollupOptions: { output: { manualChunks: { // 将第三方库单独打包避免主包过大 vendor: [vue, vue-router, pinia, axios], element: [element-plus], utils: [dayjs, nprogress] } } } }, server: { port: 3000, open: true, proxy: { /api: { target: http://localhost:8080, // 后端开发地址 changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })关键参数manualChunks将vendor框架核心、elementUI 库、utils工具库分离成独立 chunk利用浏览器缓存target: es2015确保生成兼容性代码proxy配置开发环境 API 代理避免跨域sourcemap: false减少打包体积防止源码泄露。5.2 解决 Vue 打包后布局异常的三大高频原因学生管理系统上线后常出现「表格错位」「按钮文字被截断」「夜间模式样式失效」等问题本质是 CSS 加载顺序或作用域冲突。以下是三个最有效的排查与修复技巧5.2.1 检查 CSS 优先级与覆盖规则Element Plus 的样式可能被全局reset.css或variables.scss覆盖。在main.ts中确保样式加载顺序// src/main.ts import { createApp } from vue import App from ./App.vue import router from ./router import store from ./stores import ElementPlus from element-plus import element-plus/dist/index.css // 必须在自定义样式之前引入 import /assets/styles/reset.scss import /assets/styles/variables.scss const app createApp(App) app.use(router) app.use(store) app.use(ElementPlus) app.mount(#app)原因element-plus/dist/index.css是编译后的完整样式若在其后引入reset.scss则reset中的* { box-sizing: border-box }可能被覆盖variables.scss中定义的$--color-primary等变量需在element-plus之后通过el-config-provider覆盖。5.2.2 修复 Flex 布局在 Safari 中的兼容性问题学生列表页常用display: flex实现搜索栏对齐但 Safari 旧版本对flex: 1解析异常。在BaseTable.vue的.search-bar类中添加.search-bar { display: flex; flex-wrap: wrap; gap: 12px; /* 替代 margin更可靠 */ .el-form-item { margin-bottom: 0; /* 清除 el-form-item 默认 margin */ } // Safari 兼容写法 supports not (gap: 12px) { .el-form-item { margin-right: 12px; } } }原因gap属性在 Safari 14.1 才完全支持低版本需回退到marginflex-wrap: wrap防止搜索项过多时溢出容器margin-bottom: 0消除 Element Plus 表单项底部空白。5.2.3 验证动态主题切换是否生效若实现夜间模式需确保stores/app.ts中的theme状态变更能触发全局样式重载// src/stores/app.ts import { defineStore } from pinia export const useAppStore defineStore(app, { state: () ({ theme: light as light | dark // light/dark 二值 }), actions: { toggleTheme() { this.theme this.theme light ? dark : light document.documentElement.setAttribute(data-theme, this.theme) // 触发 CSS 变量更新 this.updateCssVariables() }, updateCssVariables() { const root document.documentElement if (this.theme dark) { root.style.setProperty(--el-color-primary, #409eff) root.style.setProperty(--el-bg-color, #1f1f1f) } else { root.style.setProperty(--el-color-primary, #667eea) root.style.setProperty(--el-bg-color, #ffffff) } } } })验证方法打开浏览器开发者工具 → Elements →html标签 → 查看>script setup import { onMounted } from vue import { useAppStore } from /stores/app onMounted(() { const appStore useAppStore() // 从 localStorage 读取上次主题偏好 const savedTheme localStorage.getItem(theme) as light | dark | null if (savedTheme) { appStore.theme savedTheme } appStore.updateCssVariables() }) /script本文还有配套的精品资源点击获取
返回列表