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

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 +80 -0
  2. package/README.md +41 -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 +333 -0
  7. package/dist/src/DashboardGrid.js +183 -0
  8. package/dist/src/DesignGrid.js +712 -0
  9. package/dist/src/DesignItem.js +64 -0
  10. package/dist/src/Div.js +409 -0
  11. package/dist/src/Header.js +386 -0
  12. package/dist/src/Preview.js +28 -0
  13. package/dist/src/Tabs.js +469 -0
  14. package/dist/src/Title.js +102 -0
  15. package/dist/src/Widget.js +267 -0
  16. package/dist/src/chart-request-scope.js +33 -0
  17. package/dist/src/design-target.js +58 -0
  18. package/dist/src/grid/compact.js +69 -0
  19. package/dist/src/grid/compat/GridItem.js +36 -0
  20. package/dist/src/grid/compat/GridLayout.js +77 -0
  21. package/dist/src/grid/designState.js +22 -0
  22. package/dist/src/grid/engines.js +26 -0
  23. package/dist/src/grid/geometry.js +45 -0
  24. package/dist/src/grid/gridstackParams.js +70 -0
  25. package/dist/src/grid/mixins.js +109 -0
  26. package/dist/src/grid/model.js +224 -0
  27. package/dist/src/grid/useViewportGate.js +56 -0
  28. package/dist/src/index.js +27 -0
  29. package/dist/src/layout-util.js +109 -0
  30. package/dist/src/mobile-design.js +51 -0
  31. package/dist/src/theme-style.js +134 -0
  32. package/dist/style.css +1009 -0
  33. package/package.json +86 -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
