npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gopowerteam/form-render

v1.0.1

Published

基于 Vue 3 + Arco Design 的声明式表单渲染库,通过配置化的方式快速构建表单。

Readme

@gopowerteam/form-render

基于 Vue 3 + Arco Design 的声明式表单渲染库,通过配置化的方式快速构建表单。

目录

速查表

导出列表

| 导出名称 | 类型 | 说明 | |----------|------|------| | defineForm | 函数 | 定义表单配置,支持泛型 | | FormRender | 组件 | 表单渲染组件 | | FormRenderInstance | 类型 | 组件实例类型 | | useForm | Hook | 通过 ref 获取表单实例 | | createFormSource | 工具函数 | 手动创建表单数据源 | | FormRenderResolver | 工具函数 | unplugin-vue-components 解析器 | | FormItemsOptions | 类型 | 表单配置数组类型 | | FormItemOptions | 类型 | 表单项配置类型 | | DataRecord | 类型 | 通用数据记录类型 |

FormRender Props 速查

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | form | FormItemsOptions<T> | - | 必填,表单配置数组 | | value | Record<string, any> | - | 初始表单数据(非响应式) | | modelValue | Record<string, any> | - | 初始表单数据(支持 v-model) | | layout | 'horizontal' \| 'vertical' | 'horizontal' | 表单布局模式 | | columns | number | 自动计算 | 固定列数 | | minWidth | number | 400 | 每列最小宽度(px) | | name | string | - | 表单 name 属性 | | id | string | - | 表单 id 属性 | | collapsedMode | 'append' \| 'dialog' | 'append' | 折叠模式 | | collapsedDialogColumns | number | 2 | 弹窗模式列数 | | showFormResult | boolean | dialog 模式为 true | 显示已选条件标签 | | submitable | boolean | false | 显示提交/取消按钮 | | searchable | boolean | false | 显示搜索/重置按钮 | | resetable | boolean | true | 显示重置按钮 | | footer | boolean | false | 操作按钮底部样式 | | rowGap | number | 6 | 行间距(px) | | colGap | number | 12 | 列间距(px) |

FormRender Methods 速查

| 方法 | 签名 | 说明 | |------|------|------| | formSource | Ref<DataRecord> | 响应式表单数据 | | validate | () => Promise<void> | 验证表单 | | reset | () => void | 重置表单 | | updateFormField | (key: string, value: any) => void | 更新单个字段 | | updateFormSource | (value: DataRecord) => void | 更新整个表单 |

FormRender Events 速查

| 事件 | 参数 | 说明 | |------|------|------| | submit | formSource: Record<string, any> | 表单提交时触发 | | cancel | - | 点击取消按钮时触发 | | update:model-value | formSource: Record<string, any> | 表单数据更新时触发 |

字段类型速查

| 类型 | 渲染方法 | 关键配置 | 常用场景 | |------|----------|----------|----------| | 输入框 | r.input() | type, clearable, autoSubmit | 文本/数字输入 | | 文本域 | r.textarea() | autosize, maxLength | 多行文本 | | 选择器 | r.select() | options, multiple, cache | 单选/多选下拉 | | 树选择 | r.treeSelect() | options, treeProps, fieldNames | 树形数据选择 | | 级联选择 | r.cascader() | options, pathMode, multiple | 级联数据选择 | | 单选框 | r.radio() | options, type: 'button' | 单选按钮组 | | 开关 | r.switch() | openValue, closeValue, openLabel | 布尔切换 | | 日期 | r.date() | type, showTime, shortcuts | 日期/时间选择 | | 日期范围 | r.dateRange() | valueFormat, shortcuts | 日期区间选择 | | 金额 | r.currency() | inputUnit, outputUnit, thousands | 金额输入(支持单位转换) | | 自定义 | r.render() | 自定义渲染函数 | 完全自定义字段 |

FormItemOptions 速查

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | key | string | - | 必填,字段键名 | | title | string | - | 必填,字段标签 | | default | any \| (() => any) | - | 默认值,支持函数 | | render | FormItemRender<T> | - | 必填,渲染函数 | | rule | FieldRule \| FieldRule[] | - | 验证规则 | | visiable | boolean \| ((data: T) => boolean) | true | 条件显隐 | | collapsed | boolean | false | 折叠状态下隐藏 | | span | number | 1 | 网格跨列数 | | hideLabel | boolean | false | 隐藏字段标签 | | group | string \| string[] | - | 分组(dialog 模式) | | labelStyle | CSSProperties | - | 标签样式 | | contentStyle | CSSProperties | - | 内容样式 | | mode | 'component' \| 'text' | 'component' | 显示模式 |


