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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/API.md +160 -160
  2. package/README.md +46 -46
  3. package/dist/src/page-designer/PageDesigner.js +11 -0
  4. package/dist/src/page-designer/designer.js +21 -0
  5. package/dist/src/page-designer/previews/add-form/Preview.js +1 -1
  6. package/dist/src/page-designer/previews/add-form/Preview.vue.js +19 -8
  7. package/dist/src/page-designer/setting-panel/Setting.js +46 -10
  8. package/dist/src/visual/config-page/ComponentLibraryDialog.js +8 -2
  9. package/dist/src/visual/config-page/DashboardMobile.js +1 -1
  10. package/dist/style.css +116 -112
  11. package/package.json +8 -8
  12. package/src/index.js +27 -27
  13. package/src/page-designer/PageDesigner.vue +11 -0
  14. package/src/page-designer/designer.js +1322 -1299
  15. package/src/page-designer/previews/add-form/Preview.vue +43 -44
  16. package/src/page-designer/setting-panel/Setting.vue +24 -2
  17. package/src/page-designer/widget-preview/Mixin.js +285 -285
  18. package/src/page-designer/widget-preview/Preview.vue +322 -322
  19. package/src/page-designer/widget-preview/WidgetWrapper.vue +197 -197
  20. package/src/page-designer/widget-preview/directive.js +258 -258
  21. package/src/visual/CategoryList.vue +563 -563
  22. package/src/visual/ComponentList.vue +280 -280
  23. package/src/visual/CreatePage.vue +389 -389
  24. package/src/visual/EntityCardList.vue +449 -449
  25. package/src/visual/EntityList.vue +838 -838
  26. package/src/visual/config-page/ComponentLibraryDialog.vue +7 -1
  27. package/src/visual/config-page/DashboardMobile.vue +934 -934
  28. package/src/visual/widgets/ProcessFormItemVisual.vue +58 -58
  29. package/types/index.d.ts +22 -22
  30. package/types/page-designer.d.ts +3 -3
  31. package/types/visual-component.d.ts +1 -1
  32. package/types/visual-entity-list.d.ts +1 -1
  33. package/types/visual-entity-sub-list.d.ts +1 -1
@@ -1,35 +1,35 @@
1
-
2
- <template>
3
- <div :class="['config-container', { 'config-container--editing': showConfig && !standalone, 'config-container--standalone': standalone }]">
4
- <div v-if="!standalone" class="page-header">
5
- <i class="page-header__back el-icon-arrow-left" @click="backPage"></i>
6
- <p class="page-title">
7
- <span class="page-header__title">移动端设计</span>
8
- </p>
9
- <div class="page-header__operate">
10
- <p class="header-operate__item" @click="generateFromPc"><i class="el-icon-magic-stick"></i>按PC布局生成</p>
11
- <p class="header-operate__item" @click="saveLayout"><i class="epx-icon-save"></i>保存移动端设计</p>
12
- </div>
13
- </div>
14
- <!-- 可滚动内容区:手机框 + 已隐藏归拢区一起纵向滚动(config-container 自身 overflow:hidden 保住机身圆角裁切) -->
15
- <div class="mobile-body">
16
- <!-- 手机画布:单层网格(无容器嵌套、无外部拖入),网格参数与运行态移动端同一取法 -->
17
- <div ref="canvas" :class="['epx-dashboard', themeScope, 'is-mobile-preview', 'dashboard-design__mobile', 'dashboard-design--'+ theme]" :style="{ ...dashboardStyle, ...(standalone ? { width: canvasWidth + 'px' } : {}) }" @click.capture="selectPreviewChart">
18
- <epx-dashboard-design-grid
1
+
2
+ <template>
3
+ <div :class="['config-container', { 'config-container--editing': showConfig && !standalone, 'config-container--standalone': standalone }]">
4
+ <div v-if="!standalone" class="page-header">
5
+ <i class="page-header__back el-icon-arrow-left" @click="backPage"></i>
6
+ <p class="page-title">
7
+ <span class="page-header__title">移动端设计</span>
8
+ </p>
9
+ <div class="page-header__operate">
10
+ <p class="header-operate__item" @click="generateFromPc"><i class="el-icon-magic-stick"></i>按PC布局生成</p>
11
+ <p class="header-operate__item" @click="saveLayout"><i class="epx-icon-save"></i>保存移动端设计</p>
12
+ </div>
13
+ </div>
14
+ <!-- 可滚动内容区:手机框 + 已隐藏归拢区一起纵向滚动(config-container 自身 overflow:hidden 保住机身圆角裁切) -->
15
+ <div class="mobile-body">
16
+ <!-- 手机画布:单层网格(无容器嵌套、无外部拖入),网格参数与运行态移动端同一取法 -->
17
+ <div ref="canvas" :class="['epx-dashboard', themeScope, 'is-mobile-preview', 'dashboard-design__mobile', 'dashboard-design--'+ theme]" :style="{ ...dashboardStyle, ...(standalone ? { width: canvasWidth + 'px' } : {}) }" @click.capture="selectPreviewChart">
18
+ <epx-dashboard-design-grid
19
19
  v-if="visibleLayout.length > 0 || standalone"
20
- :items="visibleLayout"
21
- :grid="gridParams"
22
- :body-height="bodyHeight"
20
+ :items="visibleLayout"
21
+ :grid="gridParams"
22
+ :body-height="bodyHeight"
23
23
  :min-height="canvasHeight"
24
24
  :show-grid-lines="!!globalConfigs.showGridLines"
25
25
  @transfer-out="onTransferOut"
26
26
  @transfer-in="onTransferIn"
27
- >
28
- <template #item="{ item }">
27
+ >
28
+ <template #item="{ item }">
29
29
  <!-- 在 PC 上设计手机尺寸看板,独立设计时使用与 PC 一致的组件操作条。 -->
30
30
  <epx-dashboard-design-item :item="item" :data-mobile-item-id="item.id" :active="design.isActive(item)" :floatable="standalone && gridParams.mode !== 'float'" @select="openConfig">
31
31
  <div v-if="item.contentType && !reloadComponent[item.id]" :class="['drag-component', { 'drag-component--container': standalone && isContainerType(item.contentType) }]">
32
- <!-- 与运行态 Dashboard.vue 同一套传参:整块保留的容器(dashboard-div / tabs)靠 items 拿子项 / 页签、comp-grid 拿内部网格参数,缺了页签就是空的 -->
32
+ <!-- 与运行态 Dashboard.vue 同一套传参:整块保留的容器(dashboard-div / tabs)靠 items 拿子项 / 页签、comp-grid 拿内部网格参数,缺了页签就是空的 -->
33
33
  <epx-component
34
34
  class="epx-dashboard__component"
35
35
  is-dashboard
@@ -37,32 +37,32 @@
37
37
  :mode="standalone ? 'design' : undefined"
38
38
  :comp-item="item"
39
39
  :all-component-list="allComponentList"
40
- :name="item.contentType"
41
- :comp-id="item.id"
42
- :comp-title="item.title"
43
- :items="previewItem(item).items"
44
- :item-configs="designQuery.itemConfigs"
45
- :comp-configs="getCompConfigs(item, item.id)"
46
- :comp-grid="containerGrid(item)"
47
- :comp-style="compStyle(item)"
48
- :theme="theme"
49
- :theme-chart-config="themeChartConfig"
40
+ :name="item.contentType"
41
+ :comp-id="item.id"
42
+ :comp-title="item.title"
43
+ :items="previewItem(item).items"
44
+ :item-configs="designQuery.itemConfigs"
45
+ :comp-configs="getCompConfigs(item, item.id)"
46
+ :comp-grid="containerGrid(item)"
47
+ :comp-style="compStyle(item)"
48
+ :theme="theme"
49
+ :theme-chart-config="themeChartConfig"
50
50
  :mobile-appearance="item.mobileAppearance"
51
51
  @open-config="openNestedConfig"
52
52
  @changeItems="v=>item.items=v"
53
53
  ></epx-component>
54
54
  <i v-if="!standalone" class="drag-component__edit el-icon-setting" title="编辑移动端显示/样式"></i>
55
- </div>
55
+ </div>
56
56
  <span v-else>{{item.i}}</span>
57
57
  <template v-if="standalone" #operates>
58
58
  <button v-if="['chart', 'report-chart', 'report-html'].includes(item.contentType)" class="mobile-item-action" type="button" title="编辑报表" aria-label="编辑报表" @click.stop="openConfig(item); $emit('edit-data', item)"><i class="el-icon-edit-outline"></i></button>
59
59
  <button class="mobile-item-action" type="button" title="组件设置" aria-label="组件设置" @click.stop="openConfig(item)"><i class="el-icon-setting"></i></button>
60
60
  <button class="mobile-item-action" type="button" title="删除组件" aria-label="删除组件" @click.stop="$emit('delete-item', item)"><i class="el-icon-delete"></i></button>
61
61
  </template>
62
- </epx-dashboard-design-item>
63
- </template>
64
- </epx-dashboard-design-grid>
65
- <!-- 空态:完全无布局 → 引导生成;有布局但全部隐藏 → 提示去下方归拢区恢复 -->
62
+ </epx-dashboard-design-item>
63
+ </template>
64
+ </epx-dashboard-design-grid>
65
+ <!-- 空态:完全无布局 → 引导生成;有布局但全部隐藏 → 提示去下方归拢区恢复 -->
66
66
  <div v-if="!visibleLayout.length" class="mobile-empty">
67
67
  <div v-if="!layout.length && standalone" class="mobile-empty__actions">
68
68
  <button class="mobile-empty__action mobile-empty__add-report" type="button" @click="$emit('add-report')">
@@ -78,29 +78,29 @@
78
78
  <p class="mobile-empty__text">{{ layout.length ? '组件已全部隐藏' : '移动端布局为空' }}</p>
79
79
  <el-button v-if="!layout.length && !standalone" type="primary" size="small" icon="el-icon-magic-stick" @click="generateFromPc">按 PC 布局生成</el-button>
80
80
  </template>
