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

208 lines
12 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">上传 rond 备份、切换展示的数据、调整站点设置</div>
</div>
<div class="row">
<a class="btn" href="/admin/edit">数据编辑</a>
<a class="btn" href="/admin/topics">专题管理</a>
<a class="btn" href="/admin/photos">照片导入</a>
<a class="btn" href="/admin/settings">站点设置</a>
<a class="btn" href="/admin/twofa">两步验证</a>
<a class="btn" href="/admin/notes">地点备注</a>
<a class="btn" href="/admin/fillup">地点补全</a>
<a class="btn" href="/admin/password">账号设置</a>
<a class="btn" href="/" target="_blank">查看站点</a>
</div>
</div>
{{if .Flash}}<div class="alert ok">{{.Flash}}</div>{{end}}
{{if .Err}}<div class="alert err">{{.Err}}</div>{{end}}
{{if .DefaultPW}}<div class="alert err">⚠️ 您仍在使用初始口令 <span class="mono">rond-admin</span>,站点对公网开放时任何人都可以用这组口令登录后台。请尽快<a href="/admin/password">修改密码</a>,并在<a href="/admin/twofa">两步验证</a>中绑定动态口令。</div>{{end}}
<div class="card">
<h2>上传备份</h2>
<div class="card-sub">
在 rond App 中导出备份,得到 <span class="mono">.rondbackup</span> 文件后上传。
单次最大 {{size .MaxSize}}。
<b>上传是以备份为准重建数据</b>:站点里删掉的地点、改过的记录,只要下次传一份
<b>内容不同</b>的备份,就会被备份里的原样覆盖回来(重复传同一份文件不会,会提示「完全一致」)。
在站点上新加的地点属于当时那个数据集,传新备份后要看它们,可在下面的数据集列表里切回。
<br><b>拿不准就先用「分析差异」</b>:它会逐条比对这份备份与站内当前数据,告诉你哪些是新增、哪些只在站内有、哪些两边不一样,看完再决定要不要导入。
</div>
<form method="post" action="/admin/upload" enctype="multipart/form-data">
<label class="f" style="max-width:520px">
<span>选择文件</span>
<input type="file" name="backup" accept=".rondbackup,.zip" required
style="padding:8px;border:1px dashed var(--line);border-radius:8px;background:var(--bg-2)">
</label>
<div class="row">
<button class="btn primary" type="submit" formaction="/admin/preview">分析差异</button>
<button class="btn" type="submit" formaction="/admin/upload">直接导入</button>
</div>
</form>
</div>
<div class="card">
<h2>世界迷雾</h2>
<div class="card-sub">
导入 Fog of World 导出的 <span class="mono">.fwss</span> 快照,在足迹地图上叠加已探索区域。
迷雾网格为 WGS-84,站点会自动对高德等 GCJ-02 底图做纠偏。
迷雾是站点级数据,不属于任何数据集,删除数据集不影响迷雾。
{{if gt .FogBlocks 0}}
当前已入库 <span class="mono">{{num .FogBlocks}}</span> 块 / <span class="mono">{{num .FogCells}}</span> 格,
估算已探索 <span class="mono">{{printf "%.1f" .FogArea}}</span> km²{{if .FogUpdatedAt}},最近更新 {{dtt .FogUpdatedAt}}{{end}}。
{{else}}
<b>尚未导入迷雾</b>——把 Fog of World 的 <span class="mono">.fwss</span>(在 App 内导出,通常在 zip 里)上传即可点亮地图。
{{end}}
</div>
<form method="post" action="/admin/fog/preview" enctype="multipart/form-data">
<label class="f" style="max-width:520px">
<span>选择快照</span>
<input type="file" name="fwss" accept=".fwss,.zip" required
style="padding:8px;border:1px dashed var(--line);border-radius:8px;background:var(--bg-2)">
</label>
<button class="btn primary" type="submit">分析差异</button>
</form>
<div class="muted" style="font-size:var(--fs-sm);margin-top:8px">
上传后<strong>先不写入</strong>:会先给出这份快照能补多少新区域、以及「整体替换」会丢掉多少已有区域,
确认之后才真正导入。
</div>
{{if gt .FogBlocks 0}}
<div style="margin:10px 0 0;display:flex;gap:8px;flex-wrap:wrap">
<a class="btn" href="/admin/fog/export">导出当前迷雾(.fwss)</a>
<a class="btn" href="/admin/fog/mask">迷雾遮罩区{{if .FogMasks}}({{.FogMasks}}){{end}}</a>
<form method="post" action="/admin/fog/clear"
onsubmit="return confirm('将删除全部迷雾数据(数据库与地图图层同步清空),且不可恢复。确认清空?')">
<button class="btn danger" type="submit">清空迷雾</button>
</form>
</div>
{{end}}
</div>
<div class="card">
<h2>数据导出</h2>
<div class="card-sub">
把当前展示的数据集导出为通用格式,方便备份或迁移到其他平台。
坐标保持入库时的 GCJ-02,GeoJSON 里已注明坐标系。
</div>
<div class="row">
<a class="btn" href="/admin/export/places.geojson">导出地点(GeoJSON)</a>
<a class="btn" href="/admin/export/places.gpx">导出地点(GPX)</a>
</div>
<div class="card-sub" style="margin-top:14px">
也可导出为 rond 原始格式的 <span class="mono">.rondbackup</span>(含你在<a href="/admin/edit">数据编辑</a>里补录的记录),可直接导回 rond App。
坐标为 GCJ-02,原样写回、无需纠偏。
</div>
<a class="btn primary" href="/admin/export/rondbackup">导出 rondbackup(可回导 rond)</a>
</div>
<div class="card">
<div class="row" style="justify-content:space-between;align-items:center">
<h2 style="margin:0">图片挂载恢复</h2>
<form method="post" action="/admin/images/recover"
onsubmit="return confirm('扫描磁盘上的图片文件,把库里没有记录的那些按坐标挂回地点?只新增记录、不动文件,可反复执行。')">
<button class="btn sm" type="submit">扫描并恢复</button>
</form>
</div>
<div class="card-sub">
换了数据集之后地点图片看不见了?<b>早期版本的图片记录挂在数据集上,删掉旧数据集时会连记录一起删</b>,
而图片文件还留在磁盘里。点这里按坐标把它们挂回去(假如图片属于旧数据集,先别删它的底座文件——
认地点要用它)。现在这版已经不会再有这个问题了。
</div>
<div class="card-sub" style="margin-top:10px">
体检:库里 <b>{{num .ImgRows}}</b> 张 · 本地盘上 <b>{{num .ImgObjects}}</b> 个{{if gt .ImgOrphans 0}}
· <b style="color:#b42318">落单 {{num .ImgOrphans}} 个</b>{{end}}
</div>
{{if gt .ImgOrphans 0}}
<div class="alert err">
有 <b>{{num .ImgOrphans}}</b> 个图片文件在库里没有记录——它们现在谁也看不到。
点「扫描并恢复」按坐标挂回去(对象存储上的也会一并扫,需要 s3:ListBucket 权限)。
</div>
{{end}}
</div>
<div class="card">
<div class="row" style="justify-content:space-between;align-items:center">
<h2 style="margin:0">换基线(导出底稿)</h2>
<a class="btn" href="/admin/baseline">打开</a>
</div>
<div class="card-sub">
导出 rondbackup 是以一份「底座」为底铺数据的,底座必须是手机导出的包。
手机那边重新导出后想换代,用这里换:<b>站点数据一行不动</b>,旧底座留档、可回滚。
</div>
</div>
<div class="card">
<div class="row" style="justify-content:space-between;align-items:center">
<h2 style="margin:0">已导入的数据集</h2>
<form method="post" action="/admin/places/backfill"
onsubmit="return confirm('对缺地址 / 没名字的地点逐个反查高德补全?(每次处理 100 个)')">
<button class="btn sm" type="submit">补全缺失地址</button>
</form>
</div>
<div class="card-sub">同一时刻只有一份数据处于「展示中」状态,主页与地图读取的就是它</div>
{{if .Datasets}}
<div class="tw">
<table class="tb tb-stack">
<thead>
<tr><th>文件</th><th>导入时间</th><th class="num">到访</th><th class="num">地点</th><th>数据跨度</th><th>状态</th><th></th></tr>
</thead>
<tbody>
{{range .Datasets}}
<tr>
<td><span class="mono">{{trunc .Name 34}}</span></td>
<td class="muted" data-label="导入时间">{{dt .UploadedAt}}</td>
<td class="num" data-label="到访">{{num .Visits}}</td>
<td class="num" data-label="地点">{{num .Places}}</td>
<td class="muted" data-label="数据跨度">{{if .First}}{{d .First}} ~ {{d .Last}}{{else}}—{{end}}</td>
<td data-label="状态">
{{if and $.Dataset (eq $.Dataset.ID .ID)}}
<span class="tag">展示中</span>
{{else}}
<span class="muted">未启用</span>
{{end}}
</td>
<td data-label="操作" class="ops">
{{if not (and $.Dataset (eq $.Dataset.ID .ID))}}
<form method="post" action="/admin/datasets/{{.ID}}/activate" style="display:inline">
<button class="btn sm" type="submit">启用</button>
</form>
{{end}}
<form method="post" action="/admin/datasets/{{.ID}}/delete" style="display:inline"
onsubmit="return confirm('删除后这份数据集的到访、地点、位移、天气、活动、标签、原始行留档都会从数据库移除,且不可恢复(重新上传同一份备份可以再导回来)。你在站点上自建的备注、结论、地点图片、专题都不受影响,会按坐标键 / 按人保留。确认删除?')">
<button class="btn sm danger" type="submit">删除</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="empty"><div class="big">📦</div>还没有导入任何数据集</div>
{{end}}
</div>
<div class="card">
<h2>数据说明</h2>
<div class="card-sub">从备份中读取并转换的字段</div>
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>来源表</th><th>内容</th><th>用途</th></tr></thead>
<tbody>
<tr><td class="mono">ZVISIT</td><td data-label="内容">到访记录(到达 / 离开时间、类型、地点、备注、标签)</td><td data-label="用途">时间线、统计、地图</td></tr>
<tr><td class="mono">ZLOCATION</td><td data-label="内容">地点(名称、经纬度、城市、省份、POI 分类)</td><td data-label="用途">地图打点、地点库</td></tr>
<tr><td class="mono">ZACTIVITY</td><td data-label="内容">自定义类型(颜色、图标、是否家 / 工作)</td><td data-label="用途">类型分布</td></tr>
<tr><td class="mono">ZMOVEMENT</td><td data-label="内容">位移段(交通方式、起止时间)</td><td data-label="用途">出行方式统计</td></tr>
<tr><td class="mono">ZTAG</td><td data-label="内容">自定义标签及分组</td><td data-label="用途">标签筛选</td></tr>
<tr><td class="mono">ZHOURLYWEATHER</td><td data-label="内容">逐小时天气(气温、天气现象、湿度)</td><td data-label="用途">天气足迹</td></tr>
</tbody>
</table>
</div>
</div>
</div>
{{end}}