first commit

This commit is contained in:
2026-10-26 18:54:09 +08:00
commit 20a594d834
180 changed files with 18496 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
---
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"` |
## 路由提示
生成代码后,提醒用户在路由配置中添加对应路由。