Files
tamakyi ab7ccd22ce 前端改造:设计令牌 + 卡片层级 + 图表可读性 + 类型色收口 + 窄屏导航
先出方案(frontend-plan-2026-10-09.md)再动手,四批按顺序落地。方法上先存基线、再用同一份
探针在改动前后逐值 diff —— 这批里有 6 处问题只有「量」才看得见,看截图全是正常的。

A 设计令牌(app.css 为主)
- 字阶收敛到 6 档 --fs-xs/sm/md/lg/xl/2xl = 11/13/15/21/26/34px。原来 app.css 有 19 种字号、
  模板里另有 54 处内联字号,光 12~15px 之间就挤了 4 档 —— 层级挤不出来,只能靠加粗硬顶。
- 两个「字外档」不是漏的:--fs-control: 16px(窄屏表单控件必须 ≥16px,否则 iOS 聚焦放大整页,
  属功能约束);--glyph-*(✕/箭头/空态图标按控件尺寸定,硬塞进字阶只会把按钮撑坏)。
- 字重收敛到 400/500/600(删掉 650 与 700)。
- 圆角 4 档 --r-*、阴影 3 档 --sh-1/2/3(暗色整套覆盖)、间距 --sp-1..5、
  卡片内边距单列 --card-pad(与折叠卡片标题行的负 margin 绑在一起,改一个即改两处)。
- 门禁 TestNoBareFontSizePx:静态扫 app.css 与全部模板,出现 font-size: <数字>px 即红。

B 卡片分主次
- .card--hero / .card(默认)/ .card--quiet 三档。quiet 走「页面底色 + 细边 + 无阴影」——
  第一版拿 --bg-2 当底色,实测它与页面底 #f6f7f8 只差 4/255,看不出边界、像破了个洞。
- 首屏 KPI 从「4×2 等重白盒」改成 .kpi-lead(主数 + 一行上下文)+ 5 个常规指标,8 个数字一个没少。

C 图表说人话
- 月度分布:柱底加基线;峰值柱不透明拉满、柱顶标次数(.pv),副标题里不再重复写「最高 N 次」。
- 一天中的分布:把「0/6/12/18 四个数字」换成 6 小时一段的标尺(深夜/上午/下午/晚上),
  用同一套 24 列网格,段边界与柱子严格对齐。
- 首页月度图同样加峰值锚点(给 HomeData 补 MaxMonthly,与统计页同口径 maxOf)。
- 空态没动:查下来已经统一(8 页共用 emptystate、局部走 .empty)。

D1 类型色语义化(结论与计划不同,按实测改了)
- 先算对比度:白底上 yellow 1.87 / mint 2.12 / orange 2.20 / green 2.22 / cyan 2.54 / teal 2.57,
  6 个色连图形对象 3:1 都到不了;暗底只有 indigo 2.90 不够。
- 所以**没有**把类型色用到 chips / 表格类型列 / KPI 强调 —— 那几处要的是文字色,只会更难读。
  这也解释了全站为什么一直只用 8px 小圆点做身份标记。浅色那 6 个色一个没动:
  它们是手机端与地图上的同一套身份色,改暗会破坏对应关系。
- 实际做的:色值收口成 --cat-* / --cat-*-soft(26 个变量);模板 colorof → csscolor / csssoft(19 处);
  暗色只提亮 indigo(2.90 → 3.55:1);colorOf 成了死代码,删掉。
