先出方案(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 拆又容易做出「中间那笔编译不过」的历史,所以合成一笔、在消息里分段。
311 lines
16 KiB
HTML
311 lines
16 KiB
HTML
{{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}}
|