行程:#47~#51 打包 —— 双轨状态、动态类型、动作按钮、去提示 (#51)

#47 编辑浮层居中、类型换行、去掉手动状态选择、按钮改胶囊
#48 计划/实际双轨:完成即转实际行程,取消≠删除,记真实开始/结束时间
#50 类型点击无反应(@Builder 值传参不建立依赖)
#51 节点类型动态加载(服务端9种,原端上硬编码5种)+ 地址结构化回调

API 新增:GET /trips/<id>/node-types、PATCH /trips/<id>/nodes/action
DB 新增:trip_nodes.actual_started_at / actual_ended_at
This commit is contained in:
2026-11-05 12:40:57 +08:00
parent a1140437a1
commit f58a128816
9 changed files with 1361 additions and 46 deletions
+225 -6
View File
@@ -118,6 +118,193 @@
* 实测两者不是同时都有。取到哪级用哪级,取不到就留空(**绝不瞎编**)。
* @returns {string} 形如 "青岛市 市南区" / "青岛" / ''(都没有)
*/
/**
* 只取「市 + 区」,**不带省** —— 给输入框 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();
}
function readPlace(poi) {
var a = poi.addressComponent || null;
var province = '', city = '', district = '';
@@ -422,11 +609,31 @@
`poi.name` 塞回输入框(实测,v68 第一版就因此白写)——
同步写会被它覆盖,看起来像"详细地址没生效"。 */
if (detail) {
var ad0 = String(poi.address || '').trim();
if (ad0 !== '' && name !== '') {
var full = name + ' · ' + ad0;
input.value = full;
setTimeout(function () { input.value = full; }, 0);
/* 🔴🔴 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);
}
}
}
@@ -439,9 +646,21 @@
// 而"抹掉用户输入"是不可逆的(他点保存后就真没了)。
// ⇒ 空着才补;已经有内容就尊重用户的。
if (String(el.value || '').trim() !== '') { return; }
if (t === 'place' || t === 'city') {
/* 🔴 2026-11-05 修(用户:「城市…什么省也带上了,市也带上了,
区甚至都带上了,这没必要,直接城市就可以」)。
`city` 字段**不能用 `readPlace()`** —— 那个函数的用途是
「显示完整地名」(省 + 市 + 区),是给 `place` 输入框用的。
拿它填 city 会存进库里变成「山东省青岛市市南区」,
导致旅程的城市聚合统计里出现一长串行政区划。
⇒ `city` 单独走 `cityOnly()`:只取「青岛市」这一级。 */
if (t === 'place') {
var pl = readPlace(poi);
if (pl !== '') { el.value = pl; }
} else if (t === 'city') {
var co = cityOnly(poi);
if (co !== '') { el.value = co; }
} else if (t === 'address' || t === 'addr') {
var ad = String(poi.address || '').trim();
if (ad !== '') { el.value = ad; }