@epxing/designer 5.3.0-beta.2 → 5.3.0-beta.4

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.
Files changed (33) hide show
  1. package/API.md +160 -160
  2. package/README.md +46 -46
  3. package/dist/src/page-designer/PageDesigner.js +11 -0
  4. package/dist/src/page-designer/designer.js +21 -0
  5. package/dist/src/page-designer/previews/add-form/Preview.js +1 -1
  6. package/dist/src/page-designer/previews/add-form/Preview.vue.js +19 -8
  7. package/dist/src/page-designer/setting-panel/Setting.js +46 -10
  8. package/dist/src/visual/config-page/ComponentLibraryDialog.js +8 -2
  9. package/dist/src/visual/config-page/DashboardMobile.js +1 -1
  10. package/dist/style.css +116 -112
  11. package/package.json +8 -8
  12. package/src/index.js +27 -27
  13. package/src/page-designer/PageDesigner.vue +11 -0
  14. package/src/page-designer/designer.js +1322 -1299
  15. package/src/page-designer/previews/add-form/Preview.vue +43 -44
  16. package/src/page-designer/setting-panel/Setting.vue +24 -2
  17. package/src/page-designer/widget-preview/Mixin.js +285 -285
  18. package/src/page-designer/widget-preview/Preview.vue +322 -322
  19. package/src/page-designer/widget-preview/WidgetWrapper.vue +197 -197
  20. package/src/page-designer/widget-preview/directive.js +258 -258
  21. package/src/visual/CategoryList.vue +563 -563
  22. package/src/visual/ComponentList.vue +280 -280
  23. package/src/visual/CreatePage.vue +389 -389
  24. package/src/visual/EntityCardList.vue +449 -449
  25. package/src/visual/EntityList.vue +838 -838
  26. package/src/visual/config-page/ComponentLibraryDialog.vue +7 -1
  27. package/src/visual/config-page/DashboardMobile.vue +934 -934
  28. package/src/visual/widgets/ProcessFormItemVisual.vue +58 -58
  29. package/types/index.d.ts +22 -22
  30. package/types/page-designer.d.ts +3 -3
  31. package/types/visual-component.d.ts +1 -1
  32. package/types/visual-entity-list.d.ts +1 -1
  33. package/types/visual-entity-sub-list.d.ts +1 -1
