@epxing/dashboard 5.3.0-beta.2 → 5.3.0-beta.3
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 +1 -1
- package/README.md +42 -42
- package/dist/src/Dashboard.js +1 -1
- package/dist/src/Widget.js +1 -1
- package/dist/style.css +8 -8
- package/package.json +5 -5
- package/src/Dashboard.vue +459 -459
- package/src/Widget.vue +354 -354
- package/src/index.js +28 -28
- package/types/index.d.ts +7 -7
- package/types/tabs.d.ts +1 -1
- package/types/title.d.ts +1 -1
- package/types/widget.d.ts +1 -1
package/src/Dashboard.vue
CHANGED
|
@@ -1,459 +1,459 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div :class="['epx-dashboard', 'epx-dashboard-'+ this.$component.id, 'epx-dashboard--'+ theme, { 'epx-dashboard--hidden-scroll-bar': $configs.scrollBarHidden, 'is-mobile-preview': mobileDashboard }]" :style="$component.style" ref="container">
|
|
3
|
-
<epx-dashboard-header @fullscreen="openFullScreen" :title="$component.title" :scopeFlag="$configs.scopeFlag" :globalSearchFields="$configs.globalSearchFields" :searchDefaultFlag="$configs.searchDefaultFlag" @search="search"></epx-dashboard-header>
|
|
4
|
-
<div ref="dashboard" class="epx-dashboard__container" :style="dashboardStyle" :class="{ 'epx-dashboard__container--mobile': mobileDashboard }">
|
|
5
|
-
<epx-dashboard-grid
|
|
6
|
-
:items="components"
|
|
7
|
-
:grid="gridParams"
|
|
8
|
-
:load-way="loadWay"
|
|
9
|
-
:body-height="bodyHeight">
|
|
10
|
-
<template #item="{ item: component }">
|
|
11
|
-
<!-- 渲染组件 -->
|
|
12
|
-
<div v-if="component.contentType" class="epx-dashboard__component-container">
|
|
13
|
-
<epx-component v-bind="isPage? $forwardAttrs($attrs): {}" class="epx-dashboard__component"
|
|
14
|
-
:name="component.contentType"
|
|
15
|
-
is-dashboard
|
|
16
|
-
:content-type="component.contentType"
|
|
17
|
-
:comp-id="component.id"
|
|
18
|
-
:comp-title="component.title"
|
|
19
|
-
:items="component.items"
|
|
20
|
-
:itemConfigs="$component.itemConfigs"
|
|
21
|
-
:comp-style="mobileComponentStyle(component)"
|
|
22
|
-
:comp-configs="component.configs"
|
|
23
|
-
:comp-grid="containerGrid(component)"
|
|
24
|
-
:chart-cache="$configs.chartCache"
|
|
25
|
-
:theme="themeMode"
|
|
26
|
-
:mobile-appearance="mobileRendering ? component.mobileAppearance : undefined"
|
|
27
|
-
:themeChartConfig="themeChartConfig">
|
|
28
|
-
</epx-component>
|
|
29
|
-
</div>
|
|
30
|
-
</template>
|
|
31
|
-
</epx-dashboard-grid>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
</template>
|
|
35
|
-
|
|
36
|
-
<script>
|
|
37
|
-
import DashboardHeader from './Header'
|
|
38
|
-
import DashboardGrid from './DashboardGrid.vue'
|
|
39
|
-
import chartRequestScope from './chart-request-scope'
|
|
40
|
-
import component from '@epxing/core/src/mixins/component'
|
|
41
|
-
import { pageGridMixin } from './grid/mixins'
|
|
42
|
-
import { buildThemeCss, buildThemeChartConfig } from './theme-style'
|
|
43
|
-
import { stackMobileLayout } from './layout-util'
|
|
44
|
-
import { indexDashboardItems, resolveMobileItem } from './mobile-design'
|
|
45
|
-
import { isLegacyConfig } from './grid/model'
|
|
46
|
-
import { mapState } from 'vuex'
|
|
47
|
-
|
|
48
|
-
// T31/T29:子树内 component 型全刷新。原用 `this.$children` 递归(Vue3 已移除)→
|
|
49
|
-
// 改走 T31 建的父子注册表 `_epxWalkChildren`(它本身就遍历整棵子树,故不需要外层递归)。
|
|
50
|
-
function refesh () {
|
|
51
|
-
this.resolveChartRequestBroker().invalidate()
|
|
52
|
-
this._epxWalkChildren(child => {
|
|
53
|
-
if (child.$options.$componentType === 'component' && child.$refesh) child.$refesh()
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
export default {
|
|
57
|
-
name: 'epx-dashboard',
|
|
58
|
-
mixins: [component, pageGridMixin, chartRequestScope],
|
|
59
|
-
components: {
|
|
60
|
-
[DashboardHeader.name]: DashboardHeader,
|
|
61
|
-
[DashboardGrid.name]: DashboardGrid
|
|
62
|
-
},
|
|
63
|
-
$componentType: 'page', // 组件类型: 页面
|
|
64
|
-
navConfigs: {
|
|
65
|
-
flag: false
|
|
66
|
-
},
|
|
67
|
-
style: {},
|
|
68
|
-
layout: 'grid',
|
|
69
|
-
// 组件列表
|
|
70
|
-
items: [
|
|
71
|
-
],
|
|
72
|
-
// 组件配置
|
|
73
|
-
itemConfigs: {
|
|
74
|
-
|
|
75
|
-
},
|
|
76
|
-
props: {
|
|
77
|
-
inline: Boolean,
|
|
78
|
-
routeKey: [String, Number],
|
|
79
|
-
routes: {
|
|
80
|
-
type: Array,
|
|
81
|
-
default () {
|
|
82
|
-
return []
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
data () {
|
|
87
|
-
return {
|
|
88
|
-
headerTitle: null,
|
|
89
|
-
pageParams: {},
|
|
90
|
-
model: {}, // 组件全局data
|
|
91
|
-
pageEvents: {},
|
|
92
|
-
searchScopeParams: [],
|
|
93
|
-
themeMode: null,
|
|
94
|
-
themeStyles: {}
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
computed: {
|
|
98
|
-
...mapState({
|
|
99
|
-
showMenu: state => state.config.showMenu,
|
|
100
|
-
showAppNav: state => state.config.showAppNav
|
|
101
|
-
}),
|
|
102
|
-
pageTitle () {
|
|
103
|
-
return this.headerTitle || (this.page && this.page.title) || (this.meta && this.meta.name)
|
|
104
|
-
},
|
|
105
|
-
components () {
|
|
106
|
-
// items itemConfigs
|
|
107
|
-
const { items = [], mItems } = this.$component
|
|
108
|
-
let list
|
|
109
|
-
if (this.mobileDashboard) {
|
|
110
|
-
list = items.filter(item => !item.mobileHidden)
|
|
111
|
-
} else if (this.$isMobile) {
|
|
112
|
-
// 移动端:优先用单独配置的 mItems(过滤掉标记 mobileHidden 的项);没配则按 PC 布局自动单列堆叠
|
|
113
|
-
if (mItems && mItems.length) {
|
|
114
|
-
const pcIndex = indexDashboardItems(items)
|
|
115
|
-
list = mItems.filter(c => !c.mobileHidden && pcIndex.has(String(c.id))).map(c => resolveMobileItem(c, pcIndex))
|
|
116
|
-
} else {
|
|
117
|
-
const { mobileAutoStack = true } = this.$configs
|
|
118
|
-
list = mobileAutoStack ? stackMobileLayout(items, 48) : items
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
list = items
|
|
122
|
-
}
|
|
123
|
-
// 布局项必须带 i(设计器与旧配置都以 id 为准),运行时用 id 兜底补齐
|
|
124
|
-
list.forEach(item => {
|
|
125
|
-
if (item && item.i == null && item.id != null) item.i = item.id
|
|
126
|
-
})
|
|
127
|
-
return list
|
|
128
|
-
},
|
|
129
|
-
// 移动端组件样式:以 PC 组件 style 为基础,叠加 mItems 项里设置的移动端覆盖(空值/未设的键沿用 PC)
|
|
130
|
-
mobileComponentStyle () {
|
|
131
|
-
return component => {
|
|
132
|
-
if (!this.$isMobile || this.mobileDashboard) return component.style
|
|
133
|
-
const pcItem = indexDashboardItems(this.$component.items || []).get(String(component.id)) || {}
|
|
134
|
-
const override = {}
|
|
135
|
-
const cur = component.style || {}
|
|
136
|
-
for (const k in cur) {
|
|
137
|
-
if (cur[k] !== undefined && cur[k] !== null && cur[k] !== '') override[k] = cur[k]
|
|
138
|
-
}
|
|
139
|
-
return { ...(pcItem.style || {}), ...override }
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
theme () {
|
|
143
|
-
const { theme } = this.$configs
|
|
144
|
-
return theme
|
|
145
|
-
},
|
|
146
|
-
colAttribute () {
|
|
147
|
-
return (component = {}) => {
|
|
148
|
-
const { xsspan = 24, ...other } = component
|
|
149
|
-
const colAttribute = {
|
|
150
|
-
...other,
|
|
151
|
-
xs: {
|
|
152
|
-
span: xsspan
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
delete colAttribute.id
|
|
156
|
-
delete colAttribute.style
|
|
157
|
-
delete colAttribute.title
|
|
158
|
-
return colAttribute
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
pageStyle () {
|
|
162
|
-
const { layout = 'grid', direction = 'row', justify = 'flex-start', align = 'top' } = this.$configs
|
|
163
|
-
if (layout === 'flex') {
|
|
164
|
-
return { display: 'flex', 'flex-direction': direction, 'justify-content': justify, 'align-items': align }
|
|
165
|
-
} else {
|
|
166
|
-
return { }
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
dashboardStyle () {
|
|
170
|
-
const { backgroundColor, backgroundImage } = this.$configs
|
|
171
|
-
return {
|
|
172
|
-
...(this.mobileDashboard ? { width: '100%', maxWidth: `${Number(this.$configs.mobileCanvasWidth) || 375}px`, margin: '0 auto', flex: 'none' } : {}),
|
|
173
|
-
backgroundColor,
|
|
174
|
-
backgroundImage: backgroundImage ? `url(${this.$util.getFileUrl(backgroundImage)})` : null
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
componentStyle () {
|
|
178
|
-
return component => {
|
|
179
|
-
const { layout } = this.$configs
|
|
180
|
-
const { flex } = component
|
|
181
|
-
const compoentStyle = { }
|
|
182
|
-
if (layout === 'flex' && flex) {
|
|
183
|
-
compoentStyle.flex = 1
|
|
184
|
-
}
|
|
185
|
-
return compoentStyle
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
scopeParams () {
|
|
189
|
-
const { globalScopeParams: configScopeParams = [] } = this.$configs
|
|
190
|
-
const { scopeParams: queryScopeParams = [] } = this.$query
|
|
191
|
-
|
|
192
|
-
// 格式化scopeParams
|
|
193
|
-
const _scopeParams = this.$_.cloneDeep([...configScopeParams, ...queryScopeParams, ...this.searchScopeParams])
|
|
194
|
-
return this.$formatScope(_scopeParams)
|
|
195
|
-
},
|
|
196
|
-
themeChartConfig () {
|
|
197
|
-
return buildThemeChartConfig(this.themeStyles, this.mobileRendering)
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
watch: {
|
|
201
|
-
pageTitle: {
|
|
202
|
-
handler (v) {
|
|
203
|
-
this.$emit('change-route', this.routeKey, { title: v })
|
|
204
|
-
},
|
|
205
|
-
immediate: true
|
|
206
|
-
},
|
|
207
|
-
scopeParams: {
|
|
208
|
-
deep: true,
|
|
209
|
-
handler (v) {
|
|
210
|
-
if (!this.$_.isEqual(this.oldParams, v)) {
|
|
211
|
-
this.pageParams.scopeParams = v
|
|
212
|
-
}
|
|
213
|
-
this.oldParams = v
|
|
214
|
-
},
|
|
215
|
-
immediate: true
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
provide () {
|
|
219
|
-
// 主题图表配置(系列色板等)用函数式 provide 下发,跨任意容器层(dashboard-div/tabs)注入到图表组件,且保持响应式
|
|
220
|
-
// 恒定返回对象(不再区分 isPage 分支返回 undefined):顺带满足 T28 的「Vue3 provide 合并不容忍 undefined」
|
|
221
|
-
const provideData = {
|
|
222
|
-
getMobileDashboard: () => this.mobileDashboard,
|
|
223
|
-
getThemeChartConfig: () => this.themeChartConfig,
|
|
224
|
-
// 容器内网格的门控策略 / 配置是否旧版都跟随页面(provide 只跑一次,用函数延迟读)
|
|
225
|
-
getDashboardLoadWay: () => this.loadWay,
|
|
226
|
-
getDashboardLegacy: () => isLegacyConfig(this.$configs)
|
|
227
|
-
}
|
|
228
|
-
if (this.isPage) {
|
|
229
|
-
provideData.upPage = this
|
|
230
|
-
provideData.pageId = this.page ? this.page.pageId : '' // 当前页面id
|
|
231
|
-
provideData.pageMetaId = this.isPage ? (this.$metaId || (this.page ? this.page.metaId : '')) : null // 当前页面对象id
|
|
232
|
-
}
|
|
233
|
-
return provideData
|
|
234
|
-
},
|
|
235
|
-
methods: {
|
|
236
|
-
getTheme () {
|
|
237
|
-
// 未选主题时直接跳过查询:scope.CODE 为空会命中 FM_CHART_THEME 全部记录,
|
|
238
|
-
// 后端按单条返回会报「存在多条结果数据」,抛出后 setThemeStyle 中断,
|
|
239
|
-
// 连页面自身配置的 themeStyles 都编译不出来(主题样式整体不生效)
|
|
240
|
-
if (!this.theme) return Promise.resolve({})
|
|
241
|
-
return this.$http.find({
|
|
242
|
-
entityId: 'FM_CHART_THEME',
|
|
243
|
-
'scope.CODE': this.theme
|
|
244
|
-
})
|
|
245
|
-
.then(data => {
|
|
246
|
-
if (data && data.data.CONTENT) {
|
|
247
|
-
this.themeMode = data.data.STYLE
|
|
248
|
-
return JSON.parse(data?.data.CONTENT)
|
|
249
|
-
}
|
|
250
|
-
return {}
|
|
251
|
-
})
|
|
252
|
-
},
|
|
253
|
-
async setThemeStyle () {
|
|
254
|
-
// 获取主题对应的样式
|
|
255
|
-
const { themeStyles: configThemeStyles = {} } = this.$configs
|
|
256
|
-
const activeThemeStyles = await this.getTheme()
|
|
257
|
-
const allThemeStyles = this.$_.assignWith(configThemeStyles, activeThemeStyles, (configThemeItem, themeItem) => {
|
|
258
|
-
// 仅对对象做分区合并,customCss 等字符串值直接取优先级高的一方
|
|
259
|
-
return configThemeItem && themeItem && typeof configThemeItem === 'object' && typeof themeItem === 'object'
|
|
260
|
-
? { ...themeItem, ...configThemeItem }
|
|
261
|
-
: configThemeItem || themeItem
|
|
262
|
-
})
|
|
263
|
-
this.themeStyles = allThemeStyles
|
|
264
|
-
const styles = buildThemeCss(allThemeStyles, `.epx-dashboard-${this.$component.id}`, path => this.$util.getFileUrl(path), { mobilePreview: this.mobileDashboard })
|
|
265
|
-
let styleElement = document.head.querySelector(`#dashboard-${this.$component.id}`)
|
|
266
|
-
if (!styleElement) {
|
|
267
|
-
styleElement = document.createElement('style')
|
|
268
|
-
styleElement.id = `dashboard-${this.$component.id}`
|
|
269
|
-
styleElement.type = 'text/css'
|
|
270
|
-
styleElement.textContent = styles
|
|
271
|
-
document.head.appendChild(styleElement)
|
|
272
|
-
} else {
|
|
273
|
-
styleElement.textContent = styles
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
removeThemeStyle () {
|
|
277
|
-
const styleElement = document.head.querySelector(`#dashboard-${this.$component.id}`)
|
|
278
|
-
styleElement?.remove()
|
|
279
|
-
},
|
|
280
|
-
// reloadPage 会经 RouterView 以新 key 重建整个页面实例,滚动位置无法在此恢复(旧实例的 $refs 已失效)
|
|
281
|
-
$refesh () {
|
|
282
|
-
refesh.apply(this)
|
|
283
|
-
if (this.upPage) this.upPage.$refesh()
|
|
284
|
-
this.reloadPage()
|
|
285
|
-
},
|
|
286
|
-
enableAutoReload () {
|
|
287
|
-
if (this.refreshInterval) {
|
|
288
|
-
clearInterval(this.refreshInterval)
|
|
289
|
-
}
|
|
290
|
-
this.refreshInterval = setInterval(() => {
|
|
291
|
-
refesh.apply(this)
|
|
292
|
-
}, (this.$configs.refreshInterval || 60) * 1000)
|
|
293
|
-
},
|
|
294
|
-
openFullScreen () {
|
|
295
|
-
if (this.$refs.dashboard.requestFullscreen) {
|
|
296
|
-
this.$refs.dashboard.requestFullscreen()
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
search (v) {
|
|
300
|
-
this.searchScopeParams = v
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
created () {
|
|
304
|
-
// 这里原本存了 showAppNav/showMenu 用于卸载时「还原」,已删除,理由见 beforeUnmount 的注释。
|
|
305
|
-
// 行高 / 间距按视口等比(旧 autoHeight)时才需要跟随窗口尺寸
|
|
306
|
-
const grid = this.gridParams
|
|
307
|
-
if (grid.rowHeight === 'auto' || grid.padding === 'auto') {
|
|
308
|
-
window.addEventListener('resize', this.updateBodyHeight)
|
|
309
|
-
}
|
|
310
|
-
if (this.$configs.autoRefresh) {
|
|
311
|
-
this.enableAutoReload()
|
|
312
|
-
}
|
|
313
|
-
this.setThemeStyle()
|
|
314
|
-
},
|
|
315
|
-
|
|
316
|
-
beforeUnmount () {
|
|
317
|
-
// 与 Widget.vue 同一处问题,已删除两行全局写入。三个理由:
|
|
318
|
-
// ① 整个 packages/dashboard 没有任何地方隐藏菜单/应用导航(openFullScreen 走原生 requestFullscreen),
|
|
319
|
-
// 这是**只还原、从不获取**的全局写入;
|
|
320
|
-
// ② created 里存的值是交叉的(defaultShowMenu 存的是 showAppNav,反之亦然),还原本身就是错的;
|
|
321
|
-
// ③ 标签是 v-for + v-show 全挂载,关**后台**标签或 LRU 淘汰内容都会走到这里 ——
|
|
322
|
-
// 表现为「关掉一个大屏标签,把另一个正在看的标签的菜单弹了出来」。
|
|
323
|
-
// 这段此前从未执行(keep-alive 的页不真卸载),M0 改成真卸载后才被唤醒。
|
|
324
|
-
window.removeEventListener('resize', this.updateBodyHeight)
|
|
325
|
-
this.removeThemeStyle()
|
|
326
|
-
// 清理自动刷新定时器
|
|
327
|
-
if (this.refreshInterval) {
|
|
328
|
-
clearInterval(this.refreshInterval)
|
|
329
|
-
this.refreshInterval = null
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
</script>
|
|
334
|
-
<style lang="scss">
|
|
335
|
-
@include b(dashboard) {
|
|
336
|
-
width: 100%;
|
|
337
|
-
height: 100%;
|
|
338
|
-
overflow: auto;
|
|
339
|
-
overflow-x: hidden;
|
|
340
|
-
display: flex;
|
|
341
|
-
flex-direction: column;
|
|
342
|
-
@include m(hidden-scroll-bar) {
|
|
343
|
-
@include e(container) {
|
|
344
|
-
&::-webkit-scrollbar-thumb {
|
|
345
|
-
background: none;
|
|
346
|
-
&:hover {
|
|
347
|
-
background-color: rgba(95, 95, 95, .3);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
@include e(container) {
|
|
353
|
-
// background: #f0f2f6;
|
|
354
|
-
width: 100%;
|
|
355
|
-
flex: 1;
|
|
356
|
-
overflow: auto;
|
|
357
|
-
}
|
|
358
|
-
@include e(component-container) {
|
|
359
|
-
width: 100%;
|
|
360
|
-
height: 100%;
|
|
361
|
-
// 不裁剪子组件的 box-shadow:阴影渲染进栅格间距;滚动下移到 __component 自身
|
|
362
|
-
overflow: visible;
|
|
363
|
-
}
|
|
364
|
-
@include e(component) {
|
|
365
|
-
height: 100%;
|
|
366
|
-
// 自身内容溢出仍可滚动;元素自身的 box-shadow 不受自身 overflow 裁剪
|
|
367
|
-
overflow: auto;
|
|
368
|
-
}
|
|
369
|
-
.epx-card-num__title {
|
|
370
|
-
color: inher
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
// 移动端专属缩放:收紧卡片内边距、缩小标题与数字字号,让密集看板在小屏更耐看
|
|
374
|
-
@media only screen and (max-width: 992px) {
|
|
375
|
-
@include b(dashboard) {
|
|
376
|
-
.epx-card-num {
|
|
377
|
-
padding: 12px;
|
|
378
|
-
}
|
|
379
|
-
.epx-card-num__title {
|
|
380
|
-
font-size: 13px;
|
|
381
|
-
}
|
|
382
|
-
.epx-card-num__num > font {
|
|
383
|
-
font-size: 22px;
|
|
384
|
-
}
|
|
385
|
-
.epx-chart__title {
|
|
386
|
-
font-size: 14px;
|
|
387
|
-
}
|
|
388
|
-
.epx-chart__header {
|
|
389
|
-
padding: 0 8px;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
</style>
|
|
394
|
-
<style lang="scss" scoped>
|
|
395
|
-
|
|
396
|
-
// .dashboard--hidden-scroll-bar {
|
|
397
|
-
// .dashboard-container {
|
|
398
|
-
// &::-webkit-scrollbar-thumb {
|
|
399
|
-
// background: none;
|
|
400
|
-
// &:hover {
|
|
401
|
-
// background-color: rgba(95, 95, 95, .3);
|
|
402
|
-
// }
|
|
403
|
-
// }
|
|
404
|
-
// }
|
|
405
|
-
// }
|
|
406
|
-
// .dashboard-container {
|
|
407
|
-
|
|
408
|
-
// }
|
|
409
|
-
// .dashboard-component {
|
|
410
|
-
|
|
411
|
-
// }
|
|
412
|
-
|
|
413
|
-
// .dashboard--dark {
|
|
414
|
-
// .dashboard-container{
|
|
415
|
-
// background: #151C3D;
|
|
416
|
-
// .dashboard-component {
|
|
417
|
-
// >div {
|
|
418
|
-
// // background: rgba(255,255,255, .1)
|
|
419
|
-
// }
|
|
420
|
-
// }
|
|
421
|
-
// }
|
|
422
|
-
// }
|
|
423
|
-
// .dashboard--technology {
|
|
424
|
-
// .dashboard-container{
|
|
425
|
-
// background: url('@/assets/images/dashboard/technology_bg.jpg') no-repeat center;
|
|
426
|
-
// background-size: cover;
|
|
427
|
-
|
|
428
|
-
// }
|
|
429
|
-
// .component {
|
|
430
|
-
// // position: relative;
|
|
431
|
-
// // &::before {
|
|
432
|
-
// // content: '';
|
|
433
|
-
// // position: absolute;
|
|
434
|
-
// // top: 0;
|
|
435
|
-
// // display: block;
|
|
436
|
-
// // width: 100%;
|
|
437
|
-
// // height: 1px;
|
|
438
|
-
// // background: linear-gradient(90deg, rgba(45, 164, 247, 0) 0%, #2DA4F7 50%, rgba(45, 164, 247, 0) 100%);
|
|
439
|
-
// // opacity: 0.5;
|
|
440
|
-
// // }
|
|
441
|
-
// // &::after {
|
|
442
|
-
// // content: '';
|
|
443
|
-
// // position: absolute;
|
|
444
|
-
// // bottom: 0;
|
|
445
|
-
// // display: block;
|
|
446
|
-
// // width: 100%;
|
|
447
|
-
// // height: 1px;
|
|
448
|
-
// // background: linear-gradient(90deg, rgba(45, 164, 247, 0) 0%, #2DA4F7 50%, rgba(45, 164, 247, 0) 100%);
|
|
449
|
-
// // opacity: 0.5;
|
|
450
|
-
// // }
|
|
451
|
-
// // background: rgba(255,255,255, .1);
|
|
452
|
-
// // backdrop-filter: blur(4px);
|
|
453
|
-
// // >div {
|
|
454
|
-
// // background: rgba(255,255,255, .1);
|
|
455
|
-
// // backdrop-filter: blur(4px);
|
|
456
|
-
// // }
|
|
457
|
-
// }
|
|
458
|
-
// }
|
|
459
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="['epx-dashboard', 'epx-dashboard-'+ this.$component.id, 'epx-dashboard--'+ theme, { 'epx-dashboard--hidden-scroll-bar': $configs.scrollBarHidden, 'is-mobile-preview': mobileDashboard }]" :style="$component.style" ref="container">
|
|
3
|
+
<epx-dashboard-header @fullscreen="openFullScreen" :title="$component.title" :scopeFlag="$configs.scopeFlag" :globalSearchFields="$configs.globalSearchFields" :searchDefaultFlag="$configs.searchDefaultFlag" @search="search"></epx-dashboard-header>
|
|
4
|
+
<div ref="dashboard" class="epx-dashboard__container" :style="dashboardStyle" :class="{ 'epx-dashboard__container--mobile': mobileDashboard }">
|
|
5
|
+
<epx-dashboard-grid
|
|
6
|
+
:items="components"
|
|
7
|
+
:grid="gridParams"
|
|
8
|
+
:load-way="loadWay"
|
|
9
|
+
:body-height="bodyHeight">
|
|
10
|
+
<template #item="{ item: component }">
|
|
11
|
+
<!-- 渲染组件 -->
|
|
12
|
+
<div v-if="component.contentType" class="epx-dashboard__component-container">
|
|
13
|
+
<epx-component v-bind="isPage? $forwardAttrs($attrs): {}" class="epx-dashboard__component"
|
|
14
|
+
:name="component.contentType"
|
|
15
|
+
is-dashboard
|
|
16
|
+
:content-type="component.contentType"
|
|
17
|
+
:comp-id="component.id"
|
|
18
|
+
:comp-title="component.title"
|
|
19
|
+
:items="component.items"
|
|
20
|
+
:itemConfigs="$component.itemConfigs"
|
|
21
|
+
:comp-style="mobileComponentStyle(component)"
|
|
22
|
+
:comp-configs="component.configs"
|
|
23
|
+
:comp-grid="containerGrid(component)"
|
|
24
|
+
:chart-cache="$configs.chartCache"
|
|
25
|
+
:theme="themeMode"
|
|
26
|
+
:mobile-appearance="mobileRendering ? component.mobileAppearance : undefined"
|
|
27
|
+
:themeChartConfig="themeChartConfig">
|
|
28
|
+
</epx-component>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
</epx-dashboard-grid>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
import DashboardHeader from './Header'
|
|
38
|
+
import DashboardGrid from './DashboardGrid.vue'
|
|
39
|
+
import chartRequestScope from './chart-request-scope'
|
|
40
|
+
import component from '@epxing/core/src/mixins/component'
|
|
41
|
+
import { pageGridMixin } from './grid/mixins'
|
|
42
|
+
import { buildThemeCss, buildThemeChartConfig } from './theme-style'
|
|
43
|
+
import { stackMobileLayout } from './layout-util'
|
|
44
|
+
import { indexDashboardItems, resolveMobileItem } from './mobile-design'
|
|
45
|
+
import { isLegacyConfig } from './grid/model'
|
|
46
|
+
import { mapState } from 'vuex'
|
|
47
|
+
|
|
48
|
+
// T31/T29:子树内 component 型全刷新。原用 `this.$children` 递归(Vue3 已移除)→
|
|
49
|
+
// 改走 T31 建的父子注册表 `_epxWalkChildren`(它本身就遍历整棵子树,故不需要外层递归)。
|
|
50
|
+
function refesh () {
|
|
51
|
+
this.resolveChartRequestBroker().invalidate()
|
|
52
|
+
this._epxWalkChildren(child => {
|
|
53
|
+
if (child.$options.$componentType === 'component' && child.$refesh) child.$refesh()
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
export default {
|
|
57
|
+
name: 'epx-dashboard',
|
|
58
|
+
mixins: [component, pageGridMixin, chartRequestScope],
|
|
59
|
+
components: {
|
|
60
|
+
[DashboardHeader.name]: DashboardHeader,
|
|
61
|
+
[DashboardGrid.name]: DashboardGrid
|
|
62
|
+
},
|
|
63
|
+
$componentType: 'page', // 组件类型: 页面
|
|
64
|
+
navConfigs: {
|
|
65
|
+
flag: false
|
|
66
|
+
},
|
|
67
|
+
style: {},
|
|
68
|
+
layout: 'grid',
|
|
69
|
+
// 组件列表
|
|
70
|
+
items: [
|
|
71
|
+
],
|
|
72
|
+
// 组件配置
|
|
73
|
+
itemConfigs: {
|
|
74
|
+
|
|
75
|
+
},
|
|
76
|
+
props: {
|
|
77
|
+
inline: Boolean,
|
|
78
|
+
routeKey: [String, Number],
|
|
79
|
+
routes: {
|
|
80
|
+
type: Array,
|
|
81
|
+
default () {
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
data () {
|
|
87
|
+
return {
|
|
88
|
+
headerTitle: null,
|
|
89
|
+
pageParams: {},
|
|
90
|
+
model: {}, // 组件全局data
|
|
91
|
+
pageEvents: {},
|
|
92
|
+
searchScopeParams: [],
|
|
93
|
+
themeMode: null,
|
|
94
|
+
themeStyles: {}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
computed: {
|
|
98
|
+
...mapState({
|
|
99
|
+
showMenu: state => state.config.showMenu,
|
|
100
|
+
showAppNav: state => state.config.showAppNav
|
|
101
|
+
}),
|
|
102
|
+
pageTitle () {
|
|
103
|
+
return this.headerTitle || (this.page && this.page.title) || (this.meta && this.meta.name)
|
|
104
|
+
},
|
|
105
|
+
components () {
|
|
106
|
+
// items itemConfigs
|
|
107
|
+
const { items = [], mItems } = this.$component
|
|
108
|
+
let list
|
|
109
|
+
if (this.mobileDashboard) {
|
|
110
|
+
list = items.filter(item => !item.mobileHidden)
|
|
111
|
+
} else if (this.$isMobile) {
|
|
112
|
+
// 移动端:优先用单独配置的 mItems(过滤掉标记 mobileHidden 的项);没配则按 PC 布局自动单列堆叠
|
|
113
|
+
if (mItems && mItems.length) {
|
|
114
|
+
const pcIndex = indexDashboardItems(items)
|
|
115
|
+
list = mItems.filter(c => !c.mobileHidden && pcIndex.has(String(c.id))).map(c => resolveMobileItem(c, pcIndex))
|
|
116
|
+
} else {
|
|
117
|
+
const { mobileAutoStack = true } = this.$configs
|
|
118
|
+
list = mobileAutoStack ? stackMobileLayout(items, 48) : items
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
list = items
|
|
122
|
+
}
|
|
123
|
+
// 布局项必须带 i(设计器与旧配置都以 id 为准),运行时用 id 兜底补齐
|
|
124
|
+
list.forEach(item => {
|
|
125
|
+
if (item && item.i == null && item.id != null) item.i = item.id
|
|
126
|
+
})
|
|
127
|
+
return list
|
|
128
|
+
},
|
|
129
|
+
// 移动端组件样式:以 PC 组件 style 为基础,叠加 mItems 项里设置的移动端覆盖(空值/未设的键沿用 PC)
|
|
130
|
+
mobileComponentStyle () {
|
|
131
|
+
return component => {
|
|
132
|
+
if (!this.$isMobile || this.mobileDashboard) return component.style
|
|
133
|
+
const pcItem = indexDashboardItems(this.$component.items || []).get(String(component.id)) || {}
|
|
134
|
+
const override = {}
|
|
135
|
+
const cur = component.style || {}
|
|
136
|
+
for (const k in cur) {
|
|
137
|
+
if (cur[k] !== undefined && cur[k] !== null && cur[k] !== '') override[k] = cur[k]
|
|
138
|
+
}
|
|
139
|
+
return { ...(pcItem.style || {}), ...override }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
theme () {
|
|
143
|
+
const { theme } = this.$configs
|
|
144
|
+
return theme
|
|
145
|
+
},
|
|
146
|
+
colAttribute () {
|
|
147
|
+
return (component = {}) => {
|
|
148
|
+
const { xsspan = 24, ...other } = component
|
|
149
|
+
const colAttribute = {
|
|
150
|
+
...other,
|
|
151
|
+
xs: {
|
|
152
|
+
span: xsspan
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
delete colAttribute.id
|
|
156
|
+
delete colAttribute.style
|
|
157
|
+
delete colAttribute.title
|
|
158
|
+
return colAttribute
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
pageStyle () {
|
|
162
|
+
const { layout = 'grid', direction = 'row', justify = 'flex-start', align = 'top' } = this.$configs
|
|
163
|
+
if (layout === 'flex') {
|
|
164
|
+
return { display: 'flex', 'flex-direction': direction, 'justify-content': justify, 'align-items': align }
|
|
165
|
+
} else {
|
|
166
|
+
return { }
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
dashboardStyle () {
|
|
170
|
+
const { backgroundColor, backgroundImage } = this.$configs
|
|
171
|
+
return {
|
|
172
|
+
...(this.mobileDashboard ? { width: '100%', maxWidth: `${Number(this.$configs.mobileCanvasWidth) || 375}px`, margin: '0 auto', flex: 'none' } : {}),
|
|
173
|
+
backgroundColor,
|
|
174
|
+
backgroundImage: backgroundImage ? `url(${this.$util.getFileUrl(backgroundImage)})` : null
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
componentStyle () {
|
|
178
|
+
return component => {
|
|
179
|
+
const { layout } = this.$configs
|
|
180
|
+
const { flex } = component
|
|
181
|
+
const compoentStyle = { }
|
|
182
|
+
if (layout === 'flex' && flex) {
|
|
183
|
+
compoentStyle.flex = 1
|
|
184
|
+
}
|
|
185
|
+
return compoentStyle
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
scopeParams () {
|
|
189
|
+
const { globalScopeParams: configScopeParams = [] } = this.$configs
|
|
190
|
+
const { scopeParams: queryScopeParams = [] } = this.$query
|
|
191
|
+
|
|
192
|
+
// 格式化scopeParams
|
|
193
|
+
const _scopeParams = this.$_.cloneDeep([...configScopeParams, ...queryScopeParams, ...this.searchScopeParams])
|
|
194
|
+
return this.$formatScope(_scopeParams)
|
|
195
|
+
},
|
|
196
|
+
themeChartConfig () {
|
|
197
|
+
return buildThemeChartConfig(this.themeStyles, this.mobileRendering)
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
watch: {
|
|
201
|
+
pageTitle: {
|
|
202
|
+
handler (v) {
|
|
203
|
+
this.$emit('change-route', this.routeKey, { title: v })
|
|
204
|
+
},
|
|
205
|
+
immediate: true
|
|
206
|
+
},
|
|
207
|
+
scopeParams: {
|
|
208
|
+
deep: true,
|
|
209
|
+
handler (v) {
|
|
210
|
+
if (!this.$_.isEqual(this.oldParams, v)) {
|
|
211
|
+
this.pageParams.scopeParams = v
|
|
212
|
+
}
|
|
213
|
+
this.oldParams = v
|
|
214
|
+
},
|
|
215
|
+
immediate: true
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
provide () {
|
|
219
|
+
// 主题图表配置(系列色板等)用函数式 provide 下发,跨任意容器层(dashboard-div/tabs)注入到图表组件,且保持响应式
|
|
220
|
+
// 恒定返回对象(不再区分 isPage 分支返回 undefined):顺带满足 T28 的「Vue3 provide 合并不容忍 undefined」
|
|
221
|
+
const provideData = {
|
|
222
|
+
getMobileDashboard: () => this.mobileDashboard,
|
|
223
|
+
getThemeChartConfig: () => this.themeChartConfig,
|
|
224
|
+
// 容器内网格的门控策略 / 配置是否旧版都跟随页面(provide 只跑一次,用函数延迟读)
|
|
225
|
+
getDashboardLoadWay: () => this.loadWay,
|
|
226
|
+
getDashboardLegacy: () => isLegacyConfig(this.$configs)
|
|
227
|
+
}
|
|
228
|
+
if (this.isPage) {
|
|
229
|
+
provideData.upPage = this
|
|
230
|
+
provideData.pageId = this.page ? this.page.pageId : '' // 当前页面id
|
|
231
|
+
provideData.pageMetaId = this.isPage ? (this.$metaId || (this.page ? this.page.metaId : '')) : null // 当前页面对象id
|
|
232
|
+
}
|
|
233
|
+
return provideData
|
|
234
|
+
},
|
|
235
|
+
methods: {
|
|
236
|
+
getTheme () {
|
|
237
|
+
// 未选主题时直接跳过查询:scope.CODE 为空会命中 FM_CHART_THEME 全部记录,
|
|
238
|
+
// 后端按单条返回会报「存在多条结果数据」,抛出后 setThemeStyle 中断,
|
|
239
|
+
// 连页面自身配置的 themeStyles 都编译不出来(主题样式整体不生效)
|
|
240
|
+
if (!this.theme) return Promise.resolve({})
|
|
241
|
+
return this.$http.find({
|
|
242
|
+
entityId: 'FM_CHART_THEME',
|
|
243
|
+
'scope.CODE': this.theme
|
|
244
|
+
})
|
|
245
|
+
.then(data => {
|
|
246
|
+
if (data && data.data.CONTENT) {
|
|
247
|
+
this.themeMode = data.data.STYLE
|
|
248
|
+
return JSON.parse(data?.data.CONTENT)
|
|
249
|
+
}
|
|
250
|
+
return {}
|
|
251
|
+
})
|
|
252
|
+
},
|
|
253
|
+
async setThemeStyle () {
|
|
254
|
+
// 获取主题对应的样式
|
|
255
|
+
const { themeStyles: configThemeStyles = {} } = this.$configs
|
|
256
|
+
const activeThemeStyles = await this.getTheme()
|
|
257
|
+
const allThemeStyles = this.$_.assignWith(configThemeStyles, activeThemeStyles, (configThemeItem, themeItem) => {
|
|
258
|
+
// 仅对对象做分区合并,customCss 等字符串值直接取优先级高的一方
|
|
259
|
+
return configThemeItem && themeItem && typeof configThemeItem === 'object' && typeof themeItem === 'object'
|
|
260
|
+
? { ...themeItem, ...configThemeItem }
|
|
261
|
+
: configThemeItem || themeItem
|
|
262
|
+
})
|
|
263
|
+
this.themeStyles = allThemeStyles
|
|
264
|
+
const styles = buildThemeCss(allThemeStyles, `.epx-dashboard-${this.$component.id}`, path => this.$util.getFileUrl(path), { mobilePreview: this.mobileDashboard })
|
|
265
|
+
let styleElement = document.head.querySelector(`#dashboard-${this.$component.id}`)
|
|
266
|
+
if (!styleElement) {
|
|
267
|
+
styleElement = document.createElement('style')
|
|
268
|
+
styleElement.id = `dashboard-${this.$component.id}`
|
|
269
|
+
styleElement.type = 'text/css'
|
|
270
|
+
styleElement.textContent = styles
|
|
271
|
+
document.head.appendChild(styleElement)
|
|
272
|
+
} else {
|
|
273
|
+
styleElement.textContent = styles
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
removeThemeStyle () {
|
|
277
|
+
const styleElement = document.head.querySelector(`#dashboard-${this.$component.id}`)
|
|
278
|
+
styleElement?.remove()
|
|
279
|
+
},
|
|
280
|
+
// reloadPage 会经 RouterView 以新 key 重建整个页面实例,滚动位置无法在此恢复(旧实例的 $refs 已失效)
|
|
281
|
+
$refesh () {
|
|
282
|
+
refesh.apply(this)
|
|
283
|
+
if (this.upPage) this.upPage.$refesh()
|
|
284
|
+
this.reloadPage()
|
|
285
|
+
},
|
|
286
|
+
enableAutoReload () {
|
|
287
|
+
if (this.refreshInterval) {
|
|
288
|
+
clearInterval(this.refreshInterval)
|
|
289
|
+
}
|
|
290
|
+
this.refreshInterval = setInterval(() => {
|
|
291
|
+
refesh.apply(this)
|
|
292
|
+
}, (this.$configs.refreshInterval || 60) * 1000)
|
|
293
|
+
},
|
|
294
|
+
openFullScreen () {
|
|
295
|
+
if (this.$refs.dashboard.requestFullscreen) {
|
|
296
|
+
this.$refs.dashboard.requestFullscreen()
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
search (v) {
|
|
300
|
+
this.searchScopeParams = v
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
created () {
|
|
304
|
+
// 这里原本存了 showAppNav/showMenu 用于卸载时「还原」,已删除,理由见 beforeUnmount 的注释。
|
|
305
|
+
// 行高 / 间距按视口等比(旧 autoHeight)时才需要跟随窗口尺寸
|
|
306
|
+
const grid = this.gridParams
|
|
307
|
+
if (grid.rowHeight === 'auto' || grid.padding === 'auto') {
|
|
308
|
+
window.addEventListener('resize', this.updateBodyHeight)
|
|
309
|
+
}
|
|
310
|
+
if (this.$configs.autoRefresh) {
|
|
311
|
+
this.enableAutoReload()
|
|
312
|
+
}
|
|
313
|
+
this.setThemeStyle()
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
beforeUnmount () {
|
|
317
|
+
// 与 Widget.vue 同一处问题,已删除两行全局写入。三个理由:
|
|
318
|
+
// ① 整个 packages/dashboard 没有任何地方隐藏菜单/应用导航(openFullScreen 走原生 requestFullscreen),
|
|
319
|
+
// 这是**只还原、从不获取**的全局写入;
|
|
320
|
+
// ② created 里存的值是交叉的(defaultShowMenu 存的是 showAppNav,反之亦然),还原本身就是错的;
|
|
321
|
+
// ③ 标签是 v-for + v-show 全挂载,关**后台**标签或 LRU 淘汰内容都会走到这里 ——
|
|
322
|
+
// 表现为「关掉一个大屏标签,把另一个正在看的标签的菜单弹了出来」。
|
|
323
|
+
// 这段此前从未执行(keep-alive 的页不真卸载),M0 改成真卸载后才被唤醒。
|
|
324
|
+
window.removeEventListener('resize', this.updateBodyHeight)
|
|
325
|
+
this.removeThemeStyle()
|
|
326
|
+
// 清理自动刷新定时器
|
|
327
|
+
if (this.refreshInterval) {
|
|
328
|
+
clearInterval(this.refreshInterval)
|
|
329
|
+
this.refreshInterval = null
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
</script>
|
|
334
|
+
<style lang="scss">
|
|
335
|
+
@include b(dashboard) {
|
|
336
|
+
width: 100%;
|
|
337
|
+
height: 100%;
|
|
338
|
+
overflow: auto;
|
|
339
|
+
overflow-x: hidden;
|
|
340
|
+
display: flex;
|
|
341
|
+
flex-direction: column;
|
|
342
|
+
@include m(hidden-scroll-bar) {
|
|
343
|
+
@include e(container) {
|
|
344
|
+
&::-webkit-scrollbar-thumb {
|
|
345
|
+
background: none;
|
|
346
|
+
&:hover {
|
|
347
|
+
background-color: rgba(95, 95, 95, .3);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
@include e(container) {
|
|
353
|
+
// background: #f0f2f6;
|
|
354
|
+
width: 100%;
|
|
355
|
+
flex: 1;
|
|
356
|
+
overflow: auto;
|
|
357
|
+
}
|
|
358
|
+
@include e(component-container) {
|
|
359
|
+
width: 100%;
|
|
360
|
+
height: 100%;
|
|
361
|
+
// 不裁剪子组件的 box-shadow:阴影渲染进栅格间距;滚动下移到 __component 自身
|
|
362
|
+
overflow: visible;
|
|
363
|
+
}
|
|
364
|
+
@include e(component) {
|
|
365
|
+
height: 100%;
|
|
366
|
+
// 自身内容溢出仍可滚动;元素自身的 box-shadow 不受自身 overflow 裁剪
|
|
367
|
+
overflow: auto;
|
|
368
|
+
}
|
|
369
|
+
.epx-card-num__title {
|
|
370
|
+
color: inher
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// 移动端专属缩放:收紧卡片内边距、缩小标题与数字字号,让密集看板在小屏更耐看
|
|
374
|
+
@media only screen and (max-width: 992px) {
|
|
375
|
+
@include b(dashboard) {
|
|
376
|
+
.epx-card-num {
|
|
377
|
+
padding: 12px;
|
|
378
|
+
}
|
|
379
|
+
.epx-card-num__title {
|
|
380
|
+
font-size: 13px;
|
|
381
|
+
}
|
|
382
|
+
.epx-card-num__num > font {
|
|
383
|
+
font-size: 22px;
|
|
384
|
+
}
|
|
385
|
+
.epx-chart__title {
|
|
386
|
+
font-size: 14px;
|
|
387
|
+
}
|
|
388
|
+
.epx-chart__header {
|
|
389
|
+
padding: 0 8px;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
</style>
|
|
394
|
+
<style lang="scss" scoped>
|
|
395
|
+
|
|
396
|
+
// .dashboard--hidden-scroll-bar {
|
|
397
|
+
// .dashboard-container {
|
|
398
|
+
// &::-webkit-scrollbar-thumb {
|
|
399
|
+
// background: none;
|
|
400
|
+
// &:hover {
|
|
401
|
+
// background-color: rgba(95, 95, 95, .3);
|
|
402
|
+
// }
|
|
403
|
+
// }
|
|
404
|
+
// }
|
|
405
|
+
// }
|
|
406
|
+
// .dashboard-container {
|
|
407
|
+
|
|
408
|
+
// }
|
|
409
|
+
// .dashboard-component {
|
|
410
|
+
|
|
411
|
+
// }
|
|
412
|
+
|
|
413
|
+
// .dashboard--dark {
|
|
414
|
+
// .dashboard-container{
|
|
415
|
+
// background: #151C3D;
|
|
416
|
+
// .dashboard-component {
|
|
417
|
+
// >div {
|
|
418
|
+
// // background: rgba(255,255,255, .1)
|
|
419
|
+
// }
|
|
420
|
+
// }
|
|
421
|
+
// }
|
|
422
|
+
// }
|
|
423
|
+
// .dashboard--technology {
|
|
424
|
+
// .dashboard-container{
|
|
425
|
+
// background: url('@/assets/images/dashboard/technology_bg.jpg') no-repeat center;
|
|
426
|
+
// background-size: cover;
|
|
427
|
+
|
|
428
|
+
// }
|
|
429
|
+
// .component {
|
|
430
|
+
// // position: relative;
|
|
431
|
+
// // &::before {
|
|
432
|
+
// // content: '';
|
|
433
|
+
// // position: absolute;
|
|
434
|
+
// // top: 0;
|
|
435
|
+
// // display: block;
|
|
436
|
+
// // width: 100%;
|
|
437
|
+
// // height: 1px;
|
|
438
|
+
// // background: linear-gradient(90deg, rgba(45, 164, 247, 0) 0%, #2DA4F7 50%, rgba(45, 164, 247, 0) 100%);
|
|
439
|
+
// // opacity: 0.5;
|
|
440
|
+
// // }
|
|
441
|
+
// // &::after {
|
|
442
|
+
// // content: '';
|
|
443
|
+
// // position: absolute;
|
|
444
|
+
// // bottom: 0;
|
|
445
|
+
// // display: block;
|
|
446
|
+
// // width: 100%;
|
|
447
|
+
// // height: 1px;
|
|
448
|
+
// // background: linear-gradient(90deg, rgba(45, 164, 247, 0) 0%, #2DA4F7 50%, rgba(45, 164, 247, 0) 100%);
|
|
449
|
+
// // opacity: 0.5;
|
|
450
|
+
// // }
|
|
451
|
+
// // background: rgba(255,255,255, .1);
|
|
452
|
+
// // backdrop-filter: blur(4px);
|
|
453
|
+
// // >div {
|
|
454
|
+
// // background: rgba(255,255,255, .1);
|
|
455
|
+
// // backdrop-filter: blur(4px);
|
|
456
|
+
// // }
|
|
457
|
+
// }
|
|
458
|
+
// }
|
|
459
|
+
</style>
|