--- name: project-crud description: 列表页 + 新增/编辑弹窗的标准写法(Naive UI + 本项目约定)。新增或修改 CRUD 页面时加载本技能。 --- # CRUD 列表 / 表单(标准写法) 先找一个同类页面照抄结构(`src/views/tenant/index.vue`、`src/views/user/index.vue`、 `src/views/dict/index.vue` 是最典型的三份)。 ## 列表页骨架 ```vue 共 {{ total }} 条 新建 xxx 搜索 { query.page = p; load() }" @update:page-size="(s) => { query.page = 1; query.pageSize = s; load() }" /> ``` ### 三个必须记住的点 1. **远程分页要处理两个事件**:`@update:page` **和** `@update:page-size`, 且都要**重新取数**。naive 的表格在 `pagination` 非 false 时会自己再切一次数据 (内部 `flattenData`)—— 只改本地每页条数不重新取数,会出现"分页器说 50 条/页、表里还是 20 条"。 2. **分页器容器用 `.page-card` + `.page-card__body`**(`styles/index.scss`): 否则卡片变高会把分页器顶出可视区、怎么滚都看不到(本项目反复踩过)。 3. **`listPagination()` 而不是手写对象**:手写时 `prefix` 的参数少一个 `?` 就报 TS2322。 ### 版面(2026-10 卡片式基线) - **卡片外观是全局给的**(白底 + 描边 + 轻阴影 + 卡片头浅灰色带),页面**不要**再自己写边框/阴影; 卡片之所以"浮"得起来,靠的是 `layout` 那一层的 `--xt-bg` 灰底。 - 卡片**内部分区**用 `.section-title`(左侧蓝色小竖条)+ 浅灰小格子,**不要卡里套卡**。 - 工具条用 `.toolbar`(自带下分隔线,把"筛选区"和"表格区"切开)。 - 详细规则见 [code-css](../code-css/SKILL.md) 的「设计基线」「卡片式的三条规矩」「按钮的可见性」。 ## 表单弹窗 - `NModal` + `NForm` + `NFormItem`,`ref` 拿实例,提交前 `await formRef.value?.validate()`。 - 校验规则用 `FormRules`,与后端约束对齐(**但服务端才是权威**,前端只减少无效提交)。 - 成功后 `message.success(...)` + 关弹窗 + `load()`。 - 危险动作(删除/停用/重置密码)用 `useDialog()` 二次确认,文案写明后果。 ## 权限与展示 - 按钮显隐:`computed(() => user.has('xxx:manage'))`;**不要**用 `v-hasPerm`。 - 状态色:`tagOf(row.status)`;状态文案映射放 `apis/xt/<域>.ts` 里与接口同处。 - 敏感字段(手机号/证件号)后端只回**脱敏值** → 编辑时**无法回填原文**,要改就得重填(照抄账号页的写法)。