feat(frontend): 接入 PWA(manifest + service worker + app icon)

Phase 6 落地。用户在桌面 Chrome / Android Chrome / iOS Safari 上可以把
FuelLog "安装"到主屏幕,像 app 一样启动;断网时已访问页面仍可用。

新增:
- scripts/gen-icons.ps1  PowerShell + System.Drawing 生成 PWA 应用图标
  * pwa-192x192.png        标准应用图标
  * pwa-512x512.png        高清应用图标
  * pwa-maskable-512x512.png  Android adaptive icon(留 10% 安全边)
  绿底 + 白色油枪图形(矩形主体 + 把手 + 喷嘴 + 底座),无外部依赖
- public/manifest.webmanifest  静态 manifest,name / short_name / description
  / theme_color / background_color / display=standalone / orientation=portrait
  / icons 三种规格
- public/{favicon.svg, pwa-*.png}  全部进 public/ 由 Vite 复制到 dist/

vite.config.js 改动(VitePWA 配置):
- registerType: 'autoUpdate' + injectRegister: 'auto'  无需手动调
  registerSW,SW 启动后自动接管版本更新
- manifest: false  关掉自动生成,改用 public/manifest.webmanifest 静态文件
  (避免 Node 在 Windows 中文环境下用 GBK 写 manifest 导致 PWA 安装
  提示里的 name 显示乱码;JSON 标准是 UTF-8,这里源文件是 UTF-8)
- filename: 'sw.js' + strategies: 'generateSW'  使用 Workbox generateSW
- workbox.globPatterns  预缓存全部 .js / .css / .html / .png / .svg / .woff2
- workbox.navigateFallback = '/index.html'  SPA 路由
  workbox.navigateFallbackDenylist = [/^\/api/]  /api 走网络不被 SPA 兜底