81
- </div>
82
- </div>
83
- <!-- 已隐藏组件归拢区:隐藏项不占画布,收在这里,点一下恢复到画布 -->
84
- <div v-if="hiddenItems.length" class="mobile-hidden-tray">
85
- <div class="mobile-hidden-tray__title"><i class="el-icon-folder-opened"></i>已隐藏组件({{ hiddenItems.length }})</div>
86
- <div class="mobile-hidden-tray__list">
87
- <div v-for="item in hiddenItems" :key="item.id" class="mobile-hidden-tray__item" @click="setHidden(item, false)">
88
- <span class="mobile-hidden-tray__name">{{ item.title || item.contentType || item.i }}</span>
89
- <i class="mobile-hidden-tray__restore el-icon-refresh-left" title="恢复显示"></i>
90
- </div>
91
- </div>
92
- </div>
93
- <!-- 容器策略:PC 里的容器默认拆开成子组件参与堆叠;打开「保留整块」则整块独占一行(写在 PC 容器项 mobileExpand 上、随页面配置自动保存),下次「按 PC 布局生成」生效 -->
94
- <div v-if="!standalone && pcContainers.length" class="mobile-container-tray">
95
- <div class="mobile-container-tray__title"><i class="el-icon-s-grid"></i>容器策略({{ pcContainers.length }})<span class="mobile-container-tray__tip">改后点「按PC布局生成」生效</span></div>
96
- <div v-for="c in pcContainers" :key="c.id" class="mobile-container-tray__row">
97
- <span class="mobile-container-tray__name">{{ c.title || c.contentType }}</span>
98
- <span class="mobile-container-tray__mode">{{ c.mobileExpand === false ? '保留整块' : '拆开堆叠' }}</span>
99
- <el-switch :model-value="c.mobileExpand === false" size="small" @change="v=>setContainerKeep(c, v)"></el-switch>
100
- </div>
101
- </div>
102
- </div>
103
- <!-- 移动端显示/样式编辑:改动直接写在该组件的 mItems 项上 -->
81
+ </div>
82
+ </div>
83
+ <!-- 已隐藏组件归拢区:隐藏项不占画布,收在这里,点一下恢复到画布 -->
84
+ <div v-if="hiddenItems.length" class="mobile-hidden-tray">
85
+ <div class="mobile-hidden-tray__title"><i class="el-icon-folder-opened"></i>已隐藏组件({{ hiddenItems.length }})</div>
86
+ <div class="mobile-hidden-tray__list">
87
+ <div v-for="item in hiddenItems" :key="item.id" class="mobile-hidden-tray__item" @click="setHidden(item, false)">
88
+ <span class="mobile-hidden-tray__name">{{ item.title || item.contentType || item.i }}</span>
89
+ <i class="mobile-hidden-tray__restore el-icon-refresh-left" title="恢复显示"></i>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ <!-- 容器策略:PC 里的容器默认拆开成子组件参与堆叠;打开「保留整块」则整块独占一行(写在 PC 容器项 mobileExpand 上、随页面配置自动保存),下次「按 PC 布局生成」生效 -->
94
+ <div v-if="!standalone && pcContainers.length" class="mobile-container-tray">
95
+ <div class="mobile-container-tray__title"><i class="el-icon-s-grid"></i>容器策略({{ pcContainers.length }})<span class="mobile-container-tray__tip">改后点「按PC布局生成」生效</span></div>
96
+ <div v-for="c in pcContainers" :key="c.id" class="mobile-container-tray__row">
97
+ <span class="mobile-container-tray__name">{{ c.title || c.contentType }}</span>
98
+ <span class="mobile-container-tray__mode">{{ c.mobileExpand === false ? '保留整块' : '拆开堆叠' }}</span>
99
+ <el-switch :model-value="c.mobileExpand === false" size="small" @change="v=>setContainerKeep(c, v)"></el-switch>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ <!-- 移动端显示/样式编辑:改动直接写在该组件的 mItems 项上 -->
104
104
  <aside v-if="standalone" class="mobile-properties">
105
105
  <div class="mobile-config-header mobile-properties__header"><strong>组件配置</strong></div>
106
106
  <template v-if="selectComponent">
@@ -122,294 +122,294 @@
122
122
  <div v-else class="mobile-config mobile-config__hint">选择组件,配置属性、样式和布局。</div>
123
123
  </aside>
124
124
  <el-drawer v-else title="移动端外观" class="mobile-appearance-drawer" append-to-body direction="rtl" size="min(400px, 44vw)" :modal="false" modal-penetrable :lock-scroll="false" :close-on-click-modal="false" v-model="showConfig">
125
- <template #header>
126
- <div class="mobile-config-header">
127
- <strong>移动端外观</strong>
128
- <p>在左侧滚动、点击组件即可切换设置</p>
129
- </div>
130
- </template>
131
- <div class="mobile-config" v-if="selectComponent">
132
- <div class="mobile-config__title">{{componentLabel(selectComponent)}} <span class="mobile-config__type">{{ componentTypeLabel }}</span></div>
125
+ <template #header>
126
+ <div class="mobile-config-header">
127
+ <strong>移动端外观</strong>
128
+ <p>在左侧滚动、点击组件即可切换设置</p>
129
+ </div>
130
+ </template>
131
+ <div class="mobile-config" v-if="selectComponent">
132
+ <div class="mobile-config__title">{{componentLabel(selectComponent)}} <span class="mobile-config__type">{{ componentTypeLabel }}</span></div>
133
133
  <p class="mobile-config__hint">仅影响手机端;未设置的属性继承 PC,数据与权限共用。</p>
134
134
  <div class="mobile-config__group-title">布局与显示</div>
135
- <div class="mobile-config__row">
136
- <span>移动端显示</span>
137
- <el-switch :model-value="!selectComponent.mobileHidden" @change="v=>setHidden(selectComponent, !v)"></el-switch>
138
- </div>
139
- <template v-if="!selectComponent.mobileHidden">
140
- <!-- 列宽快捷:整行 / 半宽 / 三分之一 -->
141
- <div class="mobile-config__row">
142
- <span>列宽</span>
143
- <el-radio-group :model-value="spanLevel(selectComponent)" size="small" @change="v=>setSpan(selectComponent, v)">
144
- <el-radio-button label="full">整行</el-radio-button>
145
- <el-radio-button label="half">半宽</el-radio-button>
146
- <el-radio-button label="third">三分之一</el-radio-button>
147
- </el-radio-group>
148
- </div>
149
- <!-- 高度快捷(网格行数) -->
150
- <div class="mobile-config__row">
151
- <span>高度(行)</span>
152
- <el-input-number :model-value="selectComponent.h" :min="2" :max="200" size="small" controls-position="right" @change="v=>setMHeight(selectComponent, v)"></el-input-number>
153
- </div>
154
- <!-- 排序快捷键:置顶 / 上移 / 下移 / 置底 -->
155
- <div class="mobile-config__row">
156
- <span>位置</span>
157
- <el-button-group>
158
- <el-button size="small" icon="el-icon-upload2" title="置顶" @click="reorder('top')"></el-button>
159
- <el-button size="small" icon="el-icon-top" title="上移" @click="reorder('up')"></el-button>
160
- <el-button size="small" icon="el-icon-bottom" title="下移" @click="reorder('down')"></el-button>
161
- <el-button size="small" icon="el-icon-download" title="置底" @click="reorder('bottom')"></el-button>
162
- </el-button-group>
163
- </div>
164
- <template v-if="appearanceTargets.length">
165
- <div class="mobile-config__group-title">移动端图表外观</div>
166
- <el-select v-if="appearanceTargets.length > 1 || appearanceTarget !== selectComponent" :model-value="appearanceTarget.id" size="small" @change="v=>appearanceTargetId=v">
167
- <el-option v-for="target in appearanceTargets" :key="target.id" :label="componentLabel(target)" :value="target.id"></el-option>
168
- </el-select>
169
- <p v-if="appearanceTarget !== selectComponent" class="mobile-config__chart-type">{{ appearanceTypeLabel }}</p>
170
- <p v-if="!appearanceFields.length" class="mobile-config__hint">{{ appearanceChartType ? '当前类型可通过下方卡片样式调整外观。' : '正在读取图表类型…' }}</p>
171
- <template v-for="group in appearanceGroups" :key="group.label">
172
- <div class="mobile-config__section-title">{{ group.label }}</div>
173
- <div v-for="field in group.fields" :key="field.key" class="mobile-appearance" :data-appearance="field.key">
174
- <div class="mobile-appearance__heading">
175
- <span>{{ field.label }}</span>
176
- <span :class="['mobile-appearance__status', { 'is-independent': hasAppearance(field.key) }]">{{ hasAppearance(field.key) ? '移动端独立' : (standalone ? '默认样式' : '继承 PC') }}</span>
135
+ <div class="mobile-config__row">
136
+ <span>移动端显示</span>
137
+ <el-switch :model-value="!selectComponent.mobileHidden" @change="v=>setHidden(selectComponent, !v)"></el-switch>
138
+ </div>
139
+ <template v-if="!selectComponent.mobileHidden">
140
+ <!-- 列宽快捷:整行 / 半宽 / 三分之一 -->
141
+ <div class="mobile-config__row">
142
+ <span>列宽</span>
143
+ <el-radio-group :model-value="spanLevel(selectComponent)" size="small" @change="v=>setSpan(selectComponent, v)">
144
+ <el-radio-button label="full">整行</el-radio-button>
145
+ <el-radio-button label="half">半宽</el-radio-button>
146
+ <el-radio-button label="third">三分之一</el-radio-button>
147
+ </el-radio-group>
148
+ </div>
149
+ <!-- 高度快捷(网格行数) -->
150
+ <div class="mobile-config__row">
151
+ <span>高度(行)</span>
152
+ <el-input-number :model-value="selectComponent.h" :min="2" :max="200" size="small" controls-position="right" @change="v=>setMHeight(selectComponent, v)"></el-input-number>
153
+ </div>
154
+ <!-- 排序快捷键:置顶 / 上移 / 下移 / 置底 -->
155
+ <div class="mobile-config__row">
156
+ <span>位置</span>
157
+ <el-button-group>
158
+ <el-button size="small" icon="el-icon-upload2" title="置顶" @click="reorder('top')"></el-button>
159
+ <el-button size="small" icon="el-icon-top" title="上移" @click="reorder('up')"></el-button>
160
+ <el-button size="small" icon="el-icon-bottom" title="下移" @click="reorder('down')"></el-button>
161
+ <el-button size="small" icon="el-icon-download" title="置底" @click="reorder('bottom')"></el-button>
162
+ </el-button-group>
163
+ </div>
164
+ <template v-if="appearanceTargets.length">
165
+ <div class="mobile-config__group-title">移动端图表外观</div>
166
+ <el-select v-if="appearanceTargets.length > 1 || appearanceTarget !== selectComponent" :model-value="appearanceTarget.id" size="small" @change="v=>appearanceTargetId=v">
167
+ <el-option v-for="target in appearanceTargets" :key="target.id" :label="componentLabel(target)" :value="target.id"></el-option>
168
+ </el-select>
169
+ <p v-if="appearanceTarget !== selectComponent" class="mobile-config__chart-type">{{ appearanceTypeLabel }}</p>
170
+ <p v-if="!appearanceFields.length" class="mobile-config__hint">{{ appearanceChartType ? '当前类型可通过下方卡片样式调整外观。' : '正在读取图表类型…' }}</p>
171
+ <template v-for="group in appearanceGroups" :key="group.label">
172
+ <div class="mobile-config__section-title">{{ group.label }}</div>
173
+ <div v-for="field in group.fields" :key="field.key" class="mobile-appearance" :data-appearance="field.key">
174
+ <div class="mobile-appearance__heading">
175
+ <span>{{ field.label }}</span>
176
+ <span :class="['mobile-appearance__status', { 'is-independent': hasAppearance(field.key) }]">{{ hasAppearance(field.key) ? '移动端独立' : (standalone ? '默认样式' : '继承 PC') }}</span>
177
177
  <el-button v-if="hasAppearance(field.key)" size="small" link @click="setAppearance(field.key, undefined)">{{ standalone ? '恢复默认' : '恢复继承' }}</el-button>