- ⚠️ 返回值必须是 template.CSS:html/template 的 CSS 过滤器不认识 var(),普通 string 会被静默
  换成 ZgotmplZ —— 页面上所有圆点当场变透明,而且不报错、模板测试照过。挂了它等于关掉那道过滤,
  所以数据侧色值先过严格校验(只有 # + 6 位 hex 放行),否则 #"><img… 能逃出 style 属性。
  守卫 TestCSSColorSurvivesTemplateSanitizer 把两个方向都钉住(做过变异验证)。

D2 窄屏导航
- 改前 ≤620px 靠横向滚动兜底:390 溢 63px、320 溢 133px,「统计」被切成半个字紧贴后面的按钮,
  渐隐提示实际看不出来。现在链接本体抽成 {{define "navitems"}} 渲染两遍:桌面 .nav-links 平铺、
  窄屏 .nav-menu(原生 <details>,不靠 JS 也能开合);JS 只补点外面 / Esc / 点链接后收起。
- ⚠️ 顶栏 --z-nav 抬到 1200:菜单是 .nav 的子元素、自己抬不动层叠上下文,不抬就会被地图页的
  --z-map-overlay(1100) 盖住。已在 /map 上用 elementFromPoint 实测。
- 守卫 TestNarrowNavCollapsesToMenu 钉「CSS 两半 + 模板两个类 + 渲染两遍」的成对关系。

途中发现并一并修掉的(都只有量 DOM 才看得见)
- Leaflet 的 .leaflet-container 字号是 0.75rem、跟着根字号漂:根 14 → 13px 后地图署名/比例尺
  掉到 9.8px(低于本站 11px 下限)、弹窗正文也跟着缩。已按自己的字阶钉死。
- h1..h4 原来只设了字重与 margin、没设字号,靠浏览器默认的 2em/1.5em/1.17em —— 同样跟着根字号漂:
  /stats 那两个 h2 从 21px 变成 19.5px,比同页卡片标题大一圈。已显式给出字号。
- <b>/<strong> 浏览器默认 700,比全站最高层级(标题 600)还重,已钉成 600。

回归
- ./scripts/verify.sh:PASS=348 FAIL=0 SKIP=0
- internal/fog 加 -race:PASS=22 FAIL=0 SKIP=0
- JS 单测 34/34
- 移动端 320 / 390 溢出全 0(改前 /admin 在 320 还溢 34px);桌面 1440 与改前一致(−15 是滚动条宽)
- 全站 7 个页面都没有低于 11px 的文字

说明:A/B 与 C/D 本想拆两笔提交,但 app.css / layout.html / view.go 里两批改动交织,按文件拆不开、
按 hunk 拆又容易做出「中间那笔编译不过」的历史,所以合成一笔、在消息里分段。
2026-10-09 17:01:08 +08:00

311 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "content"}}
<div class="wrap">
<div class="page-head">
<div>
<h1>备份差异预览</h1>
<div class="sub">
这份备份与当前展示的「{{.DatasetName}}」逐条比对的结果。<strong>还没有导入</strong>,站内数据一个字节都没动。
</div>
</div>
<div class="row"><a class="btn" href="/admin">← 返回后台</a></div>
</div>
{{if .Identical}}
<div class="card">
<div class="alert ok">这份备份与当前数据集<strong>完全一致</strong>:没有新增、没有被删、也没有冲突。</div>
</div>
{{end}}
{{if .Truncated}}
<div class="card"><div class="alert err">差异较多,下面每类只列出前 200 条,确切数量看概览表。</div></div>
{{end}}
<div class="card">
<h2>概览</h2>
<div class="tw">
<table class="tb tb-stack">
<thead>
<tr>
<th>实体</th><th class="num">备份</th><th class="num">站内</th>
<th class="num">备份新增</th><th class="num">仅站内有</th><th class="num">冲突</th><th class="num">一致</th>
</tr>
</thead>
<tbody>
<tr>
<td>到访</td>
<td class="num" data-label="备份">{{num .BackupVisits}}</td>
<td class="num" data-label="站内">{{num .StoredVisits}}</td>
<td class="num" data-label="备份新增">{{num .VisitCounts.Added}}</td>
<td class="num" data-label="仅站内有">{{num .VisitCounts.Removed}}</td>
<td class="num" data-label="冲突">{{num .VisitCounts.Changed}}</td>
<td class="num" data-label="一致">{{num .VisitCounts.Same}}</td>
</tr>
<tr>
<td>地点</td>
<td class="num" data-label="备份">{{num .BackupPlaces}}</td>
<td class="num" data-label="站内">{{num .StoredPlaces}}</td>
<td class="num" data-label="备份新增">{{num .PlaceCounts.Added}}</td>
<td class="num" data-label="仅站内有">{{num .PlaceCounts.Removed}}</td>
<td class="num" data-label="冲突">{{num .PlaceCounts.Changed}}</td>
<td class="num" data-label="一致">{{num .PlaceCounts.Same}}</td>
</tr>
<tr>
<td>活动类型</td>
<td class="num" data-label="备份">{{num .BackupActs}}</td>
<td class="num" data-label="站内">{{num .StoredActs}}</td>
<td class="num" data-label="备份新增">{{num .ActCounts.Added}}</td>
<td class="num" data-label="仅站内有">{{num .ActCounts.Removed}}</td>
<td class="num" data-label="冲突">{{num .ActCounts.Changed}}</td>
<td class="num" data-label="一致">{{num .ActCounts.Same}}</td>
</tr>
</tbody>
</table>
</div>
<div class="card-sub" style="margin-top:10px">
对齐按每条记录的 <strong>UUID</strong> 做:rond 每次导出都会重排记录编号(同样的 822 条到访,两次导出的编号 100% 不同,UUID 却逐条一模一样),
所以只有 UUID 认得出来是同一条;地点没有 UUID,退到「名称 + 坐标」。
站内补录的记录不会出现在真机备份里,一律归到「仅站内有」。
</div>
</div>
<div class="card">
<h2>到访</h2>
<div class="card-sub"><strong>备份里多出来的({{num .VisitCounts.Added}})</strong> —— 目前站内没有这些记录</div>
{{if .VisitAdded}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>到达</th><th>离开</th><th>地点</th><th>活动</th><th>备注</th></tr></thead>
<tbody>
{{range .VisitAdded}}
<tr>
<td class="mono">{{dtt .Arrival}}</td>
<td class="mono" data-label="离开">{{dtt .Departure}}</td>
<td data-label="地点">{{if .Place}}{{.Place}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="活动">{{if .Activity}}{{.Activity}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="备注">{{if .Remark}}{{.Remark}}{{else}}<span class="muted">—</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px">
<strong>只在站内有({{num .VisitCounts.Removed}})</strong> —— 备份里找不到;可能是你在这里补录的,也可能是实机上删掉的。
<strong>默认一条都不删</strong>;确实想清掉的,勾上左列的「合并时删除」。
</div>
{{if .VisitGone}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>到达</th><th>离开</th><th>地点</th><th>活动</th><th>备注</th><th>来源</th><th>合并时删除</th></tr></thead>
<tbody>
{{range .VisitGone}}
<tr>
<td class="mono">{{dtt .Arrival}}</td>
<td class="mono" data-label="离开">{{dtt .Departure}}</td>
<td data-label="地点">{{if .Place}}{{.Place}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="活动">{{if .Activity}}{{.Activity}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="备注">{{if .Remark}}{{.Remark}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="来源">{{if .UserAdded}}<span>站内补录</span>{{else}}<span class="muted">备份里已无</span>{{end}}</td>
<td data-label="合并时删除">
<input type="checkbox" form="merge-form" name="dv_{{.SrcPK}}" value="1" title="勾上=合并时删掉这条到访">
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px"><strong>两边都有但不一样({{num .VisitCounts.Changed}})</strong> —— 左边是站内当前的值,右边是备份里的值</div>
{{if .VisitClash}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>到达</th><th>地点</th><th>活动</th><th>差异</th><th>合并时</th></tr></thead>
<tbody>
{{range .VisitClash}}
<tr>
<td class="mono">{{dtt .Arrival}}</td>
<td data-label="地点">{{if .Place}}{{.Place}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="活动">{{if .Activity}}{{.Activity}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="差异">
{{range .Fields}}
<div style="font-size:var(--fs-sm);line-height:1.7">
<span class="muted">{{.Label}}</span>
<span style="text-decoration:line-through;opacity:.65">{{if .From}}{{.From}}{{else}}(空){{end}}</span>
<span style="opacity:.5">→</span>
<strong>{{if .To}}{{.To}}{{else}}(空){{end}}</strong>
</div>
{{end}}
</td>
<td data-label="合并时" style="white-space:nowrap">
<label style="margin-right:8px"><input type="radio" form="merge-form" name="vc_{{.Key}}" value="backup" checked> 用实机</label>
<label style="margin-right:8px"><input type="radio" form="merge-form" name="vc_{{.Key}}" value="db"> 用站内</label>
<label><input type="radio" form="merge-form" name="vc_{{.Key}}" value="skip"> 跳过</label>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted">没有</div>{{end}}
</div>
<div class="card">
<h2>地点</h2>
<div class="card-sub"><strong>备份里多出来的({{num .PlaceCounts.Added}})</strong></div>
{{if .PlaceAdded}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>名称</th><th>省 / 市</th><th>分类</th><th class="num">坐标</th></tr></thead>
<tbody>
{{range .PlaceAdded}}
<tr>
<td>{{if .Name}}{{.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="省 / 市">{{.Province}} {{.City}}</td>
<td data-label="分类">{{if .POICategory}}{{.POICategory}}{{else}}<span class="muted">—</span>{{end}}</td>
<td class="num mono" data-label="坐标">{{printf "%.5f" .Lat}}, {{printf "%.5f" .Lon}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px">
<strong>只在站内有({{num .PlaceCounts.Removed}})</strong>
—— 名下还有到访的地点删不掉(会连带删掉那些到访),会直接报错让你先处理。
</div>
{{if .PlaceGone}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>名称</th><th>省 / 市</th><th class="num">到访</th><th class="num">坐标</th><th>合并时删除</th></tr></thead>
<tbody>
{{range .PlaceGone}}
<tr>
<td>{{if .Name}}{{.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="省 / 市">{{.Province}} {{.City}}</td>
<td class="num" data-label="到访">{{num .VisitCount}}</td>
<td class="num mono" data-label="坐标">{{printf "%.5f" .Lat}}, {{printf "%.5f" .Lon}}</td>
<td data-label="合并时删除">
{{if gt .VisitCount 0}}
<span class="muted" style="font-size:var(--fs-sm)">还有到访,不能删</span>
{{else}}
<input type="checkbox" form="merge-form" name="dp_{{.SrcPK}}" value="1">
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px"><strong>两边都有但地址不一样({{num .PlaceCounts.Changed}})</strong></div>
{{if .PlaceClash}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>名称</th><th>差异</th><th>合并时</th></tr></thead>
<tbody>
{{range .PlaceClash}}
<tr>
<td>{{if .DB.Name}}{{.DB.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="差异">
{{range .Fields}}
<div style="font-size:var(--fs-sm);line-height:1.7">
<span class="muted">{{.Label}}</span>
<span style="text-decoration:line-through;opacity:.65">{{if .From}}{{.From}}{{else}}(空){{end}}</span>
<span style="opacity:.5">→</span>
<strong>{{if .To}}{{.To}}{{else}}(空){{end}}</strong>
</div>
{{end}}
</td>
<td data-label="合并时" style="white-space:nowrap">
<label style="margin-right:8px"><input type="radio" form="merge-form" name="pc_{{.DB.Key}}" value="backup" checked> 用实机</label>
<label style="margin-right:8px"><input type="radio" form="merge-form" name="pc_{{.DB.Key}}" value="db"> 用站内</label>
<label><input type="radio" form="merge-form" name="pc_{{.DB.Key}}" value="skip"> 跳过</label>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted">没有</div>{{end}}
</div>
<div class="card">
<h2>活动类型</h2>
<div class="card-sub">
新增 {{num .ActCounts.Added}} · 仅站内有 {{num .ActCounts.Removed}} · 冲突 {{num .ActCounts.Changed}} · 一致 {{num .ActCounts.Same}}
</div>
<div class="row" style="gap:24px;align-items:flex-start;flex-wrap:wrap">
<div style="flex:1 1 220px">
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:4px">备份新增</div>
{{range .ActAdded}}<div>{{.Name}}</div>{{else}}<div class="muted">没有</div>{{end}}
</div>
<div style="flex:1 1 220px">
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:4px">仅站内有(勾上=合并时删除)</div>
{{range .ActGone}}
<label style="display:block"><input type="checkbox" form="merge-form" name="da_{{.SrcPK}}" value="1"> {{.Name}}</label>
{{else}}<div class="muted">没有</div>{{end}}
</div>
<div style="flex:1 1 260px">
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:4px">名称不同</div>
{{range .ActClash}}
<div style="margin-bottom:6px">
{{range .Fields}}<div><span style="text-decoration:line-through;opacity:.65">{{.From}}</span> → <strong>{{.To}}</strong></div>{{end}}
<div style="margin-top:4px">
<label style="margin-right:8px"><input type="radio" form="merge-form" name="ac_{{actKey .DB}}" value="backup" checked> 用实机</label>
<label style="margin-right:8px"><input type="radio" form="merge-form" name="ac_{{actKey .DB}}" value="db"> 用站内</label>
<label><input type="radio" form="merge-form" name="ac_{{actKey .DB}}" value="skip"> 跳过</label>
</div>
</div>
{{else}}<div class="muted">没有</div>{{end}}
</div>
</div>
</div>
<div class="card">
<h2>其它实体(只列数量)</h2>
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>实体</th><th class="num">备份</th><th class="num">站内</th></tr></thead>
<tbody>
<tr><td>位移</td><td class="num" data-label="备份">{{num .BackupMovements}}</td><td class="num" data-label="站内">{{num .StoredMovements}}</td></tr>
<tr><td>小时天气</td><td class="num" data-label="备份">{{num .BackupWeather}}</td><td class="num" data-label="站内">{{num .StoredWeather}}</td></tr>
<tr><td>原始定位点</td><td class="num" data-label="备份">{{num .BackupRaw}}</td><td class="num" data-label="站内"><span class="muted">不单独落库</span></td></tr>
</tbody>
</table>
</div>
<div class="card-sub" style="margin-top:10px">
位移跟着它的起止到访走、天气跟着到访走,所以这两项按到访对齐结果一并处理,不单独做逐条比对。
</div>
</div>
<div class="card">
<h2>执行合并</h2>
<div class="card-sub">
把选中的改动写进当前数据集「{{.DatasetName}}」。整个过程跑在一个事务里,任何一步失败都会<strong>整体回滚</strong>,数据不会半改。
<br>「仅站内有」的记录<strong>默认一条都不删</strong>;确实要清掉的,在上面各表里逐条勾「合并时删除」。
名下有到访的地点会被拒绝删除(否则会连带删掉那些到访),先处理到访再来。
</div>
<form id="merge-form" method="post" action="/admin/preview/{{.Token}}/merge"></form>
<label style="display:flex;align-items:flex-start;gap:8px;margin:4px 0 12px">
<input type="checkbox" form="merge-form" name="add_new" value="1" checked style="margin-top:3px">
<span>把备份里多出来的
<strong>{{num .VisitCounts.Added}}</strong> 条到访、
<strong>{{num .PlaceCounts.Added}}</strong> 个地点、
<strong>{{num .ActCounts.Added}}</strong> 个活动并进来
<span class="muted">(新到访会连它的地点、活动、标签、位移与天气一起带过来)</span>
</span>
</label>
<button class="btn primary" form="merge-form" type="submit"
onclick="return confirm('确认把选中的改动写进当前数据集?出错会自动整体回滚。')">执行合并</button>
<div class="card-sub" style="margin:10px 0 0">
合并后再传一次同一份备份,差异应当变成「完全一致」——那说明对齐键(UUID 与坐标)都写进去了。
</div>
</div>
</div>
{{end}}