快速开始

基础用法

<script setup lang="ts">
import { defineForm, FormRender } from '@gopowerteam/form-render'

interface UserForm {
  name: string
  email: string
  status: string
}

const form = defineForm<UserForm>([
  {
    key: 'name',
    title: '姓名',
    render: r => r.input({ placeholder: '请输入姓名', clearable: true }),
  },
  {
    key: 'email',
    title: '邮箱',
    render: r => r.input({ placeholder: '请输入邮箱' }),
  },
  {
    key: 'status',
    title: '状态',
    render: r => r.select({
      options: new Map([['1', '启用'], ['0', '禁用']]),
      clearable: true,
    }),
  },
])

const handleSubmit = (data: UserForm) => {
  console.log('表单数据:', data)
}
</script>

<template>
  <FormRender
    :form="form"
    searchable
    @submit="handleSubmit"
  />
</template>

defineForm 配置函数

defineForm 是一个类型安全的表单配置函数,支持泛型参数:

interface User {
  name: string
  age: number
}

// 使用泛型获得类型提示
const form = defineForm<User>([
  {
    key: 'name',  // 自动推导为 'name' | 'age'
    title: '姓名',
    render: r => r.input(),
  },
  {
    key: 'age',
    title: '年龄',
    render: r => r.input({ type: 'number' }),
  },
])

表单布局

布局模式

支持 horizontal(水平)和 vertical(垂直)两种布局:

<!-- 水平布局(默认) -->
<FormRender :form="form" layout="horizontal" />

<!-- 垂直布局 -->
<FormRender :form="form" layout="vertical" />

响应式列布局

表单会根据容器宽度自动计算列数:

<!-- 自动计算列数,每列最小宽度 400px -->
<FormRender :form="form" :min-width="400" />

<!-- 固定 2 列 -->
<FormRender :form="form" :columns="2" />

折叠模式

当表单字段较多时,可以使用折叠模式隐藏部分字段:

append 模式(默认)

点击"展开/收起"按钮,在原位置展开更多字段:

<FormRender
  :form="form"
  collapsed-mode="append"
  searchable
/>

dialog 模式

点击"高级搜索"按钮,在弹窗中显示更多字段:

<FormRender
  :form="form"
  collapsed-mode="dialog"
  :collapsed-dialog-columns="2"
  searchable
/>

配置字段为折叠状态:

const form = defineForm([
  {
    key: 'name',
    title: '姓名',
    // 此字段在折叠状态下隐藏
    collapsed: true,
    render: r => r.input(),
  },
])

表单间距

<FormRender
  :form="form"
  :row-gap="6"
  :col-gap="12"
/>

表单字段类型

Input 输入框

interface RenderInputItemOptions {
  placeholder?: string
  clearable?: boolean
  readonly?: boolean
  autoSubmit?: boolean
  type?: 'string' | 'number'
}

基础用法:

const form = defineForm([
  {
    key: 'name',
    title: '姓名',
    render: r => r.input({ placeholder: '请输入姓名' }),
  },
])

数字输入:

{
  key: 'age',
  title: '年龄',
  render: r => r.input({ type: 'number', placeholder: '请输入年龄' }),
}

自动提交:

{
  key: 'keyword',
  title: '关键词',
  render: r => r.input({ autoSubmit: true, clearable: true }),
}

Textarea 文本域

interface RenderTextareaItemOptions {
  placeholder?: string
  rows?: number
  resize?: 'none' | 'both' | 'horizontal' | 'vertical'
  autosize?: { minRows: number, maxRows: number }
  maxLength?: number
}

基础用法:

const form = defineForm([
  {
    key: 'description',
    title: '描述',
    render: r => r.textarea({ placeholder: '请输入描述' }),
  },
])

自适应高度:

{
  key: 'content',
  title: '内容',
  render: r => r.textarea({
    autosize: { minRows: 3, maxRows: 6 },
    maxLength: 500,
  }),
}

Select 选择器

