|
@@ -248,71 +248,224 @@ const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
|
|
|
};
|
|
};
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 4. 实时地图组件
|
|
|
|
|
|
|
+### 4. 高德地图组件(国内地图)
|
|
|
```typescript
|
|
```typescript
|
|
|
-import { MapContainer, TileLayer, CircleMarker, Popup } from 'react-leaflet';
|
|
|
|
|
-import 'leaflet/dist/leaflet.css';
|
|
|
|
|
|
|
+import { useEffect, useRef, useState } from 'react';
|
|
|
|
|
+import { motion } from 'framer-motion';
|
|
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
|
|
+
|
|
|
|
|
+// 高德地图组件
|
|
|
|
|
+declare global {
|
|
|
|
|
+ interface Window {
|
|
|
|
|
+ AMap: any;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface MapData {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ lat: number;
|
|
|
|
|
+ lng: number;
|
|
|
|
|
+ value: number;
|
|
|
|
|
+ details?: any;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ChinaAMapProps {
|
|
|
|
|
+ data: MapData[];
|
|
|
|
|
+ height?: string;
|
|
|
|
|
+ className?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ChinaAMap = ({ data, height = "100%", className = "" }: ChinaAMapProps) => {
|
|
|
|
|
+ const mapRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
+ const [map, setMap] = useState<any>(null);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (!window.AMap || !mapRef.current) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建地图实例
|
|
|
|
|
+ const newMap = new window.AMap.Map(mapRef.current, {
|
|
|
|
|
+ zoom: 5,
|
|
|
|
|
+ center: [104.195397, 35.86166], // 中国中心坐标
|
|
|
|
|
+ viewMode: '3D',
|
|
|
|
|
+ pitch: 30,
|
|
|
|
|
+ mapStyle: 'amap://styles/darkblue', // 深色主题
|
|
|
|
|
+ resizeEnable: true,
|
|
|
|
|
+ showBuildingBlock: false,
|
|
|
|
|
+ showIndoorMap: false
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 添加控件
|
|
|
|
|
+ new window.AMap.plugin(['AMap.Scale', 'AMap.ToolBar'], () => {
|
|
|
|
|
+ newMap.addControl(new window.AMap.Scale());
|
|
|
|
|
+ newMap.addControl(new window.AMap.ToolBar({
|
|
|
|
|
+ position: 'RT',
|
|
|
|
|
+ offset: new window.AMap.Pixel(10, 10)
|
|
|
|
|
+ }));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ setMap(newMap);
|
|
|
|
|
+
|
|
|
|
|
+ // 添加标记点
|
|
|
|
|
+ if (data && data.length > 0) {
|
|
|
|
|
+ data.forEach((item) => {
|
|
|
|
|
+ const circleMarker = new window.AMap.CircleMarker({
|
|
|
|
|
+ center: [item.lng, item.lat],
|
|
|
|
|
+ radius: Math.max(10, Math.min(40, item.value / 100)),
|
|
|
|
|
+ strokeColor: '#00ff88',
|
|
|
|
|
+ strokeWeight: 2,
|
|
|
|
|
+ fillColor: '#00ff88',
|
|
|
|
|
+ fillOpacity: 0.6,
|
|
|
|
|
+ zIndex: 100
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 添加点击事件
|
|
|
|
|
+ circleMarker.on('click', () => {
|
|
|
|
|
+ const infoWindow = new window.AMap.InfoWindow({
|
|
|
|
|
+ content: `
|
|
|
|
|
+ <div class="bg-black/80 backdrop-blur-md text-white p-4 rounded-lg border border-white/20">
|
|
|
|
|
+ <h4 class="font-bold text-lg mb-2 text-cyan-400">${item.name}</h4>
|
|
|
|
|
+ <div class="space-y-1 text-sm">
|
|
|
|
|
+ <div>数值: ${item.value}</div>
|
|
|
|
|
+ ${item.details ? `<div>详情: ${JSON.stringify(item.details)}</div>` : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `,
|
|
|
|
|
+ offset: new window.AMap.Pixel(0, -30)
|
|
|
|
|
+ });
|
|
|
|
|
+ infoWindow.open(newMap, [item.lng, item.lat]);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ newMap.add(circleMarker);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ newMap.destroy();
|
|
|
|
|
+ };
|
|
|
|
|
+ }, [data]);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className={`relative ${className}`} style={{ height }}>
|
|
|
|
|
+ <div ref={mapRef} className="w-full h-full rounded-xl overflow-hidden" />
|
|
|
|
|
+
|
|
|
|
|
+ {/* 地图标题 */}
|
|
|
|
|
+ <motion.div
|
|
|
|
|
+ className="absolute top-4 left-4 bg-black/50 backdrop-blur-sm px-4 py-2 rounded-lg border border-white/10"
|
|
|
|
|
+ initial={{ opacity: 0, y: -20 }}
|
|
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
+ transition={{ duration: 0.5 }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <h3 className="text-xl font-bold text-white">全国数据分布</h3>
|
|
|
|
|
+ <p className="text-sm text-gray-300">点击查看详情</p>
|
|
|
|
|
+ </motion.div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 加载提示 */}
|
|
|
|
|
+ {!map && (
|
|
|
|
|
+ <div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-xl">
|
|
|
|
|
+ <div className="text-cyan-400">正在加载高德地图...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 地图加载器组件
|
|
|
|
|
+const AMapLoader = ({ apiKey, children }: { apiKey: string; children: React.ReactNode }) => {
|
|
|
|
|
+ const [loaded, setLoaded] = useState(false);
|
|
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const loadAMap = async () => {
|
|
|
|
|
+ if (window.AMap) {
|
|
|
|
|
+ setLoaded(true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const script = document.createElement('script');
|
|
|
|
|
+ script.src = `https://webapi.amap.com/maps?v=2.0&key=${apiKey}&plugin=AMap.Scale,AMap.ToolBar`;
|
|
|
|
|
+ script.async = true;
|
|
|
|
|
+
|
|
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
|
|
+ script.onload = resolve;
|
|
|
|
|
+ script.onerror = () => reject(new Error('Failed to load AMap'));
|
|
|
|
|
+ document.head.appendChild(script);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ setLoaded(true);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setError(err instanceof Error ? err.message : 'Unknown error');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ loadAMap();
|
|
|
|
|
+ }, [apiKey]);
|
|
|
|
|
+
|
|
|
|
|
+ if (error) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex items-center justify-center h-full bg-black/50 rounded-xl">
|
|
|
|
|
+ <div className="text-center text-red-400">
|
|
|
|
|
+ <div className="text-lg mb-2">地图加载失败</div>
|
|
|
|
|
+ <div className="text-sm">{error}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ if (!loaded) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex items-center justify-center h-full bg-black/50 rounded-xl">
|
|
|
|
|
+ <div className="text-center">
|
|
|
|
|
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-cyan-400 mx-auto mb-4"></div>
|
|
|
|
|
+ <div className="text-cyan-400">正在加载高德地图...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return <>{children}</>;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 使用示例
|
|
|
const RealTimeMap = () => {
|
|
const RealTimeMap = () => {
|
|
|
const [positions, setPositions] = useState([]);
|
|
const [positions, setPositions] = useState([]);
|
|
|
|
|
|
|
|
// 实时数据获取
|
|
// 实时数据获取
|
|
|
const { data: mapData } = useQuery({
|
|
const { data: mapData } = useQuery({
|
|
|
- queryKey: ['real-time-positions'],
|
|
|
|
|
- queryFn: () => apiClient.map.$get(),
|
|
|
|
|
|
|
+ queryKey: ['china-cities-data'],
|
|
|
|
|
+ queryFn: () => apiClient.chinaCities.$get(),
|
|
|
refetchInterval: 5000,
|
|
refetchInterval: 5000,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (mapData) {
|
|
if (mapData) {
|
|
|
- setPositions(mapData.positions);
|
|
|
|
|
|
|
+ setPositions(mapData);
|
|
|
}
|
|
}
|
|
|
}, [mapData]);
|
|
}, [mapData]);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="h-full w-full rounded-xl overflow-hidden relative">
|
|
<div className="h-full w-full rounded-xl overflow-hidden relative">
|
|
|
- <MapContainer
|
|
|
|
|
- center={[39.9042, 116.4074]}
|
|
|
|
|
- zoom={10}
|
|
|
|
|
- className="h-full w-full"
|
|
|
|
|
- style={{ background: 'transparent' }}
|
|
|
|
|
- >
|
|
|
|
|
- <TileLayer
|
|
|
|
|
- url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
|
|
|
|
- attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- {positions.map((pos, index) => (
|
|
|
|
|
- <CircleMarker
|
|
|
|
|
- key={index}
|
|
|
|
|
- center={[pos.lat, pos.lng]}
|
|
|
|
|
- radius={8}
|
|
|
|
|
- pathOptions={{
|
|
|
|
|
- fillColor: '#00ff88',
|
|
|
|
|
- color: '#00ff88',
|
|
|
|
|
- weight: 2,
|
|
|
|
|
- opacity: 0.8,
|
|
|
|
|
- fillOpacity: 0.6,
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- <Popup>
|
|
|
|
|
- <div className="text-white">
|
|
|
|
|
- <p>设备ID: {pos.deviceId}</p>
|
|
|
|
|
- <p>状态: {pos.status}</p>
|
|
|
|
|
- <p>时间: {new Date(pos.timestamp).toLocaleTimeString()}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </Popup>
|
|
|
|
|
- </CircleMarker>
|
|
|
|
|
- ))}
|
|
|
|
|
- </MapContainer>
|
|
|
|
|
-
|
|
|
|
|
- {/* 地图遮罩 */}
|
|
|
|
|
- <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />
|
|
|
|
|
|
|
+ <AMapLoader apiKey="您的高德地图API密钥">
|
|
|
|
|
+ <ChinaAMap data={positions || []} />
|
|
|
|
|
+ </AMapLoader>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+### 5. 地图配置
|
|
|
|
|
+```typescript
|
|
|
|
|
+// mapConfig.ts
|
|
|
|
|
+export const mapConfig = {
|
|
|
|
|
+ gaodeApiKey: import.meta.env.VITE_GAODE_MAP_KEY || '您的高德地图API密钥',
|
|
|
|
|
+ defaultCenter: [104.195397, 35.86166], // 中国中心坐标
|
|
|
|
|
+ defaultZoom: 5,
|
|
|
|
|
+ mapStyle: 'amap://styles/darkblue', // 深色主题适配大屏
|
|
|
|
|
+ viewMode: '3D',
|
|
|
|
|
+ pitch: 30
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
### 5. 实时图表组件
|
|
### 5. 实时图表组件
|
|
|
```typescript
|
|
```typescript
|
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
@@ -659,13 +812,28 @@ const App = () => (
|
|
|
"react-countup": "^6.4.2",
|
|
"react-countup": "^6.4.2",
|
|
|
"react-intersection-observer": "^9.5.2",
|
|
"react-intersection-observer": "^9.5.2",
|
|
|
"@tanstack/react-query": "^5.8.4",
|
|
"@tanstack/react-query": "^5.8.4",
|
|
|
- "leaflet": "^1.9.4",
|
|
|
|
|
- "react-leaflet": "^4.2.1",
|
|
|
|
|
"recharts": "^2.8.0"
|
|
"recharts": "^2.8.0"
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+### 高德地图集成
|
|
|
|
|
+使用高德地图API替换Leaflet,提供国内地图支持:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 地图配置
|
|
|
|
|
+export const mapConfig = {
|
|
|
|
|
+ gaodeApiKey: import.meta.env.VITE_GAODE_MAP_KEY || '您的高德地图API密钥',
|
|
|
|
|
+ defaultCenter: [104.195397, 35.86166], // 中国中心坐标
|
|
|
|
|
+ defaultZoom: 5,
|
|
|
|
|
+ mapStyle: 'amap://styles/darkblue', // 深色主题适配大屏
|
|
|
|
|
+ viewMode: '3D',
|
|
|
|
|
+ pitch: 30
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 无需额外依赖,通过CDN加载高德地图
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
## 部署优化
|
|
## 部署优化
|
|
|
|
|
|
|
|
### 1. 构建配置
|
|
### 1. 构建配置
|
|
@@ -709,17 +877,21 @@ import { loadFull } from "tsparticles";
|
|
|
import CountUp from 'react-countup';
|
|
import CountUp from 'react-countup';
|
|
|
import { useInView } from 'react-intersection-observer';
|
|
import { useInView } from 'react-intersection-observer';
|
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
-import { MapContainer, TileLayer, CircleMarker, Popup } from 'react-leaflet';
|
|
|
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
|
|
|
|
|
|
|
|
+// 高德地图集成
|
|
|
|
|
+import { AMapLoader } from '@/components/AMapLoader';
|
|
|
|
|
+import ChinaAMap from '@/components/ChinaAMap';
|
|
|
|
|
+import { mapConfig } from '@/config/mapConfig';
|
|
|
|
|
+
|
|
|
// UI组件
|
|
// UI组件
|
|
|
import { Card } from '@/client/components/ui/card';
|
|
import { Card } from '@/client/components/ui/card';
|
|
|
import { Button } from '@/client/components/ui/button';
|
|
import { Button } from '@/client/components/ui/button';
|
|
|
import { Badge } from '@/client/components/ui/badge';
|
|
import { Badge } from '@/client/components/ui/badge';
|
|
|
|
|
|
|
|
// 图标
|
|
// 图标
|
|
|
-import {
|
|
|
|
|
- Activity, Users, DollarSign, TrendingUp, TrendingDown,
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ Activity, Users, DollarSign, TrendingUp, TrendingDown,
|
|
|
MapPin, Clock, BarChart3, PieChart, LineChart, Globe,
|
|
MapPin, Clock, BarChart3, PieChart, LineChart, Globe,
|
|
|
Wifi, WifiOff, AlertCircle, RefreshCw, Zap
|
|
Wifi, WifiOff, AlertCircle, RefreshCw, Zap
|
|
|
} from 'lucide-react';
|
|
} from 'lucide-react';
|