From f396cb1fef32aeb9695fbdc9b681830b2fa626ac Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Wed, 29 Jul 2026 12:56:51 +0800 Subject: [PATCH] feat(partner): map pick UX and Tencent LBS SN signature Hide locate on store forms; restore optimized locpicker overlay. Add TENCENT_LBS_SECRET_KEY and server-side sig for WebServiceAPI. Co-authored-by: Cursor --- .../components/TencentLocPickerOverlay.tsx | 344 +++++++++--------- apps/h5-partner/src/lib/tencentLocPicker.ts | 75 ++++ apps/h5-partner/src/pages/StoreCreatePage.tsx | 31 +- apps/h5-partner/src/pages/StoreDetailPage.tsx | 34 +- packages/shared-types/src/config.ts | 8 +- server/dukang-api/.env.example | 5 +- .../system-config/system-config.registry.ts | 20 +- .../integrations/map/tencent-lbs.provider.ts | 71 ++-- .../src/integrations/map/tencent-lbs.sign.ts | 39 ++ 9 files changed, 349 insertions(+), 278 deletions(-) create mode 100644 server/dukang-api/src/integrations/map/tencent-lbs.sign.ts diff --git a/apps/h5-partner/src/components/TencentLocPickerOverlay.tsx b/apps/h5-partner/src/components/TencentLocPickerOverlay.tsx index 1ac0b05..52af88d 100644 --- a/apps/h5-partner/src/components/TencentLocPickerOverlay.tsx +++ b/apps/h5-partner/src/components/TencentLocPickerOverlay.tsx @@ -1,8 +1,9 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import type { ClientRuntimeConfig } from '@dukang/shared-types'; import { request } from '../lib/api'; import { - placeToPicked, - type LbsPlaceItem, + buildTencentLocPickerUrl, + parseTencentLocPickerMessage, type TencentPickedLocation, } from '../lib/tencentLocPicker'; @@ -12,117 +13,84 @@ type Props = { onPick: (loc: TencentPickedLocation) => void; latitude?: number | null; longitude?: number | null; + /** 保留兼容,iframe 选点不使用 */ region?: string | null; }; +let cachedKey: string | null | undefined; + +async function loadTencentLbsKey(): Promise { + if (cachedKey) return cachedKey; + const cfg = await request('PARTNER_H5', '/common/client-config', { + silent: true, + }); + const key = (cfg.tencentLbsKey || '').trim(); + if (!key) { + throw new Error('未配置腾讯位置服务 Key,请联系管理员在系统设置中配置'); + } + cachedKey = key; + return key; +} + export default function TencentLocPickerOverlay({ open, onClose, onPick, latitude, longitude, - region, }: Props) { - const [keyword, setKeyword] = useState(''); - const [items, setItems] = useState([]); + const [key, setKey] = useState(cachedKey ?? null); const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); const [pending, setPending] = useState(null); - const [hint, setHint] = useState('输入地点名称搜索'); - const seqRef = useRef(0); + const [iframeReady, setIframeReady] = useState(false); useEffect(() => { if (!open) { - setKeyword(''); - setItems([]); setPending(null); - setHint('输入地点名称搜索'); + setIframeReady(false); + setError(''); return; } - const lat = latitude != null ? Number(latitude) : NaN; - const lng = longitude != null ? Number(longitude) : NaN; - if (Number.isFinite(lat) && Number.isFinite(lng) && !(lat === 0 && lng === 0)) { - void loadNearby(lat, lng); + let cancelled = false; + if (key) return; + setLoading(true); + void loadTencentLbsKey() + .then((k) => { + if (!cancelled) setKey(k); + }) + .catch((e) => { + if (!cancelled) setError(e instanceof Error ? e.message : '加载地图配置失败'); + }) + .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); } - // eslint-disable-next-line react-hooks/exhaustive-deps + window.addEventListener('message', onMessage); + return () => window.removeEventListener('message', onMessage); }, [open]); - async function loadNearby(lat: number, lng: number) { - const seq = ++seqRef.current; - setLoading(true); - setHint('正在加载附近地点…'); - try { - const res = await request<{ items: LbsPlaceItem[] }>( - 'PARTNER_H5', - `/common/lbs/nearby?lat=${encodeURIComponent(String(lat))}&lng=${encodeURIComponent(String(lng))}`, - { silent: true }, - ); - if (seq !== seqRef.current) return; - setItems(res.items ?? []); - setHint(res.items?.length ? `附近 ${res.items.length} 个地点` : '附近暂无地点,请搜索'); - } catch (e) { - if (seq !== seqRef.current) return; - setItems([]); - setHint(e instanceof Error ? e.message : '加载附近地点失败'); - } finally { - if (seq === seqRef.current) setLoading(false); - } - } - - async function runSearch() { - const trimmed = keyword.trim(); - if (!trimmed) { - setHint('请输入搜索关键词'); - 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[] }>( - 'PARTNER_H5', - `/common/lbs/suggest?${params.toString()}`, - { silent: true }, - ); - if (seq !== seqRef.current) return; - setItems(res.items ?? []); - setHint(res.items?.length ? `找到 ${res.items.length} 个结果` : '无匹配结果'); - } catch (e) { - if (seq !== seqRef.current) return; - setItems([]); - setHint(e instanceof Error ? e.message : '搜索失败'); - } finally { - if (seq === seqRef.current) setLoading(false); - } - } - - function useBrowserLocation() { - if (!navigator.geolocation) { - setHint('当前浏览器不支持定位'); - 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); - setHint('定位失败,请检查定位权限'); - }, - { enableHighAccuracy: true, timeout: 12000 }, - ); - } + 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]); if (!open) return null; @@ -134,6 +102,7 @@ export default function TencentLocPickerOverlay({ return (
- - 地图选点 + 腾讯地图选点
-
-
- setKeyword(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') void runSearch(); - }} - placeholder="输入小区 / 写字楼 / 门店名称" - style={{ - flex: 1, - padding: '10px 12px', - border: '1px solid rgba(0,0,0,0.12)', - borderRadius: 8, - fontSize: 14, - }} - /> - + {pending ? ( +
+
+ 已选位置 +
+
+ {pending.name || '地图选点'} + {pending.address ? ` · ${pending.address}` : ''} +
+ {pending.latitude.toFixed(6)}, {pending.longitude.toFixed(6)} +
-
- - {hint} -
- {pending ? ( -

- 已选 {pending.latitude.toFixed(6)}, {pending.longitude.toFixed(6)} - {pending.name ? ` · ${pending.name}` : ''} -

- ) : null} -
+ ) : ( +

+ 在地图上点选或搜索地点,选中后点右上角「确认」 +

+ )} -
- {loading && !items.length ? ( +
+ {loading ? (

- 加载中… + 正在加载腾讯地图…

- ) : items.length ? ( -
    - {items.map((item) => { - const active = - pending?.latitude === item.latitude && pending?.longitude === item.longitude; - return ( -
  • - -
  • - ); - })} -
- ) : ( -

- {hint} -

- )} + ) : error ? ( +
+

+ {error} +

+

+ 请确认系统设置中已配置腾讯位置服务 Key,并白名单 apis.map.qq.com。 +

+
+ ) : src ? ( + <> + {!iframeReady ? ( +

+ 地图加载中… +

+ ) : null} +