|
|
@@ -6,8 +6,9 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
import CountUp from 'react-countup';
|
|
|
import { useInView } from 'react-intersection-observer';
|
|
|
import { MapContainer, TileLayer, CircleMarker, Popup } from 'react-leaflet';
|
|
|
-import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
|
-import { Maximize2, Minimize2 } from 'lucide-react';
|
|
|
+import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
|
|
+import { Maximize2, Minimize2, TrendingUp, Users, Briefcase, BookOpen } from 'lucide-react';
|
|
|
+import { useSimulatedGuizhouStats } from '../hooks/useGuizhouStats';
|
|
|
import 'leaflet/dist/leaflet.css';
|
|
|
|
|
|
// 玻璃拟态卡片组件
|
|
|
@@ -40,7 +41,7 @@ const AnimatedNumber = ({ value, duration = 2 }) => {
|
|
|
};
|
|
|
|
|
|
// KPI卡片组件
|
|
|
-const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
|
|
|
+const KpiCard = ({ title, value, unit, trend, color = 'cyan', icon: Icon }) => {
|
|
|
const isPositive = trend > 0;
|
|
|
const colorClasses = {
|
|
|
cyan: 'from-cyan-400 to-blue-500',
|
|
|
@@ -55,7 +56,10 @@ const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
|
|
|
<div className={`absolute inset-0 bg-gradient-to-br ${colorClasses[color]} opacity-10`} />
|
|
|
|
|
|
<div className="relative z-10">
|
|
|
- <h3 className="text-sm md:text-lg text-gray-300 mb-2">{title}</h3>
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
+ <h3 className="text-sm md:text-lg text-gray-300">{title}</h3>
|
|
|
+ <Icon className="w-6 h-6 text-cyan-400" />
|
|
|
+ </div>
|
|
|
<div className="flex items-baseline space-x-2">
|
|
|
<AnimatedNumber value={value} />
|
|
|
<span className="text-lg text-gray-400">{unit}</span>
|
|
|
@@ -71,7 +75,7 @@ const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
|
|
|
<span className={`text-sm ${isPositive ? 'text-green-400' : 'text-red-400'}`}>
|
|
|
{Math.abs(trend)}%
|
|
|
</span>
|
|
|
- <span className="text-xs text-gray-500">vs 上期</span>
|
|
|
+ <span className="text-xs text-gray-500">vs 昨日</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -91,174 +95,226 @@ const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-// 实时地图组件
|
|
|
-const RealTimeMap = () => {
|
|
|
- const [positions, setPositions] = useState([
|
|
|
- { id: 1, lat: 39.9042, lng: 116.4074, deviceId: '银龄智慧001', status: '正常' },
|
|
|
- { id: 2, lat: 31.2304, lng: 121.4737, deviceId: '银龄智慧002', status: '正常' },
|
|
|
- { id: 3, lat: 22.5431, lng: 114.0579, deviceId: '银龄智慧003', status: '警告' },
|
|
|
- { id: 4, lat: 39.0842, lng: 117.2006, deviceId: '银龄智慧004', status: '正常' },
|
|
|
- { id: 5, lat: 30.2796, lng: 120.1597, deviceId: '银龄智慧005', status: '正常' },
|
|
|
- { id: 6, lat: 23.1291, lng: 113.2644, deviceId: '银龄智慧006', status: '正常' },
|
|
|
- { id: 7, lat: 36.6512, lng: 117.1200, deviceId: '银龄智慧007', status: '故障' },
|
|
|
- { id: 8, lat: 29.5630, lng: 106.5516, deviceId: '银龄智慧008', status: '正常' },
|
|
|
- { id: 9, lat: 34.2632, lng: 108.9480, deviceId: '银龄智慧009', status: '正常' },
|
|
|
- { id: 10, lat: 25.0389, lng: 102.7183, deviceId: '银龄智慧010', status: '警告' },
|
|
|
- ]);
|
|
|
-
|
|
|
- // 模拟数据更新
|
|
|
- useEffect(() => {
|
|
|
- const interval = setInterval(() => {
|
|
|
- setPositions(prev => prev.map(pos => ({
|
|
|
- ...pos,
|
|
|
- status: Math.random() > 0.9 ? '警告' : pos.status,
|
|
|
- lng: pos.lng + (Math.random() - 0.5) * 0.01
|
|
|
- })));
|
|
|
- }, 5000);
|
|
|
+// 贵州省地图组件
|
|
|
+const GuizhouMap = ({ data }) => {
|
|
|
+ const [selectedCity, setSelectedCity] = useState(null);
|
|
|
+
|
|
|
+ // 转换数据格式以适配地图组件
|
|
|
+ const cityData = data.map(city => ({
|
|
|
+ code: city.cityCode,
|
|
|
+ name: city.cityName,
|
|
|
+ lat: city.latitude,
|
|
|
+ lng: city.longitude,
|
|
|
+ dailyNewJobs: city.dailyNewJobs,
|
|
|
+ totalJobs: city.totalJobs,
|
|
|
+ dailyNewTalents: city.dailyNewTalents,
|
|
|
+ totalTalents: city.totalTalents
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 计算圆圈大小
|
|
|
+ const getCircleRadius = (total) => {
|
|
|
+ const max = Math.max(...cityData.map(c => c.totalJobs + c.totalTalents));
|
|
|
+ return Math.max(8, (total / max) * 25);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算颜色
|
|
|
+ const getCircleColor = (city) => {
|
|
|
+ const total = city.totalJobs + city.totalTalents;
|
|
|
+ const max = Math.max(...cityData.map(c => c.totalJobs + c.totalTalents));
|
|
|
+ const ratio = total / max;
|
|
|
|
|
|
- return () => clearInterval(interval);
|
|
|
- }, []);
|
|
|
+ if (ratio > 0.8) return '#00ff88';
|
|
|
+ if (ratio > 0.6) return '#00ffff';
|
|
|
+ if (ratio > 0.4) return '#ff00ff';
|
|
|
+ return '#faad14';
|
|
|
+ };
|
|
|
|
|
|
return (
|
|
|
<div className="h-full w-full rounded-xl overflow-hidden relative">
|
|
|
- <MapContainer
|
|
|
- center={[35.8617, 104.1954]}
|
|
|
- zoom={4}
|
|
|
+ <MapContainer
|
|
|
+ center={[26.5832, 106.7070]}
|
|
|
+ zoom={7}
|
|
|
className="h-full w-full"
|
|
|
style={{ background: 'transparent' }}
|
|
|
+ zoomControl={false}
|
|
|
+ attributionControl={false}
|
|
|
>
|
|
|
<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) => (
|
|
|
+ {cityData.map((city) => (
|
|
|
<CircleMarker
|
|
|
- key={pos.id}
|
|
|
- center={[pos.lat, pos.lng]}
|
|
|
- radius={pos.status === '故障' ? 10 : 8}
|
|
|
+ key={city.code}
|
|
|
+ center={[city.lat, city.lng]}
|
|
|
+ radius={getCircleRadius(city.totalJobs + city.totalTalents)}
|
|
|
pathOptions={{
|
|
|
- fillColor: pos.status === '故障' ? '#ff4d4f' : pos.status === '警告' ? '#faad14' : '#00ff88',
|
|
|
- color: pos.status === '故障' ? '#ff4d4f' : pos.status === '警告' ? '#faad14' : '#00ff88',
|
|
|
+ fillColor: getCircleColor(city),
|
|
|
+ color: getCircleColor(city),
|
|
|
weight: 2,
|
|
|
opacity: 0.8,
|
|
|
fillOpacity: 0.6,
|
|
|
}}
|
|
|
+ eventHandlers={{
|
|
|
+ click: () => setSelectedCity(city),
|
|
|
+ }}
|
|
|
>
|
|
|
<Popup>
|
|
|
- <div className="text-white">
|
|
|
- <p>设备ID: {pos.deviceId}</p>
|
|
|
- <p>状态: {pos.status}</p>
|
|
|
- <p>时间: {new Date().toLocaleTimeString()}</p>
|
|
|
+ <div className="text-white min-w-48">
|
|
|
+ <h4 className="font-bold text-lg mb-2">{city.name}</h4>
|
|
|
+ <div className="space-y-1">
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span>每日新增银龄岗:</span>
|
|
|
+ <span className="text-cyan-400 font-bold">{city.dailyNewJobs}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span>累计银龄岗:</span>
|
|
|
+ <span className="text-green-400 font-bold">{city.totalJobs}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span>每日新增银龄库:</span>
|
|
|
+ <span className="text-cyan-400 font-bold">{city.dailyNewTalents}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span>累计银龄库:</span>
|
|
|
+ <span className="text-green-400 font-bold">{city.totalTalents}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</Popup>
|
|
|
</CircleMarker>
|
|
|
))}
|
|
|
</MapContainer>
|
|
|
|
|
|
- {/* 地图遮罩 */}
|
|
|
- <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />
|
|
|
-
|
|
|
{/* 地图标题 */}
|
|
|
<div className="absolute top-4 left-4 bg-black/50 backdrop-blur-sm px-4 py-2 rounded-lg border border-white/10">
|
|
|
- <h3 className="text-xl font-bold text-white">银龄智慧服务覆盖地图</h3>
|
|
|
- <p className="text-sm text-gray-300">实时监控全国服务节点状态</p>
|
|
|
+ <h3 className="text-xl font-bold text-white">贵州省银龄数据分布</h3>
|
|
|
+ <p className="text-sm text-gray-300">点击城市查看详细数据</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 图例 */}
|
|
|
+ <div className="absolute bottom-4 left-4 bg-black/50 backdrop-blur-sm px-3 py-2 rounded-lg border border-white/10">
|
|
|
+ <div className="text-sm text-white space-y-1">
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
+ <div className="w-3 h-3 rounded-full bg-green-400"></div>
|
|
|
+ <span>数据量高</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
+ <div className="w-3 h-3 rounded-full bg-cyan-400"></div>
|
|
|
+ <span>数据量中</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
+ <div className="w-3 h-3 rounded-full bg-purple-400"></div>
|
|
|
+ <span>数据量低</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-// 实时图表组件
|
|
|
-const RealTimeChart = ({ type = 'line', title, dataKey = 'value' }) => {
|
|
|
- // 生成模拟数据
|
|
|
- const generateData = () => {
|
|
|
- const data = [];
|
|
|
- const now = new Date();
|
|
|
-
|
|
|
- for (let i = 59; i >= 0; i--) {
|
|
|
- const time = new Date(now.getTime() - i * 60000);
|
|
|
- const formattedTime = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
|
-
|
|
|
- if (type === 'line') {
|
|
|
- data.push({
|
|
|
- time: formattedTime,
|
|
|
- value: Math.floor(Math.random() * 1000) + 500
|
|
|
- });
|
|
|
- } else if (type === 'bar') {
|
|
|
- data.push({
|
|
|
- time: formattedTime,
|
|
|
- value: Math.floor(Math.random() * 500) + 100
|
|
|
- });
|
|
|
- } else if (type === 'pie') {
|
|
|
- return [
|
|
|
- { name: '华东', value: 35 },
|
|
|
- { name: '华北', value: 25 },
|
|
|
- { name: '华南', value: 20 },
|
|
|
- { name: '西部', value: 15 },
|
|
|
- { name: '东北', value: 5 }
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return data;
|
|
|
- };
|
|
|
-
|
|
|
- const [chartData, setChartData] = useState(generateData());
|
|
|
-
|
|
|
- // 模拟数据更新
|
|
|
- useEffect(() => {
|
|
|
- const interval = setInterval(() => {
|
|
|
- setChartData(generateData());
|
|
|
- }, 5000);
|
|
|
-
|
|
|
- return () => clearInterval(interval);
|
|
|
- }, [type]);
|
|
|
+// 排行榜组件
|
|
|
+const CityRankingList = ({ data }) => {
|
|
|
+ // 转换数据格式以适配排行榜组件
|
|
|
+ const cityData = data.map(city => ({
|
|
|
+ code: city.cityCode,
|
|
|
+ name: city.cityName,
|
|
|
+ totalJobs: city.totalJobs,
|
|
|
+ totalTalents: city.totalTalents
|
|
|
+ }));
|
|
|
+
|
|
|
+ const sortedCities = [...cityData].sort((a, b) =>
|
|
|
+ (b.totalJobs + b.totalTalents) - (a.totalJobs + a.totalTalents)
|
|
|
+ );
|
|
|
|
|
|
+ return (
|
|
|
+ <div className="h-full">
|
|
|
+ <h3 className="text-lg font-semibold text-cyan-400 mb-4">城市数据排行</h3>
|
|
|
+ <div className="space-y-2">
|
|
|
+ {sortedCities.map((city, index) => (
|
|
|
+ <motion.div
|
|
|
+ key={city.code}
|
|
|
+ className="flex items-center space-x-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition-colors"
|
|
|
+ initial={{ opacity: 0, x: -20 }}
|
|
|
+ animate={{ opacity: 1, x: 0 }}
|
|
|
+ transition={{ delay: index * 0.1 }}
|
|
|
+ >
|
|
|
+ <div className={`
|
|
|
+ w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm
|
|
|
+ ${index === 0 ? 'bg-yellow-500 text-black' : ''}
|
|
|
+ ${index === 1 ? 'bg-gray-400 text-black' : ''}
|
|
|
+ ${index === 2 ? 'bg-orange-600 text-white' : ''}
|
|
|
+ ${index > 2 ? 'bg-gray-600 text-white' : ''}
|
|
|
+ `}>
|
|
|
+ {index + 1}
|
|
|
+ </div>
|
|
|
+ <div className="flex-1">
|
|
|
+ <p className="text-white font-medium">{city.name}</p>
|
|
|
+ <p className="text-gray-400 text-sm">
|
|
|
+ 银龄岗: {city.totalJobs} | 银龄库: {city.totalTalents}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div className="text-cyan-400 font-bold text-sm">
|
|
|
+ {city.totalJobs + city.totalTalents}
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 实时图表组件
|
|
|
+const GuizhouChart = ({ type = 'line', title, data }) => {
|
|
|
const CustomTooltip = ({ active, payload, label }) => {
|
|
|
if (active && payload && payload.length) {
|
|
|
return (
|
|
|
- <GlassCard className="p-2">
|
|
|
- <p className="text-cyan-400">{`${label}`}</p>
|
|
|
- <p className="text-white">{`${payload[0].value}`}</p>
|
|
|
+ <GlassCard className="p-3">
|
|
|
+ <p className="text-cyan-400 font-medium">{`${label}`}</p>
|
|
|
+ {payload.map((entry, index) => (
|
|
|
+ <p key={index} className="text-white">
|
|
|
+ {entry.name}: {entry.value}
|
|
|
+ </p>
|
|
|
+ ))}
|
|
|
</GlassCard>
|
|
|
);
|
|
|
}
|
|
|
return null;
|
|
|
};
|
|
|
|
|
|
- // 饼图特殊处理
|
|
|
+ // 准备图表数据
|
|
|
+ const chartData = data.map(city => ({
|
|
|
+ name: city.cityName,
|
|
|
+ 银龄岗: city.totalJobs,
|
|
|
+ 银龄库: city.totalTalents,
|
|
|
+ 日新增: city.dailyNewJobs + city.dailyNewTalents
|
|
|
+ }));
|
|
|
+
|
|
|
+ const COLORS = ['#00ff88', '#00ffff', '#ff00ff', '#faad14', '#ff4d4f', '#52c41a', '#1890ff', '#722ed1', '#fa8c16'];
|
|
|
+
|
|
|
if (type === 'pie') {
|
|
|
return (
|
|
|
<div className="h-full w-full">
|
|
|
<h3 className="text-lg font-semibold text-cyan-400 mb-4">{title}</h3>
|
|
|
<ResponsiveContainer width="100%" height="80%">
|
|
|
- <AreaChart data={chartData}>
|
|
|
- <defs>
|
|
|
- <linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
|
|
- <stop offset="5%" stopColor="#00ff88" stopOpacity={0.8}/>
|
|
|
- <stop offset="95%" stopColor="#00ff88" stopOpacity={0.1}/>
|
|
|
- </linearGradient>
|
|
|
- </defs>
|
|
|
- <CartesianGrid strokeDasharray="3 3" stroke="#ffffff20" />
|
|
|
- <XAxis
|
|
|
- dataKey="name"
|
|
|
- stroke="#ffffff60"
|
|
|
- tick={{ fill: '#ffffff80' }}
|
|
|
- />
|
|
|
- <YAxis
|
|
|
- stroke="#ffffff60"
|
|
|
- tick={{ fill: '#ffffff80' }}
|
|
|
- />
|
|
|
+ <PieChart>
|
|
|
+ <Pie
|
|
|
+ data={chartData}
|
|
|
+ cx="50%"
|
|
|
+ cy="50%"
|
|
|
+ labelLine={false}
|
|
|
+ label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
|
|
+ outerRadius={60}
|
|
|
+ fill="#8884d8"
|
|
|
+ dataKey="银龄岗"
|
|
|
+ >
|
|
|
+ {chartData.map((entry, index) => (
|
|
|
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
|
+ ))}
|
|
|
+ </Pie>
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
- <Area
|
|
|
- type="monotone"
|
|
|
- dataKey="value"
|
|
|
- stroke="#00ff88"
|
|
|
- strokeWidth={2}
|
|
|
- fillOpacity={1}
|
|
|
- fill="url(#colorGradient)"
|
|
|
- />
|
|
|
- </AreaChart>
|
|
|
+ </PieChart>
|
|
|
</ResponsiveContainer>
|
|
|
</div>
|
|
|
);
|
|
|
@@ -270,16 +326,22 @@ const RealTimeChart = ({ type = 'line', title, dataKey = 'value' }) => {
|
|
|
<ResponsiveContainer width="100%" height="80%">
|
|
|
<AreaChart data={chartData}>
|
|
|
<defs>
|
|
|
- <linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
|
|
+ <linearGradient id="colorJobs" x1="0" y1="0" x2="0" y2="1">
|
|
|
<stop offset="5%" stopColor="#00ff88" stopOpacity={0.8}/>
|
|
|
<stop offset="95%" stopColor="#00ff88" stopOpacity={0.1}/>
|
|
|
</linearGradient>
|
|
|
+ <linearGradient id="colorTalents" x1="0" y1="0" x2="0" y2="1">
|
|
|
+ <stop offset="5%" stopColor="#00ffff" stopOpacity={0.8}/>
|
|
|
+ <stop offset="95%" stopColor="#00ffff" stopOpacity={0.1}/>
|
|
|
+ </linearGradient>
|
|
|
</defs>
|
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff20" />
|
|
|
<XAxis
|
|
|
- dataKey="time"
|
|
|
+ dataKey="name"
|
|
|
stroke="#ffffff60"
|
|
|
- tick={{ fill: '#ffffff80' }}
|
|
|
+ tick={{ fill: '#ffffff80', fontSize: 12 }}
|
|
|
+ angle={-45}
|
|
|
+ textAnchor="end"
|
|
|
/>
|
|
|
<YAxis
|
|
|
stroke="#ffffff60"
|
|
|
@@ -288,11 +350,21 @@ const RealTimeChart = ({ type = 'line', title, dataKey = 'value' }) => {
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
<Area
|
|
|
type="monotone"
|
|
|
- dataKey={dataKey}
|
|
|
+ dataKey="银龄岗"
|
|
|
+ stackId="1"
|
|
|
stroke="#00ff88"
|
|
|
strokeWidth={2}
|
|
|
fillOpacity={1}
|
|
|
- fill="url(#colorGradient)"
|
|
|
+ fill="url(#colorJobs)"
|
|
|
+ />
|
|
|
+ <Area
|
|
|
+ type="monotone"
|
|
|
+ dataKey="银龄库"
|
|
|
+ stackId="1"
|
|
|
+ stroke="#00ffff"
|
|
|
+ strokeWidth={2}
|
|
|
+ fillOpacity={1}
|
|
|
+ fill="url(#colorTalents)"
|
|
|
/>
|
|
|
</AreaChart>
|
|
|
</ResponsiveContainer>
|
|
|
@@ -300,52 +372,6 @@ const RealTimeChart = ({ type = 'line', title, dataKey = 'value' }) => {
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-// 排行榜组件
|
|
|
-const RankingList = () => {
|
|
|
- const [rankings] = useState([
|
|
|
- { id: 1, name: '上海银龄服务中心', value: '服务人数: 12,589', score: 98.7 },
|
|
|
- { id: 2, name: '北京智慧养老社区', value: '服务人数: 9,842', score: 96.5 },
|
|
|
- { id: 3, name: '广州颐养中心', value: '服务人数: 8,756', score: 94.2 },
|
|
|
- { id: 4, name: '深圳老年公寓', value: '服务人数: 7,412', score: 92.8 },
|
|
|
- { id: 5, name: '杭州康养社区', value: '服务人数: 6,953', score: 90.5 },
|
|
|
- { id: 6, name: '南京夕阳红公寓', value: '服务人数: 5,871', score: 88.3 },
|
|
|
- ]);
|
|
|
-
|
|
|
- return (
|
|
|
- <div className="h-full">
|
|
|
- <h3 className="text-lg font-semibold text-cyan-400 mb-4">服务质量排行</h3>
|
|
|
- <div className="space-y-2">
|
|
|
- {rankings.map((item, index) => (
|
|
|
- <motion.div
|
|
|
- key={item.id}
|
|
|
- className="flex items-center space-x-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition-colors"
|
|
|
- initial={{ opacity: 0, x: -20 }}
|
|
|
- animate={{ opacity: 1, x: 0 }}
|
|
|
- transition={{ delay: index * 0.1 }}
|
|
|
- >
|
|
|
- <div className={`
|
|
|
- w-8 h-8 rounded-full flex items-center justify-center font-bold
|
|
|
- ${index === 0 ? 'bg-yellow-500 text-black' : ''}
|
|
|
- ${index === 1 ? 'bg-gray-400 text-black' : ''}
|
|
|
- ${index === 2 ? 'bg-orange-600 text-white' : ''}
|
|
|
- ${index > 2 ? 'bg-gray-600 text-white' : ''}
|
|
|
- `}>
|
|
|
- {index + 1}
|
|
|
- </div>
|
|
|
- <div className="flex-1">
|
|
|
- <p className="text-white font-medium">{item.name}</p>
|
|
|
- <p className="text-gray-400 text-sm">{item.value}</p>
|
|
|
- </div>
|
|
|
- <div className="text-cyan-400 font-bold">
|
|
|
- {item.score}
|
|
|
- </div>
|
|
|
- </motion.div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- );
|
|
|
-};
|
|
|
-
|
|
|
// 数字时钟组件
|
|
|
const DigitalClock = () => {
|
|
|
const [time, setTime] = useState(new Date());
|
|
|
@@ -359,7 +385,7 @@ const DigitalClock = () => {
|
|
|
}, []);
|
|
|
|
|
|
return (
|
|
|
- <div className="flex items-center space-x-2">
|
|
|
+ <div className="flex items-center space-x-4">
|
|
|
<div className="text-cyan-400 text-lg font-mono">
|
|
|
{time.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}
|
|
|
</div>
|
|
|
@@ -451,100 +477,119 @@ const FullscreenToggle = () => {
|
|
|
};
|
|
|
|
|
|
// 主仪表盘组件
|
|
|
-const DashboardGrid = () => (
|
|
|
- <div className="flex-1 p-4 md:p-8 grid grid-cols-12 gap-4">
|
|
|
- {/* 顶部标题区 */}
|
|
|
- <motion.div
|
|
|
- className="col-span-12 h-20"
|
|
|
- initial={{ opacity: 0, y: -50 }}
|
|
|
- animate={{ opacity: 1, y: 0 }}
|
|
|
- transition={{ duration: 0.8 }}
|
|
|
- >
|
|
|
- <div className="h-full glass-card rounded-xl flex items-center justify-between px-6">
|
|
|
- <h1 className="text-2xl md:text-4xl font-bold bg-gradient-to-r from-cyan-400 to-purple-400 bg-clip-text text-transparent">
|
|
|
- 银龄智慧数据大屏
|
|
|
- </h1>
|
|
|
- <div className="flex items-center space-x-4">
|
|
|
- <DigitalClock />
|
|
|
- <FullscreenToggle />
|
|
|
+const DashboardGrid = () => {
|
|
|
+ const { data: cities = [], summary } = useSimulatedGuizhouStats();
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="flex-1 p-4 md:p-8 grid grid-cols-12 gap-4">
|
|
|
+ {/* 顶部标题区 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 h-20"
|
|
|
+ initial={{ opacity: 0, y: -50 }}
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
+ transition={{ duration: 0.8 }}
|
|
|
+ >
|
|
|
+ <div className="h-full glass-card rounded-xl flex items-center justify-between px-6">
|
|
|
+ <h1 className="text-2xl md:text-4xl font-bold bg-gradient-to-r from-cyan-400 to-purple-400 bg-clip-text text-transparent">
|
|
|
+ 贵州省银龄智慧数据大屏
|
|
|
+ </h1>
|
|
|
+ <div className="flex items-center space-x-4">
|
|
|
+ <DigitalClock />
|
|
|
+ <FullscreenToggle />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </motion.div>
|
|
|
+ </motion.div>
|
|
|
|
|
|
- {/* 左侧统计区 */}
|
|
|
-
|
|
|
- {/* 左侧上部分 - 四大核心指标 */}
|
|
|
- <motion.div
|
|
|
- className="col-span-12 md:col-span-3 space-y-4"
|
|
|
- initial={{ opacity: 0, x: -50 }}
|
|
|
- animate={{ opacity: 1, x: 0 }}
|
|
|
- transition={{ duration: 0.8, delay: 0.2 }}
|
|
|
- >
|
|
|
- <KpiCard title="服务人数" value={123456} unit="人" trend={12.5} color="cyan" />
|
|
|
- <KpiCard title="服务时长" value={89012} unit="小时" trend={8.3} color="green" />
|
|
|
- <KpiCard title="异常事件" value={42} unit="起" trend={-5.2} color="red" />
|
|
|
- <KpiCard title="满意度" value={98.7} unit="%" trend={2.1} color="purple" />
|
|
|
- </motion.div>
|
|
|
-
|
|
|
- {/* 中间主图表区 */}
|
|
|
- <motion.div
|
|
|
- className="col-span-12 md:col-span-6"
|
|
|
- initial={{ opacity: 0, scale: 0.9 }}
|
|
|
- animate={{ opacity: 1, scale: 1 }}
|
|
|
- transition={{ duration: 0.8, delay: 0.4 }}
|
|
|
- >
|
|
|
- <div className="h-full glass-card rounded-xl p-4">
|
|
|
- <RealTimeMap />
|
|
|
- </div>
|
|
|
- </motion.div>
|
|
|
-
|
|
|
- {/* 右侧排行榜 */}
|
|
|
- <motion.div
|
|
|
- className="col-span-12 md:col-span-3"
|
|
|
- initial={{ opacity: 0, x: 50 }}
|
|
|
- animate={{ opacity: 1, x: 0 }}
|
|
|
- transition={{ duration: 0.8, delay: 0.6 }}
|
|
|
- >
|
|
|
- <div className="h-full glass-card rounded-xl p-4">
|
|
|
- <RankingList />
|
|
|
- </div>
|
|
|
- </motion.div>
|
|
|
-
|
|
|
- {/* 底部图表区 */}
|
|
|
- <motion.div
|
|
|
- className="col-span-12 md:col-span-4"
|
|
|
- initial={{ opacity: 0, y: 50 }}
|
|
|
- animate={{ opacity: 1, y: 0 }}
|
|
|
- transition={{ duration: 0.8, delay: 0.8 }}
|
|
|
- >
|
|
|
- <div className="h-64 glass-card rounded-xl p-4">
|
|
|
- <RealTimeChart type="line" title="服务趋势" />
|
|
|
- </div>
|
|
|
- </motion.div>
|
|
|
+ {/* 左侧统计区 - 四大核心指标 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-3 space-y-4"
|
|
|
+ initial={{ opacity: 0, x: -50 }}
|
|
|
+ animate={{ opacity: 1, x: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.2 }}
|
|
|
+ >
|
|
|
+ <KpiCard
|
|
|
+ title="今日新增银龄岗"
|
|
|
+ value={summary?.totalDailyNewJobs || 0}
|
|
|
+ unit="个"
|
|
|
+ trend={8.5}
|
|
|
+ color="cyan"
|
|
|
+ icon={Briefcase}
|
|
|
+ />
|
|
|
+ <KpiCard
|
|
|
+ title="累计银龄岗"
|
|
|
+ value={summary?.totalJobs || 0}
|
|
|
+ unit="个"
|
|
|
+ trend={12.3}
|
|
|
+ color="green"
|
|
|
+ icon={Briefcase}
|
|
|
+ />
|
|
|
+ <KpiCard
|
|
|
+ title="今日新增银龄库"
|
|
|
+ value={summary?.totalDailyNewTalents || 0}
|
|
|
+ unit="人"
|
|
|
+ trend={15.2}
|
|
|
+ color="purple"
|
|
|
+ icon={Users}
|
|
|
+ />
|
|
|
+ <KpiCard
|
|
|
+ title="累计银龄库"
|
|
|
+ value={summary?.totalTalents || 0}
|
|
|
+ unit="人"
|
|
|
+ trend={18.7}
|
|
|
+ color="red"
|
|
|
+ icon={Users}
|
|
|
+ />
|
|
|
+ </motion.div>
|
|
|
|
|
|
- <motion.div
|
|
|
- className="col-span-12 md:col-span-4"
|
|
|
- initial={{ opacity: 0, y: 50 }}
|
|
|
- animate={{ opacity: 1, y: 0 }}
|
|
|
- transition={{ duration: 0.8, delay: 1.0 }}
|
|
|
- >
|
|
|
- <div className="h-64 glass-card rounded-xl p-4">
|
|
|
- <RealTimeChart type="bar" title="设备分布" />
|
|
|
- </div>
|
|
|
- </motion.div>
|
|
|
+ {/* 中间主地图区 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-6"
|
|
|
+ initial={{ opacity: 0, scale: 0.9 }}
|
|
|
+ animate={{ opacity: 1, scale: 1 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.4 }}
|
|
|
+ >
|
|
|
+ <div className="h-full glass-card rounded-xl p-4">
|
|
|
+ <GuizhouMap data={cities} />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
|
|
|
- <motion.div
|
|
|
- className="col-span-12 md:col-span-4"
|
|
|
- initial={{ opacity: 0, y: 50 }}
|
|
|
- animate={{ opacity: 1, y: 0 }}
|
|
|
- transition={{ duration: 0.8, delay: 1.2 }}
|
|
|
- >
|
|
|
- <div className="h-64 glass-card rounded-xl p-4">
|
|
|
- <RealTimeChart type="pie" title="区域分布" />
|
|
|
- </div>
|
|
|
- </motion.div>
|
|
|
- </div>
|
|
|
-);
|
|
|
+ {/* 右侧排行榜 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-3"
|
|
|
+ initial={{ opacity: 0, x: 50 }}
|
|
|
+ animate={{ opacity: 1, x: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.6 }}
|
|
|
+ >
|
|
|
+ <div className="h-full glass-card rounded-xl p-4">
|
|
|
+ <CityRankingList data={cities} />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ {/* 底部图表区 */}
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-6"
|
|
|
+ initial={{ opacity: 0, y: 50 }}
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 0.8 }}
|
|
|
+ >
|
|
|
+ <div className="h-64 glass-card rounded-xl p-4">
|
|
|
+ <GuizhouChart type="line" title="各市州银龄数据对比" data={cities} />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+
|
|
|
+ <motion.div
|
|
|
+ className="col-span-12 md:col-span-6"
|
|
|
+ initial={{ opacity: 0, y: 50 }}
|
|
|
+ animate={{ opacity: 1, y: 0 }}
|
|
|
+ transition={{ duration: 0.8, delay: 1.0 }}
|
|
|
+ >
|
|
|
+ <div className="h-64 glass-card rounded-xl p-4">
|
|
|
+ <GuizhouChart type="pie" title="银龄岗分布占比" data={cities} />
|
|
|
+ </div>
|
|
|
+ </motion.div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
|
|
|
// 全屏数据大屏组件
|
|
|
const HomePage = () => {
|