feat(frontend): 管理端改用 Gi Admin Pro 框架 + 腾讯云主题

前端此前只有本地实现、从未入库,本次首次纳入版本管理。

框架切换
  · 以 Gi Admin Pro(Vue3 + Vite + TS + Arco Design Vue + Pinia)为底
  · 剥离 14 个演示页面目录 / 121 个文件(demo 28、file 20、form 19、system 18…)
  · 依赖 dependencies 35 → 19,devDependencies 31 → 27
  · 保留框架核心:layout、Gi* 组件库、hooks、权限指令、错误页、登录页

主题:腾讯云 / TDesign 官方色板
  · 主色 #0052D9(腾讯蓝)、成功 #00A870、警告 #ED7B2F、危险 #E34D59
  · 页面底色 #f3f3f3、文字 rgba(0,0,0,90/60/40/26%)
  · 覆盖 Arco 底层色板 + 显式语义别名(primary/success/warning/danger/link/neutral)
  · 三个改了不生效也不报错的坑已记入注释:
      1) Arco 色值是逗号分隔 RGB 三元组,不是 #RRGGBB
      2) Arco 把色板定义在 body 而非 :root,必须写 `:root, body`
      3) setting.json 的 themeColor 会以内联样式写进 body,优先级高于样式表

对接后端(三处契约差异在 @/utils/http 统一适配)
  · 响应信封 {code,msg,data} → {code,success,message,data}
  · Token 头 token → Authorization: Bearer
  · 分页 {list,total} → 补一份 records,list 原样保留
  · 40101 → 登录页;登录密码 RSA 加密(公钥走 /auth/public-key,轮换无需重建前端)
  · 路由:后端只下发菜单键,前端在本地路由表上过滤(有哪些页面归代码,谁能看归后端)

已实现页面(浏览器实测通过)
  · 登录:RSA 加密 + 自适应验证码/租户码 + 账号锁定提示
  · 工作台:真实数据(组织 6 个 / 角色 1 个 / 权限 25 项)
  · 组织架构:三级树 + CRUD + 占用校验
  · 租户管理:开通租户 + 初始密码只显示一次的强确认
  · 待建页:未实现菜单显示所属里程碑
  · 权限菜单:学校管理员看不到「租户管理」

视觉资产按腾讯云主题重做
  · logo.svg(渐变圆角方 + 填)、favicon.svg(小尺寸做减法)、login-hero.svg(材料归集)
  · 登录页点阵纹理走主题变量,换主色自动跟随
  · 文案改为产品实际能力:自动改名 / 强制压缩 / 隐私保护

清理模板遗留(约 17MB + 三处外部依赖)
  · 百度统计脚本(上报到 gi-demo 作者的统计账号)
  · 用户菜单「项目地址」(跳转模板作者的 Gitee 仓库)
  · 两份 8.8MB mp3、388KB favicon.ico、57KB 模板插画、未引用字体与 loading.gif
  · 「个人中心」指向已删除的 /system/account(点了 404)
  · 头部头像在无 avatarUrl 时渲染成裂图 → 改为姓名首字兜底

后端修复
  · 新增 GET /auth/public-key(前端加密登录密码用)
  · DataSeeder.ancestor_path 生成错误:子节点本该是 /16/17/ 却写成 /17/
    该字段是「按院系统计」的索引,错了会漏人/多人且不报错 —— 改为从库里读父节点路径
  · seedDemoOrgs 曾放在租户作用域外 + admin 早退分支之后,导致启动失败 / 永不执行
  · 示范租户补一套三级组织树(1 学校 / 2 院系 / 3 班级),供界面演示
