上一批把显示区改成了「一组圆」,这一批让前台真正认多个圆。 - clip-path 从 circle() 换成 path():circle() 只表达得了一个圆。每个圆一段 圆弧子路径、**同绕向**(sweep=0),nonzero 填充规则下多段自然取并集, 重叠处不会互相挖洞。 - 拼路径的活单独放 fog-clip.js(rond-map.js 是个大 IIFE,函数取不出来), 这样能直接跑 node 单测;layout 里必须排在 rond-map.js 之前加载。 - 顺带清掉已经死掉的 fitCircle:多圆之后「圆心 + 半径」表达不了,留着就是 两套并行的贴合口径。fitParams 现在只给 fitBBox(单圆时外接矩形正是那个 圆的外接正方形,视野等价)。 - 守卫改名 TestTopicFogIsClippedToAreas,钉住两端(模板发 fogAreas ↔ 脚本认 fogAreas)与两处顺序(裁剪注册在 applyFit 之后、fog-clip.js 在 rond-map.js 之前);fogAreas 必须**无条件**下发(空数组 = 不限区域),不要包在条件里。 - verify.sh 挂进前端单测:以前只靠人记得手动跑 = 等于没有。找不到 node 直接 失败而不是跳过(跳过会让验收看着全绿、实际一行前端脚本都没验)。 验证(临时两圆专题,SQL 造的,已删): - fog-clip.test.js 15 条:把生成的 path 解析回圆弧再验几何,含绕向一致性与 半径 0/负数的夹取。 - 端到端像素验证:自校准出「CSS 容器坐标 → 图像像素」(scale≈2、offset≈19.2, 正是 site_cdp 的 --clip 外扩 10px×scale,别再手算);三张掩码图证明并集语义 —— A 独有点只有 A 亮、B 独有点只有 B 亮、**重叠中心两圆都亮且并集也亮 (无洞)**,整体集合差异 0.013%。 - 迷雾掩码:86,776 个像素,按两圆并集的解析式判 **圆外 0 个**;两个圆内分别 有 52,609 / 46,757 个迷雾像素 ⇒ 第二个圆真的被认了,不是只看第一个。
101 lines
4.3 KiB
HTML
101 lines
4.3 KiB
HTML
{{define "content"}}
|
|
{{if not .HasData}}
|
|
<div class="wrap">{{template "emptystate" .}}</div>
|
|
{{else}}
|
|
<div class="wrap">
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>{{.Topic.Title}}</h1>
|
|
{{if .Topic.Subtitle}}<div class="sub">{{.Topic.Subtitle}}</div>{{end}}
|
|
{{/* 时间范围独立成行:塞进 KPI 网格里会被挤成好几行 */}}
|
|
<div class="topic-range">{{.Topic.RangeLabel}}{{if .Topic.DayCount}} · 共 {{num .Topic.DayCount}} 天{{end}}</div>
|
|
</div>
|
|
<div class="row">
|
|
<a class="btn" href="/topics">← 全部专题</a>
|
|
{{if .IsAdmin}}<a class="btn" href="/admin/topics?edit={{.Topic.ID}}">编辑</a>{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
{{if not .Topic.Enabled}}<div class="alert err">这个专题尚未对外展示,只有你自己能看到。</div>{{end}}
|
|
|
|
{{if .Topic.Description}}<div class="card topic-desc">{{.Topic.Description}}</div>{{end}}
|
|
|
|
<div class="card">
|
|
<div class="kpis">
|
|
<div class="kpi"><div class="k">{{if .Topic.HasArea}}区域内地点{{else}}地点{{end}}</div><div class="v">{{num .Topic.PlaceCount}}</div></div>
|
|
<div class="kpi"><div class="k">到访次数</div><div class="v">{{num .Topic.VisitCount}}</div></div>
|
|
<div class="kpi"><div class="k">涉及城市</div><div class="v">{{num .Topic.CityCount}}</div></div>
|
|
<div class="kpi"><div class="k">累计停留</div><div class="v">{{dur .Topic.DwellMin}}</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="padding:10px">
|
|
{{/* 高度写 inline:站点里 #map{height:100%} 是 ID 选择器,只靠外部 CSS 类
|
|
容易在别处规则变动/缓存不一致时把容器算成 0 高,地图就整块消失 */}}
|
|
<div id="map" class="topic-map" style="height:520px">
|
|
<div id="map-fallback" class="map-fallback">地图加载中……若一直不出现,请强制刷新(Ctrl+F5)或检查浏览器是否拦截了地图脚本</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>去过的地方</h2>
|
|
<div class="card-sub">共 {{num .Topic.PlaceCount}} 个,按到访次数排序</div>
|
|
{{if .Points}}
|
|
<div class="tw">
|
|
<table class="tb tb-stack">
|
|
<thead><tr><th>地点</th><th>城市</th><th>类型</th><th class="num">到访</th><th class="num">停留</th><th>最近一次</th></tr></thead>
|
|
<tbody>
|
|
{{range .Points}}
|
|
<tr>
|
|
<td>{{if .Name}}{{.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
|
|
<td class="muted" data-label="城市">{{.City}}</td>
|
|
<td class="muted" data-label="类型">{{if .Act}}<span class="dot" style="background:{{csscolor .Color}}"></span>{{.Act}}{{end}}</td>
|
|
<td class="num" data-label="到访">{{num .Count}}</td>
|
|
<td class="num muted" data-label="停留">{{dur .Dwell}}</td>
|
|
<td class="muted mono" data-label="最近一次">{{.LastAt}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<div class="empty">这个时间 / 区域范围内没有记录</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
function whenReady(cb) {
|
|
if (document.readyState === 'complete') { setTimeout(cb, 0); return; }
|
|
window.addEventListener('load', cb);
|
|
}
|
|
whenReady(function () {
|
|
var el = document.getElementById('map');
|
|
// 移动端矮一些(inline 高度优先于外部 CSS,只能在这里按视口改)
|
|
if (el && window.matchMedia('(max-width: 620px)').matches) el.style.height = '360px';
|
|
|
|
if (window.rondMap) {
|
|
window.rondMap.full({
|
|
id: 'map',
|
|
points: {{.PointsJSON}},
|
|
fitBBox: {{.FitBBoxJSON}},
|
|
showFog: {{if .Topic.ShowFog}}true{{else}}false{{end}},
|
|
{{/* 圈了显示区的专题:迷雾只在**这些圆的并集**内显示(rond-map.js 给迷雾单独一个
|
|
pane,再用 fog-clip.js 拼出多圆的 clip-path: path())。不限区域时是空数组。 */}}
|
|
fogAreas: {{.Topic.AreasJSON}},
|
|
back: '{{.BackQS}}'
|
|
});
|
|
}
|
|
|
|
// 地图脚本没就绪(被拦 / 加载失败)时,把兜底提示留在原位,用户至少知道发生了什么
|
|
setTimeout(function () {
|
|
var fb = document.getElementById('map-fallback');
|
|
if (fb && el && el.classList.contains('leaflet-container')) fb.style.display = 'none';
|
|
}, 1200);
|
|
});
|
|
})();
|
|
</script>
|
|
{{end}}
|
|
{{end}}
|