refactor(wel): 重构欢迎页为模块化组件

- 将欢迎页整体结构拆分为多个功能区组件,提高代码可维护性
- 移除原有欢迎横幅、指标卡、图表区、下方区及快捷入口的静态实现
- 引入并注册 SectionKpi、SectionStructure、SectionBusiness 等八个子组件
- 删除冗余样式,更新整体背景和布局样式
- 精简模板内容,仅保留子组件渲染及页脚版权信息
- 移除无用的响应式数据、计算属性及方法
- 包含依赖包 echarts 的新增,支持后续图表功能开发
This commit is contained in:
2026-09-30 17:39:42 +08:00
parent 50c1afe9c8
commit 0b81001ce7
18 changed files with 1479 additions and 824 deletions
+11
View File
@@ -0,0 +1,11 @@
# 2026-09-30
## /wel/index 首页重构为运营数据看板(按蓝湖效果图)
- 新增依赖:echarts ^6.1.0(corepack pnpm add echarts)。
- 重构 `src/views/wel/index.vue`:改为「核心经营指标 / 运输结构 / 业务单元情况 / 项目情况 / 财务情况 / 车队与线路 / 车务管理 / 底部占比与趋势」8 大板块 + 版权页脚,删除原欢迎横幅/快捷入口。
- 新增文件:
- `src/views/wel/chart-options.js`:排行条形/盈亏双向/环形/分组柱/面积折线/堆叠柱 6 类 option 生成器(fmt 千分位、红涨绿跌 rich label)。
- `src/views/wel/dashboard.scss`:看板公共样式,各组件 scoped `@import '../dashboard.scss'`(子目录注意路径)。
- `src/views/wel/components/`:wel-chart.vue(ECharts 封装:ResizeObserver + activated resize + dispose)、wel-filters.vue(统计时间/条件筛选 mock)、rank-panel.vue(排行/盈亏通用面板)、section-kpi / section-structure / section-business / section-project / section-finance / section-fleet / section-vehicle / section-summary.vue。
- 数据全部为 Mock,组件内已注释标注对接点;蓝湖红字标注的指标口径未定,待后端统计接口(估 12~18 个)确定后替换。
- 自检:dev server(2889) 下所有新文件及 `?vue&type=style&...&lang.scss` 均 200;`/node_modules/.vite/deps/echarts.js` 200。vite build 因 facelogin.vue 沙箱中断不可用,未跑。
+1
View File
@@ -23,6 +23,7 @@
"crypto-js": "^4.1.1",
"dayjs": "^1.10.6",
"disable-devtool": "^0.3.8",
"echarts": "^6.1.0",
"element-plus": "^2.14.2",
"exceljs": "^4.4.0",
"highlight.js": "^11.9.0",
+62 -57
View File
@@ -5,9 +5,7 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
'@smallwei/avue':
hash: 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
path: patches/@smallwei__avue.patch
'@smallwei/avue': 5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db
importers:
@@ -33,16 +31,16 @@ importers:
version: 1.7.0
'@smallwei/avue':
specifier: ~3.9.2
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)
animate.css:
specifier: ^4.1.1
version: 4.1.1
avue-plugin-ueditor:
specifier: ^1.0.4
version: 1.0.4(axios@1.16.0)(vue@3.5.34)
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34)
axios:
specifier: ^1.13.5
version: 1.16.0
version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
codemirror:
specifier: ^5.65.21
version: 5.65.21
@@ -55,6 +53,9 @@ importers:
disable-devtool:
specifier: ^0.3.8
version: 0.3.9
echarts:
specifier: ^6.1.0
version: 6.1.0
element-plus:
specifier: ^2.14.2
version: 2.14.2(vue@3.5.34)
@@ -63,13 +64,13 @@ importers:
version: 4.4.0
highlight.js:
specifier: ^11.9.0
version: 11.11.1
version: 11.12.0
js-base64:
specifier: ^3.7.4
version: 3.7.8
version: 3.9.4
js-cookie:
specifier: ^3.0.0
version: 3.0.5
version: 3.0.8
js-md5:
specifier: ^0.7.3
version: 0.7.3
@@ -127,7 +128,7 @@ importers:
version: 5.4.21(sass@1.99.0)(terser@5.46.2)
vite-plugin-compression:
specifier: ^0.5.1
version: 0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
version: 0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
vite-plugin-vue-setup-extend:
specifier: ^0.4.0
version: 0.4.0(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
@@ -1208,6 +1209,7 @@ packages:
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1453,8 +1455,8 @@ packages:
heic2any@0.0.4:
resolution: {integrity: sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==}
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
highlight.js@11.12.0:
resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==}
engines: {node: '>=12.0.0'}
hls.js@1.6.16:
@@ -1532,12 +1534,11 @@ packages:
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
js-base64@3.9.4:
resolution: {integrity: sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q==}
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-cookie@3.0.8:
resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==}
js-md5@0.7.3:
resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==}
@@ -2653,10 +2654,10 @@ snapshots:
countup.js: 2.10.0
signature_pad: 5.1.3
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34))(vue@3.5.34)':
dependencies:
'@element-plus/icons-vue': 2.3.2(vue@3.5.34)
axios: 0.21.4
axios: 0.21.4(debug@4.4.3(supports-color@7.2.0))
countup.js: 1.9.3
dayjs: 1.11.20
element-plus: 2.14.2(vue@3.5.34)
@@ -2792,25 +2793,25 @@ snapshots:
dependencies:
vue: 3.5.34
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 3.0.0
is-url: 1.2.4
lodash.throttle: 4.1.1
nanoid: 3.3.12
slate: 0.72.8
nanoid: 5.1.11
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
prismjs: 1.30.0
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@types/event-emitter': 0.3.5
'@uppy/core': 2.3.4
@@ -2826,9 +2827,9 @@ snapshots:
lodash.foreach: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.12
nanoid: 5.1.11
scroll-into-view-if-needed: 3.1.0
slate: 0.72.8
slate: 0.123.0
slate-history: 0.115.0(slate@0.123.0)
snabbdom: 3.6.3
@@ -2836,13 +2837,13 @@ snapshots:
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
@@ -2855,16 +2856,16 @@ snapshots:
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
@@ -2872,22 +2873,22 @@ snapshots:
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
lodash.foreach: 4.5.0
slate: 0.123.0
snabbdom: 3.6.3
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
dom7: 4.0.6
nanoid: 5.1.11
slate: 0.123.0
@@ -3064,22 +3065,22 @@ snapshots:
asynckit@0.4.0: {}
avue-plugin-ueditor@1.0.4(axios@1.16.0)(vue@3.5.34):
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34):
dependencies:
'@wangeditor/editor': 5.1.23
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)
axios: 1.16.0
axios: 1.16.0(debug@4.4.3(supports-color@7.2.0))
vue: 3.5.34
axios@0.21.4:
axios@0.21.4(debug@4.4.3(supports-color@7.2.0)):
dependencies:
follow-redirects: 1.16.0
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
transitivePeerDependencies:
- debug
axios@1.16.0:
axios@1.16.0(debug@4.4.3(supports-color@7.2.0)):
dependencies:
follow-redirects: 1.16.0
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
@@ -3247,9 +3248,11 @@ snapshots:
dayjs@1.11.20: {}
debug@4.4.3:
debug@4.4.3(supports-color@7.2.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 7.2.0
delayed-stream@1.0.0: {}
@@ -3457,7 +3460,9 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
follow-redirects@1.16.0: {}
follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
optionalDependencies:
debug: 4.4.3(supports-color@7.2.0)
form-data@4.0.5:
dependencies:
@@ -3546,7 +3551,7 @@ snapshots:
heic2any@0.0.4: {}
highlight.js@11.11.1: {}
highlight.js@11.12.0: {}
hls.js@1.6.16: {}
@@ -3605,9 +3610,9 @@ snapshots:
isarray@1.0.0: {}
js-base64@3.7.8: {}
js-base64@3.9.4: {}
js-cookie@3.0.5: {}
js-cookie@3.0.8: {}
js-md5@0.7.3: {}
@@ -4152,10 +4157,10 @@ snapshots:
uuid@8.3.2: {}
vite-plugin-compression@0.5.1(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
vite-plugin-compression@0.5.1(supports-color@7.2.0)(vite@5.4.21(sass@1.99.0)(terser@5.46.2)):
dependencies:
chalk: 4.1.2
debug: 4.4.3
debug: 4.4.3(supports-color@7.2.0)
fs-extra: 10.1.0
vite: 5.4.21(sass@1.99.0)(terser@5.46.2)
transitivePeerDependencies:
+5
View File
@@ -1,5 +1,10 @@
packages:
- '.'
allowBuilds:
'@parcel/watcher': set this to true or false
es5-ext: set this to true or false
esbuild: set this to true or false
patchedDependencies:
'@smallwei/avue': patches/@smallwei__avue.patch
+300
View File
@@ -0,0 +1,300 @@
/**
* 首页看板 ECharts 通用配置生成器
* 所有数据均为 Mock,对接后端统计接口后由各板块组件传入真实数据
*/
/* 横向渐变色(排行条形图) */
function hGrad(color, from = 0.9, to = 0.55) {
return {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{ offset: 0, color: color },
{ offset: 1, color: hexFade(color, to) },
],
};
}
/* 十六进制颜色加透明度 */
function hexFade(hex, alpha) {
const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
/* 千分位 */
export function fmt(n) {
if (n === null || n === undefined || n === '') return '-';
const num = Number(n);
if (Number.isNaN(num)) return n;
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
}
/**
* 排行条形图(左名称 + 中条形 + 右数值/涨跌)
* items: [{ name, value, change? }] change 为百分比涨跌(number,可空)
*/
export function rankOption(items, { color = '#4a90e2', unit = '', nameWidth = 100 } = {}) {
const sorted = [...items].sort((a, b) => a.value - b.value);
const hasChange = sorted.some(i => typeof i.change === 'number');
const rich = {
v: { color: '#303133', fontWeight: 600, fontSize: 12, align: 'right' },
u: { color: '#909399', fontSize: 11, align: 'right' },
up: { color: '#f04747', fontSize: 11, align: 'right' },
down: { color: '#2ba471', fontSize: 11, align: 'right' },
};
return {
grid: { left: 4, right: hasChange ? 96 : 58, top: 6, bottom: 6, containLabel: true },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: p => {
const d = p[0].data;
let s = `${p[0].name}<br/>${d.value}${unit}`;
if (typeof d.change === 'number') {
s += `(环比 ${d.change >= 0 ? '+' : ''}${d.change}%)`;
}
return s;
},
},
xAxis: { type: 'value', show: false },
yAxis: {
type: 'category',
data: sorted.map(i => i.name),
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#606266', fontSize: 12, width: nameWidth, overflow: 'truncate' },
},
series: [
{
type: 'bar',
barWidth: 10,
showBackground: true,
backgroundStyle: { color: '#f0f2f5', borderRadius: [0, 5, 5, 0] },
itemStyle: { color: hGrad(color), borderRadius: [0, 5, 5, 0] },
label: {
show: true,
position: 'right',
distance: 8,
formatter: p => {
const d = p.data;
let s = `{v|${fmt(d.value)}${unit}}`;
if (typeof d.change === 'number') {
const cls = d.change >= 0 ? 'up' : 'down';
const txt = `${d.change >= 0 ? '↑ +' : '↓ '}${d.change}%`;
s += ` {${cls}|${txt}}`;
}
return s;
},
rich,
},
data: sorted.map(i => ({ value: i.value, change: i.change })),
},
],
};
}
/**
* 盈亏双向对比条形图(正=红,负=绿)
* items: [{ name, value }]
*/
export function diffOption(items) {
const sorted = [...items].sort((a, b) => b.value - a.value);
return {
grid: { left: 4, right: 60, top: 6, bottom: 6, containLabel: true },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: p => `${p[0].name}<br/>${p[0].value > 0 ? '盈利' : '亏损'} ${fmt(Math.abs(p[0].value))} 万元`,
},
xAxis: { type: 'value', show: false },
yAxis: {
type: 'category',
data: sorted.map(i => i.name),
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#606266', fontSize: 12, width: 100, overflow: 'truncate' },
},
series: [
{
type: 'bar',
barWidth: 10,
itemStyle: {
borderRadius: 5,
color: p => (p.value >= 0 ? '#e04b4b' : '#3aa15c'),
},
label: {
show: true,
position: 'right',
distance: 8,
formatter: p => `${p.value > 0 ? '+' : ''}${fmt(p.value)}`,
color: '#303133',
fontSize: 12,
fontWeight: 600,
},
data: sorted.map(i => i.value),
},
],
};
}
/**
* 环形占比图
* items: [{ name, value }]
*/
export function donutOption(items, { centerTitle = '', centerSub = '' } = {}) {
const palette = ['#4a90e2', '#2bb3a3', '#e0b14c', '#8b5cf6', '#e04b4b', '#3aa15c', '#9aa5b5', '#5aa9ff'];
return {
color: palette,
tooltip: { trigger: 'item', formatter: p => `${p.name}<br/>${fmt(p.value)}(${p.percent}%)` },
legend: { show: false },
series: [
{
type: 'pie',
radius: ['52%', '74%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
itemStyle: { borderColor: '#fff', borderWidth: 2 },
label: {
show: true,
color: '#606266',
fontSize: 11,
formatter: p => `${p.name}\n${p.percent}%`,
},
labelLine: { length: 10, length2: 8, lineStyle: { color: '#c0c4cc' } },
data: items,
},
],
graphic: centerTitle
? [
{
type: 'text',
left: 'center',
top: '44%',
style: { text: centerTitle, fontSize: 18, fontWeight: 700, fill: '#303133' },
},
{
type: 'text',
left: 'center',
top: '56%',
style: { text: centerSub, fontSize: 11, fill: '#909399' },
},
]
: [],
};
}
/**
* 分组柱状图(收入/费用对比)
* categories: string[];series: [{ name, data, color }]
*/
export function groupBarOption(categories, series) {
return {
tooltip: { trigger: 'axis' },
legend: { top: 0, right: 0, itemWidth: 14, itemHeight: 8, textStyle: { color: '#606266', fontSize: 12 } },
grid: { left: 8, right: 16, top: 36, bottom: 4, containLabel: true },
xAxis: {
type: 'category',
data: categories,
axisTick: { show: false },
axisLine: { lineStyle: { color: '#e4e7ed' } },
axisLabel: { color: '#909399', fontSize: 11, interval: 0, rotate: categories.length > 8 ? 30 : 0 },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#f0f2f5' } },
axisLabel: { color: '#909399', fontSize: 11 },
},
series: series.map(s => ({
name: s.name,
type: 'bar',
barWidth: 14,
itemStyle: { color: s.color, borderRadius: [3, 3, 0, 0] },
data: s.data,
})),
};
}
/**
* 面积折线图
* categories: string[];series: [{ name, data, color }]
*/
export function lineAreaOption(categories, series, { rotate = false } = {}) {
return {
tooltip: { trigger: 'axis' },
legend: series.length > 1
? { top: 0, right: 0, itemWidth: 16, itemHeight: 8, textStyle: { color: '#606266', fontSize: 12 } }
: { show: false },
grid: { left: 8, right: 20, top: series.length > 1 ? 36 : 20, bottom: 4, containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: categories,
axisTick: { show: false },
axisLine: { lineStyle: { color: '#e4e7ed' } },
axisLabel: { color: '#909399', fontSize: 11, rotate: rotate ? 30 : 0 },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#f0f2f5' } },
axisLabel: { color: '#909399', fontSize: 11 },
},
series: series.map(s => ({
name: s.name,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
lineStyle: { color: s.color, width: 2 },
itemStyle: { color: s.color },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: hexFade(s.color, 0.18) },
{ offset: 1, color: hexFade(s.color, 0.02) },
],
},
},
data: s.data,
})),
};
}
/**
* 堆叠柱状图
* categories: string[];series: [{ name, data, color }]
*/
export function stackBarOption(categories, series) {
return {
tooltip: { trigger: 'axis' },
legend: { top: 0, right: 0, itemWidth: 14, itemHeight: 8, textStyle: { color: '#606266', fontSize: 12 } },
grid: { left: 8, right: 16, top: 36, bottom: 4, containLabel: true },
xAxis: {
type: 'category',
data: categories,
axisTick: { show: false },
axisLine: { lineStyle: { color: '#e4e7ed' } },
axisLabel: { color: '#909399', fontSize: 11, interval: 0, rotate: categories.length > 8 ? 30 : 0 },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#f0f2f5' } },
axisLabel: { color: '#909399', fontSize: 11 },
},
series: series.map(s => ({
name: s.name,
type: 'bar',
stack: 'total',
barWidth: 22,
itemStyle: { color: s.color },
data: s.data,
})),
};
}
+46
View File
@@ -0,0 +1,46 @@
<template>
<div class="wel-card rank-panel">
<div class="panel-title">
{{ title }}
<span v-if="unit" class="panel-title__unit">{{ unit }}</span>
</div>
<wel-chart
v-if="type === 'rank'"
:option="rankOpt"
:height="height"
/>
<wel-chart v-else :option="diffOpt" :height="height" />
</div>
</template>
<script>
import WelChart from './wel-chart.vue';
import { rankOption, diffOption } from '../chart-options';
export default {
name: 'rank-panel',
components: { WelChart },
props: {
title: { type: String, required: true },
unit: { type: String, default: '' },
/* rank = 普通排行;diff = 盈亏双向对比 */
type: { type: String, default: 'rank' },
color: { type: String, default: '#4a90e2' },
items: { type: Array, required: true },
height: { type: String, default: '300px' },
nameWidth: { type: Number, default: 100 },
},
computed: {
rankOpt() {
return rankOption(this.items, { color: this.color, unit: this.unit, nameWidth: this.nameWidth });
},
diffOpt() {
return diffOption(this.items);
},
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
</style>
@@ -0,0 +1,164 @@
<template>
<section class="wel-sec">
<div class="wel-sec__head">
<span class="wel-sec__title">业务单元情况</span>
<wel-filters condition-label="业务单元" />
</div>
<el-row :gutter="16">
<el-col v-for="p in panels" :key="p.title" :xs="24" :md="12">
<rank-panel
:title="p.title"
:unit="p.unit"
:type="p.type"
:color="p.color"
:items="p.items"
:height="p.height"
:name-width="p.nameWidth"
class="business-panel"
/>
</el-col>
</el-row>
</section>
</template>
<script>
import RankPanel from './rank-panel.vue';
import WelFilters from './wel-filters.vue';
export default {
name: 'section-business',
components: { RankPanel, WelFilters },
data() {
return {
// Mock:业务单元排行数据,对接统计接口后替换
panels: [
{
title: '营业收入排行',
unit: '万元',
type: 'rank',
color: '#4a90e2',
height: '320px',
items: [
{ name: '广西物产集团', value: 5218.7, change: 12.3 },
{ name: '南宁港务物流', value: 4183.4, change: 8.6 },
{ name: '柳州钢铁物流', value: 3603.9, change: -2.4 },
{ name: '北部湾港务', value: 3131.7, change: 5.1 },
{ name: '桂林粮油储运', value: 2750.3, change: 9.8 },
{ name: '玉林建材集团', value: 2455.5, change: -1.2 },
{ name: '钦州石化物流', value: 2088.3, change: 6.5 },
{ name: '贵港航运公司', value: 1823.5, change: 3.9 },
{ name: '百色矿产运输', value: 1588.4, change: -4.7 },
{ name: '梧州商贸物流', value: 1214.8, change: 2.2 },
],
},
{
title: '运输情况排行',
unit: '单',
type: 'rank',
color: '#2bb3a3',
height: '320px',
items: [
{ name: '广西物产集团', value: 238.2, change: 9.4 },
{ name: '南宁港务物流', value: 196.3, change: 6.7 },
{ name: '柳州钢铁物流', value: 168.1, change: 3.2 },
{ name: '北部湾港务', value: 154.7, change: -2.8 },
{ name: '桂林粮油储运', value: 139.2, change: 7.5 },
{ name: '玉林建材集团', value: 128.6, change: 4.1 },
{ name: '钦州石化物流', value: 118.3, change: -1.6 },
{ name: '贵港航运公司', value: 106.2, change: 5.3 },
{ name: '百色矿产运输', value: 98.8, change: 2.9 },
{ name: '梧州商贸物流', value: 89.6, change: -3.4 },
],
},
{
title: '各部门毛利占比',
unit: '%',
type: 'rank',
color: '#c9a227',
height: '320px',
items: [
{ name: '公路运输一部', value: 30.7 },
{ name: '公路运输二部', value: 28.2 },
{ name: '铁路物流部', value: 23.2 },
{ name: '水运事业部', value: 19.6 },
{ name: '多式联运部', value: 15.2 },
{ name: '仓储配送部', value: 13.1 },
{ name: '冷链事业部', value: 10.4 },
{ name: '大件运输部', value: 8.6 },
{ name: '危险品运输部', value: 5.2 },
{ name: '综合业务部', value: 2.8 },
],
},
{
title: '项目盈亏对比',
unit: '万元',
type: 'diff',
color: '#e04b4b',
height: '320px',
items: [
{ name: '铝锭外运项目', value: 156.5 },
{ name: '冷链城配项目', value: 134.8 },
{ name: '钢材南下项目', value: 118.2 },
{ name: '粮食储备调运', value: 96.1 },
{ name: '矿石短驳项目', value: 82.6 },
{ name: '集装箱班列', value: 74.3 },
{ name: '化工原料运输', value: 61.2 },
{ name: '建材集散运输', value: 48.9 },
{ name: '汽车零部件配送', value: 21.5 },
{ name: '设备大件运输', value: 16.2 },
{ name: '生鲜冷链专线', value: -8.3 },
{ name: '危化品试点线', value: -25.4 },
],
},
{
title: '运单量排行',
unit: '单',
type: 'rank',
color: '#4a90e2',
height: '320px',
items: [
{ name: '南宁分部', value: 320, change: 6.3 },
{ name: '柳州分部', value: 273, change: 4.8 },
{ name: '桂林分部', value: 241, change: -2.1 },
{ name: '玉林分部', value: 215, change: 8.2 },
{ name: '钦州港分部', value: 203, change: 3.6 },
{ name: '贵港分部', value: 187, change: -1.4 },
{ name: '百色分部', value: 158, change: 5.7 },
{ name: '梧州分部', value: 132, change: 2.3 },
{ name: '北海分部', value: 108, change: -3.9 },
{ name: '河池分部', value: 86, change: 1.6 },
],
},
{
title: '运输收入排行',
unit: '万元',
type: 'rank',
color: '#8b5cf6',
height: '320px',
items: [
{ name: '公路整车', value: 1863.2, change: 7.1 },
{ name: '铁路整车', value: 1421.8, change: 4.5 },
{ name: '水运集装箱', value: 1182.3, change: 9.2 },
{ name: '公路零担', value: 968.5, change: 2.8 },
{ name: '多式联运', value: 812.4, change: 11.6 },
{ name: '航空货运', value: 655.7, change: -1.8 },
{ name: '冷链专线', value: 542.1, change: 6.4 },
{ name: '大件运输', value: 388.9, change: 3.1 },
{ name: '危险品运输', value: 296.3, change: -2.5 },
{ name: '仓储增值服务', value: 184.6, change: 8.8 },
],
},
],
};
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.business-panel {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,68 @@
<template>
<section class="wel-sec">
<div class="wel-sec__head">
<span class="wel-sec__title">财务情况</span>
<wel-filters condition-label="核算口径" />
</div>
<div class="wel-card finance-panel">
<div class="panel-title">
营业收支对比
<span class="panel-title__unit">万元</span>
</div>
<wel-chart :option="barOpt" height="300px" />
</div>
<div class="wel-card finance-panel">
<div class="panel-title">
主营业务收入/费用趋势
<span class="panel-title__unit">万元</span>
</div>
<wel-chart :option="lineOpt" height="280px" />
</div>
</section>
</template>
<script>
import WelChart from './wel-chart.vue';
import WelFilters from './wel-filters.vue';
import { groupBarOption, lineAreaOption } from '../chart-options';
export default {
name: 'section-finance',
components: { WelChart, WelFilters },
data() {
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
return {
months,
// Mock:财务收支数据,对接统计接口后替换
incomeData: [2860, 1520, 1180, 1150, 980, 920, 760, 720, 690, 520, 380, 260],
costData: [2410, 1330, 1010, 980, 850, 800, 660, 630, 610, 470, 340, 230],
trendIncome: [1780, 1850, 1810, 1880, 1920, 1950, 1720, 1650, 1830, 1900, 1980, 2060],
trendCost: [1520, 1560, 1540, 1590, 1610, 1620, 1450, 1400, 1530, 1580, 1640, 1700],
};
},
computed: {
barOpt() {
return groupBarOption(this.months, [
{ name: '收入', data: this.incomeData, color: '#4a90e2' },
{ name: '费用', data: this.costData, color: '#d9dce3' },
]);
},
lineOpt() {
return lineAreaOption(this.months, [
{ name: '主营业务收入', data: this.trendIncome, color: '#4a90e2' },
{ name: '主营业务费用', data: this.trendCost, color: '#2bb3a3' },
]);
},
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.finance-panel {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,75 @@
<template>
<section class="wel-sec">
<div class="wel-sec__head">
<span class="wel-sec__title">车队与线路</span>
<wel-filters condition-label="运输方式" />
</div>
<el-row :gutter="16">
<el-col :xs="24" :md="12">
<rank-panel
title="下游车队 TOP10"
unit="单"
color="#1a9e94"
:items="fleetItems"
height="320px"
:name-width="120"
class="fleet-panel"
/>
</el-col>
<el-col :xs="24" :md="12">
<rank-panel
title="运输线路 TOP5"
unit="单"
color="#c9a227"
:items="routeItems"
height="320px"
:name-width="120"
class="fleet-panel"
/>
</el-col>
</el-row>
</section>
</template>
<script>
import RankPanel from './rank-panel.vue';
import WelFilters from './wel-filters.vue';
export default {
name: 'section-fleet',
components: { RankPanel, WelFilters },
data() {
return {
// Mock:车队/线路排行数据,对接统计接口后替换
fleetItems: [
{ name: '顺达车队', value: 186.5 },
{ name: '穗联物流车队', value: 158.2 },
{ name: '宏运车队', value: 132.6 },
{ name: '桂海运输车队', value: 118.4 },
{ name: '南北快运车队', value: 96.7 },
{ name: '恒通货运车队', value: 85.3 },
{ name: '运达车队', value: 74.2 },
{ name: '捷安物流车队', value: 62.8 },
{ name: '广通运输车队', value: 54.6 },
{ name: '长顺车队', value: 43.1 },
],
routeItems: [
{ name: '南宁 → 柳州', value: 342.5 },
{ name: '钦州港 → 南宁', value: 285.6 },
{ name: '贵港 → 广州', value: 226.3 },
{ name: '桂林 → 南宁', value: 168.2 },
{ name: '玉林 → 深圳', value: 142.6 },
],
};
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.fleet-panel {
margin-bottom: 16px;
}
</style>
+183
View File
@@ -0,0 +1,183 @@
<template>
<section class="wel-sec">
<div class="wel-sec__head">
<span class="wel-sec__title">核心经营指标</span>
<wel-filters />
</div>
<el-row :gutter="16">
<el-col v-for="card in cards" :key="card.label" :xs="24" :md="8">
<div class="kpi-card">
<div class="kpi-card__top">
<span class="kpi-card__label">{{ card.label }}</span>
<span
class="kpi-card__badge"
:class="card.trend >= 0 ? 'is-up' : 'is-down'"
>
环比 {{ card.trend >= 0 ? '+' : '' }}{{ card.trend }}%
</span>
</div>
<div class="kpi-card__value">
{{ card.value }}<span class="kpi-card__unit">{{ card.unit }}</span>
</div>
<ul class="kpi-card__subs">
<li v-for="s in card.subs" :key="s.name">
<span class="kpi-card__sub-name">{{ s.name }}</span>
<span class="kpi-card__sub-value">{{ s.value }}</span>
<span
v-if="typeof s.change === 'number'"
class="kpi-card__sub-change"
:class="s.change >= 0 ? 'is-up' : 'is-down'"
>
{{ s.change >= 0 ? '↑' : '↓' }} {{ Math.abs(s.change) }}%
</span>
</li>
</ul>
</div>
</el-col>
</el-row>
</section>
</template>
<script>
import WelFilters from './wel-filters.vue';
export default {
name: 'section-kpi',
components: { WelFilters },
data() {
return {
// Mock:营业额/运单量/利润等核心指标,对接统计接口后替换
cards: [
{
label: '营业额',
value: '58,814.0',
unit: '万元',
trend: 12.6,
subs: [
{ name: '毛利率', value: '23.5%', change: 1.2 },
{ name: '运输成本', value: '45,031 万元', change: -0.8 },
{ name: '回款金额', value: '39,640 万元', change: 3.4 },
{ name: '应收账款', value: '19,174 万元', change: -2.1 },
],
},
{
label: '运单量',
value: '1,497.1',
unit: '单',
trend: 8.4,
subs: [
{ name: '已完成', value: '1,205 单', change: 6.2 },
{ name: '在途', value: '135 单', change: 12.0 },
{ name: '待发运', value: '92 单', change: -4.5 },
{ name: '异常单', value: '65 单', change: 1.8 },
],
},
{
label: '运输利润',
value: '2,058.2',
unit: '万元',
trend: 15.7,
subs: [
{ name: '利润率', value: '13.7%', change: 1.5 },
{ name: '单均利润', value: '1.38 万元', change: 5.6 },
{ name: '燃油成本', value: '8,931 万元', change: -3.2 },
{ name: '过路费用', value: '4,208 万元', change: 0.9 },
],
},
],
};
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.kpi-card {
background: #ffffff;
border-radius: 10px;
padding: 18px 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
margin-bottom: 16px;
}
.kpi-card__top {
display: flex;
align-items: center;
justify-content: space-between;
}
.kpi-card__label {
font-size: 13px;
color: #909399;
}
.kpi-card__badge {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
&.is-up {
color: #f04747;
background: #fdecec;
}
&.is-down {
color: #2ba471;
background: #e8f7ef;
}
}
.kpi-card__value {
font-size: 30px;
font-weight: 700;
color: #303133;
margin: 8px 0 12px;
}
.kpi-card__unit {
font-size: 13px;
font-weight: 400;
color: #909399;
margin-left: 6px;
}
.kpi-card__subs {
list-style: none;
margin: 0;
padding: 10px 0 0;
border-top: 1px solid #f0f2f5;
li {
display: flex;
align-items: center;
padding: 5px 0;
font-size: 13px;
}
}
.kpi-card__sub-name {
color: #909399;
flex: 1;
}
.kpi-card__sub-value {
color: #303133;
font-weight: 600;
margin-right: 10px;
}
.kpi-card__sub-change {
width: 56px;
text-align: right;
&.is-up {
color: #f04747;
}
&.is-down {
color: #2ba471;
}
}
</style>
@@ -0,0 +1,70 @@
<template>
<section class="wel-sec">
<div class="wel-sec__head">
<span class="wel-sec__title">项目情况</span>
<wel-filters condition-label="项目状态" />
</div>
<el-row :gutter="16">
<el-col :xs="24" :md="12">
<rank-panel
title="项目收入 TOP5"
unit="万元"
color="#8b5cf6"
:items="incomeItems"
height="280px"
:name-width="180"
class="project-panel"
/>
</el-col>
<el-col :xs="24" :md="12">
<rank-panel
title="项目运量 TOP5"
unit="万吨"
color="#2bb3a3"
:items="volumeItems"
height="280px"
:name-width="180"
class="project-panel"
/>
</el-col>
</el-row>
</section>
</template>
<script>
import RankPanel from './rank-panel.vue';
import WelFilters from './wel-filters.vue';
export default {
name: 'section-project',
components: { RankPanel, WelFilters },
data() {
return {
// Mock:项目排行数据,对接统计接口后替换
incomeItems: [
{ name: '广西华谊氯碱化工物流项目', value: 1285.6 },
{ name: '柳钢集团钢材外运项目(二期)', value: 982.4 },
{ name: '南宁轨道交通建材配送项目', value: 756.3 },
{ name: '北部湾港集装箱短驳项目', value: 621.7 },
{ name: '中粮集团粮食储备调运项目', value: 468.2 },
],
volumeItems: [
{ name: '广西华谊氯碱化工物流项目', value: 32.5 },
{ name: '柳钢集团钢材外运项目(二期)', value: 26.8 },
{ name: '南宁轨道交通建材配送项目', value: 21.4 },
{ name: '北部湾港集装箱短驳项目', value: 18.6 },
{ name: '中粮集团粮食储备调运项目', value: 15.2 },
],
};
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.project-panel {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,114 @@
<template>
<section class="wel-sec">
<div class="wel-sec__head">
<span class="wel-sec__title">运输结构</span>
<wel-filters condition-label="业务类型" />
</div>
<!-- 小指标卡 -->
<el-row :gutter="16" class="structure-cards">
<el-col v-for="c in miniCards" :key="c.label" :xs="12" :md="6">
<div class="mini-card structure-cards__item">
<div class="mini-card__label">{{ c.label }}</div>
<div class="mini-card__value">
{{ c.value }}<span class="mini-card__unit">{{ c.unit }}</span>
</div>
<div v-if="typeof c.change === 'number'" class="mini-card__trend" :class="c.change >= 0 ? 'is-up' : 'is-down'">
环比 {{ c.change >= 0 ? '+' : '' }}{{ c.change }}%
</div>
</div>
</el-col>
</el-row>
<!-- 三个占比环形图 -->
<el-row :gutter="16">
<el-col v-for="d in donuts" :key="d.title" :xs="24" :md="8">
<div class="wel-card donut-panel">
<div class="panel-title">
{{ d.title }}
<span class="panel-title__unit">{{ d.unit }}</span>
</div>
<wel-chart :option="donutOpt(d)" height="230px" />
</div>
</el-col>
</el-row>
</section>
</template>
<script>
import WelChart from './wel-chart.vue';
import WelFilters from './wel-filters.vue';
import { donutOption } from '../chart-options';
export default {
name: 'section-structure',
components: { WelChart, WelFilters },
data() {
return {
// Mock:运输结构统计,对接统计接口后替换
miniCards: [
{ label: '总运单量', value: '1,391', unit: '单', change: 30 },
{ label: '在途运单', value: '487', unit: '单', change: 12.5 },
{ label: '待发运', value: '135', unit: '单', change: -6.3 },
{ label: '总运量', value: '15.2', unit: '万吨', change: 8.1 },
],
donuts: [
{
title: '运输方式占比',
unit: '按运单量',
centerTitle: '1,391',
centerSub: '总运单(单)',
items: [
{ name: '公路', value: 620 },
{ name: '铁路', value: 335 },
{ name: '水运', value: 215 },
{ name: '航空', value: 121 },
{ name: '多式联运', value: 100 },
],
},
{
title: '货物类型占比',
unit: '按运量',
centerTitle: '15.2',
centerSub: '总运量(万吨)',
items: [
{ name: '普货', value: 6.4 },
{ name: '冷链', value: 3.2 },
{ name: '大件', value: 2.6 },
{ name: '危险品', value: 1.8 },
{ name: '其他', value: 1.2 },
],
},
{
title: '客户类型占比',
unit: '按运单量',
centerTitle: '326',
centerSub: '合作客户(家)',
items: [
{ name: '合同客户', value: 810 },
{ name: '散单客户', value: 372 },
{ name: '平台客户', value: 209 },
],
},
],
};
},
methods: {
donutOpt(d) {
return donutOption(d.items, { centerTitle: d.centerTitle, centerSub: d.centerSub });
},
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.structure-cards__item {
margin-bottom: 16px;
}
.donut-panel {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,67 @@
<template>
<section class="wel-sec">
<el-row :gutter="16">
<el-col :xs="24" :md="12">
<div class="wel-card summary-panel">
<div class="panel-title">
车辆耗时占比
<span class="panel-title__unit">按小时</span>
</div>
<wel-chart :option="donutOpt" height="260px" />
</div>
</el-col>
<el-col :xs="24" :md="12">
<div class="wel-card summary-panel">
<div class="panel-title">
单据完成趋势
<span class="panel-title__unit">按周</span>
</div>
<wel-chart :option="lineOpt" height="260px" />
</div>
</el-col>
</el-row>
</section>
</template>
<script>
import WelChart from './wel-chart.vue';
import { donutOption, lineAreaOption } from '../chart-options';
export default {
name: 'section-summary',
components: { WelChart },
data() {
return {
// Mock:车辆耗时与单据完成数据,对接统计接口后替换
timeItems: [
{ name: '在途运输', value: 86.2 },
{ name: '装货等待', value: 32.5 },
{ name: '卸货作业', value: 28.4 },
{ name: '返程空驶', value: 21.6 },
{ name: '场站等待', value: 18.3 },
{ name: '维修保养', value: 9.8 },
],
weeks: ['1周', '2周', '3周', '4周', '5周', '6周', '7周'],
doneData: [520, 545, 508, 532, 560, 548, 572],
};
},
computed: {
donutOpt() {
return donutOption(this.timeItems, { centerTitle: '196.8', centerSub: '总耗时(小时)' });
},
lineOpt() {
return lineAreaOption(this.weeks, [
{ name: '完成单据', data: this.doneData, color: '#1a9e94' },
]);
},
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.summary-panel {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,76 @@
<template>
<section class="wel-sec">
<div class="wel-sec__head">
<span class="wel-sec__title">车务管理</span>
<wel-filters condition-label="车辆类型" />
</div>
<!-- 车务小指标卡 -->
<el-row :gutter="16" class="vehicle-cards">
<el-col v-for="c in miniCards" :key="c.label" :xs="12" :md="6">
<div class="mini-card vehicle-cards__item">
<div class="mini-card__label">{{ c.label }}</div>
<div class="mini-card__value">
{{ c.value }}<span class="mini-card__unit">{{ c.unit }}</span>
</div>
<div v-if="typeof c.change === 'number'" class="mini-card__trend" :class="c.change >= 0 ? 'is-up' : 'is-down'">
环比 {{ c.change >= 0 ? '+' : '' }}{{ c.change }}%
</div>
</div>
</el-col>
</el-row>
<!-- 车辆耗费构成堆叠柱状图 -->
<div class="wel-card">
<div class="panel-title">
车辆耗费构成
<span class="panel-title__unit">万元</span>
</div>
<wel-chart :option="stackOpt" height="320px" />
</div>
</section>
</template>
<script>
import WelChart from './wel-chart.vue';
import WelFilters from './wel-filters.vue';
import { stackBarOption } from '../chart-options';
export default {
name: 'section-vehicle',
components: { WelChart, WelFilters },
data() {
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
return {
months,
// Mock:车务统计与耗费数据,对接统计接口后替换
miniCards: [
{ label: '可用车辆', value: 47, unit: '辆', change: 4.2 },
{ label: '维修保养中', value: 7, unit: '辆', change: -8.6 },
{ label: '年检临期', value: 9, unit: '辆', change: 12.5 },
{ label: '违章未处理', value: 12, unit: '起', change: -2.1 },
],
fuelData: [68, 52, 41, 36, 32, 38, 24, 46, 38, 28, 12, 14],
repairData: [22, 25, 18, 20, 16, 18, 14, 12, 15, 11, 6, 8],
otherData: [15, 14, 12, 13, 11, 12, 9, 11, 10, 8, 5, 6],
};
},
computed: {
stackOpt() {
return stackBarOption(this.months, [
{ name: '油费', data: this.fuelData, color: '#e0b14c' },
{ name: '维修保养', data: this.repairData, color: '#58b98a' },
{ name: '其他费用', data: this.otherData, color: '#4a90e2' },
]);
},
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.vehicle-cards__item {
margin-bottom: 16px;
}
</style>
+57
View File
@@ -0,0 +1,57 @@
<template>
<div ref="chartEl" class="wel-chart" :style="{ height: height }"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'wel-chart',
props: {
option: { type: Object, required: true },
height: { type: String, default: '260px' },
},
data() {
return {
chart: null,
resizeObserver: null,
};
},
watch: {
option: {
deep: true,
handler() {
this.render();
},
},
},
mounted() {
this.chart = echarts.init(this.$refs.chartEl);
this.render();
this.resizeObserver = new ResizeObserver(() => {
this.chart && this.chart.resize();
});
this.resizeObserver.observe(this.$refs.chartEl);
},
activated() {
/* keep-alive 恢复时重算尺寸 */
this.chart && this.chart.resize();
},
beforeUnmount() {
this.resizeObserver && this.resizeObserver.disconnect();
this.chart && this.chart.dispose();
this.chart = null;
},
methods: {
render() {
this.chart && this.chart.setOption(this.option, true);
},
},
};
</script>
<style lang="scss" scoped>
.wel-chart {
width: 100%;
}
</style>
+49
View File
@@ -0,0 +1,49 @@
<template>
<div class="wel-filter">
<span class="wel-filter__label">统计时间</span>
<el-select v-model="year" size="small">
<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y" />
</el-select>
<el-select v-model="range" size="small" class="wel-filter__range">
<el-option v-for="r in rangeOptions" :key="r.value" :label="r.label" :value="r.value" />
</el-select>
<template v-if="conditionLabel">
<span class="wel-filter__label">{{ conditionLabel }}</span>
<el-select v-model="condition" size="small">
<el-option label="全部" value="all" />
</el-select>
</template>
</div>
</template>
<script>
export default {
name: 'wel-filters',
props: {
conditionLabel: { type: String, default: '' },
},
data() {
const now = new Date().getFullYear();
return {
year: now,
yearOptions: [now, now - 1, now - 2, now - 3],
range: 'year',
rangeOptions: [
{ label: '全年', value: 'year' },
{ label: '上半年', value: 'h1' },
{ label: '下半年', value: 'h2' },
{ label: '自定义', value: 'custom' },
],
condition: 'all',
};
},
};
</script>
<style lang="scss" scoped>
@import '../dashboard.scss';
.wel-filter__range {
width: 110px;
}
</style>
+96
View File
@@ -0,0 +1,96 @@
/* 首页看板公共样式:各板块组件 scoped 引入 */
.wel-card {
background: #ffffff;
border-radius: 10px;
padding: 16px 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.wel-sec__head {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.wel-sec__title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.wel-filter {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.wel-filter__label {
font-size: 13px;
color: #909399;
white-space: nowrap;
}
.wel-filter .el-select {
width: 120px;
}
.panel-title {
display: flex;
align-items: baseline;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
.panel-title__unit {
font-size: 12px;
font-weight: 400;
color: #909399;
}
}
.mini-card {
background: #ffffff;
border-radius: 10px;
padding: 14px 18px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
.mini-card__label {
font-size: 13px;
color: #909399;
margin-bottom: 6px;
}
.mini-card__value {
font-size: 22px;
font-weight: 700;
color: #303133;
line-height: 1.2;
}
.mini-card__unit {
font-size: 12px;
font-weight: 400;
color: #909399;
margin-left: 4px;
}
.mini-card__trend {
font-size: 12px;
margin-top: 4px;
color: #909399;
&.is-up {
color: #f04747;
}
&.is-down {
color: #2ba471;
}
}
}
+35 -767
View File
@@ -1,788 +1,56 @@
<template>
<div class="wel-dashboard">
<!-- 欢迎横幅 -->
<div class="wel-banner">
<div class="wel-banner__deco">
<span class="wel-banner__circle wel-banner__circle--1"></span>
<span class="wel-banner__circle wel-banner__circle--2"></span>
<span class="wel-banner__circle wel-banner__circle--3"></span>
<span class="wel-banner__dot-grid">
<i v-for="n in 6" :key="'d'+n"></i>
</span>
</div>
<div class="wel-banner__left">
<div class="wel-banner__avatar">
<el-icon><Avatar /></el-icon>
</div>
<div class="wel-banner__info">
<h2 class="wel-banner__hello">
你好,{{ userName }}<span class="wel-banner__wave">👋</span>
</h2>
<p class="wel-banner__sub">
TMS-ERP 货运管理系统 · {{ todayText }}
</p>
</div>
</div>
<div class="wel-banner__right">
<el-button type="primary" :icon="Plus" @click="goPath('/business/waybill-manage')">
新建运单
</el-button>
</div>
</div>
<section-kpi />
<section-structure />
<section-business />
<section-project />
<section-finance />
<section-fleet />
<section-vehicle />
<section-summary />
<!-- 核心指标卡 -->
<el-row :gutter="16" class="wel-metrics">
<el-col v-for="m in metrics" :key="m.key" :xs="12" :sm="12" :md="6">
<div class="metric-card" :style="{ '--accent': m.color }">
<div class="metric-card__icon">
<el-icon><component :is="m.icon" /></el-icon>
</div>
<div class="metric-card__body">
<div class="metric-card__label">{{ m.label }}</div>
<div class="metric-card__value">
{{ m.value }}<span class="metric-card__unit">{{ m.unit }}</span>
</div>
<div
class="metric-card__trend"
:class="m.trend >= 0 ? 'is-up' : 'is-down'"
>
<el-icon>
<component :is="m.trend >= 0 ? 'Top' : 'Bottom'" />
</el-icon>
{{ Math.abs(m.trend) }}%
<span class="metric-card__trend-sub">{{ m.sub }}</span>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 图表区 -->
<el-row :gutter="16" class="wel-charts">
<el-col :xs="24" :md="16">
<div class="panel">
<div class="panel__head">
<span class="panel__title">运单量趋势</span>
<span class="panel__extra">近 7 日(单)</span>
</div>
<div class="trend">
<div v-for="b in trendBars" :key="b.label" class="trend__col">
<div class="trend__val">{{ b.value }}</div>
<div class="trend__bar">
<div
class="trend__fill"
:style="{ height: b.percent + '%' }"
></div>
</div>
<div class="trend__label">{{ b.label }}</div>
</div>
</div>
</div>
</el-col>
<el-col :xs="24" :md="8">
<div class="panel">
<div class="panel__head">
<span class="panel__title">货物类型分布</span>
</div>
<div class="donut-wrap">
<div class="donut">
<svg viewBox="0 0 140 140" class="donut__svg">
<circle
cx="70"
cy="70"
r="52"
fill="none"
stroke="#eef0f5"
stroke-width="14"
/>
<circle
v-for="(s, i) in donutSegments"
:key="i"
cx="70"
cy="70"
r="52"
fill="none"
:stroke="s.color"
stroke-width="14"
:stroke-dasharray="s.dash"
:stroke-dashoffset="s.offset"
transform="rotate(-90 70 70)"
/>
</svg>
<div class="donut__center">
<div class="donut__num">{{ donutTotal }}</div>
<div class="donut__unit">总运单</div>
</div>
</div>
<ul class="donut-legend">
<li v-for="(s, i) in donutSegments" :key="i">
<span class="dot" :style="{ background: s.color }"></span>
{{ s.name }}
<em>{{ s.value }}%</em>
</li>
</ul>
</div>
</div>
</el-col>
</el-row>
<!-- 下方区 -->
<el-row :gutter="16" class="wel-bottom">
<el-col :xs="24" :md="14">
<div class="panel">
<div class="panel__head">
<span class="panel__title">运输方式占比</span>
</div>
<div class="bar-list">
<div v-for="t in transportData" :key="t.name" class="bar-row">
<div class="bar-row__name">{{ t.name }}</div>
<div class="bar-row__track">
<div
class="bar-row__fill"
:style="{ width: t.value + '%', background: t.color }"
></div>
</div>
<div class="bar-row__val">{{ t.value }}%</div>
</div>
</div>
</div>
</el-col>
<el-col :xs="24" :md="10">
<div class="panel">
<div class="panel__head">
<span class="panel__title">预警提醒</span>
<el-badge :value="alerts.length" class="panel__badge" />
</div>
<ul class="alert-list">
<li
v-for="(a, i) in alerts"
:key="i"
class="alert-item"
:class="'lv-' + a.level"
>
<span class="alert-item__dot"></span>
<div class="alert-item__main">
<div class="alert-item__title">{{ a.title }}</div>
<div class="alert-item__desc">{{ a.desc }}</div>
</div>
<div class="alert-item__time">{{ a.time }}</div>
</li>
</ul>
</div>
</el-col>
</el-row>
<!-- 快捷入口 -->
<div class="panel wel-quick">
<div class="panel__head">
<span class="panel__title">快捷入口</span>
</div>
<div class="quick-grid">
<div
v-for="q in quickEntries"
:key="q.path"
class="quick-item"
:style="{ '--accent': q.color }"
@click="goPath(q.path)"
>
<div class="quick-item__icon">
<el-icon><component :is="q.icon" /></el-icon>
</div>
<div class="quick-item__name">{{ q.name }}</div>
</div>
</div>
</div>
<footer class="wel-footer">
© 2026 gxwebsoft · TMS-ERP 货运管理系统
</footer>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { Plus } from '@element-plus/icons-vue';
import SectionKpi from './components/section-kpi.vue';
import SectionStructure from './components/section-structure.vue';
import SectionBusiness from './components/section-business.vue';
import SectionProject from './components/section-project.vue';
import SectionFinance from './components/section-finance.vue';
import SectionFleet from './components/section-fleet.vue';
import SectionVehicle from './components/section-vehicle.vue';
import SectionSummary from './components/section-summary.vue';
export default {
name: 'welIndex',
data() {
return {
Plus,
// ===== Mock 数据:实际接入时替换为对应后端接口返回值 =====
// 核心指标:可对接各模块统计接口(如 /blade-business/waybill/count)
metrics: [
{ key: 'waybill', label: '今日运单', value: 186, unit: '单', icon: 'Document', color: '#409eff', trend: 12.5, sub: '较昨日' },
{ key: 'transit', label: '在途运输', value: 43, unit: '单', icon: 'Van', color: '#67c23a', trend: 3.2, sub: '在途运单' },
{ key: 'todo', label: '待办事项', value: 9, unit: '项', icon: 'Bell', color: '#e6a23c', trend: -2.0, sub: '待处理' },
{ key: 'capacity', label: '运力车辆', value: 128, unit: '辆', icon: 'Truck', color: '#9254de', trend: 5.1, sub: '在运+待命' },
],
// 运单趋势:可对接 /blade-business/waybill/trend?days=7
trendData: [
{ label: '周一', value: 120 },
{ label: '周二', value: 132 },
{ label: '周三', value: 101 },
{ label: '周四', value: 158 },
{ label: '周五', value: 190 },
{ label: '周六', value: 142 },
{ label: '周日', value: 186 },
],
// 货物类型分布:可对接 /blade-business/waybill/cargoStat
cargoData: [
{ name: '普货', value: 42, color: '#409eff' },
{ name: '冷链', value: 21, color: '#13c2c2' },
{ name: '大件', value: 16, color: '#fa8c16' },
{ name: '危险品', value: 12, color: '#f5222d' },
{ name: '其他', value: 9, color: '#bfbfbf' },
],
// 运输方式占比:可对接 /blade-business/waybill/transportStat
transportData: [
{ name: '海运', value: 48, color: '#409eff' },
{ name: '铁路', value: 22, color: '#52c41a' },
{ name: '公路', value: 20, color: '#fa8c16' },
{ name: '空运', value: 10, color: '#722ed1' },
],
// 预警提醒:可对接车辆年检/保险/违章 + 合同待审批等接口聚合
alerts: [
{ level: 'danger', title: '3 辆车年检临期', desc: '桂B·12345 等将于 7 日内到期', time: '今天' },
{ level: 'warning', title: '5 单合同待审批', desc: '客户「广西物产」等提交待审核', time: '今天' },
{ level: 'danger', title: '2 起违章未处理', desc: '桂A·88xxx 超速记录待缴纳', time: '昨天' },
{ level: 'info', title: '4 辆保险下月到期', desc: '建议提前安排续保', time: '本周' },
],
// 快捷入口:path 与菜单路由一致
quickEntries: [
{ name: '运单管理', path: '/business/waybill-manage', icon: 'Document', color: '#409eff' },
{ name: '运输计划', path: '/business/transport-plan', icon: 'Promotion', color: '#13c2c2' },
{ name: '合同管理', path: '/business/contract-manage', icon: 'Files', color: '#fa8c16' },
{ name: '车辆管理', path: '/vehicle/customer-archive', icon: 'Van', color: '#67c23a' },
{ name: '维修记录', path: '/vehicle/maintenance-record', icon: 'Tools', color: '#9254de' },
{ name: '违章记录', path: '/vehicle/violation-record', icon: 'Warning', color: '#f5222d' },
{ name: '工作待办', path: '/work/todo', icon: 'Bell', color: '#e6a23c' },
{ name: '报表中心', path: '/report/reportlist', icon: 'DataLine', color: '#1677ff' },
],
};
},
computed: {
...mapGetters(['userInfo']),
userName() {
return this.userInfo.real_name || this.userInfo.user_name || '用户';
},
todayText() {
const d = new Date();
const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const pad = n => (n < 10 ? '0' + n : n);
return `${d.getFullYear()}年${pad(d.getMonth() + 1)}月${pad(d.getDate())}日 ${week[d.getDay()]}`;
},
trendBars() {
const max = Math.max(...this.trendData.map(d => d.value)) || 1;
return this.trendData.map(d => ({
...d,
percent: Math.round((d.value / max) * 100),
}));
},
donutTotal() {
return this.cargoData.reduce((s, d) => s + d.value, 0);
},
donutSegments() {
const total = this.cargoData.reduce((s, d) => s + d.value, 0) || 1;
const r = 52;
const c = 2 * Math.PI * r;
let acc = 0;
return this.cargoData.map(d => {
const len = (d.value / total) * c;
const seg = { ...d, dash: `${len} ${c - len}`, offset: -acc };
acc += len;
return seg;
});
},
},
methods: {
goPath(path) {
this.$router.push({ path });
},
components: {
SectionKpi,
SectionStructure,
SectionBusiness,
SectionProject,
SectionFinance,
SectionFleet,
SectionVehicle,
SectionSummary,
},
};
</script>
<style lang="scss" scoped>
.wel-dashboard {
padding: 8px 4px;
padding: 12px 4px 0;
background: #f5f7fa;
min-height: 100%;
}
/* 横幅 */
.wel-banner {
display: flex;
align-items: center;
justify-content: space-between;
background: #ffffff;
border-radius: 12px;
padding: 26px 30px;
color: #303133;
margin-bottom: 16px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
/* 横幅装饰元素 */
.wel-banner__deco {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.wel-banner__circle {
position: absolute;
border-radius: 50%;
background: rgba(115, 209, 61, 0.07);
&--1 {
width: 200px;
height: 200px;
right: -40px;
top: -60px;
background: rgba(115, 209, 61, 0.1);
}
&--2 {
width: 140px;
height: 140px;
right: 120px;
bottom: -50px;
background: rgba(115, 209, 61, 0.06);
}
&--3 {
width: 80px;
height: 80px;
left: 25%;
top: -20px;
background: rgba(115, 209, 61, 0.05);
}
}
.wel-banner__dot-grid {
position: absolute;
right: 28px;
top: 50%;
transform: translateY(-50%);
display: grid;
grid-template-columns: repeat(3, 5px);
gap: 8px;
i {
width: 5px;
height: 5px;
border-radius: 50%;
background: rgba(115, 209, 61, 0.2);
&:nth-child(3n) { background: rgba(115, 209, 61, 0.35); }
&:nth-child(3n + 1) { background: rgba(115, 209, 61, 0.12); }
}
}
.wel-banner__left {
display: flex;
align-items: center;
gap: 16px;
position: relative;
z-index: 1;
}
.wel-banner__avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(115, 209, 61, 0.15);
color: #389e0d;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
}
.wel-banner__hello {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.wel-banner__wave {
margin-left: 6px;
}
.wel-banner__sub {
margin: 6px 0 0;
font-size: 13px;
opacity: 0.9;
}
.wel-banner__right {
position: relative;
z-index: 1;
}
/* 指标卡 */
.wel-metrics {
margin-bottom: 0;
}
/* 图表区:左右面板等高 */
.wel-charts {
margin-bottom: 0;
align-items: stretch;
.el-col {
display: flex;
> .panel {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
/* 下方区:左右面板等高 */
.wel-bottom {
align-items: stretch;
.el-col {
display: flex;
> .panel {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.metric-card {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
border-left: 4px solid var(--accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
}
.metric-card__icon {
width: 54px;
height: 54px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
color: #fff;
background: var(--accent);
}
.metric-card__label {
font-size: 13px;
color: #909399;
}
.metric-card__value {
font-size: 26px;
font-weight: 700;
color: #303133;
line-height: 1.3;
}
.metric-card__unit {
font-size: 13px;
font-weight: 400;
color: #909399;
margin-left: 4px;
}
.metric-card__trend {
font-size: 12px;
display: flex;
align-items: center;
gap: 2px;
&.is-up {
color: #67c23a;
}
&.is-down {
color: #f56c6c;
}
}
.metric-card__trend-sub {
color: #c0c4cc;
margin-left: 4px;
}
/* 面板通用 */
.panel {
background: #fff;
border-radius: 10px;
padding: 18px 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
// 当面板在 flex 列容器中时,内容区自动撑满
.trend,
.donut-wrap,
.bar-list,
.alert-list {
flex: 1;
}
}
.panel__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding-left: 8px;
border-left: 4px solid #409eff;
}
.panel__title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.panel__extra {
font-size: 12px;
color: #909399;
}
.panel__badge {
margin-top: -2px;
}
/* 趋势柱 */
.trend {
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 200px;
padding: 0 4px;
}
.trend__col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
}
.trend__val {
font-size: 12px;
color: #606266;
margin-bottom: 6px;
}
.trend__bar {
flex: 1;
width: 60%;
max-width: 36px;
background: #f0f2f5;
border-radius: 6px 6px 0 0;
display: flex;
align-items: flex-end;
overflow: hidden;
}
.trend__fill {
width: 100%;
background: linear-gradient(180deg, #5aa9ff, #409eff);
border-radius: 6px 6px 0 0;
transition: height 0.4s ease;
}
.trend__label {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
/* 环形图 */
.donut-wrap {
display: flex;
align-items: center;
gap: 18px;
min-height: 200px;
}
.donut {
position: relative;
width: 140px;
height: 140px;
flex-shrink: 0;
}
.donut__svg {
transform: rotate(0deg);
}
.donut__center {
position: absolute;
top: 0;
left: 0;
width: 140px;
height: 140px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.donut__num {
font-size: 24px;
font-weight: 700;
color: #303133;
}
.donut__unit {
font-size: 12px;
color: #909399;
}
.donut-legend {
list-style: none;
margin: 0;
padding: 0;
flex: 1;
li {
display: flex;
align-items: center;
font-size: 13px;
color: #606266;
padding: 5px 0;
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
em {
margin-left: auto;
font-style: normal;
color: #909399;
}
}
}
/* 横向条 */
.bar-list {
padding: 4px 0;
}
.bar-row {
display: flex;
align-items: center;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.bar-row__name {
width: 48px;
font-size: 13px;
color: #606266;
}
.bar-row__track {
flex: 1;
height: 10px;
background: #f0f2f5;
border-radius: 6px;
overflow: hidden;
margin: 0 12px;
}
.bar-row__fill {
height: 100%;
border-radius: 6px;
transition: width 0.4s ease;
}
.bar-row__val {
width: 44px;
text-align: right;
font-size: 13px;
color: #909399;
}
/* 预警列表 */
.alert-list {
list-style: none;
margin: 0;
padding: 0;
}
.alert-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f2f5;
&:last-child {
border-bottom: none;
}
}
.alert-item__dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 12px;
flex-shrink: 0;
}
.alert-item.lv-danger .alert-item__dot {
background: #f56c6c;
}
.alert-item.lv-warning .alert-item__dot {
background: #e6a23c;
}
.alert-item.lv-info .alert-item__dot {
background: #409eff;
}
.alert-item__main {
flex: 1;
min-width: 0;
}
.alert-item__title {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.alert-item__desc {
font-size: 12px;
color: #909399;
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert-item__time {
font-size: 12px;
color: #c0c4cc;
flex-shrink: 0;
margin-left: 12px;
}
/* 快捷入口 */
.wel-quick {
margin-bottom: 0;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.quick-item {
background: #fafbfc;
border: 1px solid #eef0f5;
border-radius: 10px;
padding: 18px 12px;
.wel-footer {
text-align: center;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: var(--accent);
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
}
.quick-item__icon {
width: 44px;
height: 44px;
margin: 0 auto 10px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: #fff;
background: var(--accent);
}
.quick-item__name {
font-size: 13px;
color: #606266;
}
@media (max-width: 768px) {
.quick-grid {
grid-template-columns: repeat(2, 1fr);
}
.wel-banner {
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
font-size: 12px;
color: #c0c4cc;
padding: 8px 0 20px;
}
</style>