Files
xk-admin/.cursor/rules/Code-Standards.mdc

188 lines
11 KiB
Plaintext
Raw Normal View History

---
2026-07-20 15:05:34 +08:00
description: 萧康云医管理后台Vben Admin v5 + Vue 3 + TS + Ant Design Vue代码规范
alwaysApply: true
---
2026-07-20 15:05:34 +08:00
# 基础规范(所有项目通用)
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2026-07-20 15:05:34 +08:00
2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于 2 行
3. 有封装好的方法、组件需要复用,不要重复造轮子
2026-07-20 15:05:34 +08:00
4. 小程序端的抽屉全部需要用 page-container 来防止用户意外退出页面(记得使用 v-if 而不是 v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化
6. 卡片强调样式:**禁止**用左侧竖色条(`border-left: Npx solid 色值` / 左侧色块做卡片强调或分类标识统一用「1px 同色系边框 + 微光」——`border: 1px solid hsl(var(--主题色) / 30%)` 配 `box-shadow: 0 0 6px hsl(var(--主题色) / 18%)`;历史页面暂不强制回改,新增/改动的卡片必须遵守
2026-07-20 15:05:34 +08:00
# 全局架构规范
## 目录结构规范
- 业务页面统一放在 `apps/web-antd/src/views/<端>/<模块>/`system / business / doctor 等)
- 标准 CRUD 模块目录结构:
```
views/<端>/<模块>/
├── index.vue # 列表页Page + Grid + Modal
├── api/index.ts # 本模块 APICRUD
├── config/
│ ├── table.ts # vxe-grid 列定义 + proxyConfig
│ ├── search.ts # 顶部搜索表单 schema
│ └── form.ts # 新增/编辑弹窗 form schema
├── components/
│ └── modal.vue # 新增/编辑弹窗
└── utils/ # 模块工具(可选)
```
- 全局复用组件放 `apps/web-antd/src/components/`
- 表单内可用组件放 `apps/web-antd/src/components/form/components/`,文件名 kebab-case自动注册为 schema 的 `component` 名PascalCase 引用)
## 强制复用封装(禁止重复造轮子)
- 表格统一用 `useVbenVxeGrid`(来自 `#/adapter/vxe-table`**不要直接 new VxeGrid**
- 表单统一用 `useVbenForm`(来自 `#/adapter/form`**不要直接写 `<Form>` + 手动校验**
- 弹窗统一用 `useVbenModal`(来自 `@vben/common-ui`**不要用原生 `<Modal>`**
- 页面外壳统一用 `<Page>` 组件(来自 `@vben/common-ui`
- HTTP 请求统一用 `requestClient`(来自 `#/api/request`**禁止直接 axios / fetch**
- 行操作和工具栏按钮统一用 `<TableAction>` 组件(来自 `#/components/table-action`
- 业务选择器(员工、医生、门店、推广员、地区等)必须复用 `#/components/form/components/*-picker.vue`,不要重写
## API 层规范
- API 文件统一放在 `<模块>/api/index.ts`
- 函数命名约定:`get{Entity}List` / `get{Entity}Option` / `get{Entity}Info` / `create{Entity}` / `update{Entity}` / `delete{Entity}`
- URL 使用 kebab-case统一前缀变量 `const prefix = 'xxx/'`**URL 末尾不补 `/`**
- GET 用 `params`POST 用 `data`,例如:
```ts
import { requestClient } from '#/api/request';
const prefix = 'oa-robot/';
export async function getOaRobotList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
```
## 字段命名对齐后端
- 数据库字段在前端 schema 的 `field` / `fieldName` 中保持 snake_case如 `created_at`、`store_id`、`platform_code`**不要转换为 camelCase**
- 时间戳字段(`created_at` 等)后端查询器已格式化为字符串,前端**无需再次格式化**
- TS 变量用 camelCase常量用 UPPER_SNAKE_CASE类型用 PascalCase
## 表单校验
- 必填用字符串规则 `rules: 'required'`(选择项用 `'selectRequired'`),规则定义在 `#/adapter/form.ts` 的 `defineRules`
- 校验调用统一用 `formApi.validate().then(e => { if (e.valid) {...} })`
- 必填字段在 schema 中加 `rules: 'required'`,禁止散落 `validator` 写法
## 字典/枚举管理
- 模块内建 `config/constants.ts`,导出 `{ label, value }[]` 数组,禁止在 schema 中散落字面量
- 例如:
```ts
export const OA_MESSAGE_TYPE_OPTIONS = [
{ label: '文本', value: 'text' },
{ label: 'Markdown', value: 'markdown' },
];
```
## 路由规范
- 业务路由通过后端动态菜单驱动(`xk_menu` 表),**不要在前端 `router/routes/modules/` 静态注册业务路由**
- `defineOptions({ name: 'PascalCaseName' })` 必须与 `xk_menu.name` 字段一致
## Tab 用法
- 统一用 antd 原生 `Tabs` + `Tabs.TabPane`**不要找 Vben 自封装的 Tabs**
- Tab 选中状态持久化走 `localStorage`(参考 `system-config/index.vue` 的 `TAB_STORAGE_KEY` 模式)
## 抽屉用法小程序端admin 不涉及)
- 小程序端的抽屉用 `page-container` + `v-if`(不是 `v-show`)防止用户意外退出页面
## 中文注释
- 每个 `<script setup>` 顶部说明模块用途
- 每个主要函数(特别是有业务逻辑的)必须有中文注释
- 复杂的 schema 字段配置要注释意图
## 空行控制
- 上一部分代码和下一部分代码中间空行不超过 2 行
## VIP 功能判断PC 管理端强制)
- **判断方法**:统一用 `#/utils/vip` 的 `hasVipPermission` / `useVipPermission` / `VIP_FEATURE` 常量
- **显隐组件**:模板包一层 `<VipGate code="medical_record" :vip="receptionVip">``#/components/vip/VipGate.vue`
- 接诊/复诊/开方:**必须传履约诊所 vip**`get-current-store-type` 的 `vip`),禁止只靠登录态判断门店权益
- 显式传 `null` 表示「尚无履约诊所权益」,**不会**回落登录态;不传第二参才用 `userInfo.vip`
- 禁止在业务页再手写 `permissions.indexOf('medical_record')` 或复制一套 VIP 拉取逻辑
## 暗色模式适配PC 管理端强制)
后台支持亮/暗主题切换,**新增或改样式时必须同时适配暗色**,禁止只按白底写死颜色。
### 优先用法(推荐)
- 颜色、边框、背景一律用主题 CSS 变量,例如:
- 文字:`hsl(var(--foreground))` / `hsl(var(--muted-foreground))`
- 背景:`hsl(var(--background))` / `hsl(var(--card, var(--background)))` / `hsl(var(--muted) / 0.25)`
- 边框:`hsl(var(--border))`
- 强调/选中:`hsl(var(--primary))`、`hsl(var(--primary) / 0.1)`
- 警告:`hsl(var(--warning))`、`hsl(var(--warning) / 0.12)`(参考 `AiDisclaimerBanner.vue`
- **禁止**业务样式写死 `#fff`、`#000`、`#0f172a`、`#f8fafc`、`#64748b` 等仅适合亮色的色值
- **禁止**用 `:global(.dark)` 改全局变量以免污染整站配色
- 优先「一套变量样式自动跟主题」,避免再写一套 `.dark .xxx { ... }`;仅当变量无法表达(如图片反色、特殊阴影)才用局部 `.dark .xxx`(且必须 `scoped`
### 自检清单
- 弹窗 / 抽屉 / 卡片 / 预览对照区:在暗色下背景、文字、边框对比度可读
- 选中态、成功/导入高亮:用 `--primary` 透明度,不要写死浅绿底 `#f0faf8`
- 表格空态、分割线、次要文案:用 `--muted` / `--muted-foreground` / `--border`
- 改完后切换暗色主题肉眼过一遍,不要只测亮色
2026-07-20 15:05:34 +08:00
## Vben 组件使用规范
### VbenModal来自 @vben/common-ui
- 创建弹窗必须用 `useVbenModal`,禁止直接用 antd `Modal` 或原生 `<dialog>`
- 抽离弹窗内容到子组件时,外层用 `connectedComponent` 参数连接,内外组件共享 `modalApi`
- 底部按钮扩展 slot 优先级:`prepend-footer`(取消按钮左侧)> `center-footer`(取消/确定之间)> `append-footer`(确定右侧)
- **严禁覆盖 `#footer` slot**,会丢失默认的取消/确定按钮和 loading 行为
- 表单提交 loading 走 `modalApi.setState({ confirmLoading: true })`,禁止自己写 button loading
- 提交防抖/防重复用 `modalApi.lock()` / `unlock()`>5.5.3),禁止手动 disabled
- 拖拽开 `draggable: true`,需要拖出可视区时同时开 `overflow: true`
- 数据回填走 `modalApi.setData()` + `onOpenChange(isOpen) { const data = modalApi.getData() }`
### VbenDrawer来自 @vben/common-ui
- 创建抽屉必须用 `useVbenDrawer`,禁止直接用 antd `Drawer`
- 底部按钮扩展 slot 与 Modal 一致:`prepend-footer` / `center-footer` / `append-footer`
- **同样严禁覆盖 `#footer` slot**
- 关闭前校验用 `onBeforeClose`>5.5.2 支持 Promise禁止自己拦截
- 提交防抖/防重复用 `drawerApi.lock()` / `unlock()`>5.5.3
- 小程序端的「抽屉」概念与此无关,小程序端按现有规则用 `page-container`
### VbenVxeTable来自 #/adapter/vxe-table
- 表格必须用 `useVbenVxeGrid`,禁止直接 `new VxeGrid` 或 antd `Table`
- 远程数据加载走 `gridOptions.proxyConfig.ajax.query`**禁止自己 fetch 后 `data = []` 赋值**
- query 接口返回结构必须匹配适配器配置:`{ items: [], total: number }`(在 `apps/web-antd/src/adapter/vxe-table.ts` 的 `response` 字段统一配置)
- 分页参数从 `{ page }` 解构取:`page.currentPage` / `page.pageSize`,禁止自己读 URL
- 刷新表格用 `gridApi.query()`(保留当前页)或 `gridApi.reload()`(回到第一页),禁止整页 `window.location.reload()`
- 工具栏按钮插 slot`toolbar-actions`(标题左侧)、`toolbar-tools`(工具按钮左侧),禁止改 vxe-grid 内部模板
- 表格 loading 用 `gridApi.setLoading(bool)`,禁止自己遮罩
- 搜索表单由 `formOptions` 配置(底层是 Vben Form开关走 `gridOptions.toolbarConfig.search = true`
- 自定义列渲染走 `slots: { default: 'action' }` + Grid 子组件内 `<template #action="{ row }">`,禁止 column 渲染函数里写 JSX
### VbenForm来自 #/adapter/form
- 表单必须用 `useVbenForm`,禁止直接 `<form>` 或 antd `Form`
- 动态修改 schema 必须用 `formApi.updateSchema([...])`**vben form 没有 `setComponentProps` 这个方法**(这是已踩过的坑)
- 重置表单用 `formApi.resetForm()`**严禁 `formApi.resetFields()`**(那是 antd Form 的 APIVben Form 没有,会报 `resetFields is not a function`;已踩过坑)
2026-07-20 15:05:34 +08:00
- 取值/赋值用 `formApi.getValues()` / `formApi.setValues(obj)`,禁止自己 `v-model` 收集
- 校验用 `formApi.validate().then(e => { if (e.valid) {...} })` 或 `formApi.validateAndSubmitForm()`
- 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `defineRules` 注册
- schema 中字段名(`fieldName`)必须与后端字段 snake_case 对齐,禁止前端 camelCase
- 密码/敏感字段用 `VbenInputPassword` 组件,禁止用 `VbenInput` 配 `type="password"`
- 单选/多选选项来自接口时,必须在弹窗/页面 `onMounted` 或 `onOpenChange` 里拉取后通过 `updateSchema` 注入,禁止写死常量