20 KiB
20 KiB
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/ # 认证页面
│ │ ├── 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. 组件开发规范
组件命名
- 单文件组件: 使用 PascalCase 命名,如
UserList.vue - 公共组件: 以
sc开头,如scTable.vue - 页面组件: 使用语义化命名,如
UserManagement.vue
组件结构
<template>
<!-- 模板内容 -->
</template>
<script setup>
// 导入
import { ref, computed, onMounted } from 'vue'
// 响应式数据
const state = ref({})
// 计算属性
const computedValue = computed(() => {})
// 方法
const handleAction = () => {}
// 生命周期
onMounted(() => {})
</script>
<style scoped>
/* 样式 */
</style>
3. API 接口开发规范
API 文件组织
每个业务模块对应一个 API 文件,统一放在 src/api/ 目录下。
// 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')
},
},
},
}
使用示例
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
}
4. 路由开发规范
路由类型
项目包含两类路由:
- 静态路由: 定义在
src/router/systemRoutes.js中的基础路由,如登录页、404 页面等 - 动态路由: 用户登录后,通过 API 获取菜单数据,动态添加到路由中
静态路由定义
// 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: [
// 动态路由将被添加到这里
]
}
]
动态路由加载
用户登录后,系统会自动执行以下流程:
- 调用后端 API 获取用户菜单和权限信息
- 将菜单数据转换为路由格式
- 将动态路由添加到路由器中
- 生成菜单树用于侧边栏展示
路由元信息 (meta):
title: 页面标题icon: 菜单图标(使用 Ant Design Vue 图标名称)hidden: 是否隐藏菜单noAuth: 是否不需要认证keepAlive: 是否缓存页面affix: 是否固定标签页
后端菜单数据格式:
菜单权限节点需要遵循以下规范:
-
顶级菜单(parent_id=0)不需要 component 值
- 顶级菜单作为分组容器,不需要指定组件路径
- 示例:系统管理菜单
-
只有最后一级的菜单才需要 component 值
- 如果菜单没有子菜单,则需要指定 component
- 如果菜单有子菜单,则不需要指定 component
-
非菜单类型(如 button)不需要 component 值
- 按钮权限只用于权限控制,不涉及页面路由
-
所有页面组件的菜单在前端处理时都拉平挂载在 Layouts 框架组件下
- 前端会将菜单数据转换为路由,所有页面路由都会挂载到 Layout 布局下
- 不需要在前端维护嵌套的路由结构
// 示例:顶级菜单(无 component)
{
name: '系统管理',
code: 'system',
type: 'menu',
parent_id: 0,
route: '/system',
component: null, // 顶级菜单不需要 component
meta: {
icon: 'Setting',
hidden: false
},
sort: 1
}
// 示例:最后一级菜单(有 component)
{
name: '用户管理',
code: 'system.users',
type: 'menu',
parent_id: 0,
route: '/system/users',
component: 'system/users/index', // 最后一级菜单需要 component
meta: {
icon: 'User',
hidden: false
},
sort: 1
}
// 示例:按钮权限(无 component)
{
name: '查看用户',
code: 'system.users.view',
type: 'button',
parent_id: 0,
route: 'admin.users.index',
component: null, // 非菜单类型不需要 component
meta: null,
sort: 1
}
路由转换逻辑:
前端会将后端返回的菜单数据转换为 Vue Router 路由格式,所有页面路由都会拉平挂载在 Layout 布局组件下:
// 将后端菜单转换为路由格式
function transformMenusToRoutes(menus) {
return menus.map(menu => {
const route = {
path: menu.route,
name: menu.name || menu.code,
meta: {
title: menu.name,
icon: menu.meta?.icon,
hidden: menu.meta?.hidden,
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 值只需要在最后一级菜单(叶子节点)中设置
路由守卫
系统通过路由守卫实现权限控制和动态路由加载:
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()
}
})
5. 状态管理规范
Pinia Store 定义
使用组合式 API 定义 Store。
User Store(用户认证与权限)
src/stores/modules/user.js 负责管理用户认证信息和权限数据:
// 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 使用
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 实现数据持久化:
{
persist: {
key: 'user-store',
storage: customStorage,
pick: ['token', 'refreshToken', 'userInfo', 'menu']
}
}
权限指令
项目提供权限指令,用于在模板中控制元素显示:
<template>
<!-- 只有拥有 user.create 权限时显示 -->
<a-button v-permission="'user.create'">新增</a-button>
<!-- 拥有多个权限之一时显示 -->
<a-button v-permission="['user.create', 'user.update']">编辑</a-button>
</template>
<script setup>
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
</script>
6. 表格开发规范
使用 useTable Hook
项目提供了 useTable Hook 简化表格开发:
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 组件使用
<template>
<sc-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
>
<template #action="{ record }">
<a-button @click="handleEdit(record)">编辑</a-button>
</template>
</sc-table>
</template>
7. 表单开发规范
scForm 组件使用
<template>
<sc-form
ref="formRef"
:model="formData"
:rules="formRules"
:items="formItems"
@submit="handleSubmit"
/>
</template>
<script setup>
import { ref } from 'vue'
const formRef = ref(null)
const formData = ref({
username: '',
email: ''
})
const formRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }
]
}
const formItems = [
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名'
},
{
type: 'input',
prop: 'email',
label: '邮箱',
placeholder: '请输入邮箱'
}
]
const handleSubmit = async () => {
await formRef.value.validate()
// 提交逻辑
}
</script>
8. 图标使用规范
Ant Design Vue Icons
<!-- 直接使用,无需导入 -->
<template>
<a-icon type="user" />
<a-icon type="setting" />
</template>
常用图标
user: 用户setting: 设置delete: 删除edit: 编辑plus: 添加search: 搜索reload: 刷新download: 下载upload: 上传eye: 查看eye-invisible: 隐藏check-circle: 成功close-circle: 失败info-circle: 信息warning: 警告
9. 国际化 (i18n) 规范
使用 i18n
import { useI18n } from '@/hooks/useI18n'
const { t } = useI18n()
// 使用
console.log(t('common.save'))
console.log(t('user.deleteConfirm'))
语言文件组织
// src/i18n/locales/zh.js
export default {
common: {
save: '保存',
cancel: '取消',
confirm: '确认',
delete: '删除'
},
user: {
deleteConfirm: '确定要删除该用户吗?'
}
}
10. 文件上传规范
scUpload 组件使用
<template>
<sc-upload
v-model="imageUrl"
:limit="1"
accept="image/*"
list-type="picture-card"
/>
</template>
<script setup>
import { ref } from 'vue'
const imageUrl = ref('')
</script>
11. 富文本编辑器规范
scEditor 组件使用
<template>
<sc-editor
v-model="content"
:height="400"
:toolbar="toolbarConfig"
/>
</template>
<script setup>
import { ref } from 'vue'
const content = ref('')
const toolbarConfig = [
'bold', 'italic', 'underline', 'strike',
'list', 'orderedList', 'quote', 'codeBlock',
'image', 'link'
]
</script>
12. 样式规范
全局样式
在 src/style.css 中定义全局样式。
组件样式
使用 scoped 避免样式污染:
<style scoped>
.user-list {
padding: 20px;
}
.user-list .table {
margin-top: 20px;
}
</style>
命名规范
- 使用 BEM 命名法
- 类名使用 kebab-case
13. 工具函数使用
request.js (HTTP 请求)
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 (工具函数)
import { formatDate, deepClone } from '@/utils/tool'
// 格式化日期
formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
// 深拷贝
deepClone(originalObject)
14. 开发流程
登录流程
- 用户输入用户名和密码
- 调用
authApi.login.post()发送登录请求 - 后端返回
token、refreshToken、userInfo、menu、permissions - 前端保存数据到 Store(持久化)
- 路由守卫检测到登录状态,加载动态路由
- 跳转到首页或重定向页
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('登录失败')
}
}
添加新页面
- 在
src/pages/对应模块下创建页面组件 - 在
src/api/中创建对应的 API 文件 - 在后端添加对应的菜单和权限配置
- 前端会自动加载动态路由(无需手动配置路由)
添加新组件
- 在
src/components/或对应子目录下创建组件 - 遵循组件结构规范
- 添加必要的 Props 和 Emits
- 编写组件文档
15. 常用命令
# 安装依赖
npm install
# 开发环境启动
npm run dev
# 生产环境构建
npm run build
# 预览生产构建
npm run preview
# 代码格式化
npm run format
# 代码检查
npm run lint
16. 注意事项
- 禁止重复引入图标: Ant Design Vue 图标已全局引入,直接使用即可
- 使用组合式 API: 新代码统一使用
<script setup>语法 - 组件复用: 优先使用项目提供的公共组件(scTable、scForm 等)
- API 统一管理: 所有接口统一在
src/api/目录下管理 - 路由懒加载: 路由组件必须使用动态导入
- 环境变量: 通过
import.meta.env访问环境变量 - 不要编写 demo: 开发过程中不编写示例代码
- 测试提示: 如需测试,提示用户是否运行测试,不主动运行
17. 代码质量
- 遵循 Vue 3 官方风格指南
- 保持代码简洁、可读
- 适当添加注释说明复杂逻辑
- 使用语义化的变量和函数命名
- 避免过多的嵌套层级
18. 性能优化
- 合理使用
v-if和v-show - 列表渲染必须设置
key - 大列表使用虚拟滚动
- 图片懒加载
- 路由懒加载
- 组件按需引入(除图标外)