Skip to content

前端运行时

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_ossOSS 上传
upload_cosCOS 上传
upload_chunk分片上传
icon_select图标选择
rich_text富文本编辑器
select_options后端选项选择
remote_cascader远程级联
remote_select远程选择
remote_tree远程树
remote_tree_select远程树选择
remote_api_select远程 API 选择
transferElement Plus Transfer
tree_selectElement 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 会处理相对路径请求:

  • GETPOSTPUTDELETE 使用项目 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(boolstring $ignore = true)`
ignoreWhenHidden()字段隐藏时忽略提交
fetch(...)声明远程数据请求
`fetchOptions(arrayFetch $options)`
`handler(string $event, stringHandler $handlerKey)`
`hook(string $name, stringHandler $handlerKey)`
`computed(string $target, stringHandler $handlerKey)`
`update(stringHandler $handlerKey)`
`injectEvent(boolarray $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-formconfig

  • 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() 会把项目 httpresponse.data 交给 form-create。后端响应结构和组件需要的数据位置要匹配,必要时通过 toparse 指定写入位置。

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' })
}

编辑页面没有回显

编辑态需要传入 primarycatch-form 会通过 api + primary 调用详情接口并写入表单数据。

vue
<Create :config="{ ...form, primary: row?.id }" />

详情接口返回的数据字段需要和 schema 中的字段名一致。级联、树选择、多选这类组件要返回组件期望的值结构。

resetExceptFields 和 emptyFieldsValue 参数怎么理解

这两个方法的参数表示保留字段:

ts
formRef.value?.resetExceptFields('type')
formRef.value?.emptyFieldsValue(['type', 'module'])

上面的代码会保留 typetype/module,处理其他字段。

快速切换远程联动时数据错乱

需要请求竞态保护。内置 roles.parentChanged 已使用最后一次请求结果。新增 handler 涉及异步请求时,建议按相同方式记录 request id,只应用最后一次响应。