diff --git a/.cursor/rules/Code-Standards.mdc b/.cursor/rules/Code-Standards.mdc index b4e5423f..296de99d 100644 --- a/.cursor/rules/Code-Standards.mdc +++ b/.cursor/rules/Code-Standards.mdc @@ -84,8 +84,8 @@ alwaysApply: true ```ts export const STATUS_OPTIONS = [ - { label: '启用', value: 1 }, - { label: '禁用', value: 0 }, + { label: '启用', value: 0 }, + { label: '禁用', value: 1 }, ]; ``` diff --git a/.cursor/rules/Vben-Components.mdc b/.cursor/rules/Vben-Components.mdc new file mode 100644 index 00000000..879d51f4 --- /dev/null +++ b/.cursor/rules/Vben-Components.mdc @@ -0,0 +1,231 @@ +--- +description: nl-admin-view Vben 官方组件用法规范(Page/Form/Modal/Drawer/VxeTable 等) +globs: apps/web-antd/**/*.{vue,ts} +alwaysApply: false +--- + +# Vben 组件规则(对齐官方文档 + 本仓库约定) + +文档来源:https://doc.vben.pro/components/ +业务页统一在 `apps/web-antd`;表单/表格必须走本仓库适配器,不要直接依赖底层实现细节。 + +## 选型速查 + +| 场景 | 用什么 | 不要用 | +|---|---|---| +| 页面外壳 | `Page`(`@vben/common-ui`) | 自写页面头/底容器 | +| 列表+搜索 | `useVbenVxeGrid`(`#/adapter/vxe-table`) | 直接 `new VxeGrid` / antd `Table` | +| 表单 | `useVbenForm`(`#/adapter/form`) | antd `Form` / 手写校验 | +| 复杂弹窗(表单/多步) | `useVbenModal` | antd `Modal` | +| 侧滑面板 | `useVbenDrawer` | antd `Drawer` | +| 轻量确认/提示/单输入 | `alert` / `confirm` / `prompt` | 为简单确认再开 Modal | +| 表格行/工具栏按钮 | `#/components/table-action` 的 `TableAction` | 散落一排 `Button` | +| 远程下拉/树选 | 适配器里的 `ApiSelect` / `ApiTreeSelect`(底层 `ApiComponent`) | 页面里手写 options 拉取逻辑 | +| 超长文本 | `EllipsisText` | CSS 省略后无 Tooltip | +| 只读详情字段组 | `VbenDescriptions` | 手写 label-value 网格 | +| 头像/固定比例裁剪 | `VCropper` | 无裁剪直接上传原图(需要裁剪时) | +| 富文本 | `VbenTiptap`(`@vben/plugins/tiptap`) | 再引一套编辑器 | + +--- + +## Page + +- 业务页根节点用 ``;列表页常见:`` +- `title` / `description` / `#extra` 都没有内容时,头部不渲染 +- `autoContentHeight`:内容区按可视高度自动扣减头/底;额外偏移用 `heightOffset`(px) +- `appendToMain` 的 Modal/Drawer 挂到内容区时,**Page 必须开 `auto-content-height`**,否则高度计算不对 +- 插槽:`default` / `title` / `description` / `extra` / `footer` + +--- + +## Modal(`useVbenModal`) + +```ts +const [Modal, modalApi] = useVbenModal({ + connectedComponent: ExtraModal, // 复杂内容抽离子组件 + draggable: true, + // overflow: true, // 需要拖出可视区时再开 +}); +``` + +强制约定: + +- 复杂弹窗用 `connectedComponent`;内外共享 `modalApi` +- **严禁覆盖 `#footer`**(会丢掉默认取消/确定与 loading) +- 扩展底部只用:`prepend-footer` / `center-footer` / `append-footer` +- 提交防重复:`modalApi.lock()` / `unlock()`(或 `setState({ confirmLoading: true })`) +- 回填:`modalApi.setData(data).open()`;内部 `onOpenChange` 里 `modalApi.getData()` +- 参数优先级:`slot` > `props` > `state`;`connectedComponent` 时同名事件**以内侧为准**(`onOpenChange` 内外都会触发) +- 默认属性可在 `apps/web-antd/src/bootstrap.ts` 的 `setDefaultModalProps` 统一改 +- 动画:`animationType: 'slide' | 'scale'`(默认 slide) + +常用 API:`open` / `close` / `setState` / `setData` / `getData` / `lock` / `unlock` + +--- + +## Drawer(`useVbenDrawer`) + +- API 形态与 Modal 基本一致:`connectedComponent`、`setData/getData`、`lock/unlock`、footer 三插槽 +- **同样严禁覆盖 `#footer`** +- `placement`: `left | right | top | bottom`(默认 `right`) +- 关闭前校验用 `onBeforeClose`(支持 Promise);需要重置内部状态可开 `destroyOnClose` +- 挂内容区同样要配合 Page 的 `auto-content-height` +- 默认属性:`setDefaultDrawerProps` + +--- + +## Alert(轻量提示) + +来自 `@vben/common-ui`:`alert` / `confirm` / `prompt` / `useAlertContext` + +- **简单确认、提示、单字段输入**用这套;复杂表单/多控件仍用 Modal +- `confirm().then/catch`;异步关闭用 `beforeClose`,返回 `false` 可阻止关闭 +- `prompt` 可传 `component` + `componentProps`(antd Select 需 `popupClassName: 'pointer-events-auto'`) +- 自定义内容里需要主动确认/取消:`useAlertContext().doConfirm/doCancel`(仅 setup/函数组件) +- 动态创建的 Alert **不支持 HMR**,改代码后需关掉重开 + +--- + +## Form(`useVbenForm` from `#/adapter/form`) + +```ts +const [Form, formApi] = useVbenForm({ + schema: [ + { + fieldName: 'nick_name', // 与后端 snake_case 对齐 + label: '昵称', + component: 'Input', + rules: 'required', // 选择类用 selectRequired + componentProps: { placeholder: '请输入' }, + }, + ], +}); +``` + +强制约定: + +- 只从 `#/adapter/form` 引入,不直接玩 TanStack Form 实例 +- 动态改字段:`formApi.updateSchema([...])`;**没有** `setComponentProps` +- 重置:`formApi.resetForm()`;**禁止** `resetFields()`(antd API,会报错) +- 取值/赋值:`getValues()` / `setValues()`;需要提交编解码时用 `codec` / `getRawValues()` +- 校验:`formApi.validate().then(e => { if (e.valid) ... })` 或 `validateAndSubmitForm()` +- 字段自定义 slot:控件绑定用 `v-bind="slotProps.componentProps"`(不要再 `v-bind="slotProps"`) +- 联动优先 `dependencies.resolve(context)` 一次返回 `show/disabled/rules/componentProps...` +- 远程 options:打开时拉取后 `updateSchema` 注入;或用 `ApiSelect`/`ApiTreeSelect` +- 密码字段用 `InputPassword`(适配器组件名),不要 `Input` + `type="password"` + +--- + +## Vxe Table(`useVbenVxeGrid` from `#/adapter/vxe-table`) + +```ts +const [Grid, gridApi] = useVbenVxeGrid({ + formOptions, // 搜索表单(Vben Form) + gridOptions: { + proxyConfig: { + ajax: { + query: async ({ page }, formValues) => { + // 返回适配器约定结构 + return { items: [], total: 0 }; + }, + }, + }, + toolbarConfig: { search: true }, + }, + gridEvents, +}); +``` + +强制约定: + +- 远程列表走 `proxyConfig.ajax.query`,禁止自己 fetch 再赋 `data` +- 响应结构对齐适配器:`{ items, total }`(见 `apps/web-antd/src/adapter/vxe-table.ts`) +- 分页从 `{ page }` 取 `currentPage` / `pageSize` +- 刷新:`gridApi.query()` 保留页;`gridApi.reload()` 回第一页 +- loading:`gridApi.setLoading(bool)` +- 工具栏插槽:`toolbar-actions`(左)/ `toolbar-tools`(右) +- 操作列:列上 `slots: { default: 'action' }`,页面 `