diff --git a/apps/admin-web/src/components/TencentLocPickerModal.tsx b/apps/admin-web/src/components/TencentLocPickerModal.tsx index f848b5c..bdd5ff0 100644 --- a/apps/admin-web/src/components/TencentLocPickerModal.tsx +++ b/apps/admin-web/src/components/TencentLocPickerModal.tsx @@ -1,10 +1,9 @@ -import { useEffect, useMemo, useState } from 'react'; -import { Button, Modal, Space, Spin, Typography, message } from 'antd'; -import type { ClientRuntimeConfig } from '@dukang/shared-types'; +import { useEffect, useRef, useState } from 'react'; +import { Button, Empty, Input, List, Modal, Space, Spin, Typography, message } from 'antd'; import { request } from '../lib/api'; import { - buildTencentLocPickerUrl, - parseTencentLocPickerMessage, + placeToPicked, + type LbsPlaceItem, type TencentPickedLocation, } from '../lib/tencentLocPicker'; @@ -14,19 +13,14 @@ type Props = { onPick: (loc: TencentPickedLocation) => void; latitude?: number | null; longitude?: number | null; + /** 城市名,提升搜索相关性 */ + region?: string | null; }; -let cachedKey: string | null | undefined; - -async function loadTencentLbsKey(): Promise { - if (cachedKey) return cachedKey; - const cfg = await request('/common/client-config'); - const key = (cfg.tencentLbsKey || '').trim(); - if (!key) { - throw new Error('未配置腾讯位置服务 Key,请在系统设置中填写 TENCENT_LBS_KEY'); - } - cachedKey = key; - return key; +function hasCoords(lat: unknown, lng: unknown): lat is number { + const a = typeof lat === 'number' ? lat : Number(lat); + const b = typeof lng === 'number' ? lng : Number(lng); + return Number.isFinite(a) && Number.isFinite(b) && !(a === 0 && b === 0); } export default function TencentLocPickerModal({ @@ -35,64 +29,110 @@ export default function TencentLocPickerModal({ onPick, latitude, longitude, + region, }: Props) { - const [key, setKey] = useState(cachedKey ?? null); + const [keyword, setKeyword] = useState(''); + const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); const [pending, setPending] = useState(null); + const [hint, setHint] = useState('输入地点名称搜索,或加载附近地点'); + const seqRef = useRef(0); useEffect(() => { if (!open) { + setKeyword(''); + setItems([]); setPending(null); + setHint('输入地点名称搜索,或加载附近地点'); return; } - let cancelled = false; - setError(''); - if (key) return; - setLoading(true); - void loadTencentLbsKey() - .then((k) => { - if (!cancelled) setKey(k); - }) - .catch((e) => { - if (!cancelled) { - const msg = e instanceof Error ? e.message : '加载地图配置失败'; - setError(msg); - message.error(msg); - } - }) - .finally(() => { - if (!cancelled) setLoading(false); - }); - return () => { - cancelled = true; - }; - }, [open, key]); - - useEffect(() => { - if (!open) return; - function onMessage(event: MessageEvent) { - const picked = parseTencentLocPickerMessage(event.data); - if (!picked) return; - setPending(picked); + const lat = latitude != null ? Number(latitude) : NaN; + const lng = longitude != null ? Number(longitude) : NaN; + if (hasCoords(lat, lng)) { + void loadNearby(lat, lng); } - window.addEventListener('message', onMessage); - return () => window.removeEventListener('message', onMessage); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); - const src = useMemo(() => { - if (!key) return ''; - const lat = latitude != null ? Number(latitude) : undefined; - const lng = longitude != null ? Number(longitude) : undefined; - return buildTencentLocPickerUrl(key, { - latitude: lat != null && Number.isFinite(lat) ? lat : undefined, - longitude: lng != null && Number.isFinite(lng) ? lng : undefined, - }); - }, [key, latitude, longitude]); + async function loadNearby(lat: number, lng: number) { + const seq = ++seqRef.current; + setLoading(true); + setHint('正在加载附近地点…'); + try { + const res = await request<{ items: LbsPlaceItem[] }>( + `/common/lbs/nearby?lat=${encodeURIComponent(String(lat))}&lng=${encodeURIComponent(String(lng))}`, + ); + if (seq !== seqRef.current) return; + setItems(res.items ?? []); + setHint(res.items?.length ? `附近 ${res.items.length} 个地点,点击选择` : '附近暂无地点,请搜索'); + } catch (e) { + if (seq !== seqRef.current) return; + const msg = e instanceof Error ? e.message : '加载附近地点失败'; + setItems([]); + setHint(msg); + message.error(msg); + } finally { + if (seq === seqRef.current) setLoading(false); + } + } + + async function runSearch(q: string) { + const trimmed = q.trim(); + if (!trimmed) { + message.warning('请输入搜索关键词'); + return; + } + const seq = ++seqRef.current; + setLoading(true); + setHint('搜索中…'); + try { + const params = new URLSearchParams({ keyword: trimmed }); + if (region?.trim()) params.set('region', region.trim()); + const lat = latitude != null ? Number(latitude) : NaN; + const lng = longitude != null ? Number(longitude) : NaN; + if (Number.isFinite(lat) && Number.isFinite(lng)) { + params.set('lat', String(lat)); + params.set('lng', String(lng)); + } + const res = await request<{ items: LbsPlaceItem[] }>(`/common/lbs/suggest?${params.toString()}`); + if (seq !== seqRef.current) return; + setItems(res.items ?? []); + setHint(res.items?.length ? `找到 ${res.items.length} 个结果,点击选择` : '无匹配结果,换个关键词试试'); + } catch (e) { + if (seq !== seqRef.current) return; + const msg = e instanceof Error ? e.message : '搜索失败'; + setItems([]); + setHint(msg); + message.error(msg); + } finally { + if (seq === seqRef.current) setLoading(false); + } + } + + function useBrowserLocation() { + if (!navigator.geolocation) { + message.error('当前浏览器不支持定位'); + return; + } + setLoading(true); + navigator.geolocation.getCurrentPosition( + (pos) => { + const lat = pos.coords.latitude; + const lng = pos.coords.longitude; + setPending({ latitude: lat, longitude: lng, name: '当前位置' }); + void loadNearby(lat, lng); + }, + () => { + setLoading(false); + message.error('定位失败,请检查浏览器定位权限'); + }, + { enableHighAccuracy: true, timeout: 12000 }, + ); + } function confirmPick() { if (!pending) { - message.warning('请先在地图上选择或搜索一个位置'); + message.warning('请先从列表中选择一个地点'); return; } onPick(pending); @@ -101,12 +141,11 @@ export default function TencentLocPickerModal({ return ( @@ -114,7 +153,7 @@ export default function TencentLocPickerModal({ ? `${pending.latitude.toFixed(6)}, ${pending.longitude.toFixed(6)}${ pending.name ? ` · ${pending.name}` : '' }` - : '在地图中点选 / 搜索后,点击确认选点'} + : '搜索或选择附近地点后确认'} @@ -125,26 +164,67 @@ export default function TencentLocPickerModal({ } > - {loading ? ( -
- + + + setKeyword(e.target.value)} + onPressEnter={() => void runSearch(keyword)} + /> + + + + + {hint} + +
+ {loading && !items.length ? ( +
+ +
+ ) : items.length ? ( + { + const active = + pending?.latitude === item.latitude && pending?.longitude === item.longitude; + return ( + setPending(placeToPicked(item))} + > + + {item.address} +
+ + {item.latitude.toFixed(6)}, {item.longitude.toFixed(6)} + + + } + /> +
+ ); + }} + /> + ) : ( + + )}
- ) : error ? ( -
- {error} - - 若地图能开但看不到附近地点列表,请在腾讯位置服务控制台为该 Key 开启 - WebServiceAPI,并将白名单域名加入 apis.map.qq.com。 - -
- ) : src ? ( -