# Vue3 后台管理项目开发规范 ## 项目概述 本项目是一个基于 Vue3 的后台管理系统,采用现代化技术栈构建,提供高效、美观的管理界面。 ## 技术栈 - **Vue 3**: 渐进式 JavaScript 框架 - **Vite**: 下一代前端构建工具 - **Ant Design Vue**: 基于 Vue 3 的 UI 组件库 - **Vue Router**: Vue.js 官方路由管理器 - **Pinia**: Vue 3 官方状态管理库 - **Axios**: HTTP 客户端 - **JavaScript**: 主要开发语言(非 TypeScript) - **Composition API**: 组合式 API 开发模式 ## 图标系统 项目采用 Ant Design Vue 图标库,已全局引入: - **Ant Design Vue Icons**: Ant Design Vue 官方图标库 **重要提示**: 图标已全局默认引入,开发时请勿重复引入或按需引入,直接使用即可。 ## 开发规范 ### 1. 项目结构 ``` resources/admin/ ├── src/ │ ├── api/ # API 接口层 │ │ ├── auth.js # 认证相关接口 │ │ ├── menu.js # 菜单接口 │ │ └── system.js # 系统相关接口 │ ├── assets/ # 静态资源 │ │ ├── images/ # 图片资源 │ │ └── style/ # 全局样式 │ ├── components/ # 公共组件 │ │ ├── scEditor/ # 富文本编辑器 │ │ ├── scForm/ # 表单组件 │ │ ├── scIconPicker/ # 图标选择器 │ │ ├── scTable/ # 表格组件 │ │ └── scUpload/ # 上传组件 │ ├── config/ # 配置文件 │ │ ├── index.js # 主配置 │ │ ├── routes.js # 路由配置 │ │ └── upload.js # 上传配置 │ ├── hooks/ # 组合式 API Hooks │ │ ├── useI18n.js # 国际化 Hook │ │ └── useTable.js # 表格 Hook │ ├── i18n/ # 国际化配置 │ │ ├── index.js # i18n 配置 │ │ └── locales/ # 语言包 │ ├── layouts/ # 布局组件 │ │ ├── components/ # 布局子组件 │ │ ├── other/ # 其他布局 │ │ └── index.vue # 主布局 │ ├── pages/ # 页面组件 │ │ ├── auth/ # 认证页面 │ │ │ ├── users/ # 用户管理 │ │ │ │ ├── index.vue # 主页面 │ │ │ │ └── components/ # 页面私有组件 │ │ │ │ ├── SaveDialog.vue # 新增/编辑弹窗 │ │ │ │ ├── RoleDialog.vue # 角色设置弹窗 │ │ │ │ └── DetailDialog.vue # 详情弹窗 │ │ │ ├── roles/ # 角色管理 │ │ │ │ ├── index.vue │ │ │ │ └── components/ │ │ │ │ ├── SaveDialog.vue │ │ │ │ └── PermissionDialog.vue │ │ │ └── ... │ │ ├── home/ # 首页 │ │ ├── login/ # 登录页 │ │ ├── system/ # 系统管理 │ │ └── ucenter/ # 个人中心 │ ├── router/ # 路由配置 │ │ ├── index.js # 主路由 │ │ └── systemRoutes.js # 系统路由 │ ├── stores/ # 状态管理 │ │ ├── index.js # Store 入口 │ │ ├── persist.js # 持久化配置 │ │ └── modules/ # Store 模块 │ ├── utils/ # 工具函数 │ │ ├── request.js # Axios 封装 │ │ └── tool.js # 工具函数 │ ├── App.vue # 根组件 │ ├── boot.js # 引导文件 │ ├── main.js # 入口文件 │ └── style.css # 全局样式 ├── public/ # 公共资源 ├── index.html # HTML 模板 ├── package.json # 依赖配置 ├── vite.config.js # Vite 配置 └── README.md # 项目说明 ``` ### 2. 页面组件开发规范 #### 页面组件目录结构 每个页面模块应遵循以下目录结构: ``` pages/ └── {module}/ # 模块目录(如 auth, system 等) └── {resource}/ # 资源目录(如 users, roles 等) ├── index.vue # 主页面(列表页) └── components/ # 页面私有组件目录 ├── SaveDialog.vue # 新增/编辑弹窗(字段较少时使用) ├── SaveDrawer.vue # 新增/编辑抽屉(字段较多时使用) ├── DetailDialog.vue # 详情弹窗 ├── RoleDialog.vue # 角色设置弹窗 ├── SearchDrawer.vue # 高级搜索抽屉 └── ... # 其他页面专用组件 ``` **目录结构说明**: - `index.vue`: 页面的主入口组件,通常是列表展示页面 - `components/`: 存放该页面专用的私有组件 - 弹窗/抽屉组件:根据表单字段数量选择使用弹窗或抽屉 - 字段较少(3-5个):使用 `*Dialog.vue` 弹窗组件 - 字段较多(5个以上):使用 `*Drawer.vue` 抽屉组件 - `SaveDialog.vue` / `SaveDrawer.vue`: 用于新增和编辑数据的表单组件 - `DetailDialog.vue`: 用于查看数据详情的弹窗 - `SearchDrawer.vue`: 用于高级搜索条件的抽屉组件 - 其他与该页面紧密关联的组件 **组件命名规范**: - 页面私有组件使用 PascalCase 命名 - 建议以功能命名: - 表单类:`SaveDialog.vue`, `SaveDrawer.vue`, `EditDialog.vue` 等 - 功能类:`RoleDialog.vue`, `PermissionDialog.vue` 等 - 搜索类:`SearchDrawer.vue` - 避免 `Dialog.vue` 或 `Drawer.vue` 这样过于通用的命名 #### 页面布局规范 **基本布局要求**: 1. **使用 Flex 布局**: 页面容器必须使用 Flex 布局,确保占满全部内容区域 2. **表格高度自适应**: 表格使用封装的 `scTable` 组件,高度占满剩余内容空间 **标准页面布局模板**: ```vue ``` **布局样式说明**: - 页面根元素使用 `display: flex` 和 `flex-direction: column` 实现垂直布局 - `height: 100%` 确保页面占满父容器高度 - `padding: 0` 去除内边距(由布局组件控制外边距) - `table-content` 使用 `flex: 1` 占据剩余空间 - `overflow: hidden` 防止出现滚动条(表格内部自己处理滚动) **侧边栏布局示例** (如用户管理页面): ```vue ``` #### 列表搜索区域规范 **搜索区域设计原则**: 1. **精简显示**: 搜索条件不全部显示,只显示最常用的 1-2 项 2. **去除 label 属性**: 搜索输入框不使用 label 属性,通过 placeholder 直接说明用途,保持界面简洁 3. **优先使用表格筛选**: 对于状态等枚举类型的筛选,优先使用 Ant Design Vue Table 的自定义过滤器功能 4. **高级搜索抽屉**: 其他复杂搜索条件使用抽屉弹出(需要在页面 components 目录下创建 SearchDrawer.vue 组件) 5. **保持界面整洁**: 避免搜索区域占用过多空间 **实现方式**: **方式一: 使用表格自定义筛选(推荐)** ```vue ``` **方式二: 使用高级搜索抽屉(复杂搜索条件)** ```vue ``` **SearchDrawer.vue 组件示例**: ```vue ``` **搜索条件选择建议**: 常用搜索条件(显示在工具栏): - 主要关键字搜索(如用户名、角色名称) - 无需筛选的简单字段 表格筛选(推荐用于): - 状态、类型等枚举值字段 - 不需要组合条件的单一筛选 高级搜索抽屉(用于): - 次要字段(如邮箱、手机号) - 日期范围筛选 - 多条件组合筛选 - 复杂的搜索逻辑 **scTable 组件筛选器优化说明**: scTable 组件已支持 Ant Design Vue Table 的筛选功能,使用方式: 1. 在 `columns` 配置中添加 `filters` 属性 2. 通过 `@filterChange` 事件处理筛选变化 3. 将筛选条件合并到搜索表单中提交 ```javascript // 在 useTable Hook 中处理筛选变化 const handleFilterChange = (pagination, filters) => { // 将筛选条件添加到搜索表单 if (filters.status) { searchForm.status = filters.status[0] // filterMultiple: false 时取第一个值 } handleSearch() } ``` ### 3. 组件开发规范 #### 组件命名 - **单文件组件**: 使用 PascalCase 命名,如 `UserList.vue` - **公共组件**: 以 `sc` 开头,如 `scTable.vue` - **页面私有组件**: 使用语义化命名,如 `SaveDialog.vue`, `RoleDialog.vue` #### 组件结构 ```vue ``` ### 4. API 接口开发规范 #### API 文件组织 每个业务模块对应一个 API 文件,统一放在 `src/api/` 目录下。 ```javascript // src/api/auth.js import request from '@/utils/request' export default { // 认证相关 login: { post: async function (params) { return await request.post('auth/login', params) }, }, logout: { post: async function () { return await request.post('auth/logout') }, }, me: { get: async function () { return await request.get('auth/me') }, }, // 权限和菜单 permissions: { menu: { get: async function () { return await request.get('permissions/menu') }, }, tree: { get: async function () { return await request.get('permissions/tree') }, }, }, } ``` #### 使用示例 ```javascript import authApi from '@/api/auth' const login = async () => { try { const res = await authApi.login.post({ username: 'admin', password: '123456' }) // 处理响应 } catch (error) { // 处理错误 } } // 获取用户菜单 const getMenu = async () => { const res = await authApi.permissions.menu.get() return res.data } ``` ### 5. 路由开发规范 #### 路由类型 项目包含两类路由: 1. **静态路由**: 定义在 `src/router/systemRoutes.js` 中的基础路由,如登录页、404 页面等 2. **动态路由**: 用户登录后,通过 API 获取菜单数据,动态添加到路由中 #### 静态路由定义 ```javascript // src/router/systemRoutes.js export default [ { path: '/login', name: 'Login', component: () => import('@/pages/login/index.vue'), meta: { title: '登录', hidden: true } }, { path: '/', name: 'Layout', component: () => import('@/layouts/index.vue'), redirect: '/dashboard', children: [ // 动态路由将被添加到这里 ] } ] ``` #### 动态路由加载 用户登录后,系统会自动执行以下流程: 1. 调用后端 API 获取用户菜单和权限信息 2. 将菜单数据转换为路由格式 3. 将动态路由添加到路由器中 4. 生成菜单树用于侧边栏展示 **路由元信息 (meta)**: - `title`: 页面标题 - `icon`: 菜单图标(使用 Ant Design Vue 图标名称) - `hidden`: 是否隐藏菜单 - `noAuth`: 是否不需要认证 - `keepAlive`: 是否缓存页面 - `affix`: 是否固定标签页 **后端菜单数据格式**: 菜单权限节点数据结构如下: | 字段 | 类型 | 说明 | |------|------|------| | id | number | 权限ID | | title | string | 权限标题(用于菜单显示) | | name | string | 权限编码(唯一标识) | | type | string | 权限类型:menu(菜单)、api(接口)、button(按钮)、url(链接) | | parent_id | number | 父级ID,顶级菜单为 0 | | path | string | 路由路径(如 `/system/users`) | | component | string | 前端组件路径(如 `system/users/index`) | | meta | object | 元数据(包含 icon、hidden、keepAlive 等) | | sort | number | 排序 | | status | number | 状态:1启用 0禁用 | | children | array | 子权限列表(树形结构) | **菜单节点规范**: 1. **顶级菜单**(parent_id=0)不需要 component 值 - 顶级菜单作为分组容器,不需要指定组件路径 - 示例:系统管理菜单 2. **只有最后一级的菜单**才需要 component 值 - 如果菜单没有子菜单,则需要指定 component - 如果菜单有子菜单,则不需要指定 component 3. **非菜单类型**(如 button)不需要 component 值 - 按钮权限只用于权限控制,不涉及页面路由 4. **所有页面组件的菜单**在前端处理时都拉平挂载在 Layouts 框架组件下 - 前端会将菜单数据转换为路由,所有页面路由都会挂载到 Layout 布局下 - 不需要在前端维护嵌套的路由结构 **数据示例**: ```javascript // 示例:顶级菜单(无 component) { id: 1, title: '系统管理', name: 'system', type: 'menu', parent_id: 0, path: '/system', component: null, // 顶级菜单不需要 component meta: { icon: 'Setting', hidden: false, hiddenBreadcrumb: false, keepAlive: false }, sort: 1, status: 1, children: [] } // 示例:最后一级菜单(有 component) { id: 2, title: '用户管理', name: 'system.users', type: 'menu', parent_id: 0, path: '/system/users', component: 'system/users/index', // 最后一级菜单需要 component meta: { icon: 'User', hidden: false, hiddenBreadcrumb: false, keepAlive: true }, sort: 1, status: 1, children: [] } // 示例:按钮权限(无 component) { id: 3, title: '查看用户', name: 'system.users.view', type: 'button', parent_id: 2, path: null, component: null, // 非菜单类型不需要 component meta: null, sort: 1, status: 1, children: [] } // 示例:API 权限 { id: 4, title: '获取用户列表', name: 'system.users.list', type: 'api', parent_id: 0, path: 'admin.users.index', component: null, meta: null, sort: 1, status: 1, children: [] } ``` **路由转换逻辑**: 前端会将后端返回的菜单数据转换为 Vue Router 路由格式,所有页面路由都会拉平挂载在 Layout 布局组件下: ```javascript // 将后端菜单转换为路由格式 function transformMenusToRoutes(menus) { return menus.map(menu => { const route = { path: menu.path, name: menu.name, meta: { title: menu.title, icon: menu.meta?.icon, hidden: menu.meta?.hidden || false, keepAlive: menu.meta?.keepAlive || false } } // 只有菜单类型且有 component 值的才加载组件 if (menu.type === 'menu' && menu.component) { route.component = loadComponent(menu.component) } // 不处理 children,所有菜单都会拉平到 Layout 下 // if (menu.children) { // route.children = transformMenusToRoutes(menu.children) // } return route }) } // 加载组件函数 function loadComponent(componentPath) { return () => import(`@/pages/${componentPath}.vue`) } ``` **菜单拉平处理说明**: 由于前端将所有页面菜单拉平挂载在 Layout 下,因此: - 后端菜单的 `parent_id` 主要用于构建菜单树的层级关系(侧边栏显示) - 路由层面不需要维护嵌套结构,所有页面路由都在同一层级 - `component` 值只需要在最后一级菜单(叶子节点)中设置 - 路由 `path` 使用后端返回的 `path` 字段 - 路由 `name` 使用后端返回的 `name` 字段(权限编码) - 路由 `meta.title` 使用后端返回的 `title` 字段(权限标题) #### 路由守卫 系统通过路由守卫实现权限控制和动态路由加载: ```javascript router.beforeEach(async (to, from, next) => { const userStore = useUserStore() const isLoggedIn = userStore.isLoggedIn() // 白名单直接放行 if (whiteList.includes(to.path)) { return next() } // 未登录跳转登录页 if (!isLoggedIn) { return next({ path: '/login', query: { redirect: to.fullPath } }) } // 动态路由加载 if (!isDynamicRouteLoaded) { const menus = userStore.getMenu() const dynamicRoutes = transformMenusToRoutes(menus) // 添加动态路由 dynamicRoutes.forEach(route => { router.addRoute('Layout', route) }) isDynamicRouteLoaded = true next({ ...to, replace: true }) } else { next() } }) ``` ### 6. 状态管理规范 #### Pinia Store 定义 使用组合式 API 定义 Store。 #### User Store(用户认证与权限) `src/stores/modules/user.js` 负责管理用户认证信息和权限数据: ```javascript // src/stores/modules/user.js import { ref } from 'vue' import { defineStore } from 'pinia' import { resetRouter } from '../../router' export const useUserStore = defineStore('user', () => { // State const token = ref('') // 访问令牌 const refreshToken = ref('') // 刷新令牌 const userInfo = ref(null) // 用户信息 const menu = ref([]) // 用户菜单 const permissions = ref([]) // 用户权限节点 // Getters const isLoggedIn = () => !!token.value // Actions function setToken(newToken) { token.value = newToken } function setUserInfo(info) { userInfo.value = info } // 设置菜单(合并静态菜单和后端菜单) function setMenu(newMenu) { const staticMenus = userRoutes || [] let mergedMenus = [...staticMenus] if (newMenu && newMenu.length > 0) { const menuMap = new Map() // 添加静态菜单 staticMenus.forEach(m => { if (m.path) menuMap.set(m.path, m) }) // 添加后端菜单(覆盖重复路径) newMenu.forEach(m => { if (m.path) menuMap.set(m.path, m) }) mergedMenus = Array.from(menuMap.values()) } menu.value = mergedMenus } function getMenu() { return menu.value } function setPermissions(data) { permissions.value = data } function hasPermission(permission) { if (!permissions.value || permissions.value.length === 0) { return false } return permissions.value.includes(permission) } function logout() { token.value = '' refreshToken.value = '' userInfo.value = null menu.value = [] resetRouter() } return { token, refreshToken, userInfo, menu, permissions, setToken, setUserInfo, setMenu, getMenu, setPermissions, hasPermission, logout, isLoggedIn } }) ``` #### Store 使用 ```javascript import { useUserStore } from '@/stores/modules/user' const userStore = useUserStore() // 使用 state console.log(userStore.token) console.log(userStore.menu) console.log(userStore.permissions) // 调用 action userStore.setToken('xxx') userStore.setMenu(menus) userStore.setPermissions(permissions) // 检查权限 if (userStore.hasPermission('user.create')) { // 有权限,执行操作 } // 登出 userStore.logout() ``` #### Store 持久化 使用 `pinia-plugin-persistedstate` 实现数据持久化: ```javascript { persist: { key: 'user-store', storage: customStorage, pick: ['token', 'refreshToken', 'userInfo', 'menu'] } } ``` #### 权限指令 项目提供权限指令,用于在模板中控制元素显示: ```vue ``` ### 7. 表格开发规范 #### 使用 useTable Hook 项目提供了 `useTable` Hook 简化表格开发: ```javascript import { useTable } from '@/hooks/useTable' import { ref } from 'vue' const { loading, dataSource, pagination, handleSearch, handleReset, handlePageChange } = useTable({ api: userApi.getList, // API 方法 immediate: true // 是否立即加载 }) // 搜索参数 const searchParams = ref({ keyword: '', status: '' }) ``` #### scTable 组件使用 ```vue ``` ### 8. 表单开发规范 #### scForm 组件使用 ```vue ``` ### 9. 图标使用规范 #### Ant Design Vue Icons ```vue ``` #### 常用图标 - `user`: 用户 - `setting`: 设置 - `delete`: 删除 - `edit`: 编辑 - `plus`: 添加 - `search`: 搜索 - `reload`: 刷新 - `download`: 下载 - `upload`: 上传 - `eye`: 查看 - `eye-invisible`: 隐藏 - `check-circle`: 成功 - `close-circle`: 失败 - `info-circle`: 信息 - `warning`: 警告 ### 10. 国际化 (i18n) 规范 #### 使用 i18n ```javascript import { useI18n } from '@/hooks/useI18n' const { t } = useI18n() // 使用 console.log(t('common.save')) console.log(t('user.deleteConfirm')) ``` #### 语言文件组织 ```javascript // src/i18n/locales/zh.js export default { common: { save: '保存', cancel: '取消', confirm: '确认', delete: '删除' }, user: { deleteConfirm: '确定要删除该用户吗?' } } ``` ### 11. 文件上传规范 #### scUpload 组件使用 ```vue ``` ### 12. 富文本编辑器规范 #### scEditor 组件使用 ```vue ``` ### 13. 样式规范 #### 全局样式 在 `src/style.css` 中定义全局样式。 #### 页面公共样式 页面布局的公共样式已提取到全局样式文件中,各页面可直接使用: **在 `src/assets/style/pages.scss` 中定义的公共类:** ```scss // 标准页面布局(垂直布局) .pages-base-layout { display: flex; flex-direction: column; height: 100%; padding: 0; .tool-bar { padding: 12px 16px; background: #fff; border-bottom: 1px solid #f0f0f0; .left-panel { display: flex; align-items: center; flex: 1; } .right-panel { display: flex; gap: 8px; } } .table-content { flex: 1; overflow: hidden; padding: 16px; background: #f5f5f5; } } // 侧边栏布局(左右分栏) .pages-sidebar-layout { display: flex; flex-direction: row; height: 100%; padding: 0; .left-box { width: 260px; border-right: 1px solid #f0f0f0; display: flex; flex-direction: column; background: #fff; .header { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; background: #fafafa; } .body { flex: 1; overflow-y: auto; padding: 16px; } } .right-box { flex: 1; display: flex; flex-direction: column; overflow: hidden; .tool-bar { padding: 12px 16px; background: #fff; border-bottom: 1px solid #f0f0f0; .left-panel { display: flex; align-items: center; flex: 1; } .right-panel { display: flex; gap: 8px; } } .table-content { flex: 1; overflow: hidden; padding: 16px; background: #f5f5f5; } } } ``` **使用示例:** ```vue ``` #### 组件样式 使用 `scoped` 避免样式污染: ```vue ``` #### 命名规范 - 使用 BEM 命名法 - 类名使用 kebab-case ### 14. 工具函数使用 #### request.js (HTTP 请求) ```javascript import request from '@/utils/request' // GET 请求 request.get('/api/users', { params: { page: 1 } }) // POST 请求 request.post('/api/users', { name: 'test' }) // PUT 请求 request.put('/api/users/1', { name: 'updated' }) // DELETE 请求 request.delete('/api/users/1') ``` #### tool.js (工具函数) ```javascript import { formatDate, deepClone } from '@/utils/tool' // 格式化日期 formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss') // 深拷贝 deepClone(originalObject) ``` ### 15. 开发流程 #### 登录流程 1. 用户输入用户名和密码 2. 调用 `authApi.login.post()` 发送登录请求 3. 后端返回 `token`、`refreshToken`、`userInfo`、`menu`、`permissions` 4. 前端保存数据到 Store(持久化) 5. 路由守卫检测到登录状态,加载动态路由 6. 跳转到首页或重定向页 ```javascript import { useUserStore } from '@/stores/modules/user' import authApi from '@/api/auth' const userStore = useUserStore() const handleLogin = async () => { try { const res = await authApi.login.post({ username: 'admin', password: '123456' }) // 保存 token userStore.setToken(res.data.token) userStore.setRefreshToken(res.data.refreshToken) // 保存用户信息 userStore.setUserInfo(res.data.user) // 保存菜单(合并静态菜单) userStore.setMenu(res.data.menu) // 保存权限节点 userStore.setPermissions(res.data.permissions) // 跳转首页 router.push('/dashboard') } catch (error) { message.error('登录失败') } } ``` #### 添加新页面 1. 在 `src/pages/` 对应模块下创建页面组件 2. 在 `src/api/` 中创建对应的 API 文件 3. 在后端添加对应的菜单和权限配置 4. 前端会自动加载动态路由(无需手动配置路由) #### 添加新组件 1. 在 `src/components/` 或对应子目录下创建组件 2. 遵循组件结构规范 3. 添加必要的 Props 和 Emits 4. 编写组件文档 ### 16. 常用命令 ```bash # 安装依赖 npm install # 开发环境启动 npm run dev # 生产环境构建 npm run build # 预览生产构建 npm run preview # 代码格式化 npm run format # 代码检查 npm run lint ``` ### 17. 注意事项 1. **禁止重复引入图标**: Ant Design Vue 图标已全局引入,直接使用即可 2. **使用组合式 API**: 新代码统一使用 `