pay-company/src/components/ModalsMapLeaflet.tsx
Your Name 9a2e1afe56
All checks were successful
Build and Push Docker Image / build (push) Successful in 5m10s
feat:初始化
2026-01-08 16:35:06 +08:00

716 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}