@longhongguo/form-create-ant-design-vue 3.3.35 → 3.3.37

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,674 +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 (
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
- }
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 容器上,不和 prop.style 混用
553
+ const componentStyle = ctx?._componentStyle || {}
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: componentStyle,
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
+ }