专题页的迷雾按专题圈的区域收口
以前专题的中心/半径只用来定视图,迷雾图层一直是全局的;管理页那句 「迷雾由地图按视野自动呈现,不需要单独圈选」其实是把「没做」写成了 「不用做」。现在专题圈了区域时,迷雾只在那个圆内显示。 做法是给迷雾单独一个 pane(zIndex 250,底图 200 之上)再对该 pane 做 clip-path: circle()——不能直接裁 tilePane,底图也在里面,会把地图一起挖空。 两个坑: - 圆心必须先 project() 到底图坐标系(与 fitCircle 一致),否则整体偏移; - 裁剪圆的注册必须排在 applyFit() 之后:视图未定时 latLngToContainerPoint() 会抛「Set map center and zoom first.」,把后续图层添加一起打断——实测 表现是 pane 建好了、迷雾瓦片一张都不出来,而且不报错。 迷雾是累积快照、没有时间维度,所以只按区域收口,不按专题的时间范围收口。 守卫 TestTopicFogIsClippedToArea 钉住两端与注册顺序。
This commit is contained in:
@@ -34,3 +34,14 @@
|
||||
|
||||
**回归**:verify.sh **PASS=349 FAIL=0 SKIP=0**、JS 单测 45/45、移动端 390 溢出全 0、
|
||||
桌面 1440 与改前一致(−15 是滚动条宽)。
|
||||
|
||||
## 专题的迷雾按区域收口
|
||||
|
||||
用户问「我创建了一个专题,但是迷雾好像无法根据选择显示范围?」——查下来是**实现缺口**:
|
||||
专题的中心/半径只用来 `fitCircle` 定视图,迷雾图层一直是全局的;管理页那句
|
||||
「区域内的世界迷雾由地图按视野自动呈现,不需要单独圈选」属于**文档与实现不符**(把「没做」写成「不用做」)。
|
||||
用户选定口径:**按专题区域收口**(迷雾是累积快照、无时间维度,所以不按时间范围收口)。
|
||||
|
||||
改 4 个文件:`rond-map.js`(新建 `rondFog` pane + `clip-path: circle()`)、`topic.html`(`.Topic.HasArea` 时下发
|
||||
`fogArea`)、`admin/topics.html`(把那句错提示删掉、补正确说明)、`frontend_fixes_test.go`(守卫)。
|
||||
细节与三个坑(不能裁 `tilePane`、圆心要 `project()`、注册必须排在 `applyFit()` 之后)见 details 四十五。
|
||||
|
||||
@@ -1078,5 +1078,35 @@ HTML,一行里必然带 `</script>`,一拒就把正常用法挡死了(我
|
||||
一大片空白 —— `align-items: start` 只能消掉**卡内**的空白,消不掉并排本身的空。各占整行才对
|
||||
(列表横铺、24 根柱也终于有宽度)。`align-items: start` 仍然保留:其它同行卡片也会体量不等。
|
||||
|
||||
## 四十五、专题的迷雾按区域收口(2026-10-10)
|
||||
|
||||
**现象**:用户建了专题问「迷雾好像无法根据选择显示范围?」。查下来是**实现缺口**,不是配置问题 ——
|
||||
迷雾图层一直是全局的,专题的区域(中心+半径)只用来 `fitCircle` 定视图,跟迷雾完全无关。
|
||||
管理页那句「区域内的世界迷雾由地图按视野自动呈现,**不需要单独圈选**」纯属**文档与实现不符**,
|
||||
把「没做」写成了「不用做」。用户选的口径:**按专题区域收口**(不按时间收口)。
|
||||
|
||||
**实现**(`rond-map.js` + `topic.html`):`opts.fogArea = {lat, lon, radiusKm}` 由 `topic.html`
|
||||
在 `.Topic.HasArea` 时下发;脚本给迷雾**单独建一个 pane** 再对该 pane 做 `clip-path: circle()`。
|
||||
|
||||
⚠️ 三个必踩的坑:
|
||||
|
||||
- **不能裁默认 `tilePane`** —— 底图瓦片也在那里面,裁下去地图跟着一起被挖空。所以
|
||||
`map.createPane('rondFog')`(`zIndex=250`:底图 200 之上、覆盖物 400 之下,`pointerEvents='none'`),
|
||||
迷雾 tileLayer 走 `pane: fogPane ? 'rondFog' : 'tilePane'`。
|
||||
- **圆心必须 `project()` 到底图坐标系**(跟 `fitCircle` 一样),直接拿经纬度当容器坐标会整体偏出几百米。
|
||||
半径用「同经度上偏移 radiusKm/111320 纬度」再量容器像素距离,随缩放自然变化。
|
||||
- 🔴 **注册点必须排在 `applyFit()` 之后**。`latLngToContainerPoint()` 在视图未定时会抛
|
||||
「Set map center and zoom first.」—— 实测放早了:pane 建好了、**迷雾瓦片一张都不出来**
|
||||
(那行异常把后面所有图层添加一起打断,且不报错给用户,页面看着只是「迷雾没了」)。
|
||||
正确写法:`applyFit(); if (applyFogClip) { map.on('move zoom viewreset resize', applyFogClip); applyFogClip(); }`
|
||||
|
||||
- 迷雾是**累积快照、没有时间维度** ⇒ 只按区域收口,不按专题的时间范围收口(管理页文案已写明)。
|
||||
- 老浏览器不认 `clip-path` 只是「不裁剪」(退回全局迷雾),不会报错。
|
||||
- 门禁 `TestTopicFogIsClippedToArea`:钉住两端 —— `topic.html` 得发 `fogArea:`、`rond-map.js` 得认
|
||||
`opts.fogArea`/`createPane('rondFog')`/`clipPath`,且注册点下标 > `var applyFit = function` 的下标
|
||||
(防的就是上面那个「放早了整片迷雾消失」)。
|
||||
- 实测:临时专题 id=16(中心 24.698/108.075、半径 3km)→ `clip = circle(173px at 529px 260px)`,
|
||||
反解半径正好 **3000 m**,`setZoom(9)` 后仍正确;`/map`、`/stats` 不受影响(它们没有 `fogArea`)。
|
||||
- ⚠️ 用户自己的专题 `id=15`「2026年国庆」`radius_km=0` ⇒ 收口不生效,要在后台把中心与半径填上。
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# rond-with-you 硬规矩(索引)
|
||||
|
||||
> 全文:`.workbuddy/memory/MEMORY-details.md`(38 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
|
||||
> 全文:`.workbuddy/memory/MEMORY-details.md`(46 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
|
||||
>
|
||||
> ⚠️ 本文件**按字符数**注入,硬上限 `MAX_MEMORY_CHARS = 1e4`(WorkBuddy `app.asar` 里 `getWorkingMemoryContent`;中文 1 字算 1,不是按字节)。超了会被 `substring(0,1e4)` 截断并写明「先整理再干活」——即整段尾部静默丢失。日常日志与本 details **不参与注入**,可以随便写。现值见 `wc -m`,留 20% 余量。
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
- 瓦片须与底图同 `crs`;带地图页须 `p.HasMap=true`。
|
||||
- `tileDigits` 补前导 0;`tileName` 尾缀 `len(s)-2` 单数字编号时为 -1 ⇒ panic;解码守卫 `len(name)<8`。
|
||||
- `fogIndexV` 必须 `atomic.Pointer`(普通指针=真 data race)。`fog_race_test.go`(-race)
|
||||
- **专题页迷雾按区域收口**(`opts.fogArea`)=**单独 pane** + `clip-path`:不能裁 `tilePane`(底图也在里面);圆心须 `project()` 到底图坐标系;⚠️ 注册必须排在 `applyFit()` **之后** —— 视图未定时 `latLngToContainerPoint()` 会抛「Set map center and zoom first.」,把后续图层添加一起打断(实测=迷雾整片消失、不报错)。`TestTopicFogIsClippedToArea`(四十五)
|
||||
|
||||
## 前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D、卅七)
|
||||
- ⚠️ **对访客公开的块,措辞必须按身份分支**(`{{if .IsAdmin}}我的…{{else}}站长…{{end}}`):
|
||||
|
||||
@@ -265,6 +265,47 @@ func TestDateInputsGoThroughAttachAll(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 专题的迷雾要和「区域」成对:模板传 fogArea、地图脚本认 fogArea ----------
|
||||
//
|
||||
// 2026-10-10 用户实测发现:专题圈了区域,迷雾却还是全局的。而管理页原本的说明写的是
|
||||
// 「区域内的世界迷雾由地图按视野自动呈现,不需要单独圈选」—— **文档与实现不一致**
|
||||
// (迷雾一直是全局瓦片,完全不认区域),这种「说明写了、实现没做」最容易蒙混过关。
|
||||
//
|
||||
// 这条守卫钉住两端与一处顺序:
|
||||
// ① topic.html 必须把专题区域传给地图;
|
||||
// ② rond-map.js 必须实现圆形收口(单独 pane + clip-path,不能裁默认 tilePane——底图在里面);
|
||||
// ③ 裁剪圆的注册必须排在 applyFit() 之后:视图还没定时 latLngToContainerPoint() 会抛
|
||||
// 「Set map center and zoom first.」,那行异常会把后面的图层添加一起打断
|
||||
// (实测:pane 建好了、迷雾瓦片一张都没有)。
|
||||
func TestTopicFogIsClippedToArea(t *testing.T) {
|
||||
tpl, err := tmplFS.ReadFile("templates/topic.html")
|
||||
if err != nil {
|
||||
t.Fatalf("读取 topic.html 失败: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(tpl), "fogArea:") {
|
||||
t.Error("topic.html 没把专题区域(fogArea)传给地图 —— 迷雾不会被收口到那个圆内")
|
||||
}
|
||||
|
||||
js, err := staticFS.ReadFile("static/js/rond-map.js")
|
||||
if err != nil {
|
||||
t.Fatalf("读取 rond-map.js 失败: %v", err)
|
||||
}
|
||||
src := string(js)
|
||||
for _, needle := range []string{"opts.fogArea", "createPane('rondFog')", "clipPath"} {
|
||||
if !strings.Contains(src, needle) {
|
||||
t.Errorf("rond-map.js 里缺 %q —— 迷雾的圆形收口没实现或被改掉了", needle)
|
||||
}
|
||||
}
|
||||
// 顺序:注册点必须排在 `var applyFit = function` 之后 —— 放早了(比如塞在迷雾图层那段里)
|
||||
// 就是在视图定下来之前算裁剪圆,会抛「Set map center and zoom first.」并打断后续图层添加。
|
||||
reg := strings.Index(src, "map.on('move zoom viewreset resize', applyFogClip)")
|
||||
fitDef := strings.Index(src, "var applyFit = function")
|
||||
if reg < 0 || fitDef < 0 || reg < fitDef {
|
||||
t.Error("applyFogClip 的注册必须排在 applyFit 定义(也就是视图设定)之后 —— " +
|
||||
"视图未定时算裁剪圆会抛错,并把后面的图层添加一起打断")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 字阶纪律:app.css 与模板内联样式都只走令牌 ----------
|
||||
//
|
||||
// 改前 app.css 里散着 10 / 10.5 / 11 / 11.5 / 12 / 12.5 / 12.8 / 13 / 13.5 / 14 / 14.5 / 15 /
|
||||
|
||||
@@ -704,9 +704,31 @@
|
||||
// 专题可以在管理页关掉迷雾(opts.showFog=false),此时图层、浓度滑杆、图层开关都不出现。
|
||||
var wantFog = opts.showFog !== false;
|
||||
var fogLayer = null;
|
||||
/* 专题可以圈一个区域(opts.fogArea = {lat, lon, radiusKm}):此时迷雾只在该圆内显示。
|
||||
做法是给迷雾**单独一个 pane** 再对这个 pane 做圆形 clip-path —— 不能直接裁默认的
|
||||
tilePane,底图也在那里面,会把地图一起裁掉。
|
||||
圆心必须跟 fitCircle 一样先 project() 到底图坐标系,否则会整体偏出几百米。
|
||||
clip-path 不认识的老浏览器只是「不裁剪」(退回全局迷雾),不会出错。 */
|
||||
var fogPane = null, applyFogClip = null;
|
||||
if (wantFog && cfg.fog && cfg.fog.blocks > 0 && opts.fogArea && opts.fogArea.radiusKm > 0) {
|
||||
map.createPane('rondFog');
|
||||
fogPane = map.getPane('rondFog');
|
||||
fogPane.style.zIndex = 250; // 底图瓦片 200 之上、覆盖物 400 之下
|
||||
fogPane.style.pointerEvents = 'none';
|
||||
var fogC = project(opts.fogArea.lat, opts.fogArea.lon, def.crs);
|
||||
var fogCenter = L.latLng(fogC[0], fogC[1]);
|
||||
var fogNorth = L.latLng(fogCenter.lat + (opts.fogArea.radiusKm * 1000) / 111320, fogCenter.lng);
|
||||
applyFogClip = function () {
|
||||
var p = map.latLngToContainerPoint(fogCenter);
|
||||
var r = Math.max(1, p.distanceTo(map.latLngToContainerPoint(fogNorth)));
|
||||
var v = 'circle(' + r.toFixed(1) + 'px at ' + p.x.toFixed(1) + 'px ' + p.y.toFixed(1) + 'px)';
|
||||
fogPane.style.clipPath = v;
|
||||
fogPane.style.webkitClipPath = v;
|
||||
};
|
||||
}
|
||||
if (wantFog && cfg.fog && cfg.fog.blocks > 0) {
|
||||
fogLayer = L.tileLayer('/tiles/fog/{z}/{x}/{y}?crs=' + def.crs + '&v=' + cfg.fog.ver, {
|
||||
minZoom: 3, maxZoom: 18, opacity: 0.9
|
||||
minZoom: 3, maxZoom: 18, opacity: 0.9, pane: fogPane ? 'rondFog' : 'tilePane'
|
||||
});
|
||||
}
|
||||
var overlayDefs = {};
|
||||
@@ -759,6 +781,14 @@
|
||||
};
|
||||
applyFit();
|
||||
|
||||
// 圈了区域的专题:迷雾只在该圆内显示。必须**等视图定下来之后**再算裁剪圆 ——
|
||||
// 视图没定时 latLngToContainerPoint() 会抛「Set map center and zoom first.」,
|
||||
// 那行异常会把后面的图层添加一起打断(实测:pane 建好了、迷雾瓦片一张都没有)。
|
||||
if (applyFogClip) {
|
||||
map.on('move zoom viewreset resize', applyFogClip);
|
||||
applyFogClip();
|
||||
}
|
||||
|
||||
var st = state4(map, points, group, def.crs, true);
|
||||
st.clustered = true;
|
||||
st.pad = 0.15;
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
<div class="card">
|
||||
<h2>{{if .IsNew}}新建专题{{else}}编辑专题 #{{.Form.ID}}{{end}}</h2>
|
||||
<div class="card-sub">时间范围可精确到分钟;半径为 0 表示不限区域(时间范围内的地点画到哪算哪)。
|
||||
区域内的世界迷雾由地图按视野自动呈现,不需要单独圈选。</div>
|
||||
<div class="card-sub">时间范围可精确到分钟;半径为 0 表示不限区域(时间范围内的地点画到哪算哪)。</div>
|
||||
<form method="post" action="/admin/topics/save">
|
||||
{{if not .IsNew}}<input type="hidden" name="id" value="{{.Form.ID}}">{{end}}
|
||||
<div class="row">
|
||||
@@ -40,6 +39,10 @@
|
||||
<label class="check" style="margin:0"><input type="checkbox" name="show_fog" {{if .Form.ShowFog}}checked{{end}}> 展示世界迷雾</label>
|
||||
<label class="check" style="margin:0"><input type="checkbox" name="enabled" {{if .Form.Enabled}}checked{{end}}> 对外展示(取消勾选则访客看不到)</label>
|
||||
</div>
|
||||
<div class="muted" style="font-size:var(--fs-sm);margin-bottom:10px">
|
||||
填了中心与半径时,迷雾只在这个圆内显示;留空则显示全局迷雾。
|
||||
(迷雾是一份累积快照,没有时间维度,所以不收口到上面的时间范围。)
|
||||
</div>
|
||||
<button class="btn primary" type="submit">{{if .IsNew}}创建专题{{else}}保存修改{{end}}</button>
|
||||
{{if not .IsNew}}<a class="btn" href="/admin/topics">取消编辑</a>{{end}}
|
||||
</form>
|
||||
|
||||
@@ -82,6 +82,8 @@
|
||||
fitCircle: {{.FitCircleJSON}},
|
||||
fitBBox: {{.FitBBoxJSON}},
|
||||
showFog: {{if .Topic.ShowFog}}true{{else}}false{{end}},
|
||||
{{/* 圈了区域的专题:迷雾只在该圆内显示(rond-map.js 里给迷雾单独一个 pane 做 clip-path) */}}
|
||||
{{if .Topic.HasArea}}fogArea: {lat: {{.Topic.CenterLat}}, lon: {{.Topic.CenterLon}}, radiusKm: {{.Topic.RadiusKm}}},{{end}}
|
||||
back: '{{.BackQS}}'
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user