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

5.0 KiB
Raw Blame History

name, description
name description
project-crud 列表页 + 新增/编辑弹窗的标准写法(Naive UI + 本项目约定)。新增或修改 CRUD 页面时加载本技能。

CRUD 列表 / 表单(标准写法)

先找一个同类页面照抄结构(src/views/tenant/index.vue、src/views/user/index.vue、 src/views/dict/index.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 的「设计基线」「卡片式的三条规矩」「按钮的可见性」。

表单弹窗

  • 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 里与接口同处。
  • 敏感字段(手机号/证件号)后端只回脱敏值 → 编辑时无法回填原文,要改就得重填(照抄账号页的写法)。