| name | v3-create-crud |
| description | 创建增删改查(CRUD)页面,基于 Element Plus 组件库,包含表格、搜索、分页、新增/编辑弹窗、删除确认等功能。当用户提到以下任何场景时都应触发:创建管理页面、创建列表页、创建表格页。即使用户没有明确说 CRUD,只要意图是创建带表格和表单操作的后台页面就应该使用此 Skill。使用时需提供模块名称和字段信息。 |
| metadata | {"author":"Glittering Ma & pany","version":"2026.06.13"} |
创建 CRUD 页面
根据用户提供的模块名称和字段信息,生成增删改查页面。
本 Skill 定义的是默认 CRUD 模式,当用户的实际需求与本 Skill 约定冲突时,以用户需求为准,灵活调整。
输入要求
用户需提供:
- 模块名称(如
product、order)— 用于目录名和命名
- 字段列表 — 每个字段需明确:
- 字段名(英文,camelCase)
- 中文标签
- 类型(string / number / boolean / enum / date 等)
- 是否必填
- 是否作为搜索条件
- 是否需要自定义渲染(如 tag 状态展示)
如果用户信息不完整,主动询问补全后再生成。
生成文件
在 src/pages/<模块路径>/ 下生成:
index.vue — 页面主文件
apis/index.ts — 接口文件
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)
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)
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)
function handleUpdate(row: XxxData) {
dialogVisible.value = true
formData.value = cloneDeep(row)
}
为什么用 cloneDeep:避免编辑时直接修改表格行数据(引用类型),用户取消编辑时表格数据不会被污染。
查(Read)
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 页时直接请求,否则重置页码并由分页监听触发请求,避免重复调用。
分页监听
watchPagination()
为什么用 watchPagination 而不是 onMounted:让分页变化和初始加载共享同一个入口,数据获取逻辑只写一处。
usePagination 用法
import { usePagination } from "@@/composables/usePagination"
const { paginationData, resetCurrentPage, watchPagination } = usePagination({
callback: getTableData
})
paginationData 是 reactive 对象,包含:total、currentPage、pageSizes、pageSize、layout。模板中使用 v-model 双向绑定页码和每页条数:
<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 渲染。思路是:突出重要/异常值,其余用温和颜色兜底。
<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>
<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"
导入规范
import type { CreateOrUpdateXxxRequestData, XxxData } from "./apis/type"
import type { FormRules } from "element-plus"
import { createXxxApi, deleteXxxApi, getXxxApi, updateXxxApi } from "./apis"
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>,以下为推荐的基础样式:
.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" |
路由提示
生成代码后,提醒用户在路由配置中添加对应路由。