interface RenderSelectItemOptions {
  placeholder?: string
  clearable?: boolean
  searchable?: boolean
  createable?: boolean
  options: SelectOptions | (() => SelectOptions) | (() => Promise<SelectOptions>) | Ref<SelectOptions>
  multiple?: boolean
  maxTagCount?: number
  default?: string | number | boolean
  autoSubmit?: boolean
  cache?: boolean
  onChange?: (value) => void
}

type SelectOptions = Map<string | number | boolean, string>

静态选项:

const form = defineForm([
  {
    key: 'status',
    title: '状态',
    render: r => r.select({
      options: new Map([['1', '启用'], ['0', '禁用']]),
      clearable: true,
    }),
  },
])

多选:

{
  key: 'tags',
  title: '标签',
  render: r => r.select({
    multiple: true,
    options: new Map([['vue', 'Vue'], ['react', 'React'], ['angular', 'Angular']]),
  }),
}

异步选项:

const loadOptions = () => {
  return Promise.resolve(new Map([['a', '选项A'], ['b', '选项B']]))
}

{
  key: 'category',
  title: '分类',
  render: r => r.select({
    options: loadOptions,
    cache: true,  // 开启缓存,避免重复请求
  }),
}

响应式选项:

import { ref } from 'vue'

const options = ref(new Map([['a', '选项A']]))

// 动态更新选项
setTimeout(() => {
  options.value = new Map([['b', '选项B'], ['c', '选项C']])
}, 3000)

{
  key: 'type',
  title: '类型',
  render: r => r.select({ options }),
}

TreeSelect 树选择

interface RenderTreeSelectItemOptions {
  placeholder?: string
  clearable?: boolean
  searchable?: boolean
  fieldNames?: TreeFieldNames
  options: TreeNodeData[] | (() => ...) | Ref<...>
  multiple?: boolean
  maxTagCount?: number
  default?: string | number | boolean
  autoSubmit?: boolean
  cache?: boolean
  treeProps?: Partial<TreeProps>
  treeCheckStrictly?: boolean
  treeCheckedStrategy?: 'all' | 'parent' | 'child'
  slots?: Record<string, (data?: TreeNodeData) => JSX.Element>
}

基础用法:

const form = defineForm([
  {
    key: 'department',
    title: '部门',
    render: r => r.treeSelect({
      options: [
        {
          key: '1',
          title: '技术部',
          children: [
            { key: '1-1', title: '前端组' },
            { key: '1-2', title: '后端组' },
          ],
        },
        {
          key: '2',
          title: '产品部',
        },
      ],
    }),
  },
])

自定义字段映射:

{
  key: 'org',
  title: '组织',
  render: r => r.treeSelect({
    fieldNames: { key: 'id', title: 'name', children: 'children' },
    options: [{ id: '1', name: '组织A', children: [] }],
  }),
}

使用插槽:

{
  key: 'category',
  title: '分类',
  render: r => r.treeSelect({
    options: treeOptions,
    slots: {
      'tree-slot-title': (data) => <div>{data.title}</div>,
      'footer': () => <div>自定义底部</div>,
    },
  }),
}

Cascader 级联选择

interface RenderCascaderItemOptions {
  placeholder?: string
  clearable?: boolean
  searchable?: boolean
  pathMode?: boolean
  options: CascaderOption[] | (() => ...) | Ref<...>
  multiple?: boolean
  maxTagCount?: number
  default?: string | number | boolean
  autoSubmit?: boolean
  checkStrictly?: boolean
  triggerProps?: TriggerProps
  cache?: boolean
}

基础用法:

const form = defineForm([
  {
    key: 'region',
    title: '地区',
    render: r => r.cascader({
      options: [
        {
          value: 'beijing',
          label: '北京',
          children: [
            { value: 'chaoyang', label: '朝阳区' },
            { value: 'haidian', label: '海淀区' },
          ],
        },
        {
          value: 'shanghai',
          label: '上海',
          children: [
            { value: 'pudong', label: '浦东新区' },
          ],
        },
      ],
    }),
  },
])

路径模式:

{
  key: 'address',
  title: '地址',
  render: r => r.cascader({
    pathMode: true,  // 返回路径数组 ['beijing', 'chaoyang']
    multiple: true,
    options: addressOptions,
  }),
}

