feat(frontend): 统计页接入 ECharts,四张图(油价/油耗/费用/成本)

把 Statistics 从占位 Empty 升级成真正的图表页,plan §9.4 落地:
- echarts + vue-echarts,按需引入 CanvasRenderer / LineChart / BarChart
  + Grid / Tooltip / Legend / Title / DataZoom(全部 tree-shakable)
- 四张图,每张独立 NCard,统一主题色 brand #18a058
  1) 油价趋势(折线 + 面积)—— 每月 total_amount / total_liters 当月均价
  2) 标准油耗趋势(折线)—— 每月在"连续两次 is_full"之间采样的均值
  3) 月度费用(柱状)—— 每月加油 total_amount 之和
  4) 每公里成本(折线)—— 同油耗采样算法
- 时间窗选择:近 3 月 / 6 月 / 12 月 / 全部(NSelect 顶栏)
- 油耗 / 每公里成本采样逻辑与后端 pkg/decimal 严格对齐:
  * 列表按 refuel_time 升序遍历
  * 只在"is_full 序列"内采样
  * 半箱油记录自动跳过,不污染指标
- autoresize 适配移动端旋转 / 桌面窗口
- Loading / Empty / Error 状态完整;切车自动重拉

未做(留待后续):
- DataZoom 区域缩放 —— 已在 echarts 引入但没启用,数据量大时再上
- 多车辆对比 —— 计划里没明确要求,先不上
- 油价对比基准线(同地区均值) —— 暂无数据源

验证:
  pnpm typecheck   0 errors
  pnpm build       18.1s(echarts 入了 4 个组件 + 1 个 renderer,bundle 略增)
This commit is contained in:
2026-09-13 16:52:26 +08:00
parent ea158f2a53
commit 33dfdc824c
3 changed files with 265 additions and 6 deletions
+37
View File
@@ -11,6 +11,9 @@ importers:
axios:
specifier: ^1.7.7
version: 1.20.0
echarts:
specifier: ^6.1.0
version: 6.1.0
naive-ui:
specifier: ^2.40.1
version: 2.45.3(vue@3.5.42(typescript@5.6.3))
@@ -23,6 +26,9 @@ importers:
vue:
specifier: ^3.5.13
version: 3.5.42(typescript@5.6.3)
vue-echarts:
specifier: ^8.3.0
version: 8.3.0(echarts@6.1.0)(vue@3.5.42(typescript@5.6.3))
vue-router:
specifier: ^4.4.5
version: 4.6.4(vue@3.5.42(typescript@5.6.3))
@@ -639,6 +645,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
echarts@6.1.0:
resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
electron-to-chromium@1.5.427:
resolution: {integrity: sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==}
@@ -1057,6 +1066,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
typescript@5.6.3:
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
engines: {node: '>=14.17'}
@@ -1167,6 +1179,12 @@ packages:
'@vue/composition-api':
optional: true
vue-echarts@8.3.0:
resolution: {integrity: sha512-gqOZ5uuBF4x4KTXQpTmgH5BuAwUSO+y9U7L1pHCIESQvgnTitXtnyjrfePH+9YXMRhmTLjchEx3EnsPhoiIX/A==}
peerDependencies:
echarts: ^6.0.0
vue: ^3.3.0
vue-router@4.6.4:
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
@@ -1194,6 +1212,9 @@ packages:
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
zrender@6.1.0:
resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
snapshots:
'@alloc/quick-lru@5.3.0': {}
@@ -1644,6 +1665,11 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
echarts@6.1.0:
dependencies:
tslib: 2.3.0
zrender: 6.1.0
electron-to-chromium@1.5.427: {}
entities@7.0.1: {}
@@ -2101,6 +2127,8 @@ snapshots:
ts-interface-checker@0.1.13: {}
tslib@2.3.0: {}
typescript@5.6.3: {}
ufo@1.6.4: {}
@@ -2198,6 +2226,11 @@ snapshots:
dependencies:
vue: 3.5.42(typescript@5.6.3)
vue-echarts@8.3.0(echarts@6.1.0)(vue@3.5.42(typescript@5.6.3)):
dependencies:
echarts: 6.1.0
vue: 3.5.42(typescript@5.6.3)
vue-router@4.6.4(vue@3.5.42(typescript@5.6.3)):
dependencies:
'@vue/devtools-api': 6.6.4
@@ -2231,3 +2264,7 @@ snapshots:
vue: 3.5.42(typescript@5.6.3)
webpack-virtual-modules@0.6.2: {}
zrender@6.1.0:
dependencies:
tslib: 2.3.0