feat(frontend): 搭建 Vue 3 + Vite + TS + Pinia + Naive UI 骨架
Phase 5(项目骨架部分)。后端已经稳了,现在前端可以挂上 curl。
本 commit 只做"能跑、能路由、能调 /api",业务页面留到后续 commit 一个个填。
新增:
- package.json + .env.example
* vue 3.5 / vite 5.4 / typescript 5.6 / pinia 2.3 / vue-router 4.6
* naive-ui 2.45 + vfonts(Naive UI 官方推荐字体)
* tailwindcss 3.4 + autoprefixer + postcss
* unplugin-auto-import / unplugin-vue-components 减少样板代码
* axios 1.7
scripts:dev / build / preview / typecheck / lint
- vite.config.js(注意:实际生效的是 .js,因为早期 vue-tsc -b 留了 .js 残留;
.ts 留作类型参考,内容保持一致)
* /api 代理到 127.0.0.1:8080,本地一站跑通前后端
* 强制 es2020 target,关闭 sourcemap
- tsconfig.json / tsconfig.node.json
* skipLibCheck 跳过第三方 .d.mts 噪音
* strict + noImplicitAny
- tailwind.config.js
* 主题色用 CSS 变量(rgb(var(--brand) / <alpha-value>)),
组件层不写死颜色,符合 plan §12 / AGENTS.md
- postcss.config.js
- index.html + public/favicon.svg(简单加油机图标)
- src/main.ts
* 引入 vfonts / tailwind / theme.css
* createApp + Pinia + router
- src/styles/tailwind.css + src/styles/theme.css
* Light/Dark 模式都从 :root / html.dark 拿 CSS 变量
* 全局滚动条 / 移动端 -webkit-tap-highlight 处理
- src/App.vue
* NConfigProvider + NDialog + NNotification + NMessage + NLoadingBar
* 跟随系统 dark 模式,watchEffect 同步 html.dark class
- src/router/index.ts
* 路由: / /dashboard /refuels /refuels/new /vehicles /statistics
* /:pathMatch(.*)* -> NotFound
* afterEach 改 document.title
- src/api/{client,types,vehicles,refuels}.ts
* axios 实例,统一把后端 {code,message} 错误解到 Error.cause
* 类型与后端 model/DTO 一一对应
- src/stores/vehicle.ts
* Pinia setup store,持有车辆列表 + 当前选中
* loadList 自动挑 default_vehicle
- src/utils/decimal.ts
* 与后端 pkg/decimal 对齐的 pricePerLiter / consumptionL100km / costPerKm
* 配套 fmt* 展示格式化函数
- src/layouts/AppLayout.vue
* 桌面端顶栏 + 移动端底部 Tab
* 移动端底部 safe-area-inset 适配刘海/小白条
- src/pages/{Dashboard,Refuels,RefuelNew,Vehicles,Statistics,NotFound}.vue
* Dashboard 真的拿 /api/vehicles/:id/statistics 渲染主指标(其它字段保留为占位)
* Refuels / RefuelNew / Vehicles / Statistics 是占位 Empty,Phase 5 业务页接入
- src/env.d.ts
* Vite ImportMeta 类型 + .vue 模块声明
期间踩到的两个坑(都修了):
1) `tsconfig.json` extends 了 @vue/tsconfig 但里面引用了不存在的
`@vue/tsconfig/tsconfig.node.json` —— 改成不依赖 extends,自己写
2) `vite build` 报 "Could not resolve entry module 'vueuc'":根因是早期
`vue-tsc -b` 在项目根生成了 vite.config.js / vite.config.d.ts 残留,
Vite 优先读 .js(老的 manualChunks 引用了 vueuc),整套 plugin 也不加载
(3 modules transformed)。修法:把正确配置同步写到 vite.config.js,
让 Vite 一直读 .js;.ts 留作类型参考
验证:
pnpm install 199 packages 安装成功
pnpm typecheck 0 errors
pnpm build 11.5s,dist 643 KB(uncompressed),主 JS 344 KB
gzipped 107 KB(naive-ui 主力,符合预期)
未做(留待后续 commit):
- 各业务页(Dashboard 仅骨架,Refuels / RefuelNew / Vehicles / Statistics 都是 Empty)
- 真实表单校验、Loading / Empty / Error 三态完整接入
- PWA(manifest + service worker) —— Phase 12
- 暗色模式手工切换(目前只跟随系统,Phase 11 UI 精修再补)
- ESLint 规则初始化
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { NButton, NCard, NSpin, NTag, NEmpty, NText } 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 stats = ref<RefuelStatistics | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadStats() {
|
||||
if (!vehicleStore.currentId) return
|
||||
loading.value = true
|
||||
try {
|
||||
stats.value = await refuelsApi.statistics(vehicleStore.currentId)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await vehicleStore.loadList()
|
||||
await loadStats()
|
||||
})
|
||||
</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>
|
||||
</NEmpty>
|
||||
</NCard>
|
||||
|
||||
<template v-else>
|
||||
<div>
|
||||
<NText depth="3" class="text-sm">当前车辆</NText>
|
||||
<div class="text-2xl font-semibold mt-1">
|
||||
{{ vehicleStore.getCurrent()?.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-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>
|
||||
<div class="text-xs text-ink-soft">平均油价</div>
|
||||
<div class="text-lg font-semibold mt-0.5">
|
||||
{{ fmtPrice(stats.avg_price) }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-ink-soft">平均油耗</div>
|
||||
<div class="text-lg font-semibold mt-0.5">
|
||||
{{ fmtConsumption(stats.avg_consumption) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<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>
|
||||
</NCard>
|
||||
</NSpin>
|
||||
|
||||
<NButton
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="$router.push({ name: 'refuel-new' })"
|
||||
>
|
||||
+ 记录加油
|
||||
</NButton>
|
||||
|
||||
<NCard v-if="stats?.last_refuel" title="最近加油">
|
||||
<div class="flex justify-between items-baseline">
|
||||
<div class="font-medium">
|
||||
{{ stats.last_refuel.station_name || '未填写加油站' }}
|
||||
</div>
|
||||
<NTag size="small" :type="stats.last_refuel.is_full ? 'success' : 'default'">
|
||||
{{ stats.last_refuel.is_full ? '加满' : '半箱' }}
|
||||
</NTag>
|
||||
</div>
|
||||
<div class="text-sm text-ink-soft mt-1">
|
||||
{{ stats.last_refuel.liters.toFixed(2) }} L ·
|
||||
¥{{ stats.last_refuel.total_amount.toFixed(2) }} ·
|
||||
{{ new Date(stats.last_refuel.refuel_time).toLocaleDateString() }}
|
||||
</div>
|
||||
</NCard>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { NResult, NButton } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-6">
|
||||
<NResult status="404" title="404" description="页面不存在">
|
||||
<template #footer>
|
||||
<NButton type="primary" @click="router.replace({ name: 'dashboard' })">
|
||||
回首页
|
||||
</NButton>
|
||||
</template>
|
||||
</NResult>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { NEmpty } from 'naive-ui'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-screen-md mx-auto">
|
||||
<NEmpty description="新增加油页(骨架占位,Phase 4 接入)">
|
||||
<div class="text-sm text-ink-soft mt-2">
|
||||
字段:加油时间 / 里程 / 加油量 / 总金额 / 油品 / 加油站 / 是否加满 / 备注
|
||||
</div>
|
||||
<div class="text-sm text-ink-soft mt-1">
|
||||
实际油价(price_per_liter)由后端实时计算,前端只入 amount + liters
|
||||
</div>
|
||||
</NEmpty>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { NEmpty } from 'naive-ui'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-screen-md mx-auto">
|
||||
<NEmpty description="加油记录列表(骨架占位,Phase 4 接入)">
|
||||
<div class="text-sm text-ink-soft mt-2">
|
||||
API:GET /api/refuels?vehicle_id=...&start_date=...&end_date=...
|
||||
</div>
|
||||
</NEmpty>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { NEmpty } from 'naive-ui'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-screen-md mx-auto">
|
||||
<NEmpty description="统计图表(骨架占位,Phase 4 接入 ECharts)">
|
||||
<div class="text-sm text-ink-soft mt-2">
|
||||
油价趋势 / 油耗趋势 / 月度费用 / 每公里成本
|
||||
</div>
|
||||
</NEmpty>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { NEmpty } from 'naive-ui'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-screen-md mx-auto">
|
||||
<NEmpty description="车辆管理页(骨架占位,Phase 4 接入)">
|
||||
<div class="text-sm text-ink-soft mt-2">
|
||||
API:GET/POST/PUT/DELETE /api/vehicles;POST /api/vehicles/:id/default
|
||||
</div>
|
||||
</NEmpty>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user