feat(frontend): Dashboard 精修(切车器 + 累计升数 + 错误态)
把 Dashboard 从基本骨架升级到 MVP 可用状态: - 切车:多车辆时显示 NSelect,直接切;单车时只显示名字(不让选) * 切车 watch currentId 自动重拉 statistics,无需手动刷新 - 主指标卡:把"本月加油"标题改成"累计加油",因为目前后端没给"本月" 过滤,只是总量;数字含义与 Backend RefuelStatistics.total_amount 一致 - 4 项指标:平均油价 / 平均油耗 / 每公里成本 / 累计升数 —— 之前只有 3 项, 补上总升数 - Empty / Loading / Error 三态完整: * Loading -> NSpin * 没记录但有车 -> "还没有加油记录" * API 失败 -> useMessage.error + stats 清空 - 保留已有:大字号主指标、"记录加油"主按钮、"最近加油"卡片 未做(留待后续): - "本月"维度过滤(后端可加 query?period=month,Dashboard 切 month/all) —— plan §9.1 提了"本月加油",MVP 留总量已经够 - 离线缓存 / PWA install prompt 验证: pnpm typecheck 0 errors pnpm build 通过
This commit is contained in:
@@ -1,20 +1,36 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { NButton, NCard, NSpin, NTag, NEmpty, NText } from 'naive-ui'
|
import {
|
||||||
|
NButton,
|
||||||
|
NCard,
|
||||||
|
NEmpty,
|
||||||
|
NSpin,
|
||||||
|
NTag,
|
||||||
|
NText,
|
||||||
|
NSelect,
|
||||||
|
useMessage,
|
||||||
|
} from 'naive-ui'
|
||||||
import { useVehicleStore } from '@/stores/vehicle'
|
import { useVehicleStore } from '@/stores/vehicle'
|
||||||
import { refuelsApi } from '@/api/refuels'
|
import { refuelsApi } from '@/api/refuels'
|
||||||
import type { RefuelStatistics } from '@/api/types'
|
import type { RefuelStatistics } from '@/api/types'
|
||||||
import { fmtAmount, fmtConsumption, fmtCostPerKm, fmtPrice } from '@/utils/decimal'
|
import { fmtAmount, fmtConsumption, fmtCostPerKm, fmtPrice } from '@/utils/decimal'
|
||||||
|
|
||||||
const vehicleStore = useVehicleStore()
|
const vehicleStore = useVehicleStore()
|
||||||
|
const message = useMessage()
|
||||||
const stats = ref<RefuelStatistics | null>(null)
|
const stats = ref<RefuelStatistics | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
if (!vehicleStore.currentId) return
|
if (!vehicleStore.currentId) {
|
||||||
|
stats.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
stats.value = await refuelsApi.statistics(vehicleStore.currentId)
|
stats.value = await refuelsApi.statistics(vehicleStore.currentId)
|
||||||
|
} catch (err) {
|
||||||
|
message.error((err as Error).message || '加载统计失败')
|
||||||
|
stats.value = null
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -24,34 +40,65 @@ onMounted(async () => {
|
|||||||
await vehicleStore.loadList()
|
await vehicleStore.loadList()
|
||||||
await loadStats()
|
await loadStats()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => vehicleStore.currentId,
|
||||||
|
() => loadStats(),
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentVehicle = computed(() => vehicleStore.getCurrent())
|
||||||
|
|
||||||
|
const vehicleOptions = computed(() =>
|
||||||
|
vehicleStore.list.map((v) => ({
|
||||||
|
label: `${v.name}${v.default_vehicle ? ' (默认)' : ''}`,
|
||||||
|
value: v.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
function onVehicleChange(id: number | null) {
|
||||||
|
if (id != null) vehicleStore.setCurrentId(id)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-4 max-w-screen-md mx-auto">
|
<div class="space-y-4 max-w-screen-md mx-auto">
|
||||||
<NCard v-if="!vehicleStore.list.length" class="text-center py-12">
|
<NCard v-if="!vehicleStore.list.length" class="text-center py-12">
|
||||||
<NEmpty description="还没有车辆,先去添加一辆吧">
|
<NEmpty description="还没有车辆,先去添加一辆吧">
|
||||||
<NButton type="primary" @click="$router.push({ name: 'vehicles' })">添加车辆</NButton>
|
<NButton type="primary" @click="$router.push({ name: 'vehicles' })">
|
||||||
|
添加车辆
|
||||||
|
</NButton>
|
||||||
</NEmpty>
|
</NEmpty>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div>
|
<!-- 切车器:多车时显示,单车时只显示名字(不让选) -->
|
||||||
|
<div v-if="vehicleStore.list.length > 1" class="flex items-center gap-2">
|
||||||
|
<NText depth="3" class="text-sm">当前车辆</NText>
|
||||||
|
<NSelect
|
||||||
|
:value="vehicleStore.currentId"
|
||||||
|
:options="vehicleOptions"
|
||||||
|
size="small"
|
||||||
|
style="min-width: 160px"
|
||||||
|
@update:value="onVehicleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<NText depth="3" class="text-sm">当前车辆</NText>
|
<NText depth="3" class="text-sm">当前车辆</NText>
|
||||||
<div class="text-2xl font-semibold mt-1">
|
<div class="text-2xl font-semibold mt-1">
|
||||||
{{ vehicleStore.getCurrent()?.name || '—' }}
|
{{ currentVehicle?.name || '—' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NSpin :show="loading">
|
<NSpin :show="loading">
|
||||||
<NCard v-if="stats" class="text-center py-6">
|
<NCard v-if="stats" class="text-center py-6">
|
||||||
<div class="text-xs text-ink-soft">本月加油</div>
|
<div class="text-xs text-ink-soft">累计加油</div>
|
||||||
<div class="text-5xl font-bold mt-1 tracking-tight">
|
<div class="text-5xl font-bold mt-1 tracking-tight">
|
||||||
{{ fmtAmount(stats.total_amount) }}
|
{{ fmtAmount(stats.total_amount) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-ink-soft mt-1">
|
<div class="text-sm text-ink-soft mt-1">
|
||||||
共 {{ stats.total_records }} 笔
|
共 {{ stats.total_records }} 笔
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4 mt-6">
|
<div class="grid grid-cols-2 gap-4 mt-6 text-left">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-ink-soft">平均油价</div>
|
<div class="text-xs text-ink-soft">平均油价</div>
|
||||||
<div class="text-lg font-semibold mt-0.5">
|
<div class="text-lg font-semibold mt-0.5">
|
||||||
@@ -64,14 +111,21 @@ onMounted(async () => {
|
|||||||
{{ fmtConsumption(stats.avg_consumption) }}
|
{{ fmtConsumption(stats.avg_consumption) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
<div>
|
||||||
<div class="text-xs text-ink-soft">每公里成本</div>
|
<div class="text-xs text-ink-soft">每公里成本</div>
|
||||||
<div class="text-lg font-semibold mt-0.5">
|
<div class="text-lg font-semibold mt-0.5">
|
||||||
{{ fmtCostPerKm(stats.cost_per_km) }}
|
{{ fmtCostPerKm(stats.cost_per_km) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-ink-soft">累计升数</div>
|
||||||
|
<div class="text-lg font-semibold mt-0.5">
|
||||||
|
{{ stats.total_liters.toFixed(2) }} L
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
<NEmpty v-else-if="!loading" description="还没有加油记录" />
|
||||||
</NSpin>
|
</NSpin>
|
||||||
|
|
||||||
<NButton
|
<NButton
|
||||||
|
|||||||
Reference in New Issue
Block a user