旅行手账
产出是一个 单个 HTML 文件:双击就开,不用装东西,照片全部内嵌在里面,拷给别人也不会丢图。数据存在浏览器本地(localStorage + IndexedDB),随时可导出 JSON 备份。
七个标签页:封面 → 手账 → 足迹地图 → 旅行日志 → 旅游规划 → 行前准备 → 费用账本。
三个阶段的流转
draft(旅游规划里的草稿)→ 点「定下来,去准备出发」→ plan(左栏「即将启程」)→ 点「出发完成,生成手账」→ done(左栏「我的行程」)。没有草稿时「即将启程」那一栏不显示。
行前准备按目的地出清单:内置规则认高原(西藏/青海/稻城)、严寒(哈尔滨/漠河/雪乡)、出境(护照签证转换插头)、自驾、海岛、南方湿热、徒步,匹配不上就给一份通用清单。第一次打开这页会自动铺成 plan.gear: [{cat, name, done}],之后不再覆盖用户改动。注意事项是动态算的,不落库。想加地区就改 GEAR_RULES,一条 {re, items, notes}。
旅游规划是专门排期的地方:左栏第三块入口进去,能放 N 条在规划的路线,点哪条右边就出哪条的按天日程(时间 / 类型 / 事项 / 备注),顶上带费用合计,「▶ 过一遍」会把四天逐张翻出来,「✅ 生成手账」转成正式行程。日程存在 plan.schedule: [{day, items:[{time, kind, name, note, cost}]}],kind 用 飞机/移动/景点/打卡/吃饭/住宿/购物/其他 这几种,对不上图标就用圆点。没写 schedule 的行程会从 route 自动推一版粗日程。足迹地图的每个停靠点就是一张旅途照片(不是图标),点开有放大图;播放时按路况自动换小车 / 轮渡 / 飞机。
何时用
- 用户要"把我的旅行做成一个手账 / 相册 / 记录本"
- 用户手里有一批照片、一份花销表、几条路线,想要一个能看、能改、能发给人的成品
- 用户要给一群人 AA 记账并算清谁该给谁多少
不需要这类技能的场景:只要图片排版(用 PPT)、只要静态网页(直接写 HTML)。
流程
1. 起步
把 assets/journal-template.html 复制成目标文件。它自带两条示例行程,不依赖任何外部数据。
cp assets/journal-template.html ./我的旅行手账.html
打开看一眼:应该有「示例 · 海边」「示例 · 山里」两条行程、封面统计、地图上的两个点。如果打不开或报错,先解决再往下。
2. 填真实数据
改文件里 function seed() 那一段(搜索 初始数据:两条示例),按同一结构换成真实行程。字段见 references/data-schema.md。
最少要有:id、title、start、end、status、partners、route、expenses。
同时改文件顶部的 const PEOPLE,把示例的人换成真实的人:
const PEOPLE = {'张':'张伟','王':'王芳','李':'李强'};
键是简称(显示在窄处),值是全名(显示在结算和提示里)。
改完必须同步改 STORE_KEY,否则用户打开时可能读到同一浏览器里别人的旧数据:
const STORE_KEY = 'travel_journal_v1'; // 换成一个不会撞的名字
3. 打包照片
python scripts/pack-photos.py \
--template assets/journal-template.html \
--photos "D:/照片" \
--out ./我的旅行手账.html
照片目录可以是「每趟一个子文件夹」(文件夹名对上行程标题就自动归类),也可以是一堆散图(按文件名里的日期归到对应行程)。脚本会打印每趟收了几张、哪些没用上。
照片压两档:大图 1600px(点开看大图)、缩略 560px(照片墙用)。80 张照片大约让文件增加 8–10MB,可以接受。
如果用户是先把数据填好再打包,注意 --template 要指向已经填好数据的那份,不是原始模板。
4. 验证
用无头浏览器打开,确认三件事:
// 数据进来了
DB.trips.length // 趟数对
photos.length // 照片张数对
// 没有异常
Runtime.exceptionThrown // 空
再逐页点一遍六个标签,看每页都有内容、没有空白或 undefined。手账页重点看:标题、日期、同行人、页脚的路线和金额。
5. 交付
告诉用户完整路径,并说明两件事:
- 双击就能开,照片已经嵌在文件里,换电脑/发给别人都不会丢
- 想改内容直接在页面上点(标题、地点小注、照片、账目都能点开改),改完会存在浏览器里;要长期保存或换设备,用右上角「导出备份」存一份 JSON
6. 持续记:从对话往手账里写
手账做好之后,用户会一直在对话里说"住了两晚 480""帮我把这张照片放上去"。这些不用重做整个手账,走补丁这条路。
补丁是一段 JSON,写进手账的 DATA_PATCH,打开时自动合并:只加不删,同 id 的行程合字段,账目/停靠点/日志/计划按 id 去重追加。所以贴重、贴错都没事,用户手改过的不会被覆盖。
两种送进去的方式,按场景选:
| 场景 | 做法 | 用户操作 |
|---|---|---|
| 手边有手账文件,能重出一份 | patch-journal.py --html 手账.html --patch 补丁.json |
双击打开,自动合并 |
| 不想等重写文件 | 把补丁放进剪贴板,点顶栏「补丁」 | 点两下,内容已自动填好 |
写补丁前先拿数据快照,不要在旧快照上叠——用户可能已经改过。让他点右上角「导出备份」,或者用 rebuild.py 时用的那份 手账数据.json。
补丁字段速查(详细见 references/patch-schema.md):
{
"ver": "2026-11-03-1",
"trips": [{"id": "t7", "expenses": [{"id": "e1", "cat": "住宿", "item": "民宿两晚", "amount": 480, "date": "2026-11-07"}]}],
"photos": [{"tripId": "t7", "file": "D:/照片/IMG_1.jpg", "shotDate": "2026-11-08"}]
}
ver 不给就自动用时间戳。同一个 ver 只合并一次,重复打开不会加两遍。
补丁是累积的:patch-journal.py 把新补丁并进手账里已有的 DATA_PATCH,不是替换。所以可以一笔记一次、一天补一遍,后面的不会顶掉前面的。
只升级界面,不动数据
改了配色、版式、封面、点位尺寸之后,别用 rebuild.py 重建 —— 那会换掉 seed(),用户手改在浏览器里的内容就白改了。用 upgrade.py:以新模板为底,把旧手账的行程数据和照片包原样填回去,CSS/HTML/JS 全部换成新版。
python scripts/upgrade.py --template assets/journal-template.html --html 我的手账.html
这条路径的好处是:日常不需要用户导出。界面改动走 upgrade、数据改动走补丁,两条都不碰他的 localStorage。
只有两种情况才要他导出:
- 换电脑、清缓存、换浏览器 —— localStorage 会空,只剩
seed() - 要删掉某条记录 —— 补丁只加不删,删不了
从已有数据重建
模板是空白的,用户如果已经有一份带数据的手账(比如上一版),用 rebuild.py 把数据搬过来,照片包原样搬、不重新压:
python scripts/rebuild.py --template assets/journal-template.html \
--data 手账数据.json --pack-from 旧手账.html --out 新手账.html
--data 要 {"trips":[...]}。这份 JSON 可以从旧手账里取:用 headless 浏览器打开它,JSON.stringify({trips:DB.trips}) 存下来。
注意 STORE_KEY 换了(模板是 travel_journal_v1,旧手账可能是别的),所以新文件不会读到旧文件在浏览器里的编辑,互不干扰——但也意味着新文件里的编辑不会自动回流到 --data,下次重建前要重新导出一份。
数据结构
完整字段说明在 references/data-schema.md。几个最容易踩的点:
partners存的是PEOPLE的键(简称),不是全名split是"每人该承担多少",用于房费这类不平均分摊;不填就按人头平摊expenses[].payer写某人就表示这笔他垫的;留空表示未指定,结算时按规则补算route[].ok为false时,地图上那个点画成虚线/待确认样式logs是旅行日志页的条目,highlights是手账页"记得的几件事"(不填会自动从 logs 取)
定制
改 :root 里的变量换整体配色(--paper 纸色、--ink 墨色、--teal / --coral / --gold 三个主色)。每趟自己的颜色在 seed() 里 color 字段。
封面插画在 .cv-hero 里的一个 <img class="cv-bg"> 上(搜 cv-bg),base64 直接内嵌,所以换图不用管路径问题。想换成自己的照片:把 src 换成照片的 base64,或者临时改成相对路径先看效果——注意换成外链路径后文件就不再是"单文件自带图"了。封面容器写死 aspect-ratio: 2/1,所以换的图最好是宽幅横构图。窗口越窄,画面里能装下的「层」越少——2:1 大约刚好能装下上框、窗外景、下框三段。
封面大标题搜 cv-title,顶栏标题搜 <h1>,页面标题在 <title>。导出文件名搜 a.download。
字体变量 --hand 控制手写体(默认楷体),换成本机装的其他手写字体即可。
边界
- 地图页要联网:足迹地图用的是 Leaflet + 在线瓦片,没网时这一页是空的。但手账页里的小地图是内嵌 SVG 画的,完全离线,所以没网也有得看。
- 浏览器清缓存会丢编辑:照片已经嵌在 HTML 里不会丢,但页面上手动改的标题、账目存在 localStorage,清掉就回到
seed()的初始状态。所以每次改完提醒用户导出备份。 - 封面不写真名同行人:人手每次都不一样,写上就固定了。封面只留标题和出发地,「谁一起去的」放在每趟自己的
partners里。 - 补丁只加不删:用户删掉的记录,如果还留在
DATA_PATCH里,下次打开会回来。要删就得改seed()或让他自己在页面上删。 - 别用
file://之外的方式打开:某些浏览器对本地文件的 IndexedDB 有额外限制,用双击打开最稳。
文件
| 路径 | 作用 |
|---|---|
assets/journal-template.html |
空白模板:两条示例行程、零照片、无真实姓名 |
scripts/pack-photos.py |
照片目录 → 压图 → 内嵌进模板 → 成品 |
scripts/rebuild.py |
行程 JSON + 旧手账的照片包 → 重出一份手账 |
scripts/patch-journal.py |
把一段补丁写进手账,顺带把新照片压好塞进照片包 |
scripts/upgrade.py |
只换界面、数据原样保留,改版式用这个而不是 rebuild |
references/data-schema.md |
DB.trips 每个字段的含义与示例 |
references/customize.md |
改配色、字体、品牌名、加新标签页的位置索引 |
references/patch-schema.md |
补丁怎么写:常见说法 → 字段对照 |