@longhongguo/form-create-ant-design-vue 3.3.34 → 3.3.36

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.
@@ -1,671 +1,674 @@
1
- import getConfig from './config'
2
- import mergeProps from '@form-create/utils/lib/mergeprops'
3
- import is, { hasProperty } from '@form-create/utils/lib/type'
4
- import extend from '@form-create/utils/lib/extend'
5
-
6
- function isTooltip(info) {
7
- return info.type === 'tooltip'
8
- }
9
-
10
- function tidy(props, name) {
11
- if (!hasProperty(props, name)) return
12
- if (is.String(props[name])) {
13
- props[name] = { [name]: props[name], show: true }
14
- }
15
- }
16
-
17
- function isFalse(val) {
18
- return val === false
19
- }
20
-
21
- function tidyBool(opt, name) {
22
- if (hasProperty(opt, name) && !is.Object(opt[name])) {
23
- opt[name] = { show: !!opt[name] }
24
- }
25
- }
26
-
27
- function tidyRule(rule) {
28
- const _rule = { ...rule }
29
- delete _rule.children
30
- return _rule
31
- }
32
-
33
- export default {
34
- validate() {
35
- const form = this.form()
36
- if (form) {
37
- return form.validate()
38
- } else {
39
- return new Promise((v) => v())
40
- }
41
- },
42
- validateField(field) {
43
- const form = this.form()
44
- if (form) {
45
- return form.validateFields(field)
46
- } else {
47
- return new Promise((v) => v())
48
- }
49
- },
50
- clearValidateState(ctx) {
51
- const fItem = this.vm.refs[ctx.wrapRef]
52
- if (fItem) {
53
- fItem.clearValidate()
54
- }
55
- },
56
- tidyOptions(options) {
57
- ;['submitBtn', 'resetBtn', 'row', 'info', 'wrap', 'col', 'title'].forEach(
58
- (name) => {
59
- tidyBool(options, name)
60
- }
61
- )
62
- return options
63
- },
64
- tidyRule({ prop }) {
65
- tidy(prop, 'title')
66
- tidy(prop, 'info')
67
- return prop
68
- },
69
- mergeProp(ctx) {
70
- const def = {
71
- info: {
72
- type: 'popover',
73
- placement: 'topLeft',
74
- icon: 'QuestionCircleOutlined'
75
- },
76
- title: {},
77
- col: { span: 24 },
78
- wrap: {}
79
- }
80
- ;['info', 'wrap', 'col', 'title'].forEach((name) => {
81
- ctx.prop[name] = mergeProps(
82
- [this.options[name] || {}, ctx.prop[name] || {}],
83
- def[name]
84
- )
85
- })
86
-
87
- // 应用 componentStyle 到包裹组件的最顶层父容器(如 .fc-form-col)
88
- // 这样在 Flex 或 Space 布局中,可以给子元素设置 flex: 1 等样式
89
- if (ctx.rule.componentStyle && typeof ctx.rule.componentStyle === 'object') {
90
- // 将 componentStyle 合并到 prop.style,这会应用到包裹组件的容器上
91
- const existingStyle = ctx.prop.style || {}
92
- ctx.prop.style = {
93
- ...existingStyle,
94
- ...ctx.rule.componentStyle
95
- }
96
- }
97
-
98
- // 预览模式下:对 upload 组件设置 disabled
99
- // wangEditor 组件使用只读模式(readOnly),允许复制和点击链接
100
- // textarea 组件使用只读模式(readOnly),允许复制和自适应高度
101
- // select 组件使用 disabled 实现只读效果(通过 CSS 样式来保持外观)
102
- // 如果组件单独设置了 readOnly 属性,也应用相同的预览模式逻辑
103
- const isPreviewMode = this.$handle.preview === true
104
- // rule.props ctx.prop.props 中读取 readOnly 属性
105
- const isReadOnly =
106
- ctx.rule.props?.readOnly === true || ctx.prop.props?.readOnly === true
107
- const shouldApplyPreviewStyle = isPreviewMode || isReadOnly
108
-
109
- if (shouldApplyPreviewStyle) {
110
- if (ctx.rule.type === 'upload') {
111
- if (ctx.prop.props) {
112
- ctx.prop.props.disabled = true
113
- }
114
- } else if (ctx.rule.type === 'fcEditor') {
115
- // wangEditor 使用只读模式,不设置 disabled
116
- // 只读模式允许复制和点击链接,但禁止编辑
117
- if (ctx.prop.props) {
118
- ctx.prop.props.readOnly = true
119
- // 预览模式和只读模式下隐藏 placeholder
120
- delete ctx.prop.props.placeholder
121
- // 预览模式和只读模式下移除固定高度,使用自适应
122
- delete ctx.prop.props.height
123
- }
124
- } else if (
125
- ctx.rule.type === 'input' &&
126
- ctx.prop.props?.type === 'textarea'
127
- ) {
128
- // textarea 使用只读模式,不设置 disabled
129
- // 只读模式允许复制,但禁止编辑
130
- if (ctx.prop.props) {
131
- ctx.prop.props.readOnly = true
132
- // 完全自适应高度,不限制最大高度
133
- ctx.prop.props.autoSize = true
134
- // 移除 rows 属性,让 autoSize 完全控制高度
135
- delete ctx.prop.props.rows
136
- // 预览模式和只读模式下隐藏 placeholder
137
- delete ctx.prop.props.placeholder
138
- // 预览模式和只读模式下隐藏字符计数
139
- ctx.prop.props.showCount = false
140
- }
141
- } else if (ctx.rule.type === 'select') {
142
- // select 组件在预览模式下使用 disabled 实现只读效果
143
- // 注意:Ant Design Vue 的 Select 组件不支持 readOnly 属性
144
- // 所以在预览模式下使用 disabled,并通过 CSS 样式来保持外观
145
- // 强制设置 disabled = true,覆盖用户可能设置的 disabled: false
146
- if (ctx.prop.props) {
147
- ctx.prop.props.disabled = true
148
- // 预览模式和只读模式下隐藏 placeholder
149
- delete ctx.prop.props.placeholder
150
- // 如果原本设置了 readOnly,保留这个标记以便 CSS 识别
151
- if (isReadOnly) {
152
- // 可以在这里添加一个标记,但 Select 组件不支持 readOnly
153
- // 所以我们通过 disabled 来实现,CSS 会处理样式
154
- }
155
- }
156
- } else if (ctx.rule.type === 'input') {
157
- // input 组件支持 readOnly 属性
158
- // 如果设置了 readOnly,直接使用组件的 readOnly 属性
159
- if (ctx.prop.props && isReadOnly) {
160
- ctx.prop.props.readOnly = true
161
- // 预览模式和只读模式下隐藏 placeholder
162
- delete ctx.prop.props.placeholder
163
- // 预览模式和只读模式下隐藏字符计数
164
- ctx.prop.props.showCount = false
165
- } else if (ctx.prop.props && isPreviewMode) {
166
- // 全局预览模式下也隐藏 placeholder
167
- delete ctx.prop.props.placeholder
168
- // 全局预览模式下隐藏字符计数
169
- ctx.prop.props.showCount = false
170
- }
171
- } else if (
172
- ctx.rule.type === 'datePicker' ||
173
- ctx.rule.type === 'timePicker' ||
174
- ctx.rule.type === 'timeRangePicker' ||
175
- ctx.rule.type === 'rangePicker' ||
176
- ctx.rule.type === 'cascader'
177
- ) {
178
- // 日期选择器、时间选择器、级联选择器等组件
179
- // 预览模式和只读模式下隐藏 placeholder
180
- if (ctx.prop.props) {
181
- if (ctx.rule.type === 'cascader') {
182
- ctx.prop.props.disabled = true
183
- }
184
- delete ctx.prop.props.placeholder
185
- }
186
- }
187
- }
188
-
189
- // 检查父容器是否是 flex 或 space 类型,如果是,自动设置 col: false 避免被 a-col 包装
190
- // 需要在合并完 col 后再检查,确保能覆盖默认值
191
- if (ctx.parent && ctx.parent.rule) {
192
- const parentType = ctx.parent.rule.type
193
- const parentMenu = ctx.parent.rule._menu
194
- if (
195
- parentType === 'flex' ||
196
- parentType === 'a-flex' ||
197
- parentMenu?.name === 'flex' ||
198
- parentType === 'space' ||
199
- parentType === 'a-space' ||
200
- parentMenu?.name === 'space'
201
- ) {
202
- // 如果父容器是 flex 或 space,强制设置 col 为 false,禁用 col 包装
203
- ctx.prop.col = false
204
- // 同时设置 rule.col 以确保在 makeWrap 中也能识别
205
- if (ctx.rule) {
206
- ctx.rule.col = false
207
- }
208
- }
209
- }
210
-
211
- // 为 upload 组件自动添加 onPreview,确保始终向父窗口发送预览通知
212
- if (ctx.rule.type === 'upload' && !ctx.prop.props.onPreview) {
213
- const sendPreviewMessage = function (file) {
214
- if (window.parent && window.parent !== window) {
215
- window.parent.postMessage(
216
- {
217
- type: 'upload-preview',
218
- file: {
219
- url: file.url,
220
- name: file.name,
221
- uid: file.uid,
222
- size: file.size,
223
- type: file.type
224
- },
225
- timestamp: Date.now()
226
- },
227
- '*'
228
- )
229
- }
230
- }
231
- ctx.prop.props.onPreview = function (file) {
232
- sendPreviewMessage(file)
233
- // 不执行默认预览,只发送消息
234
- }
235
- } else if (ctx.rule.type === 'upload' && ctx.prop.props.onPreview) {
236
- // 如果用户已经设置了 onPreview,包装它以确保先发送消息
237
- const originalOnPreview = ctx.prop.props.onPreview
238
- const sendPreviewMessage = function (file) {
239
- if (window.parent && window.parent !== window) {
240
- window.parent.postMessage(
241
- {
242
- type: 'upload-preview',
243
- file: {
244
- url: file.url,
245
- name: file.name,
246
- uid: file.uid,
247
- size: file.size,
248
- type: file.type
249
- },
250
- timestamp: Date.now()
251
- },
252
- '*'
253
- )
254
- }
255
- }
256
- ctx.prop.props.onPreview = function (file) {
257
- sendPreviewMessage(file)
258
- if (originalOnPreview && typeof originalOnPreview === 'function') {
259
- originalOnPreview.apply(this, arguments)
260
- }
261
- }
262
- }
263
-
264
- // 为 aImage 组件自动添加预览拦截,确保始终向父窗口发送预览通知
265
- if (
266
- (ctx.rule.type === 'image' || ctx.rule.type === 'aImage') &&
267
- !ctx.prop.props.preview
268
- ) {
269
- const sendImagePreviewMessage = function (src) {
270
- if (window.parent && window.parent !== window) {
271
- window.parent.postMessage(
272
- {
273
- type: 'upload-preview',
274
- file: {
275
- url: src || ctx.prop.props.src || ''
276
- },
277
- timestamp: Date.now()
278
- },
279
- '*'
280
- )
281
- }
282
- }
283
-
284
- // 拦截 Image 组件的预览,发送消息给父窗口并阻止默认预览
285
- const imageSrc = ctx.prop.props.src || ''
286
- ctx.prop.props.preview = {
287
- visible: false,
288
- src: imageSrc,
289
- onVisibleChange: (visible, prevVisible) => {
290
- if (visible && !prevVisible) {
291
- // 发送预览通知给父窗口
292
- sendImagePreviewMessage(imageSrc)
293
- // 返回 false 阻止默认预览显示
294
- return false
295
- }
296
- }
297
- }
298
- } else if (
299
- (ctx.rule.type === 'image' || ctx.rule.type === 'aImage') &&
300
- ctx.prop.props.preview
301
- ) {
302
- // 如果用户已经设置了 preview,包装它以确保先发送消息
303
- const originalPreview = ctx.prop.props.preview
304
- const sendImagePreviewMessage = function (src) {
305
- if (window.parent && window.parent !== window) {
306
- window.parent.postMessage(
307
- {
308
- type: 'upload-preview',
309
- file: {
310
- url: src || ctx.prop.props.src || ''
311
- },
312
- timestamp: Date.now()
313
- },
314
- '*'
315
- )
316
- }
317
- }
318
-
319
- const imageSrc = ctx.prop.props.src || ''
320
- const originalOnVisibleChange = originalPreview?.onVisibleChange
321
-
322
- ctx.prop.props.preview = {
323
- ...originalPreview,
324
- visible: originalPreview.visible || false,
325
- src: originalPreview.src || imageSrc,
326
- onVisibleChange: (visible, prevVisible) => {
327
- if (visible && !prevVisible) {
328
- // 先发送消息给父窗口
329
- sendImagePreviewMessage(originalPreview.src || imageSrc)
330
- }
331
- // 执行用户自定义的 onVisibleChange
332
- if (
333
- originalOnVisibleChange &&
334
- typeof originalOnVisibleChange === 'function'
335
- ) {
336
- return originalOnVisibleChange.apply(this, arguments)
337
- }
338
- // 默认阻止预览显示
339
- return false
340
- }
341
- }
342
- }
343
- },
344
- getDefaultOptions() {
345
- return getConfig()
346
- },
347
- adapterValidate(validate, validator) {
348
- validate.validator = (rule, value) => {
349
- return new Promise((resolve, reject) => {
350
- const callback = (err) => {
351
- if (err) {
352
- reject(err)
353
- } else {
354
- resolve()
355
- }
356
- }
357
- return validator(value, callback)
358
- })
359
- }
360
- return validate
361
- },
362
- update() {
363
- const form = this.options.form
364
- this.rule = {
365
- props: { ...form },
366
- on: {
367
- submit: (e) => {
368
- e.preventDefault()
369
- }
370
- },
371
- style: form.style,
372
- type: 'form'
373
- }
374
- },
375
- beforeRender() {
376
- const { key, ref, $handle } = this
377
- const form = this.options.form
378
- extend(this.rule, {
379
- key,
380
- ref,
381
- class: [
382
- form.className,
383
- form.class,
384
- 'form-create',
385
- this.$handle.preview ? 'is-preview' : ''
386
- ]
387
- })
388
- extend(this.rule.props, {
389
- model: $handle.formData
390
- })
391
- },
392
- render(children) {
393
- if (children.slotLen() && !this.$handle.preview) {
394
- children.setSlot(undefined, () => this.makeFormBtn())
395
- }
396
- return this.$r(
397
- this.rule,
398
- isFalse(this.options.row.show)
399
- ? children.getSlots()
400
- : [this.makeRow(children)]
401
- )
402
- },
403
- makeWrap(ctx, children) {
404
- const rule = ctx.prop
405
- const uni = `${this.key}${ctx.key}`
406
- const col = rule.col
407
- const isTitle = this.isTitle(rule) && rule.wrap.title !== false
408
- const { layout, col: _col } = this.rule.props
409
- const cls = rule.wrap.class
410
- delete rule.wrap.class
411
- delete rule.wrap.title
412
-
413
- // 检查父容器是否是 flex 或 space 类型,如果是,强制禁用 col 包装
414
- let shouldDisableCol = false
415
- if (ctx.parent && ctx.parent.rule) {
416
- const parentType = ctx.parent.rule.type
417
- const parentMenu = ctx.parent.rule._menu
418
- if (
419
- parentType === 'flex' ||
420
- parentType === 'a-flex' ||
421
- parentMenu?.name === 'flex' ||
422
- parentType === 'space' ||
423
- parentType === 'a-space' ||
424
- parentMenu?.name === 'space'
425
- ) {
426
- shouldDisableCol = true
427
- }
428
- }
429
-
430
- const item = isFalse(rule.wrap.show)
431
- ? children
432
- : this.$r(
433
- mergeProps([
434
- rule.wrap,
435
- {
436
- props: {
437
- ...tidyRule(rule.wrap || {}),
438
- hasFeedback: rule.hasFeedback || false,
439
- name: ctx.id,
440
- rules: ctx.injectValidate(),
441
- ...(layout !== 'horizontal'
442
- ? { labelCol: {}, wrapperCol: {} }
443
- : {})
444
- },
445
- class: this.$render.mergeClass(
446
- cls || rule.className,
447
- 'fc-form-item'
448
- ),
449
- key: `${uni}fi`,
450
- ref: ctx.wrapRef,
451
- type: 'formItem'
452
- }
453
- ]),
454
- {
455
- default: () => children,
456
- ...(isTitle ? { label: () => this.makeInfo(rule, uni, ctx) } : {})
457
- }
458
- )
459
- // 如果父容器是 flex,或者 layout inline,或者 col 被显式禁用,则不使用 col 包装
460
- return layout === 'inline' ||
461
- isFalse(_col) ||
462
- isFalse(col.show) ||
463
- shouldDisableCol
464
- ? item
465
- : this.makeCol(rule, uni, [item], ctx)
466
- },
467
- isTitle(rule) {
468
- if (this.options.form.title === false) return false
469
- const title = rule.title
470
- return !((!title.title && !title.native) || isFalse(title.show))
471
- },
472
- makeInfo(rule, uni, ctx) {
473
- const titleProp = { ...rule.title }
474
- const infoProp = { ...rule.info }
475
- if (this.options.form.title === false) return false
476
- if ((!titleProp.title && !titleProp.native) || isFalse(titleProp.show))
477
- return
478
- const isTip = isTooltip(infoProp)
479
- const titleSlot = this.getSlot('title')
480
- const children = [
481
- titleSlot
482
- ? titleSlot({
483
- title: ctx.refRule?.__$title?.value,
484
- rule: ctx.rule,
485
- options: this.options
486
- })
487
- : ctx.refRule?.__$title?.value
488
- ]
489
-
490
- if (
491
- !isFalse(infoProp.show) &&
492
- (infoProp.info || infoProp.native) &&
493
- !isFalse(infoProp.icon)
494
- ) {
495
- const prop = {
496
- type: infoProp.type || 'popover',
497
- props: tidyRule(infoProp),
498
- key: `${uni}pop`
499
- }
500
-
501
- delete prop.props.icon
502
- delete prop.props.show
503
- delete prop.props.info
504
- delete prop.props.align
505
- delete prop.props.native
506
-
507
- const field = isTip ? 'title' : 'content'
508
- if (infoProp.info && !hasProperty(prop.props, field)) {
509
- prop.props[field] = ctx.refRule?.__$info?.value
510
- }
511
- children[infoProp.align !== 'left' ? 'unshift' : 'push'](
512
- this.$r(mergeProps([infoProp, prop]), {
513
- [titleProp.slot || 'default']: () =>
514
- this.$r({
515
- type:
516
- infoProp.icon === true
517
- ? 'QuestionCircleOutlined'
518
- : infoProp.icon || '',
519
- props: {
520
- type:
521
- infoProp.icon === true
522
- ? 'QuestionCircleOutlined'
523
- : infoProp.icon
524
- },
525
- key: `${uni}i`
526
- })
527
- })
528
- )
529
- }
530
-
531
- const _prop = mergeProps([
532
- titleProp,
533
- {
534
- props: tidyRule(titleProp),
535
- key: `${uni}tit`,
536
- class: 'fc-form-title',
537
- type: titleProp.type || 'span'
538
- }
539
- ])
540
-
541
- delete _prop.props.show
542
- delete _prop.props.title
543
- delete _prop.props.native
544
-
545
- return this.$r(_prop, children)
546
- },
547
- makeCol(rule, uni, children, ctx) {
548
- const col = rule.col
549
- // 将 componentStyle 应用到 col 容器上
550
- const style = ctx?.prop?.style || {}
551
- return this.$r(
552
- {
553
- class: this.$render.mergeClass(col.class, 'fc-form-col'),
554
- type: 'col',
555
- props: col || { span: 24 },
556
- style: style,
557
- key: `${uni}col`
558
- },
559
- children
560
- )
561
- },
562
- makeRow(children) {
563
- const row = this.options.row || {}
564
- return this.$r(
565
- {
566
- type: 'row',
567
- props: row,
568
- class: this.$render.mergeClass(row.class, 'fc-form-row'),
569
- key: `${this.key}row`
570
- },
571
- children
572
- )
573
- },
574
- makeFormBtn() {
575
- let vn = []
576
- if (!isFalse(this.options.submitBtn.show)) {
577
- vn.push(this.makeSubmitBtn())
578
- }
579
- if (!isFalse(this.options.resetBtn.show)) {
580
- vn.push(this.makeResetBtn())
581
- }
582
- if (!vn.length) {
583
- return
584
- }
585
- let { labelCol, wrapperCol, layout } = this.rule.props
586
- if (layout !== 'horizontal') {
587
- labelCol = wrapperCol = {}
588
- }
589
- const item = this.$r(
590
- {
591
- type: 'formItem',
592
- class: 'fc-form-item fc-form-footer',
593
- key: `${this.key}fb`,
594
- props: {
595
- labelCol,
596
- wrapperCol,
597
- label: ' ',
598
- colon: false
599
- }
600
- },
601
- vn
602
- )
603
-
604
- return layout === 'inline'
605
- ? item
606
- : this.$r(
607
- {
608
- type: 'col',
609
- class: 'fc-form-col',
610
- props: { span: 24 },
611
- key: `${this.key}fc`
612
- },
613
- [item]
614
- )
615
- },
616
-
617
- makeResetBtn() {
618
- const resetBtn = { ...this.options.resetBtn }
619
- const innerText =
620
- resetBtn.innerText || this.$handle.api.t('reset') || '重置'
621
- delete resetBtn.innerText
622
- delete resetBtn.click
623
- delete resetBtn.col
624
- delete resetBtn.show
625
- return this.$r(
626
- {
627
- type: 'button',
628
- props: resetBtn,
629
- class: 'fc-reset-btn',
630
- style: { width: resetBtn.width, marginLeft: '10px' },
631
- on: {
632
- click: () => {
633
- const fApi = this.$handle.api
634
- this.options.resetBtn.click
635
- ? this.options.resetBtn.click(fApi)
636
- : fApi.resetFields()
637
- }
638
- },
639
- key: `${this.key}b2`
640
- },
641
- [innerText]
642
- )
643
- },
644
- makeSubmitBtn() {
645
- const submitBtn = { ...this.options.submitBtn }
646
- const innerText =
647
- submitBtn.innerText || this.$handle.api.t('submit') || '提交'
648
- delete submitBtn.innerText
649
- delete submitBtn.click
650
- delete submitBtn.col
651
- delete submitBtn.show
652
- return this.$r(
653
- {
654
- type: 'button',
655
- props: submitBtn,
656
- class: 'fc-submit-btn',
657
- style: { width: submitBtn.width },
658
- on: {
659
- click: () => {
660
- const fApi = this.$handle.api
661
- this.options.submitBtn.click
662
- ? this.options.submitBtn.click(fApi)
663
- : fApi.submit().catch(() => {})
664
- }
665
- },
666
- key: `${this.key}b1`
667
- },
668
- [innerText]
669
- )
670
- }
671
- }
1
+ import getConfig from './config'
2
+ import mergeProps from '@form-create/utils/lib/mergeprops'
3
+ import is, { hasProperty } from '@form-create/utils/lib/type'
4
+ import extend from '@form-create/utils/lib/extend'
5
+
6
+ function isTooltip(info) {
7
+ return info.type === 'tooltip'
8
+ }
9
+
10
+ function tidy(props, name) {
11
+ if (!hasProperty(props, name)) return
12
+ if (is.String(props[name])) {
13
+ props[name] = { [name]: props[name], show: true }
14
+ }
15
+ }
16
+
17
+ function isFalse(val) {
18
+ return val === false
19
+ }
20
+
21
+ function tidyBool(opt, name) {
22
+ if (hasProperty(opt, name) && !is.Object(opt[name])) {
23
+ opt[name] = { show: !!opt[name] }
24
+ }
25
+ }
26
+
27
+ function tidyRule(rule) {
28
+ const _rule = { ...rule }
29
+ delete _rule.children
30
+ return _rule
31
+ }
32
+
33
+ export default {
34
+ validate() {
35
+ const form = this.form()
36
+ if (form) {
37
+ return form.validate()
38
+ } else {
39
+ return new Promise((v) => v())
40
+ }
41
+ },
42
+ validateField(field) {
43
+ const form = this.form()
44
+ if (form) {
45
+ return form.validateFields(field)
46
+ } else {
47
+ return new Promise((v) => v())
48
+ }
49
+ },
50
+ clearValidateState(ctx) {
51
+ const fItem = this.vm.refs[ctx.wrapRef]
52
+ if (fItem) {
53
+ fItem.clearValidate()
54
+ }
55
+ },
56
+ tidyOptions(options) {
57
+ ;['submitBtn', 'resetBtn', 'row', 'info', 'wrap', 'col', 'title'].forEach(
58
+ (name) => {
59
+ tidyBool(options, name)
60
+ }
61
+ )
62
+ return options
63
+ },
64
+ tidyRule({ prop }) {
65
+ tidy(prop, 'title')
66
+ tidy(prop, 'info')
67
+ return prop
68
+ },
69
+ mergeProp(ctx) {
70
+ const def = {
71
+ info: {
72
+ type: 'popover',
73
+ placement: 'topLeft',
74
+ icon: 'QuestionCircleOutlined'
75
+ },
76
+ title: {},
77
+ col: { span: 24 },
78
+ wrap: {}
79
+ }
80
+ ;['info', 'wrap', 'col', 'title'].forEach((name) => {
81
+ ctx.prop[name] = mergeProps(
82
+ [this.options[name] || {}, ctx.prop[name] || {}],
83
+ def[name]
84
+ )
85
+ })
86
+
87
+ // 应用 componentStyle 到包裹组件的最顶层父容器(如 .fc-form-col)
88
+ // 这样在 Flex 或 Space 布局中,可以给子元素设置 flex: 1 等样式
89
+ if (
90
+ ctx.rule.componentStyle &&
91
+ typeof ctx.rule.componentStyle === 'object'
92
+ ) {
93
+ // 将 componentStyle 合并到 prop.style,这会应用到包裹组件的容器上
94
+ const existingStyle = ctx.prop.style || {}
95
+ ctx.prop.style = {
96
+ ...existingStyle,
97
+ ...ctx.rule.componentStyle
98
+ }
99
+ }
100
+
101
+ // 预览模式下:对 upload 组件设置 disabled
102
+ // wangEditor 组件使用只读模式(readOnly),允许复制和点击链接
103
+ // textarea 组件使用只读模式(readOnly),允许复制和自适应高度
104
+ // select 组件使用 disabled 实现只读效果(通过 CSS 样式来保持外观)
105
+ // 如果组件单独设置了 readOnly 属性,也应用相同的预览模式逻辑
106
+ const isPreviewMode = this.$handle.preview === true
107
+ // rule.props ctx.prop.props 中读取 readOnly 属性
108
+ const isReadOnly =
109
+ ctx.rule.props?.readOnly === true || ctx.prop.props?.readOnly === true
110
+ const shouldApplyPreviewStyle = isPreviewMode || isReadOnly
111
+
112
+ if (shouldApplyPreviewStyle) {
113
+ if (ctx.rule.type === 'upload') {
114
+ if (ctx.prop.props) {
115
+ ctx.prop.props.disabled = true
116
+ }
117
+ } else if (ctx.rule.type === 'fcEditor') {
118
+ // wangEditor 使用只读模式,不设置 disabled
119
+ // 只读模式允许复制和点击链接,但禁止编辑
120
+ if (ctx.prop.props) {
121
+ ctx.prop.props.readOnly = true
122
+ // 预览模式和只读模式下隐藏 placeholder
123
+ delete ctx.prop.props.placeholder
124
+ // 预览模式和只读模式下移除固定高度,使用自适应
125
+ delete ctx.prop.props.height
126
+ }
127
+ } else if (
128
+ ctx.rule.type === 'input' &&
129
+ ctx.prop.props?.type === 'textarea'
130
+ ) {
131
+ // textarea 使用只读模式,不设置 disabled
132
+ // 只读模式允许复制,但禁止编辑
133
+ if (ctx.prop.props) {
134
+ ctx.prop.props.readOnly = true
135
+ // 完全自适应高度,不限制最大高度
136
+ ctx.prop.props.autoSize = true
137
+ // 移除 rows 属性,让 autoSize 完全控制高度
138
+ delete ctx.prop.props.rows
139
+ // 预览模式和只读模式下隐藏 placeholder
140
+ delete ctx.prop.props.placeholder
141
+ // 预览模式和只读模式下隐藏字符计数
142
+ ctx.prop.props.showCount = false
143
+ }
144
+ } else if (ctx.rule.type === 'select') {
145
+ // select 组件在预览模式下使用 disabled 实现只读效果
146
+ // 注意:Ant Design Vue 的 Select 组件不支持 readOnly 属性
147
+ // 所以在预览模式下使用 disabled,并通过 CSS 样式来保持外观
148
+ // 强制设置 disabled = true,覆盖用户可能设置的 disabled: false
149
+ if (ctx.prop.props) {
150
+ ctx.prop.props.disabled = true
151
+ // 预览模式和只读模式下隐藏 placeholder
152
+ delete ctx.prop.props.placeholder
153
+ // 如果原本设置了 readOnly,保留这个标记以便 CSS 识别
154
+ if (isReadOnly) {
155
+ // 可以在这里添加一个标记,但 Select 组件不支持 readOnly
156
+ // 所以我们通过 disabled 来实现,CSS 会处理样式
157
+ }
158
+ }
159
+ } else if (ctx.rule.type === 'input') {
160
+ // input 组件支持 readOnly 属性
161
+ // 如果设置了 readOnly,直接使用组件的 readOnly 属性
162
+ if (ctx.prop.props && isReadOnly) {
163
+ ctx.prop.props.readOnly = true
164
+ // 预览模式和只读模式下隐藏 placeholder
165
+ delete ctx.prop.props.placeholder
166
+ // 预览模式和只读模式下隐藏字符计数
167
+ ctx.prop.props.showCount = false
168
+ } else if (ctx.prop.props && isPreviewMode) {
169
+ // 全局预览模式下也隐藏 placeholder
170
+ delete ctx.prop.props.placeholder
171
+ // 全局预览模式下隐藏字符计数
172
+ ctx.prop.props.showCount = false
173
+ }
174
+ } else if (
175
+ ctx.rule.type === 'datePicker' ||
176
+ ctx.rule.type === 'timePicker' ||
177
+ ctx.rule.type === 'timeRangePicker' ||
178
+ ctx.rule.type === 'rangePicker' ||
179
+ ctx.rule.type === 'cascader'
180
+ ) {
181
+ // 日期选择器、时间选择器、级联选择器等组件
182
+ // 预览模式和只读模式下隐藏 placeholder
183
+ if (ctx.prop.props) {
184
+ if (ctx.rule.type === 'cascader') {
185
+ ctx.prop.props.disabled = true
186
+ }
187
+ delete ctx.prop.props.placeholder
188
+ }
189
+ }
190
+ }
191
+
192
+ // 检查父容器是否是 flex 或 space 类型,如果是,自动设置 col: false 避免被 a-col 包装
193
+ // 需要在合并完 col 后再检查,确保能覆盖默认值
194
+ if (ctx.parent && ctx.parent.rule) {
195
+ const parentType = ctx.parent.rule.type
196
+ const parentMenu = ctx.parent.rule._menu
197
+ if (
198
+ parentType === 'flex' ||
199
+ parentType === 'a-flex' ||
200
+ parentMenu?.name === 'flex' ||
201
+ parentType === 'space' ||
202
+ parentType === 'a-space' ||
203
+ parentMenu?.name === 'space'
204
+ ) {
205
+ // 如果父容器是 flex 或 space,强制设置 col 为 false,禁用 col 包装
206
+ ctx.prop.col = false
207
+ // 同时设置 rule.col 以确保在 makeWrap 中也能识别
208
+ if (ctx.rule) {
209
+ ctx.rule.col = false
210
+ }
211
+ }
212
+ }
213
+
214
+ // upload 组件自动添加 onPreview,确保始终向父窗口发送预览通知
215
+ if (ctx.rule.type === 'upload' && !ctx.prop.props.onPreview) {
216
+ const sendPreviewMessage = function (file) {
217
+ if (window.parent && window.parent !== window) {
218
+ window.parent.postMessage(
219
+ {
220
+ type: 'upload-preview',
221
+ file: {
222
+ url: file.url,
223
+ name: file.name,
224
+ uid: file.uid,
225
+ size: file.size,
226
+ type: file.type
227
+ },
228
+ timestamp: Date.now()
229
+ },
230
+ '*'
231
+ )
232
+ }
233
+ }
234
+ ctx.prop.props.onPreview = function (file) {
235
+ sendPreviewMessage(file)
236
+ // 不执行默认预览,只发送消息
237
+ }
238
+ } else if (ctx.rule.type === 'upload' && ctx.prop.props.onPreview) {
239
+ // 如果用户已经设置了 onPreview,包装它以确保先发送消息
240
+ const originalOnPreview = ctx.prop.props.onPreview
241
+ const sendPreviewMessage = function (file) {
242
+ if (window.parent && window.parent !== window) {
243
+ window.parent.postMessage(
244
+ {
245
+ type: 'upload-preview',
246
+ file: {
247
+ url: file.url,
248
+ name: file.name,
249
+ uid: file.uid,
250
+ size: file.size,
251
+ type: file.type
252
+ },
253
+ timestamp: Date.now()
254
+ },
255
+ '*'
256
+ )
257
+ }
258
+ }
259
+ ctx.prop.props.onPreview = function (file) {
260
+ sendPreviewMessage(file)
261
+ if (originalOnPreview && typeof originalOnPreview === 'function') {
262
+ originalOnPreview.apply(this, arguments)
263
+ }
264
+ }
265
+ }
266
+
267
+ // 为 aImage 组件自动添加预览拦截,确保始终向父窗口发送预览通知
268
+ if (
269
+ (ctx.rule.type === 'image' || ctx.rule.type === 'aImage') &&
270
+ !ctx.prop.props.preview
271
+ ) {
272
+ const sendImagePreviewMessage = function (src) {
273
+ if (window.parent && window.parent !== window) {
274
+ window.parent.postMessage(
275
+ {
276
+ type: 'upload-preview',
277
+ file: {
278
+ url: src || ctx.prop.props.src || ''
279
+ },
280
+ timestamp: Date.now()
281
+ },
282
+ '*'
283
+ )
284
+ }
285
+ }
286
+
287
+ // 拦截 Image 组件的预览,发送消息给父窗口并阻止默认预览
288
+ const imageSrc = ctx.prop.props.src || ''
289
+ ctx.prop.props.preview = {
290
+ visible: false,
291
+ src: imageSrc,
292
+ onVisibleChange: (visible, prevVisible) => {
293
+ if (visible && !prevVisible) {
294
+ // 发送预览通知给父窗口
295
+ sendImagePreviewMessage(imageSrc)
296
+ // 返回 false 阻止默认预览显示
297
+ return false
298
+ }
299
+ }
300
+ }
301
+ } else if (
302
+ (ctx.rule.type === 'image' || ctx.rule.type === 'aImage') &&
303
+ ctx.prop.props.preview
304
+ ) {
305
+ // 如果用户已经设置了 preview,包装它以确保先发送消息
306
+ const originalPreview = ctx.prop.props.preview
307
+ const sendImagePreviewMessage = function (src) {
308
+ if (window.parent && window.parent !== window) {
309
+ window.parent.postMessage(
310
+ {
311
+ type: 'upload-preview',
312
+ file: {
313
+ url: src || ctx.prop.props.src || ''
314
+ },
315
+ timestamp: Date.now()
316
+ },
317
+ '*'
318
+ )
319
+ }
320
+ }
321
+
322
+ const imageSrc = ctx.prop.props.src || ''
323
+ const originalOnVisibleChange = originalPreview?.onVisibleChange
324
+
325
+ ctx.prop.props.preview = {
326
+ ...originalPreview,
327
+ visible: originalPreview.visible || false,
328
+ src: originalPreview.src || imageSrc,
329
+ onVisibleChange: (visible, prevVisible) => {
330
+ if (visible && !prevVisible) {
331
+ // 先发送消息给父窗口
332
+ sendImagePreviewMessage(originalPreview.src || imageSrc)
333
+ }
334
+ // 执行用户自定义的 onVisibleChange
335
+ if (
336
+ originalOnVisibleChange &&
337
+ typeof originalOnVisibleChange === 'function'
338
+ ) {
339
+ return originalOnVisibleChange.apply(this, arguments)
340
+ }
341
+ // 默认阻止预览显示
342
+ return false
343
+ }
344
+ }
345
+ }
346
+ },
347
+ getDefaultOptions() {
348
+ return getConfig()
349
+ },
350
+ adapterValidate(validate, validator) {
351
+ validate.validator = (rule, value) => {
352
+ return new Promise((resolve, reject) => {
353
+ const callback = (err) => {
354
+ if (err) {
355
+ reject(err)
356
+ } else {
357
+ resolve()
358
+ }
359
+ }
360
+ return validator(value, callback)
361
+ })
362
+ }
363
+ return validate
364
+ },
365
+ update() {
366
+ const form = this.options.form
367
+ this.rule = {
368
+ props: { ...form },
369
+ on: {
370
+ submit: (e) => {
371
+ e.preventDefault()
372
+ }
373
+ },
374
+ style: form.style,
375
+ type: 'form'
376
+ }
377
+ },
378
+ beforeRender() {
379
+ const { key, ref, $handle } = this
380
+ const form = this.options.form
381
+ extend(this.rule, {
382
+ key,
383
+ ref,
384
+ class: [
385
+ form.className,
386
+ form.class,
387
+ 'form-create',
388
+ this.$handle.preview ? 'is-preview' : ''
389
+ ]
390
+ })
391
+ extend(this.rule.props, {
392
+ model: $handle.formData
393
+ })
394
+ },
395
+ render(children) {
396
+ if (children.slotLen() && !this.$handle.preview) {
397
+ children.setSlot(undefined, () => this.makeFormBtn())
398
+ }
399
+ return this.$r(
400
+ this.rule,
401
+ isFalse(this.options.row.show)
402
+ ? children.getSlots()
403
+ : [this.makeRow(children)]
404
+ )
405
+ },
406
+ makeWrap(ctx, children) {
407
+ const rule = ctx.prop
408
+ const uni = `${this.key}${ctx.key}`
409
+ const col = rule.col
410
+ const isTitle = this.isTitle(rule) && rule.wrap.title !== false
411
+ const { layout, col: _col } = this.rule.props
412
+ const cls = rule.wrap.class
413
+ delete rule.wrap.class
414
+ delete rule.wrap.title
415
+
416
+ // 检查父容器是否是 flex 或 space 类型,如果是,强制禁用 col 包装
417
+ let shouldDisableCol = false
418
+ if (ctx.parent && ctx.parent.rule) {
419
+ const parentType = ctx.parent.rule.type
420
+ const parentMenu = ctx.parent.rule._menu
421
+ if (
422
+ parentType === 'flex' ||
423
+ parentType === 'a-flex' ||
424
+ parentMenu?.name === 'flex' ||
425
+ parentType === 'space' ||
426
+ parentType === 'a-space' ||
427
+ parentMenu?.name === 'space'
428
+ ) {
429
+ shouldDisableCol = true
430
+ }
431
+ }
432
+
433
+ const item = isFalse(rule.wrap.show)
434
+ ? children
435
+ : this.$r(
436
+ mergeProps([
437
+ rule.wrap,
438
+ {
439
+ props: {
440
+ ...tidyRule(rule.wrap || {}),
441
+ hasFeedback: rule.hasFeedback || false,
442
+ name: ctx.id,
443
+ rules: ctx.injectValidate(),
444
+ ...(layout !== 'horizontal'
445
+ ? { labelCol: {}, wrapperCol: {} }
446
+ : {})
447
+ },
448
+ class: this.$render.mergeClass(
449
+ cls || rule.className,
450
+ 'fc-form-item'
451
+ ),
452
+ key: `${uni}fi`,
453
+ ref: ctx.wrapRef,
454
+ type: 'formItem'
455
+ }
456
+ ]),
457
+ {
458
+ default: () => children,
459
+ ...(isTitle ? { label: () => this.makeInfo(rule, uni, ctx) } : {})
460
+ }
461
+ )
462
+ // 如果父容器是 flex,或者 layout 是 inline,或者 col 被显式禁用,则不使用 col 包装
463
+ return layout === 'inline' ||
464
+ isFalse(_col) ||
465
+ isFalse(col.show) ||
466
+ shouldDisableCol
467
+ ? item
468
+ : this.makeCol(rule, uni, [item], ctx)
469
+ },
470
+ isTitle(rule) {
471
+ if (this.options.form.title === false) return false
472
+ const title = rule.title
473
+ return !((!title.title && !title.native) || isFalse(title.show))
474
+ },
475
+ makeInfo(rule, uni, ctx) {
476
+ const titleProp = { ...rule.title }
477
+ const infoProp = { ...rule.info }
478
+ if (this.options.form.title === false) return false
479
+ if ((!titleProp.title && !titleProp.native) || isFalse(titleProp.show))
480
+ return
481
+ const isTip = isTooltip(infoProp)
482
+ const titleSlot = this.getSlot('title')
483
+ const children = [
484
+ titleSlot
485
+ ? titleSlot({
486
+ title: ctx.refRule?.__$title?.value,
487
+ rule: ctx.rule,
488
+ options: this.options
489
+ })
490
+ : ctx.refRule?.__$title?.value
491
+ ]
492
+
493
+ if (
494
+ !isFalse(infoProp.show) &&
495
+ (infoProp.info || infoProp.native) &&
496
+ !isFalse(infoProp.icon)
497
+ ) {
498
+ const prop = {
499
+ type: infoProp.type || 'popover',
500
+ props: tidyRule(infoProp),
501
+ key: `${uni}pop`
502
+ }
503
+
504
+ delete prop.props.icon
505
+ delete prop.props.show
506
+ delete prop.props.info
507
+ delete prop.props.align
508
+ delete prop.props.native
509
+
510
+ const field = isTip ? 'title' : 'content'
511
+ if (infoProp.info && !hasProperty(prop.props, field)) {
512
+ prop.props[field] = ctx.refRule?.__$info?.value
513
+ }
514
+ children[infoProp.align !== 'left' ? 'unshift' : 'push'](
515
+ this.$r(mergeProps([infoProp, prop]), {
516
+ [titleProp.slot || 'default']: () =>
517
+ this.$r({
518
+ type:
519
+ infoProp.icon === true
520
+ ? 'QuestionCircleOutlined'
521
+ : infoProp.icon || '',
522
+ props: {
523
+ type:
524
+ infoProp.icon === true
525
+ ? 'QuestionCircleOutlined'
526
+ : infoProp.icon
527
+ },
528
+ key: `${uni}i`
529
+ })
530
+ })
531
+ )
532
+ }
533
+
534
+ const _prop = mergeProps([
535
+ titleProp,
536
+ {
537
+ props: tidyRule(titleProp),
538
+ key: `${uni}tit`,
539
+ class: 'fc-form-title',
540
+ type: titleProp.type || 'span'
541
+ }
542
+ ])
543
+
544
+ delete _prop.props.show
545
+ delete _prop.props.title
546
+ delete _prop.props.native
547
+
548
+ return this.$r(_prop, children)
549
+ },
550
+ makeCol(rule, uni, children, ctx) {
551
+ const col = rule.col
552
+ // 将 componentStyle 应用到 col 容器上
553
+ const style = ctx?.prop?.style || {}
554
+ return this.$r(
555
+ {
556
+ class: this.$render.mergeClass(col.class, 'fc-form-col'),
557
+ type: 'col',
558
+ props: col || { span: 24 },
559
+ style: style,
560
+ key: `${uni}col`
561
+ },
562
+ children
563
+ )
564
+ },
565
+ makeRow(children) {
566
+ const row = this.options.row || {}
567
+ return this.$r(
568
+ {
569
+ type: 'row',
570
+ props: row,
571
+ class: this.$render.mergeClass(row.class, 'fc-form-row'),
572
+ key: `${this.key}row`
573
+ },
574
+ children
575
+ )
576
+ },
577
+ makeFormBtn() {
578
+ let vn = []
579
+ if (!isFalse(this.options.submitBtn.show)) {
580
+ vn.push(this.makeSubmitBtn())
581
+ }
582
+ if (!isFalse(this.options.resetBtn.show)) {
583
+ vn.push(this.makeResetBtn())
584
+ }
585
+ if (!vn.length) {
586
+ return
587
+ }
588
+ let { labelCol, wrapperCol, layout } = this.rule.props
589
+ if (layout !== 'horizontal') {
590
+ labelCol = wrapperCol = {}
591
+ }
592
+ const item = this.$r(
593
+ {
594
+ type: 'formItem',
595
+ class: 'fc-form-item fc-form-footer',
596
+ key: `${this.key}fb`,
597
+ props: {
598
+ labelCol,
599
+ wrapperCol,
600
+ label: ' ',
601
+ colon: false
602
+ }
603
+ },
604
+ vn
605
+ )
606
+
607
+ return layout === 'inline'
608
+ ? item
609
+ : this.$r(
610
+ {
611
+ type: 'col',
612
+ class: 'fc-form-col',
613
+ props: { span: 24 },
614
+ key: `${this.key}fc`
615
+ },
616
+ [item]
617
+ )
618
+ },
619
+
620
+ makeResetBtn() {
621
+ const resetBtn = { ...this.options.resetBtn }
622
+ const innerText =
623
+ resetBtn.innerText || this.$handle.api.t('reset') || '重置'
624
+ delete resetBtn.innerText
625
+ delete resetBtn.click
626
+ delete resetBtn.col
627
+ delete resetBtn.show
628
+ return this.$r(
629
+ {
630
+ type: 'button',
631
+ props: resetBtn,
632
+ class: 'fc-reset-btn',
633
+ style: { width: resetBtn.width, marginLeft: '10px' },
634
+ on: {
635
+ click: () => {
636
+ const fApi = this.$handle.api
637
+ this.options.resetBtn.click
638
+ ? this.options.resetBtn.click(fApi)
639
+ : fApi.resetFields()
640
+ }
641
+ },
642
+ key: `${this.key}b2`
643
+ },
644
+ [innerText]
645
+ )
646
+ },
647
+ makeSubmitBtn() {
648
+ const submitBtn = { ...this.options.submitBtn }
649
+ const innerText =
650
+ submitBtn.innerText || this.$handle.api.t('submit') || '提交'
651
+ delete submitBtn.innerText
652
+ delete submitBtn.click
653
+ delete submitBtn.col
654
+ delete submitBtn.show
655
+ return this.$r(
656
+ {
657
+ type: 'button',
658
+ props: submitBtn,
659
+ class: 'fc-submit-btn',
660
+ style: { width: submitBtn.width },
661
+ on: {
662
+ click: () => {
663
+ const fApi = this.$handle.api
664
+ this.options.submitBtn.click
665
+ ? this.options.submitBtn.click(fApi)
666
+ : fApi.submit().catch(() => {})
667
+ }
668
+ },
669
+ key: `${this.key}b1`
670
+ },
671
+ [innerText]
672
+ )
673
+ }
674
+ }