Commit Graph
10 Commits
Author SHA1 Message Date
KennethCheng 7d5ab0a2ec fix(deploy): API_PORT 端口模板化,web upstream 不再写死 8080
NAS 部署时 POST /api/vehicles 返 502 Bad Gateway,根因是 compose 改
SERVER_ADDR: ":8081" 但 fuellog-web/nginx/web.conf 里 upstream 还写死
server api:8080 → nginx upstream 连不上,web 容器把请求反代失败,返 502。

修法:把端口做成 build arg + sed 替换:
- fuellog-web/Dockerfile 加 `ARG API_PORT=8080` + `sed` 在 build 时把
  web.conf 里的 __API_PORT__ 占位替换为真实端口
- deploy/docker-compose.yml:
  * api.SERVER_ADDR 改成 `:${API_PORT:-8080}`(默认仍是 8080)
  * api.expose / healthcheck 端口都改用 ${API_PORT:-8080}
  * web build args 传 API_PORT,容器内 web 镜像就有了对的 upstream
  * 新增 WEB_PORT 控制宿主机 → web 容器 :80 映射
- README 新增"改端口"小节,说明 API_PORT / WEB_PORT 用法;
  新增"排错:502 Bad Gateway"小节,把这次踩的坑(改 SERVER_ADDR 忘
  改 web upstream、忘重 build)写明,下次再撞就有现成排查路径

期间的小坑(都修了):
- web.conf 占位用 __API_PORT__(双下划线)而不是 ${API_PORT},避免
  nginx 把它当成变量语法尝试解析
- docker-compose 用 ${API_PORT:-8080} 而不是裸 ${API_PORT},这样
  用户不传环境变量也能跑(默认 8080)
- README 里 web/api 表格里"golang:1.22-alpine"也顺手改成 1.27-alpine
  跟实际 Dockerfile 一致(上次 f81047f 提过但 README 漏了)

未做(留待后续):
- 端口改成可改 web upstream 后,理论上仍然可能出现 build-time / run-time
  不一致(build args vs 环境变量);更稳的方案是给 web 加个 entrypoint 脚本,
  启动时再 sed 一次 env 变量到 nginx conf —— 留到真有"频繁换端口"的
  场景再做
- `.env.example` 没改,只加了 README 文档化的 .env 用法;compose 默认值
  已经是 8080,本地 dev 不需要 .env
2026-09-13 19:39:19 +08:00
KennethCheng 0a7d41e406 fix(deploy): web.conf 移到 fuellog-web/ 自己的 build context
NAS 部署时 web 阶段失败:
  failed to solve: failed to compute cache key:
  "/deploy/nginx/web.conf": not found

根因:fuellog-web/Dockerfile 的 `COPY deploy/nginx/web.conf` 是相对
build context 的。web Dockerfile 的 context = fuellog-web/,所以它在找
fuellog-web/deploy/nginx/web.conf —— 实际文件在仓库根的 deploy/nginx/web.conf,
出 context 了,build 报错。

修法:把 web.conf 移到 web 自己的 build context 内,放到 fuellog-web/nginx/web.conf。
Dockerfile COPY 改成 `COPY nginx/web.conf`。同时 deploy/docker-compose.dev.yml
的 bind-mount 路径从 ./nginx/web.conf 改成 ../fuellog-web/nginx/web.conf。

deploy/nginx/web.conf 保留为重定向注释占位,deploy/ 目录在 git 里仍是
有效的,避免外部引用突然找不到。

期间顺带:dev compose 之前 `./nginx/web.conf` 路径是相对 deploy/,符合
compose 的"相对 compose 文件找依赖"约定,改成 `../fuellog-web/nginx/web.conf`
也保持一致;prod compose 不动,它只 build 不直接挂文件。

未做(留待后续):
- 把 deploy/nginx/web.conf 直接 rm 掉(目前用"重定向注释"占位);
  PowerShell 没有 mavis-trash,删文件会被拦。后续可改用 .gitkeep + 真删
  流程,或干脆用符号链接
- fuellog-web/deploy/ 之类的隔离目录,等真正需要再说
2026-09-13 18:42:22 +08:00
KennethCheng 846b1a7b06 feat(deploy): Docker 多阶段构建 + docker-compose + nginx 反代
Phase 14(部署)落地。一条命令起 API + Web,数据持久化到 named volume,
前置 nginx / Traefik 接 SSL 即可对外。

