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 |
|