Files
new-school-iot-admin/.agents/skills/v3-create-crud/SKILL.md
T
2026-10-26 18:54:09 +08:00

362 lines
13 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: v3-create-crud
description: 创建增删改查(CRUD)页面,基于 Element Plus 组件库,包含表格、搜索、分页、新增/编辑弹窗、删除确认等功能。当用户提到以下任何场景时都应触发:创建管理页面、创建列表页、创建表格页。即使用户没有明确说 CRUD,只要意图是创建带表格和表单操作的后台页面就应该使用此 Skill。使用时需提供模块名称和字段信息。
metadata:
author: Glittering Ma & pany
version: "2026.06.13"
---
# 创建 CRUD 页面
根据用户提供的模块名称和字段信息,生成增删改查页面。
本 Skill 定义的是默认 CRUD 模式,当用户的实际需求与本 Skill 约定冲突时,以用户需求为准,灵活调整。
## 输入要求
用户需提供:
1. **模块名称**(如 `product`、`order`)— 用于目录名和命名
2. **字段列表** — 每个字段需明确:
- 字段名(英文,camelCase)
- 中文标签
- 类型(string / number / boolean / enum / date 等)
- 是否必填
- 是否作为搜索条件
- 是否需要自定义渲染(如 tag 状态展示)
如果用户信息不完整,主动询问补全后再生成。
## 生成文件
在 `src/pages/<模块路径>/` 下生成:
1. `index.vue` — 页面主文件
2. `apis/index.ts` — 接口文件
3. `apis/type.ts` — 类型定义
## 设计决策指引
生成代码时需要做出的判断(不要机械套用,根据具体场景决定):
**弹窗宽度** — 默认用 `30%`,有复杂布局用 `50%` 或更大的比例。
**工具栏按钮** — 按需组合,不必全部包含:
- "新增" 几乎总是需要的
- "批量删除" 仅在有批量操作需求时添加(对应表格 selection 列)
- "下载/导出" 仅在有导出需求时添加
- "刷新当前页" 推荐保留,方便调试和手动刷新
**删除确认文案** — 选择对用户最有辨识度的字段作为确认提示(如用户名、订单编号、商品名称),而不是 id。
**搜索参数传递** — 保持简单,避免类型体操:
- 字段少(2-3 个)时可以逐个列出
- 字段多时用 `...searchData` 展开更简洁
- 核心原则:不要出现 `as any`,除非明确要求
**表单字段 vs 表格列** — 不是所有字段都同时出现在两处:
- 仅创建时需要的字段(如密码):表单中有,表格中无
- 系统生成的字段(如创建时间):表格中有,表单中无
- 用 `v-if="formData.id === undefined"` 控制仅新增时显示的字段
## 页面结构规范
页面整体包裹在 `<div class="app-container">` 中,由搜索区域、表格区域、弹窗三部分组成。
### 1. 搜索区域
- `el-card`:`v-loading="loading"` + `shadow="never"` + `class="search-wrapper"`
- 内含 `el-form`:`ref="searchFormRef"` + `:inline="true"` + `:model="searchData"`
- 每个搜索字段设置 `prop`(`resetFields` 依赖它)和 `label`
- 末尾放查询按钮(`:icon="Search"` + `type="primary"`)和重置按钮(`:icon="Refresh"`)
### 2. 表格区域
- `el-card`:`v-loading="loading"` + `shadow="never"`
- 工具栏(`div.toolbar-wrapper`)分左右两侧:
- 左侧:文字按钮(新增、批量删除等)
- 右侧:圆形图标按钮 + `el-tooltip`(下载、刷新当前页等)
- 表格(`div.table-wrapper` > `el-table :data="tableData"`):
- 需要批量操作时加 `type="selection"` 首列
- 数据列:`prop` + `label` + `align="center"`
- 操作列:`fixed="right"` + 适当 `width` + `align="center"`,内含修改/删除按钮(`text bg size="small"`)
- 操作列按钮将 `scope.row` 传给强类型 handler 时,使用 `scope.row as XxxData` 明确行类型,避免 Element Plus 默认 `DefaultRow` 导致 `vue-tsc` 报错,不要使用 `as any`
- 分页(`div.pager-wrapper` > `el-pagination`)
### 3. 弹窗
- `el-dialog`:`v-model="dialogVisible"` + 通过 `formData.id === undefined` 判断新增/修改标题 + `@closed="resetForm"`
- 使用 `@closed` 而非 `@close`(确保关闭动画结束后再重置,避免用户看到表单内容闪烁)
- 表单:`ref="formRef"` + `:model="formData"` + `:rules="formRules"` + `label-width="auto"`
- footer:取消按钮 + 确认按钮(`type="primary"` + `:loading="loading"`)
## 代码组织规范
使用 `<script lang="ts" setup>` + `defineOptions({ name: "PascalCase 模块名" })`。
顶部声明共享的 `loading` `ref` 和 `usePagination` 解构,分页请求统一通过 `callback`、`resetCurrentPage`、`watchPagination` 组织。
逻辑按增删改查分区,用 `// #region` 和 `// #endregion` 标记:
### 增(Create)
```ts
const DEFAULT_FORM_DATA: CreateOrUpdateXxxRequestData = {
id: undefined,
// ... 所有表单字段的初始值
}
const dialogVisible = ref<boolean>(false)
const formRef = useTemplateRef("formRef")
const formData = ref<CreateOrUpdateXxxRequestData>(cloneDeep(DEFAULT_FORM_DATA))
const formRules: FormRules<CreateOrUpdateXxxRequestData> = { /* 验证规则 */ }
function handleCreateOrUpdate() {
formRef.value?.validate((valid) => {
if (!valid) { ElMessage.error("表单校验不通过"); return }
loading.value = true
const api = formData.value.id === undefined ? createXxxApi : updateXxxApi
api(formData.value).then(() => {
ElMessage.success("操作成功")
dialogVisible.value = false
}).finally(() => {
loading.value = false
getTableData()
})
})
}
function resetForm() {
formRef.value?.clearValidate()
formData.value = cloneDeep(DEFAULT_FORM_DATA)
}
```
**为什么新增和编辑共用一个弹窗**:减少重复代码,通过 `id` 是否存在区分状态,共享验证规则和提交逻辑。
### 删(Delete)
```ts
function handleDelete(row: XxxData) {
ElMessageBox.confirm(`正在删除${模块中文名}:${row.辨识字段},确认删除?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
loading.value = true
deleteXxxApi(row.id).then(() => {
ElMessage.success("删除成功")
}).finally(() => {
loading.value = false
getTableData()
})
})
}
```
### 改(Update)
```ts
function handleUpdate(row: XxxData) {
dialogVisible.value = true
formData.value = cloneDeep(row)
}
```
**为什么用 `cloneDeep`**:避免编辑时直接修改表格行数据(引用类型),用户取消编辑时表格数据不会被污染。
### 查(Read)
```ts
const tableData = ref<XxxData[]>([])
const searchFormRef = useTemplateRef("searchFormRef")
const searchData = reactive({ /* 搜索字段,初始值为空字符串 */ })
function getTableData() {
loading.value = true
getXxxApi({
currentPage: paginationData.currentPage,
size: paginationData.pageSize,
...searchData
}).then(({ data }) => {
paginationData.total = data.total
tableData.value = data.list
}).catch(() => {
tableData.value = []
}).finally(() => { loading.value = false })
}
function handleSearch() {
resetCurrentPage()
}
function resetSearch() {
searchFormRef.value?.resetFields()
handleSearch()
}
```
**`handleSearch` 的逻辑**:直接调用 `resetCurrentPage`。`resetCurrentPage` 会在当前已是第 1 页时直接请求,否则重置页码并由分页监听触发请求,避免重复调用。
### 分页监听
```ts
watchPagination()
```
**为什么用 `watchPagination` 而不是 `onMounted`**:让分页变化和初始加载共享同一个入口,数据获取逻辑只写一处。
## usePagination 用法
```ts
import { usePagination } from "@@/composables/usePagination"
const { paginationData, resetCurrentPage, watchPagination } = usePagination({
callback: getTableData
})
```
`paginationData` 是 `reactive` 对象,包含:`total`、`currentPage`、`pageSizes`、`pageSize`、`layout`。模板中使用 `v-model` 双向绑定页码和每页条数:
```html
<el-pagination
v-model:current-page="paginationData.currentPage"
v-model:page-size="paginationData.pageSize"
:page-sizes="paginationData.pageSizes"
:total="paginationData.total"
:layout="paginationData.layout"
background
/>
```
## 表格列的自定义渲染
对于需要视觉区分的字段(状态、角色、类型等),使用 `el-tag` 渲染。思路是:突出重要/异常值,其余用温和颜色兜底。
```html
<!-- boolean:二元对立,可以用 success / danger 对比 -->
<el-table-column prop="status" label="状态" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status" type="success" effect="plain" disable-transitions>启用</el-tag>
<el-tag v-else type="danger" effect="plain" disable-transitions>禁用</el-tag>
</template>
</el-table-column>
<!-- enum:值高亮 -->
<el-table-column prop="roles" label="角色" align="center">
<template #default="scope">
<el-tag v-if="scope.row.roles === 'admin'" type="primary" effect="plain" disable-transitions>admin</el-tag>
<el-tag v-else type="warning" effect="plain" disable-transitions>{{ scope.row.roles }}</el-tag>
</template>
</el-table-column>
```
当 enum 值需要明确区分时,逐个用 `v-if / v-else-if` 列出,最后用 `v-else` 兜底。
## 表单字段组件选择
根据字段语义选择组件:
| 字段类型 | 组件 | 备注 |
|---------|------|------|
| string(短文本)| `el-input` | |
| string(长文本)| `el-input type="textarea"` | 描述、备注等 |
| string(密码)| `el-input type="password"` | |
| number | `el-input-number` | `:min` / `:max` 根据业务约束设定 |
| boolean | `el-switch` | |
| enum | `el-select` + `el-option` | 搜索区域加 `clearable` |
| date | `el-date-picker type="date" value-format="YYYY-MM-DD"` | |
| datetime | `el-date-picker type="datetime" value-format="YYYY-MM-DD HH:mm:ss"` | |
验证规则的 `trigger`:输入型组件(input、textarea)用 `"blur"`,选择型组件(select、date-picker、switch)用 `"change"`。
## 接口规范
接口文件使用命名空间导入类型:`import type * as Xxx from "./type"`
导出四个函数(注释用"增删改查"标记):
- `createXxxApi` — POST
- `deleteXxxApi` — DELETE,参数为 id
- `updateXxxApi` — PUT
- `getXxxApi` — GET,参数为分页 + 搜索条件
使用 `import { request } from "@/http/axios"` 发起请求。查询接口需指定泛型:`request<Xxx.XxxResponseData>({ ... })`。
## 类型规范
类型文件导出(`ApiResponseData` 是全局类型,无需导入):
- `CreateOrUpdateXxxRequestData` — 表单提交数据(`id?: number` + 各表单字段)
- `XxxRequestData` — 列表查询参数(`currentPage: number` + `size: number` + 搜索字段用 `?:` 可选标记)
- `XxxData` — 表格行数据(`id: number` + 各展示字段)
- `XxxResponseData` — `ApiResponseData<{ list: XxxData[]; total: number }>`
搜索字段在 `XxxRequestData` 中默认用 `string` 类型(除非明确指定类型),因为搜索框传递的默认是字符串值(除非明确指定类型),需直接保持类型正常,不要随意使用类型断言。
页面中使用具名导入:`import type { CreateOrUpdateXxxRequestData, XxxData } from "./apis/type"`
## 导入规范
```ts
// 类型导入
import type { CreateOrUpdateXxxRequestData, XxxData } from "./apis/type"
import type { FormRules } from "element-plus"
// API 导入
import { createXxxApi, deleteXxxApi, getXxxApi, updateXxxApi } from "./apis"
// Composable 导入
import { usePagination } from "@@/composables/usePagination"
// 图标导入(按需,只导入实际使用的)
import { CirclePlus, Delete, Download, Refresh, RefreshRight, Search } from "@element-plus/icons-vue"
// 工具导入
import { cloneDeep } from "lodash-es"
```
以下为自动导入,无需手动引入:`ElMessage`、`ElMessageBox`、`ref`、`reactive`、`useTemplateRef`。
## 样式规范
使用 `<style lang="scss" scoped>`,以下为推荐的基础样式:
```scss
.search-wrapper {
margin-bottom: 20px;
:deep(.el-card__body) {
padding-bottom: 2px;
}
}
.toolbar-wrapper {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.table-wrapper {
margin-bottom: 20px;
}
.pager-wrapper {
display: flex;
justify-content: flex-end;
}
```
## 命名约定
模块名为 `product` 时:
| 位置 | 命名 |
|------|------|
| 目录 | `src/pages/.../product/` |
| 组件 name | `Product` |
| API 函数 | `createProductApi` / `deleteProductApi` / `updateProductApi` / `getProductApi` |
| 类型 | `CreateOrUpdateProductRequestData` / `ProductRequestData` / `ProductData` / `ProductResponseData` |
| 命名空间 | `import type * as Product from "./type"` |
## 路由提示
生成代码后,提醒用户在路由配置中添加对应路由。