Files
laravel_swoole/.clinerules/admin-rule.md
2026-02-11 15:49:19 +08:00

1523 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<template>
<div class="pages {module}-{resource}-page">
<!-- 工具栏区域 -->
<div class="tool-bar">
<div class="left-panel">
<!-- 搜索表单 -->
</div>
<div class="right-panel">
<!-- 操作按钮 -->
</div>
</div>
<!-- 表格内容区域 -->
<div class="table-content">
<scTable
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
:row-key="rowKey"
@refresh="refreshTable"
@paginationChange="handlePaginationChange"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.{module}-{resource}-page {
display: flex;
flex-direction: column;
height: 100%;
padding: 0;
.tool-bar {
// 工具栏样式
}
.table-content {
flex: 1;
overflow: hidden;
}
}
</style>
```
**布局样式说明**:
- 页面根元素使用 `display: flex``flex-direction: column` 实现垂直布局
- `height: 100%` 确保页面占满父容器高度
- `padding: 0` 去除内边距(由布局组件控制外边距)
- `table-content` 使用 `flex: 1` 占据剩余空间
- `overflow: hidden` 防止出现滚动条(表格内部自己处理滚动)
**侧边栏布局示例** (如用户管理页面):
```vue
<template>
<div class="pages user-page">
<!-- 左侧部门树 -->
<div class="left-box">
<div class="header">
<!-- 搜索框 -->
</div>
<div class="body">
<!-- 树形组件 -->
</div>
</div>
<!-- 右侧内容区 -->
<div class="right-box">
<!-- 工具栏 -->
<div class="tool-bar">...</div>
<!-- 表格 -->
<div class="table-content">...</div>
</div>
</div>
</template>
<style scoped lang="scss">
.user-page {
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;
.table-content {
flex: 1;
overflow: hidden;
background: #f5f5f5;
}
}
}
</style>
```
#### 列表搜索区域规范
**搜索区域设计原则**:
1. **精简显示**: 搜索条件不全部显示,只显示最常用的 1-2 项
2. **去除 label 属性**: 搜索输入框不使用 label 属性,通过 placeholder 直接说明用途,保持界面简洁
3. **优先使用表格筛选**: 对于状态等枚举类型的筛选,优先使用 Ant Design Vue Table 的自定义过滤器功能
4. **高级搜索抽屉**: 其他复杂搜索条件使用抽屉弹出(需要在页面 components 目录下创建 SearchDrawer.vue 组件)
5. **保持界面整洁**: 避免搜索区域占用过多空间
**实现方式**:
**方式一: 使用表格自定义筛选(推荐)**
```vue
<template>
<div class="pages-base-layout role-page">
<div class="tool-bar">
<div class="left-panel">
<a-space>
<a-input v-model:value="searchForm.keyword" placeholder="角色名称" allow-clear style="width: 180px" />
<a-button type="primary" @click="handleSearch">
<template #icon><search-outlined /></template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon><redo-outlined /></template>
重置
</a-button>
</a-space>
</div>
<div class="right-panel">
<a-button type="primary" @click="handleAdd">
<template #icon><plus-outlined /></template>
新增
</a-button>
</div>
</div>
<div class="table-content">
<scTable
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
@refresh="refreshTable"
@paginationChange="handlePaginationChange"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useTable } from '@/hooks/useTable'
// 使用 useTable Hook
const {
tableRef,
searchForm,
tableData,
loading,
pagination,
handleSearch,
handleReset,
handlePaginationChange,
refreshTable
} = useTable({
api: api.roles.list.get,
searchForm: {
keyword: '',
status: null
},
columns: [],
needPagination: true
})
// 表格列配置(包含筛选器)
const columns = [
{
title: '角色名称',
dataIndex: 'name',
key: 'name',
width: 200
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'center',
filters: [
{ text: '正常', value: 1 },
{ text: '禁用', value: 0 }
],
filterMultiple: false,
slot: 'status'
},
// ...其他列
]
</script>
```
**方式二: 使用高级搜索抽屉(复杂搜索条件)**
```vue
<template>
<div class="pages-base-layout user-page">
<div class="tool-bar">
<div class="left-panel">
<a-space>
<a-input v-model:value="searchForm.username" placeholder="用户名" allow-clear style="width: 140px" />
<a-button type="primary" @click="handleSearch">搜索</a-button>
<a-button @click="handleReset">重置</a-button>
<a-button @click="showAdvancedSearch = true">高级搜索</a-button>
</a-space>
</div>
</div>
<div class="table-content">
<scTable
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
@refresh="refreshTable"
@paginationChange="handlePaginationChange"
/>
</div>
</div>
<!-- 高级搜索抽屉 -->
<SearchDrawer
v-model:visible="showAdvancedSearch"
:form-data="searchForm"
@confirm="handleAdvancedSearch"
/>
</template>
<script setup>
import { ref } from 'vue'
import { useTable } from '@/hooks/useTable'
import SearchDrawer from './components/SearchDrawer.vue'
const showAdvancedSearch = ref(false)
const handleAdvancedSearch = (formData) => {
// 更新搜索表单数据
Object.assign(searchForm, formData)
showAdvancedSearch.value = false
handleSearch()
}
</script>
```
**SearchDrawer.vue 组件示例**:
```vue
<template>
<a-drawer v-model:open="visible" title="高级搜索" placement="right" width="400">
<a-form :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="姓名">
<a-input v-model:value="formData.real_name" placeholder="请输入姓名" allow-clear />
</a-form-item>
<a-form-item label="邮箱">
<a-input v-model:value="formData.email" placeholder="请输入邮箱" allow-clear />
</a-form-item>
<a-form-item label="手机号">
<a-input v-model:value="formData.phone" placeholder="请输入手机号" allow-clear />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="formData.status" placeholder="请选择状态" allow-clear>
<a-select-option :value="1">正常</a-select-option>
<a-select-option :value="0">禁用</a-select-option>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleConfirm">确定</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script setup>
defineProps({
visible: Boolean,
formData: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:visible', 'confirm'])
const handleCancel = () => {
emit('update:visible', false)
}
const handleConfirm = () => {
emit('confirm', { ...formData })
}
</script>
```
**搜索条件选择建议**:
常用搜索条件(显示在工具栏):
- 主要关键字搜索(如用户名、角色名称)
- 无需筛选的简单字段
表格筛选(推荐用于):
- 状态、类型等枚举值字段
- 不需要组合条件的单一筛选
高级搜索抽屉(用于):
- 次要字段(如邮箱、手机号)
- 日期范围筛选
- 多条件组合筛选
- 复杂的搜索逻辑
**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
<template>
<!-- 模板内容 -->
</template>
<script setup>
// 导入
import { ref, computed, onMounted } from 'vue'
// 响应式数据
const state = ref({})
// 计算属性
const computedValue = computed(() => {})
// 方法
const handleAction = () => {}
// 生命周期
onMounted(() => {})
</script>
<style scoped>
/* 样式 */
</style>
```
### 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
<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>
```
### 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
<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>
```
### 8. 表单开发规范
#### scForm 组件使用
```vue
<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>
```
### 9. 图标使用规范
#### Ant Design Vue Icons
```vue
<!-- 直接使用无需导入 -->
<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`: 警告
### 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
<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>
```
### 12. 富文本编辑器规范
#### scEditor 组件使用
```vue
<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>
```
### 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
<!-- 标准页面布局 -->
<style scoped lang="scss">
.user-page {
@extend .pages-base-layout;
}
</style>
<!-- 侧边栏布局 -->
<style scoped lang="scss">
.user-page {
@extend .pages-sidebar-layout;
}
</style>
```
#### 组件样式
使用 `scoped` 避免样式污染:
```vue
<style scoped>
.user-list {
padding: 20px;
}
.user-list .table {
margin-top: 20px;
}
</style>
```
#### 命名规范
- 使用 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**: 新代码统一使用 `<script setup>` 语法
3. **组件复用**: 优先使用项目提供的公共组件scTable、scForm 等)
4. **API 统一管理**: 所有接口统一在 `src/api/` 目录下管理
5. **路由懒加载**: 路由组件必须使用动态导入
6. **环境变量**: 通过 `import.meta.env` 访问环境变量
7. **不要编写 demo**: 开发过程中不编写示例代码
8. **测试提示**: 如需测试,提示用户是否运行测试,不主动运行
### 18. 代码质量
- 遵循 Vue 3 官方风格指南
- 保持代码简洁、可读
- 适当添加注释说明复杂逻辑
- 使用语义化的变量和函数命名
- 避免过多的嵌套层级
### 19. 性能优化
- 合理使用 `v-if``v-show`
- 列表渲染必须设置 `key`
- 大列表使用虚拟滚动
- 图片懒加载
- 路由懒加载
- 组件按需引入(除图标外)