地点输入:补地址联想,下拉带详细地址好区分同名地点 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,179 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · 地址联想输入框(鸿蒙端)
|
||||||
|
*
|
||||||
|
* 🔴 2026-10-30(工单 #36)用户原话:
|
||||||
|
* 「在所有调用高德显示详细地址的地方,也就是当我输入一个简单地址、
|
||||||
|
* 下拉显示详细地址时,不能只把地点告诉我,一定要把详细地址也放在后面。
|
||||||
|
* 因为如果只是把地址给我,同一个地址可能有好几个,
|
||||||
|
* 我根本不知道应该选哪一个。
|
||||||
|
* 所以应该把详细地址也放在后面,作为参考,让用户可以更好地选择。」
|
||||||
|
*
|
||||||
|
* 网页端(`assets/js/addr_suggest.js`)已经是这个行为,本组件把它同步到端上。
|
||||||
|
* 候选每行两行字:**地名(深色)+ 详细地址(灰色小字)**,
|
||||||
|
* 同名地点靠第二行区分。
|
||||||
|
*
|
||||||
|
* 🔴🔴 为什么必须是**组件**而不是 `@Builder`(本项目反复踩过的坑):
|
||||||
|
* `@Builder` 的**值类型参数不参与状态观察**(SDK 规则 `r/quick-rules/05-state.md`:
|
||||||
|
* 按值传递「状态变化刷新 ❌」)。候选列表、展开/收起都是会变的,
|
||||||
|
* 写在 `@Builder` 里会出现"数据变了但界面不重建"——
|
||||||
|
* 表现为"输入了没反应""点候选没变化",而逻辑其实是对的。
|
||||||
|
* 见 `FbEventChip.ets` / `FbTagChip.ets` / `FbCalOptionRow.ets` 的文件头,
|
||||||
|
* 本项目在这个坑上栽过三次,统一解法是抽成 `@ComponentV2`。
|
||||||
|
*
|
||||||
|
* ⚠️ 联想的失败**绝不能影响主流程**:
|
||||||
|
* 接口挂、没网、没配 key ⇒ 一律拿到空候选 ⇒ 它就退化成普通输入框,
|
||||||
|
* 用户手敲照常保存。这条与网页端同一套降级策略。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
|
import { FbColors, FbMetric } from '../common/Theme';
|
||||||
|
import { FbGeoTip } from '../model/Types';
|
||||||
|
import { FB_GEO_MIN_LEN, fbGeoTips } from '../data/FbGeoRepo';
|
||||||
|
|
||||||
|
const DOMAIN: number = 0x0000;
|
||||||
|
const TAG: string = 'FbAddrInput';
|
||||||
|
|
||||||
|
/** 防抖时长(毫秒)—— 与网页端 `addr_suggest.js` 的 260ms 对齐 */
|
||||||
|
const DEBOUNCE_MS: number = 260;
|
||||||
|
|
||||||
|
@ComponentV2
|
||||||
|
export struct FbAddrInput {
|
||||||
|
/** 标题(如「地点」) */
|
||||||
|
@Param label: string = '地点';
|
||||||
|
/** 输入框占位文字 */
|
||||||
|
@Param placeholder: string = '';
|
||||||
|
/** 当前值(父组件持有,**受控**) */
|
||||||
|
@Param value: string = '';
|
||||||
|
/** API 令牌(空串 = 不联想,纯文本框) */
|
||||||
|
@Param token: string = '';
|
||||||
|
/** 最多显示几条候选(默认 8;端上视口有限) */
|
||||||
|
@Param limit: number = 8;
|
||||||
|
/** 值变化(用户打字 / 选中候选都会触发) */
|
||||||
|
@Event onValue: (v: string) => void = (_v: string): void => {
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 候选列表 */
|
||||||
|
@Local tips: FbGeoTip[] = [];
|
||||||
|
/** 是否展开候选面板 */
|
||||||
|
@Local open: boolean = false;
|
||||||
|
/** 防抖计时器句柄(-1 = 没有待执行) */
|
||||||
|
@Local private timer: number = -1;
|
||||||
|
/**
|
||||||
|
* 请求序号 —— 用来丢弃过期响应。
|
||||||
|
*
|
||||||
|
* 🔴 必须有:用户打字快时会有多个请求在飞,
|
||||||
|
* 先发的可能后回(网络抖动),直接铺上去会出现
|
||||||
|
* "输到第 4 个字,候选却是第 2 个字的结果"。
|
||||||
|
* 每次发起自增,回来时不等于当前值就丢弃。
|
||||||
|
*/
|
||||||
|
@Local private seq: number = 0;
|
||||||
|
|
||||||
|
/** 收起面板并清空候选 */
|
||||||
|
private close(): void {
|
||||||
|
this.open = false;
|
||||||
|
this.tips = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输入变化 → 防抖 → 查候选 */
|
||||||
|
private onType(v: string): void {
|
||||||
|
this.onValue(v); // 值先给父组件(保存用的是它,不能等联想)
|
||||||
|
if (this.timer >= 0) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = -1;
|
||||||
|
}
|
||||||
|
const kw: string = v.trim();
|
||||||
|
/* 字太少 ⇒ 不查,并把已有候选收起来。
|
||||||
|
⚠️ 一定要收:用户已经把字删了,留着旧候选会被误点。 */
|
||||||
|
if (kw.length < FB_GEO_MIN_LEN) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.timer = setTimeout((): void => {
|
||||||
|
this.timer = -1;
|
||||||
|
this.search(kw);
|
||||||
|
}, DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async search(kw: string): Promise<void> {
|
||||||
|
this.seq += 1;
|
||||||
|
const my: number = this.seq;
|
||||||
|
const list: FbGeoTip[] = await fbGeoTips(kw, this.token, this.limit);
|
||||||
|
if (my !== this.seq) {
|
||||||
|
return; // 已经有一次更新的输入了,这份丢弃
|
||||||
|
}
|
||||||
|
this.tips = list;
|
||||||
|
this.open = list.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选中一条候选 */
|
||||||
|
private pick(t: FbGeoTip): void {
|
||||||
|
this.onValue(t.name);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column({ space: 4 }) {
|
||||||
|
Text(this.label)
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(FbColors.faint)
|
||||||
|
|
||||||
|
TextInput({ text: this.value, placeholder: this.placeholder })
|
||||||
|
.placeholderColor(FbColors.faint)
|
||||||
|
.fontSize(FbMetric.fontSmall)
|
||||||
|
.fontColor(FbColors.ink)
|
||||||
|
.backgroundColor(FbColors.inputBg)
|
||||||
|
.borderRadius(FbMetric.radiusS)
|
||||||
|
.height(40)
|
||||||
|
.padding({ left: 12, right: 12 })
|
||||||
|
.onChange((v: string): void => {
|
||||||
|
this.onType(v);
|
||||||
|
})
|
||||||
|
|
||||||
|
/* 候选面板:只在有候选时占位(用 if 而不是 visibility,
|
||||||
|
否则空列表也会把下面的字段撑开一块空白)。 */
|
||||||
|
if (this.open && this.tips.length > 0) {
|
||||||
|
Column() {
|
||||||
|
ForEach(this.tips, (t: FbGeoTip, idx: number): void => {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
/* 第一行:地名(用户认的是名字) */
|
||||||
|
Text(t.name)
|
||||||
|
.fontSize(FbMetric.fontSmall)
|
||||||
|
.fontColor(FbColors.ink)
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.width('100%')
|
||||||
|
.textAlign(TextAlign.Start)
|
||||||
|
/* 第二行:详细地址 —— **这就是用户要的"参考"**。
|
||||||
|
高德对纯行政区划条目不给 address,那时 detail 就是省市区本身;
|
||||||
|
万一都空就整行不画(不要留一条空灰线)。 */
|
||||||
|
if (t.detail !== '') {
|
||||||
|
Text(t.detail)
|
||||||
|
.fontSize(FbMetric.fontTiny)
|
||||||
|
.fontColor(FbColors.faint)
|
||||||
|
.maxLines(2)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.width('100%')
|
||||||
|
.textAlign(TextAlign.Start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
|
||||||
|
.onClick((): void => {
|
||||||
|
this.pick(t);
|
||||||
|
})
|
||||||
|
}, (t: FbGeoTip, idx: number): string => t.name + '|' + t.detail + '|' + idx.toString())
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.backgroundColor(FbColors.surface)
|
||||||
|
.borderRadius(FbMetric.radiusS)
|
||||||
|
.border({ width: 1, color: FbColors.line })
|
||||||
|
/* 最多约 5 条高度,多了内部滚动 —— 不把整张表单顶下去 */
|
||||||
|
.constraintSize({ maxHeight: 260 })
|
||||||
|
.clip(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,9 @@ import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
|||||||
import { FB_CAL_SETTINGS } from '../data/FbCalSettings';
|
import { FB_CAL_SETTINGS } from '../data/FbCalSettings';
|
||||||
import { FbEventChip } from './FbEventChip';
|
import { FbEventChip } from './FbEventChip';
|
||||||
import { FbSheetRing } from './FbSheetRing';
|
import { FbSheetRing } from './FbSheetRing';
|
||||||
|
/* 🔴 2026-10-30(工单 #36):地点改成带联想的输入框 ——
|
||||||
|
用户要「输入简单地址 → 下拉里带详细地址,好区分同名地点」。 */
|
||||||
|
import { FbAddrInput } from './FbAddrInput';
|
||||||
/* 🔴 v64(用户 2026-10-19):「观影的开始/结束时间应该跟**日程里面的是一样的**。」
|
/* 🔴 v64(用户 2026-10-19):「观影的开始/结束时间应该跟**日程里面的是一样的**。」
|
||||||
⇒ 「日期 + 时间」那一套控件抽成了共用组件(见该文件头),日程与观影都用它。
|
⇒ 「日期 + 时间」那一套控件抽成了共用组件(见该文件头),日程与观影都用它。
|
||||||
⚠️ 本文件里原来的 `dateTimeRow` / `dateTimePickerSheet` / `cnDate` /
|
⚠️ 本文件里原来的 `dateTimeRow` / `dateTimePickerSheet` / `cnDate` /
|
||||||
@@ -519,8 +522,21 @@ export struct FbEventEditSheet {
|
|||||||
.margin({ top: 2 })
|
.margin({ top: 2 })
|
||||||
|
|
||||||
// ---- 地点 / 备注 ----
|
// ---- 地点 / 备注 ----
|
||||||
this.field('地点', this.fPlace, '地点(可选)', (v: string): void => {
|
/* 🔴 2026-10-30(工单 #36):地点换成**带联想**的输入框。
|
||||||
|
用户:「输入一个简单地址、下拉显示详细地址时,不能只把地点告诉我,
|
||||||
|
一定要把详细地址也放在后面。因为同一个地址可能有好几个,
|
||||||
|
我根本不知道应该选哪一个。」——与网页端 addr_suggest.js 同一行为。
|
||||||
|
⚠️ 选中候选后只把**地名**写进 location(与网页端日程那栏一致:
|
||||||
|
日程没有单独的"详细地址"字段,而带门牌会让后续按名字搜索变难;
|
||||||
|
详细地址在下拉里已经看到、能区分就够)。 */
|
||||||
|
FbAddrInput({
|
||||||
|
label: '地点',
|
||||||
|
placeholder: '地点(可选,输入后可从下拉选)',
|
||||||
|
value: this.fPlace,
|
||||||
|
token: this.token(),
|
||||||
|
onValue: (v: string): void => {
|
||||||
this.fPlace = v;
|
this.fPlace = v;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.area('备注', this.fNote, '备注(可选)', (v: string): void => {
|
this.area('备注', this.fNote, '备注(可选)', (v: string): void => {
|
||||||
this.fNote = v;
|
this.fNote = v;
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import { addTripNode, FbTripNodeDraft } from '../data/FbTripRepo';
|
|||||||
import { fetchVehicles } from '../data/FbTripSettingsRepo';
|
import { fetchVehicles } from '../data/FbTripSettingsRepo';
|
||||||
import { FbVehicleItem } from '../model/Types';
|
import { FbVehicleItem } from '../model/Types';
|
||||||
import { FbApiResult } from '../data/FbHttp';
|
import { FbApiResult } from '../data/FbHttp';
|
||||||
|
/* 🔴 2026-10-30(工单 #36):地点带联想(与日程编辑同一个组件) */
|
||||||
|
import { FbAddrInput } from './FbAddrInput';
|
||||||
|
|
||||||
/** 节点类型(⚠️ 与 `app/Trip.php` 的 `NODE_TYPES` 保持一致) */
|
/** 节点类型(⚠️ 与 `app/Trip.php` 的 `NODE_TYPES` 保持一致) */
|
||||||
interface FbNodeTypeOpt {
|
interface FbNodeTypeOpt {
|
||||||
@@ -213,9 +215,17 @@ export struct FbTripNodeSheet {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ---- 地点 ----
|
// ---- 地点 ----
|
||||||
this.textField('地点', '如「青岛北站」「崂山区·沙子口」', this.place,
|
/* 🔴 2026-10-30(工单 #36):换成带联想的输入框。
|
||||||
(v: string): void => {
|
行程地点是"出发地/目的地",同名地点(多个同名车站/商场)
|
||||||
|
最容易填错,详细地址作为参考尤其有用。 */
|
||||||
|
FbAddrInput({
|
||||||
|
label: '地点',
|
||||||
|
placeholder: '如「青岛北站」「崂山区·沙子口」',
|
||||||
|
value: this.place,
|
||||||
|
token: FB_TOKEN_STORE.get(),
|
||||||
|
onValue: (v: string): void => {
|
||||||
this.place = v;
|
this.place = v;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---- 时间(复用日程/观影那套日期时间控件)----
|
// ---- 时间(复用日程/观影那套日期时间控件)----
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
}
|
||||||
@@ -690,6 +690,37 @@ export interface FbCalBook {
|
|||||||
// 任务(tasks)
|
// 任务(tasks)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
|
地址联想(高德输入提示,走服务端代理)
|
||||||
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一条地址联想候选(`GET /api/v1/geo/tips`)。
|
||||||
|
*
|
||||||
|
* 🔴 2026-10-30(工单 #36)用户原话:
|
||||||
|
* 「在所有调用高德显示详细地址的地方…不能只把地点告诉我,
|
||||||
|
* 一定要把详细地址也放在后面。因为同一个地址可能有好几个,
|
||||||
|
* 我根本不知道应该选哪一个。」
|
||||||
|
* ⇒ `detail` 就是那个"作为参考"的详细地址,**必须画在名字下面**。
|
||||||
|
*/
|
||||||
|
export interface FbGeoTip {
|
||||||
|
/** 地点名(如「星巴克(万象城店)」)—— 用户认的是这个名字 */
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* 详细地址(省市区 + 门牌),**用来区分同名地点**。
|
||||||
|
* ⚠️ 纯行政区划条目高德不给 address,这时它就是省市区本身。
|
||||||
|
*/
|
||||||
|
detail: string;
|
||||||
|
/** 省市区 */
|
||||||
|
district: string;
|
||||||
|
/** 门牌 */
|
||||||
|
address: string;
|
||||||
|
/** 经度(字符串;没有时为空串) */
|
||||||
|
lng: string;
|
||||||
|
/** 纬度 */
|
||||||
|
lat: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 任务卡片(tasks/index.php 看板) */
|
/** 任务卡片(tasks/index.php 看板) */
|
||||||
export interface FbTaskItem {
|
export interface FbTaskItem {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user