@epxing/designer 5.3.0-beta.2 → 5.3.0-beta.3
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/API.md +1 -1
- package/README.md +46 -46
- package/dist/src/page-designer/PageDesigner.js +11 -0
- package/dist/src/page-designer/designer.js +21 -0
- package/dist/src/page-designer/previews/add-form/Preview.js +1 -1
- package/dist/src/page-designer/previews/add-form/Preview.vue.js +19 -8
- package/dist/src/page-designer/setting-panel/Setting.js +46 -10
- package/dist/src/visual/config-page/ComponentLibraryDialog.js +8 -2
- package/dist/src/visual/config-page/DashboardMobile.js +1 -1
- package/dist/style.css +116 -112
- package/package.json +8 -8
- package/src/index.js +27 -27
- package/src/page-designer/PageDesigner.vue +11 -0
- package/src/page-designer/designer.js +1322 -1299
- package/src/page-designer/previews/add-form/Preview.vue +43 -44
- package/src/page-designer/setting-panel/Setting.vue +24 -2
- package/src/page-designer/widget-preview/Mixin.js +285 -285
- package/src/page-designer/widget-preview/Preview.vue +322 -322
- package/src/page-designer/widget-preview/WidgetWrapper.vue +197 -197
- package/src/page-designer/widget-preview/directive.js +258 -258
- package/src/visual/CategoryList.vue +563 -563
- package/src/visual/ComponentList.vue +280 -280
- package/src/visual/CreatePage.vue +389 -389
- package/src/visual/EntityCardList.vue +449 -449
- package/src/visual/EntityList.vue +838 -838
- package/src/visual/config-page/ComponentLibraryDialog.vue +7 -1
- package/src/visual/config-page/DashboardMobile.vue +934 -934
- package/src/visual/widgets/ProcessFormItemVisual.vue +58 -58
- package/types/index.d.ts +22 -22
- package/types/page-designer.d.ts +3 -3
- package/types/visual-component.d.ts +1 -1
- package/types/visual-entity-list.d.ts +1 -1
- 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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
this.
|
|
1151
|
-
this.
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
},
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
},
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
},
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
this.
|
|
1221
|
-
this.
|
|
1222
|
-
},
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
this.historyData.index++
|
|
1234
|
-
}
|
|
1235
|
-
this.
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
},
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
+
}
|