高德地址联想:下拉候选只显示地点名,需在地点名后补详细地址供用户区分 #32

Closed
opened 2026-10-29 23:32:38 +00:00 by laoyang · 2 comments
Owner

现象 / 诉求

用户原话(2026-10-30):

在所有调用高德显示详细地址的地方,也就是当我输入一个简单地址、下拉显示详细地址时,
不能只把地点告诉我,一定要把详细地址也放在后面。
因为如果只是把地址给我,同一个地址可能有好几个,我根本不知道应该选哪一个。
所以应该把详细地址也放在后面,作为参考,让用户可以更好地选择。

即:地址联想的下拉候选行,目前只有地点名。同名地点(如多个「星巴克」「万象城」,
或「青岛大学」的多个校区)在候选里长得一模一样,用户无法判断该选哪一个。
要求每个候选行带上详细地址作为参考。

现状

地址联想走全站共用模块 assets/js/addr_suggest.js(高德 JS API AMap.AutoComplete),
共 4 处调用:

位置 配置
templates/calendar/_event_fields.php 日程地点 data-addr-detail="1"
templates/movies/cinemas.php 影院名 data-addr-fill="place,address"
templates/movies/form.php 新增影院 data-addr-fill="place,address"
templates/trips/_loc_field.php 行程出发/到达 data-addr-fill="address,lng,lat,city"

关键:候选下拉是 new AMap.AutoComplete({ input }) 之后由高德 SDK 自己渲染的,
我们没有任何控制权 —— 它只画 name(部分条目带灰字地址,但残缺且不可控)。
所以"补详细地址"不能靠改模板,必须接管下拉渲染。

另有一个已存在的补偿机制:data-addr-detail="1" 的输入框在选中后
会把 名字 · 门牌 写进 value,并在框下显示一行「高德:省市区 · 门牌」。
但那发生在选中之后,解决不了"选中之前分不清候选"的问题。

方案

把 AMap.AutoComplete 只当查询器用,不用它的内置下拉:

  1. 构造 new AMap.AutoComplete(options) 时不传 input ⇒ 高德不再自建下拉;
  2. 自己接管 input 的输入事件(防抖),调 ac.search(keyword, cb) 拿 result.tips;
  3. 用自建 <ul> 渲染每个候选:地点名 + 省市区 + 详细地址,
    地址在后、灰色小字,作为区分依据;
  4. 点击候选 → 复用现有全部逻辑(data-addr-fill 回填、data-addr-detail、
    showAddrHint、fb-addr-picked 事件),行为与原来完全一致。

必须原样保留的既有行为(这份文件里有多次真机踩坑记录):

  • data-addr-bound 幂等标记,且只在真的绑定成功后才打
  • data-addr-for 用 closest 取最近祖先(行程 from/to 不能串)
  • 只填空白字段,不覆盖用户手写内容
  • SDK / 插件未就绪时优雅降级为普通文本框(手敲照常保存)
  • readPlace 的三套字段兜底(addressComponent / pname+cityname+adname / district)
  • 懒加载(layout.php 的 [data-addr] 判据)不变

验收标准

  1. 4 处地址输入框,输入关键字后下拉候选每一行都能看到详细地址
  2. 同名地点可凭地址区分
  3. 点击候选后:输入框内容、附带字段回填、框下提示、fb-addr-picked 事件均与改动前一致
  4. 高德 SDK 未加载时,输入框仍是普通文本框,手敲可保存
  5. 行程表单 from/to 两组字段不串(出发地地址不会写到到达地)
  6. 部署后线上冒烟通过

关联

  • 与 #30 / #31 无关(那是鸿蒙端与编译环境),本单是网页端
  • 涉及 assets/js/addr_suggest.js(全站一份),改一处覆盖 4 个入口

为什么单独建单

这是网页端地址联想下拉的显示问题(改 assets/js/addr_suggest.js),与 #30 的 WSL 编译环境、#31 的鸿蒙端日程编辑权限都无关:改的仓库、文件、验证方式(浏览器/线上)完全不同。

