@epxing/dashboard 0.0.0-stage → 5.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/API.md +79 -0
  2. package/README.md +40 -2
  3. package/dist/_virtual/_plugin-vue_export-helper.js +10 -0
  4. package/dist/_virtual/preload-helper.js +88 -0
  5. package/dist/assets/technology_bg-oWIS2awO.jpg +0 -0
  6. package/dist/src/Dashboard.js +332 -0
  7. package/dist/src/DashboardGrid.js +182 -0
  8. package/dist/src/DesignGrid.js +711 -0
  9. package/dist/src/DesignItem.js +63 -0
  10. package/dist/src/Div.js +408 -0
  11. package/dist/src/Header.js +385 -0
  12. package/dist/src/Preview.js +27 -0
  13. package/dist/src/Tabs.js +468 -0
  14. package/dist/src/Title.js +101 -0
  15. package/dist/src/Widget.js +266 -0
  16. package/dist/src/chart-request-scope.js +32 -0
  17. package/dist/src/design-target.js +57 -0
  18. package/dist/src/grid/compact.js +68 -0
  19. package/dist/src/grid/compat/GridItem.js +35 -0
  20. package/dist/src/grid/compat/GridLayout.js +76 -0
  21. package/dist/src/grid/designState.js +21 -0
  22. package/dist/src/grid/engines.js +25 -0
  23. package/dist/src/grid/geometry.js +44 -0
  24. package/dist/src/grid/gridstackParams.js +69 -0
  25. package/dist/src/grid/mixins.js +108 -0
  26. package/dist/src/grid/model.js +223 -0
  27. package/dist/src/grid/useViewportGate.js +55 -0
  28. package/dist/src/index.js +26 -0
  29. package/dist/src/layout-util.js +108 -0
  30. package/dist/src/mobile-design.js +50 -0
  31. package/dist/src/theme-style.js +133 -0
  32. package/dist/style.css +1009 -0
  33. package/package.json +85 -3
  34. package/src/Dashboard.vue +459 -0
  35. package/src/DashboardGrid.vue +189 -0
  36. package/src/DesignGrid.vue +851 -0
  37. package/src/DesignItem.vue +124 -0
  38. package/src/Div.vue +419 -0
  39. package/src/Header.vue +501 -0
  40. package/src/Preview.vue +30 -0
  41. package/src/Tabs.vue +484 -0
  42. package/src/Title.vue +107 -0
  43. package/src/Widget.vue +354 -0
  44. package/src/chart-request-scope.js +22 -0
  45. package/src/design-target.js +55 -0
  46. package/src/grid/compact.js +81 -0
  47. package/src/grid/compat/GridItem.vue +30 -0
  48. package/src/grid/compat/GridLayout.vue +76 -0
  49. package/src/grid/designState.js +17 -0
  50. package/src/grid/engines.js +22 -0
  51. package/src/grid/geometry.js +55 -0
  52. package/src/grid/gridstackParams.js +72 -0
  53. package/src/grid/mixins.js +114 -0
  54. package/src/grid/model.js +259 -0
  55. package/src/grid/useViewportGate.js +57 -0
  56. package/src/index.js +28 -0
  57. package/src/layout-util.js +157 -0
  58. package/src/mobile-design.js +50 -0
  59. package/src/theme-style.js +185 -0
  60. package/types/div.d.ts +1 -0
  61. package/types/index.d.ts +7 -0
  62. package/types/tabs.d.ts +1 -0
  63. package/types/title.d.ts +1 -0
  64. package/types/widget.d.ts +1 -0