178
- </div>
179
- <el-select v-if="field.options" :model-value="appearanceTarget.mobileAppearance?.[field.key]" size="small" clearable :placeholder="standalone ? '默认样式' : '继承 PC'" @change="v=>setAppearance(field.key, v)">
180
- <el-option v-for="option in field.options" :key="String(option.value)" :label="option.label" :value="option.value"></el-option>
181
- </el-select>
182
- <el-input-number v-else :model-value="appearanceTarget.mobileAppearance?.[field.key]" :min="field.min" :max="field.max" size="small" controls-position="right" :placeholder="standalone ? '默认样式' : '继承 PC'" @change="v=>setAppearance(field.key, v)"></el-input-number>
183
- </div>
184
- </template>
185
- <el-button v-if="appearanceFields.length" size="small" @click="restoreAppearance">图表全部恢复继承</el-button>
186
- </template>
187
- <details :key="selectComponent.id" class="mobile-card-style" :open="!appearanceFields.length">
188
- <summary>卡片样式 <span class="mobile-appearance__status">{{ standalone ? '未设置的属性使用默认样式' : '未设置的属性继承 PC' }}</span></summary>
189
- <div v-for="(value, key) in selectComponent.style || {}" :key="key" class="mobile-appearance__heading">
190
- <span>{{ styleLabel(key) }}:{{ value }}</span>
191
- <span class="mobile-appearance__status is-independent">移动端独立</span>
192
- <el-button size="small" link @click="restoreStyle(key)">恢复继承</el-button>
193
- </div>
194
- <epx-component-style-config
195
- :key="'mstyle-'+selectComponent.id"
196
- :value="selectComponent.style || {}"
197
- @change="v=>onStyleChange(selectComponent, v)"
198
- ></epx-component-style-config>
178
+ </div>
179
+ <el-select v-if="field.options" :model-value="appearanceTarget.mobileAppearance?.[field.key]" size="small" clearable :placeholder="standalone ? '默认样式' : '继承 PC'" @change="v=>setAppearance(field.key, v)">
180
+ <el-option v-for="option in field.options" :key="String(option.value)" :label="option.label" :value="option.value"></el-option>
181
+ </el-select>
182
+ <el-input-number v-else :model-value="appearanceTarget.mobileAppearance?.[field.key]" :min="field.min" :max="field.max" size="small" controls-position="right" :placeholder="standalone ? '默认样式' : '继承 PC'" @change="v=>setAppearance(field.key, v)"></el-input-number>
183
+ </div>
184
+ </template>
185
+ <el-button v-if="appearanceFields.length" size="small" @click="restoreAppearance">图表全部恢复继承</el-button>
186
+ </template>
187
+ <details :key="selectComponent.id" class="mobile-card-style" :open="!appearanceFields.length">
188
+ <summary>卡片样式 <span class="mobile-appearance__status">{{ standalone ? '未设置的属性使用默认样式' : '未设置的属性继承 PC' }}</span></summary>
189
+ <div v-for="(value, key) in selectComponent.style || {}" :key="key" class="mobile-appearance__heading">
190
+ <span>{{ styleLabel(key) }}:{{ value }}</span>
191
+ <span class="mobile-appearance__status is-independent">移动端独立</span>
192
+ <el-button size="small" link @click="restoreStyle(key)">恢复继承</el-button>
193
+ </div>
194
+ <epx-component-style-config
195
+ :key="'mstyle-'+selectComponent.id"
196
+ :value="selectComponent.style || {}"
197
+ @change="v=>onStyleChange(selectComponent, v)"
198
+ ></epx-component-style-config>
199
199
  </details>
200
200
  </template>
201
201
  </div>
202
202
  </el-drawer>
203
- <!-- 覆盖确认:MessageBox 默认样式与页面设计语言不统一,换页内轻确认(图标 + 文案 + 双按钮),复用全局 dialog--noheader 隐藏标题栏 -->
204
- <el-dialog v-model="confirmRegenerate" width="340px" append-to-body :show-close="false" :close-on-click-modal="false" class="dialog--noheader">
205
- <div class="mobile-confirm">
206
- <i class="el-icon-warning-outline mobile-confirm__icon"></i>
207
- <p class="mobile-confirm__text">将按 PC 布局重新生成手机端位置和尺寸,覆盖当前移动端布局。保留移动端图表外观、卡片样式和隐藏设置,是否继续?</p>
208
- <div class="mobile-confirm__actions">
209
- <el-button size="small" @click="confirmRegenerate = false">取消</el-button>
210
- <el-button size="small" type="primary" @click="regenerateFromConfirm">生成</el-button>
211
- </div>
212
- </div>
213
- </el-dialog>
214
- </div>
215
- </template>
216
-
217
- <script>
218
- import { defineAsyncComponent } from 'vue'
219
- import { getChartType } from '@epxing/utils/src/chart-registry'
220
- import { stackMobileLayout, reorderStack, normalizeGridItem } from '@epxing/dashboard/src/layout-util'
221
- import { getPageGrid, getContainerGrid, isLegacyConfig, MOBILE_GRID, isContainerType } from '@epxing/dashboard/src/grid/model'
222
- import { createDesignState } from '@epxing/dashboard/src/grid/designState'
223
- import { buildThemeCss, buildThemeChartConfig } from '@epxing/dashboard/src/theme-style'
224
- import { indexDashboardItems, resolveMobileItem, regenerateMobileDesign } from '@epxing/dashboard/src/mobile-design'
225
- import { getMobileAppearanceFields, normalizeMobileAppearance } from '@epxing/utils/src/mobile-chart-appearance'
226
- import DesignItem from '@epxing/dashboard/src/DesignItem.vue'
203
+ <!-- 覆盖确认:MessageBox 默认样式与页面设计语言不统一,换页内轻确认(图标 + 文案 + 双按钮),复用全局 dialog--noheader 隐藏标题栏 -->
204
+ <el-dialog v-model="confirmRegenerate" width="340px" append-to-body :show-close="false" :close-on-click-modal="false" class="dialog--noheader">
205
+ <div class="mobile-confirm">
206
+ <i class="el-icon-warning-outline mobile-confirm__icon"></i>
207
+ <p class="mobile-confirm__text">将按 PC 布局重新生成手机端位置和尺寸,覆盖当前移动端布局。保留移动端图表外观、卡片样式和隐藏设置,是否继续?</p>
208
+ <div class="mobile-confirm__actions">
209
+ <el-button size="small" @click="confirmRegenerate = false">取消</el-button>
210
+ <el-button size="small" type="primary" @click="regenerateFromConfirm">生成</el-button>
211
+ </div>
212
+ </div>
213
+ </el-dialog>
214
+ </div>
215
+ </template>
216
+
217
+ <script>
218
+ import { defineAsyncComponent } from 'vue'
219
+ import { getChartType } from '@epxing/utils/src/chart-registry'
220
+ import { stackMobileLayout, reorderStack, normalizeGridItem } from '@epxing/dashboard/src/layout-util'
221
+ import { getPageGrid, getContainerGrid, isLegacyConfig, MOBILE_GRID, isContainerType } from '@epxing/dashboard/src/grid/model'
222
+ import { createDesignState } from '@epxing/dashboard/src/grid/designState'
223
+ import { buildThemeCss, buildThemeChartConfig } from '@epxing/dashboard/src/theme-style'
224
+ import { indexDashboardItems, resolveMobileItem, regenerateMobileDesign } from '@epxing/dashboard/src/mobile-design'
225
+ import { getMobileAppearanceFields, normalizeMobileAppearance } from '@epxing/utils/src/mobile-chart-appearance'
226
+ import DesignItem from '@epxing/dashboard/src/DesignItem.vue'
227
227
  import StyleConfig from './StyleConfig.vue'