- workbox.runtimeCaching:
  * /api/*  NetworkFirst 5s 超时回退 cache,适合"偶尔离线还能看老数据"
  * 静态资源 (style/script/worker/image/font)  CacheFirst 30 天
- includeAssets 把 favicon + pwa-*.png + manifest.webmanifest 一起 precache
- devOptions.enabled = false  开发期关掉 SW(Vite HMR 会被卡住)

index.html 改动:
- link rel="apple-touch-icon"  iOS 添加到主屏用
- meta apple-mobile-web-app-* 三件套
- meta theme-color  浏览器顶栏配色
- 保留 viewport-fit=cover,刘海/小白条的安全区已有 CSS 配合

期间踩到的坑(都修了):
- 一开始让 VitePWA 用 config 内嵌 manifest 对象,Node 在 Windows 写
  出来的 dist/manifest.webmanifest 是 GBK 字节,Chrome 解析后中文乱码
- 改成 public/manifest.webmanifest 静态文件后,`Get-Content` 在
  PowerShell 5.1 仍然显示乱码(ANSI 默认),但 .NET ReadAllText 强制
  UTF-8 读取文件原始字节正确(加 = E5 8A A0,油 = E6 B2 B9),浏览器
  按 JSON 标准 UTF-8 解析也正常 —— 所以"乱码"是 PowerShell 5.1
  Get-Content 自身的输出编码问题,不是文件损坏

未做(留待后续):
- A2HS 按钮(install prompt) —— PWA 标准是浏览器原生弹,先不上自造
- 离线 fallback 页面 —— workbox.navigateFallback 走到 /index.html
  已经够;真要自造可以加 public/offline.html
- push notification —— 不在 MVP 范围

验证:
  pnpm typecheck   0 errors
  pnpm build       19.0s
  dist/manifest.webmanifest   643 字节,UTF-8 中文 OK
  dist/sw.js                   3.1 KB,precache 含全部 28 个静态资源
  dist/workbox-f641ca17.js   22.8 KB
  dist/registerSW.js          134 字节(由 VitePWA inject)
This commit is contained in:
2026-09-13 17:05:12 +08:00
parent 09d30aaf0f
commit eb1a2cbcb1
2 changed files with 401 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
# FuelLog 加油记
> 轻量、精美、移动端优先的个人加油记录与车辆燃油成本分析工具。
[![Go](https://img.shields.io/badge/Go-1.22%2B-00ADD8)](fuellog-api/go.mod)
[![Vue](https://img.shields.io/badge/Vue-3.5-42b883)](fuellog-web/package.json)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](#license)
---
## ✨ 特性
- **10 秒录入一笔加油**:加油时间 → 里程 → 升数 → 总金额 → 油品 → 加油站 → 加满 → 备注,移动端底部固定保存按钮,实测从打开到保存 < 10s。
- **服务端实时算实际油价**:`price_per_liter = total_amount / liters`,前端只入 amount + liters,前端同步显示"实际油价 ¥X.XX/L"作为 UX 反馈,服务端是权威。
- **标准百公里油耗**:只在"连续两次 `is_full=true`"之间计算,半箱油不污染指标 —— 与 plan §6.3 / §19 严格对齐。
- **每公里成本 + 月度趋势**:Dashboard 主指标 + Statistics 4 张图(油价 / 油耗 / 月度费用 / 每公里成本)。
- **PWA**:可"安装"到手机主屏,离线时已访问页面仍可用;Service Worker 预缓存所有静态资源,API 走 NetworkFirst 兜底 cache。
- **多车辆 + 唯一默认**:可加多辆,默认车辆唯一,切换走事务。
---
## 🛠 技术栈
| 层 | 选型 | 理由 |
|---|---|---|
| 前端 | Vue 3 + Vite + TypeScript + Pinia + Vue Router + Tailwind CSS + Naive UI + ECharts | plan §3 锁定;轻量、Mobile First、TypeScript 严格、Naive UI 适配暗色 |
| 后端 | Go 1.22 + Gin + GORM | plan §3 锁定;单文件、编译快、goroutine 适合个人项目 |
| 数据库 | SQLite (glebarez/sqlite 纯 Go) | plan §3 锁定;零依赖、文件级持久化;WAL + 外键 + busy_timeout |
| 精度 | shopspring/decimal | plan §16;金额 / 油耗 / 成本用 decimal 避免 float 累计误差 |
| 部署 | Docker + Docker Compose + Nginx | plan §17;反代 + volume 持久化 SQLite |
---
## 🚀 快速开始(开发期)
### 前置
- Node.js ≥ 20(已测 24.15)
- pnpm ≥ 9(已测 9.7)或 npm
- Go ≥ 1.22(已测 1.27)
- git,可选:make
### 1. 克隆 + 装依赖
```bash
git clone https://gitea.kennethcheng.cc/KennethCheng/FuelLog.git
cd FuelLog
# 后端
cd fuellog-api
go mod tidy
cp .env.example .env
# 前端(另开一个 shell)
cd fuellog-web
pnpm install
cp .env.example .env.local
```
### 2. 启后端
```bash
cd fuellog-api
go run ./cmd/server # 监听 :8080
```
输出形如:
```
time=... msg="db ready" path=./data/fuellog.db
time=... msg="server starting" addr=:8080 mode=debug
```
### 3. 启前端
```bash
cd fuellog-web
pnpm dev # 监听 :5173,/api 自动代理到 :8080
```
打开 <http://127.0.0.1:5173> 即可。首次进会看到"还没有车辆,先去添加"的引导。
### 4. (可选)冒烟测试
```bash
cd fuellog-api
pwsh -ExecutionPolicy Bypass -File scripts/smoke.ps1
# 13/13 断言:建车 / 加油 / 油价 / 油耗 / 删记录 / 删车 全部命中 plan §6 数值
```
### 5. (可选)单测
```bash
cd fuellog-api
go test ./... # 22 decimal + 5 service
pnpm typecheck # 前端 vue-tsc
pnpm build # 前端 vite build
```
---
## 📁 仓库结构
```
FuelLog/
├── fuellog-api/ # Go 后端
│ ├── cmd/server/main.go # 入口
│ ├── internal/
│ │ ├── config/ # env 加载
│ │ ├── db/ # GORM open + migration
│ │ ├── handler/ # HTTP 入参 / 出参
│ │ ├── service/ # 业务逻辑 + 核心计算
│ │ ├── repository/ # GORM 数据访问
│ │ ├── model/ # GORM 实体
│ │ ├── dto/ # 请求/响应 DTO
│ │ ├── middleware/ # logger / recover / cors
│ │ └── router/ # Gin 路由
│ ├── pkg/
│ │ ├── apperror/ # 统一错误类型
│ │ └── decimal/ # 金额 / 油耗精度工具
│ ├── migrations/ # 迁移(目前 GORM AutoMigrate;后续手写)
│ ├── scripts/ # smoke.ps1 等
│ └── go.mod
│
├── fuellog-web/ # Vue 前端
│ ├── src/
│ │ ├── api/ # axios + 类型化 client
│ │ ├── components/ # 业务组件
│ │ ├── layouts/ # AppLayout(顶栏 + 移动端底部 Tab)
│ │ ├── pages/ # Dashboard / Refuels / RefuelNew / Vehicles / Statistics / NotFound
│ │ ├── router/ # Vue Router 配置
│ │ ├── stores/ # Pinia
│ │ ├── styles/ # tailwind.css + theme.css(CSS Variables)
│ │ ├── utils/ # decimal 工具
│ │ ├── App.vue
│ │ └── main.ts
│ ├── public/ # favicon / manifest / PWA icons
│ └── package.json
│
├── deploy/ # (待) docker-compose + nginx
├── docs/
│ └── api.md # 完整 API 文档
├── data/ # SQLite 文件(默认 .gitignore,挂载 volume)
├── AGENTS.md # Agent 工程操作手册(规则、约束、Phase 顺序)
├── README.md # 本文件
└── .gitignore
```
---
## 🔢 核心计算规则
| 指标 | 公式 | 范围 |
|---|---|---|
| 实际油价 | `total_amount / liters` | 服务端权威,前端只展示 |
| 行驶里程 | `current.odometer - previous.odometer` | 负数 / 零视为无效 |
| 百公里油耗 | `liters / mileage × 100` | **只在 `is_full=true` 序列内采样** |
| 每公里成本 | `total_amount / mileage` | 同上 |
所有除法走 `shopspring/decimal`(后端) / 同步实现(前端 `utils/decimal.ts`)。
---
## 🧪 测试覆盖
- `pkg/decimal/money_test.go` — 22 个子测试,覆盖 plan §6.1-6.4 全部案例
- `internal/service/refuel_record_test.go` — 5 个测试,覆盖"半箱油不进入标准油耗"硬规则
- `scripts/smoke.ps1` — 13 步端到端冒烟,跑通"建车 → 加 3 次油 → 统计 → 删记录 → 删车"
---
## 🐳 部署(待完成)
```bash
cd deploy
docker compose up -d
# 浏览器访问 http://your-host
```
详见 `deploy/` 目录(待补充)。
---
## 🗺 路线图
- [x] Phase 0~1:初始化 + 后端骨架 + 数据层 + API + 单测 + 冒烟
- [x] Phase 2~3:前端骨架 + 4 个业务页
- [x] Phase 4:PWA(manifest / SW / 添加到主屏)
- [ ] Phase 5:数据导入导出(JSON / CSV)
- [ ] Phase 6:多用户 / 鉴权(目前是个人单机)
- [ ] Phase 7:Docker 部署 + 反代
- [ ] Phase 8:充电 / 保养 / 停车等扩展
---
## 📐 设计原则
- **Mobile first**:360 / 390 / 430 三档断点先验证;布局用 `grid + md:`,CSS Variables 切暗色。
- **不写死颜色 / 间距 / 字号**:Tailwind theme + `:root` CSS Variables 双轨;`brand` / `ink` / `surface` 都是变量,组件层一律 `text-brand` / `bg-surface`。
- **Handler 不写业务**:Handler 只做参数解析、错误码映射;Service 持有 Repository 接口,便于单测替换。
- **金额 decimal**:任何除法都不直接用 float64。
- **半箱油不污染油耗**:plan §6.3 + §19 验收的硬规则,单测里有专门 case。
- **错误统一格式**:`{ "code": "INVALID_ARGUMENT", "message": "..." }`,前端 axios 拦截器解到 `Error.cause`。
- **PWA 添加到主屏**:`display: standalone` + maskable icon + SW 预缓存,真像 app。
---
## 📄 License
MIT
---
## 📎 相关
- [AGENTS.md](./AGENTS.md) — Agent 工程操作手册(规则、约束、Phase 顺序、Definition of Done)
- [docs/api.md](./docs/api.md) — 完整 API 文档
- [FuelLog_PLAN.md](./FuelLog_PLAN.md) — 原始产品 / 技术决策文档(仓库内)