异步加载:

const loadOptions = () => {
  return Promise.resolve([
    { value: '1', label: '选项1', children: [] },
  ])
}

{
  key: 'category',
  title: '分类',
  render: r => r.cascader({
    options: loadOptions,
    cache: true,
  }),
}

Radio 单选框

interface RenderRadioItemOptions {
  options: RadioOptions | (() => RadioOptions) | Ref<RadioOptions>
  type?: 'radio' | 'button'
  size?: 'mini' | 'small' | 'medium' | 'large'
  default?: string | number
  autoSubmit?: boolean
  cache?: boolean
  onChange?: (value: string | number) => void
}

type RadioOptions = Map<string | number, string>

基础用法:

const form = defineForm([
  {
    key: 'gender',
    title: '性别',
    render: r => r.radio({
      options: new Map([['male', '男'], ['female', '女']]),
    }),
  },
])

按钮样式:

{
  key: 'status',
  title: '状态',
  render: r => r.radio({
    type: 'button',
    size: 'large',
    options: new Map([['1', '启用'], ['0', '禁用']]),
    autoSubmit: true,
  }),
}

Switch 开关

interface RenderSwitchItemOptions {
  size?: 'small' | 'medium'
  openLabel?: string       // 默认: '是'
  closeLabel?: string      // 默认: '否'
  openValue?: string | number | boolean   // 默认: true
  closeValue?: string | number | boolean  // 默认: false
  default?: string | number | boolean
  autoSubmit?: boolean
}

基础用法:

const form = defineForm([
  {
    key: 'enabled',
    title: '启用',
    default: false,
    render: r => r.switch(),
  },
])

自定义值和标签:

{
  key: 'status',
  title: '状态',
  render: r => r.switch({
    openLabel: '开启',
    closeLabel: '关闭',
    openValue: 1,
    closeValue: 0,
  }),
}

Date 日期选择

interface RenderDateItemOptions {
  placeholder?: string
  clearable?: boolean
  disabledDate?: (value: string, date: Date) => boolean
  type?: 'date' | 'year' | 'quarter' | 'month' | 'week'
  valueFormat?: string
  labelFormat?: string
  shortcuts?: ShortcutType[]
  showTime?: boolean
  onChange?: (value: string | number | Date | undefined) => void
  autoSubmit?: boolean
}

日期选择:

const form = defineForm([
  {
    key: 'birthday',
    title: '出生日期',
    render: r => r.date({ type: 'date' }),
  },
])

日期时间选择:

{
  key: 'createdAt',
  title: '创建时间',
  render: r => r.date({ showTime: true }),
}

年/月/季度/周选择:

{
  key: 'year',
  title: '年份',
  render: r => r.date({ type: 'year' }),
}
{
  key: 'month',
  title: '月份',
  render: r => r.date({ type: 'month' }),
}
{
  key: 'quarter',
  title: '季度',
  render: r => r.date({ type: 'quarter' }),
}
{
  key: 'week',
  title: '周',
  render: r => r.date({ type: 'week' }),
}

禁用日期:

{
  key: 'date',
  title: '日期',
  render: r => r.date({
    disabledDate: (value, date) => {
      return date > new Date()  // 禁用未来日期
    },
  }),
}

DateRange 日期范围

interface RenderDateRangeItemOptions {
  placeholder?: string[]
  clearable?: boolean
  multiple?: boolean
  type?: 'date' | 'year' | 'quarter' | 'month' | 'week'
  valueFormat?: string       // 默认: 'YYYY-MM-DD HH:mm:ss'
  labelFormat?: string
  disabledDate?: (value: string[], date: Date) => boolean
  shortcuts?: ShortcutType[]
  onChange?: (value: string[] | undefined) => void
  autoSubmit?: boolean
}

基础用法:

const form = defineForm([
  {
    key: 'dateRange',
    title: '日期范围',
    render: r => r.dateRange(),
  },
])

自定义格式:

{
  key: 'dateRange',
  title: '日期范围',
  render: r => r.dateRange({
    valueFormat: 'YYYY-MM-DD',
    labelFormat: 'YYYY年MM月DD日',
    autoSubmit: true,
  }),
}

自定义快捷选项:

