@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,711 @@
1
+ import { openBlock, createElementBlock, withModifiers, normalizeStyle, normalizeClass, createElementVNode, Fragment, renderList, createBlock, Teleport, renderSlot, createCommentVNode, markRaw } from "vue";
2
+ import { DDManager, GridStack } from "gridstack";
3
+ import "gridstack/dist/gridstack.min.css";
4
+ import { convertItemBetweenGrids, partitionItems, normalizeGridParams } from "./grid/model.js";
5
+ import { resolveGrid } from "./grid/geometry.js";
6
+ import { normalizeGridItem } from "./layout-util.js";
7
+ import { toWidget, toGridstackOptions, rootStyle, gridLineStyle } from "./grid/gridstackParams.js";
8
+ import { NoCollideEngine, PushOnlyEngine } from "./grid/engines.js";
9
+ /* empty css */
10
+ import _export_sfc from "../_virtual/_plugin-vue_export-helper.js";
11
+ const pendingSubGrids = /* @__PURE__ */ new Map();
12
+ let idSeq = 0;
13
+ const PRESSED_CLASS = "epx-dashboard-design-grid__stack--pressed";
14
+ const FLOAT_CLASS = "epx-dashboard-design-grid__float";
15
+ const DRAGGING_CLASS = "epx-dashboard-design-grid--dragging";
16
+ const DRAG_END_EVENTS = ["mouseup", "touchend", "touchcancel"];
17
+ const NEAR_CONTAINER_PAUSE = 100;
18
+ const SELECTED_CLASS = "epx-dashboard-design-grid__item--selected";
19
+ let callbacksInstalled = false;
20
+ function installCallbacks() {
21
+ if (callbacksInstalled) return;
22
+ callbacksInstalled = true;
23
+ GridStack.addRemoveCB = (parent, w, add, isGrid) => {
24
+ if (!add) return void 0;
25
+ if (isGrid) {
26
+ const shell = parent.closest(".grid-stack-item");
27
+ return shell ? pendingSubGrids.get(shell) : void 0;
28
+ }
29
+ const owner = parent.__epxDesignGrid;
30
+ if (!owner) return parent.gridstack ? parent.gridstack.createWidgetDivs(w) : void 0;
31
+ return owner.createShell(w);
32
+ };
33
+ }
34
+ function ensureId(item, list) {
35
+ normalizeGridItem(item);
36
+ if (item.id != null && item.id !== "") return;
37
+ let id = item.i != null && item.i !== "" ? String(item.i) : "";
38
+ while (!id || list.some((o) => o && o !== item && String(o.id) === id)) id = "epx_item_" + ++idSeq;
39
+ item.id = id;
40
+ item.i = id;
41
+ }
42
+ function diffWidget(n, w) {
43
+ const patch = {};
44
+ for (const k of ["x", "y", "w", "h"]) if (n[k] !== w[k]) patch[k] = w[k];
45
+ for (const k of ["minW", "maxW", "minH", "maxH"]) if ((n[k] || 0) !== (w[k] || 0)) patch[k] = w[k] || 0;
46
+ if (!!n.locked !== !!w.locked) {
47
+ patch.locked = !!w.locked;
48
+ patch.noMove = !!w.locked;
49
+ patch.noResize = !!w.locked;
50
+ }
51
+ return Object.keys(patch).length ? patch : null;
52
+ }
53
+ const _sfc_main = {
54
+ name: "epx-dashboard-design-grid",
55
+ props: {
56
+ items: { type: Array, default: () => [] },
57
+ grid: { type: Object, default: () => ({}) },
58
+ // rowHeight / gap 为 'auto' 时的换算基准(document.body 高度)
59
+ bodyHeight: { type: Number, default: 0 },
60
+ // 显式容器宽度(测试 / 无 ResizeObserver 环境用),缺省由 ResizeObserver 测量;挂载后不可变
61
+ width: { type: Number, default: 0 },
62
+ // 画布可视高度(px):换算成 minRow 让根网格盖满画布,指针移到空白处才不会触发库的「离开网格」
63
+ minHeight: { type: Number, default: 0 },
64
+ showGridLines: { type: Boolean, default: false },
65
+ // 容器内子网格:父 DesignGrid 的 api(容器 inject 后传入);缺省为根实例
66
+ subGridOf: { type: Object, default: null }
67
+ },
68
+ emits: ["change", "transfer-out", "transfer-in", "select"],
69
+ // 设计态共享选中态(DashboardDesign provide):选中项的壳要置顶(R31),缺省(无外壳 / 单测)不处理
70
+ inject: { epxDashboardDesign: { default: null } },
71
+ provide() {
72
+ return { epxDashboardDesignGrid: this.api };
73
+ },
74
+ data() {
75
+ return { measuredWidth: 0, shells: {}, floatRows: 0 };
76
+ },
77
+ computed: {
78
+ gridParams() {
79
+ return normalizeGridParams(this.grid);
80
+ },
81
+ // 整页悬浮:全部项进悬浮层,不建碰撞层
82
+ floatOnly() {
83
+ return this.gridParams.mode === "float";
84
+ },
85
+ containerWidth() {
86
+ return this.width || this.measuredWidth;
87
+ },
88
+ resolved() {
89
+ return resolveGrid(this.gridParams, { containerWidth: this.containerWidth, bodyHeight: this.bodyHeight });
90
+ },
91
+ cellHeight() {
92
+ return this.resolved.rowHeight + this.resolved.gapY;
93
+ },
94
+ // 扣掉根元素上外边距、向下取整:网格盖满画布又不撑出常驻滚动条(向上取整会多出不到一行 + 外边距的高度,真实画布实测出滚动条)
95
+ minRow() {
96
+ if (!(this.minHeight > 0)) return 0;
97
+ const top = this.resolved.padY - this.resolved.gapY / 2;
98
+ return Math.max(0, Math.floor((this.minHeight - top) / this.cellHeight));
99
+ },
100
+ // R32:容器内子实例的悬浮层以容器可视行数为 maxRow(与 minRow 同一套换算、至少 1 行),悬浮子项不可拖出 / 缩出容器底部;
101
+ // 页面级根实例与尚未量到高度的容器(minHeight 0)不限(0),否则首帧会把项全夹到 1 行
102
+ floatMaxRow() {
103
+ if (!this.subGridOf || !(this.minHeight > 0)) return 0;
104
+ const top = this.resolved.padY - this.resolved.gapY / 2;
105
+ return Math.max(1, Math.floor((this.minHeight - top) / this.cellHeight));
106
+ },
107
+ activeId() {
108
+ return this.epxDashboardDesign ? this.epxDashboardDesign.state.activeId : null;
109
+ },
110
+ liveItems() {
111
+ return (this.items || []).filter(Boolean);
112
+ },
113
+ // 只追踪身份、几何、所在层与锁定字段的签名:外部改坐标 / 悬浮 / 层级(属性面板)或增删项时同步到引擎,改 configs 等不触发
114
+ layoutKey() {
115
+ 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("|");
116
+ },
117
+ // 网格线背景(可选)+ 画布可视高度变量:.grid-stack 的 CSS min-height 用它盖满画布底部那不足一行的带子(minRow 向下取整留下的),
118
+ // 指针拖到那里才不会触发库的「离开网格」;库的 _updateContainerHeight 只删 inline min-height,不影响这条 CSS 规则
119
+ hostStyle() {
120
+ const style = this.showGridLines ? { ...gridLineStyle(this.resolved) } : {};
121
+ const top = this.resolved.padY - this.resolved.gapY / 2;
122
+ if (this.minHeight > 0) style["--epx-design-grid-min-height"] = `${Math.max(0, this.minHeight - top)}px`;
123
+ if (this.floatRows > 0) style.minHeight = `${top + this.floatRows * this.cellHeight}px`;
124
+ return style;
125
+ },
126
+ stackStyle() {
127
+ return rootStyle(this.resolved);
128
+ },
129
+ // 非响应式引用集合:给容器子网格与根网格互相登记
130
+ api() {
131
+ const owner = this;
132
+ return markRaw({
133
+ owner,
134
+ makeSubGrid: (shellEl, options, subEl) => owner.makeSubGrid(shellEl, options, subEl),
135
+ register: (child) => {
136
+ owner.children.add(child);
137
+ },
138
+ unregister: (child) => {
139
+ owner.children.delete(child);
140
+ },
141
+ get root() {
142
+ return owner.rootApi;
143
+ }
144
+ });
145
+ },
146
+ rootApi() {
147
+ return this.subGridOf ? this.subGridOf.root : this.api;
148
+ }
149
+ },
150
+ watch: {
151
+ // 首帧前把 id / x/y/w/h 落定(Teleport 的 key 与引擎对账都靠 id);deep: 1 只看数组层与元素身份
152
+ items: {
153
+ deep: 1,
154
+ immediate: true,
155
+ handler(v) {
156
+ const list = v || [];
157
+ for (const item of list) if (item) ensureId(item, list);
158
+ }
159
+ },
160
+ layoutKey() {
161
+ this.syncToGrid();
162
+ },
163
+ // 列数变了整体重建(引擎按列数布局);行高 / 间距变了动态改 cellHeight / margin,壳与内容都保留
164
+ resolved(n, o) {
165
+ if (!this.ready || !o) return;
166
+ if (n.colNum !== o.colNum) {
167
+ this.rebuild();
168
+ return;
169
+ }
170
+ if (n.rowHeight !== o.rowHeight || n.gapX !== o.gapX || n.gapY !== o.gapY) {
171
+ const options = toGridstackOptions(n);
172
+ for (const grid of this.layers()) {
173
+ grid.cellHeight(options.cellHeight);
174
+ grid.margin(options.margin);
175
+ }
176
+ }
177
+ },
178
+ // 碰撞 / 整页悬浮切换:实例组成变了,整体重建
179
+ floatOnly() {
180
+ if (this.ready) this.rebuild();
181
+ },
182
+ minRow(v) {
183
+ for (const grid of this.layers()) grid.updateOptions({ minRow: v });
184
+ this.updateFloatRows();
185
+ },
186
+ floatMaxRow(v) {
187
+ this.applyFloatMaxRow(v);
188
+ },
189
+ activeId() {
190
+ this.applySelected();
191
+ }
192
+ },
193
+ created() {
194
+ this.gridstack = null;
195
+ this.floatstack = null;
196
+ this.ready = false;
197
+ this.children = /* @__PURE__ */ new Set();
198
+ this.syncing = false;
199
+ this.syncTouched = false;
200
+ this.rebuilding = false;
201
+ this.unmounted = false;
202
+ this.dragEnd = null;
203
+ installCallbacks();
204
+ },
205
+ mounted() {
206
+ if (!this.width) {
207
+ this.measure();
208
+ if (typeof ResizeObserver !== "undefined") {
209
+ this.resizeObserver = new ResizeObserver(() => this.measure());
210
+ this.resizeObserver.observe(this.$refs.host);
211
+ }
212
+ }
213
+ this.init();
214
+ },
215
+ // 壳先于内容创建(Teleport 要到下一次渲染才把内容放进壳),库建 draggable 时扫不到把手会退化成整壳可拖;内容进壳后重扫
216
+ updated() {
217
+ this.refreshDragHandles();
218
+ },
219
+ beforeUnmount() {
220
+ this.unmounted = true;
221
+ if (this.resizeObserver) this.resizeObserver.disconnect();
222
+ if (this.subGridOf) this.subGridOf.unregister(this.api);
223
+ this.markDragging(false);
224
+ this.teardown();
225
+ this.shells = {};
226
+ },
227
+ methods: {
228
+ measure() {
229
+ if (this.$refs.host) this.measuredWidth = this.$refs.host.offsetWidth;
230
+ },
231
+ // 当前存在的实例(碰撞层 / 悬浮层)
232
+ layers() {
233
+ return [this.gridstack, this.floatstack].filter(Boolean);
234
+ },
235
+ owns(grid) {
236
+ return !!grid && (grid === this.gridstack || grid === this.floatstack);
237
+ },
238
+ gridOptions(float) {
239
+ const options = toGridstackOptions(this.resolved, {
240
+ float,
241
+ engineClass: float ? NoCollideEngine : PushOnlyEngine
242
+ });
243
+ options.acceptWidgets = float ? false : (el) => !el.closest("." + FLOAT_CLASS);
244
+ options.minRow = this.minRow;
245
+ options.maxRow = float ? this.floatMaxRow : 0;
246
+ return options;
247
+ },
248
+ init() {
249
+ const el = this.$refs.root;
250
+ el.__epxDesignGrid = this;
251
+ if (!this.floatOnly) {
252
+ const options = this.gridOptions(false);
253
+ const shell = this.subGridOf ? el.closest(".grid-stack-item") : null;
254
+ if (shell) {
255
+ this.gridstack = this.subGridOf.makeSubGrid(shell, options, el);
256
+ if (!this.gridstack) {
257
+ console.error("[epx-dashboard-design-grid] 父网格未就绪,子网格未创建");
258
+ return;
259
+ }
260
+ } else {
261
+ this.gridstack = GridStack.init(options, el);
262
+ }
263
+ if (!this.gridstack.parentGridNode) {
264
+ this.gridstack.on("change", (ev, nodes) => this.dispatch(nodes, "applyChanges"));
265
+ this.gridstack.on("added", (ev, nodes) => this.dispatch(nodes, "onAdded"));
266
+ this.gridstack.on("dragstart", () => this.rootApi.owner.markDragging(true));
267
+ this.gridstack.on("dragstop", () => this.rootApi.owner.markDragging(false));
268
+ this.gridstack.on("resizestop", (ev, el2) => this.onShellResizeStop(el2));
269
+ }
270
+ this.gridstack.on("dropped", (ev, prev, node) => this.onDropped(prev, node));
271
+ }
272
+ if (this.subGridOf) this.subGridOf.register(this.api);
273
+ this.ready = true;
274
+ this.syncToGrid(true);
275
+ },
276
+ // 悬浮层实例按需创建:与碰撞层同几何参数,float + NoCollideEngine,不接受拖入
277
+ ensureFloatLayer() {
278
+ if (this.floatstack) return this.floatstack;
279
+ const el = this.$refs.floatRoot;
280
+ el.__epxDesignGrid = this;
281
+ const grid = GridStack.init(this.gridOptions(true), el);
282
+ const shellNode = grid.parentGridNode;
283
+ if (shellNode) {
284
+ if (shellNode.subGrid === grid) {
285
+ if (this.gridstack) shellNode.subGrid = this.gridstack;
286
+ else delete shellNode.subGrid;
287
+ }
288
+ delete grid.parentGridNode;
289
+ el.classList.remove("grid-stack-nested");
290
+ }
291
+ grid.on("change", (ev, nodes) => {
292
+ this.dispatch(nodes, "applyChanges");
293
+ this.updateFloatRows();
294
+ });
295
+ grid.on("added", (ev, nodes) => this.dispatch(nodes, "onAdded"));
296
+ grid.on("dragstart", () => this.rootApi.owner.markDragging(true));
297
+ grid.on("drag", () => this.updateFloatRows());
298
+ grid.on("resize", () => this.updateFloatRows());
299
+ grid.on("dragstop", () => {
300
+ this.rootApi.owner.markDragging(false);
301
+ this.applyFloatMaxRow(this.floatMaxRow);
302
+ this.updateFloatRows();
303
+ });
304
+ grid.on("resizestop", (ev, el2) => {
305
+ this.onShellResizeStop(el2);
306
+ this.applyFloatMaxRow(this.floatMaxRow);
307
+ this.updateFloatRows();
308
+ });
309
+ this.floatstack = grid;
310
+ return grid;
311
+ },
312
+ // 悬浮层空了就销毁实例(元素常驻)
313
+ destroyFloatLayer(removeDOM = false) {
314
+ const grid = this.floatstack;
315
+ if (!grid) return;
316
+ this.floatstack = null;
317
+ this.floatRows = 0;
318
+ grid.offAll();
319
+ if (removeDOM) grid.removeAll(true, false);
320
+ grid.destroy(false);
321
+ if (this.$refs.floatRoot) delete this.$refs.floatRoot.__epxDesignGrid;
322
+ },
323
+ // 父实例把容器壳转成子网格:先登记子实例的 .grid-stack,库经 addRemoveCB(isGrid) 取回它。
324
+ // 挂到壳所在的那层实例:一般是碰撞层;整页悬浮(碰撞层不存在)或容器项本身悬浮时是悬浮层——
325
+ // 子网格的选项全部显式给出(float / 引擎 / acceptWidgets),不会继承悬浮层的无碰撞语义
326
+ makeSubGrid(shellEl, options, subEl) {
327
+ const parent = shellEl && shellEl.gridstackNode && shellEl.gridstackNode.grid;
328
+ if (!parent || !this.owns(parent)) return null;
329
+ pendingSubGrids.set(shellEl, subEl);
330
+ try {
331
+ return parent.makeSubGrid(shellEl, options, void 0, false);
332
+ } finally {
333
+ pendingSubGrids.delete(shellEl);
334
+ }
335
+ },
336
+ // 按 node 所属实例分组后回调(子网格的节点 node.grid 是子实例)
337
+ dispatch(nodes, method) {
338
+ const groups = /* @__PURE__ */ new Map();
339
+ for (const node of nodes || []) {
340
+ const owner = node.grid && node.grid.el && node.grid.el.__epxDesignGrid;
341
+ if (!owner) continue;
342
+ if (!groups.has(owner)) groups.set(owner, []);
343
+ groups.get(owner).push(node);
344
+ }
345
+ for (const [owner, list] of groups) owner[method](list);
346
+ },
347
+ createShell(w) {
348
+ const shell = document.createElement("div");
349
+ shell.className = "grid-stack-item";
350
+ const content = document.createElement("div");
351
+ content.className = "grid-stack-item-content";
352
+ shell.appendChild(content);
353
+ this.shells[w.id] = content;
354
+ return shell;
355
+ },
356
+ // items → 引擎:首次 / 重建用 load 一次性建;之后按 id 增 / 删 / 改,按 float 分到碰撞层 / 悬浮层(整页悬浮时全进悬浮层)。
357
+ // 同步后按引擎落定值对账回写 items:批内 update 被边界 / min-max 夹住的节点(update 后库删了 _orig,
358
+ // batchUpdate(false) 的 getDirtyNodes(true) 不报它)与 load 时被推挤 / 顶部压紧的节点(added 先清 _dirty)
359
+ // 都不发 change,不对账的话 items 与引擎永久分叉、每次同步重发无效 update。改了值就 emit 一次 change
360
+ // (与旧库挂载后 compact 触发 layout-updated 一致,外壳 deep watch 据此保存)
361
+ syncToGrid(initial = false) {
362
+ if (!this.ready) return;
363
+ const list = this.liveItems;
364
+ for (const item of list) ensureId(item, list);
365
+ const { flow, float } = this.floatOnly ? { flow: [], float: list } : partitionItems(list);
366
+ const flowWidgets = flow.map(toWidget);
367
+ const floatWidgets = float.map(toWidget);
368
+ if (floatWidgets.length) this.ensureFloatLayer();
369
+ const plan = [[this.gridstack, flowWidgets], [this.floatstack, floatWidgets]].filter(([grid]) => grid);
370
+ this.syncing = true;
371
+ this.syncTouched = false;
372
+ let touched = false;
373
+ try {
374
+ if (initial) {
375
+ for (const [grid, widgets] of plan) grid.load(widgets);
376
+ } else {
377
+ const byId = /* @__PURE__ */ new Map();
378
+ for (const [grid] of plan) for (const n of grid.engine.nodes) byId.set(n.id, n);
379
+ const ops = [];
380
+ for (const [grid, widgets] of plan) {
381
+ for (const w of widgets) {
382
+ const n = byId.get(w.id);
383
+ if (!n) {
384
+ ops.push(["add", grid, w]);
385
+ continue;
386
+ }
387
+ byId.delete(w.id);
388
+ if (n.grid !== grid) {
389
+ ops.push(["move", grid, w, n]);
390
+ continue;
391
+ }
392
+ const patch = diffWidget(n, w);
393
+ if (patch) ops.push(["update", grid, patch, n]);
394
+ }
395
+ }
396
+ for (const [grid] of plan) grid.batchUpdate();
397
+ for (const [op, grid, w, n] of ops) {
398
+ if (op === "add") grid.addWidget(w);
399
+ else if (op === "update") grid.update(n.el, w);
400
+ else this.moveShell(n, grid, w);
401
+ }
402
+ for (const n of byId.values()) {
403
+ n.grid.removeWidget(n.el, true, false);
404
+ delete this.shells[n.id];
405
+ }
406
+ for (const [grid] of plan) grid.batchUpdate(false);
407
+ }
408
+ for (const [grid] of plan) touched = this.writeBack(grid.engine.nodes) || touched;
409
+ touched = touched || this.syncTouched;
410
+ } finally {
411
+ this.syncing = false;
412
+ this.syncTouched = false;
413
+ }
414
+ this.applyFloatZ();
415
+ if (!floatWidgets.length) this.destroyFloatLayer();
416
+ this.applySelected();
417
+ this.updateFloatRows();
418
+ if (touched) this.$emit("change", this.items);
419
+ },
420
+ // 项换层:把壳从一个实例摘下(不删 DOM)再交给另一个实例收编。同一壳元素、Teleport 目标不变,内容组件不重建;
421
+ // 用 makeWidget 而不是 addWidget:后者会把壳上残留的 gs-* 属性(如旧的 gs-locked)合并进节点
422
+ moveShell(node, to, w) {
423
+ const el = node.el;
424
+ node.grid.removeWidget(el, false, false);
425
+ el.remove();
426
+ el.style.zIndex = "";
427
+ to.makeWidget(el, w);
428
+ },
429
+ // 切换项所在层(悬浮 / 碰撞):只改 item.float,下一拍由 layoutKey 监听同步(壳在两实例间迁移、坐标不变)。
430
+ // 不在这里直接 syncToGrid:同一拍内再改回去时监听比较的还是上次基线、不会再触发,引擎会与 items 脱节
431
+ setFloat(item, on) {
432
+ const target = item ? this.findItem(item.id) : null;
433
+ if (target) target.float = !!on;
434
+ return this.$nextTick();
435
+ },
436
+ // 悬浮项壳的层级:zIndex = 10 + z(与运行态 epx-dashboard-grid 一致);碰撞层的壳不设
437
+ applyFloatZ() {
438
+ if (!this.floatstack) return;
439
+ for (const n of this.floatstack.engine.nodes) {
440
+ const item = this.findItem(n.id);
441
+ if (n.el) n.el.style.zIndex = String(10 + (Number(item && item.z) || 0));
442
+ }
443
+ },
444
+ // R31:共享选中态里的选中项(两层任一)的壳加置顶类,其余摘掉;容器内子实例各管自己的壳(选中项属于哪个实例由它自己判定)
445
+ applySelected() {
446
+ const id = this.activeId;
447
+ for (const grid of this.layers()) {
448
+ for (const n of grid.engine.nodes) {
449
+ if (n.el) n.el.classList.toggle(SELECTED_CLASS, id != null && String(n.id) === String(id));
450
+ }
451
+ }
452
+ },
453
+ // 悬浮层行数(含 minRow)→ 宿主最小高度;没有悬浮层归零
454
+ updateFloatRows() {
455
+ this.floatRows = this.floatstack ? this.floatstack.getRow() : 0;
456
+ },
457
+ // R32:悬浮层 maxRow 动态同步(容器高度 / 行高变了)。不走库的 updateOptions:它在超出时 compact(),会把悬浮项按空位重排
458
+ // (叠放的项被拆开);直接改 opts / engine 的 maxRow,超出底部的节点自己夹回(update 走 nodeBoundFix:先压高再抬 y),
459
+ // 批内 update 不发 change → 对账回写 items、改了值 emit 一次 change。
460
+ // 正在拖拽的节点(_moving,库在 dragstop 前删)跳过、由本层 dragstop 补夹;所在容器壳缩放中(每格一次 update + change 会连发自动保存)
461
+ // 也不夹,等壳的 resizestop 报到根实例后 force 夹一次
462
+ applyFloatMaxRow(v, force = false) {
463
+ const grid = this.floatstack;
464
+ if (!grid) return;
465
+ grid.opts.maxRow = grid.engine.maxRow = v;
466
+ if (!force && this.shellResizing()) return;
467
+ const over = v ? grid.engine.nodes.filter((n) => !n._moving && n.y + n.h > v) : [];
468
+ if (!over.length) return;
469
+ this.syncing = true;
470
+ this.syncTouched = false;
471
+ let touched = false;
472
+ try {
473
+ grid.batchUpdate();
474
+ for (const n of over) {
475
+ const h = Math.min(n.h, v);
476
+ grid.update(n.el, { h, y: Math.max(0, v - h) });
477
+ }
478
+ grid.batchUpdate(false);
479
+ touched = this.writeBack(grid.engine.nodes) || this.syncTouched;
480
+ } finally {
481
+ this.syncing = false;
482
+ this.syncTouched = false;
483
+ }
484
+ this.updateFloatRows();
485
+ if (touched) this.$emit("change", this.items);
486
+ },
487
+ // 本实例所在的容器壳正在被缩放(库给壳打 ui-resizable-resizing,摘除在 resizestop 回调之后)
488
+ shellResizing() {
489
+ const shell = this.$refs.host && this.$refs.host.closest(".grid-stack-item");
490
+ return !!(shell && shell.classList.contains("ui-resizable-resizing"));
491
+ },
492
+ // 根实例收到某个壳的 resizestop:壳里(任意层级)的子实例把悬浮层 maxRow 强制夹一次
493
+ onShellResizeStop(el) {
494
+ if (!el || !el.contains) return;
495
+ this.forEachDescendant((owner) => {
496
+ if (owner.$refs.host && el.contains(owner.$refs.host)) owner.applyFloatMaxRow(owner.floatMaxRow, true);
497
+ });
498
+ },
499
+ // 遍历树内全部子实例(容器子网格及其下级)
500
+ forEachDescendant(fn) {
501
+ for (const child of this.children) {
502
+ fn(child.owner);
503
+ child.owner.forEachDescendant(fn);
504
+ }
505
+ },
506
+ findItem(id) {
507
+ return this.liveItems.find((i) => String(i.id) === String(id));
508
+ },
509
+ // 引擎 → items:就地改坐标(设计器外壳的 deep watch 据此自动保存),返回是否真的改了值
510
+ writeBack(nodes) {
511
+ let touched = false;
512
+ for (const node of nodes) {
513
+ const item = this.findItem(node.id);
514
+ if (!item) continue;
515
+ for (const k of ["x", "y", "w", "h"]) {
516
+ if (item[k] !== node[k]) {
517
+ item[k] = node[k];
518
+ touched = true;
519
+ }
520
+ }
521
+ }
522
+ return touched;
523
+ },
524
+ // 库的 change 回调:回写并上报;同步期间只记下「改过值」,由 syncToGrid 结束时统一 emit 一次
525
+ applyChanges(nodes) {
526
+ const touched = this.writeBack(nodes);
527
+ if (this.syncing) this.syncTouched = this.syncTouched || touched;
528
+ else if (touched) this.$emit("change", this.items);
529
+ },
530
+ // 库把别的网格的壳搬进来(跨网格)时壳仍是同一元素:登记它,等父组件把数据 push 进 items 后 Teleport 才会渲染
531
+ onAdded(nodes) {
532
+ for (const node of nodes) {
533
+ if (node.el && !this.shells[node.id]) this.shells[node.id] = node.el.querySelector(".grid-stack-item-content");
534
+ }
535
+ },
536
+ // 跨网格搬运(库已把壳搬到本实例):按视觉尺寸换算到本网格(spec §7.3)、同步 update 让壳大小跟上,
537
+ // 再通知两边父组件搬数据:源删、本加(内容组件随之重建,spec §9.0)
538
+ onDropped(prev, node) {
539
+ this.rootApi.owner.markDragging(false);
540
+ if (!prev || !prev.grid) return;
541
+ const from = prev.grid.el && prev.grid.el.__epxDesignGrid;
542
+ if (!from || from === this) return;
543
+ const item = from.findItem(node.id);
544
+ if (!item) return;
545
+ const conv = convertItemBetweenGrids({ ...item, x: node.x, y: node.y }, from.resolved, this.resolved);
546
+ this.gridstack.update(node.el, { w: conv.w, h: conv.h });
547
+ Object.assign(conv, { x: node.x, y: node.y, w: node.w, h: node.h });
548
+ delete from.shells[node.id];
549
+ from.$emit("transfer-out", item);
550
+ this.$emit("transfer-in", conv, node);
551
+ },
552
+ // 拖拽进行中的标记(只打在根实例的宿主上,容器子网格 / 悬浮层的起手都报到这里):期间所有悬浮壳不吃指针,
553
+ // 否则指针进悬浮壳时碰撞层根元素收到 mouseleave、库临时摘掉被拖节点(3.0 探针 Step 5)。
554
+ // 摘除以两层实例的 dragstop(同网格落位、Esc 取消、touch 结束都走它,Esc / touch 没有真实 mouseup)与跨网格的 dropped 为准,
555
+ // document 捕获阶段的 mouseup / touchend / touchcancel 只兜底;卸载时一并解绑
556
+ markDragging(on) {
557
+ const host = this.$refs.host;
558
+ if (on && host && !this.dragEnd) {
559
+ host.classList.add(DRAGGING_CLASS);
560
+ this.dragEnd = () => this.markDragging(false);
561
+ for (const type of DRAG_END_EVENTS) document.addEventListener(type, this.dragEnd, true);
562
+ this.dragMove = () => this.updateDragPause();
563
+ document.addEventListener("mousemove", this.dragMove, { capture: true, passive: true });
564
+ this.updateDragPause();
565
+ } else if (!on && this.dragEnd) {
566
+ if (host) host.classList.remove(DRAGGING_CLASS);
567
+ for (const type of DRAG_END_EVENTS) document.removeEventListener(type, this.dragEnd, true);
568
+ this.dragEnd = null;
569
+ document.removeEventListener("mousemove", this.dragMove, true);
570
+ this.dragMove = null;
571
+ DDManager.pauseDrag = false;
572
+ }
573
+ },
574
+ // 所有后代子网格实例的宿主元素(容器内的画布区域,即接手拖入的区域)
575
+ subHosts() {
576
+ const hosts = [];
577
+ const walk = (api) => {
578
+ for (const child of api.owner.children) {
579
+ if (child.owner.$refs.host) hosts.push(child.owner.$refs.host);
580
+ walk(child);
581
+ }
582
+ };
583
+ walk(this.api);
584
+ return hosts;
585
+ },
586
+ // 被拖的壳与某个「还没进入的容器」的子网格区域相交 → 打开落位停顿,否则关掉。
587
+ // 排除:被拖的壳自己里面的子网格(拖的是容器)、当前接手拖拽的网格所在的容器及其祖先(已经在里面了)。
588
+ // 悬浮项(壳直接挂在悬浮层根下)本就不能靠拖拽进任何容器(碰撞层 acceptWidgets 拒收悬浮壳、悬浮层自己 acceptWidgets=false,
589
+ // 悬浮进出只走属性面板开关),碰到容器也不必开停顿——直接关掉,省掉这份拖沓
590
+ updateDragPause() {
591
+ const dragging = DDManager.dragElement && DDManager.dragElement.el;
592
+ if (!dragging) return;
593
+ const layer = dragging.parentElement;
594
+ if (layer && layer.classList.contains(FLOAT_CLASS)) {
595
+ DDManager.pauseDrag = false;
596
+ return;
597
+ }
598
+ const current = DDManager.dropElement && DDManager.dropElement.el;
599
+ const r = dragging.getBoundingClientRect();
600
+ const near = this.subHosts().some((host) => {
601
+ if (dragging.contains(host) || current && host.contains(current)) return false;
602
+ const h = host.getBoundingClientRect();
603
+ return r.right > h.left && r.left < h.right && r.bottom > h.top && r.top < h.bottom;
604
+ });
605
+ DDManager.pauseDrag = near ? NEAR_CONTAINER_PAUSE : false;
606
+ },
607
+ // 拖拽把手落在壳本身、或已不在壳子树内(跨网格搬运时库先 _removeDD 再 _prepareElement 重建 DD,
608
+ // dragEls 指向源侧旧内容里的元素,随后被 Teleport 卸载)且壳内已有把手元素的项,重扫把手(库会换掉 mousedown 监听)
609
+ refreshDragHandles() {
610
+ for (const grid of this.layers()) {
611
+ const handle = grid.opts.draggable && grid.opts.draggable.handle;
612
+ if (!handle) continue;
613
+ for (const n of grid.engine.nodes) {
614
+ const dd = n.el && n.el.ddElement && n.el.ddElement.ddDraggable;
615
+ if (!dd || !n.el.querySelector(handle)) continue;
616
+ if (dd.dragEls.some((h) => h === n.el || !n.el.contains(h))) grid.refreshDragHandles(n.el);
617
+ }
618
+ }
619
+ },
620
+ // 点击项(含拖拽起手)→ 选中;嵌套时事件会冒到父根元素,只处理属于本实例(两层任一)的壳
621
+ onMouseDown(e) {
622
+ const target = e.target && e.target.closest ? e.target : null;
623
+ if (!target) return;
624
+ const shell = target.closest(".grid-stack-item");
625
+ const node = shell && shell.gridstackNode;
626
+ const grid = node && node.grid;
627
+ const mine = this.owns(grid);
628
+ if (this.subGridOf && mine && node.noMove) {
629
+ const pressed = grid.el;
630
+ pressed.classList.add(PRESSED_CLASS);
631
+ setTimeout(() => pressed.classList.remove(PRESSED_CLASS), 0);
632
+ }
633
+ if (!mine) return;
634
+ const item = this.findItem(node.id);
635
+ if (item) this.$emit("select", item);
636
+ },
637
+ // 先拆子网格再拆自己:子网格的事件沿 parentGridNode 冒到根 el,父先销毁会让子销毁时访问已释放的 el
638
+ teardown(removeDOM = false) {
639
+ for (const child of Array.from(this.children)) child.owner.teardown();
640
+ this.children.clear();
641
+ this.ready = false;
642
+ this.destroyFloatLayer(removeDOM);
643
+ const grid = this.gridstack;
644
+ if (!grid) return;
645
+ this.gridstack = null;
646
+ grid.offAll();
647
+ if (removeDOM) grid.removeAll(true, false);
648
+ if (grid.parentGridNode) {
649
+ delete grid.parentGridNode.subGrid;
650
+ delete grid.parentGridNode;
651
+ }
652
+ grid.destroy(false);
653
+ if (this.$refs.root) delete this.$refs.root.__epxDesignGrid;
654
+ },
655
+ // 列数变化:清壳并等 Teleport 卸载内容(子网格随内容一起销毁),再重建引擎与壳;内容组件重建一次
656
+ async rebuild() {
657
+ if (this.rebuilding) return;
658
+ this.rebuilding = true;
659
+ try {
660
+ this.teardown(true);
661
+ this.shells = {};
662
+ await this.$nextTick();
663
+ if (this.unmounted || !this.$refs.root || this.ready) return;
664
+ this.init();
665
+ } catch (e) {
666
+ console.error("[epx-dashboard-design-grid] 重建网格失败", e);
667
+ } finally {
668
+ this.rebuilding = false;
669
+ }
670
+ }
671
+ }
672
+ };
673
+ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
674
+ return openBlock(), createElementBlock("div", {
675
+ ref: "host",
676
+ class: normalizeClass(["epx-dashboard-design-grid", { "epx-dashboard-design-grid--root": !$props.subGridOf }]),
677
+ style: normalizeStyle($options.hostStyle),
678
+ onDragstart: _cache[2] || (_cache[2] = withModifiers(() => {
679
+ }, ["prevent"]))
680
+ }, [
681
+ createElementVNode("div", {
682
+ ref: "root",
683
+ class: "grid-stack epx-dashboard-design-grid__stack",
684
+ style: normalizeStyle($options.stackStyle),
685
+ onMousedown: _cache[0] || (_cache[0] = (...args) => $options.onMouseDown && $options.onMouseDown(...args))
686
+ }, null, 36),
687
+ createElementVNode("div", {
688
+ ref: "floatRoot",
689
+ class: "grid-stack epx-dashboard-design-grid__float",
690
+ style: normalizeStyle($options.stackStyle),
691
+ onMousedown: _cache[1] || (_cache[1] = (...args) => $options.onMouseDown && $options.onMouseDown(...args))
692
+ }, null, 36),
693
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.liveItems, (item) => {
694
+ return openBlock(), createElementBlock(Fragment, {
695
+ key: item.id
696
+ }, [
697
+ $data.shells[item.id] ? (openBlock(), createBlock(Teleport, {
698
+ key: 0,
699
+ to: $data.shells[item.id]
700
+ }, [
701
+ renderSlot(_ctx.$slots, "item", { item }),
702
+ renderSlot(_ctx.$slots, "chrome", { item })
703
+ ], 8, ["to"])) : createCommentVNode("", true)
704
+ ], 64);
705
+ }), 128))
706
+ ], 38);
707
+ }
708
+ const DesignGrid = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
709
+ export {
710
+ DesignGrid as default
711
+ };