Files
xiaotutongxue/frontend-pc/.agents/skills/project-crud/SKILL.md
T
lovife 87ece5a971 feat(frontend): 管理端改用 Gi Admin Pro 框架 + 腾讯云主题
前端此前只有本地实现、从未入库,本次首次纳入版本管理。

框架切换
  · 以 Gi Admin Pro(Vue3 + Vite + TS + Arco Design Vue + Pinia)为底
  · 剥离 14 个演示页面目录 / 121 个文件(demo 28、file 20、form 19、system 18…)
  · 依赖 dependencies 35 → 19,devDependencies 31 → 27
  · 保留框架核心:layout、Gi* 组件库、hooks、权限指令、错误页、登录页

主题:腾讯云 / TDesign 官方色板
  · 主色 #0052D9(腾讯蓝)、成功 #00A870、警告 #ED7B2F、危险 #E34D59
  · 页面底色 #f3f3f3、文字 rgba(0,0,0,90/60/40/26%)
  · 覆盖 Arco 底层色板 + 显式语义别名(primary/success/warning/danger/link/neutral)
  · 三个改了不生效也不报错的坑已记入注释:
      1) Arco 色值是逗号分隔 RGB 三元组,不是 #RRGGBB
      2) Arco 把色板定义在 body 而非 :root,必须写 `:root, body`
      3) setting.json 的 themeColor 会以内联样式写进 body,优先级高于样式表

对接后端(三处契约差异在 @/utils/http 统一适配)
  · 响应信封 {code,msg,data} → {code,success,message,data}
  · Token 头 token → Authorization: Bearer
  · 分页 {list,total} → 补一份 records,list 原样保留
  · 40101 → 登录页;登录密码 RSA 加密(公钥走 /auth/public-key,轮换无需重建前端)
  · 路由:后端只下发菜单键,前端在本地路由表上过滤(有哪些页面归代码,谁能看归后端)

已实现页面(浏览器实测通过)
  · 登录:RSA 加密 + 自适应验证码/租户码 + 账号锁定提示
  · 工作台:真实数据(组织 6 个 / 角色 1 个 / 权限 25 项)
  · 组织架构:三级树 + CRUD + 占用校验
  · 租户管理:开通租户 + 初始密码只显示一次的强确认
  · 待建页:未实现菜单显示所属里程碑
  · 权限菜单:学校管理员看不到「租户管理」

视觉资产按腾讯云主题重做
  · logo.svg(渐变圆角方 + 填)、favicon.svg(小尺寸做减法)、login-hero.svg(材料归集)
  · 登录页点阵纹理走主题变量,换主色自动跟随
  · 文案改为产品实际能力:自动改名 / 强制压缩 / 隐私保护

清理模板遗留(约 17MB + 三处外部依赖)
  · 百度统计脚本(上报到 gi-demo 作者的统计账号)
  · 用户菜单「项目地址」(跳转模板作者的 Gitee 仓库)
  · 两份 8.8MB mp3、388KB favicon.ico、57KB 模板插画、未引用字体与 loading.gif
  · 「个人中心」指向已删除的 /system/account(点了 404)
  · 头部头像在无 avatarUrl 时渲染成裂图 → 改为姓名首字兜底

后端修复
  · 新增 GET /auth/public-key(前端加密登录密码用)
  · DataSeeder.ancestor_path 生成错误:子节点本该是 /16/17/ 却写成 /17/
    该字段是「按院系统计」的索引,错了会漏人/多人且不报错 —— 改为从库里读父节点路径
  · seedDemoOrgs 曾放在租户作用域外 + admin 早退分支之后,导致启动失败 / 永不执行
  · 示范租户补一套三级组织树(1 学校 / 2 院系 / 3 班级),供界面演示
2026-09-28 20:49:55 +08:00

5.9 KiB
Raw Blame History

name, description
name description
project-crud 在本模板项目中创建或修改 CRUD 列表页、表格列、搜索区、表单弹窗。使用 GiPageLayout、useTable、GiButton、a-table、GiForm、useDict 时加载本技能。

CRUD 列表页开发

写/改 .vue 前先读 code-vue;写样式前读 code-css。

标准页面结构

<template>
  <GiPageLayout margin collapsed>
    <template #left><!-- 可选:树/分类 --></template>

    <a-row justify="space-between" class="g-row-tool">
      <a-space wrap>
        <GiButton type="add" @click="onAdd" />
        <GiButton type="delete" @click="onBatchDelete" />
      </a-space>
      <a-space wrap>
        <!-- 搜索表单项 -->
        <GiButton type="search" @click="search" />
        <GiButton type="reset" @click="reset" />
      </a-space>
    </a-row>

    <a-table
      class="g-table"
      row-key="id"
      :loading="loading"
      :data="tableData"
      :columns="tableColumns"
      :bordered="{ cell: true }"
      :scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
      :pagination="pagination"
      :row-selection="{ type: 'checkbox', showCheckedAll: true }"
      :selected-keys="selectedKeys"
      @select="select"
      @select-all="selectAll"
    />

    <XxxFormModal ref="XxxFormModalRef" @save-success="search" />
  </GiPageLayout>