{
  key: 'dateRange',
  title: '日期范围',
  render: r => r.dateRange({
    shortcuts: [
      {
        label: '最近7天',
        value: () => {
          const end = new Date()
          const start = new Date()
          start.setTime(start.getTime() - 7 * 24 * 3600 * 1000)
          return [start, end]
        },
      },
    ],
  }),
}

Currency 金额输入

interface RenderCurrencyOptions {
  placeholder?: string
  clearable?: boolean
  readonly?: boolean
  prefix?: string | (() => JSX.Element)
  suffix?: string | (() => JSX.Element)
  thousands?: boolean     // 默认: true
  precision?: number
  inputUnit?: '分' | '元' | '万'   // 默认: '元'
  outputUnit?: '分' | '元' | '万'  // 默认: '元'
}

基础用法:

const form = defineForm([
  {
    key: 'price',
    title: '价格',
    render: r => r.currency(),
  },
])

单位转换(分转元):

{
  key: 'amount',
  title: '金额',
  render: r => r.currency({
    inputUnit: '元',     // 输入时显示单位
    outputUnit: '分',    // 输出时转换为单位
    precision: 2,
  }),
}

千分位显示:

{
  key: 'total',
  title: '总额',
  render: r => r.currency({
    thousands: true,
    suffix: '元',
  }),
}

Render 自定义渲染

interface RenderInputItemOptions<T> {
  (record: T, form?: FormItemOptions<T>): JSX.Element
}

自定义渲染:

const form = defineForm([
  {
    key: 'custom',
    title: '自定义字段',
    render: r => r.render((data, form) => {
      return (
        <div class="flex gap-2">
          <AInput v-model={data.custom} />
          <AButton>按钮</AButton>
        </div>
      )
    }),
  },
])

表单项配置

FormItemOptions 接口

interface FormItemOptions<T = Record<string, any>> {
  key: keyof T | string       // 字段键名
  title: string               // 字段标签
  default?: any | (() => any) | (() => Promise<any>)  // 默认值
  collapsed?: boolean         // 是否在折叠状态下隐藏
  group?: string | string[]   // 分组(用于 dialog 模式的 tab 分组)
  visiable?: boolean | ((record: T) => boolean)  // 条件显隐
  span?: number               // 网格跨列数
  hideLabel?: boolean         // 隐藏标签
  labelStyle?: CSSProperties  // 标签样式
  contentStyle?: CSSProperties // 内容样式
  rule?: FieldRule | FieldRule[]  // 验证规则
  mode?: 'component' | 'text' // 显示模式
  render?: FormItemRender<T>  // 渲染函数
}

配置项说明

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | key | string | - | 字段键名,对应表单数据中的属性名 | | title | string | - | 字段标签文本 | | default | any \| (() => any) | - | 默认值,支持函数形式 | | collapsed | boolean | false | 在折叠模式下隐藏此字段 | | group | string \| string[] | - | 字段分组,用于弹窗模式的 tab 切换 | | visiable | boolean \| ((record) => boolean) | true | 条件显隐,支持响应式函数 | | span | number | 1 | 网格跨列数 | | hideLabel | boolean | false | 隐藏字段标签 | | labelStyle | CSSProperties | - | 标签自定义样式 | | contentStyle | CSSProperties | - | 内容区域自定义样式 | | rule | FieldRule \| FieldRule[] | - | 表单验证规则 | | mode | 'component' \| 'text' | 'component' | 显示模式,text 模式用于只读展示 | | render | FormItemRender<T> | - | 字段渲染函数 |

条件显隐

const form = defineForm([
  {
    key: 'type',
    title: '类型',
    render: r => r.select({
      options: new Map([['1', '类型A'], ['2', '类型B']]),
    }),
  },
  {
    key: 'extra',
    title: '额外信息',
    // 当 type 为 '1' 时才显示
    visiable: (data) => data.type === '1',
    render: r => r.input(),
  },
])

字段跨度

const form = defineForm([
  {
    key: 'title',
    title: '标题',
    span: 2,  // 跨 2 列
    render: r => r.input(),
  },
  {
    key: 'content',
    title: '内容',
    span: 2,  // 跨 2 列
    render: r => r.textarea(),
  },
])

字段分组

用于 dialog 折叠模式下的 tab 分组:

