Files
xiaotutongxue/frontend-pc/.agents/skills/project-crud/SKILL.md
T
lovife 5b877451eb refactor(pc): 从 Gi Admin Pro + Arco 重搭为纯 Vue 3 + Naive UI
旧模板的 193 个文件整体删除,换成纯 Vue 3.5 + Naive UI 2.42 的树。

- 去掉 auto-import(组件与 API 全显式 import)、v-hasPerm(改 user.has('码'),全库 8 处)、
  Stylelint;n-tag 的 type 统一过 utils/tag.ts 转换(Arco 色词直接透传会静默变灰)
- 图标改为构建期抽取:scripts/extract-pc-icons.py -> icons/icon-data.ts(37 个),
  内网离线部署不会白屏
- 重搭后修掉 6 类"编译 / lint / typecheck 全绿、界面上就是坏的"故障:
  ① NGrid 的 cols 写成像素值(不是断点名 -> 永远只 1 列,工作台因此错乱)
  ② n-tab-pane 用 tab 属性(只渲染空注释节点,整排 tab 一个字都不显示)
  ③ 漏 import 被当未知自定义元素(样式整块丢) ④ .n-card__content 类名不存在
  (高度链第一环失效,表格行在 DOM 里但屏幕上没有) ⑤ 填报单状态映射错位一格
  ⑥ 有标题的卡片没有上内边距(naive 的 padding-top 只给"内容区是长子"的卡片)
- 卡片式版面提成全局基线:styles/index.scss 的 xt-* 令牌 + .page-card / .toolbar /
  .section-title,页面只引用、不再各写一套
- 品牌:改名「深学在线」,文字标换成几何标(方 = 卷宗 / 环 = 在线,不用文字字形);
  index.html 的页签图标从 404 的 /favicon.ico 改指真实存在的 /favicon.svg
2026-10-10 16:48:46 +08:00

122 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: project-crud
description: 列表页 + 新增/编辑弹窗的标准写法(Naive UI + 本项目约定)。新增或修改 CRUD 页面时加载本技能。
---
# CRUD 列表 / 表单(标准写法)
先找一个同类页面照抄结构(`src/views/tenant/index.vue`、`src/views/user/index.vue`、
`src/views/dict/index.vue` 是最典型的三份)。
## 列表页骨架
```vue
<script setup lang="ts">
import type { DataTableColumns } from 'naive-ui'
import { NButton, NCard, NDataTable, NSpace, NTag, useMessage } from 'naive-ui'
import { h, onMounted, reactive, ref } from 'vue'
import * as xxxApi from '@/apis/xt/xxx'
import { listPagination } from '@/composables/pagination'
import { fmtDate } from '@/utils/format'
import { tagOf } from '@/utils/tag'
const message = useMessage()
const loading = ref(false)
const list = ref<XxxVO[]>([])
const query = reactive({ page: 1, pageSize: 20, keyword: '' })
const total = ref(0)
async function load() {
loading.value = true
try {
const res = await xxxApi.list({ ...query })
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
const columns: DataTableColumns<XxxVO> = [
{ title: '名称', key: 'name' },
{ title: '状态', key: 'status', render: (row) => h(NTag, { type: tagOf(row.status), size: 'small' }, { default: () => STATUS_NAMES[row.status] }) },
{ title: '操作', key: 'actions', render: (row) => h(NSpace, null, { default: () => [
h(NButton, { size: 'small', onClick: () => openEdit(row) }, { default: () => '编辑' })
] }) }
]
onMounted(load)
</script>
<template>
<!--
高度链必须完整:.page-card(撑满内容区)→ .page-card__body → n-data-table[flex-height],
每一层都要 min-height: 0(前两层由 styles/index.scss 给)。少一层 → 分页器被顶出可视区。
-->
<NCard title="xxx" :bordered="false" class="page-card">
<template #header-extra>
<span class="toolbar__hint">共 {{ total }} 条</span>
</template>
<!-- 工具条:筛选在左/右,主操作 type="primary" + **默认尺寸**;
同一行控件尺寸要一致(别混 size="small",否则 28px 与 34px 参差不齐) -->
<div class="toolbar">
<NSpace :size="8">
<NButton v-if="canManage" type="primary" @click="openAdd">
新建 xxx
</NButton>
</NSpace>
<NSpace :size="8">
<NInput v-model:value="query.keyword" placeholder="搜索名称" clearable @keyup.enter="search" />
<NButton type="primary" ghost @click="search">
搜索
</NButton>
</NSpace>
</div>
<div class="page-card__body">
<NDataTable
remote
flex-height
:loading="loading"
:columns="columns"
:data="list"
:pagination="listPagination({ page: query.page, pageSize: query.pageSize, itemCount: total })"
@update:page="(p) => { query.page = p; load() }"
@update:page-size="(s) => { query.page = 1; query.pageSize = s; load() }"
/>
</div>
</NCard>
</template>
```
### 三个必须记住的点
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<FormInst | null>` 拿实例,提交前 `await formRef.value?.validate()`。
- 校验规则用 `FormRules`,与后端约束对齐(**但服务端才是权威**,前端只减少无效提交)。
- 成功后 `message.success(...)` + 关弹窗 + `load()`。
- 危险动作(删除/停用/重置密码)用 `useDialog()` 二次确认,文案写明后果。
## 权限与展示
- 按钮显隐:`computed(() => user.has('xxx:manage'))`;**不要**用 `v-hasPerm`。
- 状态色:`tagOf(row.status)`;状态文案映射放 `apis/xt/<域>.ts` 里与接口同处。
- 敏感字段(手机号/证件号)后端只回**脱敏值** → 编辑时**无法回填原文**,要改就得重填(照抄账号页的写法)。