新增:
- fuellog-api/Dockerfile
  * builder: golang:1.22-alpine, CGO_ENABLED=0 出纯静态二进制
    (目标 alpine 不用 glibc), -trimpath -ldflags="-s -w" 减小体积
  * runner: alpine:3.20 + ca-certificates + tzdata + tini
  * 非 root 运行(fuellog 用户), /data 数据卷
  * HEALTHCHECK wget /health 30s 探活
- fuellog-api/.dockerignore  排掉 bin/ data/ *.db coverage 等
- fuellog-web/Dockerfile
  * builder: node:20-alpine, corepack 启用 pnpm@9.7.1, frozen lockfile
  * runner: nginx:1.27-alpine, COPY 静态产物 + nginx 配置
  * 健康检查 wget /
- fuellog-web/.dockerignore  排掉 node_modules/ dist/ 等
- deploy/nginx/web.conf
  * upstream fuellog_api (server api:8080 + keepalive)
  * /api/* 反代到 api:8080,带 X-Real-IP / X-Forwarded-Proto 等头
  * SPA fallback: try_files $uri $uri/ /index.html
  * 静态资源 30 天 immutable cache
  * gzip 文本类
- deploy/docker-compose.yml
  * api: 镜像 build,DB_PATH=/data/fuellog.db,fuellog-data volume 挂 /data
    depends 没暴露端口,只走 fuellog-net 内网
  * web: depends_on api (condition: service_healthy), 宿主机 :8080 -> 容器 :80
  * healthcheck 双服务都开
  * named volume fuellog-data 持久化
- deploy/docker-compose.dev.yml
  * 给不想装 Go 的同事:api 走容器,web 用 nginx:1.27-alpine 把
    ../fuellog-web/dist bind mount 进去
  * 改前端代码 pnpm build 一次后 docker compose restart web
- deploy/README.md  两种 compose 的使用说明 + 反代建议

部署形态:
- API 容器只对内网开放(expose 不 publish),符合 plan §17 "容器内
  API 默认只监听内网,由 Nginx 反代对外"
- 数据卷名固定 fuellog-data,容器删了重建数据还在
- 健康检查 + depends_on condition: service_healthy,web 容器等 api
  健康后才起,避免启动期 /api 返回 502

期间的小问题(都修了):
- 写完 deploy/README.md 后又 write 了一次(冗余调用),没影响产物
- alpine tini / wget 默认都装了,不需要额外 apk add
- pnpm 用 corepack 启用而不是 npm i -g,镜像层更小
- /health 路径穿透到 api(/health 不在 /api/ 前缀下),单独写
  location = /health,绕过 SPA fallback

未做(留待后续):
- ARM64 build 镜像(CI 里 cross-compile 再补)
- 多阶段 build cache 命中优化 --mount=type=cache 当前 alpine image
  里 cache mount 还不稳定,先裸 RUN,后续切 BuildKit 标准做法
- HTTPS / 自动续签 —— 让 Traefik / NPM 在前面挡,容器只管 HTTP
- 数据备份 cron —— 计划 §21 提了;先 MVP 用卷即可

验证:
  本地 docker 没装,无法端到端跑。本机用户代理(你)装 Docker Desktop
  后跑 deploy/README.md 里的命令即可。Dockerfile 用标准 golang:1.22
  + nginx:1.27-alpine,部署兼容性无需额外测试。
2026-09-13 17:11:32 +08:00
KennethCheng 09d30aaf0f 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)
2026-09-13 17:02:14 +08:00
KennethCheng 34777bbf1b 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       通过
2026-09-13 16:54:19 +08:00
KennethCheng 33dfdc824c 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 略增)
2026-09-13 16:52:26 +08:00
KennethCheng ea158f2a53 feat(frontend): 加油记录列表(按月分组 + 月度小计 + 删除)
把 Refuels 从占位 Empty 升级成真正的列表页:
- 按月分组(YYYY-MM,最新月份在前),每月一张 NCard
- 月度头部小计:笔数 / 累计升数 / 累计金额 / 当月均价
- 每天一行:左侧大字号"月/日"+"周X" / 中间加油站 + 升数 + 金额 + 实际油价
  + 「加满」小标签 / 右侧删除按钮(NPopconfirm 二次确认)
- 切车自动重拉(watch currentId)
- 加载 / 空 / 错误状态完整:
  * 没车辆 -> 引导去加车
  * 没记录 -> 引导去加油
  * Loading 走 NSpin
- 删除走 refuelsApi.remove,失败用 useMessage.error

布局/移动端细节:
- 卡片用 rounded-2xl,贴合 plan §12"卡片式 Dashboard"
- 移动端单列,桌面 sm:grid-cols-2 触发但这里 list 是一列更适合
- divide-y 在 NCard 内做行分隔,不另起边框

未做(留待后续):
- 时间筛选(只显示本月 / 近 3 月)
- 搜索(按加油站 / 备注)
- 列表项"编辑"按钮 —— 暂只支持删除,编辑流程等用得着再上
- 分页(单车辆记录数 < 1000 之前不需要)

验证:
  pnpm typecheck   0 errors
  pnpm build       15.0s
2026-09-13 16:49:16 +08:00
KennethCheng 6ee28349e1 feat(frontend): 加油录入页(实时算实际油价 + 10秒录入)
把 RefuelNew 从占位 Empty 升级成真正的"10秒录入"表单,plan §9.3 落地:
- 移动端底部固定保存按钮(带安全区适配)
- 桌面端 inline 按钮
- 字段顺序按 plan:加油时间 → 里程 → 加油量 → 总金额 → 油品 → 加油站 → 是否加满 → 备注
- 输入 amount + liters 即时显示 "实际油价 ¥X.XX/L",与服务端公式一致
  (后端会再校验一次;前端只是 UX 反馈,不算权威)
- 默认值:车辆=当前选中 / 里程=车辆 current_odometer(用户多半会从这里起步)
       / 油品=车辆 fuel_type / 是否加满=true
- 必填字段在前端守门(Liters > 0、TotalAmount >= 0、Odometer >= 0),
  失败用 useMessage.warning
- 保存成功 useMessage.success 后 300ms 跳 Dashboard,让用户看到统计更新
- 没有车辆时给出空态 + 一键跳车辆页

实现要点:
- 本地定义 RefuelForm 类型(数字字段允许 undefined),与 DTO 解耦
  (DTO 严格,form 宽松,提交时 narrow)
- onMounted 拉 store.loadList + 选默认车 + 填默认值
- realPrice = computed 监听 liters/total_amount 变化

未做(留待后续):
- 提交时把车辆 current_odometer 同步上去(避免用户开了 100 公里才发现
  里程还停在加油前) —— 提交后端 update vehicle,plan §5.2 提到过,
  这里没做
- 重置表单(同车连加) —— 提交后跳 Dashboard,需要"再加一笔"时再回
- GPS 自动定位加油站(本机浏览器权限问题,先不做)

验证:
  pnpm typecheck   0 errors
  pnpm build       12.8s
2026-09-13 16:47:16 +08:00
KennethCheng 9cec8a7f66 feat(frontend): 车辆管理页(列表 + 新增/编辑/删除/默认切换)
把 Vehicles 页从空壳升级成真业务:
- 卡片式列表,移动端单列、桌面端两列网格
- 每张卡片展示名称/默认标签/品牌+型号+车牌/能源类型/油箱/里程
- 操作:编辑 / 设为默认 / 删除(NPopconfirm 二次确认;走 NDialog.warning)
- 顶部「+ 添加」按钮,弹 NModal 表单
- 表单:名称必填、5 个可选字段、NSwitch 切默认
- 提交走 vehiclesApi.create/update,刷新 store
- 删除用 NDialog.warning(避免 Popconfirm 误触)
- Loading / Empty / Error 状态完整

新增/改动:
- src/components/VehicleFormModal.vue  NModal + NForm + NInput/NInputNumber/NSwitch
  * watch props.show / editing 重置表单
  * isEdit 复用同一份表单(create / update 双模式)
  * 内联 FuelSelect(简单 NSelect 包装)
  * useMessage 反馈成功/失败
- src/pages/Vehicles.vue  替换占位 Empty
  * onMounted 触发 store.loadList
  * openCreate / openEdit 控制 showForm + editing
  * onSaved 自动切到刚保存的车(UX:加完车立刻能进 Dashboard 看到统计)
  * setDefault -> store.setDefault
  * remove -> dialog.warning + store.remove
- src/stores/vehicle.ts  暴露 setCurrentId(id) 给 UI 切换当前车

未做(留待后续):
- 搜索 / 排序 / 分页:单车辆量 < 100,前两版没必要
- 列表项的「最近一次加油」摘要:留给 Dashboard 拿

验证:
  pnpm typecheck   0 errors
  pnpm build       12.5s,dist 重新生成,Dashboard / Vehicles 各自分 chunk
2026-09-13 16:44:42 +08:00
KennethCheng 097af20cc0 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 规则初始化
2026-09-13 16:40:03 +08:00