228
228
  export default {
229
229
  name: 'epx-visual-config-dashboard-mobile',
230
- components: {
231
- 'epx-component-style-config': StyleConfig,
232
- // 设计态画布(gridstack 封装)按需加载,与页面级画布 DashboardDesign 同一组件;移动端只用单层、不接跨网格事件
233
- 'epx-dashboard-design-grid': defineAsyncComponent(() => import('@epxing/dashboard/src/DesignGrid.vue')),
234
- 'epx-dashboard-design-item': DesignItem
235
- },
236
- props: {
237
- standalone: Boolean,
238
- canvasWidth: { type: Number, default: 375 },
239
- source: { type: Object, default: null },
240
- pageMetaId: String,
241
- pageId: String,
242
- allComponentList: {
243
- type: Array,
244
- default () {
245
- return []
246
- }
247
- },
248
- componentList: {
249
- type: Array,
250
- default () {
251
- return []
252
- }
253
- }
254
- },
255
- provide () {
256
- return {
257
- upPage: this,
258
- pageMetaId: this.pageMetaId,
259
- pageId: this.pageId,
260
- // 设计态共享选中态:画布按它给选中项的壳置顶
261
- epxDashboardDesign: this.design,
262
- // 手机框预览标记:本设计器跑在 PC 端($isMobile 为 false),保留整块的容器(dashboard-div / tabs)内部要和真机一样按堆叠渲染(containerGridMixin 注入)
263
- epxMobilePreview: true,
264
- getMobileDashboard: () => this.standalone,
265
- getThemeChartConfig: () => this.themeChartConfig,
266
- registerMobileChartLabel: (id, label, type) => {
267
- if (label) this.chartLabels[id] = label
268
- if (type) this.chartTypes[id] = type
269
- }
270
- }
271
- },
272
- data () {
273
- return {
274
- reloadComponent: {},
275
- oldItemConfigs: {},
276
- layout: [],
277
- // 选中态(activeComponent 由它派生),provide 给画布
278
- design: createDesignState(),
279
- // 'auto' 行高(旧 autoHeight)的换算基准:document.body 高度
280
- bodyHeight: 0,
281
- // 手机画布可视高度:根网格按它撑满画布
282
- canvasHeight: 0,
230
+ components: {
231
+ 'epx-component-style-config': StyleConfig,
232
+ // 设计态画布(gridstack 封装)按需加载,与页面级画布 DashboardDesign 同一组件;移动端只用单层、不接跨网格事件
233
+ 'epx-dashboard-design-grid': defineAsyncComponent(() => import('@epxing/dashboard/src/DesignGrid.vue')),
234
+ 'epx-dashboard-design-item': DesignItem
235
+ },
236
+ props: {
237
+ standalone: Boolean,
238
+ canvasWidth: { type: Number, default: 375 },
239
+ source: { type: Object, default: null },
240
+ pageMetaId: String,
241
+ pageId: String,
242
+ allComponentList: {
243
+ type: Array,
244
+ default () {
245
+ return []
246
+ }
247
+ },
248
+ componentList: {
249
+ type: Array,
250
+ default () {
251
+ return []
252
+ }
253
+ }
254
+ },
255
+ provide () {
256
+ return {
257
+ upPage: this,
258
+ pageMetaId: this.pageMetaId,
259
+ pageId: this.pageId,
260
+ // 设计态共享选中态:画布按它给选中项的壳置顶
261
+ epxDashboardDesign: this.design,
262
+ // 手机框预览标记:本设计器跑在 PC 端($isMobile 为 false),保留整块的容器(dashboard-div / tabs)内部要和真机一样按堆叠渲染(containerGridMixin 注入)
263
+ epxMobilePreview: true,
264
+ getMobileDashboard: () => this.standalone,
265
+ getThemeChartConfig: () => this.themeChartConfig,
266
+ registerMobileChartLabel: (id, label, type) => {
267
+ if (label) this.chartLabels[id] = label
268
+ if (type) this.chartTypes[id] = type
269
+ }
270
+ }
271
+ },
272
+ data () {
273
+ return {
274
+ reloadComponent: {},
275
+ oldItemConfigs: {},
276
+ layout: [],
277
+ // 选中态(activeComponent 由它派生),provide 给画布
278
+ design: createDesignState(),
279
+ // 'auto' 行高(旧 autoHeight)的换算基准:document.body 高度
280
+ bodyHeight: 0,
281
+ // 手机画布可视高度:根网格按它撑满画布
282
+ canvasHeight: 0,
283
283
  selectComponent: null, // 当前编辑移动端设置的组件(mItems 项的引用)
284
- appearanceTargetId: null,
285
- chartLabels: {},
286
- chartTypes: {},
287
- confirmRegenerate: false // 「按PC布局生成」覆盖确认弹窗
288
- }
289
- },
290
- computed: {
291
- designQuery () {
292
- return this.source || this.$query
293
- },
294
- pcItemIndex () {
295
- return indexDashboardItems(this.designQuery.componentList || [])
296
- },
297
- previewItem () {
298
- return item => this.standalone ? item : resolveMobileItem(item, this.pcItemIndex)
299
- },
300
- appearanceTargets () {
301
- return [...indexDashboardItems(this.selectComponent ? [this.selectComponent] : []).values()]
302
- .filter(item => item.contentType === 'chart' || item.contentType === 'report-chart')
303
- },
304
- appearanceTarget () {
305
- return this.appearanceTargets.find(item => item.id === this.appearanceTargetId) || this.appearanceTargets[0]
306
- },
307
- appearanceFields () {
308
- return getMobileAppearanceFields(this.appearanceChartType)
309
- },
310
- appearanceChartType () {
311
- const target = this.appearanceTarget
312
- return target ? (this.chartTypes[target.id] || this.getCompConfigs(target).type || '') : ''
313
- },
314
- appearanceTypeLabel () {
315
- return getChartType(this.appearanceChartType)?.text || '图表'
316
- },
317
- componentTypeLabel () {
318
- if (this.appearanceTarget === this.selectComponent) return this.appearanceTypeLabel
319
- const labels = { 'dashboard-tabs': '标签页', 'dashboard-div': '容器', 'num-statistic': '数字卡', 'card-num': '数字卡', text: '文本', 'dashboard-title': '标题' }
320
- return labels[this.selectComponent?.contentType] || '组件'
321
- },
322
- appearanceGroups () {
323
- const sections = [
324
- { label: '标题', keys: ['showTitle', 'titleFontSize'] },
325
- { label: '图例', keys: ['legendPosition', 'legendFontSize'] },
326
- { label: '坐标轴', keys: ['axisFontSize', 'axisLabelRotate'] },
327
- { label: '绘图区', keys: ['gridTop', 'gridBottom', 'gridLeft', 'gridRight'] },
328
- { label: '数据标签', keys: ['showLabel', 'labelFontSize'] },
329
- { label: '饼图', keys: ['pieRadius'] }
330
- ]
331
- return sections.map(section => ({ label: section.label, fields: this.appearanceFields.filter(field => section.keys.includes(field.key)) })).filter(group => group.fields.length)
332
- },
333
- showConfig: {
334
- get () {
335
- return !!this.selectComponent
336
- },
337
- set (v) {
338
- if (!v) this.selectComponent = null
339
- }
340
- },
341
- getCompConfigs () {
342
- return (component) => {
343
- // 如果是页面 查找页面默认配置
344
- const { configs = {}, id, title, contentType, ...otherConfigs } = this.previewItem(component)
345
- const { itemConfigs = {} } = this.designQuery
346
- const itemConfig = itemConfigs[id] || {}
347
- const _configs = { ...otherConfigs, ...configs, ...itemConfig }
348
- if (title) {
349
- _configs.componentName = title
350
- }
351
- return _configs
352
- }
353
- },
354
- theme () {
355
- return this.designQuery.theme
356
- },
357
- // 使用独立作用域,手机框主题覆盖不影响仍挂载的 PC 画布。
358
- themeScope () {
359
- return `epx-dashboard-mobile-${this.pageId || this.designQuery.id || this.$.uid}`
360
- },
361
- themeStyles () {
362
- return this.designQuery.themeStyles || this.globalConfigs.themeStyles || {}
363
- },
364
- themeChartConfig () {
365
- return buildThemeChartConfig(this.themeStyles, true)
366
- },
367
- dashboardStyle () {
368
- const { backgroundColor, backgroundImage } = this.globalConfigs
369
- return {
370
- backgroundColor,
371
- backgroundImage: backgroundImage ? `url(${this.$util.getFileUrl(backgroundImage)})` : null
372
- }
373
- },
374
- // 页面全局配置(外壳 switchMobile 传入):网格与背景均沿用页面配置。
375
- globalConfigs () {
376
- return this.designQuery.globalConfigs || {}
377
- },
378
- // 与运行态 pageGridMixin 的移动端分支同一取法:行高沿用页面、间距换成堆叠参数 [8,8]、内边距跟随间距,几何才能一致
379
- gridParams () {
380
- return { ...getPageGrid(this.globalConfigs), gap: MOBILE_GRID.gap, padding: null }
381
- },
382
- // 行高按视口等比(旧 autoHeight)时才需要随窗口维护 bodyHeight(间距已固定,不会是 'auto')
383
- autoGrid () {
384
- return this.gridParams.rowHeight === 'auto'
385
- },
386
- // 整块保留的容器项的内部网格参数(与运行态 pageGridMixin.containerGrid 同一取法);非容器返回 undefined
387
- containerGrid () {
388
- return item => (isContainerType(item.contentType)
389
- ? getContainerGrid(item, this.gridParams, isLegacyConfig(this.globalConfigs))
390
- : undefined)
391
- },
392
- activeComponent () {
393
- return this.design.state.activeItem
394
- },
395
- // 移动端列数(与运行态移动端网格一致):列宽档位 整行 = colNum / 半宽 = colNum/2 / 三分之一 = colNum/3
396
- colNum () {
397
- return this.gridParams.colNum || 48
398
- },
399
- // 画布只渲染「未隐藏」的项:隐藏项移出网格、不再半透明占位,改由下方「已隐藏」区归拢
400
- visibleLayout () {
401
- return this.layout.filter(i => i && !i.mobileHidden)
402
- },
403
- // 已隐藏组件:归拢到画布下方,一键恢复
404
- hiddenItems () {
405
- return this.layout.filter(i => i && i.mobileHidden)
406
- },
407
- // PC 布局里的顶层容器(dashboard-div / dashboard-tabs):每个可单独设「移动端保留整块」。
408
- // 取的是外壳 selectComponentList 的引用(goPage params 内存传递),开关直接写在容器项上,外壳 deep watch 自动保存
409
- pcContainers () {
410
- return (this.designQuery.componentList || []).filter(i => i && isContainerType(i.contentType))
411
- }
412
- },
284
+ appearanceTargetId: null,
285
+ chartLabels: {},
286
+ chartTypes: {},
287
+ confirmRegenerate: false // 「按PC布局生成」覆盖确认弹窗
288
+ }
289
+ },
290
+ computed: {
291
+ designQuery () {
292
+ return this.source || this.$query
293
+ },
294
+ pcItemIndex () {
295
+ return indexDashboardItems(this.designQuery.componentList || [])
296
+ },
297
+ previewItem () {
298
+ return item => this.standalone ? item : resolveMobileItem(item, this.pcItemIndex)
299
+ },
300
+ appearanceTargets () {
301
+ return [...indexDashboardItems(this.selectComponent ? [this.selectComponent] : []).values()]
302
+ .filter(item => item.contentType === 'chart' || item.contentType === 'report-chart')
303
+ },
304
+ appearanceTarget () {
305
+ return this.appearanceTargets.find(item => item.id === this.appearanceTargetId) || this.appearanceTargets[0]
306
+ },
307
+ appearanceFields () {
308
+ return getMobileAppearanceFields(this.appearanceChartType)
309
+ },
310
+ appearanceChartType () {
311
+ const target = this.appearanceTarget
312
+ return target ? (this.chartTypes[target.id] || this.getCompConfigs(target).type || '') : ''
313
+ },
314
+ appearanceTypeLabel () {
315
+ return getChartType(this.appearanceChartType)?.text || '图表'
316
+ },
317
+ componentTypeLabel () {
318
+ if (this.appearanceTarget === this.selectComponent) return this.appearanceTypeLabel
319
+ const labels = { 'dashboard-tabs': '标签页', 'dashboard-div': '容器', 'num-statistic': '数字卡', 'card-num': '数字卡', text: '文本', 'dashboard-title': '标题' }
320
+ return labels[this.selectComponent?.contentType] || '组件'
321
+ },
322
+ appearanceGroups () {
323
+ const sections = [
324
+ { label: '标题', keys: ['showTitle', 'titleFontSize'] },
325
+ { label: '图例', keys: ['legendPosition', 'legendFontSize'] },
326
+ { label: '坐标轴', keys: ['axisFontSize', 'axisLabelRotate'] },
327
+ { label: '绘图区', keys: ['gridTop', 'gridBottom', 'gridLeft', 'gridRight'] },
328
+ { label: '数据标签', keys: ['showLabel', 'labelFontSize'] },
329
+ { label: '饼图', keys: ['pieRadius'] }
330
+ ]
331
+ return sections.map(section => ({ label: section.label, fields: this.appearanceFields.filter(field => section.keys.includes(field.key)) })).filter(group => group.fields.length)
332
+ },
333
+ showConfig: {
334
+ get () {
335
+ return !!this.selectComponent
336
+ },
337
+ set (v) {
338
+ if (!v) this.selectComponent = null
339
+ }
340
+ },
341
+ getCompConfigs () {
342
+ return (component) => {
343
+ // 如果是页面 查找页面默认配置
344
+ const { configs = {}, id, title, contentType, ...otherConfigs } = this.previewItem(component)
345
+ const { itemConfigs = {} } = this.designQuery
346
+ const itemConfig = itemConfigs[id] || {}
347
+ const _configs = { ...otherConfigs, ...configs, ...itemConfig }
348
+ if (title) {
349
+ _configs.componentName = title
350
+ }
351
+ return _configs
352
+ }
353
+ },
354
+ theme () {
355
+ return this.designQuery.theme
356
+ },
357
+ // 使用独立作用域,手机框主题覆盖不影响仍挂载的 PC 画布。
358
+ themeScope () {
359
+ return `epx-dashboard-mobile-${this.pageId || this.designQuery.id || this.$.uid}`
360
+ },
361
+ themeStyles () {
362
+ return this.designQuery.themeStyles || this.globalConfigs.themeStyles || {}
363
+ },
364
+ themeChartConfig () {
365
+ return buildThemeChartConfig(this.themeStyles, true)
366
+ },
367
+ dashboardStyle () {
368
+ const { backgroundColor, backgroundImage } = this.globalConfigs
369
+ return {
370
+ backgroundColor,
371
+ backgroundImage: backgroundImage ? `url(${this.$util.getFileUrl(backgroundImage)})` : null
372
+ }
373
+ },
374
+ // 页面全局配置(外壳 switchMobile 传入):网格与背景均沿用页面配置。
375
+ globalConfigs () {
376
+ return this.designQuery.globalConfigs || {}
377
+ },
378
+ // 与运行态 pageGridMixin 的移动端分支同一取法:行高沿用页面、间距换成堆叠参数 [8,8]、内边距跟随间距,几何才能一致
379
+ gridParams () {
380
+ return { ...getPageGrid(this.globalConfigs), gap: MOBILE_GRID.gap, padding: null }
381
+ },
382
+ // 行高按视口等比(旧 autoHeight)时才需要随窗口维护 bodyHeight(间距已固定,不会是 'auto')
383
+ autoGrid () {
384
+ return this.gridParams.rowHeight === 'auto'
385
+ },
386
+ // 整块保留的容器项的内部网格参数(与运行态 pageGridMixin.containerGrid 同一取法);非容器返回 undefined
387
+ containerGrid () {
388
+ return item => (isContainerType(item.contentType)
389
+ ? getContainerGrid(item, this.gridParams, isLegacyConfig(this.globalConfigs))
390
+ : undefined)
391
+ },
392
+ activeComponent () {
393
+ return this.design.state.activeItem
394
+ },
395
+ // 移动端列数(与运行态移动端网格一致):列宽档位 整行 = colNum / 半宽 = colNum/2 / 三分之一 = colNum/3
396
+ colNum () {
397
+ return this.gridParams.colNum || 48
398
+ },
399
+ // 画布只渲染「未隐藏」的项:隐藏项移出网格、不再半透明占位,改由下方「已隐藏」区归拢
400
+ visibleLayout () {
401
+ return this.layout.filter(i => i && !i.mobileHidden)
402
+ },
403
+ // 已隐藏组件:归拢到画布下方,一键恢复
404
+ hiddenItems () {
405
+ return this.layout.filter(i => i && i.mobileHidden)
406
+ },
407
+ // PC 布局里的顶层容器(dashboard-div / dashboard-tabs):每个可单独设「移动端保留整块」。
408
+ // 取的是外壳 selectComponentList 的引用(goPage params 内存传递),开关直接写在容器项上,外壳 deep watch 自动保存
409
+ pcContainers () {
410
+ return (this.designQuery.componentList || []).filter(i => i && isContainerType(i.contentType))
411
+ }
412
+ },
413
413
  watch: {
414
414
  activeComponent (item) {
415
415
  if (!this.standalone || !item || String(item.id) === String(this.selectComponent?.id)) return
@@ -417,55 +417,55 @@ export default {
417
417
  this.appearanceTargetId = null
418
418
  this.$emit('select-item', item)
419
419
  },
420
- 'source.componentList': {
421
- handler (items) {
422
- if (!this.standalone) return
423
- this.layout = items || []
424
- if (this.selectComponent && !indexDashboardItems(this.layout).has(String(this.selectComponent.id))) this.selectComponent = null
425
- },
426
- immediate: true
427
- },
428
- themeStyles: {
429
- immediate: true,
430
- deep: true,
431
- handler (v) {
432
- const styleId = this.themeScope
433
- let styleElement = document.getElementById(styleId)
434
- if (!styleElement) {
435
- styleElement = document.createElement('style')
436
- styleElement.id = styleId
437
- document.head.appendChild(styleElement)
438
- }
439
- // 手机框在桌面视口内,显式启用 mobile 覆盖层。
420
+ 'source.componentList': {
421
+ handler (items) {
422
+ if (!this.standalone) return
423
+ this.layout = items || []
424
+ if (this.selectComponent && !indexDashboardItems(this.layout).has(String(this.selectComponent.id))) this.selectComponent = null
425
+ },
426
+ immediate: true
427
+ },
428
+ themeStyles: {
429
+ immediate: true,
430
+ deep: true,
431
+ handler (v) {
432
+ const styleId = this.themeScope
433
+ let styleElement = document.getElementById(styleId)
434
+ if (!styleElement) {
435
+ styleElement = document.createElement('style')
436
+ styleElement.id = styleId
437
+ document.head.appendChild(styleElement)
438
+ }
439
+ // 手机框在桌面视口内,显式启用 mobile 覆盖层。
440
440
  styleElement.textContent = buildThemeCss(v, `.epx-dashboard.dashboard-design__mobile.${this.themeScope}`, path => this.$util.getFileUrl(path), {
441
- containerSelector: '', hoverAncestor: '.drag-component', mobilePreview: true
442
- })
443
- }
444
- },
445
- layout: {
446
- // Vue3 起浅层 watcher 不响应 push/splice;deep: 1 只看数组层
447
- deep: 1,
448
- handler (v) {
449
- for (const item of v) {
450
- if (!item) continue
451
- // x/y/w/h 必须是数字(网格引擎按数值布局)
452
- normalizeGridItem(item)
453
- }
454
- },
455
- immediate: true
456
- },
457
- autoGrid: {
458
- handler (v) {
459
- if (v) {
460
- this.calculateHeight()
461
- window.addEventListener('resize', this.calculateHeight)
462
- } else {
463
- window.removeEventListener('resize', this.calculateHeight)
464
- }
465
- },
466
- immediate: true
467
- }
468
- },
441
+ containerSelector: '', hoverAncestor: '.drag-component', mobilePreview: true
442
+ })
443
+ }
444
+ },
445
+ layout: {
446
+ // Vue3 起浅层 watcher 不响应 push/splice;deep: 1 只看数组层
447
+ deep: 1,
448
+ handler (v) {
449
+ for (const item of v) {
450
+ if (!item) continue
451
+ // x/y/w/h 必须是数字(网格引擎按数值布局)
452
+ normalizeGridItem(item)
453
+ }
454
+ },
455
+ immediate: true
456
+ },
457
+ autoGrid: {
458
+ handler (v) {
459
+ if (v) {
460
+ this.calculateHeight()
461
+ window.addEventListener('resize', this.calculateHeight)
462
+ } else {
463
+ window.removeEventListener('resize', this.calculateHeight)
464
+ }
465
+ },
466
+ immediate: true
467
+ }
468
+ },
469
469
  methods: {
470
470
  isContainerType,
471
471
  onTransferOut (item) {
@@ -476,20 +476,20 @@ export default {
476
476
  onTransferIn (item) {
477
477
  if (this.standalone && !this.layout.some(node => String(node.id) === String(item.id))) this.layout.push(item)
478
478
  },
479
- indexItems: indexDashboardItems,
480
- // upPage 契约:设计器内组件 $refeshPage() 会调到这里;画布由网格引擎维护,无需刷新
481
- $refesh () {
482
- },
483
- saveLayout () {
484
- const { changeMItems } = this.$query
485
- changeMItems && changeMItems(this.layout)
486
- this.backPage()
487
- },
488
- // 点击组件 → 选中并打开移动端设置(显示/隐藏 + 样式),改动直接落在该 mItems 项上
479
+ indexItems: indexDashboardItems,
480
+ // upPage 契约:设计器内组件 $refeshPage() 会调到这里;画布由网格引擎维护,无需刷新
481
+ $refesh () {
482
+ },
483
+ saveLayout () {
484
+ const { changeMItems } = this.$query
485
+ changeMItems && changeMItems(this.layout)
486
+ this.backPage()
487
+ },
488
+ // 点击组件 → 选中并打开移动端设置(显示/隐藏 + 样式),改动直接落在该 mItems 项上
489
489
  openConfig (item, tab) {
490
- this.design.select(item, 'mobile')
491
- this.selectComponent = item
492
- this.appearanceTargetId = null
490
+ this.design.select(item, 'mobile')
491
+ this.selectComponent = item
492
+ this.appearanceTargetId = null
493
493
  if (this.standalone) this.$emit('select-item', item, tab)
494
494
  },
495
495
  openNestedConfig (id, info, configs, upId, tab) {
@@ -497,190 +497,190 @@ export default {
497
497
  const item = indexDashboardItems(this.layout).get(String(id))
498
498
  if (item) this.openConfig(item, tab)
499
499
  },
500
- selectPreviewChart (event) {
501
- const chart = event.target.closest?.('[data-mobile-chart-id]')
502
- if (!chart) return
503
- const id = chart.dataset.mobileChartId
500
+ selectPreviewChart (event) {
501
+ const chart = event.target.closest?.('[data-mobile-chart-id]')
502
+ if (!chart) return
503
+ const id = chart.dataset.mobileChartId
504
504
  const owner = this.layout.find(item => indexDashboardItems([item]).has(id))
505
- if (!owner) return
505
+ if (!owner) return
506
506
  event.stopPropagation()
507
507
  if (this.standalone) {
508
508
  this.openConfig(indexDashboardItems([owner]).get(String(id)))
509
509
  return
510
510
  }
511
- this.openConfig(owner)
512
- this.appearanceTargetId = indexDashboardItems([owner]).get(id).id
513
- if (this.standalone) this.$emit('select-item', this.appearanceTarget)
514
- },
515
- hasAppearance (key) {
516
- return Object.prototype.hasOwnProperty.call(this.appearanceTarget?.mobileAppearance || {}, key)
517
- },
518
- componentLabel (item) {
519
- const config = this.designQuery.itemConfigs?.[item.id] || {}
520
- return this.chartLabels[item.id] || config.title || config.componentName || item.title || item.contentType || item.id
521
- },
522
- setAppearance (key, value) {
523
- const target = this.appearanceTarget
524
- if (!target) return
525
- const next = { ...target.mobileAppearance }
526
- if (value === undefined || value === null || value === '') delete next[key]
527
- else next[key] = value
528
- target.mobileAppearance = normalizeMobileAppearance(next)
529
- },
530
- restoreAppearance () {
531
- if (!this.appearanceTarget) return
532
- delete this.appearanceTarget.mobileAppearance
533
- },
534
- styleLabel (key) {
535
- const labels = { color: '文字颜色', fontSize: '字号', backgroundColor: '背景颜色', backgroundImage: '背景图片', borderRadius: '圆角', padding: '内边距', margin: '外边距', height: '高度', width: '宽度', boxShadow: '阴影' }
536
- return labels[key] || key
537
- },
538
- restoreStyle (key) {
539
- const style = { ...this.selectComponent.style }
540
- delete style[key]
541
- this.onStyleChange(this.selectComponent, style)
542
- },
543
- setHidden (item, hidden) {
544
- item.mobileHidden = hidden
545
- },
546
- // 容器「移动端保留整块」开关:写 PC 容器项的 mobileExpand(=== false 保留整块,其它 / 无键 = 默认拆开)。
547
- // 只影响「按 PC 布局生成」的结果,当前 mItems 不动;关掉时删键、不留 true 冗余
548
- setContainerKeep (container, keep) {
549
- if (keep) container.mobileExpand = false
550
- else delete container.mobileExpand
551
- },
552
- // 当前列宽档位:整行 full / 半宽 half / 三分之一 third(按与 colNum 的比例判定,供列宽单选回显)
553
- spanLevel (item) {
554
- if (!item) return 'full'
555
- const w = Number(item.w) || 0
556
- if (w >= this.colNum) return 'full'
557
- if (w <= Math.floor(this.colNum / 3)) return 'third'
558
- return 'half'
559
- },
560
- // 列宽快捷:整行 / 半宽 / 三分之一,直接改 item.w(layoutKey 变化驱动画布同步、碰撞层重排邻居)
561
- setSpan (item, level) {
562
- if (!item) return
563
- const map = { full: this.colNum, half: Math.floor(this.colNum / 2), third: Math.floor(this.colNum / 3) }
564
- item.w = map[level] || this.colNum
565
- },
566
- // 高度快捷:直接改 item.h(网格行数),至少 2 行
567
- setMHeight (item, h) {
568
- if (!item) return
569
- item.h = Math.max(2, Number(h) || 2)
570
- },
571
- // 排序快捷键:把选中项在阅读序里 置顶 / 上移 / 下移 / 置底,按各项当前列宽重新堆叠;
572
- // 只把重算出的 x/y 写回原项(保持引用,选中态与画布同步不丢)
573
- reorder (dir) {
574
- if (!this.selectComponent) return
575
- const siblings = this.standalone ? this.findSiblings(this.layout, this.selectComponent.id) || this.layout : this.layout
576
- const packed = reorderStack(siblings.filter(item => !item.mobileHidden), this.selectComponent.id, dir, this.colNum)
577
- const byId = new Map(packed.map(p => [String(p.id), p]))
578
- for (const it of siblings) {
579
- const p = byId.get(String(it.id))
580
- if (p) { it.x = p.x; it.y = p.y }
581
- }
582
- },
583
- findSiblings (items, id) {
584
- if (items.some(item => item.id === id)) return items
585
- for (const item of items) {
586
- const found = item.items && this.findSiblings(item.items, id)
587
- if (found) return found
588
- }
589
- return null
590
- },
591
- // PC 组件样式(合并预览的基础),来自 PC 组件列表
592
- pcStyle (id) {
593
- const pcItem = this.pcItemIndex.get(String(id)) || {}
594
- return pcItem.style || {}
595
- },
596
- // 预览用样式:PC 样式为基础 + 移动端覆盖(空值不覆盖)
597
- compStyle (item) {
598
- const cur = item.style || {}
599
- const override = {}
600
- for (const k in cur) {
601
- if (cur[k] !== undefined && cur[k] !== null && cur[k] !== '') override[k] = cur[k]
602
- }
603
- return { ...this.pcStyle(item.id), ...override }
604
- },
605
- // 样式配置产出真实 CSS:只把"有值的键"作为移动端覆盖存到该 mItems 项(空值不存,运行时沿用 PC)
606
- onStyleChange (item, style) {
607
- const override = {}
608
- for (const k in (style || {})) {
609
- if (style[k] !== undefined && style[k] !== null && style[k] !== '') override[k] = style[k]
610
- }
611
- item.style = override
612
- this.reloadComponent[item.id] = true
613
- this.$nextTick(() => { this.reloadComponent[item.id] = false })
614
- },
615
- // 按 PC 布局一键生成移动端堆叠布局;已有布局时先打开页内覆盖确认
616
- generateFromPc () {
617
- if (this.layout && this.layout.length) {
618
- this.confirmRegenerate = true
619
- } else {
620
- this.regenerate()
621
- }
622
- },
623
- // 确认覆盖:关弹窗后执行生成;生成本身抛错给提示
624
- regenerateFromConfirm () {
625
- this.confirmRegenerate = false
626
- try {
627
- this.regenerate()
628
- } catch (e) {
629
- this.$message.error((e && (e.msg || e.message)) || '生成移动端布局失败')
630
- }
631
- },
632
- // 重新生成:先清空让画布把旧壳全部移除、下一拍再整体放入。直接替换数组时画布会对同 id 的旧节点逐项 update,
633
- // gridstack 碰撞层在中间态推挤,会把算好的紧凑堆叠搅乱(真机实测:两张统计卡被甩到最底下)
634
- regenerate () {
635
- const next = regenerateMobileDesign(this.$_.cloneDeep(this.designQuery.componentList || []), this.$_.cloneDeep(this.layout), this.colNum)
636
- this.selectComponent = null
637
- this.layout = []
638
- this.$nextTick(() => { this.layout = next })
639
- },
640
- calculateHeight () {
641
- this.bodyHeight = document.body.offsetHeight
642
- },
643
- measureCanvas () {
644
- if (this.$refs.canvas) this.canvasHeight = this.$refs.canvas.clientHeight
645
- }
646
- },
647
- mounted () {
648
- const { mobileComponentList, componentList } = this.designQuery
649
- if (this.standalone) {
650
- this.layout = componentList || []
651
- } else if (mobileComponentList) {
652
- this.layout = this.$_.cloneDeep(mobileComponentList.filter(item => this.pcItemIndex.has(String(item.id))))
653
- } else {
654
- this.layout = stackMobileLayout(this.$_.cloneDeep(componentList || []))
655
- }
656
- this.measureCanvas()
657
- if (typeof ResizeObserver !== 'undefined') {
658
- this.canvasObserver = new ResizeObserver(() => this.measureCanvas())
659
- this.canvasObserver.observe(this.$refs.canvas)
660
- }
661
- },
662
- beforeUnmount () {
663
- document.getElementById(this.themeScope)?.remove()
664
- window.removeEventListener('resize', this.calculateHeight)
665
- if (this.canvasObserver) this.canvasObserver.disconnect()
666
- }
667
- }
668
- </script>
669
-
670
- <style lang="scss" scoped>
511
+ this.openConfig(owner)
512
+ this.appearanceTargetId = indexDashboardItems([owner]).get(id).id
513
+ if (this.standalone) this.$emit('select-item', this.appearanceTarget)
514
+ },
515
+ hasAppearance (key) {
516
+ return Object.prototype.hasOwnProperty.call(this.appearanceTarget?.mobileAppearance || {}, key)
517
+ },
518
+ componentLabel (item) {
519
+ const config = this.designQuery.itemConfigs?.[item.id] || {}
520
+ return this.chartLabels[item.id] || config.title || config.componentName || item.title || item.contentType || item.id
521
+ },
522
+ setAppearance (key, value) {
523
+ const target = this.appearanceTarget
524
+ if (!target) return
525
+ const next = { ...target.mobileAppearance }
526
+ if (value === undefined || value === null || value === '') delete next[key]
527
+ else next[key] = value
528
+ target.mobileAppearance = normalizeMobileAppearance(next)
529
+ },
530
+ restoreAppearance () {
531
+ if (!this.appearanceTarget) return
532
+ delete this.appearanceTarget.mobileAppearance
533
+ },
534
+ styleLabel (key) {
535
+ const labels = { color: '文字颜色', fontSize: '字号', backgroundColor: '背景颜色', backgroundImage: '背景图片', borderRadius: '圆角', padding: '内边距', margin: '外边距', height: '高度', width: '宽度', boxShadow: '阴影' }
536
+ return labels[key] || key
537
+ },
538
+ restoreStyle (key) {
539
+ const style = { ...this.selectComponent.style }
540
+ delete style[key]
541
+ this.onStyleChange(this.selectComponent, style)
542
+ },
543
+ setHidden (item, hidden) {
544
+ item.mobileHidden = hidden
545
+ },
546
+ // 容器「移动端保留整块」开关:写 PC 容器项的 mobileExpand(=== false 保留整块,其它 / 无键 = 默认拆开)。
547
+ // 只影响「按 PC 布局生成」的结果,当前 mItems 不动;关掉时删键、不留 true 冗余
548
+ setContainerKeep (container, keep) {
549
+ if (keep) container.mobileExpand = false
550
+ else delete container.mobileExpand
551
+ },
552
+ // 当前列宽档位:整行 full / 半宽 half / 三分之一 third(按与 colNum 的比例判定,供列宽单选回显)
553
+ spanLevel (item) {
554
+ if (!item) return 'full'
555
+ const w = Number(item.w) || 0
556
+ if (w >= this.colNum) return 'full'
557
+ if (w <= Math.floor(this.colNum / 3)) return 'third'
558
+ return 'half'
559
+ },
560
+ // 列宽快捷:整行 / 半宽 / 三分之一,直接改 item.w(layoutKey 变化驱动画布同步、碰撞层重排邻居)
561
+ setSpan (item, level) {
562
+ if (!item) return
563
+ const map = { full: this.colNum, half: Math.floor(this.colNum / 2), third: Math.floor(this.colNum / 3) }
564
+ item.w = map[level] || this.colNum
565
+ },
566
+ // 高度快捷:直接改 item.h(网格行数),至少 2 行
567
+ setMHeight (item, h) {
568
+ if (!item) return
569
+ item.h = Math.max(2, Number(h) || 2)
570
+ },
571
+ // 排序快捷键:把选中项在阅读序里 置顶 / 上移 / 下移 / 置底,按各项当前列宽重新堆叠;
572
+ // 只把重算出的 x/y 写回原项(保持引用,选中态与画布同步不丢)
573
+ reorder (dir) {
574
+ if (!this.selectComponent) return
575
+ const siblings = this.standalone ? this.findSiblings(this.layout, this.selectComponent.id) || this.layout : this.layout
576
+ const packed = reorderStack(siblings.filter(item => !item.mobileHidden), this.selectComponent.id, dir, this.colNum)
577
+ const byId = new Map(packed.map(p => [String(p.id), p]))
578
+ for (const it of siblings) {
579
+ const p = byId.get(String(it.id))
580
+ if (p) { it.x = p.x; it.y = p.y }
581
+ }
582
+ },
583
+ findSiblings (items, id) {
584
+ if (items.some(item => item.id === id)) return items
585
+ for (const item of items) {
586
+ const found = item.items && this.findSiblings(item.items, id)
587
+ if (found) return found
588
+ }
589
+ return null
590
+ },
591
+ // PC 组件样式(合并预览的基础),来自 PC 组件列表
592
+ pcStyle (id) {
593
+ const pcItem = this.pcItemIndex.get(String(id)) || {}
594
+ return pcItem.style || {}
595
+ },
596
+ // 预览用样式:PC 样式为基础 + 移动端覆盖(空值不覆盖)
597
+ compStyle (item) {
598
+ const cur = item.style || {}
599
+ const override = {}
600
+ for (const k in cur) {
601
+ if (cur[k] !== undefined && cur[k] !== null && cur[k] !== '') override[k] = cur[k]
602
+ }
603
+ return { ...this.pcStyle(item.id), ...override }
604
+ },
605
+ // 样式配置产出真实 CSS:只把"有值的键"作为移动端覆盖存到该 mItems 项(空值不存,运行时沿用 PC)
606
+ onStyleChange (item, style) {
607
+ const override = {}
608
+ for (const k in (style || {})) {
609
+ if (style[k] !== undefined && style[k] !== null && style[k] !== '') override[k] = style[k]
610
+ }
611
+ item.style = override
612
+ this.reloadComponent[item.id] = true
613
+ this.$nextTick(() => { this.reloadComponent[item.id] = false })
614
+ },
615
+ // 按 PC 布局一键生成移动端堆叠布局;已有布局时先打开页内覆盖确认
616
+ generateFromPc () {
617
+ if (this.layout && this.layout.length) {
618
+ this.confirmRegenerate = true
619
+ } else {
620
+ this.regenerate()
621
+ }
622
+ },
623
+ // 确认覆盖:关弹窗后执行生成;生成本身抛错给提示
624
+ regenerateFromConfirm () {
625
+ this.confirmRegenerate = false
626
+ try {
627
+ this.regenerate()
628
+ } catch (e) {
629
+ this.$message.error((e && (e.msg || e.message)) || '生成移动端布局失败')
630
+ }
631
+ },
632
+ // 重新生成:先清空让画布把旧壳全部移除、下一拍再整体放入。直接替换数组时画布会对同 id 的旧节点逐项 update,
633
+ // gridstack 碰撞层在中间态推挤,会把算好的紧凑堆叠搅乱(真机实测:两张统计卡被甩到最底下)
634
+ regenerate () {
635
+ const next = regenerateMobileDesign(this.$_.cloneDeep(this.designQuery.componentList || []), this.$_.cloneDeep(this.layout), this.colNum)
636
+ this.selectComponent = null
637
+ this.layout = []
638
+ this.$nextTick(() => { this.layout = next })
639
+ },
640
+ calculateHeight () {
641
+ this.bodyHeight = document.body.offsetHeight
642
+ },
643
+ measureCanvas () {
644
+ if (this.$refs.canvas) this.canvasHeight = this.$refs.canvas.clientHeight
645
+ }
646
+ },
647
+ mounted () {
648
+ const { mobileComponentList, componentList } = this.designQuery
649
+ if (this.standalone) {
650
+ this.layout = componentList || []
651
+ } else if (mobileComponentList) {
652
+ this.layout = this.$_.cloneDeep(mobileComponentList.filter(item => this.pcItemIndex.has(String(item.id))))
653
+ } else {
654
+ this.layout = stackMobileLayout(this.$_.cloneDeep(componentList || []))
655
+ }
656
+ this.measureCanvas()
657
+ if (typeof ResizeObserver !== 'undefined') {
658
+ this.canvasObserver = new ResizeObserver(() => this.measureCanvas())
659
+ this.canvasObserver.observe(this.$refs.canvas)
660
+ }
661
+ },
662
+ beforeUnmount () {
663
+ document.getElementById(this.themeScope)?.remove()
664
+ window.removeEventListener('resize', this.calculateHeight)
665
+ if (this.canvasObserver) this.canvasObserver.disconnect()
666
+ }
667
+ }
668
+ </script>
669
+
670
+ <style lang="scss" scoped>
671
671
  .config-container--standalone {
672
- flex-direction: row !important;
673
- .mobile-body {
674
- flex: 1;
675
- min-width: 0;
676
- padding: 0 16px;
672
+ flex-direction: row !important;
673
+ .mobile-body {
674
+ flex: 1;
675
+ min-width: 0;
676
+ padding: 0 16px;
677
677
  background: color-mix(in srgb, var(--epx-bg-page, #f3f5f8) 90%, var(--epx-text-primary, #303133));
678
- }
678
+ }
679
679
  .dashboard-design__mobile {
680
680
  position: relative;
681
681
  height: calc(100% - 24px);
682
682
  margin: 12px auto;
683
- max-width: calc(100% - 20px);
683
+ max-width: calc(100% - 20px);
684
684
  box-sizing: content-box;
685
685
  outline: 1px solid var(--epx-border-color, #d5dce6);
686
686
  }
@@ -718,106 +718,106 @@ export default {
718
718
  border-bottom: 1px solid var(--epx-border-color, #e5e7eb);
719
719
  }
720
720
  .mobile-properties {
721
- flex: 0 0 340px;
722
- width: 340px;
723
- max-width: 40%;
724
- overflow-y: auto;
725
- background: var(--epx-bg-card, #fff);
721
+ flex: 0 0 340px;
722
+ width: 340px;
723
+ max-width: 40%;
724
+ overflow-y: auto;
725
+ background: var(--epx-bg-card, #fff);
726
726
  border-left: 1px solid var(--epx-border-color, #e5e7eb);
727
727
  :deep(.config-container) { height: auto; }
728
- }
728
+ }
729
729
  .mobile-config__actions {
730
- display: flex;
731
- gap: 8px;
732
- margin: 12px 0;
730
+ display: flex;
731
+ gap: 8px;
732
+ margin: 12px 0;
733
+ }
734
+ .config-container {
735
+ position: relative;
736
+ width: 100%;
737
+ height: 100%;
738
+ overflow: hidden;
739
+ display: flex;
740
+ flex-direction: column;
733
741
  }
734
- .config-container {
735
- position: relative;
736
- width: 100%;
737
- height: 100%;
738
- overflow: hidden;
739
- display: flex;
740
- flex-direction: column;
741
- }
742
- .page-header {
743
- display: flex;
744
- flex-shrink: 0;
745
- align-items: center;
746
- padding: 0 20px;
747
- height: 46px;
748
- background: var(--epx-bg-card);
749
- border-bottom: 1px solid #e8e8e8;
750
- }
751
-
752
- .page-header__left {
753
- display: flex;
754
- }
755
-
756
- .page-header__back {
757
- font-size: 24px;
758
- color: #666;
759
- cursor: pointer;
760
- }
761
-
762
- .page-header__title {
763
- font-size: var(--epx-font-size-l);
764
- padding: 0 20px;
765
- font-weight: 600;
766
- border-right: 1px solid var(--epx-border);
767
- }
768
- .page-header__subtitle {
769
- font-size: var(--epx-font-size-base);
770
- color: #666;
771
- }
772
- .page-header__operate {
773
- display: flex;
774
- align-items: center;
775
- }
776
- .header-operate__item {
777
- color: #444;
778
- cursor: pointer;
779
- margin: 20px;
780
- i {
781
- margin-right: 4px;
782
- }
783
- &:hover {
784
- @include font_color_primary
785
- }
786
- }
787
- // 可滚动内容区:撑满 header 以下,手机框 + 归拢区超出时纵向滚动
788
- .mobile-body {
789
- flex: 1;
790
- min-height: 0;
791
- overflow-y: auto;
792
- overflow-x: hidden;
793
- box-sizing: border-box;
794
- transition: padding-right .2s;
795
- }
796
- .config-container--editing .mobile-body {
797
- padding-right: min(400px, 44vw);
798
- }
799
- .dashboard-design__mobile {
800
- width: 375px;
801
- height: 670px;
802
- margin: 24px auto;
803
- overflow-y: auto;
742
+ .page-header {
743
+ display: flex;
744
+ flex-shrink: 0;
745
+ align-items: center;
746
+ padding: 0 20px;
747
+ height: 46px;
748
+ background: var(--epx-bg-card);
749
+ border-bottom: 1px solid #e8e8e8;
750
+ }
751
+
752
+ .page-header__left {
753
+ display: flex;
754
+ }
755
+
756
+ .page-header__back {
757
+ font-size: 24px;
758
+ color: #666;
759
+ cursor: pointer;
760
+ }
761
+
762
+ .page-header__title {
763
+ font-size: var(--epx-font-size-l);
764
+ padding: 0 20px;
765
+ font-weight: 600;
766
+ border-right: 1px solid var(--epx-border);
767
+ }
768
+ .page-header__subtitle {
769
+ font-size: var(--epx-font-size-base);
770
+ color: #666;
771
+ }
772
+ .page-header__operate {
773
+ display: flex;
774
+ align-items: center;
775
+ }
776
+ .header-operate__item {
777
+ color: #444;
778
+ cursor: pointer;
779
+ margin: 20px;
780
+ i {
781
+ margin-right: 4px;
782
+ }
783
+ &:hover {
784
+ @include font_color_primary
785
+ }
786
+ }
787
+ // 可滚动内容区:撑满 header 以下,手机框 + 归拢区超出时纵向滚动
788
+ .mobile-body {
789
+ flex: 1;
790
+ min-height: 0;
791
+ overflow-y: auto;
792
+ overflow-x: hidden;
793
+ box-sizing: border-box;
794
+ transition: padding-right .2s;
795
+ }
796
+ .config-container--editing .mobile-body {
797
+ padding-right: min(400px, 44vw);
798
+ }
799
+ .dashboard-design__mobile {
800
+ width: 375px;
801
+ height: 670px;
802
+ margin: 24px auto;
803
+ overflow-y: auto;
804
804
  background: var(--epx-bg-card, #fff);
805
805
  border-radius: 0;
806
806
  border: 0;
807
807
  box-shadow: none;
808
- // 仪表盘根节点为 flex 容器,长画布保持原高度并在手机框内滚动。
809
- > .epx-dashboard-design-grid {
810
- flex: none;
811
- }
812
- }
813
- .mobile-empty {
814
- width: 100%;
815
- height: 100%;
816
- display: flex;
817
- flex-direction: column;
818
- justify-content: center;
819
- align-items: center;
820
- color: #909399;
808
+ // 仪表盘根节点为 flex 容器,长画布保持原高度并在手机框内滚动。
809
+ > .epx-dashboard-design-grid {
810
+ flex: none;
811
+ }
812
+ }
813
+ .mobile-empty {
814
+ width: 100%;
815
+ height: 100%;
816
+ display: flex;
817
+ flex-direction: column;
818
+ justify-content: center;
819
+ align-items: center;
820
+ color: #909399;
821
821
  &__actions {
822
822
  display: grid;
823
823
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -846,273 +846,273 @@ export default {
846
846
  box-shadow: 0 4px 8px rgba(0, 0, 0, .15);
847
847
  }
848
848
  }
849
- &__text {
850
- font-size: 14px;
851
- margin-bottom: 16px;
852
- }
853
- }
854
- .dashboard-design--dark {
855
- background: #151C3D;
856
- }
857
- .dashboard-design--technology {
858
- background: url('@/assets/images/dashboard/technology_bg.jpg') no-repeat center;
859
- background-size: cover;
860
- }
861
- .drag-component {
862
- width: 100%;
863
- height: 100%;
864
- overflow: auto;
865
- cursor: pointer;
866
- >* {
867
- pointer-events: none;
868
- }
869
- // 图表根节点接收选中点击,内部图表交互不触发钻取;标签页仍可切换与滚动。
870
- :deep([data-mobile-chart-id]),
871
- :deep(.el-tabs__header),
872
- :deep(.el-tab-pane) {
873
- pointer-events: auto;
874
- }
875
- :deep([data-mobile-chart-id] *) {
876
- pointer-events: none;
877
- }
878
- &__edit {
879
- position: absolute;
880
- top: 4px;
881
- right: 4px;
882
- z-index: 2;
883
- padding: 4px;
884
- font-size: 14px;
885
- color: #fff;
886
- background: rgba(8, 116, 231, .85);
887
- border-radius: 4px;
888
- opacity: 0;
889
- transition: opacity .2s;
890
- }
891
- &:hover &__edit {
892
- opacity: 1;
893
- }
894
- }
895
- // 已隐藏组件归拢区:手机框下方,收纳隐藏项,点一下恢复到画布
896
- .mobile-hidden-tray {
897
- width: 375px;
898
- margin: 0 auto 16px;
899
- padding: 10px 12px;
900
- background: var(--epx-bg-card);
901
- border: 1px solid var(--epx-border);
902
- border-radius: 8px;
903
- &__title {
904
- font-size: 13px;
905
- color: #666;
906
- margin-bottom: 8px;
907
- i {
908
- margin-right: 4px;
909
- }
910
- }
911
- &__list {
912
- display: flex;
913
- flex-wrap: wrap;
914
- gap: 8px;
915
- }
916
- &__item {
917
- display: inline-flex;
918
- align-items: center;
919
- max-width: 160px;
920
- padding: 4px 8px;
921
- font-size: 12px;
922
- color: #444;
923
- background: var(--epx-bg-page, #f5f7fa);
924
- border: 1px solid var(--epx-border);
925
- border-radius: 4px;
926
- cursor: pointer;
927
- &:hover {
928
- @include font_color_primary;
929
- border-color: rgba(var(--color-primary), .5);
930
- }
931
- }
932
- &__name {
933
- overflow: hidden;
934
- white-space: nowrap;
935
- text-overflow: ellipsis;
936
- }
937
- &__restore {
938
- margin-left: 6px;
939
- font-size: 13px;
940
- }
941
- }
942
- // 容器策略区:与归拢区同宽同风格,每行 容器名 + 当前模式 + 「保留整块」开关
943
- .mobile-container-tray {
944
- width: 375px;
945
- margin: 0 auto 16px;
946
- padding: 10px 12px;
947
- background: var(--epx-bg-card);
948
- border: 1px solid var(--epx-border);
949
- border-radius: 8px;
950
- &__title {
951
- display: flex;
952
- align-items: center;
953
- font-size: 13px;
954
- color: #666;
955
- margin-bottom: 6px;
956
- i {
957
- margin-right: 4px;
958
- }
959
- }
960
- &__tip {
961
- margin-left: auto;
962
- font-size: 12px;
963
- color: #909399;
964
- }
965
- &__row {
966
- display: flex;
967
- align-items: center;
968
- padding: 6px 0;
969
- font-size: 13px;
970
- color: #444;
971
- border-top: 1px solid #f0f0f0;
972
- }
973
- &__name {
974
- flex: 1;
975
- overflow: hidden;
976
- white-space: nowrap;
977
- text-overflow: ellipsis;
978
- }
979
- &__mode {
980
- margin: 0 10px;
981
- font-size: 12px;
982
- color: #909399;
983
- }
984
- }
985
- .mobile-config {
986
- padding: 16px;
987
- &__hint {
988
- font-size: 12px;
989
- line-height: 1.7;
990
- color: var(--epx-text-secondary, #909399);
991
- background: var(--epx-bg-page, #f5f7fa);
992
- border-radius: 6px;
993
- padding: 10px;
994
- }
995
- &__title {
996
- font-weight: 600;
997
- font-size: 15px;
998
- margin-bottom: 12px;
999
- color: #333;
1000
- display: flex;
1001
- align-items: center;
1002
- gap: 8px;
1003
- }
1004
- &__type {
1005
- font-size: 12px;
1006
- font-weight: normal;
1007
- padding: 3px 8px;
1008
- border-radius: 4px;
1009
- background: var(--epx-bg-page, #f5f7fa);
1010
- color: var(--epx-text-secondary, #909399);
1011
- }
1012
- &__chart-type {
1013
- margin: 8px 0;
1014
- font-size: 12px;
1015
- color: var(--epx-text-secondary, #909399);
1016
- }
1017
- &__section-title {
1018
- margin-top: 16px;
1019
- font-size: 12px;
1020
- color: var(--epx-text-secondary, #909399);
1021
- }
1022
- &__row {
1023
- display: flex;
1024
- justify-content: space-between;
1025
- align-items: center;
1026
- padding: 8px 0;
1027
- border-bottom: 1px solid #eee;
1028
- }
1029
- &__group-title {
1030
- margin: 16px 0 8px;
1031
- font-weight: 600;
1032
- color: #666;
1033
- }
1034
- }
1035
- .mobile-config-header {
1036
- strong {
1037
- font-size: 16px;
1038
- color: var(--epx-text);
1039
- }
1040
- p {
1041
- margin: 6px 0 0;
1042
- font-size: 12px;
1043
- color: var(--epx-text-secondary, #909399);
1044
- }
1045
- }
1046
- .mobile-card-style {
1047
- margin-top: 20px;
1048
- border-top: 1px solid var(--epx-border);
1049
- summary {
1050
- padding: 16px 0;
1051
- cursor: pointer;
1052
- font-size: 13px;
1053
- font-weight: 600;
1054
- }
1055
- }
1056
- .mobile-appearance {
1057
- padding: 10px 0;
1058
- border-bottom: 1px solid var(--epx-border);
1059
- &__heading {
1060
- display: flex;
1061
- align-items: center;
1062
- gap: 8px;
1063
- margin-bottom: 6px;
1064
- font-size: 13px;
1065
- > .el-button {
1066
- margin-left: auto;
1067
- }
1068
- }
1069
- &__status {
1070
- font-size: 12px;
1071
- font-weight: normal;
1072
- color: var(--epx-text-secondary, #909399);
1073
- &.is-independent {
1074
- @include font_color_primary;
1075
- }
1076
- }
1077
- }
1078
- // 覆盖确认弹窗:居中轻量卡片(图标 + 文案 + 双按钮),替代 MessageBox 默认样式
1079
- .mobile-confirm {
1080
- display: flex;
1081
- flex-direction: column;
1082
- align-items: center;
1083
- text-align: center;
1084
- padding: 8px 8px 4px;
1085
- &__icon {
1086
- font-size: 40px;
1087
- color: #e6a23c;
1088
- margin-bottom: 12px;
1089
- }
1090
- &__text {
1091
- margin: 0;
1092
- font-size: 14px;
1093
- line-height: 1.6;
1094
- color: var(--epx-text, #333);
1095
- }
1096
- &__actions {
1097
- display: flex;
1098
- justify-content: center;
1099
- gap: 12px;
1100
- margin-top: 20px;
1101
- }
1102
- }
1103
- </style>
1104
-
1105
- <style lang="scss">
1106
- // 抽屉传送到 body,只通过专用类调整,不影响其它抽屉。
1107
- .mobile-appearance-drawer {
1108
- .el-drawer__header {
1109
- padding: 16px;
1110
- margin: 0;
1111
- border-bottom: 1px solid var(--epx-border);
1112
- }
1113
- .el-drawer__body {
1114
- padding: 0;
1115
- min-height: 0;
1116
- }
1117
- }
1118
- </style>
849
+ &__text {
850
+ font-size: 14px;
851
+ margin-bottom: 16px;
852
+ }
853
+ }
854
+ .dashboard-design--dark {
855
+ background: #151C3D;
856
+ }
857
+ .dashboard-design--technology {
858
+ background: url('@/assets/images/dashboard/technology_bg.jpg') no-repeat center;
859
+ background-size: cover;
860
+ }
861
+ .drag-component {
862
+ width: 100%;
863
+ height: 100%;
864
+ overflow: auto;
865
+ cursor: pointer;
866
+ >* {
867
+ pointer-events: none;
868
+ }
869
+ // 图表根节点接收选中点击,内部图表交互不触发钻取;标签页仍可切换与滚动。
870
+ :deep([data-mobile-chart-id]),
871
+ :deep(.el-tabs__header),
872
+ :deep(.el-tab-pane) {
873
+ pointer-events: auto;
874
+ }
875
+ :deep([data-mobile-chart-id] *) {
876
+ pointer-events: none;
877
+ }
878
+ &__edit {
879
+ position: absolute;
880
+ top: 4px;
881
+ right: 4px;
882
+ z-index: 2;
883
+ padding: 4px;
884
+ font-size: 14px;
885
+ color: #fff;
886
+ background: rgba(8, 116, 231, .85);
887
+ border-radius: 4px;
888
+ opacity: 0;
889
+ transition: opacity .2s;
890
+ }
891
+ &:hover &__edit {
892
+ opacity: 1;
893
+ }
894
+ }
895
+ // 已隐藏组件归拢区:手机框下方,收纳隐藏项,点一下恢复到画布
896
+ .mobile-hidden-tray {
897
+ width: 375px;
898
+ margin: 0 auto 16px;
899
+ padding: 10px 12px;
900
+ background: var(--epx-bg-card);
901
+ border: 1px solid var(--epx-border);
902
+ border-radius: 8px;
903
+ &__title {
904
+ font-size: 13px;
905
+ color: #666;
906
+ margin-bottom: 8px;
907
+ i {
908
+ margin-right: 4px;
909
+ }
910
+ }
911
+ &__list {
912
+ display: flex;
913
+ flex-wrap: wrap;
914
+ gap: 8px;
915
+ }
916
+ &__item {
917
+ display: inline-flex;
918
+ align-items: center;
919
+ max-width: 160px;
920
+ padding: 4px 8px;
921
+ font-size: 12px;
922
+ color: #444;
923
+ background: var(--epx-bg-page, #f5f7fa);
924
+ border: 1px solid var(--epx-border);
925
+ border-radius: 4px;
926
+ cursor: pointer;
927
+ &:hover {
928
+ @include font_color_primary;
929
+ border-color: rgba(var(--color-primary), .5);
930
+ }
931
+ }
932
+ &__name {
933
+ overflow: hidden;
934
+ white-space: nowrap;
935
+ text-overflow: ellipsis;
936
+ }
937
+ &__restore {
938
+ margin-left: 6px;
939
+ font-size: 13px;
940
+ }
941
+ }
942
+ // 容器策略区:与归拢区同宽同风格,每行 容器名 + 当前模式 + 「保留整块」开关
943
+ .mobile-container-tray {
944
+ width: 375px;
945
+ margin: 0 auto 16px;
946
+ padding: 10px 12px;
947
+ background: var(--epx-bg-card);
948
+ border: 1px solid var(--epx-border);
949
+ border-radius: 8px;
950
+ &__title {
951
+ display: flex;
952
+ align-items: center;
953
+ font-size: 13px;
954
+ color: #666;
955
+ margin-bottom: 6px;
956
+ i {
957
+ margin-right: 4px;
958
+ }
959
+ }
960
+ &__tip {
961
+ margin-left: auto;
962
+ font-size: 12px;
963
+ color: #909399;
964
+ }
965
+ &__row {
966
+ display: flex;
967
+ align-items: center;
968
+ padding: 6px 0;
969
+ font-size: 13px;
970
+ color: #444;
971
+ border-top: 1px solid #f0f0f0;
972
+ }
973
+ &__name {
974
+ flex: 1;
975
+ overflow: hidden;
976
+ white-space: nowrap;
977
+ text-overflow: ellipsis;
978
+ }
979
+ &__mode {
980
+ margin: 0 10px;
981
+ font-size: 12px;
982
+ color: #909399;
983
+ }
984
+ }
985
+ .mobile-config {
986
+ padding: 16px;
987
+ &__hint {
988
+ font-size: 12px;
989
+ line-height: 1.7;
990
+ color: var(--epx-text-secondary, #909399);
991
+ background: var(--epx-bg-page, #f5f7fa);
992
+ border-radius: 6px;
993
+ padding: 10px;
994
+ }
995
+ &__title {
996
+ font-weight: 600;
997
+ font-size: 15px;
998
+ margin-bottom: 12px;
999
+ color: #333;
1000
+ display: flex;
1001
+ align-items: center;
1002
+ gap: 8px;
1003
+ }
1004
+ &__type {
1005
+ font-size: 12px;
1006
+ font-weight: normal;
1007
+ padding: 3px 8px;
1008
+ border-radius: 4px;
1009
+ background: var(--epx-bg-page, #f5f7fa);
1010
+ color: var(--epx-text-secondary, #909399);
1011
+ }
1012
+ &__chart-type {
1013
+ margin: 8px 0;
1014
+ font-size: 12px;
1015
+ color: var(--epx-text-secondary, #909399);
1016
+ }
1017
+ &__section-title {
1018
+ margin-top: 16px;
1019
+ font-size: 12px;
1020
+ color: var(--epx-text-secondary, #909399);
1021
+ }
1022
+ &__row {
1023
+ display: flex;
1024
+ justify-content: space-between;
1025
+ align-items: center;
1026
+ padding: 8px 0;
1027
+ border-bottom: 1px solid #eee;
1028
+ }
1029
+ &__group-title {
1030
+ margin: 16px 0 8px;
1031
+ font-weight: 600;
1032
+ color: #666;
1033
+ }
1034
+ }
1035
+ .mobile-config-header {
1036
+ strong {
1037
+ font-size: 16px;
1038
+ color: var(--epx-text);
1039
+ }
1040
+ p {
1041
+ margin: 6px 0 0;
1042
+ font-size: 12px;
1043
+ color: var(--epx-text-secondary, #909399);
1044
+ }
1045
+ }
1046
+ .mobile-card-style {
1047
+ margin-top: 20px;
1048
+ border-top: 1px solid var(--epx-border);
1049
+ summary {
1050
+ padding: 16px 0;
1051
+ cursor: pointer;
1052
+ font-size: 13px;
1053
+ font-weight: 600;
1054
+ }
1055
+ }
1056
+ .mobile-appearance {
1057
+ padding: 10px 0;
1058
+ border-bottom: 1px solid var(--epx-border);
1059
+ &__heading {
1060
+ display: flex;
1061
+ align-items: center;
1062
+ gap: 8px;
1063
+ margin-bottom: 6px;
1064
+ font-size: 13px;
1065
+ > .el-button {
1066
+ margin-left: auto;
1067
+ }
1068
+ }
1069
+ &__status {
1070
+ font-size: 12px;
1071
+ font-weight: normal;
1072
+ color: var(--epx-text-secondary, #909399);
1073
+ &.is-independent {
1074
+ @include font_color_primary;
1075
+ }
1076
+ }
1077
+ }
1078
+ // 覆盖确认弹窗:居中轻量卡片(图标 + 文案 + 双按钮),替代 MessageBox 默认样式
1079
+ .mobile-confirm {
1080
+ display: flex;
1081
+ flex-direction: column;
1082
+ align-items: center;
1083
+ text-align: center;
1084
+ padding: 8px 8px 4px;
1085
+ &__icon {
1086
+ font-size: 40px;
1087
+ color: #e6a23c;
1088
+ margin-bottom: 12px;
1089
+ }
1090
+ &__text {
1091
+ margin: 0;
1092
+ font-size: 14px;
1093
+ line-height: 1.6;
1094
+ color: var(--epx-text, #333);
1095
+ }
1096
+ &__actions {
1097
+ display: flex;
1098
+ justify-content: center;
1099
+ gap: 12px;
1100
+ margin-top: 20px;
1101
+ }
1102
+ }
1103
+ </style>
1104
+
1105
+ <style lang="scss">
1106
+ // 抽屉传送到 body,只通过专用类调整,不影响其它抽屉。
1107
+ .mobile-appearance-drawer {
1108
+ .el-drawer__header {
1109
+ padding: 16px;
1110
+ margin: 0;
1111
+ border-bottom: 1px solid var(--epx-border);
1112
+ }
1113
+ .el-drawer__body {
1114
+ padding: 0;
1115
+ min-height: 0;
1116
+ }
1117
+ }
1118
+ </style>