</template>

<script setup lang="tsx">
defineOptions({ name: 'ModuleName' })
</script>

useTable 用法

import type * as T from '@/apis/system/user'
import { baseAPI } from '@/apis/system/user'
import { useDict, useTable } from '@/hooks'

const { dictData } = useDict(['STATUS'] as const)

const queryParams = reactive({ status: '', username: '' })

const {
  loading,
  tableData,
  pagination,
  selectedKeys,
  search,
  select,
  selectAll,
  fixed, // 操作列 fixed: 'right'(小屏自动取消)
  onDelete,
  onBatchDelete,
  refresh
} = useTable({
  listAPI: (page) => baseAPI.getList({ ...page, ...queryParams }),
  deleteAPI: (ids) => baseAPI.delete({ ids }),
  immediate: true,
  rowKey: 'id', // 默认 id
  crossPageSelect: false // 跨页多选时设为 true
})

要点:

  • search() 会重置到第 1 页并清空选中
  • refresh() 保持当前页刷新
  • listAPI 参数为 { page, size },需展开到接口
  • 空字符串查询条件建议转 undefined 再传 API

GiButton 预设类型

add | edit | delete | search | reset | upload | import | export

列表工具栏与操作列统一使用 GiButton,操作列常用 size="mini"。

表格列(TSX)

使用 lang="tsx",从 @arco-design/web-vue 引入 TableColumnData:

const tableColumns: TableColumnData[] = [
  {
    title: '序号',
    width: 68,
    align: 'center',
    render: ({ rowIndex }) => <span>{rowIndex + 1}</span>
  },
  { title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true },
  {
    title: '状态',
    width: 100,
    align: 'center',
    render: ({ record }) => (
      <GiCellStatus status={record.status} />
    )
  },
  {
    title: '操作',
    width: 160,
    align: 'center',
    fixed: 'right', // 或绑定 useTable 的 fixed
    render: ({ record }) => (
      <Space>
        <GiButton type="edit" size="mini" onClick={() => onEdit(record as T.ListItem)} />
        <Popconfirm content="确定删除?" onBeforeOk={() => onDelete(record as T.ListItem)}>
          <GiButton type="delete" size="mini" />
        </Popconfirm>
      </Space>
    )
  }
]

常用单元格组件:GiCellAvatar、GiCellGender、GiCellStatus(从 @/components/GiCell 引入或在 TSX 中直接使用)。

GiPageLayout

属性/插槽 说明
#left 左侧栏(部门树、字典类型列表等)
#header 顶栏(少用)
margin 外边距模式
collapsed 显示折叠按钮
size 左侧宽度,如 "280px"

左栏选中后通过事件同步右侧列表,参考 dict/index.vue 的 onDictTypeRefresh。

表单弹窗(Modal + GiForm)

<a-modal v-model:visible="visible" :title="title" :mask-closable="false"
  @before-ok="save" @close="close">
  <GiForm ref="GiFormRef" :model-value="form" :columns="formColumns"
    @update:model-value="Object.assign(form, $event)" />
</a-modal>
import type { FormColumnItem } from '@/components/index'
import { useResetReactive } from '@/hooks'

const [form, resetForm] = useResetReactive({ id: '', name: '', status: '1' as Status })
const GiFormRef = useTemplateRef<InstanceType<typeof GiForm>>('GiFormRef')

const add = () => {
  detailId.value = ''
  visible.value = true
}
const edit = async (id: string) => {
  visible.value = true
  detailId.value = id
  Object.assign(form, (await baseAPI.getDetail({ id })).data)
}
const close = () => {
  GiFormRef.value?.formRef?.resetFields()
  resetForm()
}
const save = async () => {
  const valid = await GiFormRef.value?.formRef?.validate()
  if (valid) return false
  // await baseAPI.add(form) 或 baseAPI.update(form)
  emit('save-success')
  return true
}

defineExpose({ add, edit })

formColumns 常用 type:input、select、tree-select、radio-group、switch、textarea。校验可用 required、rules;正则见 @/utils/regexp。

字典下拉

const { dictData } = useDict(['STATUS'] as const)
// 模板:<a-select :options="dictData.STATUS" />

表格增强方案选型

需求 方案
内置斑马纹/全屏/列设置 GiTable
继续用 a-table + 工具栏/列设置 GiTableSetting 包裹(见 views/table/custom3)
简单列表 a-table + useTable(系统模块主流)

检查清单

  • defineOptions({ name }) 已设置
  • row-key="id" 与 useTable 的 rowKey 一致
  • 删除走 onDelete / onBatchDelete 或自定义 Popconfirm + API
  • 弹窗 defineExpose({ add, edit }),父组件 useTemplateRef 调用
  • 保存成功后 emit('save-success') 并触发 search