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