| name | new-list-page |
| description | Scaffold a standard Element Plus list+form page (search, table, create/edit dialog, delete) using this project's ProTable + composables pattern, with a typed API module. Use when the user wants a new business list page in go-admin-ui, or wants to customize/extend one already generated by the backend's new-business-module skill. |
新增列表页
给一个业务实体生成标准的"搜索 + 表格 + 新增/编辑弹窗 + 删除"页面,用项目约定的
ProTable + composables 写法,不是手写 mixin 或裸 el-table。
开始前先读 AGENTS.md(页面结构、composables 用法、Vue 3 注意事项、红线)。
完整可运行的参照物是 src/views/demo/product/index.vue 和 src/api/demo/product.ts——
逐字照抄它们的结构,只换实体名和字段,本文与它们冲突时以它们为准。
步骤
1. 确认后端接口已经存在
这个 skill 只生成前端。如果对应的后端模块(sys_api / sys_menu 种子数据)还没有,
先用 go-admin 仓库里的 new-business-module skill 把后端和权限数据建好——两边靠同一个
模块:资源:操作 字符串对齐(后端 sys_menu.permission,前端下面第 4 步的
v-permisaction),顺序不对会导致页面能看但按钮全部灰掉/不生效,且不会报错。
2. 写 API 模块(.ts,带类型参数)
放在 src/api/{模块}/,照抄 src/api/demo/product.ts 的结构:五个函数
list{Resource} / get{Resource} / add{Resource} / update{Resource} /
del{Resource},分别对应 GET/GET/POST/PUT/DELETE,统一走 @/utils/request。
类型参数写在 request<...>() 上,描述的是响应信封(ApiResponse<PageResult<T>>
这类),不是 payload——这是 useTable/useForm 能推导出行列类型的前提,缺了类型参数
composables 就退化成 any。
上传类接口必须传 FormData,不要手动设置 Content-Type(拦截器会据此跳过它,交给
浏览器自动写入带 boundary 的 multipart/form-data;手工设置会导致文件被序列化成 JSON 丢失)。
3. 用 composables 组装页面,不要手写状态
const table = useTable<Product, ProductQuery>({
api: listProduct,
idKey: 'id',
defaultQuery: () => ({ name: undefined, status: undefined })
})
const form = useForm<Product, number>({
defaultModel: () => ({ id: undefined, name: undefined }),
idKey: 'id',
api: { get: getProduct, add: addProduct, update: updateProduct },
onSuccess: () => table.getList()
})
const { remove } = useRemove({ api: delProduct, onSuccess: () => table.getList() })
useTable/useForm 返回 reactive() 对象,模板里直接 table.loading、
form.model.name,不用 .value,也不用解构
useRemove 是例外,要解构使用(const { remove } = ...)——它返回的是 ref,
解构 reactive 对象会丢失响应性,这里反而要解构
- 没有分页器的集合(部门树、菜单树)用
paginated: false
- 列表默认排序用
defaultSort: { prop, order },同一个值也要传给 ProTable,
否则手动排序会把新键加在默认键旁边,后端收到两个矛盾的排序参数
4. 写模板:PageContainer + ProTable
结构照抄 src/views/demo/product/index.vue:#search 插槽放搜索表单项,#toolbar
放新增/批量操作按钮,列照常写 <el-table-column>,#actions 插槽放行内操作按钮。
几条不遵守就会出问题、但不会报错的规则:
- 文字列一律
min-width,不用 width——width 是刚性的,列宽预算超出容器时表格
横向溢出,fixed="right" 的操作列会盖住相邻列而非滚过去。只有选择框列、固定控件列
(如状态开关)、fixed 操作列才用 width
- 操作列用
#actions 插槽,不要自己写 <el-table-column fixed="right">——插槽带了
固定列必须的 class-name,否则单元格换行、和滚动区的行对不齐
- 每行最多两个直接按钮,其余进溢出菜单
- 搜索框不要写
@keyup.enter——搜索按钮是 native-type="submit",回车已经统一走
表单提交,重复加会在单文本框搜索栏发两次请求
- 日期列用
<DateCell :value="row.createdAt" />,不要自己格式化——完整时间戳需要
~141px 才能不换行,会占掉列预算的四分之一
- 工具栏"新增"用
type="primary";依赖选中的批量操作(改/删)用次级按钮,删除加
type="danger" plain,不要用填充按钮——Element Plus 禁用态的填充按钮看起来
和启用态很像,容易被当成"坏了"
- 权限用
v-permisaction="['模块:资源:操作']",字符串必须与后端 sys_menu.permission
完全一致,错了不报错,只是按钮判断静默失效
- 组件名必须与后端
sys_menu.menu_name 一致:<script setup> 里用
defineOptions({ name: 'XxxManage' }) 声明——keep-alive 的 include 按组件名
匹配缓存名单,不一致时缓存静默失效
5. 错误处理:不要重复提示
utils/request.ts 的拦截器已经对非 200 响应直接 reject 并弹了错误消息,所以业务代码
拿到的 resolve 一定是成功的——.then(res => res.code === 200 ? ... : ...) 的 else
分支是死代码,不要写。onError(如果用到)只做额外处理(恢复 loading/submitting 状态),
不要再弹一次消息。
删除同理:useRemove 内部已经处理了确认框和错误提示,不要自己再写
ElMessageBox.confirm + 删除的手动组合——手写版本区分不了"用户点了取消"和
"服务端报错"。
6. Vue 3 检查
新代码一律 <script setup lang="ts"> + Composition API。以下 Vue 2 写法在当前版本
无效,出现说明是从旧代码抄的,需要改掉:
| 失效写法 | 应改为 |
|---|
slot-scope="scope" | #default="scope" |
:visible.sync | v-model:visible |
@keyup.enter.native | @keyup.enter |
this.$set / this.$delete | 直接赋值 |
el-tag 的 type 只接受 primary/success/info/warning/danger,空字符串是非法值。
7. 验证
pnpm dev 启动,用管理员账号登录,确认:新菜单出现在侧边栏、列表能查、新增/编辑弹窗
能提交、删除有确认框、切换到没有对应权限的角色时按钮正确消失。跑 pnpm run lint 确认
没有格式问题。