## 现象 / 诉求 用户原话(2026-10-30): > 在所有调用高德显示详细地址的地方,也就是当我输入一个简单地址、下拉显示详细地址时, > 不能只把地点告诉我,一定要把详细地址也放在后面。 > 因为如果只是把地址给我,同一个地址可能有好几个,我根本不知道应该选哪一个。 > 所以应该把详细地址也放在后面,作为参考,让用户可以更好地选择。 即:**地址联想的下拉候选行,目前只有地点名**。同名地点(如多个「星巴克」「万象城」, 或「青岛大学」的多个校区)在候选里长得一模一样,用户无法判断该选哪一个。 要求每个候选行带上**详细地址**作为参考。 ## 现状 地址联想走全站共用模块 `assets/js/addr_suggest.js`(高德 JS API `AMap.AutoComplete`), 共 4 处调用: | 位置 | 配置 | |---|---| | `templates/calendar/_event_fields.php` 日程地点 | `data-addr-detail="1"` | | `templates/movies/cinemas.php` 影院名 | `data-addr-fill="place,address"` | | `templates/movies/form.php` 新增影院 | `data-addr-fill="place,address"` | | `templates/trips/_loc_field.php` 行程出发/到达 | `data-addr-fill="address,lng,lat,city"` | **关键**:候选下拉是 `new AMap.AutoComplete({ input })` 之后**由高德 SDK 自己渲染**的, 我们没有任何控制权 —— 它只画 `name`(部分条目带灰字地址,但残缺且不可控)。 所以"补详细地址"不能靠改模板,必须**接管下拉渲染**。 另有一个已存在的补偿机制:`data-addr-detail="1"` 的输入框在**选中后** 会把 `名字 · 门牌` 写进 value,并在框下显示一行「高德:省市区 · 门牌」。 但那发生在**选中之后**,解决不了"选中之前分不清候选"的问题。 ## 方案 把 `AMap.AutoComplete` 只当**查询器**用,不用它的内置下拉: 1. 构造 `new AMap.AutoComplete(options)` 时**不传 `input`** ⇒ 高德不再自建下拉; 2. 自己接管 input 的输入事件(防抖),调 `ac.search(keyword, cb)` 拿 `result.tips`; 3. 用自建 `<ul>` 渲染每个候选:**地点名 + 省市区 + 详细地址**, 地址在后、灰色小字,作为区分依据; 4. 点击候选 → 复用现有全部逻辑(`data-addr-fill` 回填、`data-addr-detail`、 `showAddrHint`、`fb-addr-picked` 事件),行为与原来完全一致。 必须原样保留的既有行为(这份文件里有多次真机踩坑记录): - `data-addr-bound` 幂等标记,且**只在真的绑定成功后**才打 - `data-addr-for` 用 `closest` 取最近祖先(行程 from/to 不能串) - 只填**空白**字段,不覆盖用户手写内容 - SDK / 插件未就绪时优雅降级为普通文本框(手敲照常保存) - `readPlace` 的三套字段兜底(`addressComponent` / `pname`+`cityname`+`adname` / `district`) - 懒加载(`layout.php` 的 `[data-addr]` 判据)不变 ## 验收标准 1. 4 处地址输入框,输入关键字后下拉候选**每一行都能看到详细地址** 2. 同名地点可凭地址区分 3. 点击候选后:输入框内容、附带字段回填、框下提示、`fb-addr-picked` 事件均与改动前一致 4. 高德 SDK 未加载时,输入框仍是普通文本框,手敲可保存 5. 行程表单 from/to 两组字段不串(出发地地址不会写到到达地) 6. 部署后线上冒烟通过 ## 关联 - 与 #30 / #31 无关(那是鸿蒙端与编译环境),本单是**网页端** - 涉及 `assets/js/addr_suggest.js`(全站一份),改一处覆盖 4 个入口 --- ### 为什么单独建单 这是网页端地址联想下拉的显示问题(改 assets/js/addr_suggest.js),与 #30 的 WSL 编译环境、#31 的鸿蒙端日程编辑权限都无关:改的仓库、文件、验证方式(浏览器/线上)完全不同。
Author
Owner

已实现并部署,等你点验

commit df657e5(已推送 42aff02..df657e5),只改 assets/js/addr_suggest.js 一个文件(+215/-5)。
线上文件已更新并逐字节校验一致(两侧 sha256 8df38af4…)。

