944306e45c
CI / verify (pull_request) Has been cancelled
Avoid locpicker formatted_addresses crash; proxy suggest/nearby/reverse via /common/lbs. Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
7.7 KiB
TypeScript
231 lines
7.7 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { Button, Empty, Input, List, Modal, Space, Spin, Typography, message } from 'antd';
|
|
import { request } from '../lib/api';
|
|
import {
|
|
placeToPicked,
|
|
type LbsPlaceItem,
|
|
type TencentPickedLocation,
|
|
} from '../lib/tencentLocPicker';
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onPick: (loc: TencentPickedLocation) => void;
|
|
latitude?: number | null;
|
|
longitude?: number | null;
|
|
/** 城市名,提升搜索相关性 */
|
|
region?: string | null;
|
|
};
|
|
|
|
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({
|
|
open,
|
|
onClose,
|
|
onPick,
|
|
latitude,
|
|
longitude,
|
|
region,
|
|
}: Props) {
|
|
const [keyword, setKeyword] = useState('');
|
|
const [items, setItems] = useState<LbsPlaceItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [pending, setPending] = useState<TencentPickedLocation | null>(null);
|
|
const [hint, setHint] = useState('输入地点名称搜索,或加载附近地点');
|
|
const seqRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setKeyword('');
|
|
setItems([]);
|
|
setPending(null);
|
|
setHint('输入地点名称搜索,或加载附近地点');
|
|
return;
|
|
}
|
|
const lat = latitude != null ? Number(latitude) : NaN;
|
|
const lng = longitude != null ? Number(longitude) : NaN;
|
|
if (hasCoords(lat, lng)) {
|
|
void loadNearby(lat, lng);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [open]);
|
|
|
|
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('请先从列表中选择一个地点');
|
|
return;
|
|
}
|
|
onPick(pending);
|
|
onClose();
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
title="地图选点(腾讯位置服务)"
|
|
open={open}
|
|
onCancel={onClose}
|
|
width={720}
|
|
destroyOnClose
|
|
footer={
|
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
|
<Typography.Text type="secondary" style={{ maxWidth: 420 }} ellipsis>
|
|
{pending
|
|
? `${pending.latitude.toFixed(6)}, ${pending.longitude.toFixed(6)}${
|
|
pending.name ? ` · ${pending.name}` : ''
|
|
}`
|
|
: '搜索或选择附近地点后确认'}
|
|
</Typography.Text>
|
|
<Space>
|
|
<Button onClick={onClose}>取消</Button>
|
|
<Button type="primary" disabled={!pending} onClick={confirmPick}>
|
|
确认选点
|
|
</Button>
|
|
</Space>
|
|
</Space>
|
|
}
|
|
>
|
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
|
<Space.Compact style={{ width: '100%' }}>
|
|
<Input
|
|
allowClear
|
|
placeholder="输入小区 / 写字楼 / 门店名称"
|
|
value={keyword}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
onPressEnter={() => void runSearch(keyword)}
|
|
/>
|
|
<Button type="primary" loading={loading} onClick={() => void runSearch(keyword)}>
|
|
搜索
|
|
</Button>
|
|
</Space.Compact>
|
|
<Space wrap>
|
|
<Button onClick={useBrowserLocation} disabled={loading}>
|
|
定位当前位置
|
|
</Button>
|
|
<Typography.Text type="secondary">{hint}</Typography.Text>
|
|
</Space>
|
|
<div style={{ height: 420, overflow: 'auto', border: '1px solid #f0f0f0', borderRadius: 8 }}>
|
|
{loading && !items.length ? (
|
|
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Spin tip="加载中…" />
|
|
</div>
|
|
) : items.length ? (
|
|
<List
|
|
size="small"
|
|
dataSource={items}
|
|
renderItem={(item) => {
|
|
const active =
|
|
pending?.latitude === item.latitude && pending?.longitude === item.longitude;
|
|
return (
|
|
<List.Item
|
|
style={{
|
|
cursor: 'pointer',
|
|
background: active ? 'rgba(22, 119, 255, 0.08)' : undefined,
|
|
paddingInline: 12,
|
|
}}
|
|
onClick={() => setPending(placeToPicked(item))}
|
|
>
|
|
<List.Item.Meta
|
|
title={item.title}
|
|
description={
|
|
<span>
|
|
{item.address}
|
|
<br />
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
{item.latitude.toFixed(6)}, {item.longitude.toFixed(6)}
|
|
</Typography.Text>
|
|
</span>
|
|
}
|
|
/>
|
|
</List.Item>
|
|
);
|
|
}}
|
|
/>
|
|
) : (
|
|
<Empty style={{ marginTop: 80 }} description={hint} />
|
|
)}
|
|
</div>
|
|
</Space>
|
|
</Modal>
|
|
);
|
|
}
|