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">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { NButton, NCard, NSpin, NTag, NEmpty, NText } from 'naive-ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NCard,
|
||||
NEmpty,
|
||||
NSpin,
|
||||
NTag,
|
||||
NText,
|
||||
NSelect,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { useVehicleStore } from '@/stores/vehicle'
|
||||
import { refuelsApi } from '@/api/refuels'
|
||||
import type { RefuelStatistics } from '@/api/types'
|
||||
import { fmtAmount, fmtConsumption, fmtCostPerKm, fmtPrice } from '@/utils/decimal'
|
||||
|
||||
const vehicleStore = useVehicleStore()
|
||||
const message = useMessage()
|
||||
const stats = ref<RefuelStatistics | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadStats() {
|
||||
if (!vehicleStore.currentId) return
|
||||
if (!vehicleStore.currentId) {
|
||||
stats.value = null
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
stats.value = await refuelsApi.statistics(vehicleStore.currentId)
|
||||
} catch (err) {
|
||||
message.error((err as Error).message || '加载统计失败')
|
||||
stats.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -24,34 +40,65 @@ onMounted(async () => {
|
||||
await vehicleStore.loadList()
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4 max-w-screen-md mx-auto">
|
||||
<NCard v-if="!vehicleStore.list.length" class="text-center py-12">
|
||||
<NEmpty description="还没有车辆,先去添加一辆吧">
|
||||
<NButton type="primary" @click="$router.push({ name: 'vehicles' })">添加车辆</NButton>
|
||||
<NButton type="primary" @click="$router.push({ name: 'vehicles' })">
|
||||
添加车辆
|
||||
</NButton>
|
||||
</NEmpty>
|
||||
</NCard>
|
||||
|
||||
<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>
|
||||
<div class="text-2xl font-semibold mt-1">
|
||||
{{ vehicleStore.getCurrent()?.name || '—' }}
|
||||
{{ currentVehicle?.name || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NSpin :show="loading">
|
||||
<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">
|
||||
{{ fmtAmount(stats.total_amount) }}
|
||||
</div>
|
||||
<div class="text-sm text-ink-soft mt-1">
|
||||
共 {{ stats.total_records }} 笔
|
||||
</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 class="text-xs text-ink-soft">平均油价</div>
|
||||
<div class="text-lg font-semibold mt-0.5">
|
||||
@@ -64,14 +111,21 @@ onMounted(async () => {
|
||||
{{ fmtConsumption(stats.avg_consumption) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div>
|
||||
<div class="text-xs text-ink-soft">每公里成本</div>
|
||||
<div class="text-lg font-semibold mt-0.5">
|
||||
{{ fmtCostPerKm(stats.cost_per_km) }}
|
||||
</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>
|
||||
</NCard>
|
||||
<NEmpty v-else-if="!loading" description="还没有加油记录" />
|
||||
</NSpin>
|
||||
|
||||
<NButton
|
||||
|
||||
Reference in New Issue
Block a user