pay-company/src/components/ModalsMapLeaflet.tsx

716 lines
21 KiB
TypeScript
Raw Normal View History

2026-01-08 16:35:06 +08:00
import { MyBetaModalFormProps } from '@/common';
import { MyModal } from '@/components/MyModal';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { useEffect, useRef, useState } from 'react';
// 修复 Leaflet 图标问题
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
// 创建自定义图标
const createCustomIcon = (color = 'red') => {
return L.divIcon({
className: 'custom-marker',
html: `
<div style="
background-color: ${color};
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
"></div>
`,
iconSize: [20, 20],
iconAnchor: [10, 10],
});
};
interface LocationResult {
lat: number;
lng: number;
address: string;
name?: string;
}
export default function ModalsMapLeaflet(
props: MyBetaModalFormProps & {
onChange?: (location?: LocationResult) => void;
},
) {
const modalRef = useRef<any>();
const mapRef = useRef<L.Map | null>(null);
const markersRef = useRef<L.LayerGroup | null>(null);
const [searchText, setSearchText] = useState('');
const [searchResults, setSearchResults] = useState<LocationResult[]>([]);
const [selectedLocation, setSelectedLocation] =
useState<LocationResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [mapReady, setMapReady] = useState(false);
const mapInitializedRef = useRef(false);
// 天地图配置
const TIANDITU_KEY = '4ce26ecef55ae1ec47910a72a098efc0';
const tiandituUrls = {
vector: `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`,
label: `https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`,
};
// 清理地图
useEffect(() => {
return () => {
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
mapInitializedRef.current = false;
setMapReady(false);
}
};
}, []);
// 初始化地图
const initializeMap = () => {
// 确保DOM已经渲染
setTimeout(() => {
const mapContainer = document.getElementById('map');
if (!mapContainer) {
console.error('地图容器不存在');
return;
}
// 如果地图已经存在,先清理
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
mapInitializedRef.current = false;
}
// 清理容器
while (mapContainer.firstChild) {
mapContainer.removeChild(mapContainer.firstChild);
}
try {
console.log('开始初始化地图...');
const map = L.map('map', {
center: [30.258134, 120.19382669582967],
zoom: 12,
zoomControl: true,
attributionControl: false,
});
mapRef.current = map;
mapInitializedRef.current = true;
// 添加天地图图层
const baseLayer = L.tileLayer(tiandituUrls.vector, {
attribution: '&copy; 天地图',
minZoom: 3,
maxZoom: 18,
}).addTo(map);
const labelLayer = L.tileLayer(tiandituUrls.label, {
minZoom: 3,
maxZoom: 18,
}).addTo(map);
// 创建标记图层组
markersRef.current = L.layerGroup().addTo(map);
// 添加地图点击事件
map.on('click', (e: L.LeafletMouseEvent) => {
handleMapClick(e.latlng.lat, e.latlng.lng);
});
// 添加缩放和比例尺控件
L.control.scale({ imperial: false }).addTo(map);
// 地图加载完成
map.whenReady(() => {
console.log('地图加载完成');
setMapReady(true);
});
console.log('地图初始化成功');
} catch (error) {
console.error('地图初始化失败:', error);
mapInitializedRef.current = false;
setMapReady(false);
}
}, 300);
};
// 使用天地图 REST API 进行搜索
const searchWithTiandituAPI = async (
keyword: string,
): Promise<LocationResult[]> => {
try {
const url = `https://api.tianditu.gov.cn/v2/search?postStr={
"keyWord": "${encodeURIComponent(keyword)}",
"level": "12",
"mapBound": "115.38333,39.36667,117.68333,41.23333",
"queryType": "1",
"start": "0",
"count": "20"
}&type=query&tk=${TIANDITU_KEY}`;
console.log('搜索URL:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('天地图搜索返回数据:', data);
// 根据不同的结果类型处理
const resultType = data.resultType;
switch (resultType) {
case 1: // POI点数据
if (data.pois && Array.isArray(data.pois)) {
return data.pois.map((poi: any) => ({
name: poi.name,
address: poi.address,
lng: parseFloat(poi.lonlat.split(',')[0]),
lat: parseFloat(poi.lonlat.split(',')[1]),
}));
}
break;
case 2: // 推荐城市
// 这里可以处理推荐城市,暂时返回空数组
console.log('推荐城市结果:', data.prompt);
return [];
case 3: // 行政区划
if (data.area) {
// 将行政区划转换为一个位置结果
const area = data.area;
const [lng, lat] = area.lonlat.split(',').map(parseFloat);
return [
{
name: area.name,
address: area.name,
lng: lng,
lat: lat,
},
];
}
break;
case 4: // 建议词
console.log('建议词结果:', data.prompt);
return [];
case 5: // 公交信息
console.log('公交信息结果:', data.lineData);
return [];
default:
console.log('未知结果类型:', resultType);
}
// 如果没有找到有效结果,尝试使用提示信息
if (data.prompt && data.prompt.length > 0) {
const prompt = data.prompt[0];
if (prompt.admins && prompt.admins.length > 0) {
const admin = prompt.admins[0];
return [
{
name: admin.name,
address: admin.name,
lng: parseFloat(admin.lonlat.split(',')[0]),
lat: parseFloat(admin.lonlat.split(',')[1]),
},
];
}
}
throw new Error('未找到相关地点');
} catch (error) {
console.error('天地图搜索API调用失败:', error);
throw error;
}
};
// 搜索地点 - 使用天地图 REST API
const handleSearch = async () => {
if (!searchText.trim()) {
alert('请输入搜索关键词');
return;
}
// 检查天地图密钥
if (!TIANDITU_KEY) {
alert('请配置有效的天地图密钥');
return;
}
if (!mapReady) {
alert('地图尚未准备好,请稍后重试');
return;
}
setIsLoading(true);
try {
console.log('开始天地图搜索:', searchText);
// 清空之前的结果
setSearchResults([]);
if (markersRef.current) {
markersRef.current.clearLayers();
}
// 执行搜索
const results = await searchWithTiandituAPI(searchText);
console.log('搜索成功,结果数量:', results.length);
if (results.length === 0) {
alert('未找到相关地点');
return;
}
setSearchResults(results);
// 在地图上显示搜索结果
const bounds = L.latLngBounds([]);
results.forEach((result, index) => {
bounds.extend([result.lat, result.lng]);
addSearchResultMarker(result, index, results.length);
});
// 调整地图视野
if (mapRef.current) {
mapRef.current.fitBounds(bounds.pad(0.1));
}
} catch (error: any) {
console.error('搜索失败:', error);
alert(`搜索失败: ${error.message || '未知错误'}`);
} finally {
setIsLoading(false);
}
};
// 添加搜索结果标记
const addSearchResultMarker = (
location: LocationResult,
index: number,
total: number,
) => {
if (!markersRef.current) return;
const marker = L.marker([location.lat, location.lng], {
icon: createCustomIcon(index === 0 ? '#1890ff' : '#52c41a'),
}).addTo(markersRef.current);
marker.bindPopup(`
<div style="min-width: 220px;">
<h4 style="margin: 0 0 8px 0; color: #1890ff; font-size: 14px;">${
location.name
}</h4>
<p style="margin: 4px 0; font-size: 12px;"><strong>:</strong> ${
location.address
}</p>
<p style="margin: 4px 0; font-size: 12px;"><strong>:</strong> ${location.lat.toFixed(
6,
)}, ${location.lng.toFixed(6)}</p>
<div style="margin-top: 10px; display: flex; gap: 8px;">
<button onclick="window.selectSearchResult(${index})" style="
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
flex: 1;
"></button>
</div>
</div>
`);
// 第一个结果自动打开弹窗
if (index === 0) {
setTimeout(() => marker.openPopup(), 500);
}
// 将选择函数挂载到window对象
(window as any).selectSearchResult = (index: number) => {
console.log(location, searchResults, 'selectSearchResult');
if (searchResults && searchResults[index]) {
handleSelectResult(searchResults[index]);
} else {
handleConfirmLocation(location);
}
};
};
// 地图点击事件处理
const handleMapClick = async (lat: number, lng: number) => {
try {
setIsLoading(true);
// 使用天地图逆地理编码获取真实地址
const address = await reverseGeocodeWithTianditu(lat, lng);
const location: LocationResult = {
lat,
lng,
address,
name: '点击选择的位置',
};
setSelectedLocation(location);
addMarkerToMap(location);
} catch (error) {
console.error('获取地址信息失败:', error);
} finally {
setIsLoading(false);
}
};
// 使用天地图逆地理编码服务
const reverseGeocodeWithTianditu = async (
lat: number,
lng: number,
): Promise<string> => {
try {
const url = `https://api.tianditu.gov.cn/geocoder?postStr={'lon':${lng},'lat':${lat},'ver':1}&type=geocode&tk=${TIANDITU_KEY}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('逆地理编码请求失败');
}
const data = await response.json();
if (data.status === '0' && data.result) {
return data.result.formatted_address;
}
return `位置 ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
} catch (error) {
console.error('天地图逆地理编码失败:', error);
return `位置 ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
}
};
// 添加标记到地图
const addMarkerToMap = (location: LocationResult) => {
if (!mapRef.current || !markersRef.current) return;
markersRef.current.clearLayers();
const marker = L.marker([location.lat, location.lng], {
icon: createCustomIcon('#1890ff'),
}).addTo(markersRef.current);
marker
.bindPopup(
`
<div style="min-width: 200px;">
<h4 style="margin: 0 0 8px 0; color: #1890ff;"></h4>
<p style="margin: 4px 0;"><strong>:</strong> ${location.address}</p>
<p style="margin: 4px 0;"><strong>:</strong> ${location.lat.toFixed(
6,
)}, ${location.lng.toFixed(6)}</p>
<button onclick="window.confirmSelection()" style="
margin-top: 8px;
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
"></button>
</div>
`,
)
.openPopup();
(window as any).confirmSelection = () => {
console.log(location, 'confirmSelection');
handleConfirmLocation(location);
};
mapRef.current.setView([location.lat, location.lng], 15);
};
// 选择搜索结果
const handleSelectResult = (result: LocationResult) => {
setSelectedLocation(result);
addMarkerToMap(result);
setSearchResults([]);
setSearchText(result.name || result.address);
};
// 确认选择位置
const handleConfirmLocation = (location?: LocationResult) => {
const finalLocation = location || selectedLocation;
if (!finalLocation) return;
props?.onChange?.(location);
// if (props.onConfirm) {
// props.onConfirm(finalLocation);
// }
if (modalRef.current) {
modalRef.current.close();
}
};
// 清除选择
const handleClearSelection = () => {
setSelectedLocation(null);
setSearchText('');
setSearchResults([]);
if (markersRef.current) {
markersRef.current.clearLayers();
}
if (mapRef.current) {
mapRef.current.setView([30.258134, 120.19382669582967], 12);
}
};
return (
<MyModal
title={'获取经纬度'}
width="1000px"
myRef={modalRef}
size="middle"
onOpen={() => {
console.log('模态框打开,初始化地图...');
setTimeout(initializeMap, 500);
}}
node={
<div style={{ padding: '20px' }}>
{/* 状态提示 */}
{!mapReady && (
<div
style={{
padding: '10px',
backgroundColor: '#fffbe6',
border: '1px solid #ffe58f',
borderRadius: '4px',
marginBottom: '10px',
textAlign: 'center',
}}
>
...
</div>
)}
{/* 搜索区域 */}
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="请输入地点名称进行搜索(如:杭州中大广场..."
style={{
flex: 1,
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
}}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
/>
<button
onClick={handleSearch}
disabled={isLoading || !mapReady}
style={{
padding: '8px 16px',
background: !isLoading && mapReady ? '#1890ff' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isLoading || !mapReady ? 'not-allowed' : 'pointer',
}}
>
{isLoading ? '搜索中...' : !mapReady ? '地图加载中' : '搜索'}
</button>
</div>
{/* 搜索结果 */}
{searchResults.length > 0 && (
<div
style={{
border: '1px solid #e8e8e8',
borderRadius: '4px',
maxHeight: '200px',
overflowY: 'auto',
backgroundColor: '#fafafa',
}}
>
<div
style={{
padding: '8px',
fontSize: '12px',
color: '#666',
borderBottom: '1px solid #e8e8e8',
}}
>
{searchResults.length}
</div>
{searchResults.map((result, index) => (
<div
key={index}
onClick={() => handleSelectResult(result)}
style={{
padding: '10px',
borderBottom: '1px solid #f0f0f0',
cursor: 'pointer',
backgroundColor: index === 0 ? '#e6f7ff' : '#fafafa',
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = '#e6f7ff')
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor =
index === 0 ? '#e6f7ff' : '#fafafa')
}
>
<div
style={{
fontWeight: 'bold',
marginBottom: '4px',
color: '#1890ff',
}}
>
{result.name || '未知地点'}
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{result.address}
</div>
<div
style={{
fontSize: '11px',
color: '#999',
marginTop: '2px',
}}
>
: {result.lat.toFixed(6)}, {result.lng.toFixed(6)}
</div>
</div>
))}
</div>
)}
</div>
{/* 地图容器 */}
<div
id="map"
style={{
height: '400px',
border: '1px solid #e8e8e8',
borderRadius: '4px',
}}
/>
{/* 选择信息显示 */}
{selectedLocation && (
<div
style={{
marginTop: '20px',
padding: '15px',
backgroundColor: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
}}
>
<h4 style={{ margin: '0 0 10px 0', color: '#52c41a' }}>
</h4>
<p style={{ margin: '4px 0' }}>
<strong>:</strong> {selectedLocation.address}
</p>
<p style={{ margin: '4px 0' }}>
<strong>:</strong> {selectedLocation.lat.toFixed(6)},{' '}
{selectedLocation.lng.toFixed(6)}
</p>
<div style={{ marginTop: '10px', display: 'flex', gap: '10px' }}>
<button
onClick={() => handleConfirmLocation(selectedLocation)}
style={{
padding: '8px 16px',
background: '#52c41a',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold',
}}
>
</button>
<button
onClick={handleClearSelection}
style={{
padding: '8px 16px',
background: '#ff4d4f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
</button>
</div>
</div>
)}
{/* 使用说明 */}
<div
style={{
marginTop: '15px',
padding: '10px',
backgroundColor: '#f0f8ff',
border: '1px solid #91d5ff',
borderRadius: '4px',
fontSize: '12px',
color: '#666',
}}
>
<strong>使:</strong>
<ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
<li></li>
<li>使API</li>
<li>使</li>
<li></li>
<li></li>
<li>14</li>
</ul>
{!TIANDITU_KEY && (
<div style={{ color: '#ff4d4f', marginTop: '8px' }}>
</div>
)}
</div>
</div>
}
></MyModal>
);
}