const form = defineForm([
  {
    key: 'name',
    title: '姓名',
    group: ['基本信息'],  // 属于"基本信息"分组
    render: r => r.input(),
  },
  {
    key: 'address',
    title: '地址',
    group: ['联系方式'],  // 属于"联系方式"分组
    render: r => r.input(),
  },
])

FormRender 组件

Props

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | form | FormItemsOptions<T> | - | 必填,表单配置数组 | | value | Record<string, any> | - | 初始表单数据(非响应式) | | modelValue | Record<string, any> | - | 初始表单数据(支持 v-model) | | layout | 'horizontal' \| 'vertical' | 'horizontal' | 表单布局 | | columns | number | 自动计算 | 固定列数 | | minWidth | number | 400 | 每列最小宽度(用于自动计算列数) | | name | string | - | 表单 name 属性 | | id | string | - | 表单 id 属性 | | collapsedMode | 'append' \| 'dialog' | 'append' | 折叠模式 | | collapsedDialogColumns | number | 2 | 弹窗模式的列数 | | showFormResult | boolean | dialog 模式为 true | 显示已选条件标签 | | submitable | boolean | false | 显示提交/取消按钮 | | searchable | boolean | false | 显示搜索/重置按钮 | | resetable | boolean | true | 显示重置按钮 | | footer | boolean | false | 操作按钮使用底部样式 | | rowGap | number | 6 | 行间距 | | colGap | number | 12 | 列间距 |

Events

| 事件 | 参数 | 说明 | |------|------|------| | submit | formSource: Record<string, any> | 表单提交时触发 | | cancel | - | 点击取消按钮时触发 | | update:model-value | formSource: Record<string, any> | 表单数据更新时触发 |

Expose Methods

| 方法 | 参数 | 返回值 | 说明 | |------|------|--------|------| | formSource | - | Ref<DataRecord> | 响应式表单数据 | | updateFormField | (key: string, value: any) | void | 更新单个字段 | | updateFormSource | (value: DataRecord) | void | 更新整个表单 | | reset | - | void | 重置表单 | | validate | - | Promise | 验证表单 |

Slots

| 插槽 | 说明 | |------|------| | actions | 自定义操作按钮 |

使用示例

<script setup lang="ts">
import { ref } from 'vue'
import { defineForm, FormRender } from '@gopowerteam/form-render'

const formRef = ref()
const formData = ref({ name: '初始值' })

const form = defineForm([
  { key: 'name', title: '姓名', render: r => r.input() },
])

const handleSubmit = (data) => {
  console.log('提交:', data)
}

const handleValidate = async () => {
  try {
    await formRef.value?.validate()
    console.log('验证通过')
  } catch (errors) {
    console.log('验证失败:', errors)
  }
}

const handleReset = () => {
  formRef.value?.reset()
}
</script>

<template>
  <FormRender
    ref="formRef"
    :form="form"
    v-model="formData"
    searchable
    @submit="handleSubmit"
  >
    <template #actions>
      <AButton @click="handleValidate">验证</AButton>
      <AButton @click="handleReset">重置</AButton>
    </template>
  </FormRender>
</template>

useForm Hook

通过 useForm 获取表单实例的引用:

import { useForm } from '@gopowerteam/form-render'

const form = useForm('formRef')

// 访问表单数据
console.log(form.value.formSource)

// 更新字段
form.value.updateFormField('name', '新值')

// 重置表单
form.value.reset()

// 验证表单
await form.value.validate()

完整示例

<script setup lang="ts">
import { defineForm, FormRender, useForm } from '@gopowerteam/form-render'

const form = defineForm([
  { key: 'name', title: '姓名', render: r => r.input() },
  { key: 'status', title: '状态', render: r => r.select({ options: new Map() }) },
])

const formRef = useForm('myForm')

const updateName = () => {
  formRef.value.updateFormField('name', '新名字')
}

const getFormData = () => {
  console.log(formRef.value.formSource)
}
</script>

<template>
  <FormRender ref="myForm" :form="form" searchable />
  <AButton @click="updateName">更新姓名</AButton>
  <AButton @click="getFormData">获取数据</AButton>
</template>

高级功能

表单验证

使用 Arco Design 的 FieldRule 进行验证:

import type { FieldRule } from '@arco-design/web-vue'

