@formily-design/formily-antd 1.0.0 → 1.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/package.json +8 -8
- package/.umirc.js +0 -44
- package/copy.ts +0 -6
- package/playground/components/AsyncSelect/AsyncSelect.tsx +0 -38
- package/playground/components/AsyncSelect/index.tsx +0 -1
- package/playground/components/AsyncSelect/preview.tsx +0 -36
- package/playground/components/ClickEventSetter/ActionConfigModal.tsx +0 -173
- package/playground/components/ClickEventSetter/ActionList.tsx +0 -67
- package/playground/components/ClickEventSetter/DownloadPanel.tsx +0 -24
- package/playground/components/ClickEventSetter/NavigationPanel.tsx +0 -42
- package/playground/components/ClickEventSetter/RequestPanel.tsx +0 -190
- package/playground/components/ClickEventSetter/index.tsx +0 -90
- package/playground/components/ClickEventSetter/styles.less +0 -89
- package/playground/components/ClickEventSetter/types.ts +0 -43
- package/playground/components/FormButton/FormButton.tsx +0 -189
- package/playground/components/FormButton/hooks/useEventActionExecutor.ts +0 -65
- package/playground/components/FormButton/index.ts +0 -1
- package/playground/components/FormButton/preview.tsx +0 -60
- package/playground/components/FormButton/utils/actionExecutors.ts +0 -96
- package/playground/components/FormButtonGroup/FormButtonGroup.tsx +0 -21
- package/playground/components/FormButtonGroup/index.ts +0 -1
- package/playground/components/FormButtonGroup/preview.tsx +0 -66
- package/playground/components/FormList/FormList.tsx +0 -164
- package/playground/components/FormList/index.tsx +0 -1
- package/playground/components/FormList/preview.tsx +0 -429
- package/playground/components/LocationPicker/LocationModal.tsx +0 -328
- package/playground/components/LocationPicker/LocationPicker.tsx +0 -163
- package/playground/components/LocationPicker/SearchBox.tsx +0 -122
- package/playground/components/LocationPicker/api.ts +0 -77
- package/playground/components/LocationPicker/constants.ts +0 -2
- package/playground/components/LocationPicker/index.tsx +0 -44
- package/playground/components/LocationPicker/types.ts +0 -160
- package/playground/components/LocationPicker/utils.ts +0 -46
- package/playground/components/ResourceIcon/index.module.less +0 -10
- package/playground/components/ResourceIcon/index.tsx +0 -10
- package/playground/designerLocales/en-US.ts +0 -38
- package/playground/designerLocales/index.ts +0 -6
- package/playground/designerLocales/ko-KR.ts +0 -10
- package/playground/designerLocales/zh-CN.ts +0 -38
- package/playground/locales/FormButton.ts +0 -38
- package/playground/locales/FormButtonGroup.ts +0 -29
- package/playground/locales/LocationPicker.ts +0 -30
- package/playground/locales/all.ts +0 -3
- package/playground/locales/index.ts +0 -3
- package/playground/main.tsx +0 -222
- package/playground/react-app-env.d.ts +0 -4
- package/playground/request.ts +0 -123
- package/playground/schemas/FormButton.ts +0 -42
- package/playground/schemas/FormButtonGroup.ts +0 -24
- package/playground/schemas/LocationPicker.ts +0 -14
- package/playground/schemas/all.ts +0 -3
- package/playground/schemas/index.ts +0 -3
- package/playground/service/index.ts +0 -1
- package/playground/service/schema.ts +0 -22
- package/playground/template.ejs +0 -20
- package/playground/utils.ts +0 -744
- package/playground/webpack.base.ts +0 -123
- package/playground/webpack.dev.ts +0 -64
- package/playground/webpack.prod.ts +0 -46
- package/playground/widgets/ActionsWidget.tsx +0 -58
- package/playground/widgets/LogoWidget.tsx +0 -20
- package/playground/widgets/MarkupSchemaWidget.tsx +0 -164
- package/playground/widgets/PreviewWidget.tsx +0 -188
- package/playground/widgets/SchemaEditorWidget.tsx +0 -27
- package/playground/widgets/index.ts +0 -5
- package/tsconfig.build.json +0 -12
package/playground/utils.ts
DELETED
|
@@ -1,744 +0,0 @@
|
|
|
1
|
-
import { Field, GeneralField } from '@formily/core'
|
|
2
|
-
import { ISchema } from '@formily/react'
|
|
3
|
-
import { get, post, ResponseResult } from './request'
|
|
4
|
-
import { action } from '@formily/reactive'
|
|
5
|
-
import { isStr } from '@formily/shared'
|
|
6
|
-
|
|
7
|
-
/** 表单变量字段路径正则 */
|
|
8
|
-
const FORM_VARIABLE_PATH_REG = /^\{\{\$values\.([\w\.]+)\}\}$/
|
|
9
|
-
/** */
|
|
10
|
-
/** 判断是否是表单变量 {{$values.xxx}} 或 {{$values.xxx.yyy}} */
|
|
11
|
-
const isFormVariable = (str: string) => {
|
|
12
|
-
return FORM_VARIABLE_PATH_REG.test(str)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** 获取变量字符串里的表单字段路径 */
|
|
16
|
-
const getFormVariableFieldPath = (str: string) => {
|
|
17
|
-
const match = FORM_VARIABLE_PATH_REG.exec(str)
|
|
18
|
-
return match ? match[1] : ''
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 检测并捕获整个字符串是否完全是形如 {{ ... }} 的表达式(两端允许空白)。
|
|
23
|
-
* 捕获组 ([\s\S]*) 会得到 {{ 和 }} 中间的内容(包括换行)。
|
|
24
|
-
* 注意:如果字符串是 "hi {{name}}"(不是整个字符串由 {{...}} 包裹),则不会匹配
|
|
25
|
-
*/
|
|
26
|
-
const ExpRE = /^\s*\{\{([\s\S]*)\}\}\s*$/
|
|
27
|
-
const Registry = {
|
|
28
|
-
/**
|
|
29
|
-
* 是否静默执行表达式(开/关错误吞噬)。
|
|
30
|
-
* 当 silent = true 时,如果表达式执行抛错,会被 catch 吞掉,并且函数返回 undefined(因为 catch 里没有 return)。
|
|
31
|
-
* 当 silent = false 时,出错会把异常抛出来。
|
|
32
|
-
*/
|
|
33
|
-
silent: false,
|
|
34
|
-
/**
|
|
35
|
-
* 使用 new Function('$root', ...) 动态创建函数,并用 with($root) { ... } 把 scope 对象的属性当作变量直接可访问。
|
|
36
|
-
* 最终等价于在传入的 scope 上下文中执行 return ( expression );,然后返回这个值
|
|
37
|
-
*/
|
|
38
|
-
compile(expression: string, scope: Record<string, any> = {}) {
|
|
39
|
-
if (Registry.silent) {
|
|
40
|
-
try {
|
|
41
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-implied-eval
|
|
42
|
-
return new Function('$root', `with($root) { return (${expression}); }`)(
|
|
43
|
-
scope
|
|
44
|
-
)
|
|
45
|
-
} catch (e) {
|
|
46
|
-
console.error('compile error:', e)
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-implied-eval
|
|
50
|
-
return new Function('$root', `with($root) { return (${expression}); }`)(
|
|
51
|
-
scope
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 浅解析字符串模板 {{ xxx }},返回解析结果
|
|
59
|
-
* @example
|
|
60
|
-
* 简单算术 shallowCompile("{{ 1 + 2 }}") // => 3
|
|
61
|
-
* 读取 scope 中的变量 shallowCompile("{{ a + b }}", { a: 10, b: 5 }) // => 15
|
|
62
|
-
* 调用 scope 中的函数 shallowCompile("{{ add(a, b) }}", { a: 10, b: 5, add: (x, y) => x + y }) // => 15
|
|
63
|
-
* 访问对象属性 shallowCompile("{{ user.name }}", { user: { name: "小明", age: 20 } }) // => "小明"
|
|
64
|
-
* 访问对象属性 shallowCompile("{{ user.age > 18 ? '成人' : '未成年' }}", { user: { name: "小明", age: 20 } }) // => "成人"
|
|
65
|
-
* 调用函数 shallowCompile(`{{function(){${runner}}}}`,{ user: { name: "小明", age: 20 } })()
|
|
66
|
-
* 不包含模板,原样返回 shallowCompile("Hello World") // => "Hello World"
|
|
67
|
-
*/
|
|
68
|
-
export const shallowCompile = <
|
|
69
|
-
Source = any,
|
|
70
|
-
Scope extends Record<string, any> = {}
|
|
71
|
-
>(
|
|
72
|
-
source: Source,
|
|
73
|
-
scope?: Scope
|
|
74
|
-
) => {
|
|
75
|
-
if (isStr(source)) {
|
|
76
|
-
const matched = ExpRE.exec(source)
|
|
77
|
-
if (!matched) return source
|
|
78
|
-
return Registry.compile(matched[1], scope)
|
|
79
|
-
}
|
|
80
|
-
return source
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** 解析字符串函数,返回可调用函数--忽略参数名 */
|
|
84
|
-
// function parseFunction<T extends (...args: any[]) => any>(code: string): T | null {
|
|
85
|
-
// if (!code?.trim()) return null
|
|
86
|
-
// try {
|
|
87
|
-
// // 统一使用 arguments 传递参数,忽略参数名
|
|
88
|
-
// return new Function(`return (${code})(...arguments)`) as T
|
|
89
|
-
// } catch (e) {
|
|
90
|
-
// console.error('Failed to parse function:', e)
|
|
91
|
-
// return null
|
|
92
|
-
// }
|
|
93
|
-
// }
|
|
94
|
-
|
|
95
|
-
/** 解析字符串函数,返回可调用函数 */
|
|
96
|
-
function parseFunction<T extends (...args: any[]) => any>(
|
|
97
|
-
code: string,
|
|
98
|
-
params: string[]
|
|
99
|
-
): T | null {
|
|
100
|
-
if (!code?.trim()) return null
|
|
101
|
-
try {
|
|
102
|
-
// 尝试从函数声明中提取函数名
|
|
103
|
-
const namedFnMatch = /function\s+(\w+)\s*\(/.exec(code)
|
|
104
|
-
if (namedFnMatch) {
|
|
105
|
-
const fnName = namedFnMatch[1]
|
|
106
|
-
// 创建函数并自动调用命名的函数
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
108
|
-
return new Function(
|
|
109
|
-
...params,
|
|
110
|
-
`${code}\nreturn ${fnName}(${params.join(',')})`
|
|
111
|
-
) as T
|
|
112
|
-
}
|
|
113
|
-
// 没有命名函数声明,直接创建
|
|
114
|
-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
115
|
-
return new Function(...params, code) as T
|
|
116
|
-
} catch (e) {
|
|
117
|
-
console.error('Failed to parse function:', e)
|
|
118
|
-
return null
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** 替换 {{$values.xxx}} 变量占位符 */
|
|
123
|
-
function replaceVariables(str: string, field: GeneralField): string {
|
|
124
|
-
// return str.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
125
|
-
return str.replace(/\{\{\$values\.(\w+)\}\}/g, (_, key) => {
|
|
126
|
-
return field.form?.getValuesIn(key) ?? ''
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** 解析对象里的变量值,返回处理后的对象 */
|
|
131
|
-
function parseObjectVariables(
|
|
132
|
-
params: Record<string, any>,
|
|
133
|
-
scope: Record<string, any>
|
|
134
|
-
): Record<string, any> {
|
|
135
|
-
return Object.keys(params).reduce((acc, key) => {
|
|
136
|
-
if (key) {
|
|
137
|
-
const value = params[key]
|
|
138
|
-
acc[key] = /\{\{[\s\S]*\}\}/.test(value)
|
|
139
|
-
? shallowCompile(value, scope)
|
|
140
|
-
: value
|
|
141
|
-
}
|
|
142
|
-
return acc
|
|
143
|
-
}, {} as Record<string, any>)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
interface IRemoteDataSource {
|
|
147
|
-
url: string
|
|
148
|
-
method: string
|
|
149
|
-
headers: Record<string, string>
|
|
150
|
-
params: Record<string, string>
|
|
151
|
-
/** "function (config, data){\nconsole.log(config,data)\n}" */
|
|
152
|
-
beforeRequest: string
|
|
153
|
-
/** "function (res){\nreturn res.map(s => ({label: s.username, value: s.id}))\n}" */
|
|
154
|
-
dataHandler: string
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
{
|
|
159
|
-
"form": {
|
|
160
|
-
"labelCol": 6,
|
|
161
|
-
"wrapperCol": 12
|
|
162
|
-
},
|
|
163
|
-
"schema": {
|
|
164
|
-
"type": "object",
|
|
165
|
-
"properties": {
|
|
166
|
-
"w6v1auut5m3": {
|
|
167
|
-
"title": "AsyncSelect",
|
|
168
|
-
"x-decorator": "FormItem",
|
|
169
|
-
"x-component": "AsyncSelect",
|
|
170
|
-
"x-validator": [],
|
|
171
|
-
"x-component-props": {},
|
|
172
|
-
"x-decorator-props": {},
|
|
173
|
-
"x-datasource-type": "remote",
|
|
174
|
-
"x-datasource-effect": {
|
|
175
|
-
"url": "https://jsonplaceholder.typicode.com/users",
|
|
176
|
-
"method": "GET",
|
|
177
|
-
"beforeRequest": "function beforeRequest(arg) {\n console.log('before', arg)\n}",
|
|
178
|
-
"dataHandler": "function dataHandler(arg) {\n console.log('after', arg)\n}"
|
|
179
|
-
},
|
|
180
|
-
"x-designable-id": "w6v1auut5m3",
|
|
181
|
-
"x-index": 0
|
|
182
|
-
},
|
|
183
|
-
"2co13fwl7tf": {
|
|
184
|
-
"type": "array",
|
|
185
|
-
"x-decorator": "FormItem",
|
|
186
|
-
"x-component": "ArrayCards",
|
|
187
|
-
"x-component-props": {
|
|
188
|
-
"title": "Title"
|
|
189
|
-
},
|
|
190
|
-
"x-validator": [],
|
|
191
|
-
"x-decorator-props": {},
|
|
192
|
-
"x-designable-id": "2co13fwl7tf",
|
|
193
|
-
"x-index": 0,
|
|
194
|
-
"items": {
|
|
195
|
-
"type": "object",
|
|
196
|
-
"x-designable-id": "xioweoptn2h",
|
|
197
|
-
"properties": {
|
|
198
|
-
"pjwa85a289y": {
|
|
199
|
-
"type": "void",
|
|
200
|
-
"x-component": "ArrayCards.Index",
|
|
201
|
-
"x-designable-id": "pjwa85a289y",
|
|
202
|
-
"x-index": 0
|
|
203
|
-
},
|
|
204
|
-
"w6v1auut5m3": {
|
|
205
|
-
"title": "AsyncSelect",
|
|
206
|
-
"x-decorator": "FormItem",
|
|
207
|
-
"x-component": "AsyncSelect",
|
|
208
|
-
"x-validator": [],
|
|
209
|
-
"x-component-props": {
|
|
210
|
-
"showSearch": true
|
|
211
|
-
},
|
|
212
|
-
"x-decorator-props": {},
|
|
213
|
-
"x-datasource-type": "remote",
|
|
214
|
-
"x-datasource-effect": {
|
|
215
|
-
"url": "https://jsonplaceholder.typicode.com/users",
|
|
216
|
-
"method": "GET",
|
|
217
|
-
"params": [
|
|
218
|
-
{
|
|
219
|
-
"key": "size",
|
|
220
|
-
"value": "10"
|
|
221
|
-
}
|
|
222
|
-
],
|
|
223
|
-
"beforeRequest": "function beforeRequest(config) {\n console.log('before', config)\n}",
|
|
224
|
-
"dataHandler": "function dataHandler(data) {\n console.log('after', data)\n return data.map(item => ({label: item.name, value: item.name}))\n}"
|
|
225
|
-
},
|
|
226
|
-
"x-designable-id": "w6v1auut5m3",
|
|
227
|
-
"x-index": 1,
|
|
228
|
-
"x-fetchdata-onsearch": true
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
"properties": {
|
|
233
|
-
"lo4hqxkhchf": {
|
|
234
|
-
"type": "void",
|
|
235
|
-
"title": "Addition",
|
|
236
|
-
"x-component": "ArrayCards.Addition",
|
|
237
|
-
"x-designable-id": "lo4hqxkhchf",
|
|
238
|
-
"x-index": 0
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
"x-designable-id": "5r1fpvewvwq"
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
*/
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* 遍历 schema
|
|
250
|
-
* 判断存在 x-datasource-effect 的节点 x-reactions 配置是否是数组,不是数组的话,转换为数组,并创建 '{{useAsyncDataSource(loadData_[xxx])}}' 添加在 x-reactions 的第1个元素,其中 xxx 是 properties 下子对象的属性名,将原来的 x-reactions 设置为 x-reactions 的第2个元素
|
|
251
|
-
* 返回新的 schema
|
|
252
|
-
*/
|
|
253
|
-
export function transformSchema(
|
|
254
|
-
schema: ISchema,
|
|
255
|
-
scope: Record<string, any>,
|
|
256
|
-
nodeKey?: string
|
|
257
|
-
): ISchema {
|
|
258
|
-
if (!schema || typeof schema !== 'object') {
|
|
259
|
-
return schema
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 深拷贝 schema
|
|
263
|
-
const newSchema = Array.isArray(schema) ? [] : Object.create(null)
|
|
264
|
-
|
|
265
|
-
for (const key in schema) {
|
|
266
|
-
if (Object.prototype.hasOwnProperty.call(schema, key)) {
|
|
267
|
-
const value = schema[key as keyof ISchema]
|
|
268
|
-
if (key === 'properties' && value && typeof value === 'object') {
|
|
269
|
-
// 递归处理 properties,传递子对象的 key
|
|
270
|
-
newSchema.properties = {}
|
|
271
|
-
for (const propKey in value) {
|
|
272
|
-
if (Object.prototype.hasOwnProperty.call(value, propKey)) {
|
|
273
|
-
;(newSchema.properties as Record<string, ISchema>)[propKey] =
|
|
274
|
-
transformSchema(value[propKey] as ISchema, scope, propKey)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
} else if (key === 'x-datasource-effect' && value) {
|
|
278
|
-
// 节点存在 x-datasource-effect,处理 x-reactions
|
|
279
|
-
const reactions = (schema as Record<string, unknown>)['x-reactions']
|
|
280
|
-
const fetchOnSearch = (schema as Record<string, unknown>)[
|
|
281
|
-
'x-fetchdata-onsearch'
|
|
282
|
-
]
|
|
283
|
-
|
|
284
|
-
// 将 x-datasource-effect 配置转换请求函数,并添加到 x-reactions
|
|
285
|
-
const loadDataReaction = asyncLoadDataSource(
|
|
286
|
-
transformSourceEffect(value, {
|
|
287
|
-
scope,
|
|
288
|
-
searchKey: schema['x-fetchdata-searchkey'],
|
|
289
|
-
}),
|
|
290
|
-
{
|
|
291
|
-
fetchOnSearch: !!fetchOnSearch,
|
|
292
|
-
}
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* 标识 x-datasource-effect 属性存在
|
|
297
|
-
* TODO: 为什么不保留原来的 x-datasource-effect 配置数据?
|
|
298
|
-
* 因为 x-datasource-effect 里的配置会使用 FormPathPattern,如{{$values.0.productName}},这种配置在 Formily SchemaField 解析时内部会报错:
|
|
299
|
-
* Uncaught SyntaxError: Unexpected token '.',因为内部识别到了变量标识 {{xxx}} 执行了 shallowCompile, FormPathPattern 的一些格式在 shallowCompile 这里会报错。
|
|
300
|
-
*/
|
|
301
|
-
;(newSchema as Record<string, unknown>)[key] = true
|
|
302
|
-
|
|
303
|
-
if (reactions !== undefined) {
|
|
304
|
-
if (Array.isArray(reactions)) {
|
|
305
|
-
// 已经是数组,插入到第1个位置
|
|
306
|
-
newSchema['x-reactions'] = [loadDataReaction, ...reactions]
|
|
307
|
-
} else {
|
|
308
|
-
// 不是数组,转换为数组
|
|
309
|
-
newSchema['x-reactions'] = [loadDataReaction, reactions]
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
// x-reactions 不存在,创建新数组
|
|
313
|
-
newSchema['x-reactions'] = [loadDataReaction]
|
|
314
|
-
}
|
|
315
|
-
} else if (key === 'x-reactions' && value) {
|
|
316
|
-
// 如果 x-reactions 已经被上面的 x-datasource-effect 处理过了,跳过
|
|
317
|
-
if ((schema as Record<string, unknown>)['x-datasource-effect']) {
|
|
318
|
-
// 已处理,跳过
|
|
319
|
-
continue
|
|
320
|
-
} else if (Array.isArray(value)) {
|
|
321
|
-
;(newSchema as Record<string, unknown>)[key] = value.map((item) =>
|
|
322
|
-
transformSchema(item as ISchema, scope, nodeKey)
|
|
323
|
-
)
|
|
324
|
-
} else {
|
|
325
|
-
;(newSchema as Record<string, unknown>)[key] = transformSchema(
|
|
326
|
-
value as ISchema,
|
|
327
|
-
scope,
|
|
328
|
-
nodeKey
|
|
329
|
-
)
|
|
330
|
-
}
|
|
331
|
-
} else if (key === 'items' && value && typeof value === 'object') {
|
|
332
|
-
// 处理 type: array 的 items 属性,传递 items 前缀作为 fieldName 的一部分
|
|
333
|
-
;(newSchema as Record<string, unknown>)[key] = transformSchema(
|
|
334
|
-
value as ISchema,
|
|
335
|
-
scope,
|
|
336
|
-
`${nodeKey ? nodeKey + '_' : ''}items`
|
|
337
|
-
)
|
|
338
|
-
} else if (value && typeof value === 'object') {
|
|
339
|
-
;(newSchema as Record<string, unknown>)[key] = transformSchema(
|
|
340
|
-
value as ISchema,
|
|
341
|
-
scope,
|
|
342
|
-
nodeKey
|
|
343
|
-
)
|
|
344
|
-
} else {
|
|
345
|
-
// 复制基本类型值
|
|
346
|
-
Object.defineProperty(newSchema, key, {
|
|
347
|
-
value,
|
|
348
|
-
writable: true,
|
|
349
|
-
enumerable: true,
|
|
350
|
-
configurable: true,
|
|
351
|
-
})
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return newSchema as ISchema
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** 将 params 存在表单依赖的字段处理为新对象返回 */
|
|
360
|
-
function getDepsFormFieldParams(params: Record<string, any>) {
|
|
361
|
-
return Object.fromEntries(
|
|
362
|
-
Object.entries(params).filter(([, value]) => value && isFormVariable(value))
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/** 收集表单依赖的字段数据,返回参数对象 */
|
|
367
|
-
function getDepsFormFieldData(depsFieldObj: Record<string, any>, field: Field) {
|
|
368
|
-
return Object.entries(depsFieldObj).reduce<Record<string, any>>(
|
|
369
|
-
(acc, [key, value]) => {
|
|
370
|
-
// 获取表单字段的值作为参数字段值
|
|
371
|
-
acc[key] = field.query(getFormVariableFieldPath(value)).get('value')
|
|
372
|
-
return acc
|
|
373
|
-
},
|
|
374
|
-
{}
|
|
375
|
-
)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** 将 x-datasource-effect 配置转换为执行真实请求的函数 */
|
|
379
|
-
function transformSourceEffect(
|
|
380
|
-
dataConfig: IRemoteDataSource,
|
|
381
|
-
options?: {
|
|
382
|
-
/** 变量作用域 */
|
|
383
|
-
scope: Record<string, any>
|
|
384
|
-
/** 配置的搜索字段名 */
|
|
385
|
-
searchKey?: string
|
|
386
|
-
}
|
|
387
|
-
) {
|
|
388
|
-
const { scope, searchKey = 'keyword' } = options ?? {}
|
|
389
|
-
const params = dataConfig?.params
|
|
390
|
-
// 请求参数依赖的表单字段集合
|
|
391
|
-
const depsFieldsObj = params ? getDepsFormFieldParams(params) : {}
|
|
392
|
-
return async (field: Field, searchValue?: string) => {
|
|
393
|
-
// localStorage 对象数据
|
|
394
|
-
const $localStorage = getLocalStorageData() ?? {}
|
|
395
|
-
// 表单数据
|
|
396
|
-
// TODO: 这里的 field.form.values 得到的是一个 Proxy 对象,看能否转换为普通对象
|
|
397
|
-
const $values = field.form.values ?? {}
|
|
398
|
-
/** 变量局部作用域 */
|
|
399
|
-
const variablesScope = { ...scope, $values, $localStorage }
|
|
400
|
-
// 构建联动参数
|
|
401
|
-
const linkageParams = getDepsFormFieldData(depsFieldsObj, field)
|
|
402
|
-
const headers = dataConfig.headers
|
|
403
|
-
? parseObjectVariables(dataConfig.headers, variablesScope)
|
|
404
|
-
: {}
|
|
405
|
-
// 移除依赖表单字段的请求参数(将depsFieldsObj的属性从dataConfig.params中移除)
|
|
406
|
-
const configParams = Object.fromEntries(
|
|
407
|
-
Object.entries(dataConfig.params ?? {}).filter(
|
|
408
|
-
([key]) => !depsFieldsObj[key]
|
|
409
|
-
)
|
|
410
|
-
)
|
|
411
|
-
const paramsObj = parseObjectVariables(
|
|
412
|
-
Object.assign({}, configParams ?? {}, linkageParams ?? {}),
|
|
413
|
-
variablesScope
|
|
414
|
-
)
|
|
415
|
-
// TODO: 这里搜索的请求参数 key 需要优化,应该是配置的搜索字段名
|
|
416
|
-
// 如果有搜索值,添加到请求参数中
|
|
417
|
-
if (searchValue !== undefined) {
|
|
418
|
-
paramsObj[replaceVariables(searchKey, field) || 'keyword'] = searchValue
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// 处理 URL:GET 请求将参数附加到 URL,其他请求放在 body
|
|
422
|
-
let url = parseStringVariables(dataConfig.url, variablesScope)
|
|
423
|
-
const isGet = dataConfig.method?.toUpperCase() === 'GET'
|
|
424
|
-
let body: string | undefined
|
|
425
|
-
|
|
426
|
-
if (isGet) {
|
|
427
|
-
if (Object.keys(paramsObj).length > 0) {
|
|
428
|
-
const searchParams = new URLSearchParams(paramsObj)
|
|
429
|
-
url = `${url}${url.includes('?') ? '&' : '?'}${searchParams.toString()}`
|
|
430
|
-
}
|
|
431
|
-
} else {
|
|
432
|
-
body = JSON.stringify(paramsObj)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const config = {
|
|
436
|
-
url,
|
|
437
|
-
method: dataConfig.method,
|
|
438
|
-
headers: {
|
|
439
|
-
'Content-Type': 'application/json',
|
|
440
|
-
Authorization: $localStorage?.token as string,
|
|
441
|
-
...headers,
|
|
442
|
-
},
|
|
443
|
-
body,
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const beforeFn = parseFunction<(config: any) => any>(
|
|
447
|
-
dataConfig.beforeRequest,
|
|
448
|
-
['config']
|
|
449
|
-
)
|
|
450
|
-
if (beforeFn) {
|
|
451
|
-
beforeFn(config)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return fetch(config.url, {
|
|
455
|
-
method: config.method,
|
|
456
|
-
headers: config.headers,
|
|
457
|
-
body: config.body,
|
|
458
|
-
})
|
|
459
|
-
.then((res) => res.json())
|
|
460
|
-
.then((res) => {
|
|
461
|
-
const handlerFn = parseFunction<(result: typeof res) => any>(
|
|
462
|
-
dataConfig.dataHandler,
|
|
463
|
-
['res']
|
|
464
|
-
)
|
|
465
|
-
return handlerFn ? handlerFn(res) : res
|
|
466
|
-
})
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/** 内部递归函数,接收父级的 fieldName */
|
|
471
|
-
function transformScopeRecursive(
|
|
472
|
-
schema: ISchema,
|
|
473
|
-
fieldName: string
|
|
474
|
-
): Record<string, ReturnType<typeof transformSourceEffect>> {
|
|
475
|
-
const scope: Record<string, ReturnType<typeof transformSourceEffect>> = {}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* 处理当前层级的 x-datasource-effect 配置
|
|
479
|
-
* x-datasource-type: 'static' | 'remote'
|
|
480
|
-
*/
|
|
481
|
-
if (
|
|
482
|
-
schema['x-datasource-type'] === 'remote' &&
|
|
483
|
-
schema['x-datasource-effect']
|
|
484
|
-
) {
|
|
485
|
-
const sourceEffect = schema['x-datasource-effect'] as IRemoteDataSource
|
|
486
|
-
scope[`loadData_${fieldName}`] = transformSourceEffect(sourceEffect)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// 递归处理子属性
|
|
490
|
-
if (schema.properties) {
|
|
491
|
-
Object.entries(schema.properties).forEach(([key, childSchema]) => {
|
|
492
|
-
const childScope = transformScopeRecursive(childSchema as ISchema, key)
|
|
493
|
-
Object.assign(scope, childScope)
|
|
494
|
-
})
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// 处理 type: array 的 items 属性下的 x-datasource-effect
|
|
498
|
-
if (schema.items && typeof schema.items === 'object') {
|
|
499
|
-
const itemsScope = transformScopeRecursive(
|
|
500
|
-
schema.items as ISchema,
|
|
501
|
-
`${fieldName}__items`
|
|
502
|
-
)
|
|
503
|
-
Object.assign(scope, itemsScope)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return scope
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** 处理 schema 中远端数据获取的配置,添加到 scope 中 */
|
|
510
|
-
export function transformScope(
|
|
511
|
-
schema: ISchema
|
|
512
|
-
): Record<string, () => ReturnType<typeof transformSourceEffect>> {
|
|
513
|
-
/**
|
|
514
|
-
* x-datasource-type: 'static' | 'remote'
|
|
515
|
-
*/
|
|
516
|
-
if (!schema || typeof schema !== 'object') {
|
|
517
|
-
return {}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const scope: Record<string, () => ReturnType<typeof transformSourceEffect>> =
|
|
521
|
-
{}
|
|
522
|
-
|
|
523
|
-
// 递归处理子属性,使用 properties 的 key 作为 fieldName
|
|
524
|
-
if (schema.properties) {
|
|
525
|
-
Object.entries(schema.properties).forEach(([fieldName, childSchema]) => {
|
|
526
|
-
const childScope = transformScopeRecursive(
|
|
527
|
-
childSchema as ISchema,
|
|
528
|
-
fieldName
|
|
529
|
-
)
|
|
530
|
-
Object.assign(scope, childScope)
|
|
531
|
-
})
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return scope
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/** 防抖定时器 Map,用于存储每个 field 的定时器 */
|
|
538
|
-
const searchDebounceTimer = new Map<string, ReturnType<typeof setTimeout>>()
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* TODO: 异步加载数据逻辑可以考虑换到 x-reactions 里通过方法直接传入,不使用 scope 的方式。
|
|
542
|
-
* 如: x-reactions=[function loadData(field) { ... }, otherReactions]
|
|
543
|
-
* 因为使用 scope 的方式需要去维护调用的名称和创建的名称一致。
|
|
544
|
-
*/
|
|
545
|
-
/** 异步加载数据 */
|
|
546
|
-
export const asyncLoadDataSource =
|
|
547
|
-
(
|
|
548
|
-
service: (field: Field, searchValue?: string) => Promise<any>,
|
|
549
|
-
options?: { fetchOnSearch?: boolean }
|
|
550
|
-
) =>
|
|
551
|
-
(field: Field) => {
|
|
552
|
-
// console.log('field.path', field.path?.toString())
|
|
553
|
-
// console.log('field.address', field.address?.toString())
|
|
554
|
-
field.loading = true
|
|
555
|
-
service(field).then(
|
|
556
|
-
// action.bound 用于批量动作,保证更新多次数据,只触发 1 次 Reaction 执行
|
|
557
|
-
action.bound?.((data) => {
|
|
558
|
-
// TODO: 判断是依赖的表单字段变化触发的请求,考虑清空 field.value 旧值
|
|
559
|
-
// field.value = undefined
|
|
560
|
-
field.dataSource = data?.data || data
|
|
561
|
-
field.loading = false
|
|
562
|
-
})
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
// 如果启用了 fetchOnSearch,通过 setComponentProps 设置 onSearch
|
|
566
|
-
if (options?.fetchOnSearch) {
|
|
567
|
-
// 先保存原有的 onSearch 引用,避免闭包中循环调用
|
|
568
|
-
const originalOnSearch = field.componentProps?.onSearch
|
|
569
|
-
// 使用 field.path 作为唯一标识
|
|
570
|
-
const fieldKey = field.path?.toString() || field.address?.toString()
|
|
571
|
-
field.setComponentProps({
|
|
572
|
-
...field.componentProps,
|
|
573
|
-
onSearch: (value: string) => {
|
|
574
|
-
// 先调用原有的 onSearch
|
|
575
|
-
originalOnSearch?.(value)
|
|
576
|
-
|
|
577
|
-
// 清除之前的定时器
|
|
578
|
-
const existingTimer = searchDebounceTimer.get(fieldKey)
|
|
579
|
-
if (existingTimer) {
|
|
580
|
-
clearTimeout(existingTimer)
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// 设置新的定时器,使用闭包捕获当前的 value
|
|
584
|
-
const timer = setTimeout(() => {
|
|
585
|
-
searchDebounceTimer.delete(fieldKey)
|
|
586
|
-
// 重新请求
|
|
587
|
-
field.loading = true
|
|
588
|
-
service(field, value).then(
|
|
589
|
-
action.bound?.((data) => {
|
|
590
|
-
field.dataSource = data?.data || data
|
|
591
|
-
field.loading = false
|
|
592
|
-
})
|
|
593
|
-
)
|
|
594
|
-
}, 300)
|
|
595
|
-
searchDebounceTimer.set(fieldKey, timer)
|
|
596
|
-
},
|
|
597
|
-
})
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/** API 配置接口 */
|
|
602
|
-
export interface IFormApiConfig {
|
|
603
|
-
url: string
|
|
604
|
-
method: string
|
|
605
|
-
headers?: Record<string, string>
|
|
606
|
-
params?: Record<string, string>
|
|
607
|
-
beforeRequest?: string
|
|
608
|
-
dataHandler?: string
|
|
609
|
-
/**
|
|
610
|
-
* 承载表单数据的请求参数key和详情数据里获取表单数据的key
|
|
611
|
-
* 如果不配置,默认使用整个表单数据作为请求参数
|
|
612
|
-
*/
|
|
613
|
-
dataKey?: string
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/** 获取所有 localStorage 数据,返回一个对象 */
|
|
617
|
-
export function getLocalStorageData() {
|
|
618
|
-
const data: Record<string, any> = {}
|
|
619
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
620
|
-
const key = localStorage.key(i)
|
|
621
|
-
if (key) {
|
|
622
|
-
const value = localStorage.getItem(key)
|
|
623
|
-
try {
|
|
624
|
-
data[key] = value ? JSON.parse(value) : null
|
|
625
|
-
} catch {
|
|
626
|
-
data[key] = value
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return data
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* 处理字符串里的变量,返回替换后的字符串
|
|
635
|
-
* 方法内部使用 shallowCompile 进行解析
|
|
636
|
-
* @example
|
|
637
|
-
* parseStringVariables('https://example.com/api?name={{$values.name}}&userid={{$localStorage.userid}}', {$values: { name: '123' }, $localStorage: { userid: 'USER_1' }}) // => 'https://example.com/api?name=123&userid=USER_1'
|
|
638
|
-
*/
|
|
639
|
-
export function parseStringVariables(str: string, scope: Record<string, any>) {
|
|
640
|
-
// 存在变量数据
|
|
641
|
-
if (str && /\{\{[\s\S]*\}\}/.test(str)) {
|
|
642
|
-
// 将字符串按变量格式拆分成数组,元素包含变量字符串和普通字符串
|
|
643
|
-
const parts = str.split(/(\{\{[\s\S]*?\}\})/g)
|
|
644
|
-
return parts.reduce((result, part) => {
|
|
645
|
-
// 拼接解析后的字符串
|
|
646
|
-
return result + shallowCompile(part, scope)
|
|
647
|
-
}, '')
|
|
648
|
-
}
|
|
649
|
-
return str
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* 执行表单 API 请求
|
|
654
|
-
* TODO: 传入的 apiConfig 里 params 配置中如果存在变量,会被提前解析,所以这里目前只能获取到具体值,而不是变量字符串。
|
|
655
|
-
* 应该是传入 schema 配置时,formily 内部就把所有变量解析(包括自定义属性里的变量),所以该方法里所有处理变量的逻辑都没有实际生效(除了url里的变量,业务url不是直接{{开头的)
|
|
656
|
-
*/
|
|
657
|
-
export async function executeFormApi<T = any>(
|
|
658
|
-
apiConfig: IFormApiConfig,
|
|
659
|
-
formValues?: Record<string, any>,
|
|
660
|
-
options?: {
|
|
661
|
-
apiParams?: Record<string, any>
|
|
662
|
-
/** 变量作用域 */
|
|
663
|
-
scope?: Record<string, any>
|
|
664
|
-
}
|
|
665
|
-
): Promise<ResponseResult<T> | undefined> {
|
|
666
|
-
const {
|
|
667
|
-
url,
|
|
668
|
-
method,
|
|
669
|
-
headers = {},
|
|
670
|
-
params = {},
|
|
671
|
-
beforeRequest,
|
|
672
|
-
dataHandler,
|
|
673
|
-
dataKey,
|
|
674
|
-
} = apiConfig
|
|
675
|
-
const { apiParams = {}, scope } = options ?? {}
|
|
676
|
-
const $localStorage = getLocalStorageData() ?? {}
|
|
677
|
-
// 变量作用域
|
|
678
|
-
const variablesScope = { ...scope, $values: formValues ?? {}, $localStorage }
|
|
679
|
-
// 构建请求头
|
|
680
|
-
const requestHeaders: Record<string, string> = parseObjectVariables(
|
|
681
|
-
{ Authorization: $localStorage?.token, ...headers },
|
|
682
|
-
variablesScope
|
|
683
|
-
)
|
|
684
|
-
|
|
685
|
-
// 构建请求参数
|
|
686
|
-
const queryParams: Record<string, any> = {
|
|
687
|
-
...apiParams,
|
|
688
|
-
...formValues,
|
|
689
|
-
...parseObjectVariables(params, variablesScope),
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// 构建完整 URL
|
|
693
|
-
const fullUrl = parseStringVariables(url, variablesScope)
|
|
694
|
-
const config = {
|
|
695
|
-
url: fullUrl,
|
|
696
|
-
method,
|
|
697
|
-
headers: requestHeaders,
|
|
698
|
-
params: queryParams,
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// 执行 beforeRequest 钩子
|
|
702
|
-
if (beforeRequest) {
|
|
703
|
-
try {
|
|
704
|
-
const beforeFn = parseFunction(beforeRequest, ['config'])
|
|
705
|
-
beforeFn?.(config)
|
|
706
|
-
} catch (e) {
|
|
707
|
-
console.error('beforeRequest 执行失败:', e)
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// 处理参数到配置的 dataKey 字段
|
|
712
|
-
if (dataKey) {
|
|
713
|
-
config.params = {
|
|
714
|
-
...apiParams,
|
|
715
|
-
[dataKey]: config.params,
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
// 发起请求
|
|
719
|
-
let result: ResponseResult<T>
|
|
720
|
-
switch (method) {
|
|
721
|
-
case 'GET':
|
|
722
|
-
result = await get<T>(config.url, config.params, {
|
|
723
|
-
headers: config.headers,
|
|
724
|
-
})
|
|
725
|
-
break
|
|
726
|
-
default:
|
|
727
|
-
result = await post<T>(config.url, config.params, {
|
|
728
|
-
headers: config.headers,
|
|
729
|
-
method: method || 'POST',
|
|
730
|
-
})
|
|
731
|
-
break
|
|
732
|
-
}
|
|
733
|
-
// 执行 dataHandler 钩子
|
|
734
|
-
if (dataHandler) {
|
|
735
|
-
try {
|
|
736
|
-
const handlerFn = parseFunction(dataHandler, ['data'])
|
|
737
|
-
return handlerFn?.(result)
|
|
738
|
-
} catch (e) {
|
|
739
|
-
console.error('dataHandler 执行失败:', e)
|
|
740
|
-
}
|
|
741
|
-
} else {
|
|
742
|
-
return result
|
|
743
|
-
}
|
|
744
|
-
}
|