Compare commits
3
Commits
9cf3d89c3c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed3321c6e6 | ||
|
|
862160c537 | ||
|
|
6aee7e441b |
@@ -0,0 +1,49 @@
|
|||||||
|
# 2026-10-13
|
||||||
|
|
||||||
|
## 排查「专题里的圆大小不随缩放变化」(结论:未能复现,行为是对的)
|
||||||
|
|
||||||
|
用户反馈:专题里的圆无论怎么缩放,屏幕上的大小都一样;而且「新圆半径 25km 居然覆盖了整个南宁」。
|
||||||
|
库里实际存的是 3 个 `radiusKm: 20`(北海 / 南宁 22.815547,108.367767 / 柳州)。
|
||||||
|
|
||||||
|
四处实测(无头 CDP,全部换算回地面公里):
|
||||||
|
|
||||||
|
| 位置 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 编辑器 `L.circle`(半径 25) | z9=178px / z11=710px / z13=2842px(每级翻倍),反算始终 25 km,期望 177/711/2847 —— 吻合 |
|
||||||
|
| 编辑器「改半径」把手 vs 圆心 | 20.038 km(该圆半径 20 km)—— 位置准确 |
|
||||||
|
| 前台专题页的迷雾裁剪圆 | z7→z10 像素半径 17→141(应 142),换算始终 ≈20 km |
|
||||||
|
| 同一缩放改「新圆半径」 | 3→42px、10→142、25→356、50→710,与反算 43/142/355/711 一致 |
|
||||||
|
|
||||||
|
⇒ **三处画圆全都是地理锚定的、都随缩放正确变化,半径值也确实生效**,复现不出「大小不变」。
|
||||||
|
|
||||||
|
「20 km 覆盖整个南宁」是**对的**——用他自己的数据验证:圆心 20 km 内落了他 477 个地点中的
|
||||||
|
**45 个**,最远的是西乡塘的广西民族大学相思湖校区(14.5 km)与良庆的五象万象汇(10.2 km),
|
||||||
|
也就是从相思湖一直盖到五象新区,正是南宁建成区的跨度(约 20~25 km)。
|
||||||
|
20 km 半径 = 40 km 直径;只想圈市中心得用 5~8 km。
|
||||||
|
|
||||||
|
### 找到一处真实缺口
|
||||||
|
|
||||||
|
**后台编辑器的地图没有比例尺**(`L.control.scale`):`scaleBar=0`(前台专题页 `=1`)。
|
||||||
|
画圆时没有任何长度参照,很容易得出现「这个圆怎么这么大/怎么没变化」的判断 ——
|
||||||
|
大概率就是这次困惑的来源。待办:给它加比例尺;圆列表里把半径的后果写出来(如「20 公里(约 40 公里宽)」)。
|
||||||
|
|
||||||
|
### 排查时的两个自坑(记下来省时间)
|
||||||
|
|
||||||
|
- 地图与迷雾 pane 在 `load` 之后还要等 `/api/map-config` 回来才初始化:`--wait 5` 会拍到
|
||||||
|
「pane 不存在」,`--wait 8` 才稳。别把「还没初始化」当成「功能没生效」。
|
||||||
|
- `--eval-file` 里写 `(async function(){...})()` 却**不加 `--await`**,CDP 拿到的是 Promise,
|
||||||
|
`returnByValue` 序列化成 `{}` —— 看着像脚本没执行,其实是漏了参数。
|
||||||
|
|
||||||
|
### 处置:补「参照物」(提交 6aee7e4)
|
||||||
|
|
||||||
|
用户选定「加比例尺 + 标注直径」。几何逻辑**一行没动** —— 因为问题本来就不在几何:
|
||||||
|
|
||||||
|
- 编辑器地图加 `L.control.scale`(公制,`position:'bottomleft'`,跟前台一致)。
|
||||||
|
- 新增纯函数 `spanLabel(radiusKm)`:半径 →「约 40 公里宽」(直径);<1 公里改用米,
|
||||||
|
免得出现「约 0.1 公里宽」。圆列表每行标,**「新圆半径」旁边也实时跟着变**(落点前就能判断)。
|
||||||
|
- 守卫 `TestTopicAreaEditorGivesSizeReference` 钉住比例尺与跨度标注(变异验证过)。
|
||||||
|
⚠️ 它当场抓到我自己的疏漏:模板的 init 调用漏传 `spanId`,靠脚本默认值兜着 ——
|
||||||
|
已改成显式传参(**新增 opts 时记得在模板里也写出来**,否则守卫和外人都看不出这层耦合)。
|
||||||
|
|
||||||
|
教训:这次「bug」的本质是**界面缺参照物**,不是算错。以后遇到「大小看起来不对」类反馈,
|
||||||
|
先问「用户手上有什么可以量」,而不只是去验算几何。
|
||||||
@@ -1229,6 +1229,15 @@ HTML,一行里必然带 `</script>`,一拒就把正常用法挡死了(我
|
|||||||
小坑:半径输入框得套一层**定宽容器**(`.area-km-box`)——站点全局 input 规则是
|
小坑:半径输入框得套一层**定宽容器**(`.area-km-box`)——站点全局 input 规则是
|
||||||
`width:100%` 且带 10 个 `:not()`(特异性 (0,11,1)),直接给 input 写宽度压不过。
|
`width:100%` 且带 10 个 `:not()`(特异性 (0,11,1)),直接给 input 写宽度压不过。
|
||||||
|
|
||||||
|
⚠️ **给几何参数配「参照物」**:2026-10-13 用户反馈「圆无论怎么缩放大小都一样、20km 却覆盖了
|
||||||
|
整个南宁」,实测三处画圆(编辑器 `L.circle`、改半径把手、前台迷雾裁剪圆)**全是地理锚定、
|
||||||
|
随缩放正确变化**,半径值也确实生效 —— 复现不出「大小不变」。真正缺的是**参照物**:
|
||||||
|
编辑器地图以前没有比例尺(前台一直有),圈圆时只能靠地名猜。
|
||||||
|
⇒ 规矩:**编辑器地图必须有 `L.control.scale`,且半径旁要标出跨度**(`spanLabel()`:
|
||||||
|
半径 20 →「约 40 公里宽」;<1 公里改用米,别写「约 0.1 公里宽」)。
|
||||||
|
守卫 `TestTopicAreaEditorGivesSizeReference`。教训:遇到「大小看起来不对」,
|
||||||
|
先问「用户手上有什么可以量」,而不是只去验算几何。
|
||||||
|
|
||||||
⚠️ **`<input type="number">` 的 `step` 是相对 `min` 递增的**,不是「`step` 的整数倍」:
|
⚠️ **`<input type="number">` 的 `step` 是相对 `min` 递增的**,不是「`step` 的整数倍」:
|
||||||
`min=0.05` + `step=0.5` 的合法值只有 0.05/0.55/1.05…,用户填 3 或 3.5 会被 Chrome 判非法、
|
`min=0.05` + `step=0.5` 的合法值只有 0.05/0.55/1.05…,用户填 3 或 3.5 会被 Chrome 判非法、
|
||||||
提交时弹「请输入有效值。两个最接近的有效值分别为 2.55 和 3.05」。**这是浏览器原生提示,
|
提交时弹「请输入有效值。两个最接近的有效值分别为 2.55 和 3.05」。**这是浏览器原生提示,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module rond-with-you
|
module rond-with-you
|
||||||
|
|
||||||
go 1.25
|
go 1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgx/v5 v5.7.6
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
|
|||||||
@@ -1166,6 +1166,8 @@ label.f > span.span-actions { display: flex; align-items: center; justify-conten
|
|||||||
#area-map { height: 380px; border-radius: var(--r-md); background: var(--line-soft); }
|
#area-map { height: 380px; border-radius: var(--r-md); background: var(--line-soft); }
|
||||||
.area-tools { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin: 10px 0; }
|
.area-tools { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin: 10px 0; }
|
||||||
.area-tools #area-hint { flex: 1 1 240px; font-size: var(--fs-sm); }
|
.area-tools #area-hint { flex: 1 1 240px; font-size: var(--fs-sm); }
|
||||||
|
/* 半径旁边的「约 N 公里宽」:不许折行,否则圆列表每行高度会跳 */
|
||||||
|
.area-span { white-space: nowrap; font-size: var(--fs-sm); }
|
||||||
#area-hint.err { color: var(--err-fg); }
|
#area-hint.err { color: var(--err-fg); }
|
||||||
#area-list { margin-top: 4px; }
|
#area-list { margin-top: 4px; }
|
||||||
#area-list:empty { display: none; }
|
#area-list:empty { display: none; }
|
||||||
|
|||||||
@@ -86,6 +86,16 @@
|
|||||||
return km / (111.32 * c);
|
return km / (111.32 * c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 半径的「人话版」跨度:半径 20 公里 ⇒「约 40 公里宽」。
|
||||||
|
// 光写「半径 20 公里」很容易被读小 —— 直径才是眼睛看到的那个宽度。
|
||||||
|
function spanLabel(radiusKm) {
|
||||||
|
var d = num(radiusKm) * 2;
|
||||||
|
if (!isFinite(d) || d <= 0) return '';
|
||||||
|
if (d < 1) return '约 ' + Math.round(d * 1000) + ' 米宽';
|
||||||
|
if (d < 10) return '约 ' + (Math.round(d * 10) / 10) + ' 公里宽';
|
||||||
|
return '约 ' + Math.round(d) + ' 公里宽';
|
||||||
|
}
|
||||||
|
|
||||||
// 两点距离(公里),球面公式;拖把手时用它反推半径
|
// 两点距离(公里),球面公式;拖把手时用它反推半径
|
||||||
function kmBetween(a, b) {
|
function kmBetween(a, b) {
|
||||||
var dLat = (b[0] - a[0]) * Math.PI / 180, dLon = (b[1] - a[1]) * Math.PI / 180;
|
var dLat = (b[0] - a[0]) * Math.PI / 180, dLon = (b[1] - a[1]) * Math.PI / 180;
|
||||||
@@ -99,7 +109,8 @@
|
|||||||
MAX_AREAS: MAX_AREAS, MIN_KM: MIN_KM, MAX_KM: MAX_KM,
|
MAX_AREAS: MAX_AREAS, MIN_KM: MIN_KM, MAX_KM: MAX_KM,
|
||||||
normalize: normalize, validate: validate, serialize: serialize, parse: parse,
|
normalize: normalize, validate: validate, serialize: serialize, parse: parse,
|
||||||
add: add, remove: remove, setCenter: setCenter, setRadius: setRadius,
|
add: add, remove: remove, setCenter: setCenter, setRadius: setRadius,
|
||||||
latDelta: latDelta, lonDelta: lonDelta, kmBetween: kmBetween
|
latDelta: latDelta, lonDelta: lonDelta, kmBetween: kmBetween,
|
||||||
|
spanLabel: spanLabel
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- 以下需要 DOM 与 Leaflet ----------
|
// ---------- 以下需要 DOM 与 Leaflet ----------
|
||||||
@@ -126,6 +137,7 @@
|
|||||||
var listBox = document.getElementById(opts.listId || 'area-list');
|
var listBox = document.getElementById(opts.listId || 'area-list');
|
||||||
var hint = document.getElementById(opts.hintId || 'area-hint');
|
var hint = document.getElementById(opts.hintId || 'area-hint');
|
||||||
var radiusIn = document.getElementById(opts.radiusId || 'area-new-km');
|
var radiusIn = document.getElementById(opts.radiusId || 'area-new-km');
|
||||||
|
var newSpan = document.getElementById(opts.spanId || 'area-new-span');
|
||||||
var saveBtn = form.querySelector('button[type=submit]');
|
var saveBtn = form.querySelector('button[type=submit]');
|
||||||
if (!hidden || !listBox) return;
|
if (!hidden || !listBox) return;
|
||||||
|
|
||||||
@@ -149,6 +161,9 @@
|
|||||||
L.tileLayer(def.url, {
|
L.tileLayer(def.url, {
|
||||||
subdomains: subs, maxZoom: def.maxZoom || 18, minZoom: 3, attribution: def.attr || ''
|
subdomains: subs, maxZoom: def.maxZoom || 18, minZoom: 3, attribution: def.attr || ''
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
// 比例尺是必须的:没有它,圈圆时手上没有任何长度参照,只能靠地名猜,
|
||||||
|
// 「20 公里到底多大」就变成了拍脑袋(前台的地图一直有,这里以前漏了)。
|
||||||
|
L.control.scale({ imperial: false, position: 'bottomleft' }).addTo(map);
|
||||||
if (crsIn) crsIn.value = def.crs || 'gcj02';
|
if (crsIn) crsIn.value = def.crs || 'gcj02';
|
||||||
|
|
||||||
var layer = L.layerGroup().addTo(map);
|
var layer = L.layerGroup().addTo(map);
|
||||||
@@ -164,6 +179,11 @@
|
|||||||
if (!isFinite(v) || v < MIN_KM || v > MAX_KM) return 3;
|
if (!isFinite(v) || v < MIN_KM || v > MAX_KM) return 3;
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
// 「新圆半径」旁边实时显示它会画多大:落点之前就能判断合不合适
|
||||||
|
function syncNewSpan() {
|
||||||
|
if (newSpan) newSpan.textContent = spanLabel(newRadius());
|
||||||
|
}
|
||||||
|
if (radiusIn) radiusIn.addEventListener('input', syncNewSpan);
|
||||||
|
|
||||||
// 整块重画:圆最多 20 个,重建比增量对齐简单得多,也不会漏掉「删了中间一个」的下标错位
|
// 整块重画:圆最多 20 个,重建比增量对齐简单得多,也不会漏掉「删了中间一个」的下标错位
|
||||||
function redraw() {
|
function redraw() {
|
||||||
@@ -222,6 +242,7 @@
|
|||||||
hidden.value = serialize(areas);
|
hidden.value = serialize(areas);
|
||||||
redraw();
|
redraw();
|
||||||
renderList();
|
renderList();
|
||||||
|
syncNewSpan();
|
||||||
var msg = validate(areas);
|
var msg = validate(areas);
|
||||||
if (hint) {
|
if (hint) {
|
||||||
if (msg) {
|
if (msg) {
|
||||||
@@ -289,6 +310,12 @@
|
|||||||
lab.appendChild(unit);
|
lab.appendChild(unit);
|
||||||
row.appendChild(lab);
|
row.appendChild(lab);
|
||||||
|
|
||||||
|
// 「半径 20 公里」旁边补一句直径 —— 眼睛量的是宽度,光看半径容易读小
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.className = 'muted area-span';
|
||||||
|
span.textContent = spanLabel(a.radiusKm);
|
||||||
|
row.appendChild(span);
|
||||||
|
|
||||||
var go = document.createElement('button');
|
var go = document.createElement('button');
|
||||||
go.type = 'button';
|
go.type = 'button';
|
||||||
go.className = 'btn sm';
|
go.className = 'btn sm';
|
||||||
|
|||||||
@@ -124,6 +124,22 @@ console.log('=== 几何换算 ===');
|
|||||||
'半径换算自洽:按 lonDelta 摆的把手量回来仍是 ' + km + ' 公里');
|
'半径换算自洽:按 lonDelta 摆的把手量回来仍是 ' + km + ' 公里');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
console.log('=== 半径的「人话版」跨度(直径)===');
|
||||||
|
(function () {
|
||||||
|
eq(api.spanLabel(20), '约 40 公里宽', '半径 20 → 约 40 公里宽');
|
||||||
|
eq(api.spanLabel(25), '约 50 公里宽', '半径 25 → 约 50 公里宽');
|
||||||
|
eq(api.spanLabel(3.5), '约 7 公里宽', '半径 3.5 → 约 7 公里宽');
|
||||||
|
eq(api.spanLabel(0.5), '约 1 公里宽', '半径 0.5 → 约 1 公里宽');
|
||||||
|
eq(api.spanLabel(2000), '约 4000 公里宽', '大半径不带小数');
|
||||||
|
// 小于 1 公里改用米,否则会写成「约 0.1 公里宽」这种别扭说法
|
||||||
|
eq(api.spanLabel(0.05), '约 100 米宽', '半径 0.05 → 约 100 米宽');
|
||||||
|
eq(api.spanLabel(0.3), '约 600 米宽', '半径 0.3 → 约 600 米宽');
|
||||||
|
ok(api.spanLabel(0) === '', '半径 0 → 空串(调用方直接不显示)');
|
||||||
|
ok(api.spanLabel(-1) === '', '负半径 → 空串');
|
||||||
|
ok(api.spanLabel(NaN) === '', 'NaN → 空串');
|
||||||
|
ok(api.spanLabel('abc') === '', '非数字 → 空串');
|
||||||
|
})();
|
||||||
|
|
||||||
console.log('\n=== 汇总 ===');
|
console.log('\n=== 汇总 ===');
|
||||||
console.log(' pass=' + pass + ' fail=' + fail);
|
console.log(' pass=' + pass + ' fail=' + fail);
|
||||||
process.exit(fail === 0 ? 0 : 1);
|
process.exit(fail === 0 ? 0 : 1);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
{{/* step 必须是 any:它是相对 min 递增的,min=0.05 + step=0.5 的合法值只有
|
{{/* step 必须是 any:它是相对 min 递增的,min=0.05 + step=0.5 的合法值只有
|
||||||
0.05/0.55/1.05…,用户填 3 或 3.5 会被浏览器判非法并弹原生提示。 */}}
|
0.05/0.55/1.05…,用户填 3 或 3.5 会被浏览器判非法并弹原生提示。 */}}
|
||||||
<input type="number" id="area-new-km" min="0.05" max="2000" step="any" value="3"></label>
|
<input type="number" id="area-new-km" min="0.05" max="2000" step="any" value="3"></label>
|
||||||
|
<span class="muted area-span" id="area-new-span"></span>
|
||||||
<button class="btn" type="button" id="area-clear">清除全部</button>
|
<button class="btn" type="button" id="area-clear">清除全部</button>
|
||||||
<span class="muted" id="area-hint"></span>
|
<span class="muted" id="area-hint"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,6 +111,7 @@
|
|||||||
window.rondTopicAreas.init({
|
window.rondTopicAreas.init({
|
||||||
mapId: 'area-map', formId: 'topic-form', listId: 'area-list',
|
mapId: 'area-map', formId: 'topic-form', listId: 'area-list',
|
||||||
hintId: 'area-hint', radiusId: 'area-new-km', clearId: 'area-clear',
|
hintId: 'area-hint', radiusId: 'area-new-km', clearId: 'area-clear',
|
||||||
|
spanId: 'area-new-span',
|
||||||
home: {{if .MapHomeJSON}}{{.MapHomeJSON}}{{else}}null{{end}}
|
home: {{if .MapHomeJSON}}{{.MapHomeJSON}}{{else}}null{{end}}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -572,3 +572,36 @@ func TestTopicAreaRadiusInputAllowsAnyStep(t *testing.T) {
|
|||||||
t.Error("topic-areas.js 里半径输入框的 step 别写死数字(见 step 相对 min 递增那条)")
|
t.Error("topic-areas.js 里半径输入框的 step 别写死数字(见 step 相对 min 递增那条)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编辑器必须给出长度参照:地图上要有比例尺,半径旁边要标出「约多宽」。
|
||||||
|
//
|
||||||
|
// 起因:2026-10-13 用户反馈「这个圆无论怎么缩放大小都一样,20km 却覆盖了整个南宁」。
|
||||||
|
// 实测三处画圆(编辑器 L.circle、改半径把手、前台迷雾裁剪)全都是地理锚定、随缩放正确
|
||||||
|
// 变化的,半径值也确实生效 —— 真正缺的是**参照物**:编辑器地图以前没有比例尺
|
||||||
|
// (前台专题页一直有),圈圆时只能靠地名猜,自然觉得「不对」。
|
||||||
|
// 所以这条守卫钉的是「别再把这个参照物弄丢」。
|
||||||
|
func TestTopicAreaEditorGivesSizeReference(t *testing.T) {
|
||||||
|
js, err := staticFS.ReadFile("static/js/topic-areas.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取 topic-areas.js 失败: %v", err)
|
||||||
|
}
|
||||||
|
src := string(js)
|
||||||
|
if !strings.Contains(src, "L.control.scale(") {
|
||||||
|
t.Error("topic-areas.js 必须给编辑器地图加比例尺(L.control.scale)—— " +
|
||||||
|
"没有它,圈圆时没有任何长度参照,「20 公里到底多大」只能拍脑袋")
|
||||||
|
}
|
||||||
|
if !strings.Contains(src, "spanLabel") {
|
||||||
|
t.Error("topic-areas.js 必须在半径旁标出跨度(spanLabel)")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := tmplFS.ReadFile("templates/admin/topics.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取 admin/topics.html 失败: %v", err)
|
||||||
|
}
|
||||||
|
body := string(b)
|
||||||
|
for _, need := range []string{`id="area-new-span"`, "spanId"} {
|
||||||
|
if !strings.Contains(body, need) {
|
||||||
|
t.Errorf("admin/topics.html 里缺 %q —— 「新圆半径」旁边要显示它会有多宽", need)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user