const form = defineForm([
  {
    key: 'name',
    title: '姓名',
    rule: [{ required: true, message: '请输入姓名' }],
    render: r => r.input(),
  },
  {
    key: 'email',
    title: '邮箱',
    rule: [
      { required: true, message: '请输入邮箱' },
      { type: 'email', message: '邮箱格式不正确' },
    ],
    render: r => r.input(),
  },
  {
    key: 'password',
    title: '密码',
    rule: [
      { required: true, message: '请输入密码' },
      { minLength: 6, message: '密码至少6位' },
    ],
    render: r => r.input(),
  },
])

自定义验证器:

{
  key: 'confirmPassword',
  title: '确认密码',
  rule: [
    {
      validator: (value, callback) => {
        if (value !== formData.password) {
          callback('两次密码不一致')
        } else {
          callback()
        }
      },
    },
  ],
  render: r => r.input(),
}

动态显隐

基于表单数据动态控制字段显隐:

const form = defineForm([
  {
    key: 'type',
    title: '类型',
    render: r => r.radio({
      type: 'button',
      options: new Map([['personal', '个人'], ['company', '企业']]),
    }),
  },
  {
    key: 'companyName',
    title: '公司名称',
    visiable: (data) => data.type === 'company',
    rule: [{ required: true, message: '请输入公司名称' }],
    render: r => r.input(),
  },
  {
    key: 'idCard',
    title: '身份证号',
    visiable: (data) => data.type === 'personal',
    render: r => r.input(),
  },
])

异步选项加载

支持 Promise 形式的异步选项:

// 模拟 API 请求
const fetchOptions = () => {
  return new Promise<Map<string, string>>((resolve) => {
    setTimeout(() => {
      resolve(new Map([
        ['1', '选项1'],
        ['2', '选项2'],
        ['3', '选项3'],
      ]))
    }, 1000)
  })
}

const form = defineForm([
  {
    key: 'category',
    title: '分类',
    render: r => r.select({
      options: fetchOptions,
      cache: true,  // 开启缓存
    }),
  },
])

自动提交

字段值变化后自动触发表单提交:

const form = defineForm([
  {
    key: 'keyword',
    title: '关键词',
    render: r => r.input({ autoSubmit: true }),
  },
  {
    key: 'status',
    title: '状态',
    render: r => r.select({
      options: statusOptions,
      autoSubmit: true,  // 选择后立即提交
    }),
  },
])

搜索结果展示

dialog 模式下,可以显示已选择的搜索条件:

<FormRender
  :form="form"
  collapsed-mode="dialog"
  :show-form-result="true"
  searchable
  @submit="handleSearch"
/>

与 table-render 集成

form-render 可以与 @gopowerteam/table-render 配合使用,作为表格的搜索表单:

<script setup lang="ts">
import { defineForm } from '@gopowerteam/form-render'
import { defineColumns, defineTableLoad, useTable, TableRender } from '@gopowerteam/table-render'

// 定义搜索表单
const form = defineForm([
  {
    key: 'keyword',
    title: '关键词',
    hideLabel: true,
    render: r => r.input({ placeholder: '请输入关键词', autoSubmit: true }),
  },
  {
    key: 'status',
    title: '状态',
    hideLabel: true,
    render: r => r.select({
      options: new Map([['1', '启用'], ['0', '禁用']]),
      clearable: true,
      autoSubmit: true,
    }),
  },
  {
    key: 'dateRange',
    title: '日期',
    hideLabel: true,
    collapsed: true,
    render: r => r.dateRange(),
  },
])

// 定义表格列
const columns = defineColumns([
  { key: 'name', title: '名称' },
  { key: 'status', title: '状态' },
  { key: 'createdAt', title: '创建时间' },
])

// 定义数据加载
const table = useTable('tableRef')
const onTableLoad = defineTableLoad(async ({ form, update, page }) => {
  const res = await fetch('/api/list', {
    method: 'POST',
    body: JSON.stringify({ ...form, page }),
  })
  const data = await res.json()
  update(data.list)
  page.total = data.total
})
</script>

<template>
  <TableRender
    ref="tableRef"
    :form="form"
    :form-options="{ minWidth: 300 }"
    :columns="columns"
    :data-load="onTableLoad"
    collapsable
    pageable
    searchable
  />
</template>

通过 table 实例访问表单

const table = useTable('tableRef')

// 获取表单数据
console.log(table.value.formSource)

