diff --git a/.workbuddy/memory/2026-10-12.md b/.workbuddy/memory/2026-10-12.md new file mode 100644 index 0000000..d1a5b25 --- /dev/null +++ b/.workbuddy/memory/2026-10-12.md @@ -0,0 +1,48 @@ +# 2026-10-12 + +## 专题的「迷雾显示区域」改成一组圆(多圆 + 地图点选) + +用户提出两点:① 显示区不该只有一个圆,应该能圈多个;② 应该在**地图上点选**, +而不是让他手填经纬度和半径。 + +### 方案(先出清单获批,分批落地) + +先摸清了现状:单圆硬编码在 6 处(`topics` 三列 / `HasArea` / `topicAreaCond` / +`fitParams` / 后台三个输入框 / `topic.html` 的 `fogArea`)。同时发现项目里**已经有** +一套现成的地图选圆交互(`/admin/fog-mask` 那个页面:点选圆心 + 拖拽 + 半径可调 + +多圆列表),可以直接抄它的模式。而 `fog_masks` 是**另一个语义**(圈内迷雾对访客隐藏), +用户明确这轮不动它。 + +三个岔路口问过用户,都选了推荐项:只改专题(不做站点级)、后台做成**专题表单内嵌地图** +(不是独立管理页)、不动遮罩区。 + +原计划 B1(存 areas)/B2(查询认多圆)之间会留一个「存了但只认第一个圆」的陷阱状态, +所以合并成一批:数据层与查询层一起做,后台先用兼容桥(仍手填单圆,落进 areas)。 + +### 落地(三个提交) + +- `1bb35db` 后端与查询层:`topics.areas` jsonb 数组 + 存量回填;`Topic.AreaList()` + 归一(改为空格时退回老的三列,未回填的老行与直接构造 Topic 的调用方都继续工作); + `topicAreaCond` 改成 N 个圆的 OR;`fitParams` 改用所有圆的并集外接矩形。 +- `44763df` 前台渲染:`clip-path` 从 `circle()` 换成 `path()`(多子路径、同绕向 ⇒ + nonzero 下自然并集);拼路径抽成 `fog-clip.js` 以便跑 node 单测;顺带清掉已死的 + `fitCircle`;`verify.sh` 挂进前端单测。 +- 待办(B3):**后台地图编辑器**还没做(专题表单目前仍是三个手填输入框)—— + 这是用户最初诉求的第二半,下一批做。 + +### 关键取舍 + +- **为什么不单开一张表**:圆是专题定义的组成部分、与专题一起读写、没有独立生命周期; + 单开表还要过一遍「能不能从备份重导」的归属口径、改 `dataset_ownership_test` 白名单、 + 注意迁移块顺序。jsonb 一次全绕开。 +- **废弃列保留并镜像第一个圆**:回滚旧二进制时退化成「第一个圆」总比「没有区域」强。 +- **`fitCircle` 删掉**:多圆表达不了「圆心 + 半径」,留着就是两套并行的贴合口径。 + +### 只在这轮踩到的坑 + +- **CSSOM 会把 `clip-path` 规范化**:`style.clipPath` 读回来被补上空格 + (`path("M 405 296 a 87 87 …")`),且位置不固定(`296 a87`)、第一个子路径尾部 + 还可能多个空格。想解析回几何必须每处分隔都用 `\s*`/`\s+` 容忍,别写死 —— + 我连着写错两版正则(先是 `Z$` 卡在尾部空格)。 +- **`--clip` 截图的 `pad=10`**(×scale)又咬了一次:这次用**自校准**绕开的 —— + 用单圆掩码反解出 scale 与 offset(实测 2.0 / 19.2),不再手算元素坐标。 diff --git a/.workbuddy/memory/MEMORY-details.md b/.workbuddy/memory/MEMORY-details.md index 486d910..f9d2238 100644 --- a/.workbuddy/memory/MEMORY-details.md +++ b/.workbuddy/memory/MEMORY-details.md @@ -1133,3 +1133,78 @@ HTML,一行里必然带 ``,一拒就把正常用法挡死了(我 + +## 四十六、专题显示区:一组圆 + 地图点选(2026-10-12) + +**语义变了**:专题的「区域」从**一个圆**变成**一组圆**(并集——落在任一圆内即算在区域内)。 +起因是用户提的两点:不该只有一个圆;应该在**地图上点选**而不是手填经纬度和半径。 +(原管理页那句「不需要单独圈选」是文档与实现不一致,见四十五。) + +### 存储:`topics.areas JSONB`,不单开表 + +`areas` 是 `[{lat, lon, radiusKm}, ...]`,坐标 **GCJ-02**(地点过滤是拿它跟 +`places.lat/lon` 直接比球面距离的)。 + +为什么用 jsonb 而不是一张 `topic_areas` 表:这些圆是专题定义的组成部分、与专题一起 +读写、**没有独立生命周期**;单开表还要过一遍「能不能从备份重导」的归属口径、改 +`dataset_ownership_test.go` 的表白名单、注意迁移块必须排在建表之后 —— jsonb 一次全绕开。 + +- 存量回填在 schema.sql 里:`UPDATE topics SET areas = jsonb_build_array(...) WHERE radius_km > 0 AND areas = '[]'`。 +- ⚠️ `center_lat/center_lon/radius_km` **已废弃但保留**,写入时镜像第 1 个圆:回滚旧二进制时 + 退化成「第一个圆」总比「没有区域」强。判据是三列的注释,别当成还有用的列。 +- ⚠️ **出库值必须是 `[]` 不是 `null`**:列上有 NOT NULL,而 `json.Marshal(nil slice)` 恰好给 + `"null"`。用 `topicAreasColumn()` 收敛这一处。 +- `Topic.AreaList()` 做归一:`Areas` 为空时退回老的三列 ⇒ 未回填的老行与**测试里直接构造 + Topic 的写法**都继续工作(`topic_stats_test.go` 就是这么写的)。`HasArea()`/`AreasJSON()` + 都走它,保证「HasArea 为真」与「脚本拿得到圆」永远是同一份判据。 + +### 查询:N 个圆的 OR + +`topicAreaCond` 每个圆占 **3 个占位符**(纬度、经度、半径),条件用 ` AND (... OR ...)`。 +⚠️ 占位符纪律照旧(写错就是「专题内容全变 0」那种难查的事故)。判据写成**定义性不变量**: +本段引用的 `$n` 必须**恰好**是 `[prefix+1, len(args)]` 这个连续区间,且最大值 == `len(args)` +(列表页那条 UNION ALL 是靠 `len(args)` 自己算编号的,错位就靠这个抓)。注意断言的是 +「本段自己那 3n 个」,前缀那几个由查询别处引用,**不该**出现在区域条件里。 + +### 贴合:`fitCircle` 已删除,统一 `fitBBox` + +多圆之后「圆心 + 半径」表达不了,留着 `fitCircle` 就是两套并行的贴合口径。`fitParams` +改为返回所有圆的并集外接矩形(`topicAreasBounds`,高纬处夹一下余弦)—— +单圆时外接矩形正是那个圆的外接正方形,与原路径等价。 + +### 前台:`clip-path: path()`(circle() 只表达得了一个圆) + +- 每个圆一段圆弧子路径、**同绕向(sweep=0)** ⇒ nonzero 填充规则下多段自然取并集, + 重叠处**不会**互相挖洞。绕向写反的话重叠区会被抵消成一个洞。 +- 拼路径抽成独立文件 `static/js/fog-clip.js`(`rond-map.js` 是个大 IIFE,函数取不出来, + 抽出来才能跑 node 单测);⚠️ layout 里**必须排在 rond-map.js 之前**加载。 +- 给迷雾单独 pane 再裁它(不能裁 `tilePane`,底图在里面);注册时机仍要在 `applyFit()` + 之后(视图未定时 `latLngToContainerPoint()` 会抛错并把后续图层添加一起打断,见四十五)。 +- `topic.html` 的 `fogAreas` **无条件下发**(空数组 = 不限区域),别包在 `{{if}}` 里 —— + 条件分支是「有区域忘了新增分支」这类漏传的温床。 +- ⚠️ **CSSOM 会把 `clip-path` 规范化**:`style.clipPath` 读回来会被补空格 + (`path("M 405 296 a 87 87 …")`),而且空格位置不固定(可能出现 `296 a87`)、 + 第一个子路径尾部还可能多一个空格。任何「解析回几何」的代码必须每处分隔都用 + `\s*`/`\s+` 容忍 —— 写死就等着被咬(本轮连错两版正则)。 + +### 前端单测接进 `verify.sh` + +`internal/web/static/js/*.test.js` 以前只靠人记得手动跑 = 等于没有。现在挂在 +「第 1.5 段」,且**找不到 node 直接失败而不是跳过** —— 跳过会让验收看着全绿、 +实际一行前端脚本都没验。 + +### 验证方法:自校准(别再手算元素坐标) + +`site_cdp.py --clip` 会把裁切框每边外扩 `pad=10` CSS px(×`--scale`),所以 +「CSS 容器坐标 → 图像像素」不能直接换算。做法是:先拍一张**只画一个圆**的品红探针图, +用已知圆心/半径反解出 `scale` 与 `offset`(实测 2.0 / ~19.2),再用它给别处定标。 +两张图反解出的 scale/offset 必须互相吻合,否则说明取景变了、比较无意义。 + +本轮实测(两圆,重叠):A 独有点只有 A 亮、B 独有点只有 B 亮、**重叠中心两圆都亮且 +并集也亮(无洞)**,整体集合差异 0.013%;迷雾掩码 86,776 px 按并集解析式判**圆外 0 个**, +两个圆内分别 52,609 / 46,757 px ⇒ 第二个圆真的被认了。 + +### 待办 + +后台的**地图点选编辑器**(专题表单内嵌地图、多圆增删拖拽)还没做 —— 这是用户诉求的 +第二半,专题表单目前仍是三个手填输入框(走兼容桥:合成一个圆写进 areas)。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 3d7a71a..6267421 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -1,6 +1,6 @@ # rond-with-you 硬规矩(索引) -> 全文:`.workbuddy/memory/MEMORY-details.md`(46 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。 +> 全文:`.workbuddy/memory/MEMORY-details.md`(47 节);验证机制:技能 `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,7 +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`(四十五) +- **专题显示区=一组圆(并集)**:存 `topics.areas` jsonb(不单开表,省掉归属口径/白名单/迁移顺序三件事),SQL 是 N 个圆的 **OR**(每圆 3 个占位符,编号须恰好覆盖 `[prefix+1, len(args)]`)。前台裁剪=**单独 pane**(不能裁 `tilePane`,底图在里面)+ `clip-path: path()`,每圆一段**同绕向**子路径(绕向反了重叠处会被挖成洞);圆心须 `project()`;⚠️ 注册必须排在 `applyFit()` **之后** —— 视图未定时 `latLngToContainerPoint()` 会抛「Set map center and zoom first.」,把后续图层添加一起打断(实测=迷雾整片消失、不报错)。`TestTopicFogIsClippedToAreas`(四十五、四十六) ## 前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D、卅七) - ⚠️ **对访客公开的块,措辞必须按身份分支**(`{{if .IsAdmin}}我的…{{else}}站长…{{end}}`): @@ -40,7 +40,7 @@ - 改 Go 必须重启;模板/CSS/JS 只在 `debug=true` 热加载(`AssetV` 启动哈希)。⚠️ **新模板+旧二进制**=「渲染到一半仍 200」;启动自检 `smokeRender` 会拒启(四十一)。 - 字阶/圆角/阴影**只走令牌**,裸 `font-size: 13px` 有静态守卫 `TestNoBareFontSizePx`;卡片三档 `.card--hero/--quiet`。⚠️ 改**根字号**要查 Leaflet(`.75rem` 会跟着漂),(四十二)。 - 类型色走 `csscolor/csssoft`(**必须返回 `template.CSS`**,否则 `var()` 被换成 `ZgotmplZ` 且不报错);这套色连 3:1 都不到,**只能当圆点标记、别当文字色**。窄屏导航收进 `.nav-menu`,顶栏 `--z-nav` 必须高于地图浮层(四十三)。 -- 日期控件一律走 `attachAll`(原生 `type=date` 的年段会吞 6 位数字 ⇒ 敲 20260606 变 `202606/06` 且值为空);模板里出现 `input[type=date]` 选择器有守卫。`.grid` 必须 `align-items:start`(四十四)。 +- 日期控件一律走 `attachAll`(原生 `type=date` 的年段会吞 6 位数字 ⇒ 敲 20260606 变 `202606/06` 且值为空);模板里出现 `input[type=date]` 选择器有守卫。`.grid` 必须 `align-items:start`(四十四)。前端脚本单测(`static/js/*.test.js`)已挂进 verify.sh「第 1.5 段」,**node 缺失直接失败不跳过**;可测的纯函数要抽成独立文件(`rond-map.js` 是大 IIFE,函数取不出来)。(四十六) - 拼查询串走返回 `template.URL` 的方法(`PlaceLink`/`EditPlaceLink`/`MapLink`/`EditLink`),勿手工转义。 - 窄屏覆盖写 `app.css` **末尾那块**;宽表堆叠类名**成对** `class="tb tb-stack"`;触控 44px。`tb_stack_test.go` - 加表格列:`PlaceCols`/`scanPlace` **成对**、新列追加最后;CJK 挤换行只抬该表 `min-width`。