2026-10-14 01:20:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · **地址联想**(高德 AutoComplete)—— 全站一份
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 v47(用户 2026-10-13)三条:
|
|
|
|
|
|
* ①「影院管理中,输入影院名称的时候,应该自动帮我补齐所在的地方和详细地址。」
|
|
|
|
|
|
* ②「行程里面…输入出发地和到达地的时候,都需要调用高德的接口,补齐详细地址。」
|
|
|
|
|
|
* ③「日程中,当我输入地点的时候,也应该调用高德的接口,补齐详细地址。」
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么不再"每页各写一段":
|
|
|
|
|
|
* 原来的做法是 `trips/node_form.php` 里自己写一段内联脚本。v42 已经因此踩过一次
|
|
|
|
|
|
* (弹框是 AJAX 片段、内联 `<script>` 不执行 ⇒ 联想失效,只好抽了个 `fbNodeForm()`)。
|
|
|
|
|
|
* 这次要接到**三处**(影院管理页、观影新增弹框、日程表单,而日程本身也是弹框),
|
|
|
|
|
|
* 再各写一份必然出现"某个入口忘了补"——所以抽成这一份,谁用谁调 `fbAddr()`。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 与项目其它按需资源同一套规矩:
|
|
|
|
|
|
* · 脚本**按需加载**(页面上真有 `[data-addr]` 才拉,见 layout.php 的 fb-addr meta);
|
|
|
|
|
|
* · SDK 的 key/安全密钥由 layout 注入(`window.FB_AMAP`),**本文件不碰配置**;
|
|
|
|
|
|
* · **高德挂了也必须能用** —— 输入框仍是普通文本框,手敲照常保存。
|
|
|
|
|
|
* 所以所有 AMap 的调用都在 `__AMAP_LOADED` 判断之后,一条都不在外面。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 幂等:同一段表单可能被反复塞进 DOM(弹框开关),用 `data-addr-bound` 挡住重复绑定。
|
|
|
|
|
|
* **但"绑定"要分两层**:input 的事件只绑一次;每次调用都要**重画一次**已有值
|
|
|
|
|
|
* (因为重画后 DOM 是新的、可能带着服务端回填的地址)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
|
|
// 高德 SDK 的就绪状态由 layout 注入的那段脚本维护(见 fb-addr 注入块)
|
2026-10-20 21:35:47 +08:00
|
|
|
|
// 🔴 v68:**只认 `AMap.AutoComplete`**。v54 起 SDK 由一个不带 `plugin=` 的地址拉进来,
|
|
|
|
|
|
// 此时 `AMap` 有了但插件没有 ⇒ 这里必须为假,由下面 `ensurePlugin()` 去补;
|
|
|
|
|
|
// 若把判据放宽成"有 AMap 就算就绪",就会 `new AMap.AutoComplete` 抛错、
|
|
|
|
|
|
// 然后**静默**什么都不发生(正是用户看到的"输入地点没反应")。
|
2026-10-14 01:20:40 +08:00
|
|
|
|
function sdkReady() {
|
2026-10-20 21:35:47 +08:00
|
|
|
|
return !!(window.AMap && window.AMap.AutoComplete);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* v68:按需把 `AMap.AutoComplete` 插件拉起来,成功后重扫整页。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 为什么要这一步:高德 JS API 2.0 的服务插件是**独立 chunk**,
|
|
|
|
|
|
* SDK 主脚本 onload 时插件不一定在(实测:不带 `plugin=` 参数时一定不在)。
|
|
|
|
|
|
* 官方姿势就是 `AMap.plugin('AMap.AutoComplete', cb)`。
|
|
|
|
|
|
* ⚠️ `asking` 挡住重复请求:一页上可能有多个地点输入框,会同时触发。
|
|
|
|
|
|
*/
|
|
|
|
|
|
var asking = false;
|
|
|
|
|
|
function ensurePlugin() {
|
|
|
|
|
|
if (asking || !window.AMap || typeof window.AMap.plugin !== 'function') { return; }
|
|
|
|
|
|
if (window.AMap.AutoComplete) { fbAddr(document); return; }
|
|
|
|
|
|
asking = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
window.AMap.plugin('AMap.AutoComplete', function () {
|
|
|
|
|
|
asking = false;
|
|
|
|
|
|
if (sdkReady()) { fbAddr(document); } // 插件到了,重扫(此前没绑上的会补上)
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
asking = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* v68:同一个地点框,**选中候选**后往下补一行"省市区 + 详细地址"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么需要:日程表里地点只有**一栏**(`_event_fields.php` 只有 `location`),
|
|
|
|
|
|
* 用户 2026-10-20 要的是「获得准确的详细地址」—— 只把名字写回去,
|
|
|
|
|
|
* 名字照样是"青岛大学(浮山校区)",回头要导航/找人还是查不到门牌。
|
|
|
|
|
|
* ⇒ 输入框里保留**用户认得的名字**(或按 `data-addr-detail` 连地址一起写),
|
|
|
|
|
|
* 同时在**这一行下面**用 `.tip` 那行把官方地址显示出来。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 插到哪:不能塞进输入框的父节点 —— 那是 `.mv-row`(标签 62px + 控件占满剩余)
|
|
|
|
|
|
* 的两列网格,多一个子项会把右边那列挤掉(本项目 v24 踩过同类排版坑)。
|
|
|
|
|
|
* ⇒ 找"这一整行"(`.mv-row` / `.field` / `.loc-field`),插到**它后面**。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function showAddrHint(input, poi) {
|
|
|
|
|
|
var row = null;
|
|
|
|
|
|
try { row = input.closest('.mv-row, .field, .loc-field, .row'); } catch (e) { row = null; }
|
|
|
|
|
|
var anchor = row || input;
|
|
|
|
|
|
var host = anchor.parentNode;
|
|
|
|
|
|
if (!host) { return; }
|
|
|
|
|
|
|
|
|
|
|
|
var hint = host.querySelector('.fb-addr-hint');
|
|
|
|
|
|
var place = readPlace(poi);
|
|
|
|
|
|
var addr = String(poi.address || '').trim();
|
|
|
|
|
|
var text = [place, addr].filter(function (s) { return s !== ''; }).join(' · ');
|
|
|
|
|
|
if (text === '') {
|
|
|
|
|
|
if (hint) { hint.style.display = 'none'; }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!hint) {
|
|
|
|
|
|
hint = document.createElement('span');
|
|
|
|
|
|
hint.className = 'tip fb-addr-hint';
|
|
|
|
|
|
hint.style.display = 'block';
|
|
|
|
|
|
hint.style.margin = '4px 0 0 62px'; // 与 `.mv-row` 的标签列对齐(62px 见 Theme/观影式排版)
|
|
|
|
|
|
host.insertBefore(hint, anchor.nextSibling);
|
|
|
|
|
|
}
|
|
|
|
|
|
hint.textContent = '高德:' + text;
|
|
|
|
|
|
hint.style.display = 'block';
|
2026-10-14 01:20:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 把 location(AMap.LngLat 或 {lng,lat} 或字符串)统一成 {lng,lat} */
|
|
|
|
|
|
function readLngLat(loc) {
|
|
|
|
|
|
if (!loc) { return { lng: '', lat: '' }; }
|
|
|
|
|
|
if (typeof loc === 'string') {
|
|
|
|
|
|
var p = loc.split(',');
|
|
|
|
|
|
return p.length >= 2 ? { lng: p[0].trim(), lat: p[1].trim() } : { lng: '', lat: '' };
|
|
|
|
|
|
}
|
|
|
|
|
|
var lng = (loc.lng !== undefined) ? loc.lng : (loc.getLng ? loc.getLng() : '');
|
|
|
|
|
|
var lat = (loc.lat !== undefined) ? loc.lat : (loc.getLat ? loc.getLat() : '');
|
|
|
|
|
|
return { lng: lng === null || lng === undefined ? '' : lng,
|
|
|
|
|
|
lat: lat === null || lat === undefined ? '' : lat };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 取"所在地方"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 高德的返回有**两套字段**,要看运气:
|
|
|
|
|
|
* · `poi.addressComponent.{province,city,district}` —— 结构化三级;
|
|
|
|
|
|
* · `poi.pname / cityname / adname` —— 老式平铺字段。
|
|
|
|
|
|
* 实测两者不是同时都有。取到哪级用哪级,取不到就留空(**绝不瞎编**)。
|
|
|
|
|
|
* @returns {string} 形如 "青岛市 市南区" / "青岛" / ''(都没有)
|
|
|
|
|
|
*/
|
2026-11-05 12:40:57 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 只取「市 + 区」,**不带省** —— 给输入框 value 用。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 2026-11-03:用户要求详细地址写成「青岛市黄岛区珠江路1018号」。
|
|
|
|
|
|
* 而 `readPlace()` 返回的是带省的「山东省 青岛市 黄岛区」。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么 value 里**不带省**:
|
|
|
|
|
|
* · 城市名足够定位,省是冗余的(中国没有跨省同名地级市)
|
|
|
|
|
|
* · 输入框宽度有限,带上「山东省」会让后半截(门牌号)显示不全
|
|
|
|
|
|
* · 导航用不到省
|
|
|
|
|
|
*
|
|
|
|
|
|
* 直辖市的坑:`province === city === '北京市'`,只留一份。
|
|
|
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 只取**城市名**(「青岛市」),用于 `from_city` / `to_city` 这类聚合字段。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 2026-11-05 新增。用户:「城市…直接城市就可以」。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 与 `readPlace()` 的区别:
|
|
|
|
|
|
* · `readPlace()` = 省 + 市 + 区(「山东省青岛市市南区」)→ 给 `place` 显示用
|
|
|
|
|
|
* · `cityOnly()` = 只要市一级(「青岛市」) → 给 city 存库用
|
|
|
|
|
|
*
|
|
|
|
|
|
* 三级取值(高德不同接口给的字段不一样):
|
|
|
|
|
|
* ① `addressComponent.city`(AutoComplete / 逆地理)—— 最准
|
|
|
|
|
|
* ② 直辖市:`addressComponent.city` 是空,用 `province`(「北京市」)
|
|
|
|
|
|
* ③ `poi.district` 整串(inputtips:「山东省青岛市崂山区」)→ 正则剥出市名
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 拿不到就返回空串,**不猜**。空着比错着好(服务端还有一层 `Geo::cityOnly` 会补)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function cityOnly(poi) {
|
|
|
|
|
|
var a = poi.addressComponent || null;
|
|
|
|
|
|
var city = '';
|
|
|
|
|
|
var prov = '';
|
|
|
|
|
|
if (a) {
|
|
|
|
|
|
city = String(a.city || '').trim();
|
|
|
|
|
|
prov = String(a.province || '').trim();
|
|
|
|
|
|
/* 直辖市:`city` 为空(或与 province 相同),本身就是市一级 */
|
|
|
|
|
|
if (!city && prov) {
|
|
|
|
|
|
city = prov;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!city) {
|
|
|
|
|
|
/* inputtips 只给一个 district 整串:`山东省青岛市崂山区` /
|
|
|
|
|
|
`北京市丰台区` / `内蒙古自治区呼和浩特市新城区` */
|
|
|
|
|
|
var d = String(poi.district || '').trim();
|
|
|
|
|
|
city = cityFromDistrict(d);
|
|
|
|
|
|
}
|
|
|
|
|
|
return city;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从「省市区」整串里剥出**市名**(「山东省青岛市崂山区」→「青岛市」)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 规则(按可靠度排序):
|
|
|
|
|
|
* ① 取**第一个**「XX市」—— 中文地址是 省→市→区 的层级顺序,
|
|
|
|
|
|
* 第一个「市」就是地级市。
|
|
|
|
|
|
* ⚠️ 不能取最后一个:「黑龙江省黑河市五大连池市」会得到县级市;
|
|
|
|
|
|
* 「山东省青岛市市南区」的"市南区"里还有"市"字,靠"第一个"才能对。
|
|
|
|
|
|
* ② 直辖市(不带"市"字):「北京市丰台区」→ 第一个「市」就是「北京市」,
|
|
|
|
|
|
* 走 ① 即可;万一写成「北京朝阳区」才需要这级。
|
|
|
|
|
|
* ③ 都没有 ⇒ 空串。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function cityFromDistrict(d) {
|
|
|
|
|
|
if (!d) { return ''; }
|
|
|
|
|
|
var s = String(d).trim();
|
|
|
|
|
|
/* ① 第一个「XX市」:XX 里不能含"省市县区旗盟"这些层级字眼,
|
|
|
|
|
|
否则「山东省」会被当成「山东省市」 */
|
|
|
|
|
|
var m = s.match(/([^\s省市县区旗盟]{1,10})市/);
|
|
|
|
|
|
if (m) {
|
|
|
|
|
|
return m[1] + '市';
|
|
|
|
|
|
}
|
|
|
|
|
|
/* ② 直辖市:开头就是「北京市」这类,上面已覆盖;
|
|
|
|
|
|
剩下「北京朝阳区」这种去掉末尾区县,取前段 */
|
|
|
|
|
|
var m2 = s.match(/^(北京|上海|天津|重庆)/);
|
|
|
|
|
|
if (m2) {
|
|
|
|
|
|
return m2[1] + '市';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cityDistrictOf(poi) {
|
|
|
|
|
|
var a = poi.addressComponent || null;
|
|
|
|
|
|
var city = '', district = '';
|
|
|
|
|
|
if (a) {
|
|
|
|
|
|
city = String(a.city || a.province || '').trim();
|
|
|
|
|
|
district = String(a.district || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!city && !district) {
|
|
|
|
|
|
city = String(poi.cityname || poi.pname || '').trim();
|
|
|
|
|
|
district = String(poi.adname || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!city && !district) {
|
|
|
|
|
|
// inputtips 只给一个整串:「山东省青岛市崂山区」
|
|
|
|
|
|
// ⇒ 从整串里截出「XX市XX区」部分
|
|
|
|
|
|
var whole = String(poi.district || '').trim();
|
|
|
|
|
|
if (whole !== '') {
|
|
|
|
|
|
var mi = whole.indexOf('市');
|
|
|
|
|
|
if (mi >= 0) {
|
|
|
|
|
|
city = whole.substring(0, mi + 1);
|
|
|
|
|
|
district = whole.substring(mi + 1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
city = whole;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var parts = [];
|
|
|
|
|
|
if (city) { parts.push(city); }
|
|
|
|
|
|
if (district && district !== city) { parts.push(district); }
|
|
|
|
|
|
return parts.join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把「省市区」与「街道门牌」合并,**缺哪段补哪段**,已有的不重复。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 2026-11-03:原来这里判断太粗("address 含「区」字就整个不用 cd"),
|
|
|
|
|
|
* 导致 `cd='青岛市黄岛区'` + `address='黄岛区珠江路1018号'`
|
|
|
|
|
|
* 拼成 `黄岛区珠江路1018号` —— **把「青岛市」丢了**。
|
|
|
|
|
|
* 而城市名正是地理编码最关键的线索(丢了就会搜到外省同名路)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 正确逻辑:逐段检查 `address` 里有没有,没有才补。
|
|
|
|
|
|
*
|
|
|
|
|
|
* cd='青岛市黄岛区', address='珠江路1018号'
|
|
|
|
|
|
* → 两段都没有 ⇒ 全补 ⇒ '青岛市黄岛区珠江路1018号'
|
|
|
|
|
|
* cd='青岛市黄岛区', address='黄岛区珠江路1018号'
|
|
|
|
|
|
* → 有「黄岛区」、缺「青岛市」⇒ 补城市 ⇒ '青岛市黄岛区珠江路1018号'
|
|
|
|
|
|
* cd='青岛市黄岛区', address='青岛市黄岛区珠江路1018号'
|
|
|
|
|
|
* → 全都有 ⇒ 原样
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} cd cityDistrictOf 的结果(如「青岛市黄岛区」)
|
|
|
|
|
|
* @param {string} addr 高德的 poi.address(如「珠江路1018号」)
|
|
|
|
|
|
* @return {string}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function mergeAddrTail(cd, addr) {
|
|
|
|
|
|
var ad = String(addr || '').trim();
|
|
|
|
|
|
var c = String(cd || '').trim();
|
|
|
|
|
|
if (c === '') { return ad; }
|
|
|
|
|
|
if (ad === '') { return c; }
|
|
|
|
|
|
|
|
|
|
|
|
// 把 cd 拆成「城市」与「区县」两段:找第一个「市」字切开
|
|
|
|
|
|
var mi = c.indexOf('市');
|
|
|
|
|
|
var city = '', dist = '';
|
|
|
|
|
|
if (mi >= 0) {
|
|
|
|
|
|
city = c.substring(0, mi + 1); // 含「市」
|
|
|
|
|
|
dist = c.substring(mi + 1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
city = c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var needCity = city !== '' && ad.indexOf(city) < 0;
|
|
|
|
|
|
// 区县名可能不带「区/县」后缀出现在 address 里,用去掉后缀的裸名判断
|
|
|
|
|
|
var distBare = dist.replace(/[区县市旗]$/, '');
|
|
|
|
|
|
var needDist = dist !== '' && ad.indexOf(dist) < 0
|
|
|
|
|
|
&& (distBare === '' || ad.indexOf(distBare) < 0);
|
|
|
|
|
|
|
|
|
|
|
|
/* ⚠️ 拼接顺序必须是「城市 → 区县 → 街道」。
|
|
|
|
|
|
测出来的 case:cd='青岛市黄岛区'、addr='青岛市珠江路1018号'
|
|
|
|
|
|
(有城缺区)时,若直接 `prefix + ad` 会得到
|
|
|
|
|
|
「黄岛区青岛市珠江路1018号」—— 区跑到城市前面了。
|
|
|
|
|
|
⇒ 分段拼:先城市,再区,最后街道(街道在 ad 里,
|
|
|
|
|
|
但要先把 ad 里已有的城市/区剥掉避免重复)。 */
|
|
|
|
|
|
var tailAd = ad;
|
|
|
|
|
|
if (!needCity) {
|
|
|
|
|
|
// ad 已含城市 ⇒ 剥掉前缀,后面统一按 城市+区+街道 拼
|
|
|
|
|
|
var idxC = ad.indexOf(city);
|
|
|
|
|
|
if (idxC === 0) {
|
|
|
|
|
|
tailAd = ad.substring(city.length);
|
|
|
|
|
|
} else if (idxC > 0) {
|
|
|
|
|
|
tailAd = ad.substring(0, idxC) + ad.substring(idxC + city.length);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!needDist && dist !== "") {
|
|
|
|
|
|
var idxD = tailAd.indexOf(dist);
|
|
|
|
|
|
if (idxD >= 0) {
|
|
|
|
|
|
tailAd = tailAd.substring(0, idxD) + tailAd.substring(idxD + dist.length);
|
|
|
|
|
|
} else if (distBare !== "") {
|
|
|
|
|
|
var idxB = tailAd.indexOf(distBare);
|
|
|
|
|
|
if (idxB >= 0) {
|
|
|
|
|
|
// 裸名保留(「黄岛珠江路」里的「黄岛」是地址的一部分,不剥)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var prefix = "";
|
|
|
|
|
|
if (city !== "") { prefix += city; }
|
|
|
|
|
|
if (dist !== "") { prefix += dist; }
|
|
|
|
|
|
return prefix + tailAd.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-14 01:20:40 +08:00
|
|
|
|
function readPlace(poi) {
|
|
|
|
|
|
var a = poi.addressComponent || null;
|
|
|
|
|
|
var province = '', city = '', district = '';
|
|
|
|
|
|
if (a) {
|
|
|
|
|
|
// ⚠️ 直辖市的 province 与 city 会**同时**是"北京市",拼起来变"北京市 北京市"
|
|
|
|
|
|
// ⇒ 相同时只留一份。
|
|
|
|
|
|
province = String(a.province || '').trim();
|
|
|
|
|
|
city = String(a.city || '').trim();
|
|
|
|
|
|
district = String(a.district || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!province && !city && !district) {
|
|
|
|
|
|
province = String(poi.pname || '').trim();
|
|
|
|
|
|
city = String(poi.cityname || '').trim();
|
|
|
|
|
|
district = String(poi.adname || '').trim();
|
|
|
|
|
|
}
|
2026-10-20 21:35:47 +08:00
|
|
|
|
/* 🔴 v68:第三套字段 —— **输入提示**(`/v3/assistant/inputtips`,正是
|
|
|
|
|
|
AutoComplete 背后用的那个接口)只给一个 `district` 整串:
|
|
|
|
|
|
`"山东省青岛市崂山区"`。前两套都没有时用它,否则"详细地址"那行
|
|
|
|
|
|
永远只有门牌号、没有城市(实测踩到)。 */
|
|
|
|
|
|
if (!province && !city && !district) {
|
|
|
|
|
|
district = String(poi.district || '').trim();
|
|
|
|
|
|
}
|
2026-10-14 01:20:40 +08:00
|
|
|
|
var parts = [];
|
|
|
|
|
|
if (province) { parts.push(province); }
|
|
|
|
|
|
if (city && city !== province) { parts.push(city); }
|
|
|
|
|
|
if (district) { parts.push(district); }
|
|
|
|
|
|
return parts.join(' ');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-30 07:37:55 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* v69(工单 #32):自建候选面板。
|
|
|
|
|
|
*
|
|
|
|
|
|
* **为什么要自建**:`AMap.AutoComplete` 的内置下拉只渲染 `name`,
|
|
|
|
|
|
* 同名地点(多个「星巴克」、同一个商场的不同入口)在候选里长得一模一样,
|
|
|
|
|
|
* 用户没法判断该选哪个。要在地名后面补详细地址,就只能自己接管渲染。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 定位用 `absolute` + 挂在 `document.body` 上,**不挂在输入框父节点里**:
|
|
|
|
|
|
* 1. 输入框常在 `.mv-row` 这种"标签 62px + 控件占满"的两列网格里,
|
|
|
|
|
|
* 多一个子项会把右边那列挤掉(本项目 v24 踩过同类排版坑);
|
|
|
|
|
|
* 2. 表单弹框多有 `overflow: hidden`,挂里面会被裁掉。
|
|
|
|
|
|
* 用 `position: fixed` + 实时读 `getBoundingClientRect()` 定位,
|
|
|
|
|
|
* 滚动/缩放时跟着走(见 `placePanel`)。
|
|
|
|
|
|
* @param {HTMLElement} input 那个地址输入框
|
|
|
|
|
|
* @param {Function} onPick 点中候选后的回调(收到高德的 poi 对象)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function makePanel(input, onPick) {
|
|
|
|
|
|
var el = document.createElement('div');
|
|
|
|
|
|
el.className = 'fb-addr-panel';
|
|
|
|
|
|
el.setAttribute('data-fb-addr-panel', '1');
|
|
|
|
|
|
/* ⚠️ 样式内联而不写进 app.css:这份脚本是全站共用、按需加载的,
|
|
|
|
|
|
把样式焊在这里才不会有"某个页面忘了引那段 CSS"的漏。 */
|
|
|
|
|
|
el.style.cssText = [
|
|
|
|
|
|
'position:fixed', 'z-index:2147483000', 'display:none',
|
|
|
|
|
|
'margin:0', 'padding:4px 0', 'list-style:none',
|
|
|
|
|
|
'max-height:264px', 'overflow-y:auto',
|
|
|
|
|
|
'background:#fff', 'border-radius:10px',
|
|
|
|
|
|
'box-shadow:0 6px 24px rgba(0,0,0,.16), 0 1px 3px rgba(0,0,0,.10)',
|
|
|
|
|
|
'border:1px solid rgba(0,0,0,.08)',
|
|
|
|
|
|
'-webkit-overflow-scrolling:touch',
|
|
|
|
|
|
'font-size:14px', 'line-height:1.35'
|
|
|
|
|
|
].join(';');
|
|
|
|
|
|
document.body.appendChild(el);
|
|
|
|
|
|
|
|
|
|
|
|
var panel = { el: el, input: input, onPick: onPick, tips: [] };
|
|
|
|
|
|
/* 滚动/缩放时重算位置 —— 用 capture 阶段接,任何滚动容器都能收到 */
|
|
|
|
|
|
window.addEventListener('scroll', function () { placePanel(panel); }, true);
|
|
|
|
|
|
window.addEventListener('resize', function () { placePanel(panel); });
|
|
|
|
|
|
return panel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 把面板放到输入框正下方,宽度跟随输入框(不超出视口) */
|
|
|
|
|
|
function placePanel(panel) {
|
|
|
|
|
|
if (!panel.el || panel.el.style.display === 'none') { return; }
|
|
|
|
|
|
var r = panel.input.getBoundingClientRect();
|
|
|
|
|
|
/* ⚠️ 量不到尺寸(`0×0`)有两种可能:
|
|
|
|
|
|
① 输入框真的不在布局里(表单被隐藏/已从 DOM 摘掉)→ 该收起;
|
|
|
|
|
|
② **刚 append 完、样式还没算完**(同一 tick 内读 rect 常见于旧引擎),
|
|
|
|
|
|
或**无布局环境**(jsdom 做单元测试时 rect 恒为 0)。
|
|
|
|
|
|
原来的写法一律 `hidePanel` 收掉,于是②这种情况会出现
|
|
|
|
|
|
"候选明明查到了却一显示就消失"——**这不是理论风险,是实测踩到的**。
|
|
|
|
|
|
⇒ 改成:量不到就**先不定位**(保留默认位置/宽度),但**不收起**。
|
|
|
|
|
|
面板若真该消失,`hidePanel` 在别处(失焦、选出、输入过短)都会调。 */
|
|
|
|
|
|
if (r.width === 0 && r.height === 0) {
|
|
|
|
|
|
panel.el.style.minWidth = '240px';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var vw = window.innerWidth || document.documentElement.clientWidth;
|
|
|
|
|
|
var w = Math.min(Math.max(r.width, 240), vw - 16);
|
|
|
|
|
|
var left = Math.min(Math.max(r.left, 8), vw - w - 8);
|
|
|
|
|
|
panel.el.style.width = w + 'px';
|
|
|
|
|
|
panel.el.style.left = left + 'px';
|
|
|
|
|
|
/* 下方空间不足就翻到输入框上方(手机上键盘弹起时尤其常见) */
|
|
|
|
|
|
var below = window.innerHeight - r.bottom;
|
|
|
|
|
|
var h = panel.el.offsetHeight || 0;
|
|
|
|
|
|
if (below < Math.min(h, 160) && r.top > below) {
|
|
|
|
|
|
panel.el.style.top = Math.max(8, r.top - h - 6) + 'px';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
panel.el.style.top = (r.bottom + 6) + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hidePanel(panel) {
|
|
|
|
|
|
if (!panel || !panel.el) { return; }
|
|
|
|
|
|
panel.el.style.display = 'none';
|
|
|
|
|
|
panel.el.innerHTML = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 渲染候选:**每行 = 地名(深色,主)+ 省市区/详细地址(灰色,副)**。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 这正是工单 #32 的诉求 —— 地址必须出现在下拉里,作为选择依据。
|
|
|
|
|
|
* 同名地点只有靠这一行灰字才区分得开。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 高德的 `tips` 里不同条目字段并不齐(实测):
|
|
|
|
|
|
* · POI 类:`name` + `address` + `district`
|
|
|
|
|
|
* · 纯行政区划类:`name`(就是地名)+ `district`,**没有 address**
|
|
|
|
|
|
* ⇒ secondary 要把能拿到的都拼上,全空就只显示主标题(不能留一片空白)。
|
|
|
|
|
|
* ⚠️ 用 `textContent` 逐个填,**不拼 HTML 字符串**:
|
|
|
|
|
|
* 地点名是高德返回的、可能含 `<` `&`,拼 HTML 会破版甚至注入。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function renderPanel(panel, tips, onPick) {
|
|
|
|
|
|
var el = panel.el;
|
|
|
|
|
|
el.innerHTML = '';
|
|
|
|
|
|
var list = [];
|
|
|
|
|
|
for (var i = 0; i < tips.length; i++) {
|
|
|
|
|
|
var t = tips[i];
|
|
|
|
|
|
if (!t) { continue; }
|
|
|
|
|
|
/* 只要"有名字"的;`location` 为空的纯提示行(高德有时给空壳)跳过 */
|
|
|
|
|
|
var nm = String(t.name || '').trim();
|
|
|
|
|
|
if (nm === '') { continue; }
|
|
|
|
|
|
list.push(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (list.length === 0) { hidePanel(panel); return; }
|
|
|
|
|
|
|
|
|
|
|
|
list.forEach(function (t) {
|
|
|
|
|
|
var row = document.createElement('div');
|
|
|
|
|
|
row.style.cssText = [
|
|
|
|
|
|
'padding:9px 14px', 'cursor:pointer', 'display:block',
|
|
|
|
|
|
'border-bottom:1px solid rgba(0,0,0,.04)'
|
|
|
|
|
|
].join(';');
|
|
|
|
|
|
|
|
|
|
|
|
var main = document.createElement('div');
|
|
|
|
|
|
main.style.cssText = 'color:#1b1b1f;font-weight:500;word-break:break-all';
|
|
|
|
|
|
main.textContent = String(t.name || '').trim();
|
|
|
|
|
|
row.appendChild(main);
|
|
|
|
|
|
|
|
|
|
|
|
/* 副标题 = 省市区 + 详细地址。两者常常有包含关系
|
|
|
|
|
|
(`district` 是"山东省青岛市崂山区",`address` 是"香港东路 195 号"),
|
|
|
|
|
|
直接拼接不会重复;但若 address 已经以 district 开头则去重。 */
|
|
|
|
|
|
var place = readPlace(t);
|
|
|
|
|
|
var addr = String(t.address || '').trim();
|
|
|
|
|
|
var parts = [];
|
|
|
|
|
|
if (place !== '') { parts.push(place); }
|
|
|
|
|
|
if (addr !== '' && addr.indexOf(place) !== 0) { parts.push(addr); }
|
|
|
|
|
|
else if (addr !== '' && place === '') { parts.push(addr); }
|
|
|
|
|
|
var sub = parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (sub !== '') {
|
|
|
|
|
|
var desc = document.createElement('div');
|
|
|
|
|
|
desc.style.cssText = 'color:#8a8a8e;font-size:12px;margin-top:2px;word-break:break-all';
|
|
|
|
|
|
desc.textContent = sub;
|
|
|
|
|
|
row.appendChild(desc);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 悬停/按下高亮(触屏没有 hover,所以两件都绑) */
|
|
|
|
|
|
row.addEventListener('mouseenter', function () { row.style.background = '#f5f5f7'; });
|
|
|
|
|
|
row.addEventListener('mouseleave', function () { row.style.background = 'transparent'; });
|
|
|
|
|
|
row.addEventListener('touchstart', function () { row.style.background = '#f5f5f7'; });
|
|
|
|
|
|
|
|
|
|
|
|
/* ⚠️ 用 `mousedown` + `preventDefault` 而不是 `click`:
|
|
|
|
|
|
输入框失焦会触发收起逻辑,`click` 可能来不及。
|
|
|
|
|
|
`preventDefault` 还能阻止输入框先失焦再复位造成的"闪一下"。 */
|
|
|
|
|
|
row.addEventListener('mousedown', function (ev) {
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
hidePanel(panel);
|
|
|
|
|
|
onPick(t);
|
|
|
|
|
|
});
|
|
|
|
|
|
/* 触屏上 mousedown 也会触发,但为稳妥补一次 touchend(并挡掉随后的 300ms click 穿透) */
|
|
|
|
|
|
row.addEventListener('touchend', function (ev) {
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
hidePanel(panel);
|
|
|
|
|
|
onPick(t);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
el.appendChild(row);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
el.style.display = 'block';
|
|
|
|
|
|
placePanel(panel); // 先显示再量高度,才能判断"下方够不够"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-14 01:20:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 绑定一个地址输入框。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {HTMLElement} input 那个要联想的输入框(必须有 `data-addr`)
|
|
|
|
|
|
* @param {HTMLElement} scope 可选:限定字段查找范围(弹框里的一段表单)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function bindOne(input, scope) {
|
|
|
|
|
|
if (!input || input.getAttribute('data-addr-bound') === '1') { return; }
|
2026-10-20 21:35:47 +08:00
|
|
|
|
/* 🔴🔴 v68 修的**第二个** bug:这里原来一进门就打 `data-addr-bound='1'`,
|
|
|
|
|
|
而"SDK 就绪"的判断在**后面**。只要那一次判断为假(v54 之后恒假),
|
|
|
|
|
|
输入框就被永久标成"已绑定" ⇒ 后面谁来调 `fbAddr()` 都会被这行挡掉,
|
|
|
|
|
|
**哪怕插件后来加载成功了也再没机会绑上**。表现就是"以前好用、现在不好用了",
|
|
|
|
|
|
而且改什么地方都不恢复,只能刷掉重来(重来也一样假)。
|
|
|
|
|
|
⇒ 标记改成"**真的 new 成功了**"才打。 */
|
2026-10-14 01:20:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 字段查找范围:`data-addr-for` 给的是一个选择器。
|
|
|
|
|
|
// 🔴 必须优先取**最近的那个**祖先(`closest`),不能 `querySelector` 取第一个 ——
|
|
|
|
|
|
// 行程表单里 from / to 两组字段都在页面上,取第一个会把出发地的地址
|
|
|
|
|
|
// 写到到达地上("填错了但看起来像填对了",最难查)。
|
|
|
|
|
|
// 取不到祖先时才退回:先按选择器全局取第一个,最后退回 scope/document。
|
|
|
|
|
|
var root = null;
|
|
|
|
|
|
var forSel = input.getAttribute('data-addr-for');
|
|
|
|
|
|
if (forSel) {
|
|
|
|
|
|
try { root = input.closest(forSel); } catch (e) { root = null; }
|
|
|
|
|
|
if (!root) { root = (scope || document).querySelector(forSel); }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!root) {
|
|
|
|
|
|
root = (scope || document);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fieldFor(target) {
|
|
|
|
|
|
if (!target) { return null; }
|
|
|
|
|
|
// 🔴 同一个"逻辑字段"在项目里有两套命名,必须都认(v47 实测踩到):
|
|
|
|
|
|
// · 影院页 —— 全拼:`address` / `place`
|
|
|
|
|
|
// · 行程地点 —— 缩写:`from_addr` / `to_addr`(`_addr`,不是 `_address`)
|
|
|
|
|
|
// 只认全拼的话 `[name$="address"]` 匹配不到 `from_addr`
|
|
|
|
|
|
// (`from_addr` 以 `_addr` 结尾)⇒ 地址永远填不进去,
|
|
|
|
|
|
// 而"名字和经纬度都填上了"会让人误以为整个回填是好的。
|
|
|
|
|
|
var SYN = {
|
|
|
|
|
|
address: ['address', 'addr'],
|
|
|
|
|
|
addr: ['addr', 'address'],
|
|
|
|
|
|
place: ['place', 'city'],
|
|
|
|
|
|
city: ['city', 'place'],
|
|
|
|
|
|
lng: ['lng'],
|
|
|
|
|
|
lat: ['lat'],
|
|
|
|
|
|
};
|
|
|
|
|
|
var names = SYN[target] || [target];
|
|
|
|
|
|
for (var k = 0; k < names.length; k++) {
|
|
|
|
|
|
var nm = names[k];
|
|
|
|
|
|
// ① 精确名
|
|
|
|
|
|
var el = root.querySelector('[name="' + nm + '"]');
|
|
|
|
|
|
if (el) { return el; }
|
|
|
|
|
|
// ② 带前缀的(`from_addr`)—— 后缀匹配
|
|
|
|
|
|
// ⚠️ 能安全用后缀,是因为 `root` 已经收窄到**这一组**字段的容器
|
|
|
|
|
|
// (`data-addr-for=".loc-field"` + `closest`):
|
|
|
|
|
|
// 出发/到达两组字段在各自的 .loc-field 里,互不可见。
|
|
|
|
|
|
// 若在整页范围做后缀匹配,`from_addr` 与 `to_addr` 会互相抢。
|
|
|
|
|
|
try {
|
|
|
|
|
|
var cands = root.querySelectorAll('[name$="' + nm + '"]');
|
|
|
|
|
|
if (cands.length) { return cands[0]; }
|
|
|
|
|
|
cands = root.querySelectorAll('[name$="_' + nm + '"]');
|
|
|
|
|
|
if (cands.length) { return cands[0]; }
|
|
|
|
|
|
} catch (e) { /* 极老浏览器不支持 $= ⇒ 找不到就不填,不报错 */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var targets = (input.getAttribute('data-addr-fill') || '')
|
|
|
|
|
|
.split(',').map(function (s) { return s.trim(); })
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
// 手敲也要能被"补齐"(用户可能不用联想直接打字,或 SDK 没加载)
|
|
|
|
|
|
// —— 这里只在**选了候选**时填,避免把用户手写的地址覆盖掉。
|
|
|
|
|
|
|
2026-10-20 21:35:47 +08:00
|
|
|
|
if (!sdkReady()) {
|
|
|
|
|
|
/* v68:插件还没到 —— **不打已绑定标记**,去按需拉一次插件,
|
|
|
|
|
|
到了以后 `fbAddr(document)` 会重扫到这里重新执行。
|
|
|
|
|
|
这样"SDK 晚到 / 插件晚到 / 弹框晚插入"三种时序都能自愈。 */
|
|
|
|
|
|
ensurePlugin();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var detail = input.getAttribute('data-addr-detail') === '1';
|
2026-10-14 01:20:40 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-10-30 07:37:55 +08:00
|
|
|
|
/* 🔴🔴 v69(用户 2026-10-30,工单 #32):「输入一个简单地址、下拉显示详细地址时,
|
|
|
|
|
|
不能只把地点告诉我,一定要把详细地址也放在后面。因为同一个地址可能有好几个,
|
|
|
|
|
|
我根本不知道应该选哪一个。」
|
|
|
|
|
|
⇒ **不再用 `AMap.AutoComplete` 的内置下拉**(它只画 `name`,我们无法控制),
|
|
|
|
|
|
改成"高德只当查询器、下拉自己画":
|
|
|
|
|
|
· 构造时**不传 `input`** ⇒ 高德不会往 DOM 里塞它自己的候选面板;
|
|
|
|
|
|
· 用户输入 → 防抖 → `ac.search(kw, cb)` → 拿 `result.tips`;
|
|
|
|
|
|
· 自己渲染每行「**地名 + 省市区 + 详细地址**」,地址在后、灰色小字。
|
|
|
|
|
|
⚠️ 选中后的全部既有行为(回填、`data-addr-detail`、框下提示、
|
|
|
|
|
|
`fb-addr-picked` 事件)**原样保留**,只是入口从高德的 `select`
|
|
|
|
|
|
事件换成我们自己那行候选的 click —— 见下面的 `pick()`。 */
|
|
|
|
|
|
var ac = new window.AMap.AutoComplete({});
|
|
|
|
|
|
var pick = function (poi) {
|
2026-10-14 01:20:40 +08:00
|
|
|
|
if (!poi) { return; }
|
|
|
|
|
|
|
|
|
|
|
|
// 输入框里放**名字**(用户认的是名字,不是"一串门牌号")
|
|
|
|
|
|
var name = String(poi.name || '').trim();
|
|
|
|
|
|
if (name !== '') { input.value = name; }
|
|
|
|
|
|
|
2026-10-20 21:35:47 +08:00
|
|
|
|
/* v68(用户 2026-10-20「获得准确的详细地址」):日程的地点只有这一栏,
|
|
|
|
|
|
所以把**详细地址**也带上 —— 但名字在前(人认的是名字),
|
|
|
|
|
|
地址在后,用中点分隔:`青岛大学(浮山校区) · 宁夏路308号`。
|
|
|
|
|
|
⚠️ 只对显式声明 `data-addr-detail="1"` 的输入框这么做:
|
|
|
|
|
|
旅程/影院那两处都有自己的 address 字段,不能往名字框里塞门牌。
|
|
|
|
|
|
⚠️ 必须**延后一拍**写:AutoComplete 在 select 回调之后还会自己把
|
|
|
|
|
|
`poi.name` 塞回输入框(实测,v68 第一版就因此白写)——
|
|
|
|
|
|
同步写会被它覆盖,看起来像"详细地址没生效"。 */
|
|
|
|
|
|
if (detail) {
|
2026-11-05 12:40:57 +08:00
|
|
|
|
/* 🔴🔴 2026-11-03 修(用户:「这并不是详细地址,详细地址应该是
|
|
|
|
|
|
青岛市黄岛区珠江路1018号」):
|
|
|
|
|
|
|
|
|
|
|
|
原来拼的是 `name · poi.address`,而高德的 `poi.address`
|
|
|
|
|
|
**只到街道级**(「珠江路1018号」),省市区在别的字段里。
|
|
|
|
|
|
⇒ 存进库里就是「招商银行(...) · 珠江路1018号」,
|
|
|
|
|
|
**缺城市**。鸿蒙端做地理编码时抓不到城市,
|
|
|
|
|
|
全国搜「珠江路1018号」→ 搜到**江苏南通**(差 500 公里)。
|
|
|
|
|
|
|
|
|
|
|
|
现在拼三段:`name · 市+区 + 街道门牌`
|
|
|
|
|
|
→ 「招商银行(...) · 青岛市黄岛区珠江路1018号」
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 用 `cityDistrictOf()` 而不是 `readPlace()`:
|
|
|
|
|
|
value 里不带省(省冗余、输入框显示不下、导航用不到)。
|
|
|
|
|
|
⚠️ 若高德连 city 都没给(极少数),退回原来的两段拼法,
|
|
|
|
|
|
至少不比改之前差。 */
|
|
|
|
|
|
var ad0 = String(poi.address || '').trim();
|
|
|
|
|
|
var cd = cityDistrictOf(poi);
|
|
|
|
|
|
if (name !== '') {
|
|
|
|
|
|
var tail = mergeAddrTail(cd, ad0);
|
|
|
|
|
|
if (tail !== '') {
|
|
|
|
|
|
var full = name + ' · ' + tail;
|
|
|
|
|
|
input.value = full;
|
|
|
|
|
|
setTimeout(function () { input.value = full; }, 0);
|
|
|
|
|
|
}
|
2026-10-20 21:35:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-14 01:20:40 +08:00
|
|
|
|
targets.forEach(function (t) {
|
|
|
|
|
|
var el = fieldFor(t);
|
|
|
|
|
|
if (!el) { return; }
|
|
|
|
|
|
// 🔴 只填**空白**的字段。理由(用户 2026-10-13 要的是"自动补齐"):
|
|
|
|
|
|
// 用户可能已经在地址栏手写了更细的门牌("3 号楼 2 单元"),
|
|
|
|
|
|
// 高德给的 `address` 只是"到路口/到楼"的粒度 —— 直接覆盖会把他写的东西抹掉,
|
|
|
|
|
|
// 而"抹掉用户输入"是不可逆的(他点保存后就真没了)。
|
|
|
|
|
|
// ⇒ 空着才补;已经有内容就尊重用户的。
|
|
|
|
|
|
if (String(el.value || '').trim() !== '') { return; }
|
2026-11-05 12:40:57 +08:00
|
|
|
|
/* 🔴 2026-11-05 修(用户:「城市…什么省也带上了,市也带上了,
|
|
|
|
|
|
区甚至都带上了,这没必要,直接城市就可以」)。
|
|
|
|
|
|
|
|
|
|
|
|
`city` 字段**不能用 `readPlace()`** —— 那个函数的用途是
|
|
|
|
|
|
「显示完整地名」(省 + 市 + 区),是给 `place` 输入框用的。
|
|
|
|
|
|
拿它填 city 会存进库里变成「山东省青岛市市南区」,
|
|
|
|
|
|
导致旅程的城市聚合统计里出现一长串行政区划。
|
|
|
|
|
|
|
|
|
|
|
|
⇒ `city` 单独走 `cityOnly()`:只取「青岛市」这一级。 */
|
|
|
|
|
|
if (t === 'place') {
|
2026-10-14 01:20:40 +08:00
|
|
|
|
var pl = readPlace(poi);
|
|
|
|
|
|
if (pl !== '') { el.value = pl; }
|
2026-11-05 12:40:57 +08:00
|
|
|
|
} else if (t === 'city') {
|
|
|
|
|
|
var co = cityOnly(poi);
|
|
|
|
|
|
if (co !== '') { el.value = co; }
|
2026-10-14 01:20:40 +08:00
|
|
|
|
} else if (t === 'address' || t === 'addr') {
|
|
|
|
|
|
var ad = String(poi.address || '').trim();
|
|
|
|
|
|
if (ad !== '') { el.value = ad; }
|
|
|
|
|
|
} else if (t === 'lng' || t === 'lat') {
|
|
|
|
|
|
var ll = readLngLat(poi.location);
|
|
|
|
|
|
var v = (t === 'lng') ? ll.lng : ll.lat;
|
|
|
|
|
|
if (v !== '') { el.value = v; }
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-20 21:35:47 +08:00
|
|
|
|
// v68:把"官方给的省市区 + 详细地址"显示在框下(不塞进 value,见 showAddrHint)
|
|
|
|
|
|
if (detail) { showAddrHint(input, poi); }
|
|
|
|
|
|
|
2026-10-14 01:20:40 +08:00
|
|
|
|
// 让调用方知道"值被程序改过"(有些页面要跟着重画/联动)
|
|
|
|
|
|
try {
|
|
|
|
|
|
input.dispatchEvent(new CustomEvent('fb-addr-picked', {
|
|
|
|
|
|
bubbles: true,
|
|
|
|
|
|
detail: { name: name, place: readPlace(poi), address: String(poi.address || '') }
|
|
|
|
|
|
}));
|
|
|
|
|
|
} catch (err) { /* 老浏览器没有 CustomEvent 构造器,忽略即可 */ }
|
2026-10-30 07:37:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var panel = makePanel(input, pick);
|
|
|
|
|
|
var timer = -1;
|
|
|
|
|
|
|
|
|
|
|
|
/* 输入 → 防抖 260ms → 查候选。
|
|
|
|
|
|
⚠️ 260ms 是本项目其它联想(观影片名)同款节奏,别乱调:
|
|
|
|
|
|
太快会在中文输入法拼字过程中反复请求(白烧配额、候选跳来跳去)。 */
|
|
|
|
|
|
var onInput = function () {
|
|
|
|
|
|
if (timer >= 0) { clearTimeout(timer); }
|
|
|
|
|
|
var kw = String(input.value || '').trim();
|
|
|
|
|
|
/* 少于 2 个字不查 —— 一个字的候选既多又没用,还会把高德配额吃掉。
|
|
|
|
|
|
⚠️ 但要把面板收起来:用户已经把字删掉了,留着旧候选会误点。 */
|
|
|
|
|
|
if (kw.length < 2) {
|
|
|
|
|
|
if (timer >= 0) { clearTimeout(timer); timer = -1; }
|
|
|
|
|
|
hidePanel(panel);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
timer = setTimeout(function () {
|
|
|
|
|
|
timer = -1;
|
|
|
|
|
|
try {
|
|
|
|
|
|
ac.search(kw, function (status, result) {
|
|
|
|
|
|
if (status !== 'complete' || !result) { hidePanel(panel); return; }
|
|
|
|
|
|
var tips = result.tips || [];
|
|
|
|
|
|
renderPanel(panel, tips, pick);
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) { hidePanel(panel); }
|
|
|
|
|
|
}, 260);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
input.addEventListener('input', onInput);
|
|
|
|
|
|
/* 点别处收起。用 `mousedown` 而不是 `click`:
|
|
|
|
|
|
`click` 在「点候选行」时也会冒泡上来,会把候选先收掉导致点不中。 */
|
|
|
|
|
|
var onDocDown = function (ev) {
|
|
|
|
|
|
if (panel.el && panel.el.contains(ev.target)) { return; }
|
|
|
|
|
|
if (ev.target === input) { return; }
|
|
|
|
|
|
hidePanel(panel);
|
|
|
|
|
|
};
|
|
|
|
|
|
document.addEventListener('mousedown', onDocDown);
|
|
|
|
|
|
|
2026-10-20 21:35:47 +08:00
|
|
|
|
input.setAttribute('data-addr-bound', '1');
|
2026-10-14 01:20:40 +08:00
|
|
|
|
} catch (err) {
|
2026-10-20 21:35:47 +08:00
|
|
|
|
/* SDK 已加载但初始化失败(key 没绑域名等)—— 静默降级、不打扰用户。
|
|
|
|
|
|
⚠️ 不写 `data-addr-bound`:下次 `fbAddr()` 还能再试(用户切页面/重开弹框)。 */
|
2026-10-14 01:20:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化(**可重复调用**)。
|
|
|
|
|
|
* @param {HTMLElement|Document} scope 限定范围;缺省 = 整页
|
|
|
|
|
|
*/
|
|
|
|
|
|
function fbAddr(scope) {
|
|
|
|
|
|
var root = scope || document;
|
|
|
|
|
|
var inputs = Array.prototype.slice.call(root.querySelectorAll('[data-addr]'));
|
|
|
|
|
|
inputs.forEach(function (el) { bindOne(el, root); });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.fbAddr = fbAddr;
|
|
|
|
|
|
|
2026-10-20 21:35:47 +08:00
|
|
|
|
/* v68:高德 SDK 是**异步**插入的(layout 的 fbAmapSdk),而我们可能先执行;
|
|
|
|
|
|
插件(AutoComplete)又是 SDK 之后的一跳。两个时序都得兜住:
|
|
|
|
|
|
· SDK 到了 → 拉插件 → 重扫(layout 的 s.onload 会派发 fb-amap-ready)
|
|
|
|
|
|
· 本脚本自己先跑(没有 SDK)→ 什么都不做,等事件 */
|
|
|
|
|
|
try {
|
|
|
|
|
|
window.addEventListener('fb-amap-ready', function () { ensurePlugin(); });
|
|
|
|
|
|
} catch (e) { /* 极老浏览器没有 addEventListener(不可能)—— 忽略 */ }
|
|
|
|
|
|
|
2026-10-14 01:20:40 +08:00
|
|
|
|
// 首屏跑一次。⚠️ 脚本是 defer 加载的,此时 DOM 已解析完(在 </body> 前引入亦可)。
|
|
|
|
|
|
fbAddr(document);
|
2026-10-20 21:35:47 +08:00
|
|
|
|
ensurePlugin(); // SDK 若已就位但插件没拉,这里补一次
|
2026-10-14 01:20:40 +08:00
|
|
|
|
|
|
|
|
|
|
/* 通知"我上线了"。
|
|
|
|
|
|
⚠️ 为什么需要:调用方(行程/影院/日程的弹框)可能在**本脚本加载之前**
|
|
|
|
|
|
就把 `fbLoadAddr()` 调过了 —— 那种情况下它的 `window.fbAddr` 判断会落空。
|
|
|
|
|
|
它们监听这个事件补一次(见 trips/node_form.php 的 initNodeForm)。 */
|
|
|
|
|
|
try {
|
|
|
|
|
|
window.dispatchEvent(new Event('fb-addr-ready'));
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
/* 老浏览器没有 Event 构造器(极少)—— 调用方还有 fbAddr 兜底,不影响功能 */
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|