主题
前端运行时
catch-form 是后端 schema 到 form-create 的前端运行时。后端负责输出 JSON,前端负责把组件类型、远程请求、事件 key 和提交生命周期转换成真实运行能力。
基本使用
vue
<template>
<catch-form
ref="formRef"
v-model="formData"
:config="config"
:before-submit="beforeSubmit"
:after-submit="afterSubmit"
@ready="onReady"
@submit-success="onSubmitSuccess"
@submit-error="onSubmitError"
@validate-fail="onValidateFail"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type {
BeforeSubmit,
CatchFormConfig,
CatchFormExpose
} from '@/components/catchForm/types'
defineProps<{ config: CatchFormConfig }>()
const formData = ref<Record<string, unknown>>({})
const formRef = ref<CatchFormExpose>()
const beforeSubmit: BeforeSubmit = data => {
const parentId = data.parent_id
if (Array.isArray(parentId)) {
data.parent_id = parentId[0] ?? 0
}
return data
}
const afterSubmit = () => {
formRef.value?.emptyFieldsValue()
}
const onReady = (api?: unknown) => {}
const onSubmitSuccess = (data: Record<string, unknown>) => {}
const onSubmitError = (error: unknown) => {}
const onValidateFail = (error: unknown) => {}
</script>CatchFormConfig 对应后端 schema:
ts
interface CatchFormConfig {
api: string
rules: Rule[]
option: Options
formData?: Record<string, unknown>
primary?: string | number
version?: string
meta?: Record<string, unknown>
}组件注册
后端 schema 的 type 会映射到前端已注册组件。项目实现位于 web/src/components/catchForm/registry.ts。
当前默认注册:
| type | 前端组件 |
|---|---|
upload_image | 图片上传 |
upload_file | 单文件上传 |
upload_files | 多文件上传 |
upload_attach | 附件上传 |
upload_oss | OSS 上传 |
upload_cos | COS 上传 |
upload_chunk | 分片上传 |
icon_select | 图标选择 |
rich_text | 富文本编辑器 |
select_options | 后端选项选择 |
remote_cascader | 远程级联 |
remote_select | 远程选择 |
remote_tree | 远程树 |
remote_tree_select | 远程树选择 |
remote_api_select | 远程 API 选择 |
transfer | Element Plus Transfer |
tree_select | Element Plus TreeSelect |
新增业务组件时,在 registry.ts 注册:
ts
import MySelector from '@/components/admin/MySelector.vue'
export const catchFormComponents = [
{ name: 'my_selector', component: MySelector }
]PHP schema 中使用同名 type:
php
$form->custom('user_id', '用户')->type('my_selector');远程请求
后端通过 fetch() 描述远程数据:
php
$form->tree('permissions', '')
->fetch('permissions/permissions', to: 'props.data', query: ['from' => 'role']);序列化后:
json
{
"effect": {
"fetch": {
"action": "permissions/permissions",
"to": "props.data",
"method": "GET",
"query": {
"from": "role"
}
}
}
}前端 fetch.ts 会处理相对路径请求:
GET、POST、PUT、DELETE使用项目http封装。query会拼接到 URL。data会作为请求体。headers会写入临时 request 实例。/api/users会归一化为users。- 文件上传、进度回调、跨域凭证、绝对 URL 使用 form-create 原始 fetch。
Handler Key
JSON 安全策略使用 handler key 表达命令式逻辑。后端输出稳定 key,前端在 web/src/components/catchForm/handlers.ts 注册实现。
PHP:
php
$form->select('module', '所属模块')
->handler('change', 'permissions.moduleChanged')
->injectEvent();前端:
ts
registerCatchFormHandler('permissions.moduleChanged', ({ api, args }) => {
const [module] = args as [string | undefined]
if (!module) {
return
}
api?.mergeRule('component', { props: { query: { module } } })
})handler context:
ts
interface CatchFormHandlerContext {
kind: 'event' | 'computed' | 'update' | 'hook'
api?: Api
rule?: FormRule[]
self?: Rule
option?: Options
inject?: unknown
formData?: Record<string, unknown>
value?: unknown
updateArg?: unknown
hookArgs?: unknown[]
args: unknown[]
}当前内置 handler:
permissions.moduleChanged:模块变化后刷新权限标识和组件查询参数。roles.parentChanged:父级角色变化后刷新权限树,并使用最后一次请求结果。
规则 DSL
后端可以输出 form-create 支持的 JSON 安全配置。事件、计算属性、更新回调和 hook 都使用 handler key,前端负责注册真实函数。
php
use CatchForm\Support\Handler;
$form->select('user_id', '用户')
->fetch('users/options', to: 'options')
->ignoreWhenHidden()
->handler('change', 'users.changed')
->injectEvent();
$form->text('slug', 'Slug')
->computed('value', Handler::make('posts.slug'));
$form->custom('preview', '预览')
->type('post_preview')
->update('posts.previewUpdated')
->hook('mounted', 'posts.previewMounted');常用方法:
| 方法 | 用途 |
|---|---|
| `ignore(bool | string $ignore = true)` |
ignoreWhenHidden() | 字段隐藏时忽略提交 |
fetch(...) | 声明远程数据请求 |
| `fetchOptions(array | Fetch $options)` |
| `handler(string $event, string | Handler $handlerKey)` |
| `hook(string $name, string | Handler $handlerKey)` |
| `computed(string $target, string | Handler $handlerKey)` |
| `update(string | Handler $handlerKey)` |
| `injectEvent(bool | array $inject = true)` |
事件
catch-form 会向页面抛出以下事件:
| 事件 | 触发时机 |
|---|---|
ready(api) | form-create api 初始化完成 |
submit-success(formData) | 提交成功且 afterSubmit 执行完成 |
submit-error(error) | 提交请求、beforeSubmit 或 afterSubmit 抛出异常 |
validate-fail(error) | form-create 校验失败,或 beforeSubmit 返回 false |
Expose API
通过 ref<CatchFormExpose>() 调用运行时方法:
| 方法 | 用途 |
|---|---|
getForm() | 获取 form-create api |
getFormData() | 获取当前表单数据 |
setFormData(data) | 批量设置表单数据 |
setFieldValue(field, value) | 设置单个字段值 |
setData(key, data) | 写入 form-create data |
getFields() | 获取字段列表 |
resetExceptFields(fields) | 重置指定字段之外的字段 |
emptyFieldsValue(fields) | 清空指定字段之外的字段值 |
refresh() | 刷新 form-create |
常见用法:
ts
const afterSubmit = () => {
formRef.value?.resetExceptFields('type')
formRef.value?.setFieldValue('type', 1)
}提交生命周期
beforeSubmit 用于提交前规整 payload:
ts
const beforeSubmit: BeforeSubmit = data => {
const categoryId = data.category_id
if (Array.isArray(categoryId)) {
data.category_id = categoryId[0] ?? 0
}
return data
}返回值规则:
- 返回对象:使用返回对象提交。
- 返回
void:使用原始表单数据提交。 - 返回
false:中断提交并触发validate-fail。 - 抛出异常或 Promise reject:触发
submit-error。
afterSubmit 用于提交成功后的页面行为,例如清空表单、跳转路由、刷新外部列表。弹窗表单会在提交成功后自动调用注入的 closeDialog。
常见问题
页面空白或表单没有渲染
先检查传入 catch-form 的 config:
rules必须是数组。option必须存在。api必须是提交接口。- 动态页面建议在
loading结束后再渲染catch-form。
vue
<catch-form v-if="!loading" :config="{ ...form, primary: row?.id }" />自定义组件没有显示
后端输出的 type 必须和 registry.ts 中注册的 name 完全一致。
php
$form->custom('user_id', '用户')->type('my_selector');ts
{ name: 'my_selector', component: MySelector }组件需要先在 web/src/components/catchForm/registry.ts 注册,再由 bootstrapFormCreate() 统一安装到 form-create。
远程数据请求没有走项目鉴权
相对路径请求会走项目 http 封装,能自动携带项目内的 token、拦截器和统一错误处理。
php
$form->select('user_id', '用户')->fetch('users/options', to: 'options');绝对 URL、文件上传、上传进度和跨域凭证会使用 form-create 原始 fetch。需要项目鉴权时,优先使用相对路径。
接口路径多了 /api
前端会把 /api/users 归一化为 users,用于适配项目已有 http baseURL。推荐后端 schema 直接输出项目内相对路径:
php
$form->select('user_id', '用户')->fetch('users/options', to: 'options');远程数据返回了,但选项没有显示
fetch() 会把项目 http 的 response.data 交给 form-create。后端响应结构和组件需要的数据位置要匹配,必要时通过 to 或 parse 指定写入位置。
php
$form->tree('permissions', '')
->fetch('permissions/permissions', to: 'props.data', query: ['from' => 'role']);Handler 没有触发
检查三点:
- PHP schema 使用了
handler()。 - 需要表单事件参数时调用了
injectEvent()。 - 前端在
handlers.ts注册了相同 key。
php
$form->select('module', '所属模块')
->handler('change', 'permissions.moduleChanged')
->injectEvent();ts
registerCatchFormHandler('permissions.moduleChanged', handler)JSON 里需要函数怎么办
后端 JSON 保持可序列化,函数逻辑用 handler key 表达。前端本地注册真实函数,实现事件、computed、update、hook 等命令式逻辑。
php
$form->select('module', '所属模块')
->handler('change', 'permissions.moduleChanged');这种方式适合权限、角色、模块联动等业务逻辑。
beforeSubmit 修改了数据但没有生效
beforeSubmit 需要返回修改后的对象,或者直接修改原对象后返回它。
ts
const beforeSubmit: BeforeSubmit = data => {
data.parent_id = Array.isArray(data.parent_id) ? data.parent_id[0] : 0
return data
}返回 false 会中断提交并触发 validate-fail。
提交成功后弹窗没有关闭
弹窗表单依赖页面提供的 closeDialog 注入。列表页通过 catch-table dialog slot 打开表单时会自动提供该能力。
全页面表单传入 is-page="true" 后会保留当前页面,由 afterSubmit 自行处理跳转:
vue
<catch-form :is-page="true" :config="config" :after-submit="afterSubmit" />ts
const afterSubmit = () => {
router.push({ path: '/cms/articles/post' })
}编辑页面没有回显
编辑态需要传入 primary,catch-form 会通过 api + primary 调用详情接口并写入表单数据。
vue
<Create :config="{ ...form, primary: row?.id }" />详情接口返回的数据字段需要和 schema 中的字段名一致。级联、树选择、多选这类组件要返回组件期望的值结构。
resetExceptFields 和 emptyFieldsValue 参数怎么理解
这两个方法的参数表示保留字段:
ts
formRef.value?.resetExceptFields('type')
formRef.value?.emptyFieldsValue(['type', 'module'])上面的代码会保留 type 或 type/module,处理其他字段。
快速切换远程联动时数据错乱
需要请求竞态保护。内置 roles.parentChanged 已使用最后一次请求结果。新增 handler 涉及异步请求时,建议按相同方式记录 request id,只应用最后一次响应。