package/src/Widget.vue ADDED
@@ -0,0 +1,354 @@
1
+
2
+ <template>
3
+ <div :class="['dashboard', 'dashboard--'+ theme, { 'dashboard--hidden-scroll-bar': $configs.scrollBarHidden }]" :style="$component.style" ref="container">
4
+ <div ref="dashboard" class="dashboard-container" :style="mobileDashboard ? { width: '100%', maxWidth: (Number($configs.mobileCanvasWidth) || 375) + 'px', margin: '0 auto', flex: 'none' } : {}">
5
+ <epx-dashboard-grid
6
+ :items="components"
7
+ :grid="gridParams"
8
+ :load-way="loadWay"
9
+ :body-height="bodyHeight"
10
+ scroll-root-selector=".dashboard-container">
11
+ <template #item="{ item: component }">
12
+ <!-- 渲染组件 -->
13
+ <div v-if="component.contentType" class="dashboard-component">
14
+ <!-- epx-dashboard__component 是看板主题 CSS 的组件卡片选择器(见 theme-style.js),
15
+ widget 内的组件也要带上,否则外层看板的主题样式盖不到这里 -->
16
+ <epx-component v-bind="isPage? $forwardAttrs($attrs): {}" class="component epx-dashboard__component"
17
+ :name="component.contentType"
18
+ is-dashboard
19
+ :content-type="component.contentType"
20
+ :dashboard-id="$component.id"
21
+ :comp-id="getComponentId(component)"
22
+ :comp-title="component.title"
23
+ :items="component.items"
24
+ :itemConfigs="$component.itemConfigs"
25
+ :comp-style="component.style"
26
+ :comp-configs="component.configs"
27
+ :mobile-appearance="mobileRendering ? component.mobileAppearance : undefined"
28
+ :comp-grid="containerGrid(component)"
29
+ :chart-cache="$configs.chartCache"
30
+ :theme="theme">
31
+ </epx-component>
32
+ </div>
33
+ </template>
34
+ </epx-dashboard-grid>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script>
40
+ import DashboardGrid from './DashboardGrid.vue'
41
+ import chartRequestScope from './chart-request-scope'
42
+ import { evalWith } from '@epxing/utils/src/eval-with'
43
+ import component from '@epxing/core/src/mixins/component'
44
+ import { pageGridMixin } from './grid/mixins'
45
+ import { stackMobileLayout } from './layout-util'
46
+ import { indexDashboardItems, resolveMobileItem } from './mobile-design'
47
+ import { getPageGrid, isLegacyConfig } from './grid/model'
48
+ import { mapState } from 'vuex'
49
+
50
+ // T31/T29:子树内 component 型全刷新。原用 `this.$children` 递归(Vue3 已移除)→
51
+ // 改走 T31 建的父子注册表 `_epxWalkChildren`(它本身就遍历整棵子树,故不需要外层递归)。
52
+ function refesh () {
53
+ this.resolveChartRequestBroker().invalidate()
54
+ this._epxWalkChildren(child => {
55
+ if (child.$options.$componentType === 'component' && child.$refesh) child.$refesh()
56
+ })
57
+ }
58
+ export default {
59
+ name: 'epx-dashboard-widget',
60
+ mixins: [component, pageGridMixin, chartRequestScope],
61
+ components: { [DashboardGrid.name]: DashboardGrid },
62
+ $componentType: 'component',
63
+ navConfigs: {
64
+ flag: false
65
+ },
66
+ style: {},
67
+ layout: 'grid',
68
+ // 组件列表
69
+ items: [
70
+ ],
71
+ // 组件配置
72
+ itemConfigs: {
73
+
74
+ },
75
+ props: {
76
+ inline: Boolean,
77
+ routeKey: [String, Number],
78
+ routes: {
79
+ type: Array,
80
+ default () {
81
+ return []
82
+ }
83
+ }
84
+ },
85
+ data () {
86
+ return {
87
+ headerTitle: null,
88
+ pageParams: {},
89
+ model: {}, // 组件全局data
90
+ pageEvents: {},
91
+ searchScopeParams: []
92
+ }
93
+ },
94
+ computed: {
95
+ ...mapState({
96
+ showMenu: state => state.config.showMenu,
97
+ showAppNav: state => state.config.showAppNav
98
+ }),
99
+ pageTitle () {
100
+ return this.headerTitle || (this.page && this.page.title) || (this.meta && this.meta.name)
101
+ },
102
+ components () {
103
+ // items itemConfigs
104
+ const { items = [], mItems } = this.$component
105
+ let list
106
+ if (this.mobileDashboard) {
107
+ list = items.filter(item => !item.mobileHidden)
108
+ } else if (this.$isMobile) {
109
+ if (mItems && mItems.length) {
110
+ const pcIndex = indexDashboardItems(items)
111
+ list = mItems.filter(c => !c.mobileHidden && pcIndex.has(String(c.id))).map(c => resolveMobileItem(c, pcIndex))
112
+ } else {
113
+ const { mobileAutoStack = true } = this.$configs
114
+ list = mobileAutoStack ? stackMobileLayout(items, 48) : items
115
+ }
116
+ } else {
117
+ list = items
118
+ }
119
+ // 布局项必须带 i(设计器与旧配置都以 id 为准),运行时用 id 兜底补齐
120
+ list.forEach(item => {
121
+ if (item && item.i == null && item.id != null) item.i = item.id
122
+ })
123
+ return list
124
+ },
125
+ theme () {
126
+ const { theme } = this.$configs
127
+ return theme
128
+ },
129
+ // Widget 原本没有移动端收紧间距的分支(与页面版 Dashboard 不同),按「旧配置像素一致」保留原语义,覆盖 pageGridMixin 的 gridParams
130
+ gridParams () {
131
+ const grid = getPageGrid(this.$configs)
132
+ return this.mobileDashboard ? { ...grid, gap: [8, 8], padding: null } : grid
133
+ },
134
+ colAttribute () {
135
+ return (component = {}) => {
136
+ const { xsspan = 24, ...other } = component
137
+ const colAttribute = {
138
+ ...other,
139
+ xs: {
140
+ span: xsspan
141
+ }
142
+ }
143
+ delete colAttribute.id
144
+ delete colAttribute.style
145
+ delete colAttribute.title
146
+ return colAttribute
147
+ }
148
+ },
149
+ pageStyle () {
150
+ const { layout = 'grid', direction = 'row', justify = 'flex-start', align = 'top' } = this.$configs
151
+ if (layout === 'flex') {
152
+ return { display: 'flex', 'flex-direction': direction, 'justify-content': justify, 'align-items': align }
153
+ } else {
154
+ return { }
155
+ }
156
+ },
157
+ componentStyle () {
158
+ return component => {
159
+ const { layout } = this.$configs
160
+ const { flex } = component
161
+ const compoentStyle = { }
162
+ if (layout === 'flex' && flex) {
163
+ compoentStyle.flex = 1
164
+ }
165
+ return compoentStyle
166
+ }
167
+ },
168
+ scopeParams () {
169
+ const { globalScopeParams: configScopeParams = [] } = this.$configs
170
+ const { scopeParams: queryScopeParams = [] } = this.$configs
171
+
172
+ // 格式化scopeParams
173
+ const _scopeParams = this.$_.cloneDeep([...configScopeParams, ...queryScopeParams, ...this.searchScopeParams])
174
+ const that = this
175
+ for (const scopeKey in _scopeParams) {
176
+ const scope = _scopeParams[scopeKey]
177
+ if (!scope) continue
178
+ let values = scope.values
179
+ if (/\$\[(.+)\]/.test(values)) {
180
+ values = values.replace(/\$\[(.+)\]/, (match, key) => {
181
+ try {
182
+ return evalWith(`that.${key}`, { that }) || evalWith(`that.$query.${key}`, { that }) || evalWith(`that.$tab.${key}`, { that }) || ''
183
+ } catch (error) {
184
+ return scope.values
185
+ }
186
+ })
187
+ }
188
+ if (/#\{(.+)\}/.test(values)) {
189
+ values = values.replace(/#\{(.+)\}/, (match, key) => {
190
+ try {
191
+ return evalWith(`that.$query.${key}`, { that }) || evalWith(`that.$tab.${key}`, { that }) || ''
192
+ } catch (error) {
193
+ return scope.values
194
+ }
195
+ })
196
+ }
197
+ scope.values = values
198
+ }
199
+ return _scopeParams
200
+ }
201
+ },
202
+ watch: {
203
+ pageTitle: {
204
+ handler (v) {
205
+ this.$emit('change-route', this.routeKey, { title: v })
206
+ },
207
+ immediate: true
208
+ },
209
+ scopeParams: {
210
+ deep: true,
211
+ handler (v) {
212
+ this.pageParams.scopeParams = v
213
+ },
214
+ immediate: true
215
+ }
216
+ },
217
+ provide () {
218
+ // 容器内网格的门控策略 / 配置是否旧版都跟随本组件(provide 只跑一次,用函数延迟读)
219
+ const gridProvide = {
220
+ getMobileDashboard: () => this.mobileDashboard,
221
+ getDashboardLoadWay: () => this.loadWay,
222
+ getDashboardLegacy: () => isLegacyConfig(this.$configs)
223
+ }
224
+ if (this.isPage) {
225
+ return {
226
+ ...gridProvide,
227
+ upPage: this,
228
+ pageId: this.page ? this.page.pageId : '', // 当前页面id
229
+ pageMetaId: this.isPage ? (this.$metaId || (this.page ? this.page.metaId : '')) : null // 当前页面对象id
230
+ }
231
+ }
232
+ return gridProvide // T28:Vue3 provide 合并不容忍 undefined
233
+ },
234
+ methods: {
235
+ $refesh () {
236
+ refesh.apply(this)
237
+ // if (this.upPage) this.upPage.$refesh()
238
+ },
239
+ openFullScreen () {
240
+ if (this.$refs.dashboard.requestFullscreen) {
241
+ this.$refs.dashboard.requestFullscreen()
242
+ }
243
+ },
244
+ search (v) {
245
+ this.searchScopeParams = v
246
+ }
247
+ },
248
+ created () {
249
+ // 这里原本存了 showAppNav/showMenu 用于卸载时「还原」,已删除,理由见 beforeUnmount 的注释。
250
+ // 行高 / 间距按视口等比(旧 autoHeight)时才需要跟随窗口尺寸
251
+ const grid = this.gridParams
252
+ if (grid.rowHeight === 'auto' || grid.gap === 'auto') {
253
+ window.addEventListener('resize', this.updateBodyHeight)
254
+ }
255
+ },
256
+ beforeUnmount () {
257
+ // 这里原来无条件写两个**全局** UI 状态(SWITCH_MENU_SHOW / SWITCH_APP_NAV_SHOW),已删除。三个理由:
258
+ // ① **没有对应的「获取」**:整个 packages/dashboard 里没有任何地方隐藏菜单或应用导航
259
+ // (openFullScreen 走的是浏览器原生 requestFullscreen,与这两个状态无关)。
260
+ // 只还原、从不获取,等于凭空覆盖别人的状态。
261
+ // ② **存的值是反的**:created 里 `defaultShowMenu = this.showAppNav`、
262
+ // `defaultShowAppNav = this.showMenu`,两者交叉,还原时把 appNav 的旧值写进 menu、反之亦然。
263
+ // ③ **卸载不等于「用户离开了本页」**:标签是 v-for + v-show 全挂载,关掉一个**后台**标签、
264
+ // 或标签数超过 MOUNTED_TAB_MAX 被 LRU 淘汰内容,都会走到这里 ——
265
+ // 于是「关掉一个大屏标签,把另一个正在看的标签的菜单弹了出来」。
266
+ // 这段代码此前从未执行过(keep-alive 的页从不真卸载),M0 改成真卸载后才被唤醒,故一并清理。
267
+ window.removeEventListener('resize', this.updateBodyHeight)
268
+ }
269
+ }
270
+ </script>
271
+
272
+ <style lang="scss" scoped>
273
+ .dashboard {
274
+ width: 100%;
275
+ height: 100%;
276
+ overflow: auto;
277
+ overflow-x: hidden;
278
+ display: flex;
279
+ flex-direction: column;
280
+
281
+ .base-page {
282
+ position: static;
283
+ overflow-x: hidden;
284
+ padding: 0;
285
+ }
286
+ }
287
+ .dashboard--hidden-scroll-bar {
288
+ .dashboard-container {
289
+ &::-webkit-scrollbar-thumb {
290
+ background: none;
291
+ &:hover {
292
+ background-color: rgba(95, 95, 95, .3);
293
+ }
294
+ }
295
+ }
296
+ }
297
+ .dashboard-container {
298
+ width: 100%;
299
+ flex: 1;
300
+ overflow: auto;
301
+ }
302
+ .dashboard-component {
303
+ width: 100%;
304
+ height: 100%;
305
+ overflow: auto;
306
+ }
307
+
308
+ .dashboard--dark {
309
+ .dashboard-container{
310
+ background: #151C3D;
311
+ .dashboard-component {
312
+ >div {
313
+ // background: rgba(255,255,255, .1)
314
+ }
315
+ }
316
+ }
317
+ }
318
+ .dashboard--technology {
319
+ .dashboard-container{
320
+ background: url('@/assets/images/dashboard/technology_bg.jpg') no-repeat center;
321
+ background-size: cover;
322
+
323
+ }
324
+ .component {
325
+ // position: relative;
326
+ // &::before {
327
+ // content: '';
328
+ // position: absolute;
329
+ // top: 0;
330
+ // display: block;
331
+ // width: 100%;
332
+ // height: 1px;
333
+ // background: linear-gradient(90deg, rgba(45, 164, 247, 0) 0%, #2DA4F7 50%, rgba(45, 164, 247, 0) 100%);
334
+ // opacity: 0.5;
335
+ // }
336
+ // &::after {
337
+ // content: '';
338
+ // position: absolute;
339
+ // bottom: 0;
340
+ // display: block;
341
+ // width: 100%;
342
+ // height: 1px;
343
+ // background: linear-gradient(90deg, rgba(45, 164, 247, 0) 0%, #2DA4F7 50%, rgba(45, 164, 247, 0) 100%);
344
+ // opacity: 0.5;
345
+ // }
346
+ // background: rgba(255,255,255, .1);
347
+ // backdrop-filter: blur(4px);
348
+ // >div {
349
+ // background: rgba(255,255,255, .1);
350
+ // backdrop-filter: blur(4px);
351
+ // }
352
+ }
353
+ }
354
+ </style>
@@ -0,0 +1,22 @@
1
+ import { markRaw } from 'vue'
2
+ import { createChartRequestBroker } from '@epxing/utils/src/chart-request-broker'
3
+
4
+ export default {
5
+ inject: { getParentChartRequestBroker: { from: 'getChartRequestBroker', default: null } },
6
+ data () { return { ownedChartRequestBroker: null } },
7
+ provide () { return { getChartRequestBroker: () => this.resolveChartRequestBroker() } },
8
+ watch: {
9
+ '$route.query.pageId' () {
10
+ this.ownedChartRequestBroker?.dispose()
11
+ this.ownedChartRequestBroker = null
12
+ }
13
+ },
14
+ methods: {
15
+ resolveChartRequestBroker () {
16
+ if (this.getParentChartRequestBroker) return this.getParentChartRequestBroker()
17
+ if (!this.ownedChartRequestBroker) this.ownedChartRequestBroker = markRaw(createChartRequestBroker())
18
+ return this.ownedChartRequestBroker
19
+ }
20
+ },
21
+ beforeUnmount () { this.ownedChartRequestBroker?.dispose() }
22
+ }
@@ -0,0 +1,55 @@
1
+ import { stackMobileLayout } from './layout-util'
2
+
3
+ export const DEFAULT_DASHBOARD_THEME = 'light'
4
+
5
+ // 主要设计端独立于菜单 TERMINAL 和设备检测;旧看板默认 PC。
6
+ export function getDashboardDesignTarget (config = {}) {
7
+ return config.designTarget === 'mobile' ? 'mobile' : 'pc'
8
+ }
9
+
10
+ export function createDashboardConfig (config = {}, target = 'pc') {
11
+ const configuredColNum = Number(config.grid?.colNum)
12
+ const colNum = Number.isInteger(configuredColNum) && configuredColNum > 0 ? configuredColNum : ((config.items || []).length ? 48 : 24)
13
+ const out = {
14
+ ...config,
15
+ grid: { ...config.grid, colNum },
16
+ theme: config.theme || DEFAULT_DASHBOARD_THEME,
17
+ designTarget: getDashboardDesignTarget({ designTarget: target }),
18
+ ...(target === 'mobile' ? { mobileCanvasWidth: 375, autoHeight: false, grid: { ...config.grid, colNum, rowHeight: 10, gap: [8, 8], padding: null } } : {})
19
+ }
20
+ if (target === 'mobile') {
21
+ delete out.mItems
22
+ const mobileItems = items => {
23
+ const list = JSON.parse(JSON.stringify(items || []))
24
+ for (const item of list) {
25
+ if (item.contentType === 'dashboard-tabs') {
26
+ item.mobileExpand = false
27
+ for (const tab of (item.items || [])) tab.items = mobileItems(tab.items)
28
+ } else if (item.contentType === 'dashboard-div') {
29
+ item.mobileExpand = false
30
+ item.items = mobileItems(item.items)
31
+ }
32
+ }
33
+ return stackMobileLayout(list, colNum, { keepStyle: true })
34
+ }
35
+ out.items = mobileItems(config.items)
36
+ }
37
+ return out
38
+ }
39
+
40
+ export function removeDashboardItem (items, id, configs) {
41
+ for (let i = 0; i < items.length; i++) {
42
+ const item = items[i]
43
+ if (String(item.id) === String(id)) {
44
+ const clear = node => {
45
+ delete configs[node.id]
46
+ for (const child of (node.items || [])) clear(child)
47
+ }
48
+ clear(item)
49
+ items.splice(i, 1)
50
+ return true
51
+ }
52
+ if (item.items && removeDashboardItem(item.items, id, configs)) return true
53
+ }
54
+ return false
55
+ }
@@ -0,0 +1,81 @@
1
+ // 看板布局压紧(compact):移植自 vue-grid-layout 系的 grid-layout-plus 1.1.1(`src/helpers/common.ts`,
2
+ // MIT License,Copyright (c) 2023-present qmhc;算法上溯 react-grid-layout,同为 MIT)的 compact / compactItem /
3
+ // sortLayoutItemsByRowCol / getStatics / getFirstCollision / collides。旧运行态由布局库在 mounted 后就地 compact(空隙上移、重叠推下),新实现在渲染前用
4
+ // 同一算法算出一份坐标副本,保证旧页面配置的位置与现状一致。
5
+ // 与原版的差异:纯函数、不改入参——返回新数组,每项是 { ...item, x, y, w, h }(数值化)的副本;
6
+ // static(锁定)项固定不动、其它项绕开它;float(悬浮)项不参与碰撞、坐标原样;空项(null)跳过。
7
+
8
+ // 两项是否重叠(同一项不算)
9
+ export function collides (l1, l2) {
10
+ if (l1 === l2) return false
11
+ if (l1.x + l1.w <= l2.x) return false
12
+ if (l1.x >= l2.x + l2.w) return false
13
+ if (l1.y + l1.h <= l2.y) return false
14
+ if (l1.y >= l2.y + l2.h) return false
15
+ return true
16
+ }
17
+
18
+ // layout 里第一个与 item 重叠的项
19
+ export function getFirstCollision (layout, item) {
20
+ for (let i = 0, len = layout.length; i < len; i++) {
21
+ if (collides(layout[i], item)) return layout[i]
22
+ }
23
+ }
24
+
25
+ // 按行、列排序(先 y 后 x),压紧按这个顺序逐项进行
26
+ export function sortLayoutItemsByRowCol (layout) {
27
+ return Array.from(layout).sort((a, b) => {
28
+ if (a.y === b.y && a.x === b.x) return 0
29
+ if (a.y > b.y || (a.y === b.y && a.x > b.x)) return 1
30
+ return -1
31
+ })
32
+ }
33
+
34
+ // 单项压紧:先尽量上移到不碰撞为止,再被碰撞项往下推(就地改 item.y,item 已是副本)
35
+ export function compactItem (compareWith, item, verticalCompact = true) {
36
+ if (verticalCompact) {
37
+ while (item.y > 0 && !getFirstCollision(compareWith, item)) item.y--
38
+ }
39
+ let collision
40
+ while ((collision = getFirstCollision(compareWith, item))) item.y = collision.y + collision.h
41
+ return item
42
+ }
43
+
44
+ // 副本 + x/y/w/h 数值化(与 itemRect 的兜底一致:坐标缺省 0、宽高最小 1)
45
+ function cloneItem (item) {
46
+ return {
47
+ ...item,
48
+ x: Number(item.x) || 0,
49
+ y: Number(item.y) || 0,
50
+ w: Math.max(1, Number(item.w) || 1),
51
+ h: Math.max(1, Number(item.h) || 1)
52
+ }
53
+ }
54
+
55
+ export function compact (layout, verticalCompact = true) {
56
+ const source = Array.isArray(layout) ? layout.filter(Boolean) : []
57
+ const out = new Array(source.length)
58
+ const indexOf = new Map()
59
+ const flow = []
60
+ source.forEach((item, idx) => {
61
+ const copy = cloneItem(item)
62
+ // 悬浮项不参与碰撞,坐标原样
63
+ if (copy.float) {
64
+ out[idx] = copy
65
+ return
66
+ }
67
+ indexOf.set(copy, idx)
68
+ flow.push(copy)
69
+ })
70
+ // 锁定项一开始就在比较集里,其它项绕开它们
71
+ const compareWith = flow.filter(item => item.static)
72
+ for (const item of sortLayoutItemsByRowCol(flow)) {
73
+ if (!item.static) {
74
+ compactItem(compareWith, item, verticalCompact)
75
+ // 只与排在前面的项碰撞;锁定项已在集合里
76
+ compareWith.push(item)
77
+ }
78
+ out[indexOf.get(item)] = item
79
+ }
80
+ return out
81
+ }
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <div class="vgl-item vgl-item--static" :style="style"><slot></slot></div>
3
+ </template>
4
+
5
+ <script>
6
+ // 垫片:只读 <grid-item>,位置由父 GridLayout 提供的几何参数算出;`.vgl-item` 类名来自已移除的 grid-layout-plus 1.1.1(见 GridLayout.vue)
7
+ import { itemRect, rectStyle } from '../geometry'
8
+
9
+ export default {
10
+ // 组件 name 走仓库命名规范(epx- 前缀);全局注册名仍用 'GridItem'(见 index.js)
11
+ name: 'epx-grid-compat-grid-item',
12
+ inheritAttrs: false,
13
+ props: {
14
+ x: { type: Number, default: 0 },
15
+ y: { type: Number, default: 0 },
16
+ w: { type: Number, default: 1 },
17
+ h: { type: Number, default: 1 },
18
+ i: { type: [String, Number], default: '' }
19
+ },
20
+ inject: { epxCompatGrid: { default: null } },
21
+ computed: {
22
+ style () {
23
+ if (!this.epxCompatGrid) return undefined
24
+ // 坐标优先取父 GridLayout 压紧后的映射(按 i),layout 里没有本项时回落自身 props
25
+ const pos = this.epxCompatGrid.positions[String(this.i)] || { x: this.x, y: this.y, w: this.w, h: this.h }
26
+ return { position: 'absolute', ...rectStyle(itemRect(pos, this.epxCompatGrid.resolved)) }
27
+ }
28
+ }
29
+ }
30
+ </script>
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <div ref="root" class="vgl-layout epx-dashboard-grid-compat" :style="{ height: height + 'px' }"><slot></slot></div>
3
+ </template>
4
+
5
+ <script>
6
+ // 垫片:给复刻过 Dashboard 模板的产品子类用的只读 <grid-layout>。
7
+ // 接受 vue-grid-layout 时代的 props,只做定位;拖拽 / 缩放类 props 收下但忽略(inheritAttrs:false 不落 DOM)。
8
+ // `.vgl-layout` / `.vgl-item` 沿用已移除的 grid-layout-plus 1.1.1 的 class 名:产品仓复刻模板的样式按这些类写,依赖删了、类名保留。
9
+ import { resolveGrid, layoutHeight } from '../geometry'
10
+ import { compact } from '../compact'
11
+
12
+ export default {
13
+ // 组件 name 走仓库命名规范(epx- 前缀);全局注册名仍用 'GridLayout'(见 index.js),
14
+ // 保证旧模板里直接写的 <grid-layout> 标签能解析到——标签解析走注册名,不走这里的 name
15
+ name: 'epx-grid-compat-grid-layout',
16
+ inheritAttrs: false,
17
+ props: {
18
+ layout: { type: Array, default: () => [] },
19
+ colNum: { type: Number, default: 12 },
20
+ rowHeight: { type: Number, default: 150 },
21
+ margin: { type: Array, default: () => [10, 10] },
22
+ // 显式宽度(测试 / 无 ResizeObserver 环境);缺省由 ResizeObserver 测量
23
+ width: { type: Number, default: 0 }
24
+ },
25
+ provide () {
26
+ return { epxCompatGrid: this }
27
+ },
28
+ data () {
29
+ return { measuredWidth: 0 }
30
+ },
31
+ computed: {
32
+ resolved () {
33
+ const gap = [Number(this.margin[0]) || 0, Number(this.margin[1]) || 0]
34
+ return resolveGrid({ colNum: this.colNum, rowHeight: this.rowHeight, gap, padding: null }, { containerWidth: this.width || this.measuredWidth })
35
+ },
36
+ // 与旧布局库一致:layout 先 compact(空隙上移、重叠推下),grid-item 按 i 取压紧后的坐标;不写回 layout
37
+ compacted () {
38
+ return compact(this.layout)
39
+ },
40
+ // i(缺省用 id)→ 压紧后的项,供子 grid-item 取坐标;键统一转字符串,避免数字 / 字符串 id 不等
41
+ positions () {
42
+ const map = {}
43
+ for (const item of this.compacted) {
44
+ const key = item.i != null ? item.i : item.id
45
+ if (key != null) map[String(key)] = item
46
+ }
47
+ return map
48
+ },
49
+ height () {
50
+ return layoutHeight(this.compacted, this.resolved)
51
+ }
52
+ },
53
+ methods: {
54
+ measure () {
55
+ if (this.$refs.root) this.measuredWidth = this.$refs.root.offsetWidth
56
+ }
57
+ },
58
+ mounted () {
59
+ if (this.width) return
60
+ this.measure()
61
+ if (typeof ResizeObserver !== 'undefined') {
62
+ this.resizeObserver = new ResizeObserver(() => this.measure())
63
+ this.resizeObserver.observe(this.$refs.root)
64
+ }
65
+ },
66
+ beforeUnmount () {
67
+ if (this.resizeObserver) this.resizeObserver.disconnect()
68
+ }
69
+ }
70
+ </script>
71
+
72
+ <style>
73
+ .vgl-layout.epx-dashboard-grid-compat { position: relative; width: 100%; }
74
+ /* 旧库的 grid-item 自带 box-sizing: border-box,复刻模板给 grid-item 加 border / padding 时盒子不撑大 */
75
+ .vgl-layout.epx-dashboard-grid-compat .vgl-item { box-sizing: border-box; }
76
+ </style>
@@ -0,0 +1,17 @@
1
+ // 设计态共享状态:当前选中项(跨页面级画布与容器内网格),由 DashboardDesign / DashboardMobile 创建并 provide('epxDashboardDesign')。
2
+ // owner 标记选中项属于哪一层('page' / 容器 id 等),由调用方约定;只存引用、不复制项。
3
+ import { reactive } from 'vue'
4
+
5
+ export function createDesignState () {
6
+ const state = reactive({ activeId: null, activeItem: null, activeOwner: null })
7
+ return {
8
+ state,
9
+ select (item, owner) {
10
+ state.activeId = item ? item.id : null
11
+ state.activeItem = item || null
12
+ state.activeOwner = owner || null
13
+ },
14
+ clear () { this.select(null, null) },
15
+ isActive (item) { return !!item && state.activeId === item.id }
16
+ }
17
+ }
@@ -0,0 +1,22 @@
1
+ // 设计态 gridstack 自定义引擎。GridStackEngine 是纯布局算法(无 DOM),只覆写碰撞相关的三个方法:
2
+ // _fixCollisions 先试 swap(同尺寸且相邻时交换)再走 push;collide / collideAll 是全部碰撞检测的入口。
3
+ // 接入方式(实例选项 engineClass 或 GridStack.registerEngine)由 Task 3 决定,本文件只定义子类。
4
+ import { GridStackEngine } from 'gridstack'
5
+
6
+ // 悬浮层:不做任何碰撞检测 → 项可自由叠放,拖动经过别的项对方也不动
7
+ export class NoCollideEngine extends GridStackEngine {
8
+ collide () { return undefined }
9
+ collideAll () { return [] }
10
+ // 基类在有 maxRow 时会用「基类引擎」克隆一份模拟移动(碰撞检测随之回来:覆盖过半的项被推挤、不足半的移动被拒),
11
+ // 无碰撞时不存在「推挤后超出 maxRow」的情形,直接走基类无 maxRow 的简单路径:moveNode 里的 nodeBoundFix 把节点夹在 maxRow 内即可
12
+ moveNodeCheck (node, o) {
13
+ if (!this.changedPosConstrain(node, o)) return false
14
+ o.pack = true
15
+ return this.moveNode(node, o)
16
+ }
17
+ }
18
+
19
+ // 碰撞层:与旧库(vue-grid-layout)一致「一律 push」,禁用 gridstack 的同尺寸 swap
20
+ export class PushOnlyEngine extends GridStackEngine {
21
+ swap () { return false }
22
+ }