@longhongguo/form-create-ant-design-vue 3.3.25 → 3.3.27

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longhongguo/form-create-ant-design-vue",
3
- "version": "3.3.25",
3
+ "version": "3.3.27",
4
4
  "description": "AntDesignVue版本低代码表单|FormCreate 是一个可以通过 JSON 生成具有动态渲染、数据收集、验证和提交功能的低代码表单生成组件。支持6个UI框架,适配移动端,并且支持生成任何 Vue 组件。内置20种常用表单组件和自定义组件,再复杂的表单都可以轻松搞定。",
5
5
  "main": "./dist/form-create.min.js",
6
6
  "module": "./dist/form-create.esm.js",
@@ -1,12 +1,22 @@
1
1
  <template>
2
- <FcEditor
3
- :model-value="modelValue"
4
- :disabled="disabled && !readOnly"
5
- :readOnly="readOnly"
6
- :config="editorConfig"
7
- :init="initEditor"
8
- @update:model-value="$emit('update:modelValue', $event)"
9
- />
2
+ <div class="fc-editor-wrapper">
3
+ <!-- 只读/预览模式:直接用 v-html 渲染,并添加图片点击监听 -->
4
+ <div
5
+ v-if="readOnly"
6
+ ref="readOnlyContainer"
7
+ class="fc-editor-readonly"
8
+ v-html="modelValue"
9
+ ></div>
10
+ <!-- 编辑模式:使用富文本编辑器 -->
11
+ <FcEditor
12
+ v-else
13
+ :model-value="modelValue"
14
+ :disabled="disabled"
15
+ :config="editorConfig"
16
+ :init="initEditor"
17
+ @update:model-value="$emit('update:modelValue', $event)"
18
+ />
19
+ </div>
10
20
  </template>
11
21
 
12
22
  <script>
