@gopowerteam/form-render 0.0.94 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1595 -0
- package/dist/cjs/config/provide.config.d.ts +0 -1
- package/dist/cjs/defines/define-form.d.ts +0 -1
- package/dist/cjs/form-items/cascader/index.d.ts +0 -1
- package/dist/cjs/form-items/currency/index.d.ts +0 -1
- package/dist/cjs/form-items/date/index.d.ts +0 -1
- package/dist/cjs/form-items/date-range/index.d.ts +0 -1
- package/dist/cjs/form-items/input/index.d.ts +0 -1
- package/dist/cjs/form-items/radio/index.d.ts +0 -1
- package/dist/cjs/form-items/render/index.d.ts +0 -1
- package/dist/cjs/form-items/select/index.d.ts +0 -1
- package/dist/cjs/form-items/switch/index.d.ts +0 -1
- package/dist/cjs/form-items/textarea/index.d.ts +0 -1
- package/dist/cjs/form-items/tree-select/index.d.ts +1 -2
- package/dist/cjs/form-render/form-collapsed-dialog.d.ts +6 -7
- package/dist/cjs/form-render/form-item-render.d.ts +3 -3
- package/dist/cjs/form-render/index.d.ts +126 -17
- package/dist/cjs/hooks/use-form.d.ts +2 -3
- package/dist/cjs/index.cjs +5 -1
- package/dist/cjs/index.d.ts +2 -3
- package/dist/cjs/install.d.ts +0 -1
- package/dist/cjs/interfaces/form-item-options.d.ts +0 -1
- package/dist/cjs/utils/create-form-source.d.ts +0 -1
- package/dist/es/config/provide.config.d.ts +0 -1
- package/dist/es/defines/define-form.d.ts +0 -1
- package/dist/es/form-items/cascader/index.d.ts +0 -1
- package/dist/es/form-items/currency/index.d.ts +0 -1
- package/dist/es/form-items/date/index.d.ts +0 -1
- package/dist/es/form-items/date-range/index.d.ts +0 -1
- package/dist/es/form-items/input/index.d.ts +0 -1
- package/dist/es/form-items/radio/index.d.ts +0 -1
- package/dist/es/form-items/render/index.d.ts +0 -1
- package/dist/es/form-items/select/index.d.ts +0 -1
- package/dist/es/form-items/switch/index.d.ts +0 -1
- package/dist/es/form-items/textarea/index.d.ts +0 -1
- package/dist/es/form-items/tree-select/index.d.ts +1 -2
- package/dist/es/form-render/form-collapsed-dialog.d.ts +6 -7
- package/dist/es/form-render/form-item-render.d.ts +3 -3
- package/dist/es/form-render/index.d.ts +126 -17
- package/dist/es/hooks/use-form.d.ts +2 -3
- package/dist/es/index.d.ts +2 -3
- package/dist/es/index.mjs +1603 -1600
- package/dist/es/install.d.ts +0 -1
- package/dist/es/interfaces/form-item-options.d.ts +0 -1
- package/dist/es/utils/create-form-source.d.ts +0 -1
- package/dist/style.css +1 -1
- package/package.json +31 -29
package/README.md
ADDED
|
@@ -0,0 +1,1595 @@
|
|
|
1
|
+
# @gopowerteam/form-render
|
|
2
|
+
|
|
3
|
+
基于 Vue 3 + Arco Design 的声明式表单渲染库,通过配置化的方式快速构建表单。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [速查表](#速查表)
|
|
8
|
+
- [导出列表](#导出列表)
|
|
9
|
+
- [FormRender Props 速查](#formrender-props-速查)
|
|
10
|
+
- [FormRender Methods 速查](#formrender-methods-速查)
|
|
11
|
+
- [FormRender Events 速查](#formrender-events-速查)
|
|
12
|
+
- [字段类型速查](#字段类型速查)
|
|
13
|
+
- [FormItemOptions 速查](#formitemoptions-速查)
|
|
14
|
+
- [快速开始](#快速开始)
|
|
15
|
+
- [表单布局](#表单布局)
|
|
16
|
+
- [表单字段类型](#表单字段类型)
|
|
17
|
+
- [Input 输入框](#input-输入框)
|
|
18
|
+
- [Textarea 文本域](#textarea-文本域)
|
|
19
|
+
- [Select 选择器](#select-选择器)
|
|
20
|
+
- [TreeSelect 树选择](#treeselect-树选择)
|
|
21
|
+
- [Cascader 级联选择](#cascader-级联选择)
|
|
22
|
+
- [Radio 单选框](#radio-单选框)
|
|
23
|
+
- [Switch 开关](#switch-开关)
|
|
24
|
+
- [Date 日期选择](#date-日期选择)
|
|
25
|
+
- [DateRange 日期范围](#daterange-日期范围)
|
|
26
|
+
- [Currency 金额输入](#currency-金额输入)
|
|
27
|
+
- [Render 自定义渲染](#render-自定义渲染)
|
|
28
|
+
- [表单项配置](#表单项配置)
|
|
29
|
+
- [FormRender 组件](#formrender-组件)
|
|
30
|
+
- [useForm Hook](#useform-hook)
|
|
31
|
+
- [高级功能](#高级功能)
|
|
32
|
+
- [与 table-render 集成](#与-table-render-集成)
|
|
33
|
+
- [工具函数](#工具函数)
|
|
34
|
+
- [TypeScript 类型](#typescript-类型)
|
|
35
|
+
- [最佳实践](#最佳实践)
|
|
36
|
+
|
|
37
|
+
## 速查表
|
|
38
|
+
|
|
39
|
+
### 导出列表
|
|
40
|
+
|
|
41
|
+
| 导出名称 | 类型 | 说明 |
|
|
42
|
+
|----------|------|------|
|
|
43
|
+
| `defineForm` | 函数 | 定义表单配置,支持泛型 |
|
|
44
|
+
| `FormRender` | 组件 | 表单渲染组件 |
|
|
45
|
+
| `FormRenderInstance` | 类型 | 组件实例类型 |
|
|
46
|
+
| `useForm` | Hook | 通过 ref 获取表单实例 |
|
|
47
|
+
| `createFormSource` | 工具函数 | 手动创建表单数据源 |
|
|
48
|
+
| `FormRenderResolver` | 工具函数 | unplugin-vue-components 解析器 |
|
|
49
|
+
| `FormItemsOptions` | 类型 | 表单配置数组类型 |
|
|
50
|
+
| `FormItemOptions` | 类型 | 表单项配置类型 |
|
|
51
|
+
| `DataRecord` | 类型 | 通用数据记录类型 |
|
|
52
|
+
|
|
53
|
+
### FormRender Props 速查
|
|
54
|
+
|
|
55
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
56
|
+
|------|------|--------|------|
|
|
57
|
+
| `form` | `FormItemsOptions<T>` | - | **必填**,表单配置数组 |
|
|
58
|
+
| `value` | `Record<string, any>` | - | 初始表单数据(非响应式) |
|
|
59
|
+
| `modelValue` | `Record<string, any>` | - | 初始表单数据(支持 v-model) |
|
|
60
|
+
| `layout` | `'horizontal' \| 'vertical'` | `'horizontal'` | 表单布局模式 |
|
|
61
|
+
| `columns` | `number` | 自动计算 | 固定列数 |
|
|
62
|
+
| `minWidth` | `number` | `400` | 每列最小宽度(px) |
|
|
63
|
+
| `name` | `string` | - | 表单 name 属性 |
|
|
64
|
+
| `id` | `string` | - | 表单 id 属性 |
|
|
65
|
+
| `collapsedMode` | `'append' \| 'dialog'` | `'append'` | 折叠模式 |
|
|
66
|
+
| `collapsedDialogColumns` | `number` | `2` | 弹窗模式列数 |
|
|
67
|
+
| `showFormResult` | `boolean` | dialog 模式为 true | 显示已选条件标签 |
|
|
68
|
+
| `submitable` | `boolean` | `false` | 显示提交/取消按钮 |
|
|
69
|
+
| `searchable` | `boolean` | `false` | 显示搜索/重置按钮 |
|
|
70
|
+
| `resetable` | `boolean` | `true` | 显示重置按钮 |
|
|
71
|
+
| `footer` | `boolean` | `false` | 操作按钮底部样式 |
|
|
72
|
+
| `rowGap` | `number` | `6` | 行间距(px) |
|
|
73
|
+
| `colGap` | `number` | `12` | 列间距(px) |
|
|
74
|
+
|
|
75
|
+
### FormRender Methods 速查
|
|
76
|
+
|
|
77
|
+
| 方法 | 签名 | 说明 |
|
|
78
|
+
|------|------|------|
|
|
79
|
+
| `formSource` | `Ref<DataRecord>` | 响应式表单数据 |
|
|
80
|
+
| `validate` | `() => Promise<void>` | 验证表单 |
|
|
81
|
+
| `reset` | `() => void` | 重置表单 |
|
|
82
|
+
| `updateFormField` | `(key: string, value: any) => void` | 更新单个字段 |
|
|
83
|
+
| `updateFormSource` | `(value: DataRecord) => void` | 更新整个表单 |
|
|
84
|
+
|
|
85
|
+
### FormRender Events 速查
|
|
86
|
+
|
|
87
|
+
| 事件 | 参数 | 说明 |
|
|
88
|
+
|------|------|------|
|
|
89
|
+
| `submit` | `formSource: Record<string, any>` | 表单提交时触发 |
|
|
90
|
+
| `cancel` | - | 点击取消按钮时触发 |
|
|
91
|
+
| `update:model-value` | `formSource: Record<string, any>` | 表单数据更新时触发 |
|
|
92
|
+
|
|
93
|
+
### 字段类型速查
|
|
94
|
+
|
|
95
|
+
| 类型 | 渲染方法 | 关键配置 | 常用场景 |
|
|
96
|
+
|------|----------|----------|----------|
|
|
97
|
+
| 输入框 | `r.input()` | `type`, `clearable`, `autoSubmit` | 文本/数字输入 |
|
|
98
|
+
| 文本域 | `r.textarea()` | `autosize`, `maxLength` | 多行文本 |
|
|
99
|
+
| 选择器 | `r.select()` | `options`, `multiple`, `cache` | 单选/多选下拉 |
|
|
100
|
+
| 树选择 | `r.treeSelect()` | `options`, `treeProps`, `fieldNames` | 树形数据选择 |
|
|
101
|
+
| 级联选择 | `r.cascader()` | `options`, `pathMode`, `multiple` | 级联数据选择 |
|
|
102
|
+
| 单选框 | `r.radio()` | `options`, `type: 'button'` | 单选按钮组 |
|
|
103
|
+
| 开关 | `r.switch()` | `openValue`, `closeValue`, `openLabel` | 布尔切换 |
|
|
104
|
+
| 日期 | `r.date()` | `type`, `showTime`, `shortcuts` | 日期/时间选择 |
|
|
105
|
+
| 日期范围 | `r.dateRange()` | `valueFormat`, `shortcuts` | 日期区间选择 |
|
|
106
|
+
| 金额 | `r.currency()` | `inputUnit`, `outputUnit`, `thousands` | 金额输入(支持单位转换) |
|
|
107
|
+
| 自定义 | `r.render()` | 自定义渲染函数 | 完全自定义字段 |
|
|
108
|
+
|
|
109
|
+
### FormItemOptions 速查
|
|
110
|
+
|
|
111
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
112
|
+
|------|------|--------|------|
|
|
113
|
+
| `key` | `string` | - | **必填**,字段键名 |
|
|
114
|
+
| `title` | `string` | - | **必填**,字段标签 |
|
|
115
|
+
| `default` | `any \| (() => any)` | - | 默认值,支持函数 |
|
|
116
|
+
| `render` | `FormItemRender<T>` | - | **必填**,渲染函数 |
|
|
117
|
+
| `rule` | `FieldRule \| FieldRule[]` | - | 验证规则 |
|
|
118
|
+
| `visiable` | `boolean \| ((data: T) => boolean)` | `true` | 条件显隐 |
|
|
119
|
+
| `collapsed` | `boolean` | `false` | 折叠状态下隐藏 |
|
|
120
|
+
| `span` | `number` | `1` | 网格跨列数 |
|
|
121
|
+
| `hideLabel` | `boolean` | `false` | 隐藏字段标签 |
|
|
122
|
+
| `group` | `string \| string[]` | - | 分组(dialog 模式) |
|
|
123
|
+
| `labelStyle` | `CSSProperties` | - | 标签样式 |
|
|
124
|
+
| `contentStyle` | `CSSProperties` | - | 内容样式 |
|
|
125
|
+
| `mode` | `'component' \| 'text'` | `'component'` | 显示模式 |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 快速开始
|
|
130
|
+
|
|
131
|
+
### 基础用法
|
|
132
|
+
|
|
133
|
+
```vue
|
|
134
|
+
<script setup lang="ts">
|
|
135
|
+
import { defineForm, FormRender } from '@gopowerteam/form-render'
|
|
136
|
+
|
|
137
|
+
interface UserForm {
|
|
138
|
+
name: string
|
|
139
|
+
email: string
|
|
140
|
+
status: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const form = defineForm<UserForm>([
|
|
144
|
+
{
|
|
145
|
+
key: 'name',
|
|
146
|
+
title: '姓名',
|
|
147
|
+
render: r => r.input({ placeholder: '请输入姓名', clearable: true }),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: 'email',
|
|
151
|
+
title: '邮箱',
|
|
152
|
+
render: r => r.input({ placeholder: '请输入邮箱' }),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: 'status',
|
|
156
|
+
title: '状态',
|
|
157
|
+
render: r => r.select({
|
|
158
|
+
options: new Map([['1', '启用'], ['0', '禁用']]),
|
|
159
|
+
clearable: true,
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
const handleSubmit = (data: UserForm) => {
|
|
165
|
+
console.log('表单数据:', data)
|
|
166
|
+
}
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<template>
|
|
170
|
+
<FormRender
|
|
171
|
+
:form="form"
|
|
172
|
+
searchable
|
|
173
|
+
@submit="handleSubmit"
|
|
174
|
+
/>
|
|
175
|
+
</template>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### defineForm 配置函数
|
|
179
|
+
|
|
180
|
+
`defineForm` 是一个类型安全的表单配置函数,支持泛型参数:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
interface User {
|
|
184
|
+
name: string
|
|
185
|
+
age: number
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 使用泛型获得类型提示
|
|
189
|
+
const form = defineForm<User>([
|
|
190
|
+
{
|
|
191
|
+
key: 'name', // 自动推导为 'name' | 'age'
|
|
192
|
+
title: '姓名',
|
|
193
|
+
render: r => r.input(),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: 'age',
|
|
197
|
+
title: '年龄',
|
|
198
|
+
render: r => r.input({ type: 'number' }),
|
|
199
|
+
},
|
|
200
|
+
])
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 表单布局
|
|
204
|
+
|
|
205
|
+
### 布局模式
|
|
206
|
+
|
|
207
|
+
支持 `horizontal`(水平)和 `vertical`(垂直)两种布局:
|
|
208
|
+
|
|
209
|
+
```vue
|
|
210
|
+
<!-- 水平布局(默认) -->
|
|
211
|
+
<FormRender :form="form" layout="horizontal" />
|
|
212
|
+
|
|
213
|
+
<!-- 垂直布局 -->
|
|
214
|
+
<FormRender :form="form" layout="vertical" />
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 响应式列布局
|
|
218
|
+
|
|
219
|
+
表单会根据容器宽度自动计算列数:
|
|
220
|
+
|
|
221
|
+
```vue
|
|
222
|
+
<!-- 自动计算列数,每列最小宽度 400px -->
|
|
223
|
+
<FormRender :form="form" :min-width="400" />
|
|
224
|
+
|
|
225
|
+
<!-- 固定 2 列 -->
|
|
226
|
+
<FormRender :form="form" :columns="2" />
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 折叠模式
|
|
230
|
+
|
|
231
|
+
当表单字段较多时,可以使用折叠模式隐藏部分字段:
|
|
232
|
+
|
|
233
|
+
#### append 模式(默认)
|
|
234
|
+
|
|
235
|
+
点击"展开/收起"按钮,在原位置展开更多字段:
|
|
236
|
+
|
|
237
|
+
```vue
|
|
238
|
+
<FormRender
|
|
239
|
+
:form="form"
|
|
240
|
+
collapsed-mode="append"
|
|
241
|
+
searchable
|
|
242
|
+
/>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### dialog 模式
|
|
246
|
+
|
|
247
|
+
点击"高级搜索"按钮,在弹窗中显示更多字段:
|
|
248
|
+
|
|
249
|
+
```vue
|
|
250
|
+
<FormRender
|
|
251
|
+
:form="form"
|
|
252
|
+
collapsed-mode="dialog"
|
|
253
|
+
:collapsed-dialog-columns="2"
|
|
254
|
+
searchable
|
|
255
|
+
/>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
配置字段为折叠状态:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const form = defineForm([
|
|
262
|
+
{
|
|
263
|
+
key: 'name',
|
|
264
|
+
title: '姓名',
|
|
265
|
+
// 此字段在折叠状态下隐藏
|
|
266
|
+
collapsed: true,
|
|
267
|
+
render: r => r.input(),
|
|
268
|
+
},
|
|
269
|
+
])
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 表单间距
|
|
273
|
+
|
|
274
|
+
```vue
|
|
275
|
+
<FormRender
|
|
276
|
+
:form="form"
|
|
277
|
+
:row-gap="6"
|
|
278
|
+
:col-gap="12"
|
|
279
|
+
/>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## 表单字段类型
|
|
283
|
+
|
|
284
|
+
### Input 输入框
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
interface RenderInputItemOptions {
|
|
288
|
+
placeholder?: string
|
|
289
|
+
clearable?: boolean
|
|
290
|
+
readonly?: boolean
|
|
291
|
+
autoSubmit?: boolean
|
|
292
|
+
type?: 'string' | 'number'
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**基础用法:**
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
const form = defineForm([
|
|
300
|
+
{
|
|
301
|
+
key: 'name',
|
|
302
|
+
title: '姓名',
|
|
303
|
+
render: r => r.input({ placeholder: '请输入姓名' }),
|
|
304
|
+
},
|
|
305
|
+
])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**数字输入:**
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
{
|
|
312
|
+
key: 'age',
|
|
313
|
+
title: '年龄',
|
|
314
|
+
render: r => r.input({ type: 'number', placeholder: '请输入年龄' }),
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**自动提交:**
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
{
|
|
322
|
+
key: 'keyword',
|
|
323
|
+
title: '关键词',
|
|
324
|
+
render: r => r.input({ autoSubmit: true, clearable: true }),
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Textarea 文本域
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
interface RenderTextareaItemOptions {
|
|
332
|
+
placeholder?: string
|
|
333
|
+
rows?: number
|
|
334
|
+
resize?: 'none' | 'both' | 'horizontal' | 'vertical'
|
|
335
|
+
autosize?: { minRows: number, maxRows: number }
|
|
336
|
+
maxLength?: number
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**基础用法:**
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const form = defineForm([
|
|
344
|
+
{
|
|
345
|
+
key: 'description',
|
|
346
|
+
title: '描述',
|
|
347
|
+
render: r => r.textarea({ placeholder: '请输入描述' }),
|
|
348
|
+
},
|
|
349
|
+
])
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**自适应高度:**
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
{
|
|
356
|
+
key: 'content',
|
|
357
|
+
title: '内容',
|
|
358
|
+
render: r => r.textarea({
|
|
359
|
+
autosize: { minRows: 3, maxRows: 6 },
|
|
360
|
+
maxLength: 500,
|
|
361
|
+
}),
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Select 选择器
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
interface RenderSelectItemOptions {
|
|
369
|
+
placeholder?: string
|
|
370
|
+
clearable?: boolean
|
|
371
|
+
searchable?: boolean
|
|
372
|
+
createable?: boolean
|
|
373
|
+
options: SelectOptions | (() => SelectOptions) | (() => Promise<SelectOptions>) | Ref<SelectOptions>
|
|
374
|
+
multiple?: boolean
|
|
375
|
+
maxTagCount?: number
|
|
376
|
+
default?: string | number | boolean
|
|
377
|
+
autoSubmit?: boolean
|
|
378
|
+
cache?: boolean
|
|
379
|
+
onChange?: (value) => void
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
type SelectOptions = Map<string | number | boolean, string>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**静态选项:**
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const form = defineForm([
|
|
389
|
+
{
|
|
390
|
+
key: 'status',
|
|
391
|
+
title: '状态',
|
|
392
|
+
render: r => r.select({
|
|
393
|
+
options: new Map([['1', '启用'], ['0', '禁用']]),
|
|
394
|
+
clearable: true,
|
|
395
|
+
}),
|
|
396
|
+
},
|
|
397
|
+
])
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**多选:**
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
{
|
|
404
|
+
key: 'tags',
|
|
405
|
+
title: '标签',
|
|
406
|
+
render: r => r.select({
|
|
407
|
+
multiple: true,
|
|
408
|
+
options: new Map([['vue', 'Vue'], ['react', 'React'], ['angular', 'Angular']]),
|
|
409
|
+
}),
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**异步选项:**
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
const loadOptions = () => {
|
|
417
|
+
return Promise.resolve(new Map([['a', '选项A'], ['b', '选项B']]))
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
{
|
|
421
|
+
key: 'category',
|
|
422
|
+
title: '分类',
|
|
423
|
+
render: r => r.select({
|
|
424
|
+
options: loadOptions,
|
|
425
|
+
cache: true, // 开启缓存,避免重复请求
|
|
426
|
+
}),
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**响应式选项:**
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { ref } from 'vue'
|
|
434
|
+
|
|
435
|
+
const options = ref(new Map([['a', '选项A']]))
|
|
436
|
+
|
|
437
|
+
// 动态更新选项
|
|
438
|
+
setTimeout(() => {
|
|
439
|
+
options.value = new Map([['b', '选项B'], ['c', '选项C']])
|
|
440
|
+
}, 3000)
|
|
441
|
+
|
|
442
|
+
{
|
|
443
|
+
key: 'type',
|
|
444
|
+
title: '类型',
|
|
445
|
+
render: r => r.select({ options }),
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### TreeSelect 树选择
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
interface RenderTreeSelectItemOptions {
|
|
453
|
+
placeholder?: string
|
|
454
|
+
clearable?: boolean
|
|
455
|
+
searchable?: boolean
|
|
456
|
+
fieldNames?: TreeFieldNames
|
|
457
|
+
options: TreeNodeData[] | (() => ...) | Ref<...>
|
|
458
|
+
multiple?: boolean
|
|
459
|
+
maxTagCount?: number
|
|
460
|
+
default?: string | number | boolean
|
|
461
|
+
autoSubmit?: boolean
|
|
462
|
+
cache?: boolean
|
|
463
|
+
treeProps?: Partial<TreeProps>
|
|
464
|
+
treeCheckStrictly?: boolean
|
|
465
|
+
treeCheckedStrategy?: 'all' | 'parent' | 'child'
|
|
466
|
+
slots?: Record<string, (data?: TreeNodeData) => JSX.Element>
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**基础用法:**
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
const form = defineForm([
|
|
474
|
+
{
|
|
475
|
+
key: 'department',
|
|
476
|
+
title: '部门',
|
|
477
|
+
render: r => r.treeSelect({
|
|
478
|
+
options: [
|
|
479
|
+
{
|
|
480
|
+
key: '1',
|
|
481
|
+
title: '技术部',
|
|
482
|
+
children: [
|
|
483
|
+
{ key: '1-1', title: '前端组' },
|
|
484
|
+
{ key: '1-2', title: '后端组' },
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
key: '2',
|
|
489
|
+
title: '产品部',
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
}),
|
|
493
|
+
},
|
|
494
|
+
])
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**自定义字段映射:**
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
{
|
|
501
|
+
key: 'org',
|
|
502
|
+
title: '组织',
|
|
503
|
+
render: r => r.treeSelect({
|
|
504
|
+
fieldNames: { key: 'id', title: 'name', children: 'children' },
|
|
505
|
+
options: [{ id: '1', name: '组织A', children: [] }],
|
|
506
|
+
}),
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**使用插槽:**
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
{
|
|
514
|
+
key: 'category',
|
|
515
|
+
title: '分类',
|
|
516
|
+
render: r => r.treeSelect({
|
|
517
|
+
options: treeOptions,
|
|
518
|
+
slots: {
|
|
519
|
+
'tree-slot-title': (data) => <div>{data.title}</div>,
|
|
520
|
+
'footer': () => <div>自定义底部</div>,
|
|
521
|
+
},
|
|
522
|
+
}),
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Cascader 级联选择
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
interface RenderCascaderItemOptions {
|
|
530
|
+
placeholder?: string
|
|
531
|
+
clearable?: boolean
|
|
532
|
+
searchable?: boolean
|
|
533
|
+
pathMode?: boolean
|
|
534
|
+
options: CascaderOption[] | (() => ...) | Ref<...>
|
|
535
|
+
multiple?: boolean
|
|
536
|
+
maxTagCount?: number
|
|
537
|
+
default?: string | number | boolean
|
|
538
|
+
autoSubmit?: boolean
|
|
539
|
+
checkStrictly?: boolean
|
|
540
|
+
triggerProps?: TriggerProps
|
|
541
|
+
cache?: boolean
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
**基础用法:**
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
const form = defineForm([
|
|
549
|
+
{
|
|
550
|
+
key: 'region',
|
|
551
|
+
title: '地区',
|
|
552
|
+
render: r => r.cascader({
|
|
553
|
+
options: [
|
|
554
|
+
{
|
|
555
|
+
value: 'beijing',
|
|
556
|
+
label: '北京',
|
|
557
|
+
children: [
|
|
558
|
+
{ value: 'chaoyang', label: '朝阳区' },
|
|
559
|
+
{ value: 'haidian', label: '海淀区' },
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
value: 'shanghai',
|
|
564
|
+
label: '上海',
|
|
565
|
+
children: [
|
|
566
|
+
{ value: 'pudong', label: '浦东新区' },
|
|
567
|
+
],
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
}),
|
|
571
|
+
},
|
|
572
|
+
])
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**路径模式:**
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
{
|
|
579
|
+
key: 'address',
|
|
580
|
+
title: '地址',
|
|
581
|
+
render: r => r.cascader({
|
|
582
|
+
pathMode: true, // 返回路径数组 ['beijing', 'chaoyang']
|
|
583
|
+
multiple: true,
|
|
584
|
+
options: addressOptions,
|
|
585
|
+
}),
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**异步加载:**
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
const loadOptions = () => {
|
|
593
|
+
return Promise.resolve([
|
|
594
|
+
{ value: '1', label: '选项1', children: [] },
|
|
595
|
+
])
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
{
|
|
599
|
+
key: 'category',
|
|
600
|
+
title: '分类',
|
|
601
|
+
render: r => r.cascader({
|
|
602
|
+
options: loadOptions,
|
|
603
|
+
cache: true,
|
|
604
|
+
}),
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Radio 单选框
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
interface RenderRadioItemOptions {
|
|
612
|
+
options: RadioOptions | (() => RadioOptions) | Ref<RadioOptions>
|
|
613
|
+
type?: 'radio' | 'button'
|
|
614
|
+
size?: 'mini' | 'small' | 'medium' | 'large'
|
|
615
|
+
default?: string | number
|
|
616
|
+
autoSubmit?: boolean
|
|
617
|
+
cache?: boolean
|
|
618
|
+
onChange?: (value: string | number) => void
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
type RadioOptions = Map<string | number, string>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**基础用法:**
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
const form = defineForm([
|
|
628
|
+
{
|
|
629
|
+
key: 'gender',
|
|
630
|
+
title: '性别',
|
|
631
|
+
render: r => r.radio({
|
|
632
|
+
options: new Map([['male', '男'], ['female', '女']]),
|
|
633
|
+
}),
|
|
634
|
+
},
|
|
635
|
+
])
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**按钮样式:**
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
{
|
|
642
|
+
key: 'status',
|
|
643
|
+
title: '状态',
|
|
644
|
+
render: r => r.radio({
|
|
645
|
+
type: 'button',
|
|
646
|
+
size: 'large',
|
|
647
|
+
options: new Map([['1', '启用'], ['0', '禁用']]),
|
|
648
|
+
autoSubmit: true,
|
|
649
|
+
}),
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Switch 开关
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
interface RenderSwitchItemOptions {
|
|
657
|
+
size?: 'small' | 'medium'
|
|
658
|
+
openLabel?: string // 默认: '是'
|
|
659
|
+
closeLabel?: string // 默认: '否'
|
|
660
|
+
openValue?: string | number | boolean // 默认: true
|
|
661
|
+
closeValue?: string | number | boolean // 默认: false
|
|
662
|
+
default?: string | number | boolean
|
|
663
|
+
autoSubmit?: boolean
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**基础用法:**
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
const form = defineForm([
|
|
671
|
+
{
|
|
672
|
+
key: 'enabled',
|
|
673
|
+
title: '启用',
|
|
674
|
+
default: false,
|
|
675
|
+
render: r => r.switch(),
|
|
676
|
+
},
|
|
677
|
+
])
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**自定义值和标签:**
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
{
|
|
684
|
+
key: 'status',
|
|
685
|
+
title: '状态',
|
|
686
|
+
render: r => r.switch({
|
|
687
|
+
openLabel: '开启',
|
|
688
|
+
closeLabel: '关闭',
|
|
689
|
+
openValue: 1,
|
|
690
|
+
closeValue: 0,
|
|
691
|
+
}),
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Date 日期选择
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
interface RenderDateItemOptions {
|
|
699
|
+
placeholder?: string
|
|
700
|
+
clearable?: boolean
|
|
701
|
+
disabledDate?: (value: string, date: Date) => boolean
|
|
702
|
+
type?: 'date' | 'year' | 'quarter' | 'month' | 'week'
|
|
703
|
+
valueFormat?: string
|
|
704
|
+
labelFormat?: string
|
|
705
|
+
shortcuts?: ShortcutType[]
|
|
706
|
+
showTime?: boolean
|
|
707
|
+
onChange?: (value: string | number | Date | undefined) => void
|
|
708
|
+
autoSubmit?: boolean
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
**日期选择:**
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
const form = defineForm([
|
|
716
|
+
{
|
|
717
|
+
key: 'birthday',
|
|
718
|
+
title: '出生日期',
|
|
719
|
+
render: r => r.date({ type: 'date' }),
|
|
720
|
+
},
|
|
721
|
+
])
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**日期时间选择:**
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
{
|
|
728
|
+
key: 'createdAt',
|
|
729
|
+
title: '创建时间',
|
|
730
|
+
render: r => r.date({ showTime: true }),
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**年/月/季度/周选择:**
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
{
|
|
738
|
+
key: 'year',
|
|
739
|
+
title: '年份',
|
|
740
|
+
render: r => r.date({ type: 'year' }),
|
|
741
|
+
}
|
|
742
|
+
{
|
|
743
|
+
key: 'month',
|
|
744
|
+
title: '月份',
|
|
745
|
+
render: r => r.date({ type: 'month' }),
|
|
746
|
+
}
|
|
747
|
+
{
|
|
748
|
+
key: 'quarter',
|
|
749
|
+
title: '季度',
|
|
750
|
+
render: r => r.date({ type: 'quarter' }),
|
|
751
|
+
}
|
|
752
|
+
{
|
|
753
|
+
key: 'week',
|
|
754
|
+
title: '周',
|
|
755
|
+
render: r => r.date({ type: 'week' }),
|
|
756
|
+
}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
**禁用日期:**
|
|
760
|
+
|
|
761
|
+
```typescript
|
|
762
|
+
{
|
|
763
|
+
key: 'date',
|
|
764
|
+
title: '日期',
|
|
765
|
+
render: r => r.date({
|
|
766
|
+
disabledDate: (value, date) => {
|
|
767
|
+
return date > new Date() // 禁用未来日期
|
|
768
|
+
},
|
|
769
|
+
}),
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### DateRange 日期范围
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
interface RenderDateRangeItemOptions {
|
|
777
|
+
placeholder?: string[]
|
|
778
|
+
clearable?: boolean
|
|
779
|
+
multiple?: boolean
|
|
780
|
+
type?: 'date' | 'year' | 'quarter' | 'month' | 'week'
|
|
781
|
+
valueFormat?: string // 默认: 'YYYY-MM-DD HH:mm:ss'
|
|
782
|
+
labelFormat?: string
|
|
783
|
+
disabledDate?: (value: string[], date: Date) => boolean
|
|
784
|
+
shortcuts?: ShortcutType[]
|
|
785
|
+
onChange?: (value: string[] | undefined) => void
|
|
786
|
+
autoSubmit?: boolean
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
**基础用法:**
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
const form = defineForm([
|
|
794
|
+
{
|
|
795
|
+
key: 'dateRange',
|
|
796
|
+
title: '日期范围',
|
|
797
|
+
render: r => r.dateRange(),
|
|
798
|
+
},
|
|
799
|
+
])
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**自定义格式:**
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
{
|
|
806
|
+
key: 'dateRange',
|
|
807
|
+
title: '日期范围',
|
|
808
|
+
render: r => r.dateRange({
|
|
809
|
+
valueFormat: 'YYYY-MM-DD',
|
|
810
|
+
labelFormat: 'YYYY年MM月DD日',
|
|
811
|
+
autoSubmit: true,
|
|
812
|
+
}),
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**自定义快捷选项:**
|
|
817
|
+
|
|
818
|
+
```typescript
|
|
819
|
+
{
|
|
820
|
+
key: 'dateRange',
|
|
821
|
+
title: '日期范围',
|
|
822
|
+
render: r => r.dateRange({
|
|
823
|
+
shortcuts: [
|
|
824
|
+
{
|
|
825
|
+
label: '最近7天',
|
|
826
|
+
value: () => {
|
|
827
|
+
const end = new Date()
|
|
828
|
+
const start = new Date()
|
|
829
|
+
start.setTime(start.getTime() - 7 * 24 * 3600 * 1000)
|
|
830
|
+
return [start, end]
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
}),
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Currency 金额输入
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
interface RenderCurrencyOptions {
|
|
842
|
+
placeholder?: string
|
|
843
|
+
clearable?: boolean
|
|
844
|
+
readonly?: boolean
|
|
845
|
+
prefix?: string | (() => JSX.Element)
|
|
846
|
+
suffix?: string | (() => JSX.Element)
|
|
847
|
+
thousands?: boolean // 默认: true
|
|
848
|
+
precision?: number
|
|
849
|
+
inputUnit?: '分' | '元' | '万' // 默认: '元'
|
|
850
|
+
outputUnit?: '分' | '元' | '万' // 默认: '元'
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
**基础用法:**
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
const form = defineForm([
|
|
858
|
+
{
|
|
859
|
+
key: 'price',
|
|
860
|
+
title: '价格',
|
|
861
|
+
render: r => r.currency(),
|
|
862
|
+
},
|
|
863
|
+
])
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**单位转换(分转元):**
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
{
|
|
870
|
+
key: 'amount',
|
|
871
|
+
title: '金额',
|
|
872
|
+
render: r => r.currency({
|
|
873
|
+
inputUnit: '元', // 输入时显示单位
|
|
874
|
+
outputUnit: '分', // 输出时转换为单位
|
|
875
|
+
precision: 2,
|
|
876
|
+
}),
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
**千分位显示:**
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
{
|
|
884
|
+
key: 'total',
|
|
885
|
+
title: '总额',
|
|
886
|
+
render: r => r.currency({
|
|
887
|
+
thousands: true,
|
|
888
|
+
suffix: '元',
|
|
889
|
+
}),
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### Render 自定义渲染
|
|
894
|
+
|
|
895
|
+
```typescript
|
|
896
|
+
interface RenderInputItemOptions<T> {
|
|
897
|
+
(record: T, form?: FormItemOptions<T>): JSX.Element
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
**自定义渲染:**
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
const form = defineForm([
|
|
905
|
+
{
|
|
906
|
+
key: 'custom',
|
|
907
|
+
title: '自定义字段',
|
|
908
|
+
render: r => r.render((data, form) => {
|
|
909
|
+
return (
|
|
910
|
+
<div class="flex gap-2">
|
|
911
|
+
<AInput v-model={data.custom} />
|
|
912
|
+
<AButton>按钮</AButton>
|
|
913
|
+
</div>
|
|
914
|
+
)
|
|
915
|
+
}),
|
|
916
|
+
},
|
|
917
|
+
])
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
## 表单项配置
|
|
921
|
+
|
|
922
|
+
### FormItemOptions 接口
|
|
923
|
+
|
|
924
|
+
```typescript
|
|
925
|
+
interface FormItemOptions<T = Record<string, any>> {
|
|
926
|
+
key: keyof T | string // 字段键名
|
|
927
|
+
title: string // 字段标签
|
|
928
|
+
default?: any | (() => any) | (() => Promise<any>) // 默认值
|
|
929
|
+
collapsed?: boolean // 是否在折叠状态下隐藏
|
|
930
|
+
group?: string | string[] // 分组(用于 dialog 模式的 tab 分组)
|
|
931
|
+
visiable?: boolean | ((record: T) => boolean) // 条件显隐
|
|
932
|
+
span?: number // 网格跨列数
|
|
933
|
+
hideLabel?: boolean // 隐藏标签
|
|
934
|
+
labelStyle?: CSSProperties // 标签样式
|
|
935
|
+
contentStyle?: CSSProperties // 内容样式
|
|
936
|
+
rule?: FieldRule | FieldRule[] // 验证规则
|
|
937
|
+
mode?: 'component' | 'text' // 显示模式
|
|
938
|
+
render?: FormItemRender<T> // 渲染函数
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### 配置项说明
|
|
943
|
+
|
|
944
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
945
|
+
|------|------|--------|------|
|
|
946
|
+
| `key` | `string` | - | 字段键名,对应表单数据中的属性名 |
|
|
947
|
+
| `title` | `string` | - | 字段标签文本 |
|
|
948
|
+
| `default` | `any \| (() => any)` | - | 默认值,支持函数形式 |
|
|
949
|
+
| `collapsed` | `boolean` | `false` | 在折叠模式下隐藏此字段 |
|
|
950
|
+
| `group` | `string \| string[]` | - | 字段分组,用于弹窗模式的 tab 切换 |
|
|
951
|
+
| `visiable` | `boolean \| ((record) => boolean)` | `true` | 条件显隐,支持响应式函数 |
|
|
952
|
+
| `span` | `number` | `1` | 网格跨列数 |
|
|
953
|
+
| `hideLabel` | `boolean` | `false` | 隐藏字段标签 |
|
|
954
|
+
| `labelStyle` | `CSSProperties` | - | 标签自定义样式 |
|
|
955
|
+
| `contentStyle` | `CSSProperties` | - | 内容区域自定义样式 |
|
|
956
|
+
| `rule` | `FieldRule \| FieldRule[]` | - | 表单验证规则 |
|
|
957
|
+
| `mode` | `'component' \| 'text'` | `'component'` | 显示模式,text 模式用于只读展示 |
|
|
958
|
+
| `render` | `FormItemRender<T>` | - | 字段渲染函数 |
|
|
959
|
+
|
|
960
|
+
### 条件显隐
|
|
961
|
+
|
|
962
|
+
```typescript
|
|
963
|
+
const form = defineForm([
|
|
964
|
+
{
|
|
965
|
+
key: 'type',
|
|
966
|
+
title: '类型',
|
|
967
|
+
render: r => r.select({
|
|
968
|
+
options: new Map([['1', '类型A'], ['2', '类型B']]),
|
|
969
|
+
}),
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
key: 'extra',
|
|
973
|
+
title: '额外信息',
|
|
974
|
+
// 当 type 为 '1' 时才显示
|
|
975
|
+
visiable: (data) => data.type === '1',
|
|
976
|
+
render: r => r.input(),
|
|
977
|
+
},
|
|
978
|
+
])
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
### 字段跨度
|
|
982
|
+
|
|
983
|
+
```typescript
|
|
984
|
+
const form = defineForm([
|
|
985
|
+
{
|
|
986
|
+
key: 'title',
|
|
987
|
+
title: '标题',
|
|
988
|
+
span: 2, // 跨 2 列
|
|
989
|
+
render: r => r.input(),
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
key: 'content',
|
|
993
|
+
title: '内容',
|
|
994
|
+
span: 2, // 跨 2 列
|
|
995
|
+
render: r => r.textarea(),
|
|
996
|
+
},
|
|
997
|
+
])
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### 字段分组
|
|
1001
|
+
|
|
1002
|
+
用于 `dialog` 折叠模式下的 tab 分组:
|
|
1003
|
+
|
|
1004
|
+
```typescript
|
|
1005
|
+
const form = defineForm([
|
|
1006
|
+
{
|
|
1007
|
+
key: 'name',
|
|
1008
|
+
title: '姓名',
|
|
1009
|
+
group: ['基本信息'], // 属于"基本信息"分组
|
|
1010
|
+
render: r => r.input(),
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
key: 'address',
|
|
1014
|
+
title: '地址',
|
|
1015
|
+
group: ['联系方式'], // 属于"联系方式"分组
|
|
1016
|
+
render: r => r.input(),
|
|
1017
|
+
},
|
|
1018
|
+
])
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
## FormRender 组件
|
|
1022
|
+
|
|
1023
|
+
### Props
|
|
1024
|
+
|
|
1025
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
1026
|
+
|------|------|--------|------|
|
|
1027
|
+
| `form` | `FormItemsOptions<T>` | - | **必填**,表单配置数组 |
|
|
1028
|
+
| `value` | `Record<string, any>` | - | 初始表单数据(非响应式) |
|
|
1029
|
+
| `modelValue` | `Record<string, any>` | - | 初始表单数据(支持 v-model) |
|
|
1030
|
+
| `layout` | `'horizontal' \| 'vertical'` | `'horizontal'` | 表单布局 |
|
|
1031
|
+
| `columns` | `number` | 自动计算 | 固定列数 |
|
|
1032
|
+
| `minWidth` | `number` | `400` | 每列最小宽度(用于自动计算列数) |
|
|
1033
|
+
| `name` | `string` | - | 表单 name 属性 |
|
|
1034
|
+
| `id` | `string` | - | 表单 id 属性 |
|
|
1035
|
+
| `collapsedMode` | `'append' \| 'dialog'` | `'append'` | 折叠模式 |
|
|
1036
|
+
| `collapsedDialogColumns` | `number` | `2` | 弹窗模式的列数 |
|
|
1037
|
+
| `showFormResult` | `boolean` | dialog 模式为 `true` | 显示已选条件标签 |
|
|
1038
|
+
| `submitable` | `boolean` | `false` | 显示提交/取消按钮 |
|
|
1039
|
+
| `searchable` | `boolean` | `false` | 显示搜索/重置按钮 |
|
|
1040
|
+
| `resetable` | `boolean` | `true` | 显示重置按钮 |
|
|
1041
|
+
| `footer` | `boolean` | `false` | 操作按钮使用底部样式 |
|
|
1042
|
+
| `rowGap` | `number` | `6` | 行间距 |
|
|
1043
|
+
| `colGap` | `number` | `12` | 列间距 |
|
|
1044
|
+
|
|
1045
|
+
### Events
|
|
1046
|
+
|
|
1047
|
+
| 事件 | 参数 | 说明 |
|
|
1048
|
+
|------|------|------|
|
|
1049
|
+
| `submit` | `formSource: Record<string, any>` | 表单提交时触发 |
|
|
1050
|
+
| `cancel` | - | 点击取消按钮时触发 |
|
|
1051
|
+
| `update:model-value` | `formSource: Record<string, any>` | 表单数据更新时触发 |
|
|
1052
|
+
|
|
1053
|
+
### Expose Methods
|
|
1054
|
+
|
|
1055
|
+
| 方法 | 参数 | 返回值 | 说明 |
|
|
1056
|
+
|------|------|--------|------|
|
|
1057
|
+
| `formSource` | - | `Ref<DataRecord>` | 响应式表单数据 |
|
|
1058
|
+
| `updateFormField` | `(key: string, value: any)` | `void` | 更新单个字段 |
|
|
1059
|
+
| `updateFormSource` | `(value: DataRecord)` | `void` | 更新整个表单 |
|
|
1060
|
+
| `reset` | - | `void` | 重置表单 |
|
|
1061
|
+
| `validate` | - | `Promise` | 验证表单 |
|
|
1062
|
+
|
|
1063
|
+
### Slots
|
|
1064
|
+
|
|
1065
|
+
| 插槽 | 说明 |
|
|
1066
|
+
|------|------|
|
|
1067
|
+
| `actions` | 自定义操作按钮 |
|
|
1068
|
+
|
|
1069
|
+
### 使用示例
|
|
1070
|
+
|
|
1071
|
+
```vue
|
|
1072
|
+
<script setup lang="ts">
|
|
1073
|
+
import { ref } from 'vue'
|
|
1074
|
+
import { defineForm, FormRender } from '@gopowerteam/form-render'
|
|
1075
|
+
|
|
1076
|
+
const formRef = ref()
|
|
1077
|
+
const formData = ref({ name: '初始值' })
|
|
1078
|
+
|
|
1079
|
+
const form = defineForm([
|
|
1080
|
+
{ key: 'name', title: '姓名', render: r => r.input() },
|
|
1081
|
+
])
|
|
1082
|
+
|
|
1083
|
+
const handleSubmit = (data) => {
|
|
1084
|
+
console.log('提交:', data)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const handleValidate = async () => {
|
|
1088
|
+
try {
|
|
1089
|
+
await formRef.value?.validate()
|
|
1090
|
+
console.log('验证通过')
|
|
1091
|
+
} catch (errors) {
|
|
1092
|
+
console.log('验证失败:', errors)
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const handleReset = () => {
|
|
1097
|
+
formRef.value?.reset()
|
|
1098
|
+
}
|
|
1099
|
+
</script>
|
|
1100
|
+
|
|
1101
|
+
<template>
|
|
1102
|
+
<FormRender
|
|
1103
|
+
ref="formRef"
|
|
1104
|
+
:form="form"
|
|
1105
|
+
v-model="formData"
|
|
1106
|
+
searchable
|
|
1107
|
+
@submit="handleSubmit"
|
|
1108
|
+
>
|
|
1109
|
+
<template #actions>
|
|
1110
|
+
<AButton @click="handleValidate">验证</AButton>
|
|
1111
|
+
<AButton @click="handleReset">重置</AButton>
|
|
1112
|
+
</template>
|
|
1113
|
+
</FormRender>
|
|
1114
|
+
</template>
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
## useForm Hook
|
|
1118
|
+
|
|
1119
|
+
通过 `useForm` 获取表单实例的引用:
|
|
1120
|
+
|
|
1121
|
+
```typescript
|
|
1122
|
+
import { useForm } from '@gopowerteam/form-render'
|
|
1123
|
+
|
|
1124
|
+
const form = useForm('formRef')
|
|
1125
|
+
|
|
1126
|
+
// 访问表单数据
|
|
1127
|
+
console.log(form.value.formSource)
|
|
1128
|
+
|
|
1129
|
+
// 更新字段
|
|
1130
|
+
form.value.updateFormField('name', '新值')
|
|
1131
|
+
|
|
1132
|
+
// 重置表单
|
|
1133
|
+
form.value.reset()
|
|
1134
|
+
|
|
1135
|
+
// 验证表单
|
|
1136
|
+
await form.value.validate()
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
### 完整示例
|
|
1140
|
+
|
|
1141
|
+
```vue
|
|
1142
|
+
<script setup lang="ts">
|
|
1143
|
+
import { defineForm, FormRender, useForm } from '@gopowerteam/form-render'
|
|
1144
|
+
|
|
1145
|
+
const form = defineForm([
|
|
1146
|
+
{ key: 'name', title: '姓名', render: r => r.input() },
|
|
1147
|
+
{ key: 'status', title: '状态', render: r => r.select({ options: new Map() }) },
|
|
1148
|
+
])
|
|
1149
|
+
|
|
1150
|
+
const formRef = useForm('myForm')
|
|
1151
|
+
|
|
1152
|
+
const updateName = () => {
|
|
1153
|
+
formRef.value.updateFormField('name', '新名字')
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const getFormData = () => {
|
|
1157
|
+
console.log(formRef.value.formSource)
|
|
1158
|
+
}
|
|
1159
|
+
</script>
|
|
1160
|
+
|
|
1161
|
+
<template>
|
|
1162
|
+
<FormRender ref="myForm" :form="form" searchable />
|
|
1163
|
+
<AButton @click="updateName">更新姓名</AButton>
|
|
1164
|
+
<AButton @click="getFormData">获取数据</AButton>
|
|
1165
|
+
</template>
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
## 高级功能
|
|
1169
|
+
|
|
1170
|
+
### 表单验证
|
|
1171
|
+
|
|
1172
|
+
使用 Arco Design 的 `FieldRule` 进行验证:
|
|
1173
|
+
|
|
1174
|
+
```typescript
|
|
1175
|
+
import type { FieldRule } from '@arco-design/web-vue'
|
|
1176
|
+
|
|
1177
|
+
const form = defineForm([
|
|
1178
|
+
{
|
|
1179
|
+
key: 'name',
|
|
1180
|
+
title: '姓名',
|
|
1181
|
+
rule: [{ required: true, message: '请输入姓名' }],
|
|
1182
|
+
render: r => r.input(),
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
key: 'email',
|
|
1186
|
+
title: '邮箱',
|
|
1187
|
+
rule: [
|
|
1188
|
+
{ required: true, message: '请输入邮箱' },
|
|
1189
|
+
{ type: 'email', message: '邮箱格式不正确' },
|
|
1190
|
+
],
|
|
1191
|
+
render: r => r.input(),
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
key: 'password',
|
|
1195
|
+
title: '密码',
|
|
1196
|
+
rule: [
|
|
1197
|
+
{ required: true, message: '请输入密码' },
|
|
1198
|
+
{ minLength: 6, message: '密码至少6位' },
|
|
1199
|
+
],
|
|
1200
|
+
render: r => r.input(),
|
|
1201
|
+
},
|
|
1202
|
+
])
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
**自定义验证器:**
|
|
1206
|
+
|
|
1207
|
+
```typescript
|
|
1208
|
+
{
|
|
1209
|
+
key: 'confirmPassword',
|
|
1210
|
+
title: '确认密码',
|
|
1211
|
+
rule: [
|
|
1212
|
+
{
|
|
1213
|
+
validator: (value, callback) => {
|
|
1214
|
+
if (value !== formData.password) {
|
|
1215
|
+
callback('两次密码不一致')
|
|
1216
|
+
} else {
|
|
1217
|
+
callback()
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
],
|
|
1222
|
+
render: r => r.input(),
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### 动态显隐
|
|
1227
|
+
|
|
1228
|
+
基于表单数据动态控制字段显隐:
|
|
1229
|
+
|
|
1230
|
+
```typescript
|
|
1231
|
+
const form = defineForm([
|
|
1232
|
+
{
|
|
1233
|
+
key: 'type',
|
|
1234
|
+
title: '类型',
|
|
1235
|
+
render: r => r.radio({
|
|
1236
|
+
type: 'button',
|
|
1237
|
+
options: new Map([['personal', '个人'], ['company', '企业']]),
|
|
1238
|
+
}),
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
key: 'companyName',
|
|
1242
|
+
title: '公司名称',
|
|
1243
|
+
visiable: (data) => data.type === 'company',
|
|
1244
|
+
rule: [{ required: true, message: '请输入公司名称' }],
|
|
1245
|
+
render: r => r.input(),
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
key: 'idCard',
|
|
1249
|
+
title: '身份证号',
|
|
1250
|
+
visiable: (data) => data.type === 'personal',
|
|
1251
|
+
render: r => r.input(),
|
|
1252
|
+
},
|
|
1253
|
+
])
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### 异步选项加载
|
|
1257
|
+
|
|
1258
|
+
支持 Promise 形式的异步选项:
|
|
1259
|
+
|
|
1260
|
+
```typescript
|
|
1261
|
+
// 模拟 API 请求
|
|
1262
|
+
const fetchOptions = () => {
|
|
1263
|
+
return new Promise<Map<string, string>>((resolve) => {
|
|
1264
|
+
setTimeout(() => {
|
|
1265
|
+
resolve(new Map([
|
|
1266
|
+
['1', '选项1'],
|
|
1267
|
+
['2', '选项2'],
|
|
1268
|
+
['3', '选项3'],
|
|
1269
|
+
]))
|
|
1270
|
+
}, 1000)
|
|
1271
|
+
})
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const form = defineForm([
|
|
1275
|
+
{
|
|
1276
|
+
key: 'category',
|
|
1277
|
+
title: '分类',
|
|
1278
|
+
render: r => r.select({
|
|
1279
|
+
options: fetchOptions,
|
|
1280
|
+
cache: true, // 开启缓存
|
|
1281
|
+
}),
|
|
1282
|
+
},
|
|
1283
|
+
])
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
### 自动提交
|
|
1287
|
+
|
|
1288
|
+
字段值变化后自动触发表单提交:
|
|
1289
|
+
|
|
1290
|
+
```typescript
|
|
1291
|
+
const form = defineForm([
|
|
1292
|
+
{
|
|
1293
|
+
key: 'keyword',
|
|
1294
|
+
title: '关键词',
|
|
1295
|
+
render: r => r.input({ autoSubmit: true }),
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
key: 'status',
|
|
1299
|
+
title: '状态',
|
|
1300
|
+
render: r => r.select({
|
|
1301
|
+
options: statusOptions,
|
|
1302
|
+
autoSubmit: true, // 选择后立即提交
|
|
1303
|
+
}),
|
|
1304
|
+
},
|
|
1305
|
+
])
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
### 搜索结果展示
|
|
1309
|
+
|
|
1310
|
+
在 `dialog` 模式下,可以显示已选择的搜索条件:
|
|
1311
|
+
|
|
1312
|
+
```vue
|
|
1313
|
+
<FormRender
|
|
1314
|
+
:form="form"
|
|
1315
|
+
collapsed-mode="dialog"
|
|
1316
|
+
:show-form-result="true"
|
|
1317
|
+
searchable
|
|
1318
|
+
@submit="handleSearch"
|
|
1319
|
+
/>
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
## 与 table-render 集成
|
|
1323
|
+
|
|
1324
|
+
`form-render` 可以与 `@gopowerteam/table-render` 配合使用,作为表格的搜索表单:
|
|
1325
|
+
|
|
1326
|
+
```vue
|
|
1327
|
+
<script setup lang="ts">
|
|
1328
|
+
import { defineForm } from '@gopowerteam/form-render'
|
|
1329
|
+
import { defineColumns, defineTableLoad, useTable, TableRender } from '@gopowerteam/table-render'
|
|
1330
|
+
|
|
1331
|
+
// 定义搜索表单
|
|
1332
|
+
const form = defineForm([
|
|
1333
|
+
{
|
|
1334
|
+
key: 'keyword',
|
|
1335
|
+
title: '关键词',
|
|
1336
|
+
hideLabel: true,
|
|
1337
|
+
render: r => r.input({ placeholder: '请输入关键词', autoSubmit: true }),
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
key: 'status',
|
|
1341
|
+
title: '状态',
|
|
1342
|
+
hideLabel: true,
|
|
1343
|
+
render: r => r.select({
|
|
1344
|
+
options: new Map([['1', '启用'], ['0', '禁用']]),
|
|
1345
|
+
clearable: true,
|
|
1346
|
+
autoSubmit: true,
|
|
1347
|
+
}),
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
key: 'dateRange',
|
|
1351
|
+
title: '日期',
|
|
1352
|
+
hideLabel: true,
|
|
1353
|
+
collapsed: true,
|
|
1354
|
+
render: r => r.dateRange(),
|
|
1355
|
+
},
|
|
1356
|
+
])
|
|
1357
|
+
|
|
1358
|
+
// 定义表格列
|
|
1359
|
+
const columns = defineColumns([
|
|
1360
|
+
{ key: 'name', title: '名称' },
|
|
1361
|
+
{ key: 'status', title: '状态' },
|
|
1362
|
+
{ key: 'createdAt', title: '创建时间' },
|
|
1363
|
+
])
|
|
1364
|
+
|
|
1365
|
+
// 定义数据加载
|
|
1366
|
+
const table = useTable('tableRef')
|
|
1367
|
+
const onTableLoad = defineTableLoad(async ({ form, update, page }) => {
|
|
1368
|
+
const res = await fetch('/api/list', {
|
|
1369
|
+
method: 'POST',
|
|
1370
|
+
body: JSON.stringify({ ...form, page }),
|
|
1371
|
+
})
|
|
1372
|
+
const data = await res.json()
|
|
1373
|
+
update(data.list)
|
|
1374
|
+
page.total = data.total
|
|
1375
|
+
})
|
|
1376
|
+
</script>
|
|
1377
|
+
|
|
1378
|
+
<template>
|
|
1379
|
+
<TableRender
|
|
1380
|
+
ref="tableRef"
|
|
1381
|
+
:form="form"
|
|
1382
|
+
:form-options="{ minWidth: 300 }"
|
|
1383
|
+
:columns="columns"
|
|
1384
|
+
:data-load="onTableLoad"
|
|
1385
|
+
collapsable
|
|
1386
|
+
pageable
|
|
1387
|
+
searchable
|
|
1388
|
+
/>
|
|
1389
|
+
</template>
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
### 通过 table 实例访问表单
|
|
1393
|
+
|
|
1394
|
+
```typescript
|
|
1395
|
+
const table = useTable('tableRef')
|
|
1396
|
+
|
|
1397
|
+
// 获取表单数据
|
|
1398
|
+
console.log(table.value.formSource)
|
|
1399
|
+
|
|
1400
|
+
// 更新表单字段
|
|
1401
|
+
table.value.formInstance?.updateFormField('status', '1')
|
|
1402
|
+
|
|
1403
|
+
// 重置表单
|
|
1404
|
+
table.value.formInstance?.reset()
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
## 工具函数
|
|
1408
|
+
|
|
1409
|
+
### createFormSource
|
|
1410
|
+
|
|
1411
|
+
手动创建表单数据源:
|
|
1412
|
+
|
|
1413
|
+
```typescript
|
|
1414
|
+
import { createFormSource } from '@gopowerteam/form-render'
|
|
1415
|
+
|
|
1416
|
+
const form = defineForm([
|
|
1417
|
+
{ key: 'name', title: '姓名', default: '默认名' },
|
|
1418
|
+
{ key: 'age', title: '年龄', default: 18 },
|
|
1419
|
+
])
|
|
1420
|
+
|
|
1421
|
+
// 创建响应式数据源
|
|
1422
|
+
const [formSource, updateFormSource] = createFormSource(form, { name: '初始名' })
|
|
1423
|
+
|
|
1424
|
+
console.log(formSource.value) // { name: '初始名', age: 18 }
|
|
1425
|
+
|
|
1426
|
+
// 更新数据
|
|
1427
|
+
updateFormSource({ name: '新名字', age: 20 })
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
### FormRenderResolver
|
|
1431
|
+
|
|
1432
|
+
用于 `unplugin-vue-components` 自动导入:
|
|
1433
|
+
|
|
1434
|
+
```typescript
|
|
1435
|
+
// vite.config.ts
|
|
1436
|
+
import Components from 'unplugin-vue-components/vite'
|
|
1437
|
+
import { FormRenderResolver } from '@gopowerteam/form-render'
|
|
1438
|
+
|
|
1439
|
+
export default {
|
|
1440
|
+
plugins: [
|
|
1441
|
+
Components({
|
|
1442
|
+
resolvers: [FormRenderResolver()],
|
|
1443
|
+
}),
|
|
1444
|
+
],
|
|
1445
|
+
}
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
## TypeScript 类型
|
|
1449
|
+
|
|
1450
|
+
### 导出的类型
|
|
1451
|
+
|
|
1452
|
+
```typescript
|
|
1453
|
+
// 核心类型
|
|
1454
|
+
export type { DataRecord, DataProp } from './interfaces'
|
|
1455
|
+
export type { FormItemsOptions, FormItemOptions, FormItemRender, FormItemRenderFun, FormItemRenderReturn } from './interfaces'
|
|
1456
|
+
|
|
1457
|
+
// 组件实例
|
|
1458
|
+
export type { FormRenderInstance } from './form-render'
|
|
1459
|
+
|
|
1460
|
+
// 字段选项类型
|
|
1461
|
+
export type { RenderInputItemOptions } from './form-items/input'
|
|
1462
|
+
export type { RenderTextareaItemOptions } from './form-items/textarea'
|
|
1463
|
+
export type { RenderSelectItemOptions, SelectOptions } from './form-items/select'
|
|
1464
|
+
export type { RenderTreeSelectItemOptions } from './form-items/tree-select'
|
|
1465
|
+
export type { RenderCascaderItemOptions } from './form-items/cascader'
|
|
1466
|
+
export type { RenderRadioItemOptions, RadioOptions } from './form-items/radio'
|
|
1467
|
+
export type { RenderSwitchItemOptions } from './form-items/switch'
|
|
1468
|
+
export type { RenderDateItemOptions } from './form-items/date'
|
|
1469
|
+
export type { RenderDateRangeItemOptions } from './form-items/date-range'
|
|
1470
|
+
export type { RenderCurrencyOptions } from './form-items/currency'
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
### 泛型使用
|
|
1474
|
+
|
|
1475
|
+
```typescript
|
|
1476
|
+
interface UserFormData {
|
|
1477
|
+
name: string
|
|
1478
|
+
email: string
|
|
1479
|
+
age: number
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// 使用泛型获得完整的类型提示
|
|
1483
|
+
const form = defineForm<UserFormData>([
|
|
1484
|
+
{
|
|
1485
|
+
key: 'name', // 类型为 'name' | 'email' | 'age'
|
|
1486
|
+
title: '姓名',
|
|
1487
|
+
render: r => r.input(),
|
|
1488
|
+
},
|
|
1489
|
+
])
|
|
1490
|
+
|
|
1491
|
+
// 组件 Props 类型
|
|
1492
|
+
const props = defineProps<{
|
|
1493
|
+
form: FormItemsOptions<UserFormData>
|
|
1494
|
+
}>()
|
|
1495
|
+
|
|
1496
|
+
// 实例类型
|
|
1497
|
+
const formRef = ref<FormRenderInstance>()
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
## 最佳实践
|
|
1501
|
+
|
|
1502
|
+
### 1. 使用 TypeScript 泛型
|
|
1503
|
+
|
|
1504
|
+
始终使用泛型参数获得类型提示:
|
|
1505
|
+
|
|
1506
|
+
```typescript
|
|
1507
|
+
// ✅ 推荐
|
|
1508
|
+
interface FormData { name: string }
|
|
1509
|
+
const form = defineForm<FormData>([...])
|
|
1510
|
+
|
|
1511
|
+
// ❌ 不推荐
|
|
1512
|
+
const form = defineForm([...])
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
### 2. 提取选项配置
|
|
1516
|
+
|
|
1517
|
+
将复杂的选项配置提取为常量:
|
|
1518
|
+
|
|
1519
|
+
```typescript
|
|
1520
|
+
const STATUS_OPTIONS = new Map([
|
|
1521
|
+
['1', '启用'],
|
|
1522
|
+
['0', '禁用'],
|
|
1523
|
+
])
|
|
1524
|
+
|
|
1525
|
+
const form = defineForm([
|
|
1526
|
+
{
|
|
1527
|
+
key: 'status',
|
|
1528
|
+
title: '状态',
|
|
1529
|
+
render: r => r.select({ options: STATUS_OPTIONS }),
|
|
1530
|
+
},
|
|
1531
|
+
])
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
### 3. 合理使用 autoSubmit
|
|
1535
|
+
|
|
1536
|
+
在搜索表单中,对常用筛选条件开启自动提交:
|
|
1537
|
+
|
|
1538
|
+
```typescript
|
|
1539
|
+
{
|
|
1540
|
+
key: 'status',
|
|
1541
|
+
title: '状态',
|
|
1542
|
+
render: r => r.select({
|
|
1543
|
+
options: STATUS_OPTIONS,
|
|
1544
|
+
autoSubmit: true, // 选择后立即搜索
|
|
1545
|
+
}),
|
|
1546
|
+
}
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
### 4. 使用 hideLabel 优化搜索表单
|
|
1550
|
+
|
|
1551
|
+
搜索表单中可以隐藏标签以节省空间:
|
|
1552
|
+
|
|
1553
|
+
```typescript
|
|
1554
|
+
const form = defineForm([
|
|
1555
|
+
{
|
|
1556
|
+
key: 'keyword',
|
|
1557
|
+
title: '关键词',
|
|
1558
|
+
hideLabel: true,
|
|
1559
|
+
render: r => r.input({ placeholder: '请输入关键词' }),
|
|
1560
|
+
},
|
|
1561
|
+
])
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
### 5. 异步选项使用缓存
|
|
1565
|
+
|
|
1566
|
+
避免重复请求相同的选项数据:
|
|
1567
|
+
|
|
1568
|
+
```typescript
|
|
1569
|
+
{
|
|
1570
|
+
key: 'category',
|
|
1571
|
+
title: '分类',
|
|
1572
|
+
render: r => r.select({
|
|
1573
|
+
options: fetchCategories,
|
|
1574
|
+
cache: true, // 开启缓存
|
|
1575
|
+
}),
|
|
1576
|
+
}
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
### 6. 合理使用折叠模式
|
|
1580
|
+
|
|
1581
|
+
对于字段较多的表单,使用折叠模式提升用户体验:
|
|
1582
|
+
|
|
1583
|
+
```typescript
|
|
1584
|
+
const form = defineForm([
|
|
1585
|
+
{ key: 'keyword', title: '关键词', render: r => r.input() },
|
|
1586
|
+
{ key: 'status', title: '状态', render: r => r.select({ options: STATUS_OPTIONS }) },
|
|
1587
|
+
// 以下字段默认折叠
|
|
1588
|
+
{ key: 'dateRange', title: '日期范围', collapsed: true, render: r => r.dateRange() },
|
|
1589
|
+
{ key: 'category', title: '分类', collapsed: true, render: r => r.select({ options: [] }) },
|
|
1590
|
+
])
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
## License
|
|
1594
|
+
|
|
1595
|
+
MIT
|