// 更新表单字段
table.value.formInstance?.updateFormField('status', '1')

// 重置表单
table.value.formInstance?.reset()

工具函数

createFormSource

手动创建表单数据源:

import { createFormSource } from '@gopowerteam/form-render'

const form = defineForm([
  { key: 'name', title: '姓名', default: '默认名' },
  { key: 'age', title: '年龄', default: 18 },
])

// 创建响应式数据源
const [formSource, updateFormSource] = createFormSource(form, { name: '初始名' })

console.log(formSource.value)  // { name: '初始名', age: 18 }

// 更新数据
updateFormSource({ name: '新名字', age: 20 })

FormRenderResolver

用于 unplugin-vue-components 自动导入:

// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { FormRenderResolver } from '@gopowerteam/form-render'

export default {
  plugins: [
    Components({
      resolvers: [FormRenderResolver()],
    }),
  ],
}

TypeScript 类型

导出的类型

// 核心类型
export type { DataRecord, DataProp } from './interfaces'
export type { FormItemsOptions, FormItemOptions, FormItemRender, FormItemRenderFun, FormItemRenderReturn } from './interfaces'

// 组件实例
export type { FormRenderInstance } from './form-render'

// 字段选项类型
export type { RenderInputItemOptions } from './form-items/input'
export type { RenderTextareaItemOptions } from './form-items/textarea'
export type { RenderSelectItemOptions, SelectOptions } from './form-items/select'
export type { RenderTreeSelectItemOptions } from './form-items/tree-select'
export type { RenderCascaderItemOptions } from './form-items/cascader'
export type { RenderRadioItemOptions, RadioOptions } from './form-items/radio'
export type { RenderSwitchItemOptions } from './form-items/switch'
export type { RenderDateItemOptions } from './form-items/date'
export type { RenderDateRangeItemOptions } from './form-items/date-range'
export type { RenderCurrencyOptions } from './form-items/currency'

泛型使用

interface UserFormData {
  name: string
  email: string
  age: number
}

// 使用泛型获得完整的类型提示
const form = defineForm<UserFormData>([
  {
    key: 'name',  // 类型为 'name' | 'email' | 'age'
    title: '姓名',
    render: r => r.input(),
  },
])

// 组件 Props 类型
const props = defineProps<{
  form: FormItemsOptions<UserFormData>
}>()

// 实例类型
const formRef = ref<FormRenderInstance>()

最佳实践

1. 使用 TypeScript 泛型

始终使用泛型参数获得类型提示:

// ✅ 推荐
interface FormData { name: string }
const form = defineForm<FormData>([...])

// ❌ 不推荐
const form = defineForm([...])

2. 提取选项配置

将复杂的选项配置提取为常量:

const STATUS_OPTIONS = new Map([
  ['1', '启用'],
  ['0', '禁用'],
])

const form = defineForm([
  {
    key: 'status',
    title: '状态',
    render: r => r.select({ options: STATUS_OPTIONS }),
  },
])

3. 合理使用 autoSubmit

在搜索表单中,对常用筛选条件开启自动提交:

{
  key: 'status',
  title: '状态',
  render: r => r.select({
    options: STATUS_OPTIONS,
    autoSubmit: true,  // 选择后立即搜索
  }),
}

4. 使用 hideLabel 优化搜索表单

搜索表单中可以隐藏标签以节省空间:

const form = defineForm([
  {
    key: 'keyword',
    title: '关键词',
    hideLabel: true,
    render: r => r.input({ placeholder: '请输入关键词' }),
  },
])

5. 异步选项使用缓存

避免重复请求相同的选项数据:

{
  key: 'category',
  title: '分类',
  render: r => r.select({
    options: fetchCategories,
    cache: true,  // 开启缓存
  }),
}

6. 合理使用折叠模式

对于字段较多的表单,使用折叠模式提升用户体验:

const form = defineForm([
  { key: 'keyword', title: '关键词', render: r => r.input() },
  { key: 'status', title: '状态', render: r => r.select({ options: STATUS_OPTIONS }) },
  // 以下字段默认折叠
  { key: 'dateRange', title: '日期范围', collapsed: true, render: r => r.dateRange() },
  { key: 'category', title: '分类', collapsed: true, render: r => r.select({ options: [] }) },
])

License

MIT