# 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`
- 大列表使用虚拟滚动
- 图片懒加载
- 路由懒加载
- 组件按需引入（除图标外）