@@ -1,1299 +1,1322 @@
1
- import { isEmpty, cloneDeep } from 'lodash-es'
2
- import { reactive, markRaw, toRaw, watch, effectScope } from 'vue'
3
- import util from '@epxing/core/src/runtime-utils/util'
4
- import storage from '@epxing/utils/src/storage'
5
- const { genId } = util
6
- const defaultPageConfig = {}
7
-
8
- /**
9
- * 获取组件上的插槽组件列表
10
- * 插槽结构:widget.slots = { [插槽名]: 组件 },如折叠项的 { header: {...} }
11
- * 运行时由各组件自己渲染(见 packages/collapse/Collapse.vue),
12
- * 设计器这边统一按这个结构识别,保证大纲、删除、复制、拖拽等链路都能覆盖到插槽里的组件。
13
- * @param { Object } widget 组件
14
- * @returns { Array<{ name: String, widget: Object }> }
15
- */
16
- export function getWidgetSlots (widget) {
17
- const { slots } = widget || {}
18
- if (!slots || typeof slots !== 'object') return []
19
- return Object.keys(slots)
20
- .filter(name => slots[name] && slots[name].id)
21
- .map(name => ({ name, widget: slots[name] }))
22
- }
23
-
24
- /**
25
- * 获取组件的全部子组件(items + 插槽),用于需要遍历整棵树的场景
26
- * @param { Object } widget 组件
27
- * @returns { Array<Object> }
28
- */
29
- export function getWidgetChildren (widget) {
30
- const { items = [] } = widget || {}
31
- return [...items, ...getWidgetSlots(widget).map(slot => slot.widget)]
32
- }
33
- export function createDesigner (vueInstance) {
34
- // T25:设计器事件枢纽用内置 emitter,替代"组件实例当事件总线"(Vue3 无实例事件 API)
35
- const designerEvents = new Map()
36
- // 组件库 / 模板库索引:CODE → 组件信息、ID → 模板。库是只读数据,markRaw 后不再被代理与依赖收集;
37
- // 此前每个画布组件的 widgetInfo 都对整个库的每个元素建依赖(N 组件 × M 库项)
38
- const widgetInfoIndex = new Map()
39
- const templateIndex = new Map()
40
- // 组件树索引:id → { widget, parent, slotName }(存原始对象)。任何深层改动只标脏,下次查询时重建一次。
41
- // 用 deep watch 标脏而不是在改动入口标脏:外部直接 splice 的入口有 7 处(Tree/Item/Preview/Setting/Overlay),漏一处就错。
42
- // 重建时走 toRaw 的原始树,不会让调用方所在的 effect 意外依赖整棵树;查询时再 reactive() 回代理返回。
43
- let widgetIndex = new Map()
44
- let indexDirty = true
45
- const rebuildIndex = () => {
46
- widgetIndex = new Map()
47
- const walk = (list, parent, slotName) => {
48
- for (const item of toRaw(list) || []) {
49
- const raw = toRaw(item)
50
- if (!raw || !raw.id) continue
51
- // 重复 id 取深度优先的第一个,与原递归实现一致
52
- if (!widgetIndex.has(raw.id)) widgetIndex.set(raw.id, { widget: raw, parent, slotName })
53
- walk(raw.items, raw, '')
54
- for (const slot of getWidgetSlots(raw)) walk([slot.widget], raw, slot.name)
55
- }
56
- }
57
- walk(state.widgetList, null, '')
58
- indexDirty = false
59
- }
60
- const lookup = id => {
61
- if (indexDirty) rebuildIndex()
62
- return widgetIndex.get(id)
63
- }
64
- const asReactive = obj => (obj ? reactive(obj) : obj)
65
- let saveTimer = null
66
- const scope = effectScope(true)
67
- const state = reactive({
68
- version: '2.0',
69
- mode: 'page',
70
- terminal: 'pc',
71
- page: {}, // 页面
72
- widgetList: [], // 组件列表
73
- widgetConfig: {}, // 组件配置
74
- templateLibrary: [], // 模板列表
75
- templateAttributeTree: [], // 模板配置参数
76
- pageConfig: {}, // 页面配置
77
- allWidgets: [],
78
- formConfig: { cssCode: '' },
79
- selectedId: null,
80
- selectedWidget: null,
81
- selectedWidgetName: null, // 选中组件名称(唯一)
82
- // 悬浮组件状态
83
- hoverWidgetId: null,
84
- hoverWidget: null,
85
- hoverParentWidget: null,
86
- // 拖拽时当前悬停的放置目标 { type: 'root' | 'container', widgetId: string | null }
87
- dragOverTarget: null,
88
- // 当前放置目标对应的 DOM 元素,用于独立浮层定位
89
- dragOverTargetElement: null,
90
- vueInstance: vueInstance,
91
-
92
- formWidget: null, // 表单设计容器
93
-
94
- cssClassList: [], // 自定义样式列表
95
-
96
- historyData: {
97
- index: -1, // index: 0,
98
- maxStep: 20,
99
- steps: []
100
- },
101
-
102
- initDesigner (resetFormJson) {
103
- this.widgetList = []
104
- this.widgetConfig = {}
105
- this.pageConfig = cloneDeep(defaultPageConfig)
106
- if (!resetFormJson) {
107
- this.initHistoryData()
108
- }
109
- },
110
-
111
- clearDesigner (skipHistoryChange) {
112
- const emptyWidgetListFlag = (this.widgetList.length === 0)
113
- this.widgetList = []
114
- this.widgetConfig = {}
115
- this.selectedId = null
116
- this.selectedWidget = {} // this.selectedWidget = null
117
- this.pageConfig = cloneDeep(defaultPageConfig)
118
-
119
- if (skipHistoryChange) {
120
- // 什么也不做!!
121
- } else if (!emptyWidgetListFlag) {
122
- this.emitHistoryChange()
123
- } else {
124
- this.saveCurrentHistoryStep()
125
- }
126
- },
127
- initWidgetList (componentList) {
128
- this.allWidgets = markRaw([...componentList])
129
- widgetInfoIndex.clear()
130
- for (const con of this.allWidgets) {
131
- if (con && con.CODE) widgetInfoIndex.set(con.CODE, con)
132
- }
133
- },
134
- initTemplateLibrary (templateLibrary) {
135
- this.templateLibrary = markRaw(templateLibrary || [])
136
- templateIndex.clear()
137
- for (const tpl of this.templateLibrary) {
138
- if (tpl && tpl.ID) templateIndex.set(tpl.ID, tpl)
139
- }
140
- },
141
- setTemplateAttributeTree (data) {
142
- this.templateAttributeTree = data
143
- },
144
- loadFormJson (page) {
145
- this.page = page
146
- const { isComponent, isTemplate, config: formJson = {} } = page
147
- if (isComponent) this.mode = 'component'
148
- if (isTemplate) this.mode = 'template'
149
- let modifiedFlag = false
150
- if (formJson) {
151
- const { items: widgetList = [], itemConfigs: widgetConfig = {}, ...pageConfig } = formJson
152
-
153
- if (widgetList) {
154
- // this.formWidget.clearWidgetRefList()
155
- this.widgetList = widgetList
156
- this.widgetConfig = widgetConfig
157
- modifiedFlag = true
158
- }
159
- if (pageConfig) {
160
- this.pageConfig = { ...pageConfig }
161
- modifiedFlag = true
162
- }
163
- if (pageConfig.version !== this.version) {
164
- this.upgradePageConfig()
165
- this.upgradeWidgetConfig()
166
- }
167
- }
168
- // 加载完成即存一步初始快照,首次操作后就能撤销回初始状态(此前无人调用 initHistoryData,回不去)
169
- this.initHistoryData()
170
- if (modifiedFlag) {
171
- this.emitEvent('form-json-imported', []) // 通知其他组件
172
- }
173
- return modifiedFlag
174
- },
175
-
176
- setSelected (selected) {
177
- if (!selected) {
178
- this.clearSelected()
179
- return
180
- }
181
-
182
- this.selectedWidget = selected
183
- if (selected.id) {
184
- this.selectedId = selected.id
185
- }
186
- },
187
-
188
- // 根据组件 id 设置悬浮组件
189
- setHoverById (id) {
190
- if (!id) {
191
- this.clearHover()
192
- return
193
- }
194
-
195
- const widget = this.getWidgetItemById(id)
196
- if (!widget) {
197
- this.clearHover()
198
- return
199
- }
200
-
201
- // 查找父组件
202
- const parentWidget = this.findParentWidget(id)
203
-
204
- this.hoverWidgetId = id
205
- this.hoverWidget = widget
206
- this.hoverParentWidget = parentWidget || null
207
-
208
- // 通知外部悬浮组件发生变化
209
- this.emitEvent('hover-change', {
210
- widget,
211
- parentWidget: this.hoverParentWidget
212
- })
213
- },
214
-
215
- // 清理悬浮组件状态
216
- clearHover () {
217
- this.hoverWidgetId = null
218
- this.hoverWidget = null
219
- this.hoverParentWidget = null
220
- this.emitEvent('hover-change', {
221
- widget: null,
222
- parentWidget: null
223
- })
224
- },
225
-
226
- setDragOverTarget (target) {
227
- this.dragOverTarget = target
228
- },
229
- setDragOverTargetElement (el) {
230
- this.dragOverTargetElement = el
231
- },
232
- clearDragOverTarget () {
233
- this.dragOverTarget = null
234
- this.dragOverTargetElement = null
235
- },
236
-
237
- // 拖拽插入位置线 { top, left, width } 视口坐标,用于 fixed 定位
238
- dragInsertLine: null,
239
- setDragInsertLine (rect) {
240
- this.dragInsertLine = rect
241
- },
242
- clearDragInsertLine () {
243
- this.dragInsertLine = null
244
- },
245
-
246
- // 预览区域拖拽状态(用于同步到大纲树)
247
- previewDragTarget: null, // { widgetId, index, position }
248
- previewDragTargetThrottle: null, // 节流定时器
249
- setPreviewDragTarget (target) {
250
- // 防抖优化:只在目标真正改变时才更新
251
- const current = this.previewDragTarget
252
- const isSame = current && target &&
253
- current.widgetId === target.widgetId &&
254
- current.index === target.index &&
255
- current.position === target.position
256
-
257
- if (isSame) return
258
-
259
- // 使用节流,避免频繁更新
260
- if (this.previewDragTargetThrottle) {
261
- clearTimeout(this.previewDragTargetThrottle)
262
- }
263
-
264
- this.previewDragTargetThrottle = setTimeout(() => {
265
- this.previewDragTarget = target
266
- this.previewDragTargetThrottle = null
267
- }, 16) // 约 60fps
268
- },
269
- clearPreviewDragTarget () {
270
- if (this.previewDragTargetThrottle) {
271
- clearTimeout(this.previewDragTargetThrottle)
272
- this.previewDragTargetThrottle = null
273
- }
274
- this.previewDragTarget = null
275
- },
276
-
277
- // 统一的拖拽状态管理
278
- dragState: {
279
- isDragging: false, // 是否正在拖拽
280
- isNativeDragging: false, // 是否是原生 HTML5 拖拽
281
- dragSource: null, // 拖拽来源:'library' | 'preview' | 'template' | 'tree'
282
- dragData: null, // 拖拽数据
283
- ghostVisible: false, // 拖拽悬浮窗是否可见
284
- ghostTitle: '', // 悬浮窗标题
285
- ghostIcon: '', // 悬浮窗图标
286
- ghostPosition: { x: 0, y: 0 } // 悬浮窗位置
287
- },
288
-
289
- // 开始拖拽
290
- startDrag (options = {}) {
291
- const { source, data, title, icon, x, y, isNative = false } = options
292
- this.dragState.isDragging = true
293
- this.dragState.isNativeDragging = isNative
294
- this.dragState.dragSource = source
295
- this.dragState.dragData = data
296
- this.dragState.ghostVisible = true
297
- this.dragState.ghostTitle = title || ''
298
- this.dragState.ghostIcon = icon || ''
299
- this.dragState.ghostPosition = { x: x || 0, y: y || 0 }
300
-
301
- // 如果是从预览区域拖拽,自动关闭左侧面板
302
- if (source === 'preview' && this.vueInstance) {
303
- this.emitEvent('close-left-panel')
304
- }
305
-
306
- // 触发拖拽开始事件
307
- this.emitEvent('drag-state-change', { ...this.dragState })
308
- },
309
-
310
- // 更新拖拽悬浮窗位置
311
- updateDragGhostPosition (x, y) {
312
- if (this.dragState.isDragging) {
313
- this.dragState.ghostPosition = { x, y }
314
- this.emitEvent('drag-ghost-move', { x, y })
315
- }
316
- },
317
-
318
- // 结束拖拽
319
- endDrag () {
320
- this.dragState.isDragging = false
321
- this.dragState.isNativeDragging = false
322
- this.dragState.dragSource = null
323
- this.dragState.dragData = null
324
- this.dragState.ghostVisible = false
325
- this.dragState.ghostTitle = ''
326
- this.dragState.ghostIcon = ''
327
- this.dragState.ghostPosition = { x: 0, y: 0 }
328
-
329
- // 清理相关状态
330
- this.clearDragOverTarget()
331
- this.clearDragInsertLine()
332
- this.clearPreviewDragTarget()
333
-
334
- // 触发拖拽结束事件
335
- this.emitEvent('drag-state-change', { ...this.dragState })
336
- },
337
-
338
- // 获取当前拖拽状态
339
- getDragState () {
340
- return { ...this.dragState }
341
- },
342
-
343
- // ========== 统一的拖拽处理方法 ==========
344
-
345
- /**
346
- * 解析拖拽数据
347
- * @param {DataTransfer} dataTransfer - 拖拽事件的 dataTransfer 对象
348
- * @returns {Object|null} 解析后的拖拽数据 { type, payload }
349
- */
350
- parseDragData (dataTransfer) {
351
- if (!dataTransfer || !dataTransfer.types || !dataTransfer.types.includes('text/plain')) {
352
- return null
353
- }
354
-
355
- try {
356
- const raw = dataTransfer.getData('text/plain')
357
- if (!raw) return null
358
-
359
- // 组件库拖拽
360
- if (raw.startsWith('page-designer-widget:')) {
361
- const payload = JSON.parse(raw.slice('page-designer-widget:'.length))
362
- return { type: 'library', payload }
363
- }
364
-
365
- // 模板库拖拽
366
- if (raw.startsWith('page-designer-template:')) {
367
- const payload = JSON.parse(raw.slice('page-designer-template:'.length))
368
- return { type: 'template', payload }
369
- }
370
-
371
- // 组件重排序拖拽(预览区或大纲树)
372
- const data = JSON.parse(raw)
373
- if (data && data.type === 'widget-reorder') {
374
- return { type: 'reorder', payload: data }
375
- }
376
-
377
- return null
378
- } catch (e) {
379
- return null
380
- }
381
- },
382
-
383
- /**
384
- * 处理拖拽放置
385
- * @param {Object} options - 放置选项
386
- * @param {DataTransfer} options.dataTransfer - 拖拽数据
387
- * @param {Object} options.target - 放置目标 { type: 'root'|'container'|'slot', widgetId, index, slotName }
388
- * @returns {boolean} 是否成功处理
389
- */
390
- handleDrop (options = {}) {
391
- const { dataTransfer, target } = options
392
- if (!dataTransfer || !target) {
393
- return false
394
- }
395
-
396
- const dragData = this.parseDragData(dataTransfer)
397
-
398
- if (!dragData) {
399
- return false
400
- }
401
-
402
- const { type, payload } = dragData
403
-
404
- // 根据拖拽类型分发处理
405
- switch (type) {
406
- case 'library':
407
- return this.handleLibraryDrop(payload, target)
408
- case 'template':
409
- return this.handleTemplateDrop(payload, target)
410
- case 'reorder':
411
- return this.handleReorderDrop(payload, target)
412
- default:
413
- return false
414
- }
415
- },
416
-
417
- /**
418
- * 处理组件库拖拽放置
419
- * @param {Object} payload - 组件数据 { code, title, custom }
420
- * @param {Object} target - 放置目标
421
- */
422
- handleLibraryDrop (payload, target) {
423
- const { code, title, custom } = payload
424
- if (!code) return false
425
-
426
- const widgetInfo = this.getWidgetByType(code) || {}
427
- const widget = {
428
- id: this.genId(code),
429
- contentType: code,
430
- title: title || widgetInfo.TITLE
431
- }
432
- if (custom) widget.type = 'custom'
433
-
434
- // 特殊处理:chart 组件需要设置 metaId
435
- if (widgetInfo.CODE === 'chart') {
436
- this.updateWidgetConfig(widget.id, { metaId: this.page.metaId })
437
- }
438
-
439
- // 插入组件
440
- this.insertWidget(widget, target)
441
- this.setSelected(widget)
442
- this.emitHistoryChange()
443
- return true
444
- },
445
-
446
- /**
447
- * 处理模板库拖拽放置
448
- * @param {Object} payload - 模板数据 { items, itemConfigs, templateJs }
449
- * @param {Object} target - 放置目标
450
- */
451
- handleTemplateDrop (payload, target) {
452
- const { items, itemConfigs, templateJs } = payload
453
- if (!items || !items.length) return false
454
-
455
- // 插槽只能放一个组件,多根模板拒绝放入
456
- if (target.type === 'slot' && items.length > 1) {
457
- this.vueInstance && this.vueInstance.$message && this.vueInstance.$message('插槽只能放置一个组件')
458
- return false
459
- }
460
-
461
- // 插入模板组件
462
- this.insertWidgets(items, target)
463
-
464
- // 更新组件配置
465
- this.mergeWidgetConfig(itemConfigs)
466
-
467
- // 处理模板 JS
468
- if (templateJs) {
469
- this.mergeTemplateJs(templateJs)
470
- }
471
-
472
- this.setSelected(items[0])
473
- this.emitHistoryChange()
474
- return true
475
- },
476
-
477
- /**
478
- * 处理组件重排序拖拽放置
479
- * @param {Object} payload - 拖拽数据 { widgetId, parentWidgetId }
480
- * @param {Object} target - 放置目标
481
- */
482
- handleReorderDrop (payload, target) {
483
- const { widgetId, parentWidgetId } = payload
484
- if (!widgetId) {
485
- return false
486
- }
487
-
488
- const sourceWidget = this.getWidgetItemById(widgetId)
489
- if (!sourceWidget) {
490
- return false
491
- }
492
-
493
- // 标准化 ID 比较(null 和 undefined 都视为相同)
494
- const sourceParentId = parentWidgetId || null
495
- const targetParentId = target.widgetId || null
496
-
497
- // 拖进插槽:不能拖自己或自己的子孙进来,否则组件树会自己套自己
498
- if (target.type === 'slot') {
499
- if (this.isDescendantWidget(target.widgetId, sourceWidget)) return false
500
- this.removeWidgetFromParent(sourceWidget)
501
- if (!this.insertWidget(sourceWidget, target)) return false
502
- this.setSelected(sourceWidget)
503
- this.emitHistoryChange()
504
- return true
505
- }
506
-
507
- // 从插槽拖出来:先摘掉插槽再插入目标位置(即使目标就是插槽宿主本身)
508
- if (this.findWidgetSlot(widgetId)) {
509
- return this.moveWidgetToAnotherContainer(sourceWidget, sourceParentId, target)
510
- }
511
-
512
- // 同容器内移动
513
- if (sourceParentId === targetParentId) {
514
- return this.moveWidgetInSameContainer(sourceWidget, target)
515
- }
516
-
517
- // 跨容器移动
518
- return this.moveWidgetToAnotherContainer(sourceWidget, sourceParentId, target)
519
- },
520
-
521
- /**
522
- * 在同一容器内移动组件
523
- * @param {Object} widget - 要移动的组件
524
- * @param {Object} target - 目标位置
525
- */
526
- moveWidgetInSameContainer (widget, target) {
527
- const list = target.widgetId ? this.getWidgetItemById(target.widgetId).items : this.widgetList
528
- if (!list) {
529
- return false
530
- }
531
-
532
- const sourceIndex = list.findIndex(w => w.id === widget.id)
533
-
534
- if (sourceIndex === -1) {
535
- return false
536
- }
537
-
538
- let targetIndex = target.index
539
-
540
- // 关键修正:target.index 是基于过滤掉拖拽元素后的列表计算的
541
- // 需要调整为完整列表中的索引
542
- if (targetIndex > sourceIndex) {
543
- // 如果目标位置在源位置之后,需要 +1(因为 target.index 没有计算源元素)
544
- targetIndex = targetIndex + 1
545
- }
546
-
547
- // 位置没变,不需要移动
548
- if (sourceIndex === targetIndex || sourceIndex + 1 === targetIndex) {
549
- return false
550
- }
551
-
552
- // 移除原位置
553
- list.splice(sourceIndex, 1)
554
-
555
- // 调整目标索引(移除后列表变短了)
556
- if (targetIndex > sourceIndex) {
557
- targetIndex = targetIndex - 1
558
- }
559
-
560
- // 插入新位置
561
- list.splice(targetIndex, 0, widget)
562
-
563
- // 更新容器
564
- if (target.widgetId) {
565
- this.updateWidget(target.widgetId, { items: [...list] })
566
- } else {
567
- this.widgetList = [...list]
568
- }
569
-
570
- this.setSelected(widget)
571
- this.emitHistoryChange()
572
- return true
573
- },
574
-
575
- /**
576
- * 跨容器移动组件
577
- * @param {Object} widget - 要移动的组件
578
- * @param {string} sourceParentId - 源容器 ID
579
- * @param {Object} target - 目标位置
580
- */
581
- moveWidgetToAnotherContainer (widget, sourceParentId, target) {
582
- const sourceSlot = this.findWidgetSlot(widget.id)
583
- // 从源插槽删除
584
- if (sourceSlot) {
585
- this.clearWidgetSlot(sourceSlot.parent, sourceSlot.slotName)
586
- } else if (sourceParentId) {
587
- // 从源容器删除
588
- const sourceParent = this.getWidgetItemById(sourceParentId)
589
- if (sourceParent && sourceParent.items) {
590
- const sourceList = [...sourceParent.items]
591
- const sourceIndex = sourceList.findIndex(w => w.id === widget.id)
592
- if (sourceIndex !== -1) {
593
- sourceList.splice(sourceIndex, 1)
594
- this.updateWidget(sourceParent.id, { items: sourceList })
595
- }
596
- }
597
- } else {
598
- // 从根级删除
599
- const rootList = [...this.widgetList]
600
- const sourceIndex = rootList.findIndex(w => w.id === widget.id)
601
- if (sourceIndex !== -1) {
602
- rootList.splice(sourceIndex, 1)
603
- this.widgetList = rootList
604
- }
605
- }
606
-
607
- // 插入到目标容器
608
- this.insertWidget(widget, target)
609
- this.setSelected(widget)
610
- this.emitHistoryChange()
611
- return true
612
- },
613
-
614
- /**
615
- * 插入单个组件到指定位置
616
- * @param {Object} widget - 要插入的组件
617
- * @param {Object} target - 目标位置 { type: 'root'|'container'|'slot', widgetId, index, slotName }
618
- */
619
- insertWidget (widget, target) {
620
- // 放进插槽:一个插槽只放一个组件,已有组件时直接替换
621
- if (target.type === 'slot') {
622
- return this.setWidgetSlot(target.widgetId, target.slotName, widget)
623
- }
624
- if (target.type === 'root' || !target.widgetId) {
625
- // 插入到根级
626
- const list = [...this.widgetList]
627
- list.splice(target.index, 0, widget)
628
- this.widgetList = list
629
- } else {
630
- // 插入到容器内
631
- const container = this.getWidgetItemById(target.widgetId)
632
- if (!container) return false
633
-
634
- // 确保容器有响应式的 items 数组
635
- if (!container.items) {
636
- this.updateWidget(container.id, { items: [] })
637
- }
638
-
639
- const list = [...(container.items || [])]
640
- list.splice(target.index, 0, widget)
641
- this.updateWidget(container.id, { items: list })
642
- }
643
- return true
644
- },
645
-
646
- /**
647
- * 插入多个组件到指定位置
648
- * @param {Array} widgets - 要插入的组件数组
649
- * @param {Object} target - 目标位置
650
- */
651
- insertWidgets (widgets, target) {
652
- if (!widgets || !widgets.length) return false
653
-
654
- if (target.type === 'slot') {
655
- return this.insertWidget(widgets[0], target)
656
- }
657
- if (target.type === 'root' || !target.widgetId) {
658
- // 插入到根级
659
- const list = [...this.widgetList]
660
- list.splice(target.index, 0, ...widgets)
661
- this.widgetList = list
662
- } else {
663
- // 插入到容器内
664
- const container = this.getWidgetItemById(target.widgetId)
665
- if (!container) return false
666
-
667
- // 确保容器有响应式的 items 数组
668
- if (!container.items) {
669
- this.updateWidget(container.id, { items: [] })
670
- }
671
-
672
- const list = [...(container.items || [])]
673
- list.splice(target.index, 0, ...widgets)
674
- this.updateWidget(container.id, { items: list })
675
- }
676
- return true
677
- },
678
-
679
- /**
680
- * 合并模板 JS 到页面 JS
681
- * @param {string} templateJs - 模板的 JS 代码
682
- */
683
- mergeTemplateJs (templateJs) {
684
- let { JS = '' } = this.pageConfig
685
- const result = JS.match(/export\sfunction\s(\w+)/g)
686
- const events = []
687
- if (result) {
688
- result.forEach(item => {
689
- events.push(item.replace(/export\sfunction\s/, ''))
690
- })
691
- }
692
- const templateJsResult = templateJs.split(/export\s+function\s+/)
693
- templateJsResult.forEach(item => {
694
- const funtionStr = item.replace(/\/\*(\n|.)+?\*\/[\n\s]+$/, '')
695
- if (funtionStr.replace(/[\n\s]+/g, '').length > 0) {
696
- const funtionName = funtionStr.split(/\s+\(/)[0]
697
- if (![...new Set(events)].includes(funtionName)) {
698
- JS += 'export function ' + funtionStr.replace(/^\w+/, funtionName)
699
- }
700
- }
701
- })
702
- this.updatePageConfig({ JS })
703
- },
704
-
705
- updateSelectedWidgetNameAndLabel (selectedWidget, newName, newLabel) {
706
- this.selectedWidgetName = newName
707
- if (!!newLabel && (Object.keys(selectedWidget.options).indexOf('label') > -1)) {
708
- selectedWidget.options.label = newLabel
709
- }
710
- },
711
-
712
- clearSelected () {
713
- this.selectedId = null
714
- this.selectedWidgetName = null
715
- this.selectedWidget = null
716
- },
717
-
718
- // 检查组件是否可以移动(只有字段组件可以拖入子表单)
719
- checkWidgetMove (evt) {
720
- if (evt.draggedContext?.element) {
721
- const wgCategory = evt.draggedContext.element.category
722
- if (evt.to?.className === 'sub-form-table' && wgCategory === 'container') {
723
- return false
724
- }
725
- }
726
- return true
727
- },
728
-
729
- // 检查字段组件是否可以移动
730
- checkFieldMove (evt) {
731
- if (evt.draggedContext?.element) {
732
- const wgType = String(evt.draggedContext.element.type)
733
- if (evt.to?.className === 'sub-form-table' && wgType === 'slot') {
734
- return false
735
- }
736
- }
737
- return true
738
- },
739
- // 走索引 O(1);多个同 CODE 取最后一个(与原 forEach 覆盖语义一致),未命中返回 null
740
- getWidgetByType (typeName, type) {
741
- if (type === 'template') return templateIndex.get(typeName) || null
742
- return widgetInfoIndex.get(typeName) || null
743
- },
744
- getContainerByType (typeName) {
745
- let foundCon = null
746
- this.allWidgets.forEach(con => {
747
- if (con.CONTAINER === '1' && !!con.type && (con.type === typeName)) {
748
- foundCon = con
749
- }
750
- })
751
-
752
- return foundCon
753
- },
754
-
755
- getFieldWidgetByType (typeName) {
756
- let foundWidget = null
757
- this.allWidgets.forEach(widget => {
758
- if (widget.CONTAINER !== '1' && !!widget.type && (widget.type === typeName)) {
759
- foundWidget = widget
760
- }
761
- })
762
-
763
- return foundWidget
764
- },
765
-
766
- hasConfig (widget, configName) {
767
- let originalWidget = null
768
- if (widget.CONTAINER === '1') {
769
- originalWidget = this.getContainerByType(widget.type)
770
- } else {
771
- originalWidget = this.getFieldWidgetByType(widget.type)
772
- }
773
-
774
- if (!originalWidget || !originalWidget.options) {
775
- return false
776
- }
777
-
778
- return Object.keys(originalWidget.options).indexOf(configName) > -1
779
- },
780
- // 走索引;未命中返回 undefined(与原递归实现一致)
781
- getWidgetItemById (id) {
782
- const entry = lookup(id)
783
- return entry ? asReactive(entry.widget) : undefined
784
- },
785
-
786
- /**
787
- * 查找组件所在的插槽位置
788
- * @param { String } targetId 组件 id
789
- * @returns { { parent: Object, slotName: String } | null } 不在插槽里返回 null
790
- */
791
- findWidgetSlot (targetId) {
792
- const entry = lookup(targetId)
793
- return entry && entry.slotName ? { parent: asReactive(entry.parent), slotName: entry.slotName } : null
794
- },
795
-
796
- /**
797
- * 判断 targetId 是否在 widget 自身或其子孙(含插槽)里,
798
- * 用于阻止把组件拖进自己的插槽造成死循环
799
- */
800
- isDescendantWidget (targetId, widget) {
801
- if (!widget || !targetId) return false
802
- if (widget.id === targetId) return true
803
- return getWidgetChildren(widget).some(child => this.isDescendantWidget(targetId, child))
804
- },
805
-
806
- /**
807
- * 往插槽里放组件(已有组件时替换)
808
- */
809
- setWidgetSlot (ownerId, slotName, widget) {
810
- const owner = this.getWidgetItemById(ownerId)
811
- if (!owner || !slotName) return false
812
- this.updateWidget(ownerId, { slots: { ...(owner.slots || {}), [slotName]: widget } })
813
- return true
814
- },
815
-
816
- /**
817
- * 清空插槽(插槽全空时把 slots 一起删掉,避免配置里留空对象)
818
- */
819
- clearWidgetSlot (owner, slotName) {
820
- if (!owner || !owner.slots) return false
821
- const slots = { ...owner.slots }
822
- delete slots[slotName]
823
- this.updateWidget(owner.id, { slots: Object.keys(slots).length > 0 ? slots : null })
824
- return true
825
- },
826
-
827
- /**
828
- * 把组件从当前位置摘掉(插槽 / 容器 / 根级都支持),用于移动
829
- */
830
- removeWidgetFromParent (widget) {
831
- const slotInfo = this.findWidgetSlot(widget.id)
832
- if (slotInfo) {
833
- return this.clearWidgetSlot(slotInfo.parent, slotInfo.slotName)
834
- }
835
- const parent = this.findParentWidget(widget.id)
836
- if (parent && parent.items) {
837
- const list = [...parent.items]
838
- const index = list.findIndex(item => item.id === widget.id)
839
- if (index === -1) return false
840
- list.splice(index, 1)
841
- this.updateWidget(parent.id, { items: list })
842
- return true
843
- }
844
- const rootList = [...this.widgetList]
845
- const index = rootList.findIndex(item => item.id === widget.id)
846
- if (index === -1) return false
847
- rootList.splice(index, 1)
848
- this.widgetList = rootList
849
- return true
850
- },
851
- updateWidget (key, property) {
852
- const widget = this.getWidgetItemById(key)
853
- if (widget) {
854
- for (const key in property) {
855
- if (property[key] === null) {
856
- delete widget[key]
857
- } else {
858
- widget[key] = property[key]
859
- }
860
- }
861
- }
862
- },
863
- updateWidgetConfig (key, configs, type) {
864
- const property = {}
865
- for (const configKey in configs) {
866
- if (configKey.includes('componentName') || configKey === 'span' || configKey === 'flex') {
867
- property[configKey.replace('componentName', 'title')] = configs[configKey]
868
- }
869
- }
870
- this.updateWidget(key, property)
871
- const entry = this.widgetConfig[key]
872
- if (type === 'cover' || !entry) {
873
- // 新建或整体覆盖:只换这一条 entry 的引用,其它组件的 $configs 不受影响
874
- const next = { ...configs }
875
- for (const k in next) {
876
- if (next[k] === null) delete next[k]
877
- }
878
- this.widgetConfig[key] = next
879
- return
880
- }
881
- // 原地按字段改:画布上只有这个组件的 $configs 会重算(此前整体替换 widgetConfig,全画布跟着重算)
882
- for (const k in configs) {
883
- if (configs[k] === null) delete entry[k]
884
- else entry[k] = configs[k]
885
- }
886
- },
887
- // 批量并入多条组件配置(模板拖入 / 复制 / 拆分模板),逐 key 新增而不整体替换 widgetConfig
888
- mergeWidgetConfig (configs) {
889
- for (const key in configs) {
890
- if (configs[key] !== undefined) this.widgetConfig[key] = configs[key]
891
- }
892
- },
893
- updatePageConfig (configs) {
894
- this.pageConfig = { ...this.pageConfig, ...configs }
895
- },
896
- upgradeWidgetConfig () {
897
- // 判断组件是否存在重复id,存在则重新生成,不存在则查找对应层级的配置,如果有则更新配置
898
- const ids = []
899
- const upgradeWidget = (arr, parentId) => {
900
- arr.forEach(item => {
901
- const oldId = item.id
902
- if (parentId) {
903
- while (ids.includes(item.id)) {
904
- const code = item.contentType.replace(/-/g, '_')
905
- item.id = code + '_' + util.genId()
906
- }
907
- ids.push(item.id)
908
- item.i = item.id
909
- if (!item.css && !isEmpty(item.style)) {
910
- let cssStr = ':current {'
911
- for (const key in item.style) {
912
- const kebabCase = key.replace(/([A-Z])/g, $1 => {
913
- return '-' + $1.toLocaleLowerCase()
914
- })
915
- // eslint-disable-next-line no-useless-escape
916
- cssStr += `\r\n ${kebabCase}: ${item.style[key]};`
917
- }
918
- cssStr += '\r\n}'
919
- // 更新css
920
- item.css = cssStr
921
- delete item.style
922
- }
923
- // 仅当旧版 key(parentId-id)确实存在配置时才迁移,否则会把新版 key 下的已有配置覆盖成空对象
924
- const oldKey = parentId + '-' + oldId
925
- const oldWidgetConifg = this.widgetConfig[oldKey]
926
- if (oldWidgetConifg) {
927
- delete this.widgetConfig[oldKey]
928
- this.widgetConfig[item.id] = { ...this.widgetConfig[item.id], ...oldWidgetConifg }
929
- } else if (item.id !== oldId && this.widgetConfig[oldId]) {
930
- // id 因重复被重新生成时,把原 id 上的配置复制到新 id(原 id 仍被首个组件使用,不能删除)
931
- this.widgetConfig[item.id] = { ...this.widgetConfig[oldId] }
932
- }
933
- }
934
- const children = getWidgetChildren(item)
935
- if (children.length > 0) {
936
- upgradeWidget(children, parentId ? parentId + '-' + oldId : oldId)
937
- }
938
- })
939
- }
940
- upgradeWidget(this.widgetList)
941
- },
942
-
943
- upgradePageConfig () {
944
- const { css, style } = this.pageConfig
945
- if (!css && !isEmpty(style)) {
946
- let cssStr = ':current {'
947
- for (const key in style) {
948
- const kebabCase = key.replace(/([A-Z])/g, $1 => {
949
- return '-' + $1.toLocaleLowerCase()
950
- })
951
- // eslint-disable-next-line no-useless-escape
952
- cssStr += `\r\n ${kebabCase}: ${style[key]};`
953
- }
954
- cssStr += '\r\n}'
955
- // 更新css
956
- delete this.pageConfig.style
957
- this.pageConfig = { ...this.pageConfig, css: cssStr }
958
- }
959
- },
960
-
961
- moveUpWidget (parentList, indexOfParentList) {
962
- if (parentList) {
963
- if (indexOfParentList === 0) {
964
- this.vueInstance.$message(this.vueInstance.i18nt('designer.hint.moveUpFirstChildHint'))
965
- return
966
- }
967
-
968
- const tempWidget = parentList[indexOfParentList]
969
- parentList.splice(indexOfParentList, 1)
970
- parentList.splice(indexOfParentList - 1, 0, tempWidget)
971
- }
972
- },
973
-
974
- moveDownWidget (parentList, indexOfParentList) {
975
- if (parentList) {
976
- if (indexOfParentList === parentList.length - 1) {
977
- this.vueInstance.$message(this.vueInstance.i18nt('designer.hint.moveDownLastChildHint'))
978
- return
979
- }
980
-
981
- const tempWidget = parentList[indexOfParentList]
982
- parentList.splice(indexOfParentList, 1)
983
- parentList.splice(indexOfParentList + 1, 0, tempWidget)
984
- }
985
- },
986
-
987
- copyNewFieldWidget (origin) {
988
- const newWidget = cloneDeep(origin)
989
- const tempId = genId()
990
- newWidget.id = newWidget.type.replace(/-/g, '') + tempId
991
- // console.log('test id===', newWidget.id)
992
- newWidget.options.name = newWidget.id
993
- newWidget.options.label = newWidget.options.label || newWidget.type.toLowerCase()
994
-
995
- delete newWidget.displayName
996
- return newWidget
997
- },
998
-
999
- copyNewContainerWidget (origin) {
1000
- const newCon = genId(origin)
1001
- newCon.id = newCon.type.replace(/-/g, '') + genId()
1002
- newCon.options.name = newCon.id
1003
-
1004
- const newTabPane = cloneDeep(this.getContainerByType('tab-pane'))
1005
- newTabPane.id = 'tab-pane-' + genId()
1006
- newTabPane.options.name = 'tab1'
1007
- newTabPane.options.label = 'tab 1'
1008
- newCon.tabs.push(newTabPane)
1009
-
1010
- delete newCon.displayName
1011
- return newCon
1012
- },
1013
- addWidgetByInfo (widgetInfo, parentWidget) {
1014
- const widget = {
1015
- id: widgetInfo.CODE + '_' + genId(),
1016
- contentType: widgetInfo.CODE,
1017
- title: widgetInfo.TITLE
1018
- }
1019
- if (widgetInfo.COMPONENT_SOURCE === 'custom') {
1020
- widget.type = 'custom'
1021
- }
1022
- if (parentWidget) {
1023
- if (!parentWidget.items) {
1024
- parentWidget.items = [widget]
1025
- } else {
1026
- parentWidget.items.push(widget)
1027
- }
1028
- } else {
1029
- this.widgetList.push(widget)
1030
- }
1031
- if (widgetInfo.CODE === 'chart') {
1032
- this.updateWidgetConfig(widget.id, { metaId: this.page.metaId })
1033
- }
1034
- this.setSelected(widget)
1035
- },
1036
- addWidgets (widgets, parentWidget, option = {}) {
1037
- const { defaultSelected = true } = option
1038
- if (parentWidget) {
1039
- if (!parentWidget.items) {
1040
- parentWidget.items = [...widgets]
1041
- } else {
1042
- parentWidget.items.push(...widgets)
1043
- }
1044
- } else {
1045
- this.widgetList.push(...widgets)
1046
- }
1047
- if (defaultSelected) this.setSelected(widgets[widgets.length - 1])
1048
- },
1049
- addWidget (widget, parentWidget, option = {}) {
1050
- const { defaultSelected = true } = option
1051
- if (parentWidget) {
1052
- if (!parentWidget.items) {
1053
- parentWidget.items = [widget]
1054
- } else {
1055
- parentWidget.items.push(widget)
1056
- }
1057
- } else {
1058
- this.widgetList.push(widget)
1059
- }
1060
- if (defaultSelected) this.setSelected(widget)
1061
- },
1062
- // 查找父组件(公共方法):插槽里的组件返回插槽宿主;根级返回 null;不存在返回 undefined
1063
- findParentWidget (targetId) {
1064
- const entry = lookup(targetId)
1065
- return entry ? asReactive(entry.parent) : undefined
1066
- },
1067
-
1068
- copyWidget (widget, parentWidget) {
1069
- // 插槽是「一个插槽一个组件」,复制没有落点,直接提示
1070
- if (this.findWidgetSlot(widget.id)) {
1071
- this.vueInstance && this.vueInstance.$message && this.vueInstance.$message('插槽内的组件不支持复制')
1072
- return
1073
- }
1074
- const newWidget = cloneDeep(widget)
1075
- newWidget.id = widget.contentType + '_' + genId()
1076
- // 配置要深拷贝:配置改为原地更新后,共享同一个对象会让复制件和原件互相串改
1077
- const newConfigs = {
1078
- [newWidget.id]: cloneDeep(this.widgetConfig[widget.id])
1079
- }
1080
- // 子组件(含插槽里的组件)都要换新 id,否则复制出来的组件和原组件共用配置
1081
- const formatWidget = (arr) => {
1082
- for (const item of arr) {
1083
- const newId = item.contentType + '_' + genId()
1084
- newConfigs[newId] = cloneDeep(this.widgetConfig[item.id])
1085
- item.id = newId
1086
- formatWidget(getWidgetChildren(item))
1087
- }
1088
- }
1089
- formatWidget(getWidgetChildren(newWidget))
1090
- // 添加组件配置
1091
- if (parentWidget) {
1092
- if (!parentWidget.items) {
1093
- parentWidget.items = [newWidget]
1094
- } else {
1095
- parentWidget.items.push(newWidget)
1096
- }
1097
- } else {
1098
- this.widgetList.push(newWidget)
1099
- }
1100
- this.mergeWidgetConfig(newConfigs)
1101
- this.setSelected(newWidget)
1102
- },
1103
- deleteWidget (widget, parentWidget) {
1104
- delete this.widgetConfig[widget.id]
1105
- // 插槽里的组件:从 slots 上摘掉
1106
- const slotInfo = this.findWidgetSlot(widget.id)
1107
- if (slotInfo) {
1108
- this.clearWidgetSlot(slotInfo.parent, slotInfo.slotName)
1109
- if (this.selectedId === widget.id) this.clearSelected()
1110
- return 0
1111
- }
1112
- if (parentWidget && parentWidget.items) {
1113
- const index = parentWidget.items.findIndex(item => item.id === widget.id)
1114
- if (index !== -1) {
1115
- parentWidget.items.splice(index, 1)
1116
- return index
1117
- }
1118
- } else {
1119
- const index = this.widgetList.findIndex(item => item.id === widget.id)
1120
- if (index !== -1) {
1121
- this.widgetList.splice(index, 1)
1122
- return index
1123
- }
1124
- }
1125
- },
1126
- addContainerByDbClick (container) {
1127
- const newCon = this.copyNewContainerWidget(container)
1128
- this.widgetList.push(newCon)
1129
- this.setSelected(newCon)
1130
- },
1131
-
1132
- addFieldByDbClick (widget) {
1133
- const newWidget = this.copyNewFieldWidget(widget)
1134
- if (!!this.selectedWidget && this.selectedWidget.type === 'tab') {
1135
- // 获取当前激活的tabPane
1136
- let activeTab = this.selectedWidget.tabs[0]
1137
- this.selectedWidget.tabs.forEach(tabPane => {
1138
- if (tabPane.options.active) {
1139
- activeTab = tabPane
1140
- }
1141
- })
1142
-
1143
- !!activeTab && activeTab.widgetList.push(newWidget)
1144
- } else if (!!this.selectedWidget && !!this.selectedWidget.widgetList) {
1145
- this.selectedWidget.widgetList.push(newWidget)
1146
- } else {
1147
- this.widgetList.push(newWidget)
1148
- }
1149
-
1150
- this.setSelected(newWidget)
1151
- this.emitHistoryChange()
1152
- },
1153
-
1154
- addTabPaneOfTabs (tabsWidget) {
1155
- const tabPanes = tabsWidget.tabs
1156
- const newTabPane = cloneDeep(this.getContainerByType('tab-pane'))
1157
- newTabPane.id = 'tab-pane-' + genId()
1158
- newTabPane.options.name = newTabPane.id
1159
- newTabPane.options.label = 'tab ' + (tabPanes.length + 1)
1160
- tabPanes.push(newTabPane)
1161
- },
1162
-
1163
- deleteTabPaneOfTabs (tabsWidget, tpIdx) {
1164
- tabsWidget.tabs.splice(tpIdx, 1)
1165
- },
1166
-
1167
- emitEvent (evtName, evtData) { // 用于兄弟组件发射事件
1168
- const handlers = designerEvents.get(evtName)
1169
- if (handlers) handlers.slice().forEach(handler => handler(evtData))
1170
- },
1171
-
1172
- handleEvent (evtName, callback) { // 用于兄弟组件接收事件
1173
- const handlers = designerEvents.get(evtName)
1174
- if (handlers) handlers.push(callback)
1175
- else designerEvents.set(evtName, [callback])
1176
- },
1177
-
1178
- offEvent (evtName, callback) { // 解除接收
1179
- const handlers = designerEvents.get(evtName)
1180
- if (!handlers) return
1181
- if (!callback) { designerEvents.delete(evtName); return }
1182
- const index = handlers.indexOf(callback)
1183
- if (index > -1) handlers.splice(index, 1)
1184
- },
1185
-
1186
- // 快照存三段 JSON 字符串:无响应式、内存紧凑(此前每步 cloneDeep 三份全量数据且全部深度代理);
1187
- // toRaw 脱掉顶层代理,减少序列化时的 Proxy 开销
1188
- takeSnapshot () {
1189
- return {
1190
- widgetList: JSON.stringify(toRaw(this.widgetList)),
1191
- widgetConfig: JSON.stringify(toRaw(this.widgetConfig)),
1192
- pageConfig: JSON.stringify(toRaw(this.pageConfig))
1193
- }
1194
- },
1195
- applySnapshot (snapshot) {
1196
- this.widgetList = JSON.parse(snapshot.widgetList)
1197
- this.widgetConfig = JSON.parse(snapshot.widgetConfig)
1198
- this.pageConfig = JSON.parse(snapshot.pageConfig)
1199
- },
1200
- // 加载页面后重置为单步历史,首次操作即可撤销回初始状态
1201
- initHistoryData () {
1202
- this.historyData.index = 0
1203
- this.historyData.steps = [this.takeSnapshot()]
1204
- },
1205
-
1206
- emitHistoryChange () {
1207
- if (this.historyData.index === this.historyData.maxStep - 1) {
1208
- this.historyData.steps.shift()
1209
- } else {
1210
- this.historyData.index++
1211
- }
1212
- this.historyData.steps[this.historyData.index] = this.takeSnapshot()
1213
- this.savePageContentToStorage()
1214
- if (this.historyData.index < this.historyData.steps.length - 1) {
1215
- this.historyData.steps = this.historyData.steps.slice(0, this.historyData.index + 1)
1216
- }
1217
- },
1218
-
1219
- saveCurrentHistoryStep () {
1220
- this.historyData.steps[this.historyData.index] = this.takeSnapshot()
1221
- this.savePageContentToStorage()
1222
- },
1223
-
1224
- undoHistoryStep () {
1225
- if (this.historyData.index !== 0) {
1226
- this.historyData.index--
1227
- }
1228
- this.applySnapshot(this.historyData.steps[this.historyData.index])
1229
- },
1230
-
1231
- redoHistoryStep () {
1232
- if (this.historyData.index !== (this.historyData.steps.length - 1)) {
1233
- this.historyData.index++
1234
- }
1235
- this.applySnapshot(this.historyData.steps[this.historyData.index])
1236
- },
1237
-
1238
- undoEnabled () {
1239
- return (this.historyData.index > 0) && (this.historyData.steps.length > 0)
1240
- },
1241
-
1242
- redoEnabled () {
1243
- return this.historyData.index < (this.historyData.steps.length - 1)
1244
- },
1245
-
1246
- // 300ms 防抖:连续操作只写一次 localStorage;直接复用快照字符串,不再重复 stringify
1247
- savePageContentToStorage () {
1248
- if (saveTimer) clearTimeout(saveTimer)
1249
- saveTimer = setTimeout(() => {
1250
- saveTimer = null
1251
- const snapshot = this.historyData.steps[this.historyData.index] || this.takeSnapshot()
1252
- storage.set('widget__list__backup', snapshot.widgetList)
1253
- storage.set('widget__config__backup', snapshot.widgetConfig)
1254
- storage.set('page__config__backup', snapshot.pageConfig)
1255
- }, 300)
1256
- },
1257
-
1258
- loadPageContentFromStorage () {
1259
- const widgetListBackup = storage.get('widget__list__backup')
1260
- if (widgetListBackup) {
1261
- this.widgetList = JSON.parse(widgetListBackup)
1262
- }
1263
- const widgetConfigBackup = storage.get('widget__config__backup')
1264
- if (widgetConfigBackup) {
1265
- Object.assign(this.widgetConfig, JSON.parse(widgetConfigBackup)) /* 用=赋值,会导致inject依赖注入的formConfig属性变成非响应式 */
1266
- }
1267
- const pageConfigBackup = storage.get('page__config__backup')
1268
- if (pageConfigBackup) {
1269
- Object.assign(this.pageConfig, JSON.parse(pageConfigBackup)) /* 用=赋值,会导致inject依赖注入的formConfig属性变成非响应式 */
1270
- }
1271
- },
1272
-
1273
- isExistId (id) {
1274
- return lookup(id) !== undefined
1275
- },
1276
-
1277
- // 停掉索引 watch 与待写入的 storage 定时器;PageDesigner.beforeUnmount 调用
1278
- dispose () {
1279
- scope.stop()
1280
- if (saveTimer) {
1281
- clearTimeout(saveTimer)
1282
- saveTimer = null
1283
- }
1284
- },
1285
-
1286
- genId (widgetCode) {
1287
- const code = widgetCode.replace(/-/g, '_')
1288
- let id = code + '_' + util.genId()
1289
- while (this.isExistId(id)) {
1290
- id = code + '_' + util.genId()
1291
- }
1292
- return id
1293
- }
1294
- })
1295
- scope.run(() => {
1296
- watch(() => state.widgetList, () => { indexDirty = true }, { deep: true, flush: 'sync' })
1297
- })
1298
- return state
1299
- }
1
+ import { isEmpty, cloneDeep } from 'lodash-es'
2
+ import { reactive, markRaw, toRaw, watch, effectScope } from 'vue'
3
+ import util from '@epxing/core/src/runtime-utils/util'
4
+ import storage from '@epxing/utils/src/storage'
5
+ const { genId } = util
6
+ const defaultPageConfig = {}
7
+
8
+ /**
9
+ * 获取组件上的插槽组件列表
10
+ * 插槽结构:widget.slots = { [插槽名]: 组件 },如折叠项的 { header: {...} }
11
+ * 运行时由各组件自己渲染(见 packages/collapse/Collapse.vue),
12
+ * 设计器这边统一按这个结构识别,保证大纲、删除、复制、拖拽等链路都能覆盖到插槽里的组件。
13
+ * @param { Object } widget 组件
14
+ * @returns { Array<{ name: String, widget: Object }> }
15
+ */
16
+ export function getWidgetSlots (widget) {
17
+ const { slots } = widget || {}
18
+ if (!slots || typeof slots !== 'object') return []
19
+ return Object.keys(slots)
20
+ .filter(name => slots[name] && slots[name].id)
21
+ .map(name => ({ name, widget: slots[name] }))
22
+ }
23
+
24
+ /**
25
+ * 获取组件的全部子组件(items + 插槽),用于需要遍历整棵树的场景
26
+ * @param { Object } widget 组件
27
+ * @returns { Array<Object> }
28
+ */
29
+ export function getWidgetChildren (widget) {
30
+ const { items = [] } = widget || {}
31
+ return [...items, ...getWidgetSlots(widget).map(slot => slot.widget)]
32
+ }
33
+ export function createDesigner (vueInstance) {
34
+ // T25:设计器事件枢纽用内置 emitter,替代"组件实例当事件总线"(Vue3 无实例事件 API)
35
+ const designerEvents = new Map()
36
+ // 组件库 / 模板库索引:CODE → 组件信息、ID → 模板。库是只读数据,markRaw 后不再被代理与依赖收集;
37
+ // 此前每个画布组件的 widgetInfo 都对整个库的每个元素建依赖(N 组件 × M 库项)
38
+ const widgetInfoIndex = new Map()
39
+ const templateIndex = new Map()
40
+ // 组件树索引:id → { widget, parent, slotName }(存原始对象)。任何深层改动只标脏,下次查询时重建一次。
41
+ // 用 deep watch 标脏而不是在改动入口标脏:外部直接 splice 的入口有 7 处(Tree/Item/Preview/Setting/Overlay),漏一处就错。
42
+ // 重建时走 toRaw 的原始树,不会让调用方所在的 effect 意外依赖整棵树;查询时再 reactive() 回代理返回。
43
+ let widgetIndex = new Map()
44
+ let indexDirty = true
45
+ const rebuildIndex = () => {
46
+ widgetIndex = new Map()
47
+ const walk = (list, parent, slotName) => {
48
+ for (const item of toRaw(list) || []) {
49
+ const raw = toRaw(item)
50
+ if (!raw || !raw.id) continue
51
+ // 重复 id 取深度优先的第一个,与原递归实现一致
52
+ if (!widgetIndex.has(raw.id)) widgetIndex.set(raw.id, { widget: raw, parent, slotName })
53
+ walk(raw.items, raw, '')
54
+ for (const slot of getWidgetSlots(raw)) walk([slot.widget], raw, slot.name)
55
+ }
56
+ }
57
+ walk(state.widgetList, null, '')
58
+ indexDirty = false
59
+ }
60
+ const lookup = id => {
61
+ if (indexDirty) rebuildIndex()
62
+ return widgetIndex.get(id)
63
+ }
64
+ const asReactive = obj => (obj ? reactive(obj) : obj)
65
+ let saveTimer = null
66
+ const scope = effectScope(true)
67
+ const state = reactive({
68
+ version: '2.0',
69
+ mode: 'page',
70
+ terminal: 'pc',
71
+ page: {}, // 页面
72
+ widgetList: [], // 组件列表
73
+ widgetConfig: {}, // 组件配置
74
+ templateLibrary: [], // 模板列表
75
+ templateAttributeTree: [], // 模板配置参数
76
+ pageConfig: {}, // 页面配置
77
+ allWidgets: [],
78
+ formConfig: { cssCode: '' },
79
+ selectedId: null,
80
+ selectedWidget: null,
81
+ selectedWidgetName: null, // 选中组件名称(唯一)
82
+ // 悬浮组件状态
83
+ hoverWidgetId: null,
84
+ hoverWidget: null,
85
+ hoverParentWidget: null,
86
+ // 拖拽时当前悬停的放置目标 { type: 'root' | 'container', widgetId: string | null }
87
+ dragOverTarget: null,
88
+ // 当前放置目标对应的 DOM 元素,用于独立浮层定位
89
+ dragOverTargetElement: null,
90
+ vueInstance: vueInstance,
91
+
92
+ formWidget: null, // 表单设计容器
93
+
94
+ cssClassList: [], // 自定义样式列表
95
+
96
+ historyData: {
97
+ index: -1, // index: 0,
98
+ maxStep: 20,
99
+ steps: []
100
+ },
101
+
102
+ initDesigner (resetFormJson) {
103
+ this.widgetList = []
104
+ this.widgetConfig = {}
105
+ this.pageConfig = cloneDeep(defaultPageConfig)
106
+ if (!resetFormJson) {
107
+ this.initHistoryData()
108
+ }
109
+ },
110
+
111
+ clearDesigner (skipHistoryChange) {
112
+ const emptyWidgetListFlag = (this.widgetList.length === 0)
113
+ this.widgetList = []
114
+ this.widgetConfig = {}
115
+ this.selectedId = null
116
+ this.selectedWidget = {} // this.selectedWidget = null
117
+ this.pageConfig = cloneDeep(defaultPageConfig)
118
+
119
+ if (skipHistoryChange) {
120
+ // 什么也不做!!
121
+ } else if (!emptyWidgetListFlag) {
122
+ this.emitHistoryChange()
123
+ } else {
124
+ this.saveCurrentHistoryStep()
125
+ }
126
+ },
127
+ initWidgetList (componentList) {
128
+ this.allWidgets = markRaw([...componentList])
129
+ widgetInfoIndex.clear()
130
+ for (const con of this.allWidgets) {
131
+ if (con && con.CODE) widgetInfoIndex.set(con.CODE, con)
132
+ }
133
+ },
134
+ initTemplateLibrary (templateLibrary) {
135
+ this.templateLibrary = markRaw(templateLibrary || [])
136
+ templateIndex.clear()
137
+ for (const tpl of this.templateLibrary) {
138
+ if (tpl && tpl.ID) templateIndex.set(tpl.ID, tpl)
139
+ }
140
+ },
141
+ setTemplateAttributeTree (data) {
142
+ this.templateAttributeTree = data
143
+ },
144
+ loadFormJson (page) {
145
+ this.page = page
146
+ const { isComponent, isTemplate, config: formJson = {} } = page
147
+ if (isComponent) this.mode = 'component'
148
+ if (isTemplate) this.mode = 'template'
149
+ let modifiedFlag = false
150
+ if (formJson) {
151
+ const { items: widgetList = [], itemConfigs: widgetConfig = {}, ...pageConfig } = formJson
152
+
153
+ if (widgetList) {
154
+ // this.formWidget.clearWidgetRefList()
155
+ this.widgetList = widgetList
156
+ this.widgetConfig = widgetConfig
157
+ modifiedFlag = true
158
+ }
159
+ if (pageConfig) {
160
+ this.pageConfig = { ...pageConfig }
161
+ modifiedFlag = true
162
+ }
163
+ if (pageConfig.version !== this.version) {
164
+ this.upgradePageConfig()
165
+ this.upgradeWidgetConfig()
166
+ }
167
+ }
168
+ // 加载完成即存一步初始快照,首次操作后就能撤销回初始状态(此前无人调用 initHistoryData,回不去)
169
+ this.initHistoryData()
170
+ if (modifiedFlag) {
171
+ this.emitEvent('form-json-imported', []) // 通知其他组件
172
+ }
173
+ return modifiedFlag
174
+ },
175
+
176
+ setSelected (selected) {
177
+ if (!selected) {
178
+ this.clearSelected()
179
+ return
180
+ }
181
+
182
+ this.selectedWidget = selected
183
+ if (selected.id) {
184
+ this.selectedId = selected.id
185
+ }
186
+ },
187
+
188
+ // 根据组件 id 设置悬浮组件
189
+ setHoverById (id) {
190
+ if (!id) {
191
+ this.clearHover()
192
+ return
193
+ }
194
+
195
+ const widget = this.getWidgetItemById(id)
196
+ if (!widget) {
197
+ this.clearHover()
198
+ return
199
+ }
200
+
201
+ // 查找父组件
202
+ const parentWidget = this.findParentWidget(id)
203
+
204
+ this.hoverWidgetId = id
205
+ this.hoverWidget = widget
206
+ this.hoverParentWidget = parentWidget || null
207
+
208
+ // 通知外部悬浮组件发生变化
209
+ this.emitEvent('hover-change', {
210
+ widget,
211
+ parentWidget: this.hoverParentWidget
212
+ })
213
+ },
214
+
215
+ // 清理悬浮组件状态
216
+ clearHover () {
217
+ this.hoverWidgetId = null
218
+ this.hoverWidget = null
219
+ this.hoverParentWidget = null
220
+ this.emitEvent('hover-change', {
221
+ widget: null,
222
+ parentWidget: null
223
+ })
224
+ },
225
+
226
+ setDragOverTarget (target) {
227
+ this.dragOverTarget = target
228
+ },
229
+ setDragOverTargetElement (el) {
230
+ this.dragOverTargetElement = el
231
+ },
232
+ clearDragOverTarget () {
233
+ this.dragOverTarget = null
234
+ this.dragOverTargetElement = null
235
+ },
236
+
237
+ // 拖拽插入位置线 { top, left, width } 视口坐标,用于 fixed 定位
238
+ dragInsertLine: null,
239
+ setDragInsertLine (rect) {
240
+ this.dragInsertLine = rect
241
+ },
242
+ clearDragInsertLine () {
243
+ this.dragInsertLine = null
244
+ },
245
+
246
+ // 预览区域拖拽状态(用于同步到大纲树)
247
+ previewDragTarget: null, // { widgetId, index, position }
248
+ previewDragTargetThrottle: null, // 节流定时器
249
+ setPreviewDragTarget (target) {
250
+ // 防抖优化:只在目标真正改变时才更新
251
+ const current = this.previewDragTarget
252
+ const isSame = current && target &&
253
+ current.widgetId === target.widgetId &&
254
+ current.index === target.index &&
255
+ current.position === target.position
256
+
257
+ if (isSame) return
258
+
259
+ // 使用节流,避免频繁更新
260
+ if (this.previewDragTargetThrottle) {
261
+ clearTimeout(this.previewDragTargetThrottle)
262
+ }
263
+
264
+ this.previewDragTargetThrottle = setTimeout(() => {
265
+ this.previewDragTarget = target
266
+ this.previewDragTargetThrottle = null
267
+ }, 16) // 约 60fps
268
+ },
269
+ clearPreviewDragTarget () {
270
+ if (this.previewDragTargetThrottle) {
271
+ clearTimeout(this.previewDragTargetThrottle)
272
+ this.previewDragTargetThrottle = null
273
+ }
274
+ this.previewDragTarget = null
275
+ },
276
+
277
+ // 统一的拖拽状态管理
278
+ dragState: {
279
+ isDragging: false, // 是否正在拖拽
280
+ isNativeDragging: false, // 是否是原生 HTML5 拖拽
281
+ dragSource: null, // 拖拽来源:'library' | 'preview' | 'template' | 'tree'
282
+ dragData: null, // 拖拽数据
283
+ ghostVisible: false, // 拖拽悬浮窗是否可见
284
+ ghostTitle: '', // 悬浮窗标题
285
+ ghostIcon: '', // 悬浮窗图标
286
+ ghostPosition: { x: 0, y: 0 } // 悬浮窗位置
287
+ },
288
+
289
+ // 开始拖拽
290
+ startDrag (options = {}) {
291
+ const { source, data, title, icon, x, y, isNative = false } = options
292
+ this.dragState.isDragging = true
293
+ this.dragState.isNativeDragging = isNative
294
+ this.dragState.dragSource = source
295
+ this.dragState.dragData = data
296
+ this.dragState.ghostVisible = true
297
+ this.dragState.ghostTitle = title || ''
298
+ this.dragState.ghostIcon = icon || ''
299
+ this.dragState.ghostPosition = { x: x || 0, y: y || 0 }
300
+
301
+ // 如果是从预览区域拖拽,自动关闭左侧面板
302
+ if (source === 'preview' && this.vueInstance) {
303
+ this.emitEvent('close-left-panel')
304
+ }
305
+
306
+ // 触发拖拽开始事件
307
+ this.emitEvent('drag-state-change', { ...this.dragState })
308
+ },
309
+
310
+ // 更新拖拽悬浮窗位置
311
+ updateDragGhostPosition (x, y) {
312
+ if (this.dragState.isDragging) {
313
+ this.dragState.ghostPosition = { x, y }
314
+ this.emitEvent('drag-ghost-move', { x, y })
315
+ }
316
+ },
317
+
318
+ // 结束拖拽
319
+ endDrag () {
320
+ this.dragState.isDragging = false
321
+ this.dragState.isNativeDragging = false
322
+ this.dragState.dragSource = null
323
+ this.dragState.dragData = null
324
+ this.dragState.ghostVisible = false
325
+ this.dragState.ghostTitle = ''
326
+ this.dragState.ghostIcon = ''
327
+ this.dragState.ghostPosition = { x: 0, y: 0 }
328
+
329
+ // 清理相关状态
330
+ this.clearDragOverTarget()
331
+ this.clearDragInsertLine()
332
+ this.clearPreviewDragTarget()
333
+
334
+ // 触发拖拽结束事件
335
+ this.emitEvent('drag-state-change', { ...this.dragState })
336
+ },
337
+
338
+ // 获取当前拖拽状态
339
+ getDragState () {
340
+ return { ...this.dragState }
341
+ },
342
+
343
+ // ========== 统一的拖拽处理方法 ==========
344
+
345
+ /**
346
+ * 解析拖拽数据
347
+ * @param {DataTransfer} dataTransfer - 拖拽事件的 dataTransfer 对象
348
+ * @returns {Object|null} 解析后的拖拽数据 { type, payload }
349
+ */
350
+ parseDragData (dataTransfer) {
351
+ if (!dataTransfer || !dataTransfer.types || !dataTransfer.types.includes('text/plain')) {
352
+ return null
353
+ }
354
+
355
+ try {
356
+ const raw = dataTransfer.getData('text/plain')
357
+ if (!raw) return null
358
+
359
+ // 组件库拖拽
360
+ if (raw.startsWith('page-designer-widget:')) {
361
+ const payload = JSON.parse(raw.slice('page-designer-widget:'.length))
362
+ return { type: 'library', payload }
363
+ }
364
+
365
+ // 模板库拖拽
366
+ if (raw.startsWith('page-designer-template:')) {
367
+ const payload = JSON.parse(raw.slice('page-designer-template:'.length))
368
+ return { type: 'template', payload }
369
+ }
370
+
371
+ // 组件重排序拖拽(预览区或大纲树)
372
+ const data = JSON.parse(raw)
373
+ if (data && data.type === 'widget-reorder') {
374
+ return { type: 'reorder', payload: data }
375
+ }
376
+
377
+ return null
378
+ } catch (e) {
379
+ return null
380
+ }
381
+ },
382
+
383
+ /**
384
+ * 处理拖拽放置
385
+ * @param {Object} options - 放置选项
386
+ * @param {DataTransfer} options.dataTransfer - 拖拽数据
387
+ * @param {Object} options.target - 放置目标 { type: 'root'|'container'|'slot', widgetId, index, slotName }
388
+ * @returns {boolean} 是否成功处理
389
+ */
390
+ handleDrop (options = {}) {
391
+ const { dataTransfer, target } = options
392
+ if (!dataTransfer || !target) {
393
+ return false
394
+ }
395
+
396
+ const dragData = this.parseDragData(dataTransfer)
397
+
398
+ if (!dragData) {
399
+ return false
400
+ }
401
+
402
+ const { type, payload } = dragData
403
+
404
+ // 根据拖拽类型分发处理
405
+ switch (type) {
406
+ case 'library':
407
+ return this.handleLibraryDrop(payload, target)
408
+ case 'template':
409
+ return this.handleTemplateDrop(payload, target)
410
+ case 'reorder':
411
+ return this.handleReorderDrop(payload, target)
412
+ default:
413
+ return false
414
+ }
415
+ },
416
+
417
+ /**
418
+ * 处理组件库拖拽放置
419
+ * @param {Object} payload - 组件数据 { code, title, custom }
420
+ * @param {Object} target - 放置目标
421
+ */
422
+ handleLibraryDrop (payload, target) {
423
+ const { code, title, custom } = payload
424
+ if (!code) return false
425
+
426
+ const widgetInfo = this.getWidgetByType(code) || {}
427
+ const widget = {
428
+ id: this.genId(code),
429
+ contentType: code,
430
+ title: title || widgetInfo.TITLE
431
+ }
432
+ if (custom) widget.type = 'custom'
433
+
434
+ // 特殊处理:chart 组件需要设置 metaId
435
+ if (widgetInfo.CODE === 'chart') {
436
+ this.updateWidgetConfig(widget.id, { metaId: this.page.metaId })
437
+ }
438
+
439
+ // 插入组件
440
+ this.insertWidget(widget, target)
441
+ this.setSelected(widget)
442
+ this.emitHistoryChange()
443
+ return true
444
+ },
445
+
446
+ /**
447
+ * 处理模板库拖拽放置
448
+ * @param {Object} payload - 模板数据 { items, itemConfigs, templateJs }
449
+ * @param {Object} target - 放置目标
450
+ */
451
+ handleTemplateDrop (payload, target) {
452
+ const { items, itemConfigs, templateJs } = payload
453
+ if (!items || !items.length) return false
454
+
455
+ // 插槽只能放一个组件,多根模板拒绝放入
456
+ if (target.type === 'slot' && items.length > 1) {
457
+ this.vueInstance && this.vueInstance.$message && this.vueInstance.$message('插槽只能放置一个组件')
458
+ return false
459
+ }
460
+
461
+ // 插入模板组件
462
+ this.insertWidgets(items, target)
463
+
464
+ // 更新组件配置
465
+ this.mergeWidgetConfig(itemConfigs)
466
+
467
+ // 处理模板 JS
468
+ if (templateJs) {
469
+ this.mergeTemplateJs(templateJs)
470
+ }
471
+
472
+ this.setSelected(items[0])
473
+ this.emitHistoryChange()
474
+ return true
475
+ },
476
+
477
+ /**
478
+ * 处理组件重排序拖拽放置
479
+ * @param {Object} payload - 拖拽数据 { widgetId, parentWidgetId }
480
+ * @param {Object} target - 放置目标
481
+ */
482
+ handleReorderDrop (payload, target) {
483
+ const { widgetId, parentWidgetId } = payload
484
+ if (!widgetId) {
485
+ return false
486
+ }
487
+
488
+ const sourceWidget = this.getWidgetItemById(widgetId)
489
+ if (!sourceWidget) {
490
+ return false
491
+ }
492
+
493
+ // 标准化 ID 比较(null 和 undefined 都视为相同)
494
+ const sourceParentId = parentWidgetId || null
495
+ const targetParentId = target.widgetId || null
496
+
497
+ // 拖进插槽:不能拖自己或自己的子孙进来,否则组件树会自己套自己
498
+ if (target.type === 'slot') {
499
+ if (this.isDescendantWidget(target.widgetId, sourceWidget)) return false
500
+ this.removeWidgetFromParent(sourceWidget)
501
+ if (!this.insertWidget(sourceWidget, target)) return false
502
+ this.setSelected(sourceWidget)
503
+ this.emitHistoryChange()
504
+ return true
505
+ }
506
+
507
+ // 从插槽拖出来:先摘掉插槽再插入目标位置(即使目标就是插槽宿主本身)
508
+ if (this.findWidgetSlot(widgetId)) {
509
+ return this.moveWidgetToAnotherContainer(sourceWidget, sourceParentId, target)
510
+ }
511
+
512
+ // 同容器内移动
513
+ if (sourceParentId === targetParentId) {
514
+ return this.moveWidgetInSameContainer(sourceWidget, target)
515
+ }
516
+
517
+ // 跨容器移动
518
+ return this.moveWidgetToAnotherContainer(sourceWidget, sourceParentId, target)
519
+ },
520
+
521
+ /**
522
+ * 在同一容器内移动组件
523
+ * @param {Object} widget - 要移动的组件
524
+ * @param {Object} target - 目标位置
525
+ */
526
+ moveWidgetInSameContainer (widget, target) {
527
+ const list = target.widgetId ? this.getWidgetItemById(target.widgetId).items : this.widgetList
528
+ if (!list) {
529
+ return false
530
+ }
531
+
532
+ const sourceIndex = list.findIndex(w => w.id === widget.id)
533
+
534
+ if (sourceIndex === -1) {
535
+ return false
536
+ }
537
+
538
+ let targetIndex = target.index
539
+
540
+ // 关键修正:target.index 是基于过滤掉拖拽元素后的列表计算的
541
+ // 需要调整为完整列表中的索引
542
+ if (targetIndex > sourceIndex) {
543
+ // 如果目标位置在源位置之后,需要 +1(因为 target.index 没有计算源元素)
544
+ targetIndex = targetIndex + 1
545
+ }
546
+
547
+ // 位置没变,不需要移动
548
+ if (sourceIndex === targetIndex || sourceIndex + 1 === targetIndex) {
549
+ return false
550
+ }
551
+
552
+ // 移除原位置
553
+ list.splice(sourceIndex, 1)
554
+
555
+ // 调整目标索引(移除后列表变短了)
556
+ if (targetIndex > sourceIndex) {
557
+ targetIndex = targetIndex - 1
558
+ }
559
+
560
+ // 插入新位置
561
+ list.splice(targetIndex, 0, widget)
562
+
563
+ // 更新容器
564
+ if (target.widgetId) {
565
+ this.updateWidget(target.widgetId, { items: [...list] })
566
+ } else {
567
+ this.widgetList = [...list]
568
+ }
569
+
570
+ this.setSelected(widget)
571
+ this.emitHistoryChange()
572
+ return true
573
+ },
574
+
575
+ /**
576
+ * 跨容器移动组件
577
+ * @param {Object} widget - 要移动的组件
578
+ * @param {string} sourceParentId - 源容器 ID
579
+ * @param {Object} target - 目标位置
580
+ */
581
+ moveWidgetToAnotherContainer (widget, sourceParentId, target) {
582
+ const sourceSlot = this.findWidgetSlot(widget.id)
583
+ // 从源插槽删除
584
+ if (sourceSlot) {
585
+ this.clearWidgetSlot(sourceSlot.parent, sourceSlot.slotName)
586
+ } else if (sourceParentId) {
587
+ // 从源容器删除
588
+ const sourceParent = this.getWidgetItemById(sourceParentId)
589
+ if (sourceParent && sourceParent.items) {
590
+ const sourceList = [...sourceParent.items]
591
+ const sourceIndex = sourceList.findIndex(w => w.id === widget.id)
592
+ if (sourceIndex !== -1) {
593
+ sourceList.splice(sourceIndex, 1)
594
+ this.updateWidget(sourceParent.id, { items: sourceList })
595
+ }
596
+ }
597
+ } else {
598
+ // 从根级删除
599
+ const rootList = [...this.widgetList]
600
+ const sourceIndex = rootList.findIndex(w => w.id === widget.id)
601
+ if (sourceIndex !== -1) {
602
+ rootList.splice(sourceIndex, 1)
603
+ this.widgetList = rootList
604
+ }
605
+ }
606
+
607
+ // 插入到目标容器
608
+ this.insertWidget(widget, target)
609
+ this.setSelected(widget)
610
+ this.emitHistoryChange()
611
+ return true
612
+ },
613
+
614
+ /**
615
+ * 插入单个组件到指定位置
616
+ * @param {Object} widget - 要插入的组件
617
+ * @param {Object} target - 目标位置 { type: 'root'|'container'|'slot', widgetId, index, slotName }
618
+ */
619
+ insertWidget (widget, target) {
620
+ // 放进插槽:一个插槽只放一个组件,已有组件时直接替换
621
+ if (target.type === 'slot') {
622
+ return this.setWidgetSlot(target.widgetId, target.slotName, widget)
623
+ }
624
+ if (target.type === 'root' || !target.widgetId) {
625
+ // 插入到根级
626
+ const list = [...this.widgetList]
627
+ list.splice(target.index, 0, widget)
628
+ this.widgetList = list
629
+ } else {
630
+ // 插入到容器内
631
+ const container = this.getWidgetItemById(target.widgetId)
632
+ if (!container) return false
633
+
634
+ // 确保容器有响应式的 items 数组
635
+ if (!container.items) {
636
+ this.updateWidget(container.id, { items: [] })
637
+ }
638
+
639
+ const list = [...(container.items || [])]
640
+ list.splice(target.index, 0, widget)
641
+ this.updateWidget(container.id, { items: list })
642
+ }
643
+ return true
644
+ },
645
+
646
+ /**
647
+ * 插入多个组件到指定位置
648
+ * @param {Array} widgets - 要插入的组件数组
649
+ * @param {Object} target - 目标位置
650
+ */
651
+ insertWidgets (widgets, target) {
652
+ if (!widgets || !widgets.length) return false
653
+
654
+ if (target.type === 'slot') {
655
+ return this.insertWidget(widgets[0], target)
656
+ }
657
+ if (target.type === 'root' || !target.widgetId) {
658
+ // 插入到根级
659
+ const list = [...this.widgetList]
660
+ list.splice(target.index, 0, ...widgets)
661
+ this.widgetList = list
662
+ } else {
663
+ // 插入到容器内
664
+ const container = this.getWidgetItemById(target.widgetId)
665
+ if (!container) return false
666
+
667
+ // 确保容器有响应式的 items 数组
668
+ if (!container.items) {
669
+ this.updateWidget(container.id, { items: [] })
670
+ }
671
+
672
+ const list = [...(container.items || [])]
673
+ list.splice(target.index, 0, ...widgets)
674
+ this.updateWidget(container.id, { items: list })
675
+ }
676
+ return true
677
+ },
678
+
679
+ /**
680
+ * 合并模板 JS 到页面 JS
681
+ * @param {string} templateJs - 模板的 JS 代码
682
+ */
683
+ mergeTemplateJs (templateJs) {
684
+ let { JS = '' } = this.pageConfig
685
+ const result = JS.match(/export\sfunction\s(\w+)/g)
686
+ const events = []
687
+ if (result) {
688
+ result.forEach(item => {
689
+ events.push(item.replace(/export\sfunction\s/, ''))
690
+ })
691
+ }
692
+ const templateJsResult = templateJs.split(/export\s+function\s+/)
693
+ templateJsResult.forEach(item => {
694
+ const funtionStr = item.replace(/\/\*(\n|.)+?\*\/[\n\s]+$/, '')
695
+ if (funtionStr.replace(/[\n\s]+/g, '').length > 0) {
696
+ const funtionName = funtionStr.split(/\s+\(/)[0]
697
+ if (![...new Set(events)].includes(funtionName)) {
698
+ JS += 'export function ' + funtionStr.replace(/^\w+/, funtionName)
699
+ }
700
+ }
701
+ })
702
+ this.updatePageConfig({ JS })
703
+ },
704
+
705
+ updateSelectedWidgetNameAndLabel (selectedWidget, newName, newLabel) {
706
+ this.selectedWidgetName = newName
707
+ if (!!newLabel && (Object.keys(selectedWidget.options).indexOf('label') > -1)) {
708
+ selectedWidget.options.label = newLabel
709
+ }
710
+ },
711
+
712
+ clearSelected () {
713
+ this.selectedId = null
714
+ this.selectedWidgetName = null
715
+ this.selectedWidget = null
716
+ },
717
+
718
+ // 检查组件是否可以移动(只有字段组件可以拖入子表单)
719
+ checkWidgetMove (evt) {
720
+ if (evt.draggedContext?.element) {
721
+ const wgCategory = evt.draggedContext.element.category
722
+ if (evt.to?.className === 'sub-form-table' && wgCategory === 'container') {
723
+ return false
724
+ }
725
+ }
726
+ return true
727
+ },
728
+
729
+ // 检查字段组件是否可以移动
730
+ checkFieldMove (evt) {
731
+ if (evt.draggedContext?.element) {
732
+ const wgType = String(evt.draggedContext.element.type)
733
+ if (evt.to?.className === 'sub-form-table' && wgType === 'slot') {
734
+ return false
735
+ }
736
+ }
737
+ return true
738
+ },
739
+ // 走索引 O(1);多个同 CODE 取最后一个(与原 forEach 覆盖语义一致),未命中返回 null
740
+ getWidgetByType (typeName, type) {
741
+ if (type === 'template') return templateIndex.get(typeName) || null
742
+ return widgetInfoIndex.get(typeName) || null
743
+ },
744
+ getContainerByType (typeName) {
745
+ let foundCon = null
746
+ this.allWidgets.forEach(con => {
747
+ if (con.CONTAINER === '1' && !!con.type && (con.type === typeName)) {
748
+ foundCon = con
749
+ }
750
+ })
751
+
752
+ return foundCon
753
+ },
754
+
755
+ getFieldWidgetByType (typeName) {
756
+ let foundWidget = null
757
+ this.allWidgets.forEach(widget => {
758
+ if (widget.CONTAINER !== '1' && !!widget.type && (widget.type === typeName)) {
759
+ foundWidget = widget
760
+ }
761
+ })
762
+
763
+ return foundWidget
764
+ },
765
+
766
+ hasConfig (widget, configName) {
767
+ let originalWidget = null
768
+ if (widget.CONTAINER === '1') {
769
+ originalWidget = this.getContainerByType(widget.type)
770
+ } else {
771
+ originalWidget = this.getFieldWidgetByType(widget.type)
772
+ }
773
+
774
+ if (!originalWidget || !originalWidget.options) {
775
+ return false
776
+ }
777
+
778
+ return Object.keys(originalWidget.options).indexOf(configName) > -1
779
+ },
780
+ // 走索引;未命中返回 undefined(与原递归实现一致)
781
+ getWidgetItemById (id) {
782
+ const entry = lookup(id)
783
+ return entry ? asReactive(entry.widget) : undefined
784
+ },
785
+
786
+ /**
787
+ * 查找组件所在的插槽位置
788
+ * @param { String } targetId 组件 id
789
+ * @returns { { parent: Object, slotName: String } | null } 不在插槽里返回 null
790
+ */
791
+ findWidgetSlot (targetId) {
792
+ const entry = lookup(targetId)
793
+ return entry && entry.slotName ? { parent: asReactive(entry.parent), slotName: entry.slotName } : null
794
+ },
795
+
796
+ /**
797
+ * 判断 targetId 是否在 widget 自身或其子孙(含插槽)里,
798
+ * 用于阻止把组件拖进自己的插槽造成死循环
799
+ */
800
+ isDescendantWidget (targetId, widget) {
801
+ if (!widget || !targetId) return false
802
+ if (widget.id === targetId) return true
803
+ return getWidgetChildren(widget).some(child => this.isDescendantWidget(targetId, child))
804
+ },
805
+
806
+ /**
807
+ * 往插槽里放组件(已有组件时替换)
808
+ */
809
+ setWidgetSlot (ownerId, slotName, widget) {
810
+ const owner = this.getWidgetItemById(ownerId)
811
+ if (!owner || !slotName) return false
812
+ this.updateWidget(ownerId, { slots: { ...(owner.slots || {}), [slotName]: widget } })
813
+ return true
814
+ },
815
+
816
+ /**
817
+ * 清空插槽(插槽全空时把 slots 一起删掉,避免配置里留空对象)
818
+ */
819
+ clearWidgetSlot (owner, slotName) {
820
+ if (!owner || !owner.slots) return false
821
+ const slots = { ...owner.slots }
822
+ delete slots[slotName]
823
+ this.updateWidget(owner.id, { slots: Object.keys(slots).length > 0 ? slots : null })
824
+ return true
825
+ },
826
+
827
+ /**
828
+ * 把组件从当前位置摘掉(插槽 / 容器 / 根级都支持),用于移动
829
+ */
830
+ removeWidgetFromParent (widget) {
831
+ const slotInfo = this.findWidgetSlot(widget.id)
832
+ if (slotInfo) {
833
+ return this.clearWidgetSlot(slotInfo.parent, slotInfo.slotName)
834
+ }
835
+ const parent = this.findParentWidget(widget.id)
836
+ if (parent && parent.items) {
837
+ const list = [...parent.items]
838
+ const index = list.findIndex(item => item.id === widget.id)
839
+ if (index === -1) return false
840
+ list.splice(index, 1)
841
+ this.updateWidget(parent.id, { items: list })
842
+ return true
843
+ }
844
+ const rootList = [...this.widgetList]
845
+ const index = rootList.findIndex(item => item.id === widget.id)
846
+ if (index === -1) return false
847
+ rootList.splice(index, 1)
848
+ this.widgetList = rootList
849
+ return true
850
+ },
851
+ updateWidget (key, property) {
852
+ const widget = this.getWidgetItemById(key)
853
+ if (widget) {
854
+ for (const key in property) {
855
+ if (property[key] === null) {
856
+ delete widget[key]
857
+ } else {
858
+ widget[key] = property[key]
859
+ }
860
+ }
861
+ }
862
+ },
863
+ updateWidgetConfig (key, configs, type) {
864
+ const property = {}
865
+ for (const configKey in configs) {
866
+ if (configKey.includes('componentName') || configKey === 'span' || configKey === 'flex') {
867
+ property[configKey.replace('componentName', 'title')] = configs[configKey]
868
+ }
869
+ }
870
+ this.updateWidget(key, property)
871
+ const entry = this.widgetConfig[key]
872
+ if (type === 'cover' || !entry) {
873
+ // 新建或整体覆盖:只换这一条 entry 的引用,其它组件的 $configs 不受影响
874
+ const next = { ...configs }
875
+ for (const k in next) {
876
+ if (next[k] === null) delete next[k]
877
+ }
878
+ this.widgetConfig[key] = next
879
+ return
880
+ }
881
+ // 原地按字段改:画布上只有这个组件的 $configs 会重算(此前整体替换 widgetConfig,全画布跟着重算)
882
+ for (const k in configs) {
883
+ if (configs[k] === null) delete entry[k]
884
+ else entry[k] = configs[k]
885
+ }
886
+ },
887
+ // 批量并入多条组件配置(模板拖入 / 复制 / 拆分模板),逐 key 新增而不整体替换 widgetConfig
888
+ mergeWidgetConfig (configs) {
889
+ for (const key in configs) {
890
+ if (configs[key] !== undefined) this.widgetConfig[key] = configs[key]
891
+ }
892
+ },
893
+ updatePageConfig (configs) {
894
+ this.pageConfig = { ...this.pageConfig, ...configs }
895
+ },
896
+ upgradeWidgetConfig () {
897
+ // 判断组件是否存在重复id,存在则重新生成,不存在则查找对应层级的配置,如果有则更新配置
898
+ const ids = []
899
+ const upgradeWidget = (arr, parentId) => {
900
+ arr.forEach(item => {
901
+ const oldId = item.id
902
+ if (parentId) {
903
+ while (ids.includes(item.id)) {
904
+ const code = item.contentType.replace(/-/g, '_')
905
+ item.id = code + '_' + util.genId()
906
+ }
907
+ ids.push(item.id)
908
+ item.i = item.id
909
+ if (!item.css && !isEmpty(item.style)) {
910
+ let cssStr = ':current {'
911
+ for (const key in item.style) {
912
+ const kebabCase = key.replace(/([A-Z])/g, $1 => {
913
+ return '-' + $1.toLocaleLowerCase()
914
+ })
915
+ // eslint-disable-next-line no-useless-escape
916
+ cssStr += `\r\n ${kebabCase}: ${item.style[key]};`
917
+ }
918
+ cssStr += '\r\n}'
919
+ // 更新css
920
+ item.css = cssStr
921
+ delete item.style
922
+ }
923
+ // 仅当旧版 key(parentId-id)确实存在配置时才迁移,否则会把新版 key 下的已有配置覆盖成空对象
924
+ const oldKey = parentId + '-' + oldId
925
+ const oldWidgetConifg = this.widgetConfig[oldKey]
926
+ if (oldWidgetConifg) {
927
+ delete this.widgetConfig[oldKey]
928
+ this.widgetConfig[item.id] = { ...this.widgetConfig[item.id], ...oldWidgetConifg }
929
+ } else if (item.id !== oldId && this.widgetConfig[oldId]) {
930
+ // id 因重复被重新生成时,把原 id 上的配置复制到新 id(原 id 仍被首个组件使用,不能删除)
931
+ this.widgetConfig[item.id] = { ...this.widgetConfig[oldId] }
932
+ }
933
+ }
934
+ const children = getWidgetChildren(item)
935
+ if (children.length > 0) {
936
+ upgradeWidget(children, parentId ? parentId + '-' + oldId : oldId)
937
+ }
938
+ })
939
+ }
940
+ upgradeWidget(this.widgetList)
941
+ },
942
+
943
+ upgradePageConfig () {
944
+ const { css, style } = this.pageConfig
945
+ if (!css && !isEmpty(style)) {
946
+ let cssStr = ':current {'
947
+ for (const key in style) {
948
+ const kebabCase = key.replace(/([A-Z])/g, $1 => {
949
+ return '-' + $1.toLocaleLowerCase()
950
+ })
951
+ // eslint-disable-next-line no-useless-escape
952
+ cssStr += `\r\n ${kebabCase}: ${style[key]};`
953
+ }
954
+ cssStr += '\r\n}'
955
+ // 更新css
956
+ delete this.pageConfig.style
957
+ this.pageConfig = { ...this.pageConfig, css: cssStr }
958
+ }
959
+ },
960
+
961
+ moveUpWidget (parentList, indexOfParentList) {
962
+ if (parentList) {
963
+ if (indexOfParentList === 0) {
964
+ this.vueInstance.$message(this.vueInstance.i18nt('designer.hint.moveUpFirstChildHint'))
965
+ return
966
+ }
967
+
968
+ const tempWidget = parentList[indexOfParentList]
969
+ parentList.splice(indexOfParentList, 1)
970
+ parentList.splice(indexOfParentList - 1, 0, tempWidget)
971
+ }
972
+ },
973
+
974
+ moveDownWidget (parentList, indexOfParentList) {
975
+ if (parentList) {
976
+ if (indexOfParentList === parentList.length - 1) {
977
+ this.vueInstance.$message(this.vueInstance.i18nt('designer.hint.moveDownLastChildHint'))
978
+ return
979
+ }
980
+
981
+ const tempWidget = parentList[indexOfParentList]
982
+ parentList.splice(indexOfParentList, 1)
983
+ parentList.splice(indexOfParentList + 1, 0, tempWidget)
984
+ }
985
+ },
986
+
987
+ copyNewFieldWidget (origin) {
988
+ const newWidget = cloneDeep(origin)
989
+ const tempId = genId()
990
+ newWidget.id = newWidget.type.replace(/-/g, '') + tempId
991
+ // console.log('test id===', newWidget.id)
992
+ newWidget.options.name = newWidget.id
993
+ newWidget.options.label = newWidget.options.label || newWidget.type.toLowerCase()
994
+
995
+ delete newWidget.displayName
996
+ return newWidget
997
+ },
998
+
999
+ copyNewContainerWidget (origin) {
1000
+ const newCon = genId(origin)
1001
+ newCon.id = newCon.type.replace(/-/g, '') + genId()
1002
+ newCon.options.name = newCon.id
1003
+
1004
+ const newTabPane = cloneDeep(this.getContainerByType('tab-pane'))
1005
+ newTabPane.id = 'tab-pane-' + genId()
1006
+ newTabPane.options.name = 'tab1'
1007
+ newTabPane.options.label = 'tab 1'
1008
+ newCon.tabs.push(newTabPane)
1009
+
1010
+ delete newCon.displayName
1011
+ return newCon
1012
+ },
1013
+ addWidgetByInfo (widgetInfo, parentWidget) {
1014
+ const widget = {
1015
+ id: widgetInfo.CODE + '_' + genId(),
1016
+ contentType: widgetInfo.CODE,
1017
+ title: widgetInfo.TITLE
1018
+ }
1019
+ if (widgetInfo.COMPONENT_SOURCE === 'custom') {
1020
+ widget.type = 'custom'
1021
+ }
1022
+ if (parentWidget) {
1023
+ if (!parentWidget.items) {
1024
+ parentWidget.items = [widget]
1025
+ } else {
1026
+ parentWidget.items.push(widget)
1027
+ }
1028
+ } else {
1029
+ this.widgetList.push(widget)
1030
+ }
1031
+ if (widgetInfo.CODE === 'chart') {
1032
+ this.updateWidgetConfig(widget.id, { metaId: this.page.metaId })
1033
+ }
1034
+ this.setSelected(widget)
1035
+ },
1036
+ addWidgets (widgets, parentWidget, option = {}) {
1037
+ const { defaultSelected = true } = option
1038
+ if (parentWidget) {
1039
+ if (!parentWidget.items) {
1040
+ parentWidget.items = [...widgets]
1041
+ } else {
1042
+ parentWidget.items.push(...widgets)
1043
+ }
1044
+ } else {
1045
+ this.widgetList.push(...widgets)
1046
+ }
1047
+ if (defaultSelected) this.setSelected(widgets[widgets.length - 1])
1048
+ },
1049
+ addWidget (widget, parentWidget, option = {}) {
1050
+ const { defaultSelected = true } = option
1051
+ if (parentWidget) {
1052
+ if (!parentWidget.items) {
1053
+ parentWidget.items = [widget]
1054
+ } else {
1055
+ parentWidget.items.push(widget)
1056
+ }
1057
+ } else {
1058
+ this.widgetList.push(widget)
1059
+ }
1060
+ if (defaultSelected) this.setSelected(widget)
1061
+ },
1062
+ // 查找父组件(公共方法):插槽里的组件返回插槽宿主;根级返回 null;不存在返回 undefined
1063
+ findParentWidget (targetId) {
1064
+ const entry = lookup(targetId)
1065
+ return entry ? asReactive(entry.parent) : undefined
1066
+ },
1067
+
1068
+ copyWidget (widget, parentWidget) {
1069
+ // 插槽是「一个插槽一个组件」,复制没有落点,直接提示
1070
+ if (this.findWidgetSlot(widget.id)) {
1071
+ this.vueInstance && this.vueInstance.$message && this.vueInstance.$message('插槽内的组件不支持复制')
1072
+ return
1073
+ }
1074
+ const newWidget = cloneDeep(widget)
1075
+ newWidget.id = widget.contentType + '_' + genId()
1076
+ // 配置要深拷贝:配置改为原地更新后,共享同一个对象会让复制件和原件互相串改
1077
+ const newConfigs = {
1078
+ [newWidget.id]: cloneDeep(this.widgetConfig[widget.id])
1079
+ }
1080
+ // 子组件(含插槽里的组件)都要换新 id,否则复制出来的组件和原组件共用配置
1081
+ const formatWidget = (arr) => {
1082
+ for (const item of arr) {
1083
+ const newId = item.contentType + '_' + genId()
1084
+ newConfigs[newId] = cloneDeep(this.widgetConfig[item.id])
1085
+ item.id = newId
1086
+ formatWidget(getWidgetChildren(item))
1087
+ }
1088
+ }
1089
+ formatWidget(getWidgetChildren(newWidget))
1090
+ // 添加组件配置
1091
+ if (parentWidget) {
1092
+ if (!parentWidget.items) {
1093
+ parentWidget.items = [newWidget]
1094
+ } else {
1095
+ parentWidget.items.push(newWidget)
1096
+ }
1097
+ } else {
1098
+ this.widgetList.push(newWidget)
1099
+ }
1100
+ this.mergeWidgetConfig(newConfigs)
1101
+ this.setSelected(newWidget)
1102
+ },
1103
+ deleteWidget (widget, parentWidget) {
1104
+ delete this.widgetConfig[widget.id]
1105
+ // 插槽里的组件:从 slots 上摘掉
1106
+ const slotInfo = this.findWidgetSlot(widget.id)
1107
+ if (slotInfo) {
1108
+ this.clearWidgetSlot(slotInfo.parent, slotInfo.slotName)
1109
+ if (this.selectedId === widget.id) this.clearSelected()
1110
+ return 0
1111
+ }
1112
+ if (parentWidget && parentWidget.items) {
1113
+ const index = parentWidget.items.findIndex(item => item.id === widget.id)
1114
+ if (index !== -1) {
1115
+ parentWidget.items.splice(index, 1)
1116
+ return index
1117
+ }
1118
+ } else {
1119
+ const index = this.widgetList.findIndex(item => item.id === widget.id)
1120
+ if (index !== -1) {
1121
+ this.widgetList.splice(index, 1)
1122
+ return index
1123
+ }
1124
+ }
1125
+ },
1126
+ /**
1127
+ * 切换组件类型:只换 contentType(及自定义组件的 type 标记),组件 id 不变,
1128
+ * 因此按 id 存的组件配置(itemConfigs)、子组件、样式等全部保留
1129
+ * @param { Object } widget 要切换的组件
1130
+ * @param { Object } widgetInfo 目标组件记录(组件库中的 FM_PAGE)
1131
+ */
1132
+ switchWidget (widget, widgetInfo) {
1133
+ const oldInfo = this.getWidgetByType(widget.contentType, widget.type) || {}
1134
+ const property = {
1135
+ contentType: widgetInfo.CODE,
1136
+ type: widgetInfo.COMPONENT_SOURCE === 'custom' ? 'custom' : null
1137
+ }
1138
+ // 名称仍是原组件默认名时跟随新组件,改过的名称保留
1139
+ if (!widget.title || widget.title === oldInfo.TITLE) {
1140
+ property.title = widgetInfo.TITLE
1141
+ }
1142
+ this.updateWidget(widget.id, property)
1143
+ // 与新增图表一致:图表组件默认取页面对象
1144
+ const { metaId } = this.widgetConfig[widget.id] || {}
1145
+ if (widgetInfo.CODE === 'chart' && !metaId) {
1146
+ this.updateWidgetConfig(widget.id, { metaId: this.page.metaId })
1147
+ }
1148
+ },
1149
+ addContainerByDbClick (container) {
1150
+ const newCon = this.copyNewContainerWidget(container)
1151
+ this.widgetList.push(newCon)
1152
+ this.setSelected(newCon)
1153
+ },
1154
+
1155
+ addFieldByDbClick (widget) {
1156
+ const newWidget = this.copyNewFieldWidget(widget)
1157
+ if (!!this.selectedWidget && this.selectedWidget.type === 'tab') {
1158
+ // 获取当前激活的tabPane
1159
+ let activeTab = this.selectedWidget.tabs[0]
1160
+ this.selectedWidget.tabs.forEach(tabPane => {
1161
+ if (tabPane.options.active) {
1162
+ activeTab = tabPane
1163
+ }
1164
+ })
1165
+
1166
+ !!activeTab && activeTab.widgetList.push(newWidget)
1167
+ } else if (!!this.selectedWidget && !!this.selectedWidget.widgetList) {
1168
+ this.selectedWidget.widgetList.push(newWidget)
1169
+ } else {
1170
+ this.widgetList.push(newWidget)
1171
+ }
1172
+
1173
+ this.setSelected(newWidget)
1174
+ this.emitHistoryChange()
1175
+ },
1176
+
1177
+ addTabPaneOfTabs (tabsWidget) {
1178
+ const tabPanes = tabsWidget.tabs
1179
+ const newTabPane = cloneDeep(this.getContainerByType('tab-pane'))
1180
+ newTabPane.id = 'tab-pane-' + genId()
1181
+ newTabPane.options.name = newTabPane.id
1182
+ newTabPane.options.label = 'tab ' + (tabPanes.length + 1)
1183
+ tabPanes.push(newTabPane)
1184
+ },
1185
+
1186
+ deleteTabPaneOfTabs (tabsWidget, tpIdx) {
1187
+ tabsWidget.tabs.splice(tpIdx, 1)
1188
+ },
1189
+
1190
+ emitEvent (evtName, evtData) { // 用于兄弟组件发射事件
1191
+ const handlers = designerEvents.get(evtName)
1192
+ if (handlers) handlers.slice().forEach(handler => handler(evtData))
1193
+ },
1194
+
1195
+ handleEvent (evtName, callback) { // 用于兄弟组件接收事件
1196
+ const handlers = designerEvents.get(evtName)
1197
+ if (handlers) handlers.push(callback)
1198
+ else designerEvents.set(evtName, [callback])
1199
+ },
1200
+
1201
+ offEvent (evtName, callback) { // 解除接收
1202
+ const handlers = designerEvents.get(evtName)
1203
+ if (!handlers) return
1204
+ if (!callback) { designerEvents.delete(evtName); return }
1205
+ const index = handlers.indexOf(callback)
1206
+ if (index > -1) handlers.splice(index, 1)
1207
+ },
1208
+
1209
+ // 快照存三段 JSON 字符串:无响应式、内存紧凑(此前每步 cloneDeep 三份全量数据且全部深度代理);
1210
+ // toRaw 脱掉顶层代理,减少序列化时的 Proxy 开销
1211
+ takeSnapshot () {
1212
+ return {
1213
+ widgetList: JSON.stringify(toRaw(this.widgetList)),
1214
+ widgetConfig: JSON.stringify(toRaw(this.widgetConfig)),
1215
+ pageConfig: JSON.stringify(toRaw(this.pageConfig))
1216
+ }
1217
+ },
1218
+ applySnapshot (snapshot) {
1219
+ this.widgetList = JSON.parse(snapshot.widgetList)
1220
+ this.widgetConfig = JSON.parse(snapshot.widgetConfig)
1221
+ this.pageConfig = JSON.parse(snapshot.pageConfig)
1222
+ },
1223
+ // 加载页面后重置为单步历史,首次操作即可撤销回初始状态
1224
+ initHistoryData () {
1225
+ this.historyData.index = 0
1226
+ this.historyData.steps = [this.takeSnapshot()]
1227
+ },
1228
+
1229
+ emitHistoryChange () {
1230
+ if (this.historyData.index === this.historyData.maxStep - 1) {
1231
+ this.historyData.steps.shift()
1232
+ } else {
1233
+ this.historyData.index++
1234
+ }
1235
+ this.historyData.steps[this.historyData.index] = this.takeSnapshot()
1236
+ this.savePageContentToStorage()
1237
+ if (this.historyData.index < this.historyData.steps.length - 1) {
1238
+ this.historyData.steps = this.historyData.steps.slice(0, this.historyData.index + 1)
1239
+ }
1240
+ },
1241
+
1242
+ saveCurrentHistoryStep () {
1243
+ this.historyData.steps[this.historyData.index] = this.takeSnapshot()
1244
+ this.savePageContentToStorage()
1245
+ },
1246
+
1247
+ undoHistoryStep () {
1248
+ if (this.historyData.index !== 0) {
1249
+ this.historyData.index--
1250
+ }
1251
+ this.applySnapshot(this.historyData.steps[this.historyData.index])
1252
+ },
1253
+
1254
+ redoHistoryStep () {
1255
+ if (this.historyData.index !== (this.historyData.steps.length - 1)) {
1256
+ this.historyData.index++
1257
+ }
1258
+ this.applySnapshot(this.historyData.steps[this.historyData.index])
1259
+ },
1260
+
1261
+ undoEnabled () {
1262
+ return (this.historyData.index > 0) && (this.historyData.steps.length > 0)
1263
+ },
1264
+
1265
+ redoEnabled () {
1266
+ return this.historyData.index < (this.historyData.steps.length - 1)
1267
+ },
1268
+
1269
+ // 300ms 防抖:连续操作只写一次 localStorage;直接复用快照字符串,不再重复 stringify
1270
+ savePageContentToStorage () {
1271
+ if (saveTimer) clearTimeout(saveTimer)
1272
+ saveTimer = setTimeout(() => {
1273
+ saveTimer = null
1274
+ const snapshot = this.historyData.steps[this.historyData.index] || this.takeSnapshot()
1275
+ storage.set('widget__list__backup', snapshot.widgetList)
1276
+ storage.set('widget__config__backup', snapshot.widgetConfig)
1277
+ storage.set('page__config__backup', snapshot.pageConfig)
1278
+ }, 300)
1279
+ },
1280
+
1281
+ loadPageContentFromStorage () {
1282
+ const widgetListBackup = storage.get('widget__list__backup')
1283
+ if (widgetListBackup) {
1284
+ this.widgetList = JSON.parse(widgetListBackup)
1285
+ }
1286
+ const widgetConfigBackup = storage.get('widget__config__backup')
1287
+ if (widgetConfigBackup) {
1288
+ Object.assign(this.widgetConfig, JSON.parse(widgetConfigBackup)) /* 用=赋值,会导致inject依赖注入的formConfig属性变成非响应式 */
1289
+ }
1290
+ const pageConfigBackup = storage.get('page__config__backup')
1291
+ if (pageConfigBackup) {
1292
+ Object.assign(this.pageConfig, JSON.parse(pageConfigBackup)) /* 用=赋值,会导致inject依赖注入的formConfig属性变成非响应式 */
1293
+ }
1294
+ },
1295
+
1296
+ isExistId (id) {
1297
+ return lookup(id) !== undefined
1298
+ },
1299
+
1300
+ // 停掉索引 watch 与待写入的 storage 定时器;PageDesigner.beforeUnmount 调用
1301
+ dispose () {
1302
+ scope.stop()
1303
+ if (saveTimer) {
1304
+ clearTimeout(saveTimer)
1305
+ saveTimer = null
1306
+ }
1307
+ },
1308
+
1309
+ genId (widgetCode) {
1310
+ const code = widgetCode.replace(/-/g, '_')
1311
+ let id = code + '_' + util.genId()
1312
+ while (this.isExistId(id)) {
1313
+ id = code + '_' + util.genId()
1314
+ }
1315
+ return id
1316
+ }
1317
+ })
1318
+ scope.run(() => {
1319
+ watch(() => state.widgetList, () => { indexDirty = true }, { deep: true, flush: 'sync' })
1320
+ })
1321
+ return state
1322
+ }