先出方案(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 拆又容易做出「中间那笔编译不过」的历史,所以合成一笔、在消息里分段。
190 lines
9.8 KiB
HTML
190 lines
9.8 KiB
HTML
{{define "content"}}
|
||
<div class="wrap">
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>地点备注</h1>
|
||
<div class="sub">列出当前数据集的全部地点,可给任意地点起别名、写备注、标「推荐 / 踩雷」;别名会替代原名显示</div>
|
||
</div>
|
||
<div class="row">
|
||
<a class="btn" href="/admin/settings">可见性设置</a>
|
||
<a class="btn" href="/admin">← 返回后台</a>
|
||
</div>
|
||
</div>
|
||
|
||
{{if .Flash}}<div class="alert ok">{{.Flash}}</div>{{end}}
|
||
{{if .Err}}<div class="alert err">{{.Err}}</div>{{end}}
|
||
|
||
<div class="card">
|
||
<div class="card-sub" style="margin:0 0 10px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
<span>
|
||
备注对外{{if .NotesPublic}}<b>公开</b>(访客也能看到){{else}}<b>不公开</b>(只有你能看到){{end}}
|
||
· 共 {{num .Total}} 个地点
|
||
</span>
|
||
<form method="post" action="/admin/notes/rond" style="margin:0">
|
||
<button class="btn sm" type="submit"
|
||
title="把 rond 备份里按次到访写的备注汇总成地点备注;只重写「来自 rond」那一块,你手写的内容不动">
|
||
从 rond 备份回填备注
|
||
</button>
|
||
</form>
|
||
</div>
|
||
<form method="get" action="/admin/notes" class="row" style="gap:8px;flex-wrap:wrap;align-items:flex-end;margin:0">
|
||
<label class="f" style="margin:0;flex:1 1 200px"><span>关键词</span>
|
||
<input type="text" name="q" value="{{.Query}}" placeholder="地点名 / 别名 / 备注 / 城市">
|
||
</label>
|
||
<label class="f" style="margin:0;flex:0 0 145px"><span>状态</span>
|
||
<select name="status">
|
||
<option value="all" {{if eq .Status "all"}}selected{{end}}>全部地点</option>
|
||
<option value="has" {{if eq .Status "has"}}selected{{end}}>仅有备注</option>
|
||
<option value="none" {{if eq .Status "none"}}selected{{end}}>仅无备注</option>
|
||
</select>
|
||
</label>
|
||
<label class="f" style="margin:0;flex:0 0 150px"><span>城市</span>
|
||
<select name="city">
|
||
<option value="">全部城市</option>
|
||
{{range .CityOptions}}<option value="{{.}}" {{if eq . $.City}}selected{{end}}>{{.}}</option>{{end}}
|
||
</select>
|
||
</label>
|
||
<label class="f" style="margin:0;flex:0 0 140px"><span>排序</span>
|
||
<select name="sort">
|
||
<option value="visit" {{if eq .Sort "visit"}}selected{{end}}>到访次数</option>
|
||
<option value="note" {{if eq .Sort "note"}}selected{{end}}>最近编辑</option>
|
||
<option value="talk" {{if eq .Sort "talk"}}selected{{end}}>票数</option>
|
||
<option value="name" {{if eq .Sort "name"}}selected{{end}}>名称</option>
|
||
</select>
|
||
</label>
|
||
<button class="btn primary" type="submit">筛选</button>
|
||
<a class="btn" href="/admin/notes">重置</a>
|
||
</form>
|
||
</div>
|
||
|
||
{{if .Rows}}
|
||
<form id="bulk-form" method="post" action="/admin/notes/bulk"></form>
|
||
<div class="card" style="position:sticky;top:8px;z-index:3;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
<label class="check" style="margin:0"><input type="checkbox" id="chk-all"> 全选本页</label>
|
||
<span class="muted" style="font-size:var(--fs-sm)">批量:</span>
|
||
<select name="verdict" form="bulk-form" style="padding:5px 8px;border:1px solid var(--line);border-radius:8px">
|
||
<option value="1">👍 推荐</option>
|
||
<option value="-1">👎 踩雷</option>
|
||
<option value="0">取消结论</option>
|
||
</select>
|
||
<button class="btn sm" type="submit" form="bulk-form" name="action" value="verdict">设为该结论</button>
|
||
<button class="btn sm" type="submit" form="bulk-form" name="action" value="clear"
|
||
onclick="return confirm('清空所选地点的手写备注?(别名与「来自 rond」的备注都会保留)')">清空备注</button>
|
||
<button class="btn sm danger" type="submit" form="bulk-form" name="action" value="delete"
|
||
onclick="return confirm('删除所选地点的手写备注与别名?(rond 备份里的备注下次回填还会再出现)')">删除备注</button>
|
||
</div>
|
||
|
||
<div class="card" style="padding:0">
|
||
<div class="list">
|
||
{{range .Rows}}
|
||
<div class="item" style="grid-template-columns:28px 1fr;gap:10px;align-items:stretch">
|
||
<div style="display:flex;align-items:flex-start;padding-top:10px">
|
||
<input type="checkbox" name="src_pk" value="{{.SrcPK}}" form="bulk-form" class="row-chk">
|
||
</div>
|
||
<div>
|
||
<form method="post" action="/admin/place/note" style="display:grid;grid-template-columns:1fr 1.4fr auto;gap:10px;align-items:end">
|
||
<input type="hidden" name="src_pk" value="{{.SrcPK}}">
|
||
<input type="hidden" name="next" value="{{$.BackURL}}">
|
||
<label class="f" style="margin:0">
|
||
<span>别名{{if .PlaceName}}(原名:{{.PlaceName}}{{if .PlaceCity}} · {{.PlaceCity}}{{end}}){{end}}</span>
|
||
<input type="text" name="alias" value="{{.Alias}}" maxlength="60" placeholder="{{.PlaceName}}">
|
||
</label>
|
||
<label class="f" style="margin:0">
|
||
<span>备注{{if .PlaceID}} <a href="/places/{{.PlaceID}}" target="_blank">查看 ↗</a>{{end}}</span>
|
||
<input type="text" name="note" value="{{.Content}}" maxlength="2000">
|
||
</label> <div class="row" style="gap:6px;flex-wrap:nowrap">
|
||
<button class="btn primary" type="submit">保存</button>
|
||
<button class="vote-btn up {{if eq .Verdict 1}}on{{end}}" type="button"
|
||
data-verdict-btn data-src="{{.SrcPK}}" data-v="1" title="标为推荐">👍</button>
|
||
<button class="vote-btn down {{if eq .Verdict -1}}on{{end}}" type="button"
|
||
data-verdict-btn data-src="{{.SrcPK}}" data-v="-1" title="标为踩雷">👎</button>
|
||
</div>
|
||
</form>
|
||
<div class="muted" style="font-size:var(--fs-sm);margin-top:4px">
|
||
地点 ID {{.SrcPK}}{{if .VisitCount}} · 到访 {{num .VisitCount}} 次{{end}}
|
||
<span class="vote-tip" data-verdict-status="{{.SrcPK}}">{{if eq .Verdict 1}} · 已标推荐{{else if eq .Verdict -1}} · 已标踩雷{{end}}</span>
|
||
{{if or .Up .Down}} · 访客 👍{{.Up}} 👎{{.Down}}{{end}}
|
||
{{if .Content}} · 更新于 {{dt .UpdatedAt}}{{end}}
|
||
</div>
|
||
{{if .RondNote}}<div class="note-block">来自 rond:{{.RondNote}}</div>{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
{{if gt .PageCount 1}}
|
||
<div class="pager">
|
||
{{if gt .PageNo 1}}<a href="{{.PageLink (sub .PageNo 1)}}">上一页</a>{{end}}
|
||
<span class="cur">第 {{.PageNo}} / {{.PageCount}} 页</span>
|
||
{{if lt .PageNo .PageCount}}<a href="{{.PageLink (add .PageNo 1)}}">下一页</a>{{end}}
|
||
</div>
|
||
{{end}}
|
||
|
||
<script>
|
||
(function () {
|
||
var all = document.getElementById('chk-all');
|
||
var boxes = document.querySelectorAll('.row-chk');
|
||
if (all) {
|
||
all.addEventListener('change', function () {
|
||
boxes.forEach(function (b) { b.checked = all.checked; });
|
||
});
|
||
}
|
||
|
||
// 行内 👍/👎 用 AJAX(原来是 formaction 提交 → 整页刷新,会把同一行没保存的备注编辑一起丢掉)
|
||
document.querySelectorAll('.vote-btn[data-verdict-btn]').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
var src = b.getAttribute('data-src');
|
||
var v = Number(b.getAttribute('data-v'));
|
||
fetch('/api/verdict', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
credentials: 'same-origin',
|
||
body: 'src_pk=' + src + '&verdict=' + (b.classList.contains('on') ? 0 : v)
|
||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||
if (d.error) { alert(d.error); return; }
|
||
var cur = Number(d.verdict) || 0;
|
||
b.parentElement.querySelectorAll('.vote-btn').forEach(function (x) {
|
||
x.classList.toggle('on', Number(x.getAttribute('data-v')) === cur);
|
||
});
|
||
var st = document.querySelector('[data-verdict-status="' + src + '"]');
|
||
if (st) {
|
||
st.className = 'vote-tip' + (cur === 1 ? ' up' : cur === -1 ? ' down' : '');
|
||
st.textContent = cur === 1 ? ' · 已标推荐' : cur === -1 ? ' · 已标踩雷' : '';
|
||
}
|
||
}).catch(function () {});
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
{{else}}
|
||
<div class="empty"><div class="big">📝</div>{{if .Query}}没有匹配的地点{{else}}当前数据集里还没有地点{{end}}</div>
|
||
{{end}}
|
||
|
||
{{if .Orphans}}
|
||
<div class="card" style="padding:0;margin-top:16px">
|
||
<div style="padding:12px 14px 4px"><h2 style="margin:0">已不在当前数据集的备注</h2>
|
||
<div class="card-sub">这些备注挂在当前备份里已不存在的地点上(换了快照的旧地点),可逐条清理。</div>
|
||
</div>
|
||
<div class="list">
|
||
{{range .Orphans}}
|
||
<div class="item" style="grid-template-columns:1fr auto;gap:10px;align-items:center">
|
||
<div>
|
||
<div>{{if .Alias}}{{.Alias}}{{else}}<span class="muted">(无别名)</span>{{end}}{{if .Content}} · <span class="muted">{{.Content}}</span>{{end}}{{if .RondNote}} · <span class="muted">来自 rond:{{.RondNote}}</span>{{end}}</div>
|
||
<div class="muted" style="font-size:var(--fs-sm)">地点 ID {{.SrcPK}} · 更新于 {{dt .UpdatedAt}}</div>
|
||
</div>
|
||
<form method="post" action="/admin/place/note">
|
||
<input type="hidden" name="src_pk" value="{{.SrcPK}}">
|
||
<input type="hidden" name="alias" value="">
|
||
<input type="hidden" name="note" value="">
|
||
<input type="hidden" name="next" value="{{$.BackURL}}">
|
||
<button class="btn danger" type="submit" onclick="return confirm('删除这条备注?')">删除</button>
|
||
</form>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|