mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-06 14:33:11 +08:00
feat(skill): 更新 fa-crud-page-generator
This commit is contained in:
@@ -1,72 +1,56 @@
|
|||||||
---
|
---
|
||||||
name: fa-crud-page-generator
|
name: fa-crud-page-generator
|
||||||
description: "为 Fantastic-admin 框架生成完整的 CRUD 业务模块,包含列表页、详情/编辑页、表单组件、API 模块和 Mock 数据。当用户说:'帮我做一个商品管理页面'、'搭一个用户列表,能搜索、能分页、能新增编辑删除'、'生成一个订单模块'、'我需要一个带增删改查的页面'、'快速搭一个管理页面骨架',即使用户只是说'新建一个 XX 管理模块'也应触发此技能。"
|
description: "为 Fantastic-admin 框架生成完整的 CRUD 业务模块,遵循框架标准模块结构和内建 Fa* 组件体系,产出列表页、详情/编辑页、DetailForm 表单组件、API 模块、fake mock 数据、可选路由。用户说:'帮我做一个商品管理页面'、'搭一个用户列表,能搜索分页新增编辑删除'、'生成订单模块'、'新建 XX 管理模块'、'快速搭一个标准模块/CRUD 页面' 时,即使没有明确说 CRUD,也应触发此技能。"
|
||||||
---
|
---
|
||||||
|
|
||||||
# CRUD 页面生成器
|
# CRUD 页面生成器
|
||||||
|
|
||||||
在 Fantastic-admin 框架中生成完整的 CRUD 业务模块,一次性生成所有相关文件,让开发者专注于业务逻辑而非重复代码。
|
在 Fantastic-admin 框架中生成标准 CRUD 业务模块。生成时遵循框架当前标准模块结构:
|
||||||
|
列表页、独立详情页、`DetailForm` 表单组件、API 模块、fake mock、可选路由
|
||||||
|
要一次性配套生成。
|
||||||
|
|
||||||
**生成的文件清单:**
|
优先复刻框架现有写法,而不是重新发明页面结构:列表页用 `FaSearchBar` + `FaTable` +
|
||||||
- `apps/<app>/src/views/{path}/{name}/list.vue` — 列表页(搜索栏 + 表格 + 分页)
|
`FaPagination`,详情表单抽成 `components/DetailForm/index.vue`,独立详情页用
|
||||||
- `apps/<app>/src/views/{path}/{name}/detail.vue` — 详情/编辑页(仅 router 模式需要)
|
`FaFixedBar` 固定顶部标题和底部提交区。
|
||||||
- `apps/<app>/src/views/{path}/{name}/components/DetailForm/index.vue` — 表单组件
|
|
||||||
|
**生成文件清单:**
|
||||||
|
- `apps/<app>/src/views/{path}/{module}/list.vue` — 列表页
|
||||||
|
- `apps/<app>/src/views/{path}/{module}/detail.vue` — 独立详情页,仅 `router` 模式需要
|
||||||
|
- `apps/<app>/src/views/{path}/{module}/components/DetailForm/index.vue` — 新增/编辑表单
|
||||||
- `apps/<app>/src/api/modules/{fileName}.ts` — API 模块
|
- `apps/<app>/src/api/modules/{fileName}.ts` — API 模块
|
||||||
- `apps/<app>/src/api/modules/{fileName}.fake.ts` — Mock 数据(可选)
|
- `apps/<app>/src/api/fake_modules/{fileName}.fake.ts` — fake mock 数据,可选
|
||||||
|
- `apps/<app>/src/router/modules/{routeFileName}.ts` — 前端路由,可选
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 第一步:确认工作区(必须阻塞等待用户回复)
|
## 先确认应用
|
||||||
|
|
||||||
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
|
本项目是 pnpm monorepo。开始写文件前先确认目标应用:
|
||||||
|
|
||||||
1. 执行 `ls apps/` 列出所有可用应用
|
1. 如果用户已经明确写了 `apps/<app>`、`admin 应用`、`pro 应用` 等目标应用,直接使用该应用。
|
||||||
2. **立即向用户提问**,明确询问要在哪个应用中生成 CRUD 模块,并**停止等待回复**
|
2. 如果没有明确目标应用,先执行 `ls apps/` 查看可用应用,然后询问用户要生成到哪个应用,并等待回复。
|
||||||
3. 收到用户明确回复后,才能继续后续步骤
|
3. 后续所有路径都以 `apps/<app>/src/` 为根。
|
||||||
|
|
||||||
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
|
不要默认猜测应用;生成到错误应用的返工成本很高。
|
||||||
|
|
||||||
确认后,后续所有文件路径均以该应用目录为根。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 工作流程
|
## 信息收集
|
||||||
|
|
||||||
### Step 1:收集基本信息
|
尽量一次性收集会影响文件结构的信息:
|
||||||
|
|
||||||
向用户询问以下内容(可合并为一次提问):
|
- **模块名**:英文,用于目录、API 文件和路由,如 `product`、`order`、`systemUser`
|
||||||
|
- **模块中文名**:用于标题和提示,如 `商品`、`订单`、`用户`
|
||||||
|
- **存放路径**:位于 `src/views/` 下的子路径,如 `mall`、`system/user`,可为空
|
||||||
|
- **字段清单**:字段名、中文标签、类型、是否列表显示、是否搜索、是否表单显示、是否必填
|
||||||
|
- **详情展示模式**:`router`、`modal`、`drawer`,默认 `router`
|
||||||
|
- **是否启用批量删除**:默认启用
|
||||||
|
- **是否支持列表高度自适应**:默认生成 `tableAutoHeight` 代码
|
||||||
|
- **是否生成 fake mock**:开发阶段默认推荐生成
|
||||||
|
- **是否生成路由**:仅当前端路由由本地配置维护时生成
|
||||||
|
|
||||||
1. **模块名**(英文,用于文件名和路由,如 `user`、`product`、`order`)
|
如果用户只给出模块名和很少字段,使用一个最小可运行字段:
|
||||||
2. **模块中文名**(用于页面标题和提示文字,如 `用户`、`商品`、`订单`)
|
`title`(标题,`string`,列表/搜索/表单显示,必填),并在完成后提醒用户继续替换业务字段。
|
||||||
3. **存放路径**(在 `apps/<app>/src/views/` 下的子目录,如 `system`、`mall/product`,留空则直接放在 `src/views/` 下)
|
|
||||||
|
|
||||||
### Step 2:收集字段信息
|
|
||||||
|
|
||||||
询问用户该模块有哪些字段,每个字段需要:
|
|
||||||
- **字段名**(英文,如 `name`、`status`、`createdAt`)
|
|
||||||
- **中文标签**(如 `姓名`、`状态`、`创建时间`)
|
|
||||||
- **字段类型**(`string` | `number` | `boolean` | `date`)
|
|
||||||
- **是否在列表中显示**
|
|
||||||
- **是否在搜索栏中显示**
|
|
||||||
- **是否在表单中显示**(新增/编辑)
|
|
||||||
- **是否必填**(表单验证)
|
|
||||||
|
|
||||||
如果用户没有提供字段信息,使用默认字段 `title`(标题,string,必填)作为示例占位,并在生成后提示用户替换。
|
|
||||||
|
|
||||||
### Step 3:收集功能选项
|
|
||||||
|
|
||||||
以下三个选项会直接影响生成的文件结构,需要在同一轮提问中一起确认,避免后续重复生成:
|
|
||||||
|
|
||||||
4. **详情展示模式**:
|
|
||||||
- `router` — 跳转到独立的详情页(默认,适合复杂表单)
|
|
||||||
- `modal` — 弹窗(适合简单表单)
|
|
||||||
- `drawer` — 抽屉(适合中等复杂度表单)
|
|
||||||
5. **是否生成 Mock 数据**(是/否,开发阶段推荐开启)
|
|
||||||
6. **是否同时生成路由配置**(是/否,仅 `app.routeBaseOn` 为 `frontend` 时有效;选是时需额外询问归属的主导航分组名称)
|
|
||||||
|
|
||||||
### Step 4:确认并生成
|
|
||||||
|
|
||||||
汇总信息,展示将要生成的文件列表,确认后写入文件。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -74,38 +58,58 @@ description: "为 Fantastic-admin 框架生成完整的 CRUD 业务模块,包
|
|||||||
|
|
||||||
给定模块名 `name` 和路径 `path`(相对于 `apps/<app>/src/views/`):
|
给定模块名 `name` 和路径 `path`(相对于 `apps/<app>/src/views/`):
|
||||||
|
|
||||||
| 用途 | 规则 | 示例(path=system, name=user) |
|
| 用途 | 规则 | 示例(path 为空,name=product) |
|
||||||
|------|------|-------------------------------|
|
|------|------|----------------------------------------|
|
||||||
| 视图目录 | `apps/<app>/src/views/{path}/{snakeCase(name)}/` | `apps/<app>/src/views/system/user/` |
|
| 视图目录 | `src/views/{path}/{snake_case(name)}/` | `src/views/product/` |
|
||||||
| API 文件名 | path 非空时:`{camelCase(path)}_{camelCase(name)}`;path 为空时:`{camelCase(name)}` | `systemUser` |
|
| API 文件名 | path 参与业务命名时使用 camelCase;普通模块用 `camelCase(name)` | `product` |
|
||||||
| 组件名 | `PascalCase({path}-{name}-list/detail)` | `SystemUserList` |
|
| fake 文件名 | 与 API 文件名一致,放到 `src/api/fake_modules/` | `product.fake.ts` |
|
||||||
| API URL 前缀 | `/{path}/{snakeCase(name)}/` | `/system/user/` |
|
| 列表组件名 | `PascalCase({path}-{name}-list)` | `ProductList` |
|
||||||
|
| 详情组件名 | `PascalCase({path}-{name}-detail)` | `ProductDetail` |
|
||||||
|
| 路由 name | `camelCase(name)List` / `camelCase(name)Detail` | `productList` |
|
||||||
|
| API URL 前缀 | `snake_case(path/name)`,无前导斜杠 | `product` |
|
||||||
|
|
||||||
**snakeCase 规则**:多个单词用下划线连接,如 `productCategory` → `product_category`
|
`snake_case` 用下划线连接多个单词,如 `productCategory` -> `product_category`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 代码模板
|
## 代码生成规则
|
||||||
|
|
||||||
详细的代码模板见 [references/templates.md](references/templates.md),其中也包含 faker.js 字段类型映射表。
|
详细模板见 [references/templates.md](references/templates.md)。生成时按这些规则填充模板:
|
||||||
|
|
||||||
生成代码时,根据用户提供的字段信息替换模板中的占位符:
|
- `list.vue` 使用 `<script setup lang="ts">`,导入 `type { TableColumn } from '@fantastic-admin/components'`。
|
||||||
- 列表页的 `ElTableColumn` 根据"列表显示"字段生成
|
- 表格列使用 `computed<TableColumn<Item>[]>(...)` 定义,页面模板使用 `FaTable` 的 `:columns` 与 `#cell-xxx` 插槽。
|
||||||
- 搜索栏的 `ElFormItem` 根据"搜索显示"字段生成
|
- 搜索区使用 `FaSearchBar` + `FaLabel`,按钮保持“重置 / 筛选 / 展开收起”的顺序。
|
||||||
- 表单组件的 `ElFormItem` 根据"表单显示"字段生成
|
- 工具栏通过 `FaTable` 的 `#toolbar` 插槽放“新增”和批量操作。
|
||||||
- API 模块的 TypeScript 类型根据字段定义生成
|
- 行操作用 `FaButton size="icon-sm"` 编辑按钮和 `FaDropdown` 删除菜单。
|
||||||
- Mock 数据根据字段类型使用合适的 faker.js 方法生成(见 templates.md 中的映射表)
|
- 删除确认统一使用 `useFaModal().confirm`,提示成功统一使用 `useFaToast().success`。
|
||||||
|
- `router` 模式在列表页通过 `eventBus.on('get-data-list')` 接收详情页提交后的刷新事件;卸载时 `eventBus.off('get-data-list')`。
|
||||||
|
- `modal` / `drawer` 模式通过 `useFaModal().create` 或 `useFaDrawer().create` 渲染 `DetailForm`,confirm 时调用表单暴露的 `submit()`,成功后刷新列表并关闭。
|
||||||
|
- `DetailForm` 使用 Element Plus `FormInstance` / `FormRules`,通过 `useTemplateRef<FormInstance>('formRef')` 获取表单引用,并 `defineExpose({ submit() { ... } })` 给外部调用。
|
||||||
|
- fake mock 文件放在 `apps/<app>/src/api/fake_modules/`,不是 `api/modules/`。
|
||||||
|
- API 模块开启 mock 时在每个请求配置中设置 `fake: true`;不生成 fake 时去掉 `fake: true`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 路由生成(可选)
|
## 路由生成
|
||||||
|
|
||||||
如果用户选择同时生成路由,先读取 `apps/<app>/src/settings.ts` 确认 `app.routeBaseOn === 'frontend'`,然后使用 `fa-route-generator` 技能生成对应路由配置:
|
如果用户选择生成路由,优先调用或参考 `fa-route-generator`,并遵循最新标准模块路由模式:
|
||||||
- **router 模式**:生成列表路由 + 详情路由(详情路由带 `menu: false` 和 `activeMenu` 指向列表)
|
|
||||||
- **modal/drawer 模式**:只生成列表路由
|
- 父级路由指向 `Layout`,`meta.title` 使用 `$t('route.<module>.root')`,设置合适图标。
|
||||||
|
- 列表子路由 `path: ''`,`name: '<module>List'`,组件指向 `list.vue`。
|
||||||
|
- 当父级路由只作为菜单入口、列表页作为默认子页时,列表路由使用 `meta.menu: false` 和 `breadcrumb: false`;业务模块如需列表作为独立菜单项,可按用户需求保留菜单。
|
||||||
|
- `router` 模式详情路由为 `detail/:id?`,`meta.menu: false`,`activeMenu` 指向列表/父菜单路径。
|
||||||
|
- 为避免列表和详情标签页状态混乱,列表路由设置 `keepAlive: '<module>Detail'`,详情路由设置 `keepAlive: true` 与 `noKeepAlive: '<module>List'`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 生成后提示用户
|
## 生成前后检查
|
||||||
|
|
||||||
文件生成完成后简要告知:如果未生成路由,可用 `fa-route-generator` 技能添加;生成了 `.fake.ts` 则 API 请求会自动走 mock;表单字段类型(Select、DatePicker 等)需根据业务自行替换。
|
写文件前展示将生成/修改的文件列表,确认不会覆盖用户已有业务文件。若目标文件已存在,先读取并说明将如何合并。
|
||||||
|
|
||||||
|
完成后简要说明:
|
||||||
|
- 已生成哪些文件;
|
||||||
|
- 若未生成路由,提示可继续用 `fa-route-generator` 添加;
|
||||||
|
- 若生成 fake mock,说明请求已配置 `fake: true`;
|
||||||
|
- 若字段是枚举、状态、日期、上传等复杂类型,提醒用户按业务替换控件和 mock 取值。
|
||||||
|
|
||||||
|
不要修改 `packages/components/` 内建组件;确实需要改内建组件时先征得用户确认。
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
# CRUD 代码模板
|
# CRUD 代码模板
|
||||||
|
|
||||||
以下模板基于 `src/views/pages_example/manager` 真实模块提炼,使用以下占位符:
|
生成业务模块时优先保持结构一致,再按字段和业务控件做替换。
|
||||||
|
|
||||||
- `{cname}` — 模块中文名,如 `用户`
|
## 占位符
|
||||||
- `{componentNameList}` — 列表页组件名(PascalCase),如 `SystemUserList`
|
|
||||||
- `{componentNameDetail}` — 详情页组件名(PascalCase),如 `SystemUserDetail`
|
- `{cname}` — 模块中文名,如 `商品`
|
||||||
- `{fileName}` — API 文件名(camelCase),如 `systemUser`
|
- `{moduleName}` — 模块英文名 camelCase,如 `product`
|
||||||
- `{apiPrefix}` — API URL 前缀(无前导斜杠),如 `system/user`
|
- `{ModuleName}` — 模块英文名 PascalCase,如 `Product`
|
||||||
- `{routeListName}` — 列表路由 name,如 `systemUserList`
|
- `{fileName}` — API 文件名,如 `product`
|
||||||
- `{routeDetailName}` — 详情路由 name,如 `systemUserDetail`(仅 router 模式)
|
- `{apiPrefix}` — API URL 前缀,无前导斜杠,如 `product`
|
||||||
|
- `{componentNameList}` — 列表页组件名,如 `ProductList`
|
||||||
|
- `{componentNameDetail}` — 详情页组件名,如 `ProductDetail`
|
||||||
|
- `{routeListName}` — 列表路由 name,如 `productList`
|
||||||
|
- `{routeDetailName}` — 详情路由 name,如 `productDetail`
|
||||||
|
- `{routeRootName}` — 父级路由 name,如 `product`
|
||||||
|
- `{routePath}` — 父级路由 path,如 `/product`
|
||||||
|
- `{routeTitle}` — 路由标题,如 `商品管理`
|
||||||
|
- `{formMode}` — `router`、`modal` 或 `drawer`
|
||||||
|
- `{firstField}` — 删除确认中展示的主字段,如 `title`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -16,7 +25,8 @@
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api{FileName} from '@/api/modules/{fileName}'
|
import type { TableColumn } from '@fantastic-admin/components'
|
||||||
|
import api{ModuleName} from '@/api/modules/{fileName}'
|
||||||
import eventBus from '@/utils/eventBus'
|
import eventBus from '@/utils/eventBus'
|
||||||
import DetailForm from './components/DetailForm/index.vue'
|
import DetailForm from './components/DetailForm/index.vue'
|
||||||
|
|
||||||
@@ -24,8 +34,13 @@ defineOptions({
|
|||||||
name: '{componentNameList}',
|
name: '{componentNameList}',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
interface {ModuleName}Item {
|
||||||
|
id: number
|
||||||
|
{itemTypeFields}
|
||||||
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { pagination, getParams, onSizeChange, onCurrentChange, onSortChange } = usePagination()
|
const { pagination, getParams, onSizeChange, onCurrentChange } = usePagination()
|
||||||
|
|
||||||
// 表格是否自适应高度
|
// 表格是否自适应高度
|
||||||
const tableAutoHeight = ref(false)
|
const tableAutoHeight = ref(false)
|
||||||
@@ -40,7 +55,7 @@ const formMode = ref<'router' | 'modal' | 'drawer'>('{formMode}')
|
|||||||
|
|
||||||
// 详情
|
// 详情
|
||||||
const formModeProps = ref({
|
const formModeProps = ref({
|
||||||
id: '',
|
id: '' as number | string,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
@@ -55,12 +70,30 @@ function searchReset() {
|
|||||||
// 批量操作
|
// 批量操作
|
||||||
const batch = ref({
|
const batch = ref({
|
||||||
enable: true,
|
enable: true,
|
||||||
selectionDataList: [],
|
selectionDataList: [] as {ModuleName}Item[],
|
||||||
})
|
})
|
||||||
|
|
||||||
// 列表
|
// 列表
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const dataList = ref([])
|
const dataList = ref<{ModuleName}Item[]>([])
|
||||||
|
|
||||||
|
const tableColumns = computed<TableColumn<{ModuleName}Item>[]>(() => [
|
||||||
|
...(batch.value.enable
|
||||||
|
? [{
|
||||||
|
type: 'selection',
|
||||||
|
fixed: 'left',
|
||||||
|
width: 48,
|
||||||
|
} satisfies TableColumn<{ModuleName}Item>]
|
||||||
|
: []),
|
||||||
|
{tableColumns}
|
||||||
|
{
|
||||||
|
id: 'operation',
|
||||||
|
header: '操作',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getDataList()
|
getDataList()
|
||||||
@@ -83,7 +116,7 @@ function getDataList() {
|
|||||||
...getParams(),
|
...getParams(),
|
||||||
{searchParams}
|
{searchParams}
|
||||||
}
|
}
|
||||||
api{FileName}.list(params).then((res: any) => {
|
api{ModuleName}.list(params).then((res: any) => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
dataList.value = res.data.list
|
dataList.value = res.data.list
|
||||||
pagination.value.total = res.data.total
|
pagination.value.total = res.data.total
|
||||||
@@ -100,11 +133,6 @@ function currentChange(page = 1) {
|
|||||||
onCurrentChange(page).then(() => getDataList())
|
onCurrentChange(page).then(() => getDataList())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 字段排序
|
|
||||||
function sortChange({ prop, order }: { prop: string, order: string }) {
|
|
||||||
onSortChange(prop, order).then(() => getDataList())
|
|
||||||
}
|
|
||||||
|
|
||||||
const formRef = ref<InstanceType<typeof DetailForm>>()
|
const formRef = ref<InstanceType<typeof DetailForm>>()
|
||||||
|
|
||||||
const { open: openModal, update: updateModal } = useFaModal().create({
|
const { open: openModal, update: updateModal } = useFaModal().create({
|
||||||
@@ -153,46 +181,79 @@ const { open: openDrawer, update: updateDrawer } = useFaDrawer().create({
|
|||||||
|
|
||||||
function onCreate() {
|
function onCreate() {
|
||||||
if (formMode.value === 'router') {
|
if (formMode.value === 'router') {
|
||||||
router.push({ name: '{routeDetailName}' })
|
router.push({
|
||||||
|
name: '{routeDetailName}',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
formModeProps.value.id = ''
|
formModeProps.value.id = ''
|
||||||
if (formMode.value === 'modal') {
|
if (formMode.value === 'modal') {
|
||||||
updateModal({ title: '新增{cname}' })
|
updateModal({
|
||||||
|
title: '新增{cname}',
|
||||||
|
})
|
||||||
openModal()
|
openModal()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
updateDrawer({ title: '新增{cname}' })
|
updateDrawer({
|
||||||
|
title: '新增{cname}',
|
||||||
|
})
|
||||||
openDrawer()
|
openDrawer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEdit(row: any) {
|
function onEdit(row: {ModuleName}Item) {
|
||||||
if (formMode.value === 'router') {
|
if (formMode.value === 'router') {
|
||||||
router.push({ name: '{routeDetailName}', params: { id: row.id } })
|
router.push({
|
||||||
|
name: '{routeDetailName}',
|
||||||
|
params: {
|
||||||
|
id: row.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
formModeProps.value.id = row.id
|
formModeProps.value.id = row.id
|
||||||
if (formMode.value === 'modal') {
|
if (formMode.value === 'modal') {
|
||||||
updateModal({ title: '编辑{cname}' })
|
updateModal({
|
||||||
|
title: '编辑{cname}',
|
||||||
|
})
|
||||||
openModal()
|
openModal()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
updateDrawer({ title: '编辑{cname}' })
|
updateDrawer({
|
||||||
|
title: '编辑{cname}',
|
||||||
|
})
|
||||||
openDrawer()
|
openDrawer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDel(row: any) {
|
function onDel(row: {ModuleName}Item) {
|
||||||
useFaModal().confirm({
|
useFaModal().confirm({
|
||||||
title: '确认信息',
|
title: '确认信息',
|
||||||
content: `确认删除「${row.{firstField}}」吗?`,
|
content: `确认删除「${row.{firstField}}」吗?`,
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
api{FileName}.delete(row.id).then(() => {
|
api{ModuleName}.delete(row.id).then(() => {
|
||||||
getDataList()
|
getDataList()
|
||||||
faToast.success('删除成功')
|
useFaToast().success('删除成功')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBatchDel() {
|
||||||
|
const rows = batch.value.selectionDataList
|
||||||
|
if (!rows.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
useFaModal().confirm({
|
||||||
|
title: '确认信息',
|
||||||
|
content: `确认删除选中的 ${rows.length} 条数据吗?`,
|
||||||
|
onConfirm: () => {
|
||||||
|
Promise.all(rows.map(row => api{ModuleName}.delete(row.id))).then(() => {
|
||||||
|
getDataList()
|
||||||
|
useFaToast().success('批量删除成功')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -206,7 +267,7 @@ function onDel(row: any) {
|
|||||||
<FaSearchBar :show-toggle="false">
|
<FaSearchBar :show-toggle="false">
|
||||||
<template #default="{ fold, toggle }">
|
<template #default="{ fold, toggle }">
|
||||||
<div class="gap-x-8 gap-y-2 grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))]">
|
<div class="gap-x-8 gap-y-2 grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))]">
|
||||||
{fields.search}
|
{searchFields}
|
||||||
<div class="flex gap-2 col-end--1 justify-end">
|
<div class="flex gap-2 col-end--1 justify-end">
|
||||||
<FaButton variant="outline" @click="searchReset(); currentChange()">
|
<FaButton variant="outline" @click="searchReset(); currentChange()">
|
||||||
重置
|
重置
|
||||||
@@ -223,51 +284,72 @@ function onDel(row: any) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FaSearchBar>
|
</FaSearchBar>
|
||||||
<div class="mx--4 my-4 border-t border-t-dashed" />
|
<div class="mx--4 my-3 border-t border-t-dashed" />
|
||||||
<div class="flex-center-between gap-2">
|
<FaTable
|
||||||
<FaButton v-if="batch.enable" variant="outline" :disabled="!batch.selectionDataList.length">
|
v-loading="loading"
|
||||||
批量操作
|
table-root-class="rounded-lg overflow-hidden"
|
||||||
</FaButton>
|
:class="{ 'min-h-0 flex-1': tableAutoHeight }"
|
||||||
<FaButton @click="onCreate">
|
row-key="id"
|
||||||
<FaIcon name="i-ri:add-line" />
|
selectable
|
||||||
新增
|
multiple
|
||||||
</FaButton>
|
stripe
|
||||||
</div>
|
column-visibility
|
||||||
<ElTable v-loading="loading" class="my-4" :data="dataList" stripe highlight-current-row border height="100%" @sort-change="sortChange" @selection-change="batch.selectionDataList = $event">
|
border
|
||||||
<ElTableColumn v-if="batch.enable" type="selection" align="center" fixed />
|
:columns="tableColumns"
|
||||||
{fields.list}
|
:data="dataList"
|
||||||
<ElTableColumn label="操作" width="120" align="center" fixed="right">
|
@selection-change="batch.selectionDataList = $event"
|
||||||
<template #default="scope">
|
>
|
||||||
<div class="flex-center gap-2">
|
<template #toolbar>
|
||||||
<FaButton variant="outline" size="icon-sm" @click="onEdit(scope.row)">
|
<div class="flex flex-1 gap-2 items-center">
|
||||||
<FaIcon name="i-ri:edit-line" />
|
<FaButton @click="onCreate">
|
||||||
|
新增
|
||||||
|
</FaButton>
|
||||||
|
<FaDropdown
|
||||||
|
v-if="batch.enable"
|
||||||
|
:items="[
|
||||||
|
[
|
||||||
|
{ label: '批量删除', variant: 'destructive', disabled: !batch.selectionDataList.length, handle: onBatchDel },
|
||||||
|
],
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<FaButton variant="outline" :disabled="!batch.selectionDataList.length">
|
||||||
|
批量操作
|
||||||
|
<FaIcon name="i-ep:arrow-down" />
|
||||||
</FaButton>
|
</FaButton>
|
||||||
<FaDropdown
|
</FaDropdown>
|
||||||
:items="[
|
</div>
|
||||||
[
|
</template>
|
||||||
{ label: '删除', variant: 'destructive', handle: () => onDel(scope.row) },
|
{tableCellSlots}
|
||||||
],
|
<template #cell-operation="{ row }">
|
||||||
]"
|
<div class="flex-center gap-2">
|
||||||
>
|
<FaButton variant="outline" size="icon-sm" @click="onEdit(row.original)">
|
||||||
<FaButton variant="outline" size="icon-sm">
|
<FaIcon name="i-ri:edit-line" />
|
||||||
<FaIcon name="i-ri:more-line" />
|
</FaButton>
|
||||||
</FaButton>
|
<FaDropdown
|
||||||
</FaDropdown>
|
:items="[
|
||||||
</div>
|
[
|
||||||
</template>
|
{ label: '删除', variant: 'destructive', handle: () => onDel(row.original) },
|
||||||
</ElTableColumn>
|
],
|
||||||
</ElTable>
|
]"
|
||||||
<FaPagination :page="pagination.page" :size="pagination.size" :total="pagination.total" @page-change="currentChange" @size-change="sizeChange" />
|
>
|
||||||
|
<FaButton variant="outline" size="icon-sm">
|
||||||
|
<FaIcon name="i-ri:more-line" />
|
||||||
|
</FaButton>
|
||||||
|
</FaDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FaTable>
|
||||||
|
<FaPagination :page="pagination.page" :size="pagination.size" :total="pagination.total" class="mt-2" @page-change="currentChange" @size-change="sizeChange" />
|
||||||
</FaPageMain>
|
</FaPageMain>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
> 注意:`list.vue` 不需要 `<style scoped>` 块,所有样式均通过 UnoCSS 工具类实现。
|
业务页面保持简洁标题即可,不要在 `FaPageHeader #description` 中加入解释技能或模板来源的说明文字。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## detail.vue 模板(仅 router 模式)
|
## detail.vue 模板(router 模式)
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -281,6 +363,15 @@ defineOptions({
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const appPage = useAppPage()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
appPage.setCustomTitle(route.params.id ? '编辑' : '新增')
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
appPage.resetCustomTitle()
|
||||||
|
})
|
||||||
|
|
||||||
const formRef = useTemplateRef('formRef')
|
const formRef = useTemplateRef('formRef')
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
@@ -331,7 +422,7 @@ function onCancel() {
|
|||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import api{FileName} from '@/api/modules/{fileName}'
|
import api{ModuleName} from '@/api/modules/{fileName}'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id?: number | string
|
id?: number | string
|
||||||
@@ -347,10 +438,10 @@ const loading = ref(false)
|
|||||||
const formRef = useTemplateRef<FormInstance>('formRef')
|
const formRef = useTemplateRef<FormInstance>('formRef')
|
||||||
const form = ref({
|
const form = ref({
|
||||||
id: props.id,
|
id: props.id,
|
||||||
{fields.formInit}
|
{formInitialValues}
|
||||||
})
|
})
|
||||||
const formRules = ref<FormRules>({
|
const formRules = ref<FormRules>({
|
||||||
{fields.rules}
|
{formRules}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -361,9 +452,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
function getInfo() {
|
function getInfo() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
api{FileName}.detail(form.value.id).then((res: any) => {
|
api{ModuleName}.detail(form.value.id).then((res: any) => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
{fields.formAssign}
|
{formAssign}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
@@ -375,14 +466,14 @@ defineExpose({
|
|||||||
formRef.value?.validate((valid) => {
|
formRef.value?.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (form.value.id === '') {
|
if (form.value.id === '') {
|
||||||
api{FileName}.create(form.value).then(() => {
|
api{ModuleName}.create(form.value).then(() => {
|
||||||
faToast.success('新增成功')
|
useFaToast().success('新增成功')
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
api{FileName}.edit(form.value).then(() => {
|
api{ModuleName}.edit(form.value).then(() => {
|
||||||
faToast.success('编辑成功')
|
useFaToast().success('编辑成功')
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -396,7 +487,7 @@ defineExpose({
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="loading">
|
<div v-loading="loading">
|
||||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px" label-suffix=":">
|
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px" label-suffix=":">
|
||||||
{fields.form}
|
{formFields}
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -404,7 +495,7 @@ defineExpose({
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## api.ts 模板
|
## api/modules/{fileName}.ts 模板
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import api from '../index'
|
import api from '../index'
|
||||||
@@ -420,7 +511,9 @@ export default {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
detail: (id: number | string) => api.get('{apiPrefix}/detail', {
|
detail: (id: number | string) => api.get('{apiPrefix}/detail', {
|
||||||
params: { id },
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
fake: true,
|
fake: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -432,27 +525,30 @@ export default {
|
|||||||
fake: true,
|
fake: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: (id: number | string) => api.post('{apiPrefix}/delete', { id }, {
|
delete: (id: number | string) => api.post('{apiPrefix}/delete', {
|
||||||
|
id,
|
||||||
|
}, {
|
||||||
fake: true,
|
fake: true,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> 不需要 Mock 数据时,去掉所有 `fake: true` 选项即可。
|
不生成 fake mock 时,去掉所有 `fake: true` 配置,并确保接口地址与真实后端约定一致。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## fake.ts 模板
|
## api/fake_modules/{fileName}.fake.ts 模板
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { faker } from '@faker-js/faker/locale/zh_CN'
|
import { faker } from '@faker-js/faker'
|
||||||
import { defineFakeRoute } from 'vite-plugin-fake-server/client'
|
import { defineFakeRoute } from 'vite-plugin-fake-server/client'
|
||||||
|
|
||||||
|
// {cname}
|
||||||
const {moduleName}List: any[] = []
|
const {moduleName}List: any[] = []
|
||||||
for (let i = 0; i < 50; i++) {
|
for (let i = 0; i < 50; i++) {
|
||||||
{moduleName}List.push({
|
{moduleName}List.push({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
{fields.mock}
|
{mockFields}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,8 +558,9 @@ export default defineFakeRoute([
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
response: ({ query }) => {
|
response: ({ query }) => {
|
||||||
const { {searchQueryFields}, from, limit } = query
|
const { {searchQueryFields}, from, limit } = query
|
||||||
let list = {moduleName}List
|
const list = {moduleName}List.filter((item) => {
|
||||||
{searchFilters}
|
return {searchFilterExpression}
|
||||||
|
})
|
||||||
const pageList = list.filter((_item, index) => {
|
const pageList = list.filter((_item, index) => {
|
||||||
return index >= ~~from && index < (~~from + ~~limit)
|
return index >= ~~from && index < (~~from + ~~limit)
|
||||||
})
|
})
|
||||||
@@ -493,74 +590,197 @@ export default defineFakeRoute([
|
|||||||
url: '/fake/{apiPrefix}/create',
|
url: '/fake/{apiPrefix}/create',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
response: () => {
|
response: () => {
|
||||||
return { error: '', status: 1, data: { isSuccess: true } }
|
return {
|
||||||
|
error: '',
|
||||||
|
status: 1,
|
||||||
|
data: {
|
||||||
|
isSuccess: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: '/fake/{apiPrefix}/edit',
|
url: '/fake/{apiPrefix}/edit',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
response: () => {
|
response: () => {
|
||||||
return { error: '', status: 1, data: { isSuccess: true } }
|
return {
|
||||||
|
error: '',
|
||||||
|
status: 1,
|
||||||
|
data: {
|
||||||
|
isSuccess: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: '/fake/{apiPrefix}/delete',
|
url: '/fake/{apiPrefix}/delete',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
response: () => {
|
response: () => {
|
||||||
return { error: '', status: 1, data: { isSuccess: true } }
|
return {
|
||||||
|
error: '',
|
||||||
|
status: 1,
|
||||||
|
data: {
|
||||||
|
isSuccess: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果有多个搜索字段,可以用 `&&` 组合过滤条件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
return (title ? item.title.includes(title) : true)
|
||||||
|
&& (status !== undefined && status !== '' ? item.status === ~~status : true)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## router/modules/{routeFileName}.ts 模板
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
function Layout() {
|
||||||
|
return import('@/layouts/index.vue')
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw = {
|
||||||
|
path: '{routePath}',
|
||||||
|
component: Layout,
|
||||||
|
name: '{routeRootName}',
|
||||||
|
meta: {
|
||||||
|
title: '{routeTitle}',
|
||||||
|
icon: '{icon}',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '{routeListName}',
|
||||||
|
component: () => import('@/views/{viewPath}/list.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '{cname}列表',
|
||||||
|
menu: false,
|
||||||
|
breadcrumb: false,
|
||||||
|
keepAlive: '{routeDetailName}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'detail/:id?',
|
||||||
|
name: '{routeDetailName}',
|
||||||
|
component: () => import('@/views/{viewPath}/detail.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '...',
|
||||||
|
menu: false,
|
||||||
|
activeMenu: '{routePath}',
|
||||||
|
keepAlive: true,
|
||||||
|
noKeepAlive: '{routeListName}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default routes
|
||||||
|
```
|
||||||
|
|
||||||
|
`modal` / `drawer` 模式不需要详情路由,列表路由也不需要 `keepAlive: '{routeDetailName}'`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 字段生成规则
|
## 字段生成规则
|
||||||
|
|
||||||
|
### TypeScript item 字段
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
title: string
|
||||||
|
status: number
|
||||||
|
createdAt: string
|
||||||
|
```
|
||||||
|
|
||||||
|
### 搜索默认值和查询参数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const searchDefault = {
|
||||||
|
title: '',
|
||||||
|
status: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
...getParams(),
|
||||||
|
...(search.value.title && { title: search.value.title }),
|
||||||
|
...(search.value.status !== '' && { status: search.value.status }),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 搜索栏字段
|
### 搜索栏字段
|
||||||
|
|
||||||
每个搜索字段用 `FaLabel` 包裹,内部控件根据字段类型自行选择合适的 Fa* 或 El* 组件。次要搜索条件加 `v-show="!fold"` 实现折叠。
|
首个主要搜索字段直接显示,次要搜索条件使用 `v-show="!fold"`:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<FaLabel label="{label}" class="col-span-1">
|
<FaLabel label="{label}" class="col-span-1">
|
||||||
<!-- 内部放对应的输入控件,string 用 FaInput,枚举用 FaSelect,日期用 ElDatePicker 等 -->
|
<FaInput
|
||||||
|
v-model="search.{field}"
|
||||||
|
placeholder="请输入{label},支持模糊查询"
|
||||||
|
clearable
|
||||||
|
class="w-full"
|
||||||
|
@keydown.enter="currentChange()"
|
||||||
|
@clear="currentChange()"
|
||||||
|
/>
|
||||||
</FaLabel>
|
</FaLabel>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 列表列
|
### FaTable 列
|
||||||
|
|
||||||
普通列直接用 `prop` 渲染,需要自定义展示时使用 `#default` slot:
|
普通字段:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
accessorKey: '{field}',
|
||||||
|
header: '{label}',
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
需要自定义展示的字段使用 `id`,并在模板里添加 `#cell-{id}` 插槽:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: '状态',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- 普通列 -->
|
<template #cell-status="{ row }">
|
||||||
<ElTableColumn prop="{field}" label="{label}" />
|
<FaBadge :variant="row.original.status ? 'default' : 'secondary'">
|
||||||
|
{{ row.original.status ? '启用' : '停用' }}
|
||||||
<!-- 需要自定义渲染时 -->
|
</FaBadge>
|
||||||
<ElTableColumn prop="{field}" label="{label}" width="100" align="center">
|
</template>
|
||||||
<template #default="scope">
|
|
||||||
<!-- 根据字段含义选择合适的展示方式,如 ElTag、FaSwitch 等 -->
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 表单字段
|
### 表单字段
|
||||||
|
|
||||||
每个表单字段用 `ElFormItem` 包裹,内部控件根据字段类型自行选择合适的 Fa* 或 El* 组件,注意加 `class="w-full"`。
|
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<ElFormItem label="{label}" prop="{field}">
|
<ElFormItem label="{label}" prop="{field}">
|
||||||
<!-- 内部放对应的输入控件 -->
|
<FaInput v-model="form.{field}" placeholder="请输入{label}" class="w-full" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 表单验证规则
|
### 表单验证规则
|
||||||
|
|
||||||
|
文本输入:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 必填文本
|
|
||||||
{field}: [
|
{field}: [
|
||||||
{ required: true, message: '请输入{label}', trigger: 'blur' },
|
{ required: true, message: '请输入{label}', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
// 必填选择
|
```
|
||||||
|
|
||||||
|
选择、开关、日期:
|
||||||
|
|
||||||
|
```typescript
|
||||||
{field}: [
|
{field}: [
|
||||||
{ required: true, message: '请选择{label}', trigger: 'change' },
|
{ required: true, message: '请选择{label}', trigger: 'change' },
|
||||||
],
|
],
|
||||||
@@ -568,47 +788,30 @@ export default defineFakeRoute([
|
|||||||
|
|
||||||
### Mock 数据字段映射
|
### Mock 数据字段映射
|
||||||
|
|
||||||
| 字段类型 | faker 方法 |
|
| 字段类型/含义 | faker 方法 |
|
||||||
|---------|-----------|
|
|---------------|------------|
|
||||||
| 姓名 | `faker.person.fullName()` |
|
| 标题 | `faker.book.title()` 或 `faker.lorem.words(3)` |
|
||||||
| 账号/用户名 | `faker.person.firstName()` |
|
| 名称 | `faker.person.fullName()` 或 `faker.commerce.productName()` |
|
||||||
| 手机号 | `faker.phone.number({ style: 'international' })` |
|
| 用户名 | `faker.internet.username()` |
|
||||||
|
| 手机号 | `faker.phone.number()` |
|
||||||
| 邮箱 | `faker.internet.email()` |
|
| 邮箱 | `faker.internet.email()` |
|
||||||
| 标题/名称 | `faker.lorem.words(3)` |
|
|
||||||
| 描述 | `faker.lorem.sentence()` |
|
| 描述 | `faker.lorem.sentence()` |
|
||||||
| 数字(枚举) | `faker.number.int(2)` |
|
| 数量 | `faker.number.int({ min: 1, max: 100 })` |
|
||||||
| 布尔(状态) | `faker.datatype.boolean()` |
|
|
||||||
| 金额 | `faker.number.float({ min: 10, max: 9999, fractionDigits: 2 })` |
|
| 金额 | `faker.number.float({ min: 10, max: 9999, fractionDigits: 2 })` |
|
||||||
|
| 布尔状态 | `faker.datatype.boolean()` |
|
||||||
|
| 枚举状态 | `faker.helpers.arrayElement([0, 1, 2])` |
|
||||||
| 日期 | `faker.date.recent().toISOString()` |
|
| 日期 | `faker.date.recent().toISOString()` |
|
||||||
|
|
||||||
### Mock 搜索过滤模式
|
### Mock 搜索过滤
|
||||||
|
|
||||||
|
字符串模糊匹配:
|
||||||
|
|
||||||
**string 模糊匹配:**
|
|
||||||
```typescript
|
```typescript
|
||||||
list = list.filter((item) => {
|
title ? item.title.includes(title) : true
|
||||||
return {field} ? item.{field}.includes({field}) : true
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**枚举/数字精确匹配(注意:query 参数均为字符串,`0` 是 falsy,不能直接用 `{field} ?` 判断):**
|
数字/枚举精确匹配。注意 query 参数是字符串,`0` 不能用普通 truthy 判断:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
list = list.filter((item) => {
|
status !== undefined && status !== '' ? item.status === ~~status : true
|
||||||
return {field} !== undefined && {field} !== '' ? item.{field} === ~~{field} : true
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## faker.js 字段类型映射
|
|
||||||
|
|
||||||
| 字段类型 | 推荐 faker 方法 |
|
|
||||||
|---------|----------------|
|
|
||||||
| string(名称类) | `faker.person.fullName()` 或 `faker.commerce.productName()` |
|
|
||||||
| string(标题类) | `faker.lorem.words(3)` |
|
|
||||||
| string(描述类) | `faker.lorem.sentence()` |
|
|
||||||
| number(ID) | 自增 `i + 1` |
|
|
||||||
| number(金额) | `faker.number.float({ min: 10, max: 9999, fractionDigits: 2 })` |
|
|
||||||
| number(数量) | `faker.number.int({ min: 1, max: 100 })` |
|
|
||||||
| boolean | `faker.datatype.boolean()` |
|
|
||||||
| date | `faker.date.recent().toISOString()` |
|
|
||||||
| status(枚举) | `faker.helpers.arrayElement([0, 1, 2])` |
|
|
||||||
|
|||||||
Reference in New Issue
Block a user