---
description: 萧康云医管理后台（Vben Admin v5 + Vue 3 + TS + Ant Design Vue）代码规范
alwaysApply: true
---

# 基础规范（所有项目通用）

1. 写代码的时候要补充详细的中文注释（每个方法是干嘛的，为什么要这样写），如果是工作区，则所有项目都适用该规则
2. 注意不要生成太多的空行，上一部分代码和下一部分代码中间的空行不要大于 2 行
3. 有封装好的方法、组件需要复用，不要重复造轮子
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%)`；历史页面暂不强制回改，新增/改动的卡片必须遵守

# 全局架构规范

## 目录结构规范

- 业务页面统一放在 `apps/web-antd/src/views/<端>/<模块>/`（端：system / business / doctor 等）
- 标准 CRUD 模块目录结构：

  ```
  views/<端>/<模块>/
  ├── index.vue                    # 列表页（Page + Grid + Modal）
  ├── api/index.ts                 # 本模块 API（CRUD）
  ├── 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`
- 改完后切换暗色主题肉眼过一遍，不要只测亮色

## 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 的 API，Vben Form 没有，会报 `resetFields is not a function`；已踩过坑）
- 取值/赋值用 `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` 注入，禁止写死常量

