@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.
- package/API.md +160 -160
- package/README.md +46 -46
- package/dist/src/page-designer/PageDesigner.js +11 -0
- package/dist/src/page-designer/designer.js +21 -0
- package/dist/src/page-designer/previews/add-form/Preview.js +1 -1
- package/dist/src/page-designer/previews/add-form/Preview.vue.js +19 -8
- package/dist/src/page-designer/setting-panel/Setting.js +46 -10
- package/dist/src/visual/config-page/ComponentLibraryDialog.js +8 -2
- package/dist/src/visual/config-page/DashboardMobile.js +1 -1
- package/dist/style.css +116 -112
- package/package.json +8 -8
- package/src/index.js +27 -27
- package/src/page-designer/PageDesigner.vue +11 -0
- package/src/page-designer/designer.js +1322 -1299
- package/src/page-designer/previews/add-form/Preview.vue +43 -44
- package/src/page-designer/setting-panel/Setting.vue +24 -2
- package/src/page-designer/widget-preview/Mixin.js +285 -285
- package/src/page-designer/widget-preview/Preview.vue +322 -322
- package/src/page-designer/widget-preview/WidgetWrapper.vue +197 -197
- package/src/page-designer/widget-preview/directive.js +258 -258
- package/src/visual/CategoryList.vue +563 -563
- package/src/visual/ComponentList.vue +280 -280
- package/src/visual/CreatePage.vue +389 -389
- package/src/visual/EntityCardList.vue +449 -449
- package/src/visual/EntityList.vue +838 -838
- package/src/visual/config-page/ComponentLibraryDialog.vue +7 -1
- package/src/visual/config-page/DashboardMobile.vue +934 -934
- package/src/visual/widgets/ProcessFormItemVisual.vue +58 -58
- package/types/index.d.ts +22 -22
- package/types/page-designer.d.ts +3 -3
- package/types/visual-component.d.ts +1 -1
- package/types/visual-entity-list.d.ts +1 -1
- package/types/visual-entity-sub-list.d.ts +1 -1
|
@@ -1,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
|
-
.
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
font-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
.mobile-body {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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>
|