地点输入:补地址联想,下拉带详细地址好区分同名地点 laoyang/FamilyBook#36
用户 2026-10-30:「在所有调用高德显示详细地址的地方…不能只把地点告诉我, 一定要把详细地址也放在后面。因为同一个地址可能有好几个, 我根本不知道应该选哪一个。」 网页端(addr_suggest.js)已经是这个行为,端上原本**连联想都没有**(纯文本框)。 本次把能力补到端上: - 新增 FbGeoRepo.ets:调服务端 /api/v1/geo/tips,最少 2 字才查 - 新增 FbAddrInput.ets:候选每行两行字(地名 14px 深色 + 详细地址 12px 灰), 同名地点靠第二行区分;防抖 260ms(与网页端同节奏);用 seq 丢弃过期响应 - 日程编辑、行程节点两处地点接入 🔴 走服务端代理而不是端上直连高德:高德 Web 服务 key 是服务端密钥, 打进安装包等于公开(反编译/抓包即得,配额会被跑光、key 可能被封)。 网页端用的 JS API key + 安全密钥是域名白名单那套,原生端没有等价机制。 ⚠️ 必须是 @ComponentV2 组件而不是 @Builder:@Builder 的值类型参数 不参与状态观察(SDK r/quick-rules/05-state.md),候选列表/展开态写在 @Builder 里会数据变了界面不重建。本项目在 FbEventChip/FbTagChip/ FbCalOptionRow 上各踩过一次。 ⚠️ 联想失败一律返回空候选 ⇒ 退化成普通文本框,手敲照常保存 (接口挂/没网/没配 key 都不能拖垮主流程)。 编译通过(33 tasks,17 executed)。
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 地址联想仓库(鸿蒙端)
|
||||
*
|
||||
* 对应服务端 `GET /api/v1/geo/tips?kw=<关键词>&limit=<条数>`。
|
||||
*
|
||||
* 🔴 2026-10-30(工单 #36)用户原话:
|
||||
* 「在所有调用高德显示详细地址的地方,也就是当我输入一个简单地址、
|
||||
* 下拉显示详细地址时,不能只把地点告诉我,一定要把详细地址也放在后面。
|
||||
* 因为如果只是把地址给我,同一个地址可能有好几个,
|
||||
* 我根本不知道应该选哪一个。
|
||||
* 所以应该把详细地址也放在后面,作为参考,让用户可以更好地选择。」
|
||||
*
|
||||
* 网页端(`assets/js/addr_suggest.js`)早已是这个行为,本次同步到端上。
|
||||
*
|
||||
* ⚠️ 为什么端上不直连高德、而要走服务端代理:
|
||||
* 高德 Web 服务 key 是**服务端密钥**,打进安装包等于公开
|
||||
* (反编译/抓包即可拿到,配额被跑光、key 还可能被封)。
|
||||
* 网页端用的是 JS API key + 安全密钥(域名白名单那套),原生端没有等价机制。
|
||||
* ⇒ 走代理。这也顺带把高德 tips 三套不齐的字段在服务端归一好了,
|
||||
* 端上只管画「名字 + detail」两行。
|
||||
*
|
||||
* ⚠️ 这是**可选能力**:接口挂了/没配 key/没网,一律返回空数组,
|
||||
* 输入框仍是普通文本框,手敲照常保存(不能让联想拖垮主流程)。
|
||||
*/
|
||||
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { FbApiOptions, FbApiResult, fbApi } from './FbHttp';
|
||||
import { FbGeoTip } from '../model/Types';
|
||||
|
||||
const DOMAIN: number = 0x0000;
|
||||
const TAG: string = 'FbGeoRepo';
|
||||
|
||||
/** 关键词最短长度:少于 2 个字不查(与网页端、服务端同一个阈值) */
|
||||
export const FB_GEO_MIN_LEN: number = 2;
|
||||
|
||||
/**
|
||||
* 地址联想。
|
||||
*
|
||||
* @param kw 用户已输入的关键词
|
||||
* @param token API 令牌(服务端要求登录:这是要花配额的接口)
|
||||
* @param limit 最多要几条(默认 10;端上视口有限,别要太多)
|
||||
* @returns 候选列表;**任何失败都返回空数组**,不抛错
|
||||
*/
|
||||
export async function fbGeoTips(kw: string, token: string, limit: number = 10): Promise<FbGeoTip[]> {
|
||||
const q: string = kw.trim();
|
||||
/* 本地先挡一道:省一次网络往返,也让"字太少"的抖动更小 */
|
||||
if (q.length < FB_GEO_MIN_LEN || token === '') {
|
||||
return [];
|
||||
}
|
||||
/* ⚠️ ArkTS 的 `arkts-no-untyped-obj-literals`:对象字面量必须有**显式类型**,
|
||||
否则编译报 10605038。这里显式标 `FbApiOptions`,
|
||||
内层 query 也标 `Record<string, string>`。 */
|
||||
const query: Record<string, string> = {
|
||||
'kw': q,
|
||||
'limit': limit.toString()
|
||||
};
|
||||
const opts: FbApiOptions = {
|
||||
token: token,
|
||||
query: query
|
||||
};
|
||||
const res: FbApiResult = await fbApi('/api/v1/geo/tips', opts);
|
||||
if (!res.ok) {
|
||||
/* ⚠️ 只记日志、不打扰用户:联想失败时用户正对着输入框,
|
||||
弹一个"地址联想失败"只会挡住他自己打字。 */
|
||||
hilog.warn(DOMAIN, TAG, '地址联想失败:%{public}s', res.error);
|
||||
return [];
|
||||
}
|
||||
const out: FbGeoTip[] = [];
|
||||
try {
|
||||
const rows: Record<string, Object>[] = JSON.parse(res.data) as Record<string, Object>[];
|
||||
for (const r of rows) {
|
||||
const name: string = str(r, 'name');
|
||||
if (name === '') {
|
||||
continue;
|
||||
}
|
||||
out.push({
|
||||
name: name,
|
||||
detail: str(r, 'detail'),
|
||||
district: str(r, 'district'),
|
||||
address: str(r, 'address'),
|
||||
lng: str(r, 'lng'),
|
||||
lat: str(r, 'lat')
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
hilog.error(DOMAIN, TAG, '地址联想解析失败:%{public}s', JSON.stringify(err));
|
||||
return [];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 从「任意 JSON 对象」里安全取字符串(高德/服务端都可能给 null 或数字) */
|
||||
function str(o: Record<string, Object>, k: string): string {
|
||||
const v: Object | undefined = o[k];
|
||||
if (typeof v === 'string') {
|
||||
return (v as string).trim();
|
||||
}
|
||||
if (typeof v === 'number') {
|
||||
return (v as number).toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
Reference in New Issue
Block a user