# New List Page

> 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.

- Skill: `go-admin-team/new-list-page` (Agent Skill)
- Install (CLI): `npx skillmds@latest add go-admin-team/new-list-page`
- Raw SKILL.md: https://api.skillmd.com/api/skills/go-admin-team/new-list-page/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: go-admin-team (https://skillmd.com/u/go-admin-team)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/go-admin-team/new-list-page

---


# 新增列表页

给一个业务实体生成标准的"搜索 + 表格 + 新增/编辑弹窗 + 删除"页面，用项目约定的
`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 组装页面，不要手写状态

```ts
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` 确认
没有格式问题。

