Просмотр исходного кода

✨ feat(dashboard): 重构首页为数据大屏

- 添加玻璃拟态卡片组件,实现现代化UI效果
- 实现四大核心KPI指标卡片,包含数字动画效果
- 集成实时地图组件,显示全国服务节点分布
- 添加服务质量排行榜、实时数据图表等可视化组件
- 实现扫描线、3D星空背景等动态视觉效果
- 优化响应式布局,适配不同屏幕尺寸
- 添加数字时钟、区域分布图表等辅助信息展示

💄 style(ui): 提升整体视觉设计

- 采用深色主题配色方案,搭配青色、紫色渐变
- 添加动态光效和扫描线动画,增强科技感
- 实现卡片悬停效果和数据加载动画
- 优化字体层次结构,提升可读性
- 添加网格背景和渐变效果,增强视觉深度
yourname 7 месяцев назад
Родитель
Сommit
63e104e85f
1 измененных файлов с 505 добавлено и 3 удалено
  1. 505 3
      src/client/big-shadcn/pages/HomePage.tsx

+ 505 - 3
src/client/big-shadcn/pages/HomePage.tsx

@@ -1,9 +1,511 @@
-import React from 'react';
+import { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+import { Canvas } from '@react-three/fiber';
+import { Stars } from '@react-three/drei';
+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 'leaflet/dist/leaflet.css';
 
-const HomePage: React.FC = () => {
+// 玻璃拟态卡片组件
+const GlassCard = ({ children, className = '' }) => (
+  <div className={`
+    bg-white/5 backdrop-blur-md 
+    border border-white/10 rounded-2xl 
+    shadow-[0_8px_32px_0_rgba(31,38,135,0.37)] 
+    hover:shadow-[0_8px_32px_0_rgba(31,38,135,0.6)]
+    transition-all duration-300
+    ${className}
+  `}>
+    {children}
+  </div>
+);
 
+// 数字计数器组件
+const AnimatedNumber = ({ value, duration = 2 }) => {
+  const { ref, inView } = useInView({ threshold: 0.1 });
+
+  return (
+    <span ref={ref} className="text-3xl md:text-5xl font-bold text-cyan-400">
+      <CountUp 
+        end={inView ? value : 0} 
+        duration={duration}
+        separator=","
+      />
+    </span>
+  );
+};
+
+// KPI卡片组件
+const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
+  const isPositive = trend > 0;
+  const colorClasses = {
+    cyan: 'from-cyan-400 to-blue-500',
+    green: 'from-green-400 to-emerald-500',
+    red: 'from-red-400 to-pink-500',
+    purple: 'from-purple-400 to-indigo-500',
+  };
+
+  return (
+    <GlassCard className="p-4 md:p-6 relative overflow-hidden">
+      {/* 背景光效 */}
+      <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-baseline space-x-2">
+          <AnimatedNumber value={value} />
+          <span className="text-lg text-gray-400">{unit}</span>
+        </div>
+        
+        <div className="mt-2 flex items-center space-x-1">
+          <motion.div
+            animate={{ rotate: isPositive ? 0 : 180 }}
+            className={`w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-b-[8px] ${
+              isPositive ? 'border-b-green-400' : 'border-b-red-400'
+            }`}
+          />
+          <span className={`text-sm ${isPositive ? 'text-green-400' : 'text-red-400'}`}>
+            {Math.abs(trend)}%
+          </span>
+          <span className="text-xs text-gray-500">vs 上期</span>
+        </div>
+      </div>
+
+      {/* 脉冲效果 */}
+      <motion.div
+        className="absolute top-2 right-2 w-2 h-2 bg-green-400 rounded-full"
+        animate={{
+          scale: [1, 1.5, 1],
+          opacity: [1, 0.5, 1],
+        }}
+        transition={{
+          duration: 2,
+          repeat: Infinity,
+        }}
+      />
+    </GlassCard>
+  );
+};
+
+// 实时地图组件
+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);
+    
+    return () => clearInterval(interval);
+  }, []);
+
+  return (
+    <div className="h-full w-full rounded-xl overflow-hidden relative">
+      <MapContainer 
+        center={[35.8617, 104.1954]} 
+        zoom={4} 
+        className="h-full w-full"
+        style={{ background: 'transparent' }}
+      >
+        <TileLayer
+          url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
+          attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
+        />
+        
+        {positions.map((pos) => (
+          <CircleMarker
+            key={pos.id}
+            center={[pos.lat, pos.lng]}
+            radius={pos.status === '故障' ? 10 : 8}
+            pathOptions={{
+              fillColor: pos.status === '故障' ? '#ff4d4f' : pos.status === '警告' ? '#faad14' : '#00ff88',
+              color: pos.status === '故障' ? '#ff4d4f' : pos.status === '警告' ? '#faad14' : '#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().toLocaleTimeString()}</p>
+              </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>
+      </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 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>
+      );
+    }
+    return null;
+  };
+
+  // 饼图特殊处理
+  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' }}
+            />
+            <Tooltip content={<CustomTooltip />} />
+            <Area 
+              type="monotone" 
+              dataKey="value" 
+              stroke="#00ff88" 
+              strokeWidth={2}
+              fillOpacity={1} 
+              fill="url(#colorGradient)" 
+            />
+          </AreaChart>
+        </ResponsiveContainer>
+      </div>
+    );
+  }
+
+  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="time" 
+            stroke="#ffffff60"
+            tick={{ fill: '#ffffff80' }}
+          />
+          <YAxis 
+            stroke="#ffffff60"
+            tick={{ fill: '#ffffff80' }}
+          />
+          <Tooltip content={<CustomTooltip />} />
+          <Area 
+            type="monotone" 
+            dataKey={dataKey} 
+            stroke="#00ff88" 
+            strokeWidth={2}
+            fillOpacity={1} 
+            fill="url(#colorGradient)" 
+          />
+        </AreaChart>
+      </ResponsiveContainer>
+    </div>
+  );
+};
+
+// 排行榜组件
+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());
+  
+  useEffect(() => {
+    const timer = setInterval(() => {
+      setTime(new Date());
+    }, 1000);
+    
+    return () => clearInterval(timer);
+  }, []);
+  
+  return (
+    <div className="flex items-center space-x-2">
+      <div className="text-cyan-400 text-lg font-mono">
+        {time.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}
+      </div>
+      <div className="text-cyan-400 text-lg font-mono">
+        {time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
+      </div>
+    </div>
+  );
+};
+
+// 扫描线效果
+const ScanningLine = () => (
+  <motion.div
+    className="absolute inset-0 pointer-events-none"
+    animate={{
+      background: [
+        'linear-gradient(transparent 0%, rgba(0,255,136,0.1) 50%, transparent 100%)',
+        'linear-gradient(transparent 100%, rgba(0,255,136,0.1) 50%, transparent 0%)',
+      ],
+    }}
+    transition={{
+      duration: 2,
+      repeat: Infinity,
+      ease: "linear",
+    }}
+    style={{
+      backgroundSize: '100% 200%',
+      backgroundPosition: '0% 0%',
+    }}
+  />
+);
+
+// 主仪表盘组件
+const DashboardGrid = () => (
+  <div className="flex-1 p-4 md:p-8 grid grid-cols-12 gap-4 auto-rows-fr">
+    {/* 顶部标题区 */}
+    <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>
+        <DigitalClock />
+      </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-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-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>
+);
+
+// 全屏数据大屏组件
+const HomePage = () => {
   return (
-    <div className="min-h-screen">
+    <div className="fixed inset-0 bg-gradient-to-br from-gray-900 via-black to-gray-900 overflow-hidden">
+      {/* 3D背景 */}
+      <Canvas className="absolute inset-0">
+        <Stars radius={100} depth={50} count={5000} factor={4} saturation={0} fade speed={1} />
+      </Canvas>
+      
+      {/* 网格背景 */}
+      <div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff08_1px,transparent_1px),linear-gradient(to_bottom,#ffffff08_1px,transparent_1px)] bg-[size:50px_50px]" />
+      
+      {/* 主内容层 */}
+      <div className="relative z-10 h-full flex flex-col">
+        <DashboardGrid />
+      </div>
+      
+      {/* 动态光效 */}
+      <div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-transparent to-purple-500/10 animate-pulse" />
+      
+      {/* 扫描线效果 */}
+      <ScanningLine />
     </div>
   );
 };