@@ -49,16 +59,33 @@ export default defineComponent({
49
59
  emits: ['update:modelValue'],
50
60
  watch: {
51
61
  readOnly(newVal) {
52
- // 当只读状态变化时,更新编辑器状态
53
- if (this._editor) {
54
- this.setReadOnlyMode(newVal)
55
- // 如果是只读模式,设置图片点击监听
56
- if (newVal) {
57
- this.setupImageClickHandler(this._editor)
58
- }
62
+ if (!newVal && this._editor) {
63
+ // 切换到编辑模式
64
+ this.setReadOnlyMode(false)
65
+ } else if (newVal) {
66
+ // 切换到只读模式,设置图片点击监听
67
+ this.$nextTick(() => {
68
+ this.setupImageClickHandlers()
69
+ })
70
+ }
71
+ },
72
+ modelValue() {
73
+ // 内容变化时,重新设置图片点击监听(仅只读模式)
74
+ if (this.readOnly) {
75
+ this.$nextTick(() => {
76
+ this.setupImageClickHandlers()
77
+ })
59
78
  }
60
79
  }
61
80
  },
81
+ mounted() {
82
+ // 如果是只读模式,设置图片点击监听
83
+ if (this.readOnly) {
84
+ this.$nextTick(() => {
85
+ this.setupImageClickHandlers()
86
+ })
87
+ }
88
+ },
62
89
  computed: {
63
90
  editorConfig() {
64
91
  const config = {}
@@ -217,7 +244,20 @@ export default defineComponent({
217
244
  return config
218
245
  }
219
246
  },
247
+ mounted() {
248
+ // 如果是只读模式,设置图片点击监听
249
+ if (this.readOnly) {
250
+ this.$nextTick(() => {
251
+ this.setupImageClickHandlers()
252
+ })
253
+ }
254
+ },
220
255
  beforeUnmount() {
256
+ // 清理图片点击监听器
257
+ if (this._imageClickCleanup) {
258
+ this._imageClickCleanup()
259
+ this._imageClickCleanup = null
260
+ }
221
261
  // 清理内容变化事件监听器
222
262
  if (this._editor && this._editor._fcChangeHandlers) {
223
263
  this._editor._fcChangeHandlers.forEach((item) => {
@@ -346,6 +386,98 @@ export default defineComponent({
346
386
  }
347
387
  },
348
388
  methods: {
389
+ // 设置只读模式下图片的点击监听器
390
+ setupImageClickHandlers() {
391
+ if (!this.readOnly || !this.$refs.readOnlyContainer) {
392
+ console.log(
393
+ '[FcEditorWrapper] setupImageClickHandlers: Not in readOnly mode or container not found.'
394
+ )
395
+ return
396
+ }
397
+
398
+ const container = this.$refs.readOnlyContainer
399
+
400
+ // 清理旧的监听器
401
+ if (this._imageClickCleanup) {
402
+ console.log('[FcEditorWrapper] Cleaning up old image click handlers.')
403
+ this._imageClickCleanup()
404
+ this._imageClickCleanup = null
405
+ }
406
+
407
+ // 使用事件委托,在容器上监听点击事件
408
+ const handleContainerClick = (e) => {
409
+ // 查找被点击的图片元素
410
+ let target = e.target
411
+ while (target && target !== container) {
412
+ if (target.tagName === 'IMG') {
413
+ // 找到了图片
414
+ const img = target
415
+ console.log('[FcEditorWrapper] Image clicked:', img.src)
416
+
417
+ const imgSrc = img.getAttribute('src') || img.src
418
+ const imgAlt = img.getAttribute('alt') || ''
419
+ const imgTitle = img.getAttribute('title') || imgAlt
420
+
421
+ // 检查图片是否在链接内
422
+ const parentLink = img.closest('a')
423
+ let imgUrl = imgSrc
424
+ if (parentLink) {
425
+ imgUrl = parentLink.getAttribute('href') || imgSrc
426
+ console.log('[FcEditorWrapper] Image is inside a link:', imgUrl)
427
+ // 不阻止默认行为,让链接正常跳转
428
+ }
429
+
430
+ // 生成 uid(基于图片 URL)
431
+ const uid = imgSrc.split('').reduce((acc, char) => {
432
+ return ((acc << 5) - acc + char.charCodeAt(0)) | 0
433
+ }, 0)
434
+
435
+ // 发送预览消息到父窗口(类似 Upload 组件)
436
+ if (window.parent && window.parent !== window) {
437
+ const message = {
438
+ type: 'upload-preview',
439
+ file: {
440
+ url: imgUrl,
441
+ name: imgTitle || imgAlt || '图片',
442
+ uid: uid,
443
+ size: 0,
444
+ type: 'image'
445
+ },
446
+ timestamp: Date.now()
447
+ }
448
+ console.log('[FcEditorWrapper] Sending postMessage:', message)
449
+ window.parent.postMessage(message, '*')
450
+ } else {
451
+ console.warn('[FcEditorWrapper] No parent window to send message')
452
+ }
453
+
454
+ // 不阻止默认行为和事件冒泡,让链接和图片都能正常工作
455
+ // 如果图片在链接内,链接会正常跳转
456
+ // 如果图片不在链接内,图片也没有默认行为需要阻止
457
+ break
458
+ }
459
+ target = target.parentElement
460
+ }
461
+ }
462
+
463
+ // 在容器上添加点击事件监听器
464
+ container.addEventListener('click', handleContainerClick, true) // 使用 capture 阶段
465
+ console.log('[FcEditorWrapper] Added container click listener for images')
466
+
467
+ // 为所有图片设置样式
468
+ const images = container.querySelectorAll('img')
469
+ console.log('[FcEditorWrapper] Found images:', images.length)
470
+ images.forEach((img) => {
471
+ img.style.cursor = 'pointer'
472
+ img.style.pointerEvents = 'auto'
473
+ })
474
+
475
+ // 保存清理函数
476
+ this._imageClickCleanup = () => {
477
+ console.log('[FcEditorWrapper] Executing image click cleanup.')
478
+ container.removeEventListener('click', handleContainerClick, true)
479
+ }
480
+ },
349
481
  initEditor(editor) {
350
482
  if (!editor) return
351
483
 
@@ -541,21 +673,17 @@ export default defineComponent({
541
673
  // 立即设置一次
542
674
  this.$nextTick(() => {
543
675
  this.setReadOnlyMode(true)
544
- this.setupImageClickHandler(editor)
545
676
  })
546
677
 
547
678
  // 第一次延迟:等待编辑器 DOM 创建
548
679
  setTimeout(() => {
549
680
  this.setReadOnlyMode(true)
550
- this.setupImageClickHandler(editor)
551
681
  // 第二次延迟:确保设置生效
552
682
  setTimeout(() => {
553
683
  this.setReadOnlyMode(true)
554
- this.setupImageClickHandler(editor)
555
684
  // 第三次延迟:确保完全生效
556
685
  setTimeout(() => {
557
686
  this.setReadOnlyMode(true)
558
- this.setupImageClickHandler(editor)
559
687
  }, 300)
560
688
  }, 200)
561
689
  }, 100)
@@ -1045,90 +1173,6 @@ export default defineComponent({
1045
1173
  }
1046
1174
  })
1047
1175
  },
1048
- // 设置图片点击处理器(用于只读/预览模式下向父窗口发送预览消息)
1049
- setupImageClickHandler(editor) {
1050
- if (!editor) return
1051
-
1052
- // 如果已经有处理器,先清理
1053
- if (this._imageClickHandler && this._imageClickContainer) {
1054
- this._imageClickContainer.removeEventListener(
1055
- 'click',
1056
- this._imageClickHandler,
1057
- true
1058
- )
1059
- }
1060
-
1061
- // 查找编辑器文本容器
1062
- let textContainer = null
1063
- if (editor.$textElem && editor.$textElem[0]) {
1064
- textContainer = editor.$textElem[0]
1065
- } else if (editor.id) {
1066
- const editorEl = document.getElementById(editor.id)
1067
- if (editorEl) {
1068
- textContainer = editorEl.querySelector('.w-e-text')
1069
- }
1070
- }
1071
-
1072
- if (!textContainer) {
1073
- // 如果找不到容器,延迟重试
1074
- setTimeout(() => {
1075
- this.setupImageClickHandler(editor)
1076
- }, 100)
1077
- return
1078
- }
1079
-
1080
- // 创建图片点击处理器
1081
- const handleImageClick = (event) => {
1082
- // 检查是否点击的是图片
1083
- const target = event.target
1084
- if (target && target.tagName === 'IMG') {
1085
- event.preventDefault()
1086
- event.stopPropagation()
1087
-
1088
- // 提取图片信息
1089
- const imgSrc = target.getAttribute('src') || target.src
1090
- const imgAlt = target.getAttribute('alt') || ''
1091
- const imgTitle = target.getAttribute('title') || imgAlt
1092
-
1093
- // 如果图片有链接,提取链接地址
1094
- let imgUrl = imgSrc
1095
- const parentLink = target.closest('a')
1096
- if (parentLink) {
1097
- imgUrl = parentLink.getAttribute('href') || imgSrc
1098
- }
1099
-
1100
- // 发送预览消息到父窗口(类似 Upload 组件)
1101
- if (window.parent && window.parent !== window) {
1102
- // 生成一个基于图片 URL 的 uid(用于标识图片)
1103
- const uid = imgSrc.split('').reduce((acc, char) => {
1104
- return ((acc << 5) - acc + char.charCodeAt(0)) | 0
1105
- }, 0)
1106
-
1107
- window.parent.postMessage(
1108
- {
1109
- type: 'upload-preview',
1110
- file: {
1111
- url: imgUrl, // 使用图片 URL 或链接地址
1112
- name: imgTitle || imgAlt || '图片', // 使用 title、alt 或默认值
1113
- uid: uid, // 基于 URL 生成的标识
1114
- size: 0, // 图片大小未知
1115
- type: 'image' // 文件类型
1116
- },
1117
- timestamp: Date.now()
1118
- },
1119
- '*'
1120
- )
1121
- }
1122
- }
1123
- }
1124
-
1125
- // 在捕获阶段添加点击监听器(优先级高)
1126
- textContainer.addEventListener('click', handleImageClick, true)
1127
-
1128
- // 保存引用以便清理
1129
- this._imageClickHandler = handleImageClick
1130
- this._imageClickContainer = textContainer
1131
- },
1132
1176
  // 验证是否为有效的URL
1133
1177
  isValidUrl(str) {
1134
1178
  if (!str || typeof str !== 'string') return false
@@ -1184,3 +1228,30 @@ export default defineComponent({
1184
1228
  }
1185
1229
  })
1186
1230
  </script>
1231
+
1232
+ <style scoped>
1233
+ .fc-editor-wrapper {
1234
+ width: 100%;
1235
+ }
1236
+
1237
+ .fc-editor-readonly {
1238
+ padding: 0 10px;
1239
+ border-radius: 4px;
1240
+ /* 覆盖预览模式的全局 pointer-events: none 规则 */
1241
+ pointer-events: auto !important;
1242
+ }
1243
+
1244
+ .fc-editor-readonly img {
1245
+ cursor: pointer;
1246
+ max-width: 100%;
1247
+ height: auto;
1248
+ /* 确保图片可以点击,覆盖预览模式的全局规则 */
1249
+ pointer-events: auto !important;
1250
+ }
1251
+
1252
+ .fc-editor-readonly a {
1253
+ /* 确保链接可以点击,覆盖预览模式的全局规则 */
1254
+ pointer-events: auto !important;
1255
+ cursor: pointer;
1256
+ }
1257
+ </style>
@@ -27,7 +27,7 @@ import { markRaw, reactive } from 'vue'
27
27
 
28
28
  export default {
29
29
  name: 'TableForm',
30
- emits: ['change', 'add', 'delete', 'update:modelValue'],
30
+ emits: ['change', 'add', 'delete', 'update:modelValue', 'select-change'],
31
31
  props: {
32
32
  formCreateInject: Object,
33
33
  modelValue: {
@@ -66,6 +66,10 @@ export default {
66
66
  type: Boolean,
67
67
  default: true
68
68
  },
69
+ showSelectColumn: {
70
+ type: Boolean,
71
+ default: false
72
+ },
69
73
  showOperationColumn: {
70
74
  type: Boolean,
71
75
  default: true
@@ -93,6 +97,10 @@ export default {
93
97
  this.loadRule()
94
98
  this.updateTable()
95
99
  },
100
+ showSelectColumn() {
101
+ this.loadRule()
102
+ this.updateTable()
103
+ },
96
104
  showOperationColumn() {
97
105
  this.loadRule()
98
106
  this.updateTable()
@@ -112,6 +120,7 @@ export default {
112
120
  Form: markRaw(this.formCreateInject.form.$form()),
113
121
  copyTrs: '',
114
122
  oldValue: '',
123
+ selectedRows: new Set(), // 存储选中行的索引
115
124
  emptyRule: {
116
125
  type: 'tr',
117
126
  _isEmpty: true,
@@ -143,6 +152,7 @@ export default {
143
152
  return (
144
153
  this.columns.length +
145
154
  (this.showIndexColumn ? 1 : 0) +
155
+ (this.showSelectColumn ? 1 : 0) +
146
156
  (this.showOperationColumn ? (this.formCreateInject.preview ? 0 : 1) : 0)
147
157
  )
148
158
  },
@@ -187,6 +197,15 @@ export default {
187
197
  return
188
198
  }
189
199
  this.oldValue = str
200
+ // 清理超出范围的选中状态
201
+ const newSelectedRows = new Set()
202
+ this.selectedRows.forEach(idx => {
203
+ if (idx < this.modelValue.length) {
204
+ newSelectedRows.add(idx)
205
+ }
206
+ })
207
+ this.selectedRows = newSelectedRows
208
+
190
209
  this.trs = this.trs.splice(0, this.modelValue.length)
191
210
  if (!this.modelValue.length) {
192
211
  this.addEmpty()
@@ -200,6 +219,10 @@ export default {
200
219
  this.setRawData(idx, data || {})
201
220
  })
202
221
  this.rule[0].children[1].children = this.trs
222
+ // 更新全选状态
223
+ if (this.showSelectColumn) {
224
+ this.updateSelectAllState()
225
+ }
203
226
  },
204
227
  updateEmptyRule() {
205
228
  const emptyTextValue =
@@ -242,6 +265,19 @@ export default {
242
265
  ) {
243
266
  return
244
267
  }
268
+ // 删除选中状态
269
+ this.selectedRows.delete(idx)
270
+ // 更新后续行的索引
271
+ const newSelectedRows = new Set()
272
+ this.selectedRows.forEach(selectedIdx => {
273
+ if (selectedIdx < idx) {
274
+ newSelectedRows.add(selectedIdx)
275
+ } else if (selectedIdx > idx) {
276
+ newSelectedRows.add(selectedIdx - 1)
277
+ }
278
+ })
279
+ this.selectedRows = newSelectedRows
280
+
245
281
  this.trs.splice(idx, 1)
246
282
  this.updateValue()
247
283
  if (this.trs.length) {
@@ -250,6 +286,8 @@ export default {
250
286
  this.addEmpty()
251
287
  }
252
288
  this.$emit('delete', idx)
289
+ // 触发选择变化事件
290
+ this.$emit('select-change', Array.from(this.selectedRows))
253
291
  },
254
292
  addRaw(flag) {
255
293
  if (flag && this.disabled) {
@@ -265,13 +303,47 @@ export default {
265
303
  this.$emit('add', this.trs.length)
266
304
  this.updateValue()
267
305
  }
306
+ // 更新全选状态
307
+ this.updateSelectAllState()
268
308
  },
269
309
  updateRaw(tr) {
270
310
  const idx = this.trs.indexOf(tr)
311
+ let colOffset = 0
312
+
271
313
  // 更新序号列
272
- if (this.showIndexColumn && tr.children[0]) {
273
- tr.children[0].props.innerText = idx + 1
314
+ if (this.showIndexColumn && tr.children[colOffset]) {
315
+ tr.children[colOffset].props.innerText = idx + 1
316
+ colOffset++
317
+ }
318
+
319
+ // 更新选择列
320
+ if (this.showSelectColumn && tr.children[colOffset]) {
321
+ const selectCell = tr.children[colOffset]
322
+ const isSelected = this.selectedRows.has(idx)
323
+ if (selectCell.children && selectCell.children[0]) {
324
+ const checkbox = selectCell.children[0]
325
+ checkbox.props.checked = isSelected
326
+ checkbox.props.onClick = (e) => {
327
+ e.stopPropagation()
328
+ this.toggleRowSelect(idx, e.target.checked)
329
+ }
330
+ }
331
+ // 通过 DOM 更新 checkbox 状态(确保视图同步)
332
+ this.$nextTick(() => {
333
+ const tableEl = this.$el?.querySelector('._fc-tf-table')
334
+ if (tableEl) {
335
+ const rows = tableEl.querySelectorAll('tbody tr')
336
+ if (rows[idx]) {
337
+ const checkbox = rows[idx].querySelector('._fc-tf-select input[type="checkbox"]')
338
+ if (checkbox) {
339
+ checkbox.checked = isSelected
340
+ }
341
+ }
342
+ }
343
+ })
344
+ colOffset++
274
345
  }
346
+
275
347
  // 更新操作列的删除按钮
276
348
  if (this.showOperationColumn && tr.children[tr.children.length - 1]) {
277
349
  const operationCell = tr.children[tr.children.length - 1]
@@ -282,6 +354,65 @@ export default {
282
354
  }
283
355
  }
284
356
  },
357
+ toggleRowSelect(idx, checked) {
358
+ if (checked) {
359
+ this.selectedRows.add(idx)
360
+ } else {
361
+ this.selectedRows.delete(idx)
362
+ }
363
+ // 更新全选状态
364
+ this.updateSelectAllState()
365
+ // 触发选择变化事件
366
+ this.$emit('select-change', Array.from(this.selectedRows))
367
+ },
368
+ toggleSelectAll(checked) {
369
+ if (checked) {
370
+ // 全选:排除空数据行
371
+ this.trs.forEach((tr, idx) => {
372
+ if (!tr._isEmpty) {
373
+ this.selectedRows.add(idx)
374
+ }
375
+ })
376
+ } else {
377
+ // 取消全选
378
+ this.selectedRows.clear()
379
+ }
380
+ // 更新所有行的选择状态
381
+ this.trs.forEach((tr) => {
382
+ if (!tr._isEmpty) {
383
+ this.updateRaw(tr)
384
+ }
385
+ })
386
+ // 触发选择变化事件
387
+ this.$emit('select-change', Array.from(this.selectedRows))
388
+ },
389
+ updateSelectAllState() {
390
+ if (!this.showSelectColumn) return
391
+
392
+ const validRows = this.trs.filter(tr => !tr._isEmpty)
393
+ if (validRows.length === 0) return
394
+
395
+ const allSelected = validRows.every((tr) => {
396
+ const actualIdx = this.trs.indexOf(tr)
397
+ return this.selectedRows.has(actualIdx)
398
+ })
399
+ const someSelected = validRows.some((tr) => {
400
+ const actualIdx = this.trs.indexOf(tr)
401
+ return this.selectedRows.has(actualIdx)
402
+ })
403
+
404
+ // 更新表头的全选checkbox(通过 DOM 操作)
405
+ this.$nextTick(() => {
406
+ const tableEl = this.$el?.querySelector('._fc-tf-table')
407
+ if (tableEl) {
408
+ const headerCheckbox = tableEl.querySelector('._fc-tf-head-select input[type="checkbox"]')
409
+ if (headerCheckbox) {
410
+ headerCheckbox.checked = allSelected
411
+ headerCheckbox.indeterminate = !allSelected && someSelected
412
+ }
413
+ }
414
+ })
415
+ },
285
416
  loadRule() {
286
417
  const header = []
287
418
  let body = []
@@ -311,6 +442,56 @@ export default {
311
442
  })
312
443
  }
313
444
 
445
+ // 选择列
446
+ if (this.showSelectColumn) {
447
+ header.push({
448
+ type: 'th',
449
+ native: true,
450
+ class: '_fc-tf-head-select',
451
+ style: {
452
+ textAlign: 'center',
453
+ ...(this.headerBackgroundColor
454
+ ? {
455
+ backgroundColor: this.headerBackgroundColor
456
+ }
457
+ : {})
458
+ },
459
+ children: [
460
+ {
461
+ type: 'input',
462
+ native: true,
463
+ props: {
464
+ type: 'checkbox',
465
+ id: `_fc-tf-select-all-${this._uid || Date.now()}`,
466
+ onClick: (e) => {
467
+ this.toggleSelectAll(e.target.checked)
468
+ }
469
+ }
470
+ }
471
+ ]
472
+ })
473
+ body.push({
474
+ type: 'td',
475
+ class: '_fc-tf-select',
476
+ native: true,
477
+ style: {
478
+ textAlign: 'center'
479
+ },
480
+ children: [
481
+ {
482
+ type: 'input',
483
+ native: true,
484
+ props: {
485
+ type: 'checkbox',
486
+ onClick: (e) => {
487
+ // 这个会在 updateRaw 中被更新
488
+ }
489
+ }
490
+ }
491
+ ]
492
+ })
493
+ }
494
+
314
495
  // 数据列
315
496
  this.columns.forEach((column) => {
316
497
  header.push({
@@ -488,6 +488,21 @@ textarea[readonly].ant-input {
488
488
  -ms-user-select: none !important;
489
489
  }
490
490
 
491
+ /* 允许只读模式下的富文本编辑器容器(v-html 渲染)中的链接和图片可以点击 */
492
+ .form-create.is-preview .fc-editor-readonly {
493
+ pointer-events: auto !important;
494
+ }
495
+
496
+ .form-create.is-preview .fc-editor-readonly a {
497
+ pointer-events: auto !important;
498
+ cursor: pointer !important;
499
+ }
500
+
501
+ .form-create.is-preview .fc-editor-readonly img {
502
+ pointer-events: auto !important;
503
+ cursor: pointer !important;
504
+ }
505
+
491
506
  /* 禁止编辑:通过 contenteditable 属性控制 */
492
507
  .form-create.is-preview .w-e-text {
493
508
  user-select: text !important;