效果

下拉候选从"只有地名"变成两行:

星巴克(万象城店)
山东省青岛市市南区 香港中路 88 号万象城 L1
─────────────────────────────────
星巴克(万象城店)
山东省青岛市市北区 山东路 6 号甲 B1
─────────────────────────────────
青岛市崂山区
山东省青岛市

主标题 14px 深色、副标题 12px 灰色 rgb(138,138,142) —— 层级拉开,
扫一眼就能分清同名地点。

改法

原来用 AMap.AutoComplete 的内置下拉,它只画 name,我们没有任何控制权。
所以改成「高德只当查询器、下拉自己画」:

  1. new AMap.AutoComplete({}) —— 不传 input,高德就不再往 DOM 里塞它自己的面板;
  2. 输入 → 防抖 260ms → ac.search(kw, cb) → 拿 result.tips;
  3. 自己渲染每一行:地名 + 省市区 + 详细地址。

选中之后的全部既有行为原样保留,只是入口从高德的 select 事件换成自建候选的点击:
data-addr-fill 字段回填、data-addr-detail 写门牌、框下 .fb-addr-hint 提示、
fb-addr-picked 事件。所以 4 处调用(日程地点 / 影院名 ×2 / 行程出发到达)
不用改模板就同时生效。

几处刻意的取舍(都是踩过或预判到的坑)

决定 理由
面板挂 body + position:fixed 挂输入框父节点会被 .mv-row 的「标签 62px + 控件」两列网格挤掉;也会被子表单弹框的 overflow:hidden 裁掉
面板量不到尺寸时只跳过定位、不收起 原来写了"量不到就收起",结果候选查到了却一显示就消失(实测踩到,已经改掉)
少于 2 字不查,且收起面板 一个字的候选又多又没用,还白烧高德配额;留着旧候选会被误点
候选行用 textContent 逐个填,不拼 HTML 地点名由高德返回、可能含 < &,拼 HTML 会破版甚至注入
点候选用 mousedown 而非 click click 会先让输入框失焦触发收起,候选点不中
只填空白字段(沿用原逻辑) 用户手写过更细的门牌不该被覆盖

验证

① jsdom 单元测试 27 项全通过(/tmp/addrtest/test_addr.js)

覆盖:候选带地址、同名可区分、行政区划用 district 兜底、选中回填、
fb-addr-picked 事件、1 字不查/2 字才查、行程 from/to 不串、
SDK 未就绪降级为普通文本框、插件晚到后自愈能绑上。

② 真实浏览器(chromium + playwright)54 项全通过

  • 视口:桌面 1280×800、手机 390×844
  • 布局:普通表单 / 弹框内(overflow:hidden)/ 贴屏幕底部
  • 断言:面板不与输入框重叠、水平在视口内、宽度 ≥240px、
    挂载点是 body、3 行全部可见(scrollHeight == clientHeight,无裁切)、
    每行副标题有实际高度(不是被压成 0)
  • 手机视口下面板自动收窄到 376px、左右各留 8px;贴底时自动翻到输入框上方

实测渲染事实(不是推断):

面板 522×178,scrollHeight=176 clientHeight=176
第1行 主"星巴克(万象城店)" 14px rgb(27,27,31) 492×19
      副"山东省青岛市市南区 香港中路 88 号万象城 L1" 12px rgb(138,138,142) 492×16
第2行 主"星巴克(万象城店)" 14px  副"山东省青岛市市北区 山东路 6 号甲 B1"
第3行 主"青岛市崂山区" 14px      副"山东省青岛市"

③ 反向验证 3/3 个坏变体被抓出(证明判据不是恒绿)

故意弄坏 判据反应
不渲染副标题(回到改动前) ✅ 正确转红
副标题节点在但文字为空 ✅ 正确转红
面板不挂 body(挂输入框父节点) ✅ 正确转红

这一步是必要的:只测"全绿"不算测过。若断言只查 children.length >= 2
而不管内容,副标题为空也会显示通过。

需要你点验