This commit is contained in:
2026-09-28 20:49:55 +08:00
parent 9d7a8ca845
commit 87ece5a971
237 changed files with 25215 additions and 26 deletions
@@ -0,0 +1,36 @@
---
name: code-css
description: Vue SFC 样式块、BEM 命名、Stylelint 与 scoped 约定。编写或修改组件样式(.vue style、scss、less、css)时加载本技能。
---
# CSS 规范(Vue 3)
## SFC 样式块
- 位于 `<script>` 之后;默认 **scoped**;全局样式须在注释中说明原因。
- 预处理器按项目约定,同一组件内保持一致。
## 命名(BEM)
- 自定义 class:**kebab-case** + **BEM**(`block`、`block__element`、`block--modifier`)。
- 一个 SFC 通常一个 Block;元素用 `__`,修饰符用 `--`;**禁止** `block__a__b` 链式元素。
- 第三方 UI 类名、全局工具类不在 BEM 约束内。
```scss
.user-card {
&__avatar { width: 48px; }
&__badge--vip { color: var(--color-warning); }
}
```
## 编写原则
- 颜色、间距、字号优先复用项目 CSS 变量或设计令牌。
- 避免内联 `style`(动态样式除外)和滥用 `!important`。
- 遵循项目 Stylelint;改动后视情况运行 `lint:style`。
## 禁止
- ❌ 自定义 class 用 camelCase 或非语义命名
- ❌ BEM 元素链式嵌套(`block__a__b`)
- ❌ 大段内联 style 或过多 `:deep()` 堆叠
@@ -0,0 +1,38 @@
---
name: code-ts
description: Vue 3 + TypeScript 命名、导入顺序、目录组织与 Lint 约定。编写或修改 .ts/.js 代码、新增 composable 或工具函数时加载本技能。
---
# TypeScript 规范(Vue 3)
Vue SFC 见 [code-vue](../code-vue/SKILL.md),样式见 [code-css](../code-css/SKILL.md)。
## 命名
- 组件 / 文件名:**PascalCase**;变量 / 函数:**camelCase**(事件处理推荐 `handle` 前缀)。
- 常量:需要时 **UPPER_SNAKE_CASE**;Composables:`use` 前缀。
- Props camelCase;模板事件名与 emits 定义 **kebab-case**。
- 样式 class:**kebab-case** + BEM(见 [code-css](../code-css/SKILL.md));全局组件前缀遵循项目统一约定。
## TypeScript
- `import type` 与值导入分离;对象结构优先 `interface`,联合/工具类型用 `type`。
- Props 默认值:`withDefaults(defineProps<Type>(), { ... })`;避免 `any`。
- 公共类型集中管理;领域类型靠近业务模块。
## 导入顺序
1. 类型导入 → 2. 第三方 → 3. Vue/官方库 → 4. 项目内部(composables、组件、utils、样式)
不要重复 import 已自动注册的符号(类型除外)。
## 组织
- 遵循项目现有目录划分;单文件建议不超过 400 行,超出应拆分。
- 可复用 UI 放组件目录;跨页面逻辑抽 composables;纯函数抽 utils。
- 路径别名遵循项目配置,避免 `../../../` 穿越。
## Lint
- 遵循项目 ESLint、Stylelint、tsconfig,不擅自关闭规则。
- 必要的 `eslint-disable` / `@ts-expect-error` 须注释说明原因。
@@ -0,0 +1,67 @@
---
name: code-vue
description: Vue 3 Composition API、SFC 块顺序、defineEmits、useTemplateRef、响应式约定。编写或修改 .vue 文件时加载本技能。
---
# Vue 规范(Vue 3)
适用于 Composition API + `<script setup lang="ts">`。样式见 [code-css](../code-css/SKILL.md)。
## 基础
- 新组件一律 **Composition API** + `<script setup>`,禁止 Options API。
- 需要组件名时用 `defineOptions({ name: 'ComponentName' })`;路由页 `name` 与路由配置一致。
- 块顺序:`<template>` → `<script setup>` → `<style>`(遵循项目 ESLint block-order)。
## Script 内顺序
类型导入 → 第三方 → Vue API → 项目模块 → `defineOptions` → `defineModel` → `defineProps` + `withDefaults` → `defineEmits` → `defineSlots` → 常量/函数 → `ref`/`reactive` → `computed` → 方法 → 生命周期/`watch` → `defineExpose`
## 自动导入
已自动注册的 API / 组件不要重复 import;仅需类型时用 `import type`。以 `vite.config` 与现有代码为准。
## 模板
- 事件名、插槽名 **kebab-case**;组件 **PascalCase**。
- 受控表单优先 `v-model`;复杂逻辑抽到 `computed` 或方法,不要写在模板里。
- 子组件通过 props/emits 通信,避免随意访问父组件实例。
## defineEmits
使用**基于类型的函数调用签名**,禁止数组 / tuple 形式:
```typescript
// ✅
const emit = defineEmits<{
(e: 'submit'): void
(e: 'change', value: string): void
}>()
// ❌ defineEmits<{ submit: [] }>() 或 tuple 形式
```
## 模板 Ref
优先 **`useTemplateRef`**(Vue 3.5+);模板 `ref` 属性值与参数字符串一致。
```typescript
const formRef = useTemplateRef<FormInstance>('formRef')
// template: <form ref="formRef">
```
## 响应式
- 原始值 `ref`,对象 `reactive`,派生 `computed`。
- 解构 reactive 注意响应性丢失,必要时 `toRefs`。
- 深拷贝优先 `structuredClone` 或项目工具,禁止 `JSON.parse(JSON.stringify())`。
## defineExpose
仅暴露父组件确实需要调用的方法或属性(如弹窗 `open`/`close`/`reset`)。
## 禁止
- ❌ Options API、重复自动导入、defineEmits tuple 形式
- ❌ 无关 `ref()` 绑定模板 ref(优先 `useTemplateRef`)
- ❌ 模板中写复杂业务逻辑
@@ -0,0 +1,132 @@
---
name: git-commit
description: 执行 git 提交的标准流程。用户明确要求 commit、提交代码、生成提交信息时使用。涵盖变更检查、Conventional Commits 提交信息、安全约束与提交流程。
---
# Git 提交
> **前置条件**:仅当用户**明确要求**提交时才执行;未要求时不得主动 commit / push。
## 第一步:收集变更状态
同时执行(不要串行等待):
```bash
git status
git diff
```
必要时补充 `git diff --staged`;需了解分支与远程关系时执行 `git status -sb`。
## 第二步:分析变更
- 覆盖**已暂存 + 未暂存 + 未跟踪**中与本次任务相关的文件
- 判断变更类型,映射到 Conventional Commits 的 `type`
- 提交信息聚焦 **why**(1–2 句完整句子),准确反映目的
- **不要**提交 `.env`、密钥、token、credentials 等敏感文件;若用户坚持,须明确警告
- 无相关变更时不执行 commit
## 第三步:提交信息格式(Conventional Commits)
```
<type>(<scope>): <subject>
<body>
```
### type(必选)
| type | 用途 |
|------|------|
| `feat` | 新功能 |
| `fix` | 缺陷修复 |
| `docs` | 文档 |
| `style` | 格式(不影响逻辑,如空格、分号) |
| `refactor` | 重构(非新功能、非修 bug) |
| `perf` | 性能优化 |
| `test` | 测试 |
| `build` | 构建系统或外部依赖 |
| `ci` | CI 配置 |
| `chore` | 其他杂项(不改 src/test) |
| `revert` | 回滚提交 |
### subject(必选)
- 使用**祈使句**,简明描述做了什么
- 不加句号;建议 50 字符以内
- 示例:`新增用户登录表单`、`修复表格分页重置问题`
### scope(可选)
- 括号内标明影响模块,如 `feat(auth)`、`fix(table)`
- 跨模块或难以界定时可省略
### body(可选)
- 说明**为什么**改、改了什么影响
- 与 subject 空一行
### 示例
```
fix(theme): 切换明暗模式时重新生成主题色阶
generate 依赖 isDark 生成色阶,切换后需重新写入 --primary-* 变量,
否则暗色模式下主色显示异常。
```
```
feat(user): 新增用户列表批量删除
```
## 第四步:执行提交(顺序)
```bash
git add <相关文件>
git commit -m "$(cat <<'EOF'
feat(user): 新增用户列表批量删除
补充说明变更原因与影响(可选)
EOF
)"
git status
```
PowerShell 环境可用:
```powershell
git commit -m "feat(user): 新增用户列表批量删除"
git commit -m "fix(theme): 切换明暗模式时重新生成主题色阶" -m "generate 依赖 isDark 生成色阶,切换后需重新写入 CSS 变量。"
```
## 安全约束(必须遵守)
| 禁止 | 说明 |
|------|------|
| `git config` 修改 | 不更改任何 git 配置 |
| `push --force` / `reset --hard` | 除非用户明确要求 |
| `--no-verify` / `--no-gpg-sign` | 除非用户明确要求 |
| 对 main/master force push | 须警告用户 |
| 无用户请求时 push | 不主动推送远程 |
### `--amend` 仅当全部满足
1. 用户明确要求 amend,**或** pre-commit hook 自动改文件需纳入本次提交
2. HEAD 为当前会话创建的 commit,且**未 push**
3. hook 拒绝时:**修复后新建 commit**,不要 amend
## 提交后
- `git status` 确认成功
- hook 失败:修复问题后**重新 commit**(新提交,非 amend)
- 将 commit hash / 摘要反馈给用户
- 用户未要求时**不要 push**
## 检查清单
- [ ] 用户已明确要求提交
- [ ] 已查看 status + diff
- [ ] 提交信息符合 Conventional Commits
- [ ] 未包含敏感文件
- [ ] 提交后 status 正常
@@ -0,0 +1,153 @@
---
name: project-api
description: 在本模板项目中新增或修改 API 接口、类型定义与 vite-plugin-mock 模拟数据。涉及 getBaseApi、http、baseAPI、mock 目录、分页响应格式时使用。
---
# API 与 Mock
## 接口层结构
每个业务模块一个文件,例如 `src/apis/system/user.ts`:
```ts
import { getBaseApi } from '@/apis/base'
import http from '@/utils/http'
export interface ListItem {
id: string
name: string
status: Status
createTime: string
// ...
}
/** 标准 CRUD */
export const baseAPI = getBaseApi<ListItem>({ baseUrl: '/system/user' })
/** 非标准接口单独声明 */
export function getDictDataList(params: { code: string } & Pagination) {
return http.get<PageRes<DictDataItem[]>>('/system/dict/getDictDataList', params)
}
```
## getBaseApi 约定路径
| 方法 | HTTP | 路径 |
|------|------|------|
| getList | GET | `{baseUrl}/getList` |
| getDetail | GET | `{baseUrl}/getDetail` |
| add | POST | `{baseUrl}/add` |
| update | POST | `{baseUrl}/update` |
| delete | POST | `{baseUrl}/delete` |
列表参数:`{ page, size, ...filters }`
删除参数:`{ ids: string[] }`
## 响应格式
```ts
// 单条/布尔
interface ApiRes<T> {
code: number
data: T
message: string
success: boolean
}
// 分页列表 data 结构
interface PageRes<T> {
records: T
total: number
}
```
`useTable` 同时兼容 `data` 为数组或 `PageRes` 两种形态。
## http 封装
- 实例:`src/utils/http.ts`,`baseURL` = `import.meta.env.VITE_API_PREFIX`
- 开发环境 `.env.development`:`VITE_API_PREFIX = '/mock'`
- Token:请求头 `token` 字段(`getToken()`)
- 业务失败:`success === false` 时 Message/Notification 提示并 reject
- 401:跳转 `/login`
```ts
import http from '@/utils/http'
http.get<PageRes<Item[]>>('/path', { page: 1, size: 10 })
http.post<boolean>('/path', { ids: ['1'] })
```
## Mock 开发流程
### 1. 静态数据
在 `mock/_data/` 下维护 JSON/TS 数据,如 `system_dict.ts`。
### 2. 路由 Mock
在 `mock/system/`(或对应模块)新建文件:
```ts
import { defineMock } from '../_base'
import { getBaseApi, getDelayTime, resultError, resultSuccess } from '../_utils'
export default defineMock([
...getBaseApi({
baseUrl: '/system/xxx',
getListData: () => resultSuccess({ total: list.length, records: list }),
getDetailData: (query) => {
const obj = list.find((i) => i.id === query.id)
return obj ? resultSuccess(obj) : resultError(null, '未找到', 400)
}
}),
{
url: '/system/xxx/customAction',
method: 'get',
timeout: getDelayTime(),
response: ({ query }) => {
// 筛选、分页后返回
return resultSuccess({ total, records })
}
}
])
```
### 3. 工具函数
| 函数 | 用途 |
|------|------|
| `resultSuccess(data)` | `{ code:200, success:true, data, message }` |
| `resultError(data, message, code?)` | 失败响应 |
| `getDelayTime()` | 50–300ms 随机延迟 |
| `getBaseApi({ baseUrl, getListData, ... })` | 生成标准 CRUD mock |
`defineMock` 会自动给 url 加前缀 `/mock`,与 `VITE_API_PREFIX` 对应。
### 4. 注册
确保 mock 文件被 `mock/index` 或同级聚合引入(遵循现有 `mock/system/*.ts` 模式)。
## 新增模块检查清单
- [ ] `src/apis/{module}/{name}.ts`:类型 + `baseAPI` 或自定义方法
- [ ] `mock/{module}/{name}.ts`:与接口路径一致
- [ ] 列表 Mock 返回 `{ total, records }`
- [ ] 字段类型与 `Status`、`Gender` 等全局类型对齐
- [ ] 在页面通过 `useTable({ listAPI, deleteAPI })` 接入
## 自定义 listAPI 示例
```ts
useTable({
listAPI: (page) => getDictDataList({
...page,
code: dictCode.value,
name: query.name || undefined,
status: query.status === '' ? undefined : query.status
}),
immediate: false
})
```
`immediate: false` 用于依赖左侧选中项后再 `search()` 的场景。
@@ -0,0 +1,214 @@
---
name: project-crud
description: 在本模板项目中创建或修改 CRUD 列表页、表格列、搜索区、表单弹窗。使用 GiPageLayout、useTable、GiButton、a-table、GiForm、useDict 时加载本技能。
---
# CRUD 列表页开发
> 写/改 `.vue` 前先读 [code-vue](../code-vue/SKILL.md);写样式前读 [code-css](../code-css/SKILL.md)。
## 标准页面结构
```vue
<template>
<GiPageLayout margin collapsed>
<template #left><!-- 可选:树/分类 --></template>
<a-row justify="space-between" class="g-row-tool">
<a-space wrap>
<GiButton type="add" @click="onAdd" />
<GiButton type="delete" @click="onBatchDelete" />
</a-space>
<a-space wrap>
<!-- 搜索表单项 -->
<GiButton type="search" @click="search" />
<GiButton type="reset" @click="reset" />
</a-space>
</a-row>
<a-table
class="g-table"
row-key="id"
:loading="loading"
:data="tableData"
:columns="tableColumns"
:bordered="{ cell: true }"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:selected-keys="selectedKeys"
@select="select"
@select-all="selectAll"
/>
<XxxFormModal ref="XxxFormModalRef" @save-success="search" />
</GiPageLayout>
</template>
<script setup lang="tsx">
defineOptions({ name: 'ModuleName' })
</script>
```
## useTable 用法
```ts
import type * as T from '@/apis/system/user'
import { baseAPI } from '@/apis/system/user'
import { useDict, useTable } from '@/hooks'
const { dictData } = useDict(['STATUS'] as const)
const queryParams = reactive({ status: '', username: '' })
const {
loading,
tableData,
pagination,
selectedKeys,
search,
select,
selectAll,
fixed, // 操作列 fixed: 'right'(小屏自动取消)
onDelete,
onBatchDelete,
refresh
} = useTable({
listAPI: (page) => baseAPI.getList({ ...page, ...queryParams }),
deleteAPI: (ids) => baseAPI.delete({ ids }),
immediate: true,
rowKey: 'id', // 默认 id
crossPageSelect: false // 跨页多选时设为 true
})
```
要点:
- `search()` 会重置到第 1 页并清空选中
- `refresh()` 保持当前页刷新
- `listAPI` 参数为 `{ page, size }`,需展开到接口
- 空字符串查询条件建议转 `undefined` 再传 API
## GiButton 预设类型
`add` | `edit` | `delete` | `search` | `reset` | `upload` | `import` | `export`
列表工具栏与操作列统一使用 `GiButton`,操作列常用 `size="mini"`。
## 表格列(TSX)
使用 `lang="tsx"`,从 `@arco-design/web-vue` 引入 `TableColumnData`:
```tsx
const tableColumns: TableColumnData[] = [
{
title: '序号',
width: 68,
align: 'center',
render: ({ rowIndex }) => <span>{rowIndex + 1}</span>
},
{ title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true },
{
title: '状态',
width: 100,
align: 'center',
render: ({ record }) => (
<GiCellStatus status={record.status} />
)
},
{
title: '操作',
width: 160,
align: 'center',
fixed: 'right', // 或绑定 useTable 的 fixed
render: ({ record }) => (
<Space>
<GiButton type="edit" size="mini" onClick={() => onEdit(record as T.ListItem)} />
<Popconfirm content="确定删除?" onBeforeOk={() => onDelete(record as T.ListItem)}>
<GiButton type="delete" size="mini" />
</Popconfirm>
</Space>
)
}
]
```
常用单元格组件:`GiCellAvatar`、`GiCellGender`、`GiCellStatus`(从 `@/components/GiCell` 引入或在 TSX 中直接使用)。
## GiPageLayout
| 属性/插槽 | 说明 |
|-----------|------|
| `#left` | 左侧栏(部门树、字典类型列表等) |
| `#header` | 顶栏(少用) |
| `margin` | 外边距模式 |
| `collapsed` | 显示折叠按钮 |
| `size` | 左侧宽度,如 `"280px"` |
左栏选中后通过事件同步右侧列表,参考 `dict/index.vue` 的 `onDictTypeRefresh`。
## 表单弹窗(Modal + GiForm)
```vue
<a-modal v-model:visible="visible" :title="title" :mask-closable="false"
@before-ok="save" @close="close">
<GiForm ref="GiFormRef" :model-value="form" :columns="formColumns"
@update:model-value="Object.assign(form, $event)" />
</a-modal>
```
```ts
import type { FormColumnItem } from '@/components/index'
import { useResetReactive } from '@/hooks'
const [form, resetForm] = useResetReactive({ id: '', name: '', status: '1' as Status })
const GiFormRef = useTemplateRef<InstanceType<typeof GiForm>>('GiFormRef')
const add = () => {
detailId.value = ''
visible.value = true
}
const edit = async (id: string) => {
visible.value = true
detailId.value = id
Object.assign(form, (await baseAPI.getDetail({ id })).data)
}
const close = () => {
GiFormRef.value?.formRef?.resetFields()
resetForm()
}
const save = async () => {
const valid = await GiFormRef.value?.formRef?.validate()
if (valid) return false
// await baseAPI.add(form) 或 baseAPI.update(form)
emit('save-success')
return true
}
defineExpose({ add, edit })
```
`formColumns` 常用 `type`:`input`、`select`、`tree-select`、`radio-group`、`switch`、`textarea`。校验可用 `required`、`rules`;正则见 `@/utils/regexp`。
## 字典下拉
```ts
const { dictData } = useDict(['STATUS'] as const)
// 模板:<a-select :options="dictData.STATUS" />
```
## 表格增强方案选型
| 需求 | 方案 |
|------|------|
| 内置斑马纹/全屏/列设置 | `GiTable` |
| 继续用 `a-table` + 工具栏/列设置 | `GiTableSetting` 包裹(见 `views/table/custom3`) |
| 简单列表 | `a-table` + `useTable`(系统模块主流) |
## 检查清单
- [ ] `defineOptions({ name })` 已设置
- [ ] `row-key="id"` 与 `useTable` 的 `rowKey` 一致
- [ ] 删除走 `onDelete` / `onBatchDelete` 或自定义 `Popconfirm` + API
- [ ] 弹窗 `defineExpose({ add, edit })`,父组件 `useTemplateRef` 调用
- [ ] 保存成功后 `emit('save-success')` 并触发 `search`
@@ -0,0 +1,171 @@
---
name: project-guide
description: 后台管理模板的项目总览与前端强制规范。涵盖 Vue 3 + Arco Design + Gi 组件库、目录结构、自动导入、CRUD/API 约定与代码风格。在当前仓库内开发、新增页面、查阅架构或不确定项目规范时使用。
---
# 项目开发指南
> **适用范围**:本仓库 `src/**`、`mock/**`
> **规范来源**:`AGENTS.md` 常驻行为准则 + 本 skill(项目细节);编码规范按需读 `code-vue` / `code-css` / `code-ts` skill
## 本项目 Agent 约定
- 优先复用 `Gi*`、`hooks`、`getBaseApi`、现有 mock 工具;新增页面/接口对齐参考页与 `getBaseApi` 约定。
- API / Mock 以 `src/apis`、`mock` 与类型定义为准,不要猜测路径。
- 涉及 CRUD、API、Mock 时按需读取对应 skill;写 `.vue` / 样式 / TS 前读取 [code-vue](../code-vue/SKILL.md)、[code-css](../code-css/SKILL.md)、[code-ts](../code-ts/SKILL.md)。
- 改动后视情况运行 `pnpm typecheck`、`pnpm lint`、`pnpm lint:style`。
## 技术栈
| 类别 | 选型 |
|------|------|
| 框架 | Vue 3.5 + TypeScript + `<script setup lang="ts">` |
| 构建 | Vite 7 |
| UI | Arco Design Vue(按需自动导入)+ 本地 `Gi*` 组件 |
| 状态 | Pinia + pinia-plugin-persistedstate |
| 路由 | Vue Router 4(Hash 模式) |
| HTTP | `@/utils/http`,统一响应类型 `ApiRes<T>`、`PageRes<T>` |
| 工具 | @vueuse/core、lodash-es、dayjs、xe-utils |
| Mock | vite-plugin-mock + mockjs(开发默认启用) |
每个 SFC 必须 `defineOptions({ name: 'XxxYyy' })`;通用 Vue 规范见 [code-vue](../code-vue/SKILL.md),本项目补充见下文「本项目 Vue 约定」。
## 路径别名
- `@` → `src/`
- `~` → 项目根目录
## 目录约定
```
src/
├── apis/ # 接口(按业务模块分文件,标准 CRUD 用 getBaseApi)
├── components/ # Gi* 业务组件 + 部分通用组件
├── hooks/ # 组合式函数(useTable、useDict 等)
├── hooks/app/ # 业务域 hooks(useDept、useRole)
├── stores/ # Pinia
├── views/ # 页面(按模块分子目录)
├── router/ # 路由与守卫
├── utils/ # http、auth、regexp、deepClone 等
├── types/ # 全局类型(global.d.ts、api.d.ts)
mock/ # vite-plugin-mock 模拟数据
```
页面文件命名:
```
src/views/{module}/index.vue # 列表页
src/views/{module}/XxxFormModal.vue # 新增/编辑弹窗(有表单时必建)
src/apis/{module}/{name}.ts # 类型定义 + CRUD API(或 apis/system/user.ts)
mock/{module}/{name}.ts # 与 API 路径一致的 Mock
```
## 全局类型(`src/types/`)
- `Status`:`'0' | '1'`(禁用/启用,与字典 `STATUS` 一致)
- `Gender`:`'1' | '2' | '3'`
- `Pagination`:`{ page, size }`
- `ApiRes<T>`、`PageRes<T>`:统一接口响应(分页 `data` 为 `{ records, total }`)
## CRUD 列表页(强制遵循)
新建/修改表格页时,**必须**参考并遵循:
- 参考实现:`src/views/system/user/index.vue`、`UserFormModal.vue`
- Hook:`@/hooks/useTable`
- API 模板:`src/apis/system/user.ts` + `getBaseApi`
- 详细步骤:读取并遵循 [project-crud](../project-crud/SKILL.md)
核心结构:
1. `GiPageLayout` 布局(可选 `#left` 树/分类)
2. `queryParams`(reactive)+ 搜索区 + `GiButton`(search/reset)
3. `tableColumns`(`TableColumnData[]`,复杂列用 `lang="tsx"` + `render`)
4. `useTable` 绑定 `listAPI` 与 `deleteAPI`
5. `XxxFormModal` ref,`defineExpose({ add, edit })`,`@save-success="search"`
分页类型:`PageRes<T>`(`records` + `total`)。Mock 与接口详见 [project-api](../project-api/SKILL.md)。
## API 层模板
```typescript
import { getBaseApi } from '@/apis/base'
import http from '@/utils/http'
export interface ListItem {
id: string
name: string
status: Status
}
/** 标准 CRUD */
export const baseAPI = getBaseApi<ListItem>({ baseUrl: '/system/xxx' })
/** 非标准接口 */
export function customAction(params: Pagination) {
return http.get<PageRes<Item[]>>('/system/xxx/customAction', params)
}
```
- 开发环境 `VITE_API_PREFIX = '/mock'`(`.env.development`)
- 禁止绕过 `@/utils/http` 裸用 axios
## 代码风格
- 遵循 `@antfu/eslint-config`(`eslint.config.js`):箭头参数括号、1TBS、无尾随逗号;Vue block 顺序 `[['script', 'template'], 'style']`;宏顺序 `defineOptions/defineModel/defineProps/defineEmits/defineSlots`,`defineExpose` 最后。
- 布局放 `src/layout/`,Gi 组件放 `src/components/Gi*`,业务 Hook 放 `hooks/` 或 `hooks/app/`。
- 空字符串查询条件传 API 前建议转为 `undefined`。
- 通用编码规范见 [code-ts](../code-ts/SKILL.md)、[code-vue](../code-vue/SKILL.md)、[code-css](../code-css/SKILL.md);本项目补充见下文。
## 本项目编码约定
- 路径别名:`@` → `src/`,`~` → 项目根;禁止 `../../../` 穿越。
- 组件全局前缀 **`Gi`** 仅用于 `src/components/Gi*` 全局组件。
- 目录:`src/views` 页面、`src/components` 组件、`src/apis/<domain>/` + `type.ts` 接口类型、`src/types` 公共类型、`src/hooks` composables。
- API 统一 `@/utils/http`;状态 Pinia + `pinia-plugin-persistedstate`。
- 图标 `@iconify/vue`;样式从 scss/less 导入,避免内联。
- 业务注释中文,标识符英文。
- 检查:`pnpm lint`、`pnpm lint:style`、`pnpm typecheck`。
## 本项目 Vue 约定
- 每个 SFC 必须 `defineOptions({ name: 'XxxYyy' })`,名称与路由/模块对应,并与路由表 `name` 一致以支持缓存。
- **自动导入**(`vite.config.ts`,勿重复 import):
- Vue API:`ref`、`reactive`、`computed`、`watch`、`useTemplateRef` 等
- Gi 组件:`src/components/Gi*`(如 `GiPageLayout`、`GiButton`、`GiForm`)
- Arco 组件:`a-table`、`a-modal`、`a-select` 等
- 图标:`icon-*`(Iconify `icon-park-outline`)
- Gi 组件模板用 **PascalCase**;Arco 用 `a-*` 前缀;禁止手动 import Gi/Arco(类型除外)。
- 表格列复杂时用 `lang="tsx"` + `TableColumnData[]` 的 `render`;字典字段用 `useDict(['STATUS'])` 等。
- `dayjs` 默认导入变量名必须为 **`Dayjs`**(大写),禁止小写 `dayjs`。
- 深拷贝使用 `@/utils` 的 `deepClone`,禁止 `JSON.parse(JSON.stringify())`。
- Vue 参考页:`src/views/system/user/index.vue`、`UserFormModal.vue`、`src/views/table/custom3/index.vue`
## 本项目样式
- 全局样式:`src/styles/index.scss`(变量 `var.scss`、混入 `mixin.scss`、Arco 主题 `arco-ui/`)
- Stylelint:`pnpm lint:style`
- 列表页常用工具类:`g-row-tool`(工具栏行)、`g-table`(表格)、`g-full-column`(左栏撑满)、`g-mb`(下边距)
- BEM 示例:`src/views/login/LoginLeft.vue`
## 子技能(按需阅读)
| 场景 | 技能 |
|------|------|
| Vue SFC、Emits、Ref | [code-vue](../code-vue/SKILL.md) |
| 样式、BEM | [code-css](../code-css/SKILL.md) |
| TS 命名、导入 | [code-ts](../code-ts/SKILL.md) |
| 新建 CRUD 列表页、表格、弹窗表单 | [project-crud](../project-crud/SKILL.md) |
| 新增接口、Mock、baseAPI | [project-api](../project-api/SKILL.md) |
## 参考页面
- 标准 CRUD + 左树:`src/views/system/user/index.vue`
- 左右分栏 + 字典:`src/views/system/dict/index.vue`
- GiTableSetting + a-table:`src/views/table/custom3/index.vue`
- 表单弹窗:`src/views/system/user/UserFormModal.vue`
## 禁止事项
- ❌ 绕过 `@/utils/http` 直接裸用 axios
- ❌ 表格页自行实现分页/删除逻辑,而不使用 `useTable`
- ❌ 新建列表页不参考 `system/user` 与 [project-crud](../project-crud/SKILL.md) 约定