先出方案(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 拆又容易做出「中间那笔编译不过」的历史,所以合成一笔、在消息里分段。
284 lines
14 KiB
HTML
284 lines
14 KiB
HTML
{{define "content"}}
|
||
<div class="wrap">
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>数据统计</h1>
|
||
{{/* 备份文件名只对站长有意义,访客看到的是噪音(也是没必要暴露的信息) */}}
|
||
<div class="sub">{{.RangeLabel}}{{if .IsAdmin}}{{if .Dataset}} · 数据来自 {{.Dataset.Name}}{{end}}{{end}}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{if not .HasData}}
|
||
{{template "emptystate" .}}
|
||
{{else}}
|
||
{{template "filters" .}}
|
||
|
||
<div class="kpis" style="margin-bottom:16px">
|
||
<div class="kpi"><div class="v">{{num .Overview.Visits}}<small>次</small></div><div class="k">到访记录</div></div>
|
||
<div class="kpi"><div class="v">{{num .Overview.Places}}<small>个</small></div><div class="k">不同地点</div></div>
|
||
<div class="kpi"><div class="v">{{dur .Overview.DwellMin}}</div><div class="k">累计停留</div></div>
|
||
<div class="kpi"><div class="v">{{km .Overview.DistanceKm}}<small>km</small></div><div class="k">出行里程</div></div>
|
||
</div>
|
||
|
||
{{if or .IsAdmin .Settings.MarksPublic}}
|
||
{{/* 第一块:站长的结论。
|
||
措辞必须**按身份分支**:访客看到「我的结论」只会一头雾水(谁的「我」),
|
||
地点页的投票区早就是这么分的(站长看「我的结论」、访客看「大家怎么说」),这里跟上。 */}}
|
||
<div class="card" style="margin-bottom:16px">
|
||
<div class="row" style="justify-content:space-between;align-items:center;flex-wrap:wrap">
|
||
<h2 style="margin:0">推荐 & 踩雷({{if .IsAdmin}}我的结论{{else}}站长结论{{end}})</h2>
|
||
<span class="muted" style="font-size:var(--fs-sm)">推荐 {{num .VoteRec}} · 踩雷 {{num .VoteAvoid}}</span>
|
||
</div>
|
||
<div class="card-sub">
|
||
只统计<strong>{{if .IsAdmin}}你自己{{else}}站长{{end}}</strong>在地点页或地图气泡里标的 👍/👎——按坐标键存,重新上传备份也不会丢。
|
||
</div>
|
||
<div class="grid g2">
|
||
<div>
|
||
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👍 {{if .IsAdmin}}我{{else}}站长{{end}}推荐的地点</div>
|
||
{{if .Recommended}}
|
||
<div class="chips">
|
||
{{range .Recommended}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#34c759"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}{{if .City}}<span class="muted"> · {{.City}}</span>{{end}}</a>{{end}}
|
||
</div>
|
||
{{else}}<div class="empty" style="padding:10px 0">还没有标记</div>{{end}}
|
||
</div>
|
||
<div>
|
||
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👎 {{if .IsAdmin}}我{{else}}站长{{end}}踩雷的地点</div>
|
||
{{if .Avoided}}
|
||
<div class="chips">
|
||
{{range .Avoided}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#ff3b30"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}{{if .City}}<span class="muted"> · {{.City}}</span>{{end}}</a>{{end}}
|
||
</div>
|
||
{{else}}<div class="empty" style="padding:10px 0">还没有标记</div>{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{/* 第二块:访客的点赞点踩。匿名一人一票、可改可撤,与上面的站长结论是两码事。 */}}
|
||
<div class="card" style="margin-bottom:16px">
|
||
<div class="row" style="justify-content:space-between;align-items:center;flex-wrap:wrap">
|
||
<h2 style="margin:0">访客点赞</h2>
|
||
<span class="muted" style="font-size:var(--fs-sm)">👍 {{num .VoteUp}} 票 · 👎 {{num .VoteDown}} 票</span>
|
||
</div>
|
||
<div class="card-sub">
|
||
访客在地点页点的 👍/👎,一人一票、可改可撤。与上面的站长结论<strong>互不影响</strong>:各存各的表,谁也不会覆盖谁。
|
||
</div>
|
||
<div class="grid g2">
|
||
<div>
|
||
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👍 被点赞最多的地点</div>
|
||
{{if .VisitorUp}}
|
||
<div class="chips">
|
||
{{range .VisitorUp}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#34c759"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}<span class="muted"> · 👍 {{.Up}}{{if .Down}} · 👎 {{.Down}}{{end}}</span></a>{{end}}
|
||
</div>
|
||
{{else}}<div class="empty" style="padding:10px 0">还没有访客点赞</div>{{end}}
|
||
</div>
|
||
<div>
|
||
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:6px">👎 被点踩最多的地点</div>
|
||
{{if .VisitorDown}}
|
||
<div class="chips">
|
||
{{range .VisitorDown}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#ff3b30"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}<span class="muted"> · 👎 {{.Down}}{{if .Up}} · 👍 {{.Up}}{{end}}</span></a>{{end}}
|
||
</div>
|
||
{{else}}<div class="empty" style="padding:10px 0">还没有访客点踩</div>{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="card">
|
||
<h2>月度分布</h2>
|
||
<div class="card-sub">每月到访次数 · 点柱条只看那个月</div>
|
||
<div class="cols">
|
||
{{/* 点柱条直接筛出那个月;title 是悬停读数。最高的那根在柱顶标出次数(.pv),
|
||
副标题里就不再重复写一遍「最高 N 次」。 */}}
|
||
{{range .Monthly}}<a class="col{{if eq .Count $.MaxMonthly}} peak{{end}}" href="{{$.MonthLink .Key}}" title="{{.Key}}:{{.Count}} 次到访。点一下只看这个月" style="height:{{.Pct}}%">{{if eq .Count $.MaxMonthly}}<span class="pv">{{num .Count}}</span>{{end}}</a>{{end}}
|
||
</div>
|
||
<div class="cols-x">{{range .Monthly}}<span>{{.Label}}</span>{{end}}</div>
|
||
</div>
|
||
|
||
<div class="grid g2">
|
||
<div class="card">
|
||
<h2>类型分布</h2>
|
||
<div class="card-sub">按到访次数排序</div>
|
||
<div class="bars">
|
||
{{range .ActivityStats}}
|
||
<a class="bar-row" href="{{$.ActLink .ID}}" title="{{.Name}}:{{num .Count}} 次,累计 {{dur .Dwell}}。点一下按这个类型筛选">
|
||
<div class="nm">{{with icon .Icon}}{{.}} {{end}}{{.Name}}</div>
|
||
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxActivity}}%;background:{{csscolor .Color}}"></div></div>
|
||
<div class="vv">{{num .Count}} 次 · {{dur .Dwell}}</div>
|
||
</a>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>一天中的分布</h2>
|
||
<div class="card-sub">按到访开始时间统计</div>
|
||
<div class="cols" style="height:130px">
|
||
{{range $h, $c := .Hourly}}<div class="col{{if eq $c $.MaxHourly}} peak{{end}}" style="height:{{pct $c $.MaxHourly}}%" title="{{$h}} 时:{{$c}} 次"></div>{{end}}
|
||
</div>
|
||
{{/* 24 根柱光看高度分不出上午/下午:换成一行 6 小时一段的标尺,段边界与柱子严格
|
||
对齐(同一套 24 列网格),每段直接把时段与小时区间写出来,就不必再留坐标轴。 */}}
|
||
<div class="cols-band">
|
||
<span>深夜 0-6</span><span>上午 6-12</span><span>下午 12-18</span><span>晚上 18-24</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h2>城市排行</h2>
|
||
<div class="card-sub">到访次数</div>
|
||
<div class="bars">
|
||
{{range .CityStats}}
|
||
<a class="bar-row" href="{{$.CityLink .City}}" title="{{.City}}:{{num .Count}} 次到访、{{num .Places}} 个地点。点一下按这个城市筛选">
|
||
<div class="nm">{{.City}}</div>
|
||
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxCity}}%"></div></div>
|
||
<div class="vv">{{num .Count}} 次 · {{num .Places}} 地点</div>
|
||
</a>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>省级行政区</h2>
|
||
<div class="card-sub">覆盖情况</div>
|
||
<div class="tw">
|
||
<table class="tb tb-stack">
|
||
<thead><tr><th>省份</th><th class="num">城市</th><th class="num">地点</th><th class="num">到访</th></tr></thead>
|
||
<tbody>
|
||
{{range .ProvinceStats}}
|
||
<tr>
|
||
<td>{{.Province}}</td>
|
||
<td class="num" data-label="城市">{{num .Cities}}</td>
|
||
<td class="num" data-label="地点">{{num .Places}}</td>
|
||
<td class="num" data-label="到访">{{num .Count}}</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h2>星期分布</h2>
|
||
<div class="card-sub">一周中的到访规律</div>
|
||
<div class="bars">
|
||
{{range .Weekday}}
|
||
{{/* 星期没有对应的筛选维度(筛选只有日期区间),所以只补悬停读数 */}}
|
||
<div class="bar-row" title="{{.Label}}:{{num .Count}} 次到访">
|
||
<div class="nm">{{.Label}}</div>
|
||
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxWeekday}}%"></div></div>
|
||
<div class="vv">{{num .Count}} 次</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>出行方式</h2>
|
||
<div class="card-sub">次数与估算里程</div>
|
||
{{if .Transports}}
|
||
<div class="tw">
|
||
<table class="tb tb-stack">
|
||
<thead><tr><th>方式</th><th class="num">次数</th><th class="num">里程</th><th class="num">在途时间</th></tr></thead>
|
||
<tbody>
|
||
{{range .Transports}}
|
||
<tr>
|
||
<td><span class="dot" style="display:inline-block;background:{{csscolor .Color}}"></span> {{with icon .Icon}}{{.}} {{end}}{{.Name}}</td>
|
||
<td class="num" data-label="次数">{{num .Count}}</td>
|
||
<td class="num" data-label="里程">{{km .Distance}} km</td>
|
||
<td class="num" data-label="在途时间">{{dur .DwellMin}}</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{{else}}<div class="empty">暂无位移记录</div>{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
{{if .Tags}}
|
||
<div class="card">
|
||
<h2>标签统计</h2>
|
||
<div class="card-sub">自定义标签的使用情况</div>
|
||
<div class="bars">
|
||
{{range .Tags}}
|
||
<a class="bar-row" href="{{$.TagLink .ID}}" title="#{{.Name}}:{{num .Count}} 次到访。点一下按这个标签筛选">
|
||
<div class="nm"><span class="dot" style="display:inline-block;background:{{csscolor .Color}}"></span> {{.Name}}</div>
|
||
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Count $.MaxTag}}%"></div></div>
|
||
<div class="vv">{{num .Count}} 次</div>
|
||
</a>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{if .Weather}}
|
||
<div class="card">
|
||
<h2>天气与气温</h2>
|
||
<div class="card-sub">记录期间经历的小时级天气</div>
|
||
<div class="tw">
|
||
<table class="tb tb-stack">
|
||
<thead><tr><th>天气</th><th class="num">小时数</th><th class="num">平均气温</th></tr></thead>
|
||
<tbody>
|
||
{{range .Weather}}
|
||
<tr>
|
||
<td>{{condition .Condition}}</td>
|
||
<td class="num" data-label="小时数">{{num .Count}}</td>
|
||
<td class="num" data-label="平均气温">{{f1 .AvgTemp}} ℃</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{{/* 逐月平均气温:这份数据以前一直算着却没渲染(白跑一条查询),这里把它用上。
|
||
横向可滚动,十几根柱子挤在窄屏上也不会把月份标签压成一团。 */}}
|
||
{{if .Temps}}
|
||
<div class="temp-chart">
|
||
<div class="temp-head">逐月平均气温</div>
|
||
<div class="temp-strip">
|
||
{{range .Temps}}
|
||
<div class="temp-col" title="{{.Key}}:平均 {{f1 .AvgC}} ℃,{{num .Hours}} 小时有记录">
|
||
<span class="tc-v">{{f1 .AvgC}}°</span>
|
||
<div class="tc-track"><div class="tc-bar" style="height:{{.Pct}}%;background:{{.Color}}"></div></div>
|
||
<span class="tc-m">{{.Label}}</span>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
{{if .Fog}}
|
||
<div class="card">
|
||
<h2>世界迷雾覆盖</h2>
|
||
<div class="card-sub">
|
||
迷雾快照 {{num .Fog.Blocks}} 块 / {{num .Fog.Cells}} 格,估算已探索
|
||
<b>{{f1 .Fog.AreaKm2}}</b> km²;地点落在已探索格内的比例即「点亮率」
|
||
</div>
|
||
<div class="kpis" style="margin-bottom:12px">
|
||
<div class="kpi"><div class="v">{{f1 .Fog.AreaKm2}}<small>km²</small></div><div class="k">已探索面积</div></div>
|
||
<div class="kpi"><div class="v">{{pct .Fog.LitPlaces .Fog.Places}}<small>%</small></div><div class="k">地点点亮率</div></div>
|
||
<div class="kpi"><div class="v">{{num .Fog.LitPlaces}}<small>/{{num .Fog.Places}}</small></div><div class="k">点亮地点</div></div>
|
||
</div>
|
||
{{if .Fog.Cities}}
|
||
<div class="bars">
|
||
{{range .Fog.Cities}}
|
||
<a class="bar-row" href="{{$.CityLink .Name}}" title="{{.Name}}:{{.Lit}}/{{.Total}} 个地点落在已探索区。点一下按这个城市筛选">
|
||
<div class="nm">{{.Name}}</div>
|
||
<div class="bar-track"><div class="bar-fill" style="width:{{pct .Lit .Total}}%;background:var(--accent)"></div></div>
|
||
<div class="vv">{{.Lit}}/{{.Total}}</div>
|
||
</a>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|