线上 ?p=calendar 等页面要登录,我进不去,所以真实高德返回数据下的效果请你确认:

  • 日程:新建/编辑 → 地点框输入「星巴克」等
  • 观影:影院管理 → 新增影院 → 名称框
  • 行程:新建/编辑 → 出发地 / 到达地

重点看两件事:

  1. 下拉每行是否都带详细地址;
  2. 点选后:输入框内容、所在地方/详细地址等字段、框下灰色提示是否都跟以前一样。

asset() 会自动带 ?v=<md5>,文件一改版本号就变,不会被旧缓存挡住。

已知取舍

  • 高德对纯行政区划条目(如「青岛市崂山区」)不返回 address,
    只给 district ⇒ 这类候选的副标题就是省市区本身,不会空着。
  • 候选条数沿用高德默认(不额外分页),最多显示面板 max-height:264px、
    超出内部滚动。

点验没问题我就关单;有问题告诉我具体是哪个入口、什么现象。

## 已实现并部署,等你点验 commit `df657e5`(已推送 `42aff02..df657e5`),只改 `assets/js/addr_suggest.js` 一个文件(+215/-5)。 线上文件已更新并逐字节校验一致(两侧 sha256 `8df38af4…`)。 ### 效果 下拉候选从"只有地名"变成**两行**: ``` 星巴克(万象城店) 山东省青岛市市南区 香港中路 88 号万象城 L1 ───────────────────────────────── 星巴克(万象城店) 山东省青岛市市北区 山东路 6 号甲 B1 ───────────────────────────────── 青岛市崂山区 山东省青岛市 ``` 主标题 14px 深色、副标题 12px 灰色 `rgb(138,138,142)` —— 层级拉开, 扫一眼就能分清同名地点。 ### 改法 原来用 `AMap.AutoComplete` 的**内置下拉**,它只画 `name`,我们没有任何控制权。 所以改成「**高德只当查询器、下拉自己画**」: 1. `new AMap.AutoComplete({})` —— **不传 `input`**,高德就不再往 DOM 里塞它自己的面板; 2. 输入 → 防抖 260ms → `ac.search(kw, cb)` → 拿 `result.tips`; 3. 自己渲染每一行:地名 + 省市区 + 详细地址。 选中之后的**全部既有行为原样保留**,只是入口从高德的 `select` 事件换成自建候选的点击: `data-addr-fill` 字段回填、`data-addr-detail` 写门牌、框下 `.fb-addr-hint` 提示、 `fb-addr-picked` 事件。所以 4 处调用(日程地点 / 影院名 ×2 / 行程出发到达) 不用改模板就同时生效。 ### 几处刻意的取舍(都是踩过或预判到的坑) | 决定 | 理由 | |---|---| | 面板挂 `body` + `position:fixed` | 挂输入框父节点会被 `.mv-row` 的「标签 62px + 控件」两列网格挤掉;也会被子表单弹框的 `overflow:hidden` 裁掉 | | 面板量不到尺寸时**只跳过定位、不收起** | 原来写了"量不到就收起",结果**候选查到了却一显示就消失**(实测踩到,已经改掉) | | 少于 2 字不查,且收起面板 | 一个字的候选又多又没用,还白烧高德配额;留着旧候选会被误点 | | 候选行用 `textContent` 逐个填,不拼 HTML | 地点名由高德返回、可能含 `<` `&`,拼 HTML 会破版甚至注入 | | 点候选用 `mousedown` 而非 `click` | `click` 会先让输入框失焦触发收起,候选点不中 | | 只填**空白**字段(沿用原逻辑) | 用户手写过更细的门牌不该被覆盖 | ### 验证 **① jsdom 单元测试 27 项全通过**(`/tmp/addrtest/test_addr.js`) 覆盖:候选带地址、同名可区分、行政区划用 `district` 兜底、选中回填、 `fb-addr-picked` 事件、1 字不查/2 字才查、行程 from/to **不串**、 SDK 未就绪降级为普通文本框、插件晚到后**自愈**能绑上。 **② 真实浏览器(chromium + playwright)54 项全通过** - 视口:桌面 1280×800、手机 390×844 - 布局:普通表单 / 弹框内(`overflow:hidden`)/ 贴屏幕底部 - 断言:面板不与输入框重叠、水平在视口内、宽度 ≥240px、 挂载点是 `body`、3 行全部可见(`scrollHeight == clientHeight`,无裁切)、 每行副标题有实际高度(不是被压成 0) - 手机视口下面板自动收窄到 376px、左右各留 8px;贴底时**自动翻到输入框上方** 实测渲染事实(不是推断): ``` 面板 522×178,scrollHeight=176 clientHeight=176 第1行 主"星巴克(万象城店)" 14px rgb(27,27,31) 492×19 副"山东省青岛市市南区 香港中路 88 号万象城 L1" 12px rgb(138,138,142) 492×16 第2行 主"星巴克(万象城店)" 14px 副"山东省青岛市市北区 山东路 6 号甲 B1" 第3行 主"青岛市崂山区" 14px 副"山东省青岛市" ``` **③ 反向验证 3/3 个坏变体被抓出**(证明判据不是恒绿) | 故意弄坏 | 判据反应 | |---|---| | 不渲染副标题(回到改动前) | ✅ 正确转红 | | 副标题节点在但文字为空 | ✅ 正确转红 | | 面板不挂 body(挂输入框父节点) | ✅ 正确转红 | > 这一步是必要的:只测"全绿"不算测过。若断言只查 `children.length >= 2` > 而不管内容,副标题为空也会显示通过。 ### 需要你点验 线上 `?p=calendar` 等页面要登录,我进不去,所以**真实高德返回数据下的效果请你确认**: - 日程:新建/编辑 → 地点框输入「星巴克」等 - 观影:影院管理 → 新增影院 → 名称框 - 行程:新建/编辑 → 出发地 / 到达地 重点看两件事: 1. 下拉每行是否都带详细地址; 2. 点选后:输入框内容、所在地方/详细地址等字段、框下灰色提示是否都跟以前一样。 `asset()` 会自动带 `?v=<md5>`,文件一改版本号就变,**不会被旧缓存挡住**。 ### 已知取舍 - 高德对**纯行政区划**条目(如「青岛市崂山区」)不返回 `address`, 只给 `district` ⇒ 这类候选的副标题就是省市区本身,不会空着。 - 候选条数沿用高德默认(不额外分页),最多显示面板 `max-height:264px`、 超出内部滚动。 点验没问题我就关单;有问题告诉我具体是哪个入口、什么现象。
Author
Owner

