@ticatec/uniface-flexi-module 0.0.2
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/FLEXICRITERIASET_GUIDE.md +1559 -0
- package/FLEXICRITERIASET_GUIDE_CN.md +1133 -0
- package/FLEXIDATATABLE_GUIDE.md +1650 -0
- package/FLEXIDATATABLE_GUIDE_CN.md +1650 -0
- package/FLEXIFORM_GUIDE.md +1068 -0
- package/FLEXIFORM_GUIDE_CN.md +1068 -0
- package/FLEXI_CONTEXT_GUIDE_CN.md +172 -0
- package/MODULE_LOADER_CN.md +228 -0
- package/README.md +307 -0
- package/README_CN.md +51 -0
- package/SANDBOX_CN.md +201 -0
- package/dist/FlexiContext.d.ts +28 -0
- package/dist/FlexiContext.js +45 -0
- package/dist/ModuleLoader.d.ts +41 -0
- package/dist/ModuleLoader.js +55 -0
- package/dist/Sandbox.d.ts +33 -0
- package/dist/Sandbox.js +101 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte +26 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte.d.ts +22 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte +10 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateRangeField.svelte +11 -0
- package/dist/criteria-panel/components/DateRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte +11 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte +9 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/SearchField.svelte +14 -0
- package/dist/criteria-panel/components/SearchField.svelte.d.ts +33 -0
- package/dist/criteria-panel/components/TextSearchField.svelte +9 -0
- package/dist/criteria-panel/components/TextSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte +9 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte.d.ts +24 -0
- package/dist/criteria-panel/index.d.ts +6 -0
- package/dist/criteria-panel/index.js +6 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.d.ts +19 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.js +31 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.d.ts +1 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.js +127 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.d.ts +38 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.js +31 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.d.ts +24 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.js +48 -0
- package/dist/flexi-datatable/FlexiDataTable.d.ts +111 -0
- package/dist/flexi-datatable/FlexiDataTable.js +90 -0
- package/dist/flexi-datatable/index.d.ts +2 -0
- package/dist/flexi-datatable/index.js +2 -0
- package/dist/flexi-form/FlexiCompound.d.ts +34 -0
- package/dist/flexi-form/FlexiCompound.js +84 -0
- package/dist/flexi-form/FlexiFormDialog.svelte +24 -0
- package/dist/flexi-form/FlexiFormDialog.svelte.d.ts +21 -0
- package/dist/flexi-form/FlexiFormPage.svelte +26 -0
- package/dist/flexi-form/FlexiFormPage.svelte.d.ts +25 -0
- package/dist/flexi-form/Schema.d.ts +6 -0
- package/dist/flexi-form/Schema.js +1 -0
- package/dist/flexi-form/components/BreakLine.svelte +1 -0
- package/dist/flexi-form/components/BreakLine.svelte.d.ts +26 -0
- package/dist/flexi-form/components/CardTitleBar.svelte +18 -0
- package/dist/flexi-form/components/CardTitleBar.svelte.d.ts +22 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/CellFieldBuilder.d.ts +1 -0
- package/dist/flexi-form/components/CellFieldBuilder.js +178 -0
- package/dist/flexi-form/components/DateField.svelte +12 -0
- package/dist/flexi-form/components/DateField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/DateTimeField.svelte +13 -0
- package/dist/flexi-form/components/DateTimeField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/MemoField.svelte +12 -0
- package/dist/flexi-form/components/MemoField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/NumberField.svelte +12 -0
- package/dist/flexi-form/components/NumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/TextField.svelte +12 -0
- package/dist/flexi-form/components/TextField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnitNumberField.svelte +12 -0
- package/dist/flexi-form/components/UnitNumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte +5 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte.d.ts +18 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte +13 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte.d.ts +33 -0
- package/dist/flexi-form/flexi_card/FlexiCard.d.ts +64 -0
- package/dist/flexi-form/flexi_card/FlexiCard.js +66 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte +57 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte.d.ts +22 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.d.ts +50 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.js +26 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte +42 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_composite/README.md +50 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.d.ts +4 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.js +2 -0
- package/dist/flexi-form/flexi_field/FlexiField.d.ts +76 -0
- package/dist/flexi-form/flexi_field/FlexiField.js +128 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte +35 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_field/UnknownField.d.ts +3 -0
- package/dist/flexi-form/flexi_field/UnknownField.js +3 -0
- package/dist/flexi-form/flexi_form/FlexiForm.d.ts +127 -0
- package/dist/flexi-form/flexi_form/FlexiForm.js +160 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte +57 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte.d.ts +25 -0
- package/dist/flexi-form/index.d.ts +11 -0
- package/dist/flexi-form/index.js +11 -0
- package/dist/flexi-form/lib/ComponentBuilder.d.ts +15 -0
- package/dist/flexi-form/lib/ComponentBuilder.js +31 -0
- package/dist/flexi-form/lib/index.d.ts +5 -0
- package/dist/flexi-form/lib/index.js +2 -0
- package/dist/flexi-form/lib/types.d.ts +7 -0
- package/dist/flexi-form/lib/types.js +6 -0
- package/dist/flexi-form/lib/utils.d.ts +10 -0
- package/dist/flexi-form/lib/utils.js +48 -0
- package/dist/i18n-res/i18nRes.d.ts +2 -0
- package/dist/i18n-res/i18nRes.js +8 -0
- package/dist/i18n-res/index.d.ts +2 -0
- package/dist/i18n-res/index.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/uniface-flexi-module.css +46 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +8 -0
- package/package.json +135 -0
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
# FlexiForm 完整使用指南
|
|
2
|
+
|
|
3
|
+
在 Svelte 应用中构建动态、灵活表单的 FlexiForm 综合指南。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
1. [概述](#概述)
|
|
8
|
+
2. [基础用法](#基础用法)
|
|
9
|
+
3. [Schema 结构](#schema-结构)
|
|
10
|
+
4. [字段类型](#字段类型)
|
|
11
|
+
5. [布局和排列](#布局和排列)
|
|
12
|
+
6. [动态表单生成](#动态表单生成)
|
|
13
|
+
7. [事件和交互](#事件和交互)
|
|
14
|
+
8. [扩展 FlexiForm](#扩展-flexiform)
|
|
15
|
+
9. [高级模式](#高级模式)
|
|
16
|
+
10. [最佳实践](#最佳实践)
|
|
17
|
+
|
|
18
|
+
## 概述
|
|
19
|
+
|
|
20
|
+
FlexiForm 是一个强大的表单框架,可以从 JSON 配置生成动态表单。它支持多种布局模式、各种字段类型,并为自定义组件提供可扩展的架构。
|
|
21
|
+
|
|
22
|
+
### 主要特性
|
|
23
|
+
|
|
24
|
+
- **动态基于 Schema**:从 JSON 配置生成表单
|
|
25
|
+
- **灵活布局**:支持 flex 和 grid 布局
|
|
26
|
+
- **可扩展字段类型**:内置字段 + 自定义字段支持
|
|
27
|
+
- **事件系统**:丰富的表单交互事件处理
|
|
28
|
+
- **类型安全**:完整的 TypeScript 支持
|
|
29
|
+
- **模块化架构**:即插即用组件
|
|
30
|
+
|
|
31
|
+
## 基础用法
|
|
32
|
+
|
|
33
|
+
### 1. 安装和设置
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { FlexiFormPage, registerFormFieldBuilder } from '@ticatec/uniface-flexi-form/flexi-form';
|
|
37
|
+
import '@ticatec/uniface-flexi-form/uniface-flexi-form.css';
|
|
38
|
+
|
|
39
|
+
// 注册所有字段构建器(必需)
|
|
40
|
+
registerFormFieldBuilder();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. 简单表单示例
|
|
44
|
+
|
|
45
|
+
```svelte
|
|
46
|
+
<!-- SimpleForm.svelte -->
|
|
47
|
+
<script lang="ts">
|
|
48
|
+
import { FlexiFormPage } from '@ticatec/uniface-flexi-form/flexi-form';
|
|
49
|
+
|
|
50
|
+
// 表单数据
|
|
51
|
+
let userData = {
|
|
52
|
+
name: '',
|
|
53
|
+
email: '',
|
|
54
|
+
age: null,
|
|
55
|
+
birthDate: null
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 表单配置
|
|
59
|
+
const userFormSchema = {
|
|
60
|
+
mode: 'flex',
|
|
61
|
+
arrangement: 'vertical',
|
|
62
|
+
variant: 'outlined',
|
|
63
|
+
elements: {
|
|
64
|
+
'user-info': {
|
|
65
|
+
title: '用户信息',
|
|
66
|
+
fields: [
|
|
67
|
+
{
|
|
68
|
+
type: 'text-editor',
|
|
69
|
+
keyField: 'name',
|
|
70
|
+
name: 'name',
|
|
71
|
+
label: '全名',
|
|
72
|
+
required: true,
|
|
73
|
+
props: {
|
|
74
|
+
placeholder: '请输入您的全名'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'text-editor',
|
|
79
|
+
keyField: 'email',
|
|
80
|
+
name: 'email',
|
|
81
|
+
label: '邮箱地址',
|
|
82
|
+
required: true,
|
|
83
|
+
props: {
|
|
84
|
+
type: 'email',
|
|
85
|
+
placeholder: '请输入您的邮箱'
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'number-editor',
|
|
90
|
+
keyField: 'age',
|
|
91
|
+
name: 'age',
|
|
92
|
+
label: '年龄',
|
|
93
|
+
props: {
|
|
94
|
+
min: 0,
|
|
95
|
+
max: 120
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'date-picker',
|
|
100
|
+
keyField: 'birthDate',
|
|
101
|
+
name: 'birthDate',
|
|
102
|
+
label: '出生日期'
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function handleFormChange(event) {
|
|
110
|
+
console.log('表单数据改变:', event.detail);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function handleSubmit() {
|
|
114
|
+
console.log('提交表单:', userData);
|
|
115
|
+
}
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<FlexiFormPage
|
|
119
|
+
schema={userFormSchema}
|
|
120
|
+
bind:data={userData}
|
|
121
|
+
on:change={handleFormChange}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<button on:click={handleSubmit}>提交</button>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Schema 结构
|
|
128
|
+
|
|
129
|
+
### FlexiFormSchema
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
interface FlexiFormSchema {
|
|
133
|
+
mode?: 'flex' | 'grid'; // 布局模式
|
|
134
|
+
arrangement?: 'vertical' | 'horizontal'; // 布局方向
|
|
135
|
+
props?: any; // 布局特定属性
|
|
136
|
+
elements: FormElements; // 表单卡片/部分
|
|
137
|
+
label$style?: string; // 标签样式
|
|
138
|
+
variant?: 'filled' | 'outlined' | ''; // 字段变体
|
|
139
|
+
actions?: Array<FormActionSchema>; // 表单操作
|
|
140
|
+
style?: string; // 自定义 CSS
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type FormElements = {
|
|
144
|
+
[cardKey: string]: FlexiCardSchema | FlexiBlockSchema;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### FlexiCardSchema
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
interface FlexiCardSchema {
|
|
152
|
+
title?: string; // 卡片标题
|
|
153
|
+
mode?: 'flex' | 'grid'; // 卡片布局模式
|
|
154
|
+
props?: any; // 布局属性
|
|
155
|
+
arrangement?: 'vertical' | 'horizontal'; // 布局方向
|
|
156
|
+
fields: Array<FlexiFieldSchema>; // 此卡片中的字段
|
|
157
|
+
readonly?: boolean; // 使卡片只读
|
|
158
|
+
disabled?: boolean; // 禁用卡片
|
|
159
|
+
variant?: 'filled' | 'outlined' | ''; // 字段变体
|
|
160
|
+
foldable?: boolean; // 允许折叠
|
|
161
|
+
actions?: Array<ActionSchema>; // 卡片操作
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### FlexiCompositeSchema
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface FlexiCompositeSchema {
|
|
169
|
+
type?: 'block'; // 块类型标识符
|
|
170
|
+
title?: string; // 块标题
|
|
171
|
+
fields: Array<FlexiFieldSchema>; // 此组合块中的字段
|
|
172
|
+
readonly?: boolean; // 使块只读
|
|
173
|
+
disabled?: boolean; // 禁用块
|
|
174
|
+
label$style?: string; // 标签样式
|
|
175
|
+
variant?: string; // 字段变体
|
|
176
|
+
mode?: LayoutMode; // 布局模式
|
|
177
|
+
props?: any; // 布局属性
|
|
178
|
+
arrangement?: Arrangement; // 布局方向
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### FlexiCompositeFieldSchema
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
interface FlexiCompositeFieldSchema {
|
|
186
|
+
type: "composite"; // 类型标识符
|
|
187
|
+
keyField?: string; // 嵌套对象的数据绑定路径
|
|
188
|
+
name: string; // 字段名称
|
|
189
|
+
composite: string; // 引用的组合配置名称
|
|
190
|
+
cell?: any; // 单元格属性
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### FlexiFieldSchema
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
interface FlexiFieldSchema {
|
|
198
|
+
type: string; // 字段类型标识符
|
|
199
|
+
keyField: string; // 数据绑定路径
|
|
200
|
+
name: string; // 字段名称
|
|
201
|
+
label: string; // 显示标签
|
|
202
|
+
dictName?: string; // 选项字典
|
|
203
|
+
variant?: 'filled' | 'outlined' | ''; // 字段变体
|
|
204
|
+
readonly?: boolean; // 字段只读状态
|
|
205
|
+
disabled?: boolean; // 字段禁用状态
|
|
206
|
+
required?: boolean; // 字段必填状态
|
|
207
|
+
events?: Record<string, string>; // 事件处理器
|
|
208
|
+
cell?: any; // 单元格属性
|
|
209
|
+
props?: any; // 字段特定属性
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 字段类型
|
|
214
|
+
|
|
215
|
+
### 内置字段类型
|
|
216
|
+
|
|
217
|
+
| 类型 | 描述 | 常用属性 |
|
|
218
|
+
|------|-------------|--------------|
|
|
219
|
+
| `text-editor` | 单行文本输入 | `placeholder`, `maxLength`, `pattern` |
|
|
220
|
+
| `memo-editor` | 多行文本区域 | `rows`, `placeholder`, `maxLength` |
|
|
221
|
+
| `number-editor` | 数字输入 | `min`, `max`, `step`, `precision` |
|
|
222
|
+
| `unit-number-editor` | 带单位数字输入 | `min`, `max`, `step`, `unit`, `precision` |
|
|
223
|
+
| `date-picker` | 日期选择 | `min`, `max`, `format` |
|
|
224
|
+
| `datetime-picker` | 日期时间选择 | `min`, `max`, `format` |
|
|
225
|
+
| `options-selector` | 单选下拉框 | `dictName`, `placeholder` |
|
|
226
|
+
| `options-multi-selector` | 多选下拉框 | `dictName`, `maxSelections` |
|
|
227
|
+
| `cascade-options-selector` | 级联选择 | `dictName`, `levels` |
|
|
228
|
+
| `input-options-selector` | 可搜索选择 | `getOptions`, `minLength` |
|
|
229
|
+
| `-` | 分隔线/换行符 | 视觉分隔符 |
|
|
230
|
+
|
|
231
|
+
### 字段示例
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// 带验证的文本字段
|
|
235
|
+
{
|
|
236
|
+
type: 'text-editor',
|
|
237
|
+
keyField: 'username',
|
|
238
|
+
name: 'username',
|
|
239
|
+
label: '用户名',
|
|
240
|
+
required: true,
|
|
241
|
+
props: {
|
|
242
|
+
placeholder: '请输入用户名',
|
|
243
|
+
maxLength: 50,
|
|
244
|
+
pattern: '^[a-zA-Z0-9_]+$'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 带范围的数字字段
|
|
249
|
+
{
|
|
250
|
+
type: 'number-editor',
|
|
251
|
+
keyField: 'price',
|
|
252
|
+
name: 'price',
|
|
253
|
+
label: '价格',
|
|
254
|
+
props: {
|
|
255
|
+
min: 0,
|
|
256
|
+
max: 9999.99,
|
|
257
|
+
step: 0.01,
|
|
258
|
+
prefix: '¥'
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 带约束的日期字段
|
|
263
|
+
{
|
|
264
|
+
type: 'date-picker',
|
|
265
|
+
keyField: 'startDate',
|
|
266
|
+
name: 'startDate',
|
|
267
|
+
label: '开始日期',
|
|
268
|
+
props: {
|
|
269
|
+
min: '2024-01-01',
|
|
270
|
+
max: '2024-12-31',
|
|
271
|
+
format: 'YYYY-MM-DD'
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 带字典的选择字段
|
|
276
|
+
{
|
|
277
|
+
type: 'options-selector',
|
|
278
|
+
keyField: 'category',
|
|
279
|
+
name: 'category',
|
|
280
|
+
label: '分类',
|
|
281
|
+
dictName: 'product-categories',
|
|
282
|
+
required: true,
|
|
283
|
+
props: {
|
|
284
|
+
placeholder: '请选择分类'
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## 布局和排列
|
|
290
|
+
|
|
291
|
+
### Flex 布局
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const flexFormSchema = {
|
|
295
|
+
mode: 'flex',
|
|
296
|
+
arrangement: 'vertical',
|
|
297
|
+
elements: {
|
|
298
|
+
'main-card': {
|
|
299
|
+
mode: 'flex',
|
|
300
|
+
arrangement: 'horizontal', // 字段并排
|
|
301
|
+
fields: [
|
|
302
|
+
{
|
|
303
|
+
type: 'text-editor',
|
|
304
|
+
keyField: 'firstName',
|
|
305
|
+
name: 'firstName',
|
|
306
|
+
label: '名字',
|
|
307
|
+
cell: { flex: 1 } // 等宽
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: 'text-editor',
|
|
311
|
+
keyField: 'lastName',
|
|
312
|
+
name: 'lastName',
|
|
313
|
+
label: '姓氏',
|
|
314
|
+
cell: { flex: 1 } // 等宽
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Grid 布局
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const gridFormSchema = {
|
|
326
|
+
mode: 'grid',
|
|
327
|
+
props: {
|
|
328
|
+
columns: 3, // 3列网格
|
|
329
|
+
gap: '16px'
|
|
330
|
+
},
|
|
331
|
+
elements: {
|
|
332
|
+
'contact-info': {
|
|
333
|
+
mode: 'grid',
|
|
334
|
+
props: {
|
|
335
|
+
columns: 2 // 此卡片2列网格
|
|
336
|
+
},
|
|
337
|
+
fields: [
|
|
338
|
+
{
|
|
339
|
+
type: 'text-editor',
|
|
340
|
+
keyField: 'email',
|
|
341
|
+
name: 'email',
|
|
342
|
+
label: '邮箱',
|
|
343
|
+
cell: { colspan: 2 } // 跨2列
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
type: 'text-editor',
|
|
347
|
+
keyField: 'phone',
|
|
348
|
+
name: 'phone',
|
|
349
|
+
label: '电话'
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
type: 'text-editor',
|
|
353
|
+
keyField: 'mobile',
|
|
354
|
+
name: 'mobile',
|
|
355
|
+
label: '手机'
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## 动态表单生成
|
|
364
|
+
|
|
365
|
+
### 从 API 加载 Schema
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
class DynamicFormLoader {
|
|
369
|
+
async loadFormSchema(formId: string): Promise<FlexiFormSchema> {
|
|
370
|
+
const response = await fetch(`/api/forms/${formId}/schema`);
|
|
371
|
+
return await response.json();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async loadFormData(formId: string, recordId?: string): Promise<any> {
|
|
375
|
+
const url = recordId
|
|
376
|
+
? `/api/forms/${formId}/data/${recordId}`
|
|
377
|
+
: `/api/forms/${formId}/data/new`;
|
|
378
|
+
const response = await fetch(url);
|
|
379
|
+
return await response.json();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 在组件中使用
|
|
384
|
+
const loader = new DynamicFormLoader();
|
|
385
|
+
let formSchema = null;
|
|
386
|
+
let formData = {};
|
|
387
|
+
|
|
388
|
+
onMount(async () => {
|
|
389
|
+
formSchema = await loader.loadFormSchema('user-profile');
|
|
390
|
+
formData = await loader.loadFormData('user-profile', userId);
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Schema 构建器模式
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
class FormSchemaBuilder {
|
|
398
|
+
private schema: FlexiFormSchema;
|
|
399
|
+
|
|
400
|
+
constructor() {
|
|
401
|
+
this.schema = {
|
|
402
|
+
mode: 'flex',
|
|
403
|
+
arrangement: 'vertical',
|
|
404
|
+
variant: 'outlined',
|
|
405
|
+
elements: {}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
addCard(key: string, title?: string): CardBuilder {
|
|
410
|
+
const cardBuilder = new CardBuilder(title);
|
|
411
|
+
this.schema.elements[key] = cardBuilder.getSchema();
|
|
412
|
+
return cardBuilder;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
setLayout(mode: 'flex' | 'grid', arrangement?: 'vertical' | 'horizontal') {
|
|
416
|
+
this.schema.mode = mode;
|
|
417
|
+
this.schema.arrangement = arrangement;
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
build(): FlexiFormSchema {
|
|
422
|
+
return this.schema;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
class CardBuilder {
|
|
427
|
+
private card: FlexiCardSchema;
|
|
428
|
+
|
|
429
|
+
constructor(title?: string) {
|
|
430
|
+
this.card = {
|
|
431
|
+
title,
|
|
432
|
+
fields: []
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
addTextField(keyField: string, label: string, options: any = {}) {
|
|
437
|
+
this.card.fields.push({
|
|
438
|
+
type: 'text-editor',
|
|
439
|
+
keyField,
|
|
440
|
+
name: keyField,
|
|
441
|
+
label,
|
|
442
|
+
...options
|
|
443
|
+
});
|
|
444
|
+
return this;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
addSelectField(keyField: string, label: string, dictName: string, options: any = {}) {
|
|
448
|
+
this.card.fields.push({
|
|
449
|
+
type: 'options-selector',
|
|
450
|
+
keyField,
|
|
451
|
+
name: keyField,
|
|
452
|
+
label,
|
|
453
|
+
dictName,
|
|
454
|
+
...options
|
|
455
|
+
});
|
|
456
|
+
return this;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
getSchema(): FlexiCardSchema {
|
|
460
|
+
return this.card;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 使用方法
|
|
465
|
+
const schema = new FormSchemaBuilder()
|
|
466
|
+
.setLayout('flex', 'vertical')
|
|
467
|
+
.addCard('personal', '个人信息')
|
|
468
|
+
.addTextField('name', '全名', { required: true })
|
|
469
|
+
.addTextField('email', '邮箱', { required: true })
|
|
470
|
+
.addSelectField('country', '国家', 'countries')
|
|
471
|
+
.build();
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## 事件和交互
|
|
475
|
+
|
|
476
|
+
### 表单级事件
|
|
477
|
+
|
|
478
|
+
```svelte
|
|
479
|
+
<script>
|
|
480
|
+
function handleFormChange(event) {
|
|
481
|
+
const { data, field, value } = event.detail;
|
|
482
|
+
console.log(`字段 ${field} 改变为:`, value);
|
|
483
|
+
console.log('完整表单数据:', data);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function handleFormSubmit(event) {
|
|
487
|
+
const { data, isValid } = event.detail;
|
|
488
|
+
if (isValid) {
|
|
489
|
+
submitForm(data);
|
|
490
|
+
} else {
|
|
491
|
+
console.error('表单验证失败');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function handleFormReset(event) {
|
|
496
|
+
console.log('表单已重置');
|
|
497
|
+
}
|
|
498
|
+
</script>
|
|
499
|
+
|
|
500
|
+
<FlexiFormPage
|
|
501
|
+
{schema}
|
|
502
|
+
bind:data={formData}
|
|
503
|
+
on:change={handleFormChange}
|
|
504
|
+
on:submit={handleFormSubmit}
|
|
505
|
+
on:reset={handleFormReset}
|
|
506
|
+
/>
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### 字段级事件
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// 在 schema 定义中
|
|
513
|
+
{
|
|
514
|
+
type: 'text-editor',
|
|
515
|
+
keyField: 'email',
|
|
516
|
+
name: 'email',
|
|
517
|
+
label: '邮箱',
|
|
518
|
+
events: {
|
|
519
|
+
change: 'onEmailChange',
|
|
520
|
+
blur: 'onEmailBlur',
|
|
521
|
+
focus: 'onEmailFocus'
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// 在表单实现类中
|
|
526
|
+
class UserForm extends FlexiForm {
|
|
527
|
+
onEmailChange(value: string) {
|
|
528
|
+
console.log('邮箱改变:', value);
|
|
529
|
+
// 触发邮箱验证
|
|
530
|
+
this.validateEmail(value);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
onEmailBlur(event: FocusEvent) {
|
|
534
|
+
console.log('邮箱字段失去焦点');
|
|
535
|
+
// 检查邮箱重复
|
|
536
|
+
this.checkEmailUniqueness(event.target.value);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
onEmailFocus(event: FocusEvent) {
|
|
540
|
+
console.log('邮箱字段获得焦点');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
## 扩展 FlexiForm
|
|
546
|
+
|
|
547
|
+
### 创建自定义字段类型
|
|
548
|
+
|
|
549
|
+
#### 1. 创建自定义字段组件
|
|
550
|
+
|
|
551
|
+
```svelte
|
|
552
|
+
<!-- CustomRatingField.svelte -->
|
|
553
|
+
<script lang="ts">
|
|
554
|
+
import type FlexiField from "$lib/flexi-form/flexi_field/FlexiField";
|
|
555
|
+
|
|
556
|
+
export let field: FlexiField;
|
|
557
|
+
export let readonly: boolean;
|
|
558
|
+
export let disabled: boolean;
|
|
559
|
+
export let variant: "" | "outlined" | "filled";
|
|
560
|
+
|
|
561
|
+
$: props = field.props;
|
|
562
|
+
$: data = field.data;
|
|
563
|
+
$: events = field.events;
|
|
564
|
+
|
|
565
|
+
const maxRating = props.maxRating || 5;
|
|
566
|
+
let currentRating = data[field.keyField] || 0;
|
|
567
|
+
|
|
568
|
+
function setRating(rating: number) {
|
|
569
|
+
if (readonly || disabled) return;
|
|
570
|
+
currentRating = rating;
|
|
571
|
+
data[field.keyField] = rating;
|
|
572
|
+
events.change?.(rating);
|
|
573
|
+
}
|
|
574
|
+
</script>
|
|
575
|
+
|
|
576
|
+
<div class="rating-field" class:readonly class:disabled>
|
|
577
|
+
<label>{field.label}</label>
|
|
578
|
+
<div class="stars">
|
|
579
|
+
{#each Array(maxRating) as _, i}
|
|
580
|
+
<button
|
|
581
|
+
type="button"
|
|
582
|
+
class="star"
|
|
583
|
+
class:filled={i < currentRating}
|
|
584
|
+
{disabled}
|
|
585
|
+
on:click={() => setRating(i + 1)}
|
|
586
|
+
>
|
|
587
|
+
⭐
|
|
588
|
+
</button>
|
|
589
|
+
{/each}
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<style>
|
|
594
|
+
.rating-field {
|
|
595
|
+
display: flex;
|
|
596
|
+
flex-direction: column;
|
|
597
|
+
gap: 8px;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.stars {
|
|
601
|
+
display: flex;
|
|
602
|
+
gap: 4px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.star {
|
|
606
|
+
background: none;
|
|
607
|
+
border: none;
|
|
608
|
+
font-size: 24px;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
opacity: 0.3;
|
|
611
|
+
transition: opacity 0.2s;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.star.filled {
|
|
615
|
+
opacity: 1;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.star:hover:not(:disabled) {
|
|
619
|
+
opacity: 0.7;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.readonly .star, .disabled .star {
|
|
623
|
+
cursor: default;
|
|
624
|
+
}
|
|
625
|
+
</style>
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
#### 2. 注册自定义字段类型
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
import { ComponentBuilder } from '@ticatec/uniface-flexi-form/flexi-form/lib/ComponentBuilder';
|
|
632
|
+
import CustomRatingField from './CustomRatingField.svelte';
|
|
633
|
+
|
|
634
|
+
// 创建构建器函数
|
|
635
|
+
const buildRatingField = (schema: FlexiFieldSchema, dictLoader: DictionaryLoader) => {
|
|
636
|
+
return {
|
|
637
|
+
component: CustomRatingField,
|
|
638
|
+
props: schema.props
|
|
639
|
+
};
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// 注册自定义字段类型
|
|
643
|
+
const componentBuilder = ComponentBuilder.getInstance();
|
|
644
|
+
componentBuilder.register('rating-field', buildRatingField);
|
|
645
|
+
|
|
646
|
+
// 在 schema 中使用
|
|
647
|
+
const schemaWithRating = {
|
|
648
|
+
mode: 'flex',
|
|
649
|
+
elements: {
|
|
650
|
+
'review-card': {
|
|
651
|
+
title: '产品评价',
|
|
652
|
+
fields: [
|
|
653
|
+
{
|
|
654
|
+
type: 'rating-field',
|
|
655
|
+
keyField: 'rating',
|
|
656
|
+
name: 'rating',
|
|
657
|
+
label: '产品评分',
|
|
658
|
+
props: {
|
|
659
|
+
maxRating: 5
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
type: 'memo-editor',
|
|
664
|
+
keyField: 'comment',
|
|
665
|
+
name: 'comment',
|
|
666
|
+
label: '评价意见'
|
|
667
|
+
}
|
|
668
|
+
]
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### 创建自定义表单类
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
import FlexiForm, { type FlexiFormSchema } from '@ticatec/uniface-flexi-form/flexi-form';
|
|
678
|
+
|
|
679
|
+
class UserProfileForm extends FlexiForm {
|
|
680
|
+
private userId: string;
|
|
681
|
+
|
|
682
|
+
constructor(userId: string, data: any, schema: FlexiFormSchema) {
|
|
683
|
+
super(data, schema);
|
|
684
|
+
this.userId = userId;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
protected buildCards(): void {
|
|
688
|
+
// 自定义卡片构建逻辑
|
|
689
|
+
for (const [key, element] of Object.entries(this.elements)) {
|
|
690
|
+
if (element.type === 'block') {
|
|
691
|
+
// 处理块元素
|
|
692
|
+
} else {
|
|
693
|
+
// 使用自定义逻辑创建卡片
|
|
694
|
+
const card = new CustomUserCard(this, element as FlexiCardSchema);
|
|
695
|
+
this.cards.push(card);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
getTitle(): string {
|
|
701
|
+
return `用户资料 - ${this.data.name || '新用户'}`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 自定义方法
|
|
705
|
+
async saveProfile(): Promise<void> {
|
|
706
|
+
try {
|
|
707
|
+
await fetch(`/api/users/${this.userId}`, {
|
|
708
|
+
method: 'PUT',
|
|
709
|
+
headers: { 'Content-Type': 'application/json' },
|
|
710
|
+
body: JSON.stringify(this.data)
|
|
711
|
+
});
|
|
712
|
+
console.log('资料保存成功');
|
|
713
|
+
} catch (error) {
|
|
714
|
+
console.error('保存资料失败:', error);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
validateEmail(email: string): boolean {
|
|
719
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
720
|
+
return emailRegex.test(email);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 事件处理器(从字段事件调用)
|
|
724
|
+
onEmailChange(value: string): void {
|
|
725
|
+
if (!this.validateEmail(value)) {
|
|
726
|
+
console.warn('邮箱格式无效');
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
onPhoneChange(value: string): void {
|
|
731
|
+
// 格式化电话号码
|
|
732
|
+
const formatted = this.formatPhoneNumber(value);
|
|
733
|
+
this.data.phone = formatted;
|
|
734
|
+
this.invalidate(); // 触发 UI 更新
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private formatPhoneNumber(phone: string): string {
|
|
738
|
+
const cleaned = phone.replace(/\D/g, '');
|
|
739
|
+
if (cleaned.length === 11) {
|
|
740
|
+
return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 7)}-${cleaned.slice(7)}`;
|
|
741
|
+
}
|
|
742
|
+
return phone;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// 使用方法
|
|
747
|
+
const userForm = new UserProfileForm(userId, userData, formSchema);
|
|
748
|
+
await userForm.initialize();
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## 高级模式
|
|
752
|
+
|
|
753
|
+
### 条件字段显示
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
class ConditionalFormCard extends FlexiCard {
|
|
757
|
+
protected createFromSchema(): void {
|
|
758
|
+
super.createFromSchema();
|
|
759
|
+
|
|
760
|
+
// 添加条件逻辑
|
|
761
|
+
this.setupConditionalFields();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private setupConditionalFields(): void {
|
|
765
|
+
const typeField = this.field['userType'];
|
|
766
|
+
const companyField = this.field['company'];
|
|
767
|
+
|
|
768
|
+
if (typeField && companyField) {
|
|
769
|
+
typeField.events.change = (value: string) => {
|
|
770
|
+
companyField.visibility = value === 'business'
|
|
771
|
+
? Visibility.View
|
|
772
|
+
: Visibility.Hidden;
|
|
773
|
+
this.invalidate();
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### 动态字段依赖
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
class DependentFieldsHandler {
|
|
784
|
+
private form: FlexiForm;
|
|
785
|
+
private dependencies: Map<string, string[]> = new Map();
|
|
786
|
+
|
|
787
|
+
constructor(form: FlexiForm) {
|
|
788
|
+
this.form = form;
|
|
789
|
+
this.setupDependencies();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
setupDependencies(): void {
|
|
793
|
+
// 定义字段依赖关系
|
|
794
|
+
this.dependencies.set('country', ['state', 'city']);
|
|
795
|
+
this.dependencies.set('state', ['city']);
|
|
796
|
+
|
|
797
|
+
// 设置变化处理器
|
|
798
|
+
for (const [parentField, dependentFields] of this.dependencies) {
|
|
799
|
+
this.setupFieldHandler(parentField, dependentFields);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private setupFieldHandler(parentField: string, dependentFields: string[]): void {
|
|
804
|
+
const field = this.findField(parentField);
|
|
805
|
+
if (field) {
|
|
806
|
+
const originalHandler = field.events.change;
|
|
807
|
+
field.events.change = async (value: any) => {
|
|
808
|
+
// 调用原始处理器
|
|
809
|
+
originalHandler?.(value);
|
|
810
|
+
|
|
811
|
+
// 更新依赖字段
|
|
812
|
+
await this.updateDependentFields(parentField, value, dependentFields);
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private async updateDependentFields(
|
|
818
|
+
parentField: string,
|
|
819
|
+
value: any,
|
|
820
|
+
dependentFields: string[]
|
|
821
|
+
): Promise<void> {
|
|
822
|
+
for (const fieldName of dependentFields) {
|
|
823
|
+
const field = this.findField(fieldName);
|
|
824
|
+
if (field) {
|
|
825
|
+
// 清除当前值
|
|
826
|
+
this.form.data[fieldName] = null;
|
|
827
|
+
|
|
828
|
+
// 加载新选项
|
|
829
|
+
const options = await this.loadOptions(fieldName, { [parentField]: value });
|
|
830
|
+
field.props.options = options;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
this.form.invalidate();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private async loadOptions(fieldName: string, context: any): Promise<any[]> {
|
|
838
|
+
const response = await fetch(`/api/options/${fieldName}`, {
|
|
839
|
+
method: 'POST',
|
|
840
|
+
headers: { 'Content-Type': 'application/json' },
|
|
841
|
+
body: JSON.stringify(context)
|
|
842
|
+
});
|
|
843
|
+
return await response.json();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private findField(fieldName: string): FlexiField | null {
|
|
847
|
+
for (const card of this.form.cards) {
|
|
848
|
+
if (card.field[fieldName]) {
|
|
849
|
+
return card.field[fieldName];
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### 表单向导模式
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
class WizardFormManager {
|
|
861
|
+
private steps: FlexiFormSchema[] = [];
|
|
862
|
+
private currentStep = 0;
|
|
863
|
+
private stepData: any[] = [];
|
|
864
|
+
|
|
865
|
+
constructor(steps: FlexiFormSchema[]) {
|
|
866
|
+
this.steps = steps;
|
|
867
|
+
this.stepData = new Array(steps.length).fill({});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
getCurrentSchema(): FlexiFormSchema {
|
|
871
|
+
return this.steps[this.currentStep];
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
getCurrentData(): any {
|
|
875
|
+
return this.stepData[this.currentStep];
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
nextStep(): boolean {
|
|
879
|
+
if (this.currentStep < this.steps.length - 1) {
|
|
880
|
+
this.currentStep++;
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
previousStep(): boolean {
|
|
887
|
+
if (this.currentStep > 0) {
|
|
888
|
+
this.currentStep--;
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
getFinalData(): any {
|
|
895
|
+
return this.stepData.reduce((acc, stepData) => ({
|
|
896
|
+
...acc,
|
|
897
|
+
...stepData
|
|
898
|
+
}), {});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
isFirstStep(): boolean {
|
|
902
|
+
return this.currentStep === 0;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
isLastStep(): boolean {
|
|
906
|
+
return this.currentStep === this.steps.length - 1;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
getProgress(): number {
|
|
910
|
+
return ((this.currentStep + 1) / this.steps.length) * 100;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// 在 Svelte 组件中使用
|
|
915
|
+
let wizardManager = new WizardFormManager([
|
|
916
|
+
personalInfoSchema,
|
|
917
|
+
contactInfoSchema,
|
|
918
|
+
preferencesSchema
|
|
919
|
+
]);
|
|
920
|
+
|
|
921
|
+
function handleNext() {
|
|
922
|
+
if (wizardManager.nextStep()) {
|
|
923
|
+
currentSchema = wizardManager.getCurrentSchema();
|
|
924
|
+
currentData = wizardManager.getCurrentData();
|
|
925
|
+
} else {
|
|
926
|
+
// 提交最终表单
|
|
927
|
+
submitWizardData(wizardManager.getFinalData());
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
## 最佳实践
|
|
933
|
+
|
|
934
|
+
### 1. Schema 组织
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
// 好的:有组织的 schema 结构
|
|
938
|
+
const FORM_SCHEMAS = {
|
|
939
|
+
userProfile: {
|
|
940
|
+
mode: 'flex',
|
|
941
|
+
arrangement: 'vertical',
|
|
942
|
+
elements: {
|
|
943
|
+
personal: USER_PERSONAL_CARD,
|
|
944
|
+
contact: USER_CONTACT_CARD,
|
|
945
|
+
preferences: USER_PREFERENCES_CARD
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
const USER_PERSONAL_CARD: FlexiCardSchema = {
|
|
951
|
+
title: '个人信息',
|
|
952
|
+
fields: [
|
|
953
|
+
FIELDS.firstName,
|
|
954
|
+
FIELDS.lastName,
|
|
955
|
+
FIELDS.birthDate
|
|
956
|
+
]
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const FIELDS = {
|
|
960
|
+
firstName: {
|
|
961
|
+
type: 'text-editor',
|
|
962
|
+
keyField: 'firstName',
|
|
963
|
+
name: 'firstName',
|
|
964
|
+
label: '名字',
|
|
965
|
+
required: true,
|
|
966
|
+
props: { maxLength: 50 }
|
|
967
|
+
},
|
|
968
|
+
// ... 更多字段定义
|
|
969
|
+
};
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### 2. 类型安全
|
|
973
|
+
|
|
974
|
+
```typescript
|
|
975
|
+
// 为你的 schema 定义严格类型
|
|
976
|
+
interface UserFormData {
|
|
977
|
+
firstName: string;
|
|
978
|
+
lastName: string;
|
|
979
|
+
email: string;
|
|
980
|
+
birthDate: Date;
|
|
981
|
+
preferences: UserPreferences;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
interface UserPreferences {
|
|
985
|
+
newsletter: boolean;
|
|
986
|
+
language: string;
|
|
987
|
+
timezone: string;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// 使用类型化表单创建
|
|
991
|
+
class TypedUserForm extends FlexiForm {
|
|
992
|
+
protected data: UserFormData;
|
|
993
|
+
|
|
994
|
+
constructor(data: UserFormData, schema: FlexiFormSchema) {
|
|
995
|
+
super(data, schema);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
getData(): UserFormData {
|
|
999
|
+
return this.data;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### 3. 错误处理
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
class FormErrorHandler {
|
|
1008
|
+
static handleFormError(error: any, form: FlexiForm): void {
|
|
1009
|
+
if (error.validationErrors) {
|
|
1010
|
+
this.displayValidationErrors(error.validationErrors, form);
|
|
1011
|
+
} else if (error.networkError) {
|
|
1012
|
+
this.displayNetworkError(error.networkError);
|
|
1013
|
+
} else {
|
|
1014
|
+
this.displayGenericError(error);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
private static displayValidationErrors(
|
|
1019
|
+
errors: Record<string, string>,
|
|
1020
|
+
form: FlexiForm
|
|
1021
|
+
): void {
|
|
1022
|
+
for (const [fieldName, errorMessage] of Object.entries(errors)) {
|
|
1023
|
+
const field = this.findFieldInForm(fieldName, form);
|
|
1024
|
+
if (field) {
|
|
1025
|
+
field.error = errorMessage;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
form.invalidate();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### 4. 性能优化
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
// 延迟加载大型表单
|
|
1037
|
+
class LazyFormLoader {
|
|
1038
|
+
private formCache = new Map<string, FlexiFormSchema>();
|
|
1039
|
+
|
|
1040
|
+
async loadForm(formId: string): Promise<FlexiFormSchema> {
|
|
1041
|
+
if (this.formCache.has(formId)) {
|
|
1042
|
+
return this.formCache.get(formId)!;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const schema = await fetch(`/api/forms/${formId}`).then(r => r.json());
|
|
1046
|
+
this.formCache.set(formId, schema);
|
|
1047
|
+
return schema;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
preloadForms(formIds: string[]): void {
|
|
1051
|
+
formIds.forEach(id => this.loadForm(id));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 防抖表单变化
|
|
1056
|
+
function createDebouncedFormHandler(delay = 300) {
|
|
1057
|
+
let timeoutId: number;
|
|
1058
|
+
|
|
1059
|
+
return function(handler: Function) {
|
|
1060
|
+
return function(...args: any[]) {
|
|
1061
|
+
clearTimeout(timeoutId);
|
|
1062
|
+
timeoutId = setTimeout(() => handler(...args), delay);
|
|
1063
|
+
};
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
这个综合指南提供了有效使用和扩展 FlexiForm 所需的一切。示例从基础用法逐步发展到高级模式,展示了如何使用 FlexiForm 的灵活架构构建复杂的表单系统。
|