@@ -0,0 +1,851 @@
1
+ <template>
2
+ <!-- dragstart.prevent:画布内一律不走浏览器原生拖拽(选区 / 图片 / 链接)。gridstack 用的是 mouse 事件,原生拖拽一旦起来会抢走指针,
3
+ 库收不到 mousemove / mouseup 就卡在拖拽态(松手不归位、再点一下才恢复);配合样式里的 user-select: none 让选区根本形不成 -->
4
+ <div ref="host" :class="['epx-dashboard-design-grid', { 'epx-dashboard-design-grid--root': !subGridOf }]" :style="hostStyle" @dragstart.prevent>
5
+ <div ref="root" class="grid-stack epx-dashboard-design-grid__stack" :style="stackStyle" @mousedown="onMouseDown"></div>
6
+ <!-- 悬浮层:第二个 gridstack 实例(无碰撞、可叠放),与碰撞层同几何参数、绝对定位盖在其上;空白处不吃指针,只有壳吃。
7
+ 元素常驻,实例只在有悬浮项时才建(spec §9.4) -->
8
+ <div ref="floatRoot" class="grid-stack epx-dashboard-design-grid__float" :style="stackStyle" @mousedown="onMouseDown"></div>
9
+ <!-- 内容由 Vue 持有、Teleport 进库造的壳;壳还没造好(首帧)或已被库搬到别的网格时不渲染 -->
10
+ <template v-for="item in liveItems" :key="item.id">
11
+ <Teleport v-if="shells[item.id]" :to="shells[item.id]">
12
+ <slot name="item" :item="item"></slot>
13
+ <slot name="chrome" :item="item"></slot>
14
+ </Teleport>
15
+ </template>
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ // 看板网格(设计态):封装 gridstack 13。壳(.grid-stack-item)由库经 GridStack.addRemoveCB 创建,
21
+ // 内容由本组件用 <Teleport> 渲染进壳,库搬 DOM 时 Vue 不会去删元素(spec §9.0)。
22
+ // items 是唯一真值:库触发 change 时就地改 item.x/y/w/h(设计器外壳的 deep watch 据此自动保存);
23
+ // 外部改了坐标(属性面板)则同步到引擎。库实例挂 this.gridstack / this.floatstack(prop grid 是 v3 网格参数)、不进 data(响应式代理会破坏库内部)。
24
+ // 两层实例(spec §9.4):碰撞层 this.gridstack 渲染 float !== true 的项(PushOnlyEngine,推挤 + 紧凑),
25
+ // 悬浮层 this.floatstack 渲染 float === true 的项(NoCollideEngine + float,可叠放、按 z 定层级);grid.mode === 'float' 时只有悬浮层。
26
+ // 容器内的子网格是另一个本组件实例(:sub-grid-of 传父 api),其碰撞层 .grid-stack 由父实例 makeSubGrid 收编;悬浮层永远是独立实例。
27
+ import { markRaw } from 'vue'
28
+ import { GridStack, DDManager } from 'gridstack'
29
+ import 'gridstack/dist/gridstack.min.css'
30
+ import { normalizeGridParams, convertItemBetweenGrids, partitionItems } from './grid/model'
31
+ import { resolveGrid } from './grid/geometry'
32
+ import { normalizeGridItem } from './layout-util'
33
+ import { toGridstackOptions, rootStyle, toWidget, gridLineStyle } from './grid/gridstackParams'
34
+ import { PushOnlyEngine, NoCollideEngine } from './grid/engines'
35
+
36
+ // 子网格登记:父 makeSubGrid 前把「壳元素 → 子实例渲染的 .grid-stack」登记在此,
37
+ // 库建子网格时经 addRemoveCB(isGrid) 取回,收编 Vue 的元素而不是自己造一个
38
+ const pendingSubGrids = new Map()
39
+ // 无 id 项的兜底计数
40
+ let idSeq = 0
41
+ // 子网格里按在锁定项上时给所在层根元素打的标记,上级网格里容器项的 draggable.cancel 命中它就不起拖(锁定的是子项、别把整个容器拖走)
42
+ const PRESSED_CLASS = 'epx-dashboard-design-grid__stack--pressed'
43
+ // 悬浮层根元素的类:碰撞层的 acceptWidgets 据此拒收来自悬浮层的壳
44
+ const FLOAT_CLASS = 'epx-dashboard-design-grid__float'
45
+ // 拖拽进行中打在根宿主上的标记:期间所有悬浮壳不吃指针(见样式说明)
46
+ const DRAGGING_CLASS = 'epx-dashboard-design-grid--dragging'
47
+ // 拖拽结束的兜底事件(document 捕获阶段):权威来源是两层实例的 dragstop 与跨网格的 dropped,这里只兜库不发事件的路径
48
+ const DRAG_END_EVENTS = ['mouseup', 'touchend', 'touchcancel']
49
+ // 碰到「还没进入的容器」时临时打开的落位停顿(ms,写进库的全局开关 DDManager.pauseDrag,每次 mousemove 都会读):
50
+ // 平时不停顿、占位块实时跟手;被拖的壳一碰到某个容器的子网格区域就停顿——不停顿的话碰撞会先把容器推开、指针永远进不去(R27 实测),
51
+ // 进去之后(或离开)又关掉,容器内的碰撞同样实时。原 R27 是全局常开 100ms,占位块要等指针停下才落位、手感拖沓
52
+ const NEAR_CONTAINER_PAUSE = 100
53
+ // 选中项的壳(R31):z-index 抬到 600 > 悬浮层根 500,碰撞层选中项的操作条 / 把手不被悬浮项遮住,悬浮层内也压过其它悬浮壳
54
+ const SELECTED_CLASS = 'epx-dashboard-design-grid__item--selected'
55
+
56
+ // 全局回调只装一次:造壳按网格元素反查所属实例(el.__epxDesignGrid)
57
+ let callbacksInstalled = false
58
+ function installCallbacks () {
59
+ if (callbacksInstalled) return
60
+ callbacksInstalled = true
61
+ GridStack.addRemoveCB = (parent, w, add, isGrid) => {
62
+ // 删除交给库(removeWidget 自己 remove DOM)
63
+ if (!add) return undefined
64
+ if (isGrid) {
65
+ // parent 是容器壳的内容盒
66
+ const shell = parent.closest('.grid-stack-item')
67
+ return shell ? pendingSubGrids.get(shell) : undefined
68
+ }
69
+ const owner = parent.__epxDesignGrid
70
+ // 非本组件管理的实例退回库默认造壳
71
+ if (!owner) return parent.gridstack ? parent.gridstack.createWidgetDivs(w) : undefined
72
+ return owner.createShell(w)
73
+ }
74
+ }
75
+
76
+ // 项必须有 id(引擎按 id 对账、Teleport 按 id 找壳):normalizeGridItem 只在有 id 时补 i,
77
+ // 无 id 的旧数据用 i 兜底,再没有就按计数造一个;计数 id 带前缀并对当前列表查重,避免撞上持久化的同名 id
78
+ function ensureId (item, list) {
79
+ normalizeGridItem(item)
80
+ if (item.id != null && item.id !== '') return
81
+ let id = (item.i != null && item.i !== '') ? String(item.i) : ''
82
+ while (!id || list.some(o => o && o !== item && String(o.id) === id)) id = 'epx_item_' + (++idSeq)
83
+ item.id = id
84
+ item.i = id
85
+ }
86
+
87
+ // 引擎节点与目标 widget 的差异,只含变化的键(update 只对传入的键做事,避免无谓的拖放重建);
88
+ // 锁定切换要同时改 locked / noMove / noResize
89
+ function diffWidget (n, w) {
90
+ const patch = {}
91
+ for (const k of ['x', 'y', 'w', 'h']) if (n[k] !== w[k]) patch[k] = w[k]
92
+ for (const k of ['minW', 'maxW', 'minH', 'maxH']) if ((n[k] || 0) !== (w[k] || 0)) patch[k] = w[k] || 0
93
+ if (!!n.locked !== !!w.locked) {
94
+ patch.locked = !!w.locked
95
+ patch.noMove = !!w.locked
96
+ patch.noResize = !!w.locked
97
+ }
98
+ return Object.keys(patch).length ? patch : null
99
+ }
100
+
101
+ export default {
102
+ name: 'epx-dashboard-design-grid',
103
+ props: {
104
+ items: { type: Array, default: () => [] },
105
+ grid: { type: Object, default: () => ({}) },
106
+ // rowHeight / gap 为 'auto' 时的换算基准(document.body 高度)
107
+ bodyHeight: { type: Number, default: 0 },
108
+ // 显式容器宽度(测试 / 无 ResizeObserver 环境用),缺省由 ResizeObserver 测量;挂载后不可变
109
+ width: { type: Number, default: 0 },
110
+ // 画布可视高度(px):换算成 minRow 让根网格盖满画布,指针移到空白处才不会触发库的「离开网格」
111
+ minHeight: { type: Number, default: 0 },
112
+ showGridLines: { type: Boolean, default: false },
113
+ // 容器内子网格:父 DesignGrid 的 api(容器 inject 后传入);缺省为根实例
114
+ subGridOf: { type: Object, default: null }
115
+ },
116
+ emits: ['change', 'transfer-out', 'transfer-in', 'select'],
117
+ // 设计态共享选中态(DashboardDesign provide):选中项的壳要置顶(R31),缺省(无外壳 / 单测)不处理
118
+ inject: { epxDashboardDesign: { default: null } },
119
+ provide () {
120
+ return { epxDashboardDesignGrid: this.api }
121
+ },
122
+ data () {
123
+ // shells:id → 壳的内容盒(.grid-stack-item-content),Teleport 目标;两层共用一张表(id 唯一)
124
+ // floatRows:悬浮层当前行数(含 minRow),驱动宿主最小高度(悬浮层绝对定位、不撑宿主)
125
+ return { measuredWidth: 0, shells: {}, floatRows: 0 }
126
+ },
127
+ computed: {
128
+ gridParams () { return normalizeGridParams(this.grid) },
129
+ // 整页悬浮:全部项进悬浮层,不建碰撞层
130
+ floatOnly () { return this.gridParams.mode === 'float' },
131
+ containerWidth () { return this.width || this.measuredWidth },
132
+ resolved () { return resolveGrid(this.gridParams, { containerWidth: this.containerWidth, bodyHeight: this.bodyHeight }) },
133
+ cellHeight () { return this.resolved.rowHeight + this.resolved.gapY },
134
+ // 扣掉根元素上外边距、向下取整:网格盖满画布又不撑出常驻滚动条(向上取整会多出不到一行 + 外边距的高度,真实画布实测出滚动条)
135
+ minRow () {
136
+ if (!(this.minHeight > 0)) return 0
137
+ const top = this.resolved.padY - this.resolved.gapY / 2
138
+ return Math.max(0, Math.floor((this.minHeight - top) / this.cellHeight))
139
+ },
140
+ // R32:容器内子实例的悬浮层以容器可视行数为 maxRow(与 minRow 同一套换算、至少 1 行),悬浮子项不可拖出 / 缩出容器底部;
141
+ // 页面级根实例与尚未量到高度的容器(minHeight 0)不限(0),否则首帧会把项全夹到 1 行
142
+ floatMaxRow () {
143
+ if (!this.subGridOf || !(this.minHeight > 0)) return 0
144
+ const top = this.resolved.padY - this.resolved.gapY / 2
145
+ return Math.max(1, Math.floor((this.minHeight - top) / this.cellHeight))
146
+ },
147
+ activeId () { return this.epxDashboardDesign ? this.epxDashboardDesign.state.activeId : null },
148
+ liveItems () { return (this.items || []).filter(Boolean) },
149
+ // 只追踪身份、几何、所在层与锁定字段的签名:外部改坐标 / 悬浮 / 层级(属性面板)或增删项时同步到引擎,改 configs 等不触发
150
+ layoutKey () {
151
+ return this.liveItems.map(i => [i.id, i.x, i.y, i.w, i.h, i.float ? 1 : 0, i.z, i.static ? 1 : 0, i.minW, i.maxW, i.minH, i.maxH].join(':')).join('|')
152
+ },
153
+ // 网格线背景(可选)+ 画布可视高度变量:.grid-stack 的 CSS min-height 用它盖满画布底部那不足一行的带子(minRow 向下取整留下的),
154
+ // 指针拖到那里才不会触发库的「离开网格」;库的 _updateContainerHeight 只删 inline min-height,不影响这条 CSS 规则
155
+ hostStyle () {
156
+ const style = this.showGridLines ? { ...gridLineStyle(this.resolved) } : {}
157
+ const top = this.resolved.padY - this.resolved.gapY / 2
158
+ if (this.minHeight > 0) style['--epx-design-grid-min-height'] = `${Math.max(0, this.minHeight - top)}px`
159
+ // 宿主高度取两层最大:碰撞层是流内元素自然撑起宿主,悬浮层绝对定位不参与,其高度(上外边距 + 行数 × cellHeight)
160
+ // 作为宿主最小高度——悬浮项拖到碰撞层内容之下时宿主跟着撑高、网格线延伸,与运行态 layoutHeight(含悬浮项坐标)语义一致
161
+ if (this.floatRows > 0) style.minHeight = `${top + this.floatRows * this.cellHeight}px`
162
+ return style
163
+ },
164
+ stackStyle () { return rootStyle(this.resolved) },
165
+ // 非响应式引用集合:给容器子网格与根网格互相登记
166
+ api () {
167
+ const owner = this
168
+ return markRaw({
169
+ owner,
170
+ makeSubGrid: (shellEl, options, subEl) => owner.makeSubGrid(shellEl, options, subEl),
171
+ register: child => { owner.children.add(child) },
172
+ unregister: child => { owner.children.delete(child) },
173
+ get root () { return owner.rootApi }
174
+ })
175
+ },
176
+ rootApi () { return this.subGridOf ? this.subGridOf.root : this.api }
177
+ },
178
+ watch: {
179
+ // 首帧前把 id / x/y/w/h 落定(Teleport 的 key 与引擎对账都靠 id);deep: 1 只看数组层与元素身份
180
+ items: {
181
+ deep: 1,
182
+ immediate: true,
183
+ handler (v) {
184
+ const list = v || []
185
+ for (const item of list) if (item) ensureId(item, list)
186
+ }
187
+ },
188
+ layoutKey () { this.syncToGrid() },
189
+ // 列数变了整体重建(引擎按列数布局);行高 / 间距变了动态改 cellHeight / margin,壳与内容都保留
190
+ resolved (n, o) {
191
+ if (!this.ready || !o) return
192
+ if (n.colNum !== o.colNum) {
193
+ this.rebuild()
194
+ return
195
+ }
196
+ if (n.rowHeight !== o.rowHeight || n.gapX !== o.gapX || n.gapY !== o.gapY) {
197
+ const options = toGridstackOptions(n)
198
+ for (const grid of this.layers()) {
199
+ grid.cellHeight(options.cellHeight)
200
+ grid.margin(options.margin)
201
+ }
202
+ }
203
+ },
204
+ // 碰撞 / 整页悬浮切换:实例组成变了,整体重建
205
+ floatOnly () { if (this.ready) this.rebuild() },
206
+ minRow (v) {
207
+ for (const grid of this.layers()) grid.updateOptions({ minRow: v })
208
+ this.updateFloatRows()
209
+ },
210
+ floatMaxRow (v) { this.applyFloatMaxRow(v) },
211
+ activeId () { this.applySelected() }
212
+ },
213
+ created () {
214
+ this.gridstack = null
215
+ this.floatstack = null
216
+ // 实例已就绪(init 完成、teardown 前):items → 引擎的同步只在此期间进行
217
+ this.ready = false
218
+ this.children = new Set()
219
+ // items → 引擎同步期间,库回调的 change 只回写不上报;同步结束按是否改了值统一 emit 一次
220
+ this.syncing = false
221
+ this.syncTouched = false
222
+ // rebuild 进行中 / 组件已卸载的竞态守卫
223
+ this.rebuilding = false
224
+ this.unmounted = false
225
+ // 拖拽结束的 document 兜底监听(mouseup / touchend / touchcancel),只有根实例持有
226
+ this.dragEnd = null
227
+ installCallbacks()
228
+ },
229
+ mounted () {
230
+ if (!this.width) {
231
+ this.measure()
232
+ if (typeof ResizeObserver !== 'undefined') {
233
+ this.resizeObserver = new ResizeObserver(() => this.measure())
234
+ this.resizeObserver.observe(this.$refs.host)
235
+ }
236
+ }
237
+ this.init()
238
+ },
239
+ // 壳先于内容创建(Teleport 要到下一次渲染才把内容放进壳),库建 draggable 时扫不到把手会退化成整壳可拖;内容进壳后重扫
240
+ updated () {
241
+ this.refreshDragHandles()
242
+ },
243
+ beforeUnmount () {
244
+ this.unmounted = true
245
+ if (this.resizeObserver) this.resizeObserver.disconnect()
246
+ if (this.subGridOf) this.subGridOf.unregister(this.api)
247
+ this.markDragging(false)
248
+ this.teardown()
249
+ this.shells = {}
250
+ },
251
+ methods: {
252
+ measure () { if (this.$refs.host) this.measuredWidth = this.$refs.host.offsetWidth },
253
+ // 当前存在的实例(碰撞层 / 悬浮层)
254
+ layers () { return [this.gridstack, this.floatstack].filter(Boolean) },
255
+ owns (grid) { return !!grid && (grid === this.gridstack || grid === this.floatstack) },
256
+ gridOptions (float) {
257
+ const options = toGridstackOptions(this.resolved, {
258
+ float,
259
+ engineClass: float ? NoCollideEngine : PushOnlyEngine
260
+ })
261
+ // 碰撞层接受跨网格拖入,但不收来自悬浮层的壳(悬浮项只经属性面板开关进出悬浮层,不靠拖拽跨层);悬浮层不建 droppable。
262
+ // 子网格会继承父 opts(含 minRow),这两个键都显式给;悬浮层另有 maxRow(R32,根实例 / 碰撞层为 0 不限)
263
+ options.acceptWidgets = float ? false : el => !el.closest('.' + FLOAT_CLASS)
264
+ options.minRow = this.minRow
265
+ options.maxRow = float ? this.floatMaxRow : 0
266
+ return options
267
+ },
268
+ init () {
269
+ const el = this.$refs.root
270
+ el.__epxDesignGrid = this
271
+ // 整页悬浮(grid.mode === 'float')只有悬浮层实例,碰撞层不建
272
+ if (!this.floatOnly) {
273
+ const options = this.gridOptions(false)
274
+ // 子网格:壳由父实例造,Teleport 内容(含本组件)在壳里,由父实例把本组件的 .grid-stack 收编成子网格
275
+ const shell = this.subGridOf ? el.closest('.grid-stack-item') : null
276
+ if (shell) {
277
+ this.gridstack = this.subGridOf.makeSubGrid(shell, options, el)
278
+ if (!this.gridstack) {
279
+ // eslint-disable-next-line no-console
280
+ console.error('[epx-dashboard-design-grid] 父网格未就绪,子网格未创建')
281
+ return
282
+ }
283
+ } else {
284
+ this.gridstack = GridStack.init(options, el)
285
+ }
286
+ // change / added 只在最外层 el 派发(子网格的也冒到根),根实例按 node.grid 反查所属实例分发;
287
+ // dragstart / dragstop 同样只报到最外层实例(子网格的拖拽起手 / 落位也在这里收到)
288
+ if (!this.gridstack.parentGridNode) {
289
+ this.gridstack.on('change', (ev, nodes) => this.dispatch(nodes, 'applyChanges'))
290
+ this.gridstack.on('added', (ev, nodes) => this.dispatch(nodes, 'onAdded'))
291
+ this.gridstack.on('dragstart', () => this.rootApi.owner.markDragging(true))
292
+ this.gridstack.on('dragstop', () => this.rootApi.owner.markDragging(false))
293
+ // 容器壳缩放结束(子网格的也报到根):壳里的子实例把悬浮层 maxRow 夹一次(缩放过程中不逐格夹,见 applyFloatMaxRow)
294
+ this.gridstack.on('resizestop', (ev, el) => this.onShellResizeStop(el))
295
+ }
296
+ // dropped 在接收方实例触发、不冒泡:每个实例都挂
297
+ this.gridstack.on('dropped', (ev, prev, node) => this.onDropped(prev, node))
298
+ }
299
+ if (this.subGridOf) this.subGridOf.register(this.api)
300
+ this.ready = true
301
+ this.syncToGrid(true)
302
+ },
303
+ // 悬浮层实例按需创建:与碰撞层同几何参数,float + NoCollideEngine,不接受拖入
304
+ ensureFloatLayer () {
305
+ if (this.floatstack) return this.floatstack
306
+ const el = this.$refs.floatRoot
307
+ el.__epxDesignGrid = this
308
+ const grid = GridStack.init(this.gridOptions(true), el)
309
+ // 库构造时按最近的壳(容器项)自动把实例挂成该壳的子网格,会顶掉碰撞层的子网格登记(一个壳只能有一个子网格);
310
+ // 悬浮层是独立实例(事件在自己的 el 派发、不参与跨网格拖放),把这层挂接撤掉、还给碰撞层
311
+ const shellNode = grid.parentGridNode
312
+ if (shellNode) {
313
+ if (shellNode.subGrid === grid) {
314
+ if (this.gridstack) shellNode.subGrid = this.gridstack
315
+ else delete shellNode.subGrid
316
+ }
317
+ delete grid.parentGridNode
318
+ el.classList.remove('grid-stack-nested')
319
+ }
320
+ // change / added 同样按 node 所属实例分发:挂在悬浮层壳里的容器子网格(整页悬浮 / 悬浮的容器项)事件冒到这里
321
+ grid.on('change', (ev, nodes) => {
322
+ this.dispatch(nodes, 'applyChanges')
323
+ this.updateFloatRows()
324
+ })
325
+ grid.on('added', (ev, nodes) => this.dispatch(nodes, 'onAdded'))
326
+ grid.on('dragstart', () => this.rootApi.owner.markDragging(true))
327
+ // 拖动 / 缩放进行中(每落一格触发一次)就撑高宿主,指针到碰撞层内容之下时网格线跟着延伸;
328
+ // 结束时再算一次:Esc 取消会把节点放回原位而不发 change(实测),行数要跟着回落
329
+ grid.on('drag', () => this.updateFloatRows())
330
+ grid.on('resize', () => this.updateFloatRows())
331
+ // 拖拽 / 缩放结束:拖动中被跳过的 maxRow 夹回(容器在拖拽期间变矮)在这里补做,再算行数
332
+ grid.on('dragstop', () => {
333
+ this.rootApi.owner.markDragging(false)
334
+ this.applyFloatMaxRow(this.floatMaxRow)
335
+ this.updateFloatRows()
336
+ })
337
+ grid.on('resizestop', (ev, el) => {
338
+ // 悬浮层里的容器壳缩放结束:壳内子实例夹一次(与碰撞层根的 resizestop 同一处理)
339
+ this.onShellResizeStop(el)
340
+ this.applyFloatMaxRow(this.floatMaxRow)
341
+ this.updateFloatRows()
342
+ })
343
+ this.floatstack = grid
344
+ return grid
345
+ },
346
+ // 悬浮层空了就销毁实例(元素常驻)
347
+ destroyFloatLayer (removeDOM = false) {
348
+ const grid = this.floatstack
349
+ if (!grid) return
350
+ this.floatstack = null
351
+ this.floatRows = 0
352
+ grid.offAll()
353
+ if (removeDOM) grid.removeAll(true, false)
354
+ grid.destroy(false)
355
+ if (this.$refs.floatRoot) delete this.$refs.floatRoot.__epxDesignGrid
356
+ },
357
+ // 父实例把容器壳转成子网格:先登记子实例的 .grid-stack,库经 addRemoveCB(isGrid) 取回它。
358
+ // 挂到壳所在的那层实例:一般是碰撞层;整页悬浮(碰撞层不存在)或容器项本身悬浮时是悬浮层——
359
+ // 子网格的选项全部显式给出(float / 引擎 / acceptWidgets),不会继承悬浮层的无碰撞语义
360
+ makeSubGrid (shellEl, options, subEl) {
361
+ // 壳还没进任何实例(父重建中 / 已拆)时不建子网格:父重建完成后 Teleport 会重新挂载子实例
362
+ const parent = shellEl && shellEl.gridstackNode && shellEl.gridstackNode.grid
363
+ if (!parent || !this.owns(parent)) return null
364
+ pendingSubGrids.set(shellEl, subEl)
365
+ try {
366
+ return parent.makeSubGrid(shellEl, options, undefined, false)
367
+ } finally {
368
+ pendingSubGrids.delete(shellEl)
369
+ }
370
+ },
371
+ // 按 node 所属实例分组后回调(子网格的节点 node.grid 是子实例)
372
+ dispatch (nodes, method) {
373
+ const groups = new Map()
374
+ for (const node of nodes || []) {
375
+ const owner = node.grid && node.grid.el && node.grid.el.__epxDesignGrid
376
+ if (!owner) continue
377
+ if (!groups.has(owner)) groups.set(owner, [])
378
+ groups.get(owner).push(node)
379
+ }
380
+ for (const [owner, list] of groups) owner[method](list)
381
+ },
382
+ createShell (w) {
383
+ const shell = document.createElement('div')
384
+ shell.className = 'grid-stack-item'
385
+ const content = document.createElement('div')
386
+ content.className = 'grid-stack-item-content'
387
+ shell.appendChild(content)
388
+ this.shells[w.id] = content
389
+ return shell
390
+ },
391
+ // items → 引擎:首次 / 重建用 load 一次性建;之后按 id 增 / 删 / 改,按 float 分到碰撞层 / 悬浮层(整页悬浮时全进悬浮层)。
392
+ // 同步后按引擎落定值对账回写 items:批内 update 被边界 / min-max 夹住的节点(update 后库删了 _orig,
393
+ // batchUpdate(false) 的 getDirtyNodes(true) 不报它)与 load 时被推挤 / 顶部压紧的节点(added 先清 _dirty)
394
+ // 都不发 change,不对账的话 items 与引擎永久分叉、每次同步重发无效 update。改了值就 emit 一次 change
395
+ // (与旧库挂载后 compact 触发 layout-updated 一致,外壳 deep watch 据此保存)
396
+ syncToGrid (initial = false) {
397
+ if (!this.ready) return
398
+ const list = this.liveItems
399
+ for (const item of list) ensureId(item, list)
400
+ const { flow, float } = this.floatOnly ? { flow: [], float: list } : partitionItems(list)
401
+ // load / addWidget 会把传入对象直接当引擎节点用,必须传纯拷贝(toWidget 返回新对象)
402
+ const flowWidgets = flow.map(toWidget)
403
+ const floatWidgets = float.map(toWidget)
404
+ if (floatWidgets.length) this.ensureFloatLayer()
405
+ const plan = [[this.gridstack, flowWidgets], [this.floatstack, floatWidgets]].filter(([grid]) => grid)
406
+ this.syncing = true
407
+ this.syncTouched = false
408
+ let touched = false
409
+ try {
410
+ if (initial) {
411
+ for (const [grid, widgets] of plan) grid.load(widgets)
412
+ } else {
413
+ // 先按同步前两层引擎的状态把全部差异算齐再应用:批内 update 的推挤会改别的节点,
414
+ // 边算边应用会把 items 里没改过的项又「更新」回旧位置、和推挤打架
415
+ const byId = new Map()
416
+ for (const [grid] of plan) for (const n of grid.engine.nodes) byId.set(n.id, n)
417
+ const ops = []
418
+ for (const [grid, widgets] of plan) {
419
+ for (const w of widgets) {
420
+ const n = byId.get(w.id)
421
+ if (!n) {
422
+ ops.push(['add', grid, w])
423
+ continue
424
+ }
425
+ byId.delete(w.id)
426
+ // 项换层:壳从另一层搬过来
427
+ if (n.grid !== grid) {
428
+ ops.push(['move', grid, w, n])
429
+ continue
430
+ }
431
+ const patch = diffWidget(n, w)
432
+ if (patch) ops.push(['update', grid, patch, n])
433
+ }
434
+ }
435
+ for (const [grid] of plan) grid.batchUpdate()
436
+ for (const [op, grid, w, n] of ops) {
437
+ if (op === 'add') grid.addWidget(w)
438
+ else if (op === 'update') grid.update(n.el, w)
439
+ else this.moveShell(n, grid, w)
440
+ }
441
+ for (const n of byId.values()) {
442
+ n.grid.removeWidget(n.el, true, false)
443
+ delete this.shells[n.id]
444
+ }
445
+ for (const [grid] of plan) grid.batchUpdate(false)
446
+ }
447
+ for (const [grid] of plan) touched = this.writeBack(grid.engine.nodes) || touched
448
+ touched = touched || this.syncTouched
449
+ } finally {
450
+ this.syncing = false
451
+ this.syncTouched = false
452
+ }
453
+ this.applyFloatZ()
454
+ if (!floatWidgets.length) this.destroyFloatLayer()
455
+ // 新建 / 搬过来 / 重建的壳不带选中类,按共享选中态补齐
456
+ this.applySelected()
457
+ this.updateFloatRows()
458
+ if (touched) this.$emit('change', this.items)
459
+ },
460
+ // 项换层:把壳从一个实例摘下(不删 DOM)再交给另一个实例收编。同一壳元素、Teleport 目标不变,内容组件不重建;
461
+ // 用 makeWidget 而不是 addWidget:后者会把壳上残留的 gs-* 属性(如旧的 gs-locked)合并进节点
462
+ moveShell (node, to, w) {
463
+ const el = node.el
464
+ node.grid.removeWidget(el, false, false)
465
+ el.remove()
466
+ // 层级只属于悬浮层(回碰撞层清掉;进悬浮层由 applyFloatZ 重设)
467
+ el.style.zIndex = ''
468
+ to.makeWidget(el, w)
469
+ },
470
+ // 切换项所在层(悬浮 / 碰撞):只改 item.float,下一拍由 layoutKey 监听同步(壳在两实例间迁移、坐标不变)。
471
+ // 不在这里直接 syncToGrid:同一拍内再改回去时监听比较的还是上次基线、不会再触发,引擎会与 items 脱节
472
+ setFloat (item, on) {
473
+ const target = item ? this.findItem(item.id) : null
474
+ if (target) target.float = !!on
475
+ return this.$nextTick()
476
+ },
477
+ // 悬浮项壳的层级:zIndex = 10 + z(与运行态 epx-dashboard-grid 一致);碰撞层的壳不设
478
+ applyFloatZ () {
479
+ if (!this.floatstack) return
480
+ for (const n of this.floatstack.engine.nodes) {
481
+ const item = this.findItem(n.id)
482
+ if (n.el) n.el.style.zIndex = String(10 + (Number(item && item.z) || 0))
483
+ }
484
+ },
485
+ // R31:共享选中态里的选中项(两层任一)的壳加置顶类,其余摘掉;容器内子实例各管自己的壳(选中项属于哪个实例由它自己判定)
486
+ applySelected () {
487
+ const id = this.activeId
488
+ for (const grid of this.layers()) {
489
+ for (const n of grid.engine.nodes) {
490
+ if (n.el) n.el.classList.toggle(SELECTED_CLASS, id != null && String(n.id) === String(id))
491
+ }
492
+ }
493
+ },
494
+ // 悬浮层行数(含 minRow)→ 宿主最小高度;没有悬浮层归零
495
+ updateFloatRows () {
496
+ this.floatRows = this.floatstack ? this.floatstack.getRow() : 0
497
+ },
498
+ // R32:悬浮层 maxRow 动态同步(容器高度 / 行高变了)。不走库的 updateOptions:它在超出时 compact(),会把悬浮项按空位重排
499
+ // (叠放的项被拆开);直接改 opts / engine 的 maxRow,超出底部的节点自己夹回(update 走 nodeBoundFix:先压高再抬 y),
500
+ // 批内 update 不发 change → 对账回写 items、改了值 emit 一次 change。
501
+ // 正在拖拽的节点(_moving,库在 dragstop 前删)跳过、由本层 dragstop 补夹;所在容器壳缩放中(每格一次 update + change 会连发自动保存)
502
+ // 也不夹,等壳的 resizestop 报到根实例后 force 夹一次
503
+ applyFloatMaxRow (v, force = false) {
504
+ const grid = this.floatstack
505
+ if (!grid) return
506
+ grid.opts.maxRow = grid.engine.maxRow = v
507
+ if (!force && this.shellResizing()) return
508
+ const over = v ? grid.engine.nodes.filter(n => !n._moving && n.y + n.h > v) : []
509
+ if (!over.length) return
510
+ this.syncing = true
511
+ this.syncTouched = false
512
+ let touched = false
513
+ try {
514
+ grid.batchUpdate()
515
+ for (const n of over) {
516
+ const h = Math.min(n.h, v)
517
+ grid.update(n.el, { h, y: Math.max(0, v - h) })
518
+ }
519
+ grid.batchUpdate(false)
520
+ touched = this.writeBack(grid.engine.nodes) || this.syncTouched
521
+ } finally {
522
+ this.syncing = false
523
+ this.syncTouched = false
524
+ }
525
+ this.updateFloatRows()
526
+ if (touched) this.$emit('change', this.items)
527
+ },
528
+ // 本实例所在的容器壳正在被缩放(库给壳打 ui-resizable-resizing,摘除在 resizestop 回调之后)
529
+ shellResizing () {
530
+ const shell = this.$refs.host && this.$refs.host.closest('.grid-stack-item')
531
+ return !!(shell && shell.classList.contains('ui-resizable-resizing'))
532
+ },
533
+ // 根实例收到某个壳的 resizestop:壳里(任意层级)的子实例把悬浮层 maxRow 强制夹一次
534
+ onShellResizeStop (el) {
535
+ if (!el || !el.contains) return
536
+ this.forEachDescendant(owner => {
537
+ if (owner.$refs.host && el.contains(owner.$refs.host)) owner.applyFloatMaxRow(owner.floatMaxRow, true)
538
+ })
539
+ },
540
+ // 遍历树内全部子实例(容器子网格及其下级)
541
+ forEachDescendant (fn) {
542
+ for (const child of this.children) {
543
+ fn(child.owner)
544
+ child.owner.forEachDescendant(fn)
545
+ }
546
+ },
547
+ findItem (id) {
548
+ return this.liveItems.find(i => String(i.id) === String(id))
549
+ },
550
+ // 引擎 → items:就地改坐标(设计器外壳的 deep watch 据此自动保存),返回是否真的改了值
551
+ writeBack (nodes) {
552
+ let touched = false
553
+ for (const node of nodes) {
554
+ const item = this.findItem(node.id)
555
+ if (!item) continue
556
+ for (const k of ['x', 'y', 'w', 'h']) {
557
+ if (item[k] !== node[k]) {
558
+ item[k] = node[k]
559
+ touched = true
560
+ }
561
+ }
562
+ }
563
+ return touched
564
+ },
565
+ // 库的 change 回调:回写并上报;同步期间只记下「改过值」,由 syncToGrid 结束时统一 emit 一次
566
+ applyChanges (nodes) {
567
+ const touched = this.writeBack(nodes)
568
+ if (this.syncing) this.syncTouched = this.syncTouched || touched
569
+ else if (touched) this.$emit('change', this.items)
570
+ },
571
+ // 库把别的网格的壳搬进来(跨网格)时壳仍是同一元素:登记它,等父组件把数据 push 进 items 后 Teleport 才会渲染
572
+ onAdded (nodes) {
573
+ for (const node of nodes) {
574
+ if (node.el && !this.shells[node.id]) this.shells[node.id] = node.el.querySelector('.grid-stack-item-content')
575
+ }
576
+ },
577
+ // 跨网格搬运(库已把壳搬到本实例):按视觉尺寸换算到本网格(spec §7.3)、同步 update 让壳大小跟上,
578
+ // 再通知两边父组件搬数据:源删、本加(内容组件随之重建,spec §9.0)
579
+ onDropped (prev, node) {
580
+ // 跨网格搬运时源实例不发 dragstop(库在目标网格接手后早退),拖拽标记在这里摘
581
+ this.rootApi.owner.markDragging(false)
582
+ // 外部拖入(阶段 5 预留)
583
+ if (!prev || !prev.grid) return
584
+ const from = prev.grid.el && prev.grid.el.__epxDesignGrid
585
+ if (!from || from === this) return
586
+ const item = from.findItem(node.id)
587
+ if (!item) return
588
+ const conv = convertItemBetweenGrids({ ...item, x: node.x, y: node.y }, from.resolved, this.resolved)
589
+ this.gridstack.update(node.el, { w: conv.w, h: conv.h })
590
+ // 取引擎落定后的位置(update 可能被边界夹住或推挤)
591
+ Object.assign(conv, { x: node.x, y: node.y, w: node.w, h: node.h })
592
+ delete from.shells[node.id]
593
+ from.$emit('transfer-out', item)
594
+ this.$emit('transfer-in', conv, node)
595
+ },
596
+ // 拖拽进行中的标记(只打在根实例的宿主上,容器子网格 / 悬浮层的起手都报到这里):期间所有悬浮壳不吃指针,
597
+ // 否则指针进悬浮壳时碰撞层根元素收到 mouseleave、库临时摘掉被拖节点(3.0 探针 Step 5)。
598
+ // 摘除以两层实例的 dragstop(同网格落位、Esc 取消、touch 结束都走它,Esc / touch 没有真实 mouseup)与跨网格的 dropped 为准,
599
+ // document 捕获阶段的 mouseup / touchend / touchcancel 只兜底;卸载时一并解绑
600
+ markDragging (on) {
601
+ const host = this.$refs.host
602
+ if (on && host && !this.dragEnd) {
603
+ host.classList.add(DRAGGING_CLASS)
604
+ this.dragEnd = () => this.markDragging(false)
605
+ for (const type of DRAG_END_EVENTS) document.addEventListener(type, this.dragEnd, true)
606
+ // 拖拽期间每次指针移动重算「是否碰到还没进入的容器」,据此开关落位停顿
607
+ this.dragMove = () => this.updateDragPause()
608
+ document.addEventListener('mousemove', this.dragMove, { capture: true, passive: true })
609
+ this.updateDragPause()
610
+ } else if (!on && this.dragEnd) {
611
+ if (host) host.classList.remove(DRAGGING_CLASS)
612
+ for (const type of DRAG_END_EVENTS) document.removeEventListener(type, this.dragEnd, true)
613
+ this.dragEnd = null
614
+ document.removeEventListener('mousemove', this.dragMove, true)
615
+ this.dragMove = null
616
+ DDManager.pauseDrag = false
617
+ }
618
+ },
619
+ // 所有后代子网格实例的宿主元素(容器内的画布区域,即接手拖入的区域)
620
+ subHosts () {
621
+ const hosts = []
622
+ const walk = api => {
623
+ for (const child of api.owner.children) {
624
+ if (child.owner.$refs.host) hosts.push(child.owner.$refs.host)
625
+ walk(child)
626
+ }
627
+ }
628
+ walk(this.api)
629
+ return hosts
630
+ },
631
+ // 被拖的壳与某个「还没进入的容器」的子网格区域相交 → 打开落位停顿,否则关掉。
632
+ // 排除:被拖的壳自己里面的子网格(拖的是容器)、当前接手拖拽的网格所在的容器及其祖先(已经在里面了)。
633
+ // 悬浮项(壳直接挂在悬浮层根下)本就不能靠拖拽进任何容器(碰撞层 acceptWidgets 拒收悬浮壳、悬浮层自己 acceptWidgets=false,
634
+ // 悬浮进出只走属性面板开关),碰到容器也不必开停顿——直接关掉,省掉这份拖沓
635
+ updateDragPause () {
636
+ const dragging = DDManager.dragElement && DDManager.dragElement.el
637
+ if (!dragging) return
638
+ const layer = dragging.parentElement
639
+ if (layer && layer.classList.contains(FLOAT_CLASS)) {
640
+ DDManager.pauseDrag = false
641
+ return
642
+ }
643
+ const current = DDManager.dropElement && DDManager.dropElement.el
644
+ const r = dragging.getBoundingClientRect()
645
+ const near = this.subHosts().some(host => {
646
+ if (dragging.contains(host) || (current && host.contains(current))) return false
647
+ const h = host.getBoundingClientRect()
648
+ return r.right > h.left && r.left < h.right && r.bottom > h.top && r.top < h.bottom
649
+ })
650
+ DDManager.pauseDrag = near ? NEAR_CONTAINER_PAUSE : false
651
+ },
652
+ // 拖拽把手落在壳本身、或已不在壳子树内(跨网格搬运时库先 _removeDD 再 _prepareElement 重建 DD,
653
+ // dragEls 指向源侧旧内容里的元素,随后被 Teleport 卸载)且壳内已有把手元素的项,重扫把手(库会换掉 mousedown 监听)
654
+ refreshDragHandles () {
655
+ for (const grid of this.layers()) {
656
+ const handle = grid.opts.draggable && grid.opts.draggable.handle
657
+ if (!handle) continue
658
+ for (const n of grid.engine.nodes) {
659
+ const dd = n.el && n.el.ddElement && n.el.ddElement.ddDraggable
660
+ if (!dd || !n.el.querySelector(handle)) continue
661
+ if (dd.dragEls.some(h => h === n.el || !n.el.contains(h))) grid.refreshDragHandles(n.el)
662
+ }
663
+ }
664
+ },
665
+ // 点击项(含拖拽起手)→ 选中;嵌套时事件会冒到父根元素,只处理属于本实例(两层任一)的壳
666
+ onMouseDown (e) {
667
+ const target = e.target && e.target.closest ? e.target : null
668
+ if (!target) return
669
+ const shell = target.closest('.grid-stack-item')
670
+ const node = shell && shell.gridstackNode
671
+ const grid = node && node.grid
672
+ const mine = this.owns(grid)
673
+ // 子网格里按在本实例的锁定项上(它的 DD 已停用、事件会一路冒到容器壳)→ 本次事件派发期间给所在层(碰撞层 / 悬浮层)的根打「按下」标记,
674
+ // 随后冒泡到容器项内容盒时其 draggable.cancel 命中标记、容器不起拖(锁定的是子项、别把整个容器拖走);派发完(宏任务)即摘除。
675
+ // 子网格空白处按下不标记:事件冒到容器壳内容盒、容器由此起拖(容器没有专门的起手区,与旧版「空白处可拖容器」一致);
676
+ // 子项内按下也不标记(子项自己的 DD 起拖并置 mouseHandled)
677
+ if (this.subGridOf && mine && node.noMove) {
678
+ const pressed = grid.el
679
+ pressed.classList.add(PRESSED_CLASS)
680
+ setTimeout(() => pressed.classList.remove(PRESSED_CLASS), 0)
681
+ }
682
+ if (!mine) return
683
+ const item = this.findItem(node.id)
684
+ if (item) this.$emit('select', item)
685
+ },
686
+ // 先拆子网格再拆自己:子网格的事件沿 parentGridNode 冒到根 el,父先销毁会让子销毁时访问已释放的 el
687
+ teardown (removeDOM = false) {
688
+ for (const child of Array.from(this.children)) child.owner.teardown()
689
+ this.children.clear()
690
+ this.ready = false
691
+ this.destroyFloatLayer(removeDOM)
692
+ const grid = this.gridstack
693
+ if (!grid) return
694
+ this.gridstack = null
695
+ grid.offAll()
696
+ if (removeDOM) grid.removeAll(true, false)
697
+ if (grid.parentGridNode) {
698
+ delete grid.parentGridNode.subGrid
699
+ delete grid.parentGridNode
700
+ }
701
+ grid.destroy(false)
702
+ if (this.$refs.root) delete this.$refs.root.__epxDesignGrid
703
+ },
704
+ // 列数变化:清壳并等 Teleport 卸载内容(子网格随内容一起销毁),再重建引擎与壳;内容组件重建一次
705
+ async rebuild () {
706
+ // 竞态守卫:重建进行中再触发不重复排队(init 读的是最新 resolved);等待期间组件卸载则不再建
707
+ if (this.rebuilding) return
708
+ this.rebuilding = true
709
+ try {
710
+ this.teardown(true)
711
+ this.shells = {}
712
+ await this.$nextTick()
713
+ if (this.unmounted || !this.$refs.root || this.ready) return
714
+ this.init()
715
+ } catch (e) {
716
+ // eslint-disable-next-line no-console
717
+ console.error('[epx-dashboard-design-grid] 重建网格失败', e)
718
+ } finally {
719
+ this.rebuilding = false
720
+ }
721
+ }
722
+ }
723
+ }
724
+ </script>
725
+
726
+ <style lang="scss">
727
+ @include b(dashboard-design-grid) {
728
+ position: relative;
729
+ min-height: 60px;
730
+ // 设计态不需要选中文字:在不起拖的位置(子网格空白 / 按钮 / 锁定项,库不 preventDefault)按下拖动会拉出文字选区,
731
+ // 下次按在选区上拖就变成浏览器原生拖选区(幽灵图里是文字 + 图表 canvas)、把 gridstack 的拖拽卡死;从根上不让选区形成
732
+ user-select: none;
733
+ // R31:根宿主自成堆叠上下文,两层的 z-index(悬浮层根 500 / 选中壳 600 / 拖动中 10000)只在画布内比较、不与画布外元素比;
734
+ // 容器内的子宿主不隔离:子实例两层的 z 直接参与根宿主的上下文(容器壳 / 子宿主都不建上下文),
735
+ // 容器里的选中项(600)与拖动中的子壳(10000)才压得过页面级悬浮层(500)
736
+ @include m(root) {
737
+ isolation: isolate;
738
+ }
739
+ @include e(stack) {
740
+ min-height: var(--epx-design-grid-min-height, 60px);
741
+ }
742
+ // 悬浮层根:同几何参数(宽 / 外边距由 stackStyle 给)绝对定位盖在碰撞层上;空白处不吃指针(点击 / 拖动穿透到碰撞层),只有壳吃。
743
+ // 自成堆叠上下文并压过碰撞层项的缩放把手(库 100 / 101)与操作条(200),但低于选中壳(600)与拖动 / 缩放中的项(10000);
744
+ // 碰撞层根不设 z-index(其壳的 z 直接参与宿主上下文)。选择器比库的 .grid-stack { position: relative } 多一级,不靠样式加载顺序
745
+ > .epx-dashboard-design-grid__float {
746
+ position: absolute;
747
+ top: 0;
748
+ left: 0;
749
+ z-index: 500;
750
+ pointer-events: none;
751
+ > .grid-stack-item {
752
+ pointer-events: auto;
753
+ }
754
+ }
755
+ // R31:选中项的壳置顶——碰撞层的选中壳压过悬浮层根(500),操作条 / 8 向把手不被悬浮项遮住;悬浮层内的选中壳同样压过其它悬浮壳
756
+ // (悬浮壳的 z 按 10 + z 写在 inline 样式上,要 !important 才压得过)。拖动 / 缩放中的壳(下一条,同优先级、靠后者胜)仍最高
757
+ .grid-stack > .grid-stack-item > .grid-stack-item-content {
758
+ // 选中 / 悬停边框画在内容盒上(与缩放圆点同一层,对齐不受壳 margin 影响);
759
+ // 内容盒 overflow:visible 不会裁切伪元素
760
+ &::after {
761
+ content: '';
762
+ position: absolute;
763
+ inset: 0;
764
+ border: 1px dashed transparent;
765
+ pointer-events: none;
766
+ z-index: 601;
767
+ }
768
+ }
769
+ .grid-stack > .grid-stack-item {
770
+ &:hover > .grid-stack-item-content::after,
771
+ &.epx-dashboard-design-grid__item--selected > .grid-stack-item-content::after {
772
+ border-color: rgba(var(--color-primary), 1);
773
+ }
774
+ // 容器 / 标签页:指针移到子网格里的项上、或子项被选中时,只显示最内层项的边框、圆点和操作条
775
+ &:has(.grid-stack-item:hover, .grid-stack-item.epx-dashboard-design-grid__item--selected) {
776
+ > .grid-stack-item-content::after {
777
+ border-color: transparent !important;
778
+ }
779
+ > .grid-stack-item-content > .epx-dashboard-design-item > .epx-dashboard-design-item__handle,
780
+ > .grid-stack-item-content > .epx-dashboard-design-item > .epx-dashboard-design-item__operates {
781
+ display: none !important;
782
+ }
783
+ }
784
+ }
785
+ .grid-stack > .grid-stack-item.epx-dashboard-design-grid__item--selected {
786
+ z-index: 600 !important;
787
+ }
788
+ .grid-stack > .grid-stack-item.ui-draggable-dragging,
789
+ .grid-stack > .grid-stack-item.ui-resizable-resizing {
790
+ z-index: 10000 !important;
791
+ }
792
+ // 拖拽进行中(标记打在根实例宿主上,盖住容器内的悬浮层):所有悬浮壳不吃指针,
793
+ // 否则指针进悬浮壳时碰撞层根元素收到 mouseleave、库临时摘掉被拖节点(3.0 探针 Step 5)
794
+ @include m(dragging) {
795
+ .epx-dashboard-design-grid__float > .grid-stack-item {
796
+ pointer-events: none;
797
+ }
798
+ // 拖拽进行中不显示任何项的选中 / 悬停虚线框(含正在拖的项——拖的时候把原本的选中框也取消,画面干净;
799
+ // 被指针穿透经过的下方项也不亮)
800
+ .grid-stack > .grid-stack-item > .grid-stack-item-content::after {
801
+ border-color: transparent !important;
802
+ }
803
+ // 缩放圆点与操作条同样全隐:拖是移位不是缩放,选中态的 chrome(圆点 / 操作按钮)在拖拽时都多余
804
+ .epx-dashboard-design-item__handle,
805
+ .epx-dashboard-design-item__operates {
806
+ display: none !important;
807
+ }
808
+ }
809
+ // 壳只负责定位;内容盒撑满且不裁切(选中边框 / 操作条要露出壳外),选择器优先级压过库的 overflow 规则
810
+ .grid-stack > .grid-stack-item > .grid-stack-item-content {
811
+ overflow: visible;
812
+ }
813
+ // 图表限高:在 design-item__body 层裁切溢出,design-item 自身保持 overflow:visible 让 outline 选中边框正常渲染。
814
+ // 设计态结构:壳 → design-item → __body → .drag-component → epx-chart
815
+ // 运行态结构:壳 → .dashboard-component → epx-chart
816
+ .grid-stack-item-content > .epx-dashboard-design-item {
817
+ > .epx-dashboard-design-item__body {
818
+ height: 100%;
819
+ overflow: hidden;
820
+ }
821
+ .drag-component,
822
+ .epx-chart {
823
+ height: 100% !important;
824
+ min-height: 0 !important;
825
+ max-height: 100% !important;
826
+ overflow: hidden !important;
827
+ }
828
+ }
829
+ .grid-stack-item-content > .dashboard-component {
830
+ height: 100% !important;
831
+ overflow: hidden !important;
832
+ .epx-chart {
833
+ height: 100% !important;
834
+ min-height: 0 !important;
835
+ max-height: 100% !important;
836
+ overflow: hidden !important;
837
+ }
838
+ }
839
+ // 拖动中的壳连同内部元素都不吃指针:库给壳写了 inline pointer-events:none 让指针下的目标网格能收到 mouseenter / mouseleave,
840
+ // 但容器组件根元素(dashboard-div / dashboard-tabs)带 pointer-events:all !important 会把它盖回去,容器就永远拖不进别的网格
841
+ .grid-stack-item.ui-draggable-dragging,
842
+ .grid-stack-item.ui-draggable-dragging * {
843
+ pointer-events: none !important;
844
+ }
845
+ // 占位块不吃指针:它贴着指针走(与被拖的壳几乎重合),吃指针的话指针永远进不到旁边容器的壳里,
846
+ // 容器收不到 mouseenter 就不会接手拖入(实测:慢速拖动只会把容器推开、进不去)
847
+ .grid-stack-placeholder {
848
+ pointer-events: none;
849
+ }
850
+ }
851
+ </style>