用户验收通过,关单(2026-11-01)

用户 2026-11-01 明确回复「#32,可以关单了」。

交付回顾

commit df657e5,已部署线上(两侧 sha256 一致)。

下拉候选从「只有地名」变成两行:主标题 14px 深色(地名)+ 副标题 12px 灰色 rgb(138,138,142)(省市区 + 详细地址),
同名地点可凭地址区分。4 处地址输入框(日程地点 / 影院名 ×2 / 行程出发到达)一次覆盖。

改法:高德只当查询器(new AMap.AutoComplete({}) 不传 input),下拉自绘。
选中后的既有行为全部保留(data-addr-fill 回填、data-addr-detail、框下提示、fb-addr-picked 事件)。

验证:jsdom 27 项 + 真实浏览器(chromium/playwright)54 项全过 + 反向验证 3/3 抓到。

## 用户验收通过,关单(2026-11-01) 用户 2026-11-01 明确回复「#32,可以关单了」。 ### 交付回顾 commit `df657e5`,已部署线上(两侧 sha256 一致)。 下拉候选从「只有地名」变成两行:主标题 14px 深色(地名)+ 副标题 12px 灰色 `rgb(138,138,142)`(省市区 + 详细地址), 同名地点可凭地址区分。4 处地址输入框(日程地点 / 影院名 ×2 / 行程出发到达)一次覆盖。 改法:高德只当查询器(`new AMap.AutoComplete({})` 不传 input),下拉自绘。 选中后的既有行为全部保留(`data-addr-fill` 回填、`data-addr-detail`、框下提示、`fb-addr-picked` 事件)。 验证:jsdom 27 项 + 真实浏览器(chromium/playwright)54 项全过 + 反向验证 3/3 抓到。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FamilyBook#32