From 29193e47e2181bacc76bcd4516d1c049ea1a717e Mon Sep 17 00:00:00 2001 From: Hooray <304327508@qq.com> Date: Sat, 27 Jun 2026 17:31:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20FaForm=20=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E7=BB=84=E4=BB=B6=E5=8F=8A=E7=A4=BA=E4=BE=8B=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=99=BB=E5=BD=95=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/fa-crud-page-generator/SKILL.md | 147 +++-- .../references/templates.md | 618 +++++++++++++----- .agents/skills/fa-form-builder/SKILL.md | 17 +- .../fa-form-builder/references/templates.md | 228 +++---- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../AppAccountForm/edit-password.vue | 111 ++-- .../src/components/AppAccountForm/login.vue | 118 ++-- .../components/AppAccountForm/register.vue | 117 ++-- .../AppAccountForm/reset-password.vue | 110 ++-- .../src/router/modules/component.example.ts | 8 + apps/example/src/types/components.d.ts | 4 + .../src/views/component_example/form.vue | 17 + packages/components/package.json | 1 + packages/components/resolver.ts | 2 + .../src/basic/checkbox/CheckboxGroup.vue | 3 +- .../components/src/basic/form/FormItem.vue | 312 +++++++++ packages/components/src/basic/form/README.md | 68 ++ .../src/basic/form/_examples/_async.vue | 74 +++ .../src/basic/form/_examples/_basic.vue | 68 ++ .../src/basic/form/_examples/_composite.vue | 90 +++ .../src/basic/form/_examples/_disabled.vue | 64 ++ .../src/basic/form/_examples/_dynamic.vue | 113 ++++ .../src/basic/form/_examples/_layout.vue | 60 ++ .../src/basic/form/_examples/_methods.vue | 84 +++ .../src/basic/form/_examples/_slot.vue | 66 ++ .../src/basic/form/_examples/index.ts | 59 ++ packages/components/src/basic/form/context.ts | 13 + .../src/basic/form/form/FormControl.vue | 17 + .../src/basic/form/form/FormDescription.vue | 21 + .../src/basic/form/form/FormItem.vue | 23 + .../src/basic/form/form/FormLabel.vue | 25 + .../src/basic/form/form/FormMessage.vue | 33 + .../components/src/basic/form/form/index.ts | 7 + .../src/basic/form/form/injectionKeys.ts | 3 + .../src/basic/form/form/useFormField.ts | 31 + packages/components/src/basic/form/index.ts | 5 + packages/components/src/basic/form/index.vue | 319 +++++++++ .../src/basic/radio-group/index.vue | 3 +- packages/components/src/examples.ts | 1 + packages/components/src/index.ts | 2 + pnpm-lock.yaml | 3 + skills/fa-crud-page-generator/SKILL.md | 13 +- .../references/templates.md | 151 +++-- skills/fa-form-builder/SKILL.md | 17 +- .../fa-form-builder/references/templates.md | 228 +++---- 72 files changed, 4021 insertions(+), 2645 deletions(-) create mode 100644 apps/example/src/views/component_example/form.vue create mode 100644 packages/components/src/basic/form/FormItem.vue create mode 100644 packages/components/src/basic/form/README.md create mode 100644 packages/components/src/basic/form/_examples/_async.vue create mode 100644 packages/components/src/basic/form/_examples/_basic.vue create mode 100644 packages/components/src/basic/form/_examples/_composite.vue create mode 100644 packages/components/src/basic/form/_examples/_disabled.vue create mode 100644 packages/components/src/basic/form/_examples/_dynamic.vue create mode 100644 packages/components/src/basic/form/_examples/_layout.vue create mode 100644 packages/components/src/basic/form/_examples/_methods.vue create mode 100644 packages/components/src/basic/form/_examples/_slot.vue create mode 100644 packages/components/src/basic/form/_examples/index.ts create mode 100644 packages/components/src/basic/form/context.ts create mode 100644 packages/components/src/basic/form/form/FormControl.vue create mode 100644 packages/components/src/basic/form/form/FormDescription.vue create mode 100644 packages/components/src/basic/form/form/FormItem.vue create mode 100644 packages/components/src/basic/form/form/FormLabel.vue create mode 100644 packages/components/src/basic/form/form/FormMessage.vue create mode 100644 packages/components/src/basic/form/form/index.ts create mode 100644 packages/components/src/basic/form/form/injectionKeys.ts create mode 100644 packages/components/src/basic/form/form/useFormField.ts create mode 100644 packages/components/src/basic/form/index.ts create mode 100644 packages/components/src/basic/form/index.vue diff --git a/.agents/skills/fa-crud-page-generator/SKILL.md b/.agents/skills/fa-crud-page-generator/SKILL.md index 201b6645..75563660 100644 --- a/.agents/skills/fa-crud-page-generator/SKILL.md +++ b/.agents/skills/fa-crud-page-generator/SKILL.md @@ -1,72 +1,57 @@ --- name: fa-crud-page-generator -description: "为 Fantastic-admin 框架生成完整的 CRUD 业务模块,包含列表页、详情/编辑页、表单组件、API 模块和 Mock 数据。当用户说:'帮我做一个商品管理页面'、'搭一个用户列表,能搜索、能分页、能新增编辑删除'、'生成一个订单模块'、'我需要一个带增删改查的页面'、'快速搭一个管理页面骨架',即使用户只是说'新建一个 XX 管理模块'也应触发此技能。" +description: "为 Fantastic-admin 框架生成完整的 CRUD 业务模块,遵循框架标准模块结构和内建 Fa* 组件体系,使用 FaForm/FaFormItem 生成新增/编辑表单,产出列表页、详情/编辑页、DetailForm 表单组件、API 模块、fake mock 数据、可选路由。用户说:'帮我做一个商品管理页面'、'搭一个用户列表,能搜索分页新增编辑删除'、'生成订单模块'、'新建 XX 管理模块'、'快速搭一个标准模块/CRUD 页面' 时,即使没有明确说 CRUD,也应触发此技能。" --- # CRUD 页面生成器 -在 Fantastic-admin 框架中生成完整的 CRUD 业务模块,一次性生成所有相关文件,让开发者专注于业务逻辑而非重复代码。 +在 Fantastic-admin 框架中生成标准 CRUD 业务模块。生成时遵循框架当前标准模块结构: +列表页、独立详情页、`DetailForm` 表单组件、API 模块、fake mock、可选路由和 route i18n +要一次性配套生成。 -**生成的文件清单:** -- `apps//src/views/{path}/{name}/list.vue` — 列表页(搜索栏 + 表格 + 分页) -- `apps//src/views/{path}/{name}/detail.vue` — 详情/编辑页(仅 router 模式需要) -- `apps//src/views/{path}/{name}/components/DetailForm/index.vue` — 表单组件 +优先复刻框架现有写法,而不是重新发明页面结构:列表页用 `FaSearchBar` + `FaTable` + +`FaPagination`,详情表单抽成 `components/DetailForm/index.vue`,独立详情页用 +`FaFixedBar` 固定顶部标题和底部提交区。 + +**生成文件清单:** +- `apps//src/views/{path}/{module}/list.vue` — 列表页 +- `apps//src/views/{path}/{module}/detail.vue` — 独立详情页,仅 `router` 模式需要 +- `apps//src/views/{path}/{module}/components/DetailForm/index.vue` — 新增/编辑表单 - `apps//src/api/modules/{fileName}.ts` — API 模块 -- `apps//src/api/modules/{fileName}.fake.ts` — Mock 数据(可选) +- `apps//src/api/fake_modules/{fileName}.fake.ts` — fake mock 数据,可选 +- `apps//src/router/modules/{routeFileName}.ts` — 前端路由,可选 +- `apps//src/locales/lang/route/{locale}.json` — 路由标题 i18n,可选 --- -## 第一步:确认工作区(必须阻塞等待用户回复) +## 先确认应用 -本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用: +本项目是 pnpm monorepo。开始写文件前先确认目标应用: -1. 执行 `ls apps/` 列出所有可用应用 -2. **立即向用户提问**,明确询问要在哪个应用中生成 CRUD 模块,并**停止等待回复** -3. 收到用户明确回复后,才能继续后续步骤 +1. 如果用户已经明确写了 `apps/`、`admin 应用`、`pro 应用` 等目标应用,直接使用该应用。 +2. 如果没有明确目标应用,先执行 `ls apps/` 查看可用应用,然后询问用户要生成到哪个应用,并等待回复。 +3. 后续所有路径都以 `apps//src/` 为根。 -> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。 - -确认后,后续所有文件路径均以该应用目录为根。 +不要默认猜测应用;生成到错误应用的返工成本很高。 --- -## 工作流程 +## 信息收集 -### Step 1:收集基本信息 +尽量一次性收集会影响文件结构的信息: -向用户询问以下内容(可合并为一次提问): +- **模块名**:英文,用于目录、API 文件和路由,如 `product`、`order`、`systemUser` +- **模块中文名**:用于标题和提示,如 `商品`、`订单`、`用户` +- **存放路径**:位于 `src/views/` 下的子路径,如 `mall`、`system/user`,可为空 +- **字段清单**:字段名、中文标签、类型、是否列表显示、是否搜索、是否表单显示、是否必填 +- **详情展示模式**:`router`、`modal`、`drawer`,默认 `router` +- **是否启用批量删除**:默认启用 +- **是否支持列表高度自适应**:默认生成 `tableAutoHeight` 代码 +- **是否生成 fake mock**:开发阶段默认推荐生成 +- **是否生成路由和 route i18n**:仅当前端路由由本地配置维护时生成 -1. **模块名**(英文,用于文件名和路由,如 `user`、`product`、`order`) -2. **模块中文名**(用于页面标题和提示文字,如 `用户`、`商品`、`订单`) -3. **存放路径**(在 `apps//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:确认并生成 - -汇总信息,展示将要生成的文件列表,确认后写入文件。 +如果用户只给出模块名和很少字段,使用一个最小可运行字段: +`title`(标题,`string`,列表/搜索/表单显示,必填),并在完成后提醒用户继续替换业务字段。 --- @@ -74,38 +59,60 @@ description: "为 Fantastic-admin 框架生成完整的 CRUD 业务模块,包 给定模块名 `name` 和路径 `path`(相对于 `apps//src/views/`): -| 用途 | 规则 | 示例(path=system, name=user) | -|------|------|-------------------------------| -| 视图目录 | `apps//src/views/{path}/{snakeCase(name)}/` | `apps//src/views/system/user/` | -| API 文件名 | path 非空时:`{camelCase(path)}_{camelCase(name)}`;path 为空时:`{camelCase(name)}` | `systemUser` | -| 组件名 | `PascalCase({path}-{name}-list/detail)` | `SystemUserList` | -| API URL 前缀 | `/{path}/{snakeCase(name)}/` | `/system/user/` | +| 用途 | 规则 | 示例(path 为空,name=product) | +|------|------|----------------------------------------| +| 视图目录 | `src/views/{path}/{snake_case(name)}/` | `src/views/product/` | +| API 文件名 | path 参与业务命名时使用 camelCase;普通模块用 `camelCase(name)` | `product` | +| fake 文件名 | 与 API 文件名一致,放到 `src/api/fake_modules/` | `product.fake.ts` | +| 列表组件名 | `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)。生成时按这些规则填充模板: -生成代码时,根据用户提供的字段信息替换模板中的占位符: -- 列表页的 `ElTableColumn` 根据"列表显示"字段生成 -- 搜索栏的 `ElFormItem` 根据"搜索显示"字段生成 -- 表单组件的 `ElFormItem` 根据"表单显示"字段生成 -- API 模块的 TypeScript 类型根据字段定义生成 -- Mock 数据根据字段类型使用合适的 faker.js 方法生成(见 templates.md 中的映射表) +- `list.vue` 使用 ` ``` --- -## api.ts 模板 +## api/modules/{fileName}.ts 模板 ```typescript import api from '../index' @@ -420,7 +537,9 @@ export default { }), detail: (id: number | string) => api.get('{apiPrefix}/detail', { - params: { id }, + params: { + id, + }, fake: true, }), @@ -432,27 +551,30 @@ export default { fake: true, }), - delete: (id: number | string) => api.post('{apiPrefix}/delete', { id }, { + delete: (id: number | string) => api.post('{apiPrefix}/delete', { + id, + }, { fake: true, }), } ``` -> 不需要 Mock 数据时,去掉所有 `fake: true` 选项即可。 +不生成 fake mock 时,去掉所有 `fake: true` 配置,并确保接口地址与真实后端约定一致。 --- -## fake.ts 模板 +## api/fake_modules/{fileName}.fake.ts 模板 ```typescript -import { faker } from '@faker-js/faker/locale/zh_CN' +import { faker } from '@faker-js/faker' import { defineFakeRoute } from 'vite-plugin-fake-server/client' +// {cname} const {moduleName}List: any[] = [] for (let i = 0; i < 50; i++) { {moduleName}List.push({ id: i + 1, - {fields.mock} + {mockFields} }) } @@ -462,8 +584,9 @@ export default defineFakeRoute([ method: 'get', response: ({ query }) => { const { {searchQueryFields}, from, limit } = query - let list = {moduleName}List - {searchFilters} + const list = {moduleName}List.filter((item) => { + return {searchFilterExpression} + }) const pageList = list.filter((_item, index) => { return index >= ~~from && index < (~~from + ~~limit) }) @@ -493,122 +616,257 @@ export default defineFakeRoute([ url: '/fake/{apiPrefix}/create', method: 'post', response: () => { - return { error: '', status: 1, data: { isSuccess: true } } + return { + error: '', + status: 1, + data: { + isSuccess: true, + }, + } }, }, { url: '/fake/{apiPrefix}/edit', method: 'post', response: () => { - return { error: '', status: 1, data: { isSuccess: true } } + return { + error: '', + status: 1, + data: { + isSuccess: true, + }, + } }, }, { url: '/fake/{apiPrefix}/delete', method: 'post', 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' +import { $t } from '@/locales' + +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 - + ``` -### 列表列 +### FaTable 列 -普通列直接用 `prop` 渲染,需要自定义展示时使用 `#default` slot: +普通字段: + +```typescript +{ + accessorKey: '{field}', + header: '{label}', +}, +``` + +需要自定义展示的字段使用 `id`,并在模板里添加 `#cell-{id}` 插槽: + +```typescript +{ + id: 'status', + header: '状态', + width: 100, + align: 'center', +}, +``` ```vue - - - - - - - + ``` ### 表单字段 -每个表单字段用 `ElFormItem` 包裹,内部控件根据字段类型自行选择合适的 Fa* 或 El* 组件,注意加 `class="w-full"`。 - ```vue - - - + + + ``` -### 表单验证规则 +`FaFormItem` 会自动向第一个子控件注入 `modelValue` 和 `onUpdate:modelValue`,不要再手写 +`v-model="model.{field}"` 或 `v-model="model.value.{field}"`。 + +### 表单 model 类型字段 ```typescript -// 必填文本 -{field}: [ - { required: true, message: '请输入{label}', trigger: 'blur' }, -], -// 必填选择 -{field}: [ - { required: true, message: '请选择{label}', trigger: 'change' }, -], +{field}: string +status: number +enabled: boolean ``` +### 表单初始值 + +```typescript +{field}: '', +status: 0, +enabled: true, +``` + +### 表单详情赋值 + +```typescript +model.value.{field} = res.data.{field} +``` + +### 表单验证 schema + +文本输入: + +```typescript +{field}(value: string) { + return value?.trim() ? true : '请输入{label}' +}, +``` + +选择、开关、日期: + +```typescript +{field}(value: string | number | boolean | undefined) { + return value !== undefined && value !== '' ? true : '请选择{label}' +}, +``` + +非必填字段不要生成 validation schema 项;生成必填字段时,在对应 `FaFormItem` 上同时加 +`required`。 + ### Mock 数据字段映射 -| 字段类型 | faker 方法 | -|---------|-----------| -| 姓名 | `faker.person.fullName()` | -| 账号/用户名 | `faker.person.firstName()` | -| 手机号 | `faker.phone.number({ style: 'international' })` | +| 字段类型/含义 | faker 方法 | +|---------------|------------| +| 标题 | `faker.book.title()` 或 `faker.lorem.words(3)` | +| 名称 | `faker.person.fullName()` 或 `faker.commerce.productName()` | +| 用户名 | `faker.internet.username()` | +| 手机号 | `faker.phone.number()` | | 邮箱 | `faker.internet.email()` | -| 标题/名称 | `faker.lorem.words(3)` | | 描述 | `faker.lorem.sentence()` | -| 数字(枚举) | `faker.number.int(2)` | -| 布尔(状态) | `faker.datatype.boolean()` | +| 数量 | `faker.number.int({ min: 1, max: 100 })` | | 金额 | `faker.number.float({ min: 10, max: 9999, fractionDigits: 2 })` | +| 布尔状态 | `faker.datatype.boolean()` | +| 枚举状态 | `faker.helpers.arrayElement([0, 1, 2])` | | 日期 | `faker.date.recent().toISOString()` | -### Mock 搜索过滤模式 +### Mock 搜索过滤 + +字符串模糊匹配: -**string 模糊匹配:** ```typescript -list = list.filter((item) => { - return {field} ? item.{field}.includes({field}) : true -}) +title ? item.title.includes(title) : true ``` -**枚举/数字精确匹配(注意:query 参数均为字符串,`0` 是 falsy,不能直接用 `{field} ?` 判断):** +数字/枚举精确匹配。注意 query 参数是字符串,`0` 不能用普通 truthy 判断: + ```typescript -list = list.filter((item) => { - return {field} !== undefined && {field} !== '' ? item.{field} === ~~{field} : true -}) +status !== undefined && status !== '' ? item.status === ~~status : 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])` | diff --git a/.agents/skills/fa-form-builder/SKILL.md b/.agents/skills/fa-form-builder/SKILL.md index fbd97ca4..b48222fe 100755 --- a/.agents/skills/fa-form-builder/SKILL.md +++ b/.agents/skills/fa-form-builder/SKILL.md @@ -1,11 +1,11 @@ --- name: fa-form-builder -description: "为 Fantastic-admin 框架生成独立的表单页面,使用 vee-validate + zod 验证,全部使用框架内建 Fa* 组件。当用户说:'帮我做一个用户信息填写页'、'我只需要一个提交表单,不需要列表'、'做个设置页面,有几个输入框和保存按钮'、'生成一个注册/编辑/配置表单页'、'只要表单页,不需要增删改查',即使用户只是说'做个表单页面'也应触发此技能。" +description: "为 Fantastic-admin 框架生成独立的表单页面,使用 FaForm/FaFormItem + vee-validate + zod 验证,表单 model 使用 ref,全部使用框架内建 Fa* 组件。当用户说:'帮我做一个用户信息填写页'、'我只需要一个提交表单,不需要列表'、'做个设置页面,有几个输入框和保存按钮'、'生成一个注册/编辑/配置表单页'、'只要表单页,不需要增删改查',即使用户只是说'做个表单页面'也应触发此技能。" --- # 表单页面生成器 -在 Fantastic-admin 框架中生成独立的 Router 表单页面,使用 vee-validate + zod 完成表单验证,全部使用框架内建 Fa* 组件,不引入任何 Element Plus 组件。 +在 Fantastic-admin 框架中生成独立的 Router 表单页面,使用 `FaForm/FaFormItem` 承接 vee-validate + zod 表单验证。 **生成的文件:** - `apps//src/views/{path}/{name}/index.vue` — 表单页面(含验证、提交骨架、固定操作栏) @@ -50,14 +50,14 @@ description: "为 Fantastic-admin 框架生成独立的表单页面,使用 vee 字段较多时单列会导致页面过长,双列更紧凑。根据字段数量和复杂度判断是否询问用户布局偏好: -- **单列**:`max-w-600px`,`space-y-6`(字段少、字段较长时适合) +- **单列**:`max-w-600px`,`grid gap-6`(字段少、字段较长时适合) - **双列**:`max-w-1200px`,`grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2`(字段多、字段较短时适合) ### Step 4:确认并生成 汇总信息,展示将要生成的文件,确认后写入。 -生成完成后提示:如需配置路由,请使用 `fa-route-generator` 技能;如需 API 模块,请手动创建。 +生成完成后提示:如需配置路由,请使用 `fa-route-generator` 技能;如需 API 模块,请使用 `fa-crud-page-generator` 或手动创建。 --- @@ -80,9 +80,10 @@ description: "为 Fantastic-admin 框架生成独立的表单页面,使用 vee - `{cname}` → 模块中文名 - `{componentName}` → PascalCase 组件名 - `{zodSchema}` → zod 字段定义(每个必填字段对应一行 zod 规则) -- `{initialValues}` → 字段初始值(string 默认 `''`,boolean 默认 `false`,number 默认 `0`,array 默认 `[]`) -- `{formItems}` → 各字段对应的 FormField 代码片段 +- `{initialValues}` → `model` 字段初始值(string 默认 `''`,boolean 默认 `false`,number 默认 `0`,array 默认 `[]`) +- `{formItems}` → 各字段对应的 FaFormItem 代码片段 +- `{imports}` → 必要的额外 import;默认留空,Fa* 组件通常由自动导入处理 - `{maxWidth}` → 单列 `max-w-600px` / 双列 `max-w-1200px` -- `{gridClass}` → 双列时 `grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2` / 单列时 `space-y-6` +- `{gridClass}` → 双列时 `grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2` / 单列时 `grid gap-6` -生成的代码是骨架:API 调用处用 `// TODO:` 注释标记,动态数据源(select options、upload action 等)用占位注释标记,用户根据实际接口替换。操作栏按钮使用 `FaButton`:取消用 `variant="outline"`,提交用默认 variant 并传 `:loading="isSubmitting"`。 +生成的代码是骨架:API 调用处用 `// TODO:` 注释标记,动态数据源(select options、upload action 等)用占位注释标记,用户根据实际接口替换。表单对象必须使用 `const model = ref({...})`,模板中直接 `:model="model"`;脚本里读写字段使用 `model.value.xxx`。操作栏按钮使用 `FaButton`:取消用 `variant="outline"`,提交用默认 variant 并传 `:loading="loading"`。 diff --git a/.agents/skills/fa-form-builder/references/templates.md b/.agents/skills/fa-form-builder/references/templates.md index 2bc3d242..7d1ef15c 100755 --- a/.agents/skills/fa-form-builder/references/templates.md +++ b/.agents/skills/fa-form-builder/references/templates.md @@ -1,16 +1,16 @@ # 表单页面代码模板 -使用 vee-validate + zod 验证,全部使用 Fa* 内建组件,不引入任何 Element Plus 组件。 +使用 FaForm/FaFormItem + vee-validate + zod 验证。 占位符说明: - `{cname}` — 模块中文名 - `{componentName}` — 组件名(PascalCase) - `{zodSchema}` — zod 字段定义 -- `{initialValues}` — 表单初始值 -- `{formItems}` — FormField 列表 +- `{initialValues}` — `model` 表单初始值 +- `{formItems}` — FaFormItem 列表 - `{imports}` — 需要手动 import 的组件 - `{maxWidth}` — 单列 `max-w-600px` / 双列 `max-w-1200px` -- `{gridClass}` — 双列时 `grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2` / 单列时 `space-y-6` +- `{gridClass}` — 双列时 `grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2` / 单列时 `grid gap-6` --- @@ -18,10 +18,10 @@ ```vue diff --git a/apps/core-ant-design-vue/src/components/AppAccountForm/login.vue b/apps/core-ant-design-vue/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/core-ant-design-vue/src/components/AppAccountForm/login.vue +++ b/apps/core-ant-design-vue/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/core-ant-design-vue/src/components/AppAccountForm/register.vue b/apps/core-ant-design-vue/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/core-ant-design-vue/src/components/AppAccountForm/register.vue +++ b/apps/core-ant-design-vue/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-ant-design-vue/src/components/AppAccountForm/reset-password.vue b/apps/core-ant-design-vue/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/core-ant-design-vue/src/components/AppAccountForm/reset-password.vue +++ b/apps/core-ant-design-vue/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-antdv-next/src/components/AppAccountForm/login.vue b/apps/core-antdv-next/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/core-antdv-next/src/components/AppAccountForm/login.vue +++ b/apps/core-antdv-next/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/core-antdv-next/src/components/AppAccountForm/register.vue b/apps/core-antdv-next/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/core-antdv-next/src/components/AppAccountForm/register.vue +++ b/apps/core-antdv-next/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-antdv-next/src/components/AppAccountForm/reset-password.vue b/apps/core-antdv-next/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/core-antdv-next/src/components/AppAccountForm/reset-password.vue +++ b/apps/core-antdv-next/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-arco-design-vue/src/components/AppAccountForm/login.vue b/apps/core-arco-design-vue/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/core-arco-design-vue/src/components/AppAccountForm/login.vue +++ b/apps/core-arco-design-vue/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/core-arco-design-vue/src/components/AppAccountForm/register.vue b/apps/core-arco-design-vue/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/core-arco-design-vue/src/components/AppAccountForm/register.vue +++ b/apps/core-arco-design-vue/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-arco-design-vue/src/components/AppAccountForm/reset-password.vue b/apps/core-arco-design-vue/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/core-arco-design-vue/src/components/AppAccountForm/reset-password.vue +++ b/apps/core-arco-design-vue/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-naive-ui/src/components/AppAccountForm/login.vue b/apps/core-naive-ui/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/core-naive-ui/src/components/AppAccountForm/login.vue +++ b/apps/core-naive-ui/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/core-naive-ui/src/components/AppAccountForm/register.vue b/apps/core-naive-ui/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/core-naive-ui/src/components/AppAccountForm/register.vue +++ b/apps/core-naive-ui/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-naive-ui/src/components/AppAccountForm/reset-password.vue b/apps/core-naive-ui/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/core-naive-ui/src/components/AppAccountForm/reset-password.vue +++ b/apps/core-naive-ui/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-tdesign/src/components/AppAccountForm/login.vue b/apps/core-tdesign/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/core-tdesign/src/components/AppAccountForm/login.vue +++ b/apps/core-tdesign/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/core-tdesign/src/components/AppAccountForm/register.vue b/apps/core-tdesign/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/core-tdesign/src/components/AppAccountForm/register.vue +++ b/apps/core-tdesign/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-tdesign/src/components/AppAccountForm/reset-password.vue b/apps/core-tdesign/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/core-tdesign/src/components/AppAccountForm/reset-password.vue +++ b/apps/core-tdesign/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-vexip-ui/src/components/AppAccountForm/login.vue b/apps/core-vexip-ui/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/core-vexip-ui/src/components/AppAccountForm/login.vue +++ b/apps/core-vexip-ui/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/core-vexip-ui/src/components/AppAccountForm/register.vue b/apps/core-vexip-ui/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/core-vexip-ui/src/components/AppAccountForm/register.vue +++ b/apps/core-vexip-ui/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/core-vexip-ui/src/components/AppAccountForm/reset-password.vue b/apps/core-vexip-ui/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/core-vexip-ui/src/components/AppAccountForm/reset-password.vue +++ b/apps/core-vexip-ui/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ diff --git a/apps/core/src/components/AppAccountForm/login.vue b/apps/core/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/core/src/components/AppAccountForm/login.vue +++ b/apps/core/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/core/src/components/AppAccountForm/register.vue b/apps/core/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/core/src/components/AppAccountForm/register.vue +++ b/apps/core/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/core/src/components/AppAccountForm/reset-password.vue b/apps/core/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/core/src/components/AppAccountForm/reset-password.vue +++ b/apps/core/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ diff --git a/apps/example/src/components/AppAccountForm/login.vue b/apps/example/src/components/AppAccountForm/login.vue index 191b61d9..4a307afd 100755 --- a/apps/example/src/components/AppAccountForm/login.vue +++ b/apps/example/src/components/AppAccountForm/login.vue @@ -1,8 +1,7 @@ @@ -79,48 +86,33 @@ function testAccount(account: string) { />
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - 记住我 - - - - -
- + + + + + + + + + + + +
+ + + 记住我 + + + 忘记密码了?
@@ -129,11 +121,11 @@ function testAccount(account: string) {
还没有帐号? - + 注册新帐号
- +
演示账号一键登录
diff --git a/apps/example/src/components/AppAccountForm/register.vue b/apps/example/src/components/AppAccountForm/register.vue index e6f8fc09..66c84a21 100755 --- a/apps/example/src/components/AppAccountForm/register.vue +++ b/apps/example/src/components/AppAccountForm/register.vue @@ -1,8 +1,6 @@ diff --git a/apps/example/src/components/AppAccountForm/reset-password.vue b/apps/example/src/components/AppAccountForm/reset-password.vue index 7faa6321..2a2e06aa 100755 --- a/apps/example/src/components/AppAccountForm/reset-password.vue +++ b/apps/example/src/components/AppAccountForm/reset-password.vue @@ -1,8 +1,6 @@ + + diff --git a/packages/components/package.json b/packages/components/package.json index 3f802cf3..8afa6acd 100755 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -37,6 +37,7 @@ "filesize": "catalog:", "reka-ui": "catalog:", "tailwind-merge": "catalog:", + "vee-validate": "catalog:", "vue-input-otp": "catalog:", "vue-sonner": "catalog:" } diff --git a/packages/components/resolver.ts b/packages/components/resolver.ts index e8651585..f3baa6df 100755 --- a/packages/components/resolver.ts +++ b/packages/components/resolver.ts @@ -22,6 +22,8 @@ const BASIC_COMPONENT_NAMES = [ 'FaDropdown', 'FaFileUpload', 'FaFixedBar', + 'FaForm', + 'FaFormItem', 'FaHoverCard', 'FaIcon', 'FaImagePreview', diff --git a/packages/components/src/basic/checkbox/CheckboxGroup.vue b/packages/components/src/basic/checkbox/CheckboxGroup.vue index 5878fdda..c44592bf 100755 --- a/packages/components/src/basic/checkbox/CheckboxGroup.vue +++ b/packages/components/src/basic/checkbox/CheckboxGroup.vue @@ -167,11 +167,10 @@ function handleCustomOptionKeydown(option: TOption) { @keydown.space.prevent="handleCustomOptionKeydown(option)" >