Browse Source

✨ feat(guizhou): 新增贵州省银龄智慧数据大屏功能

- 创建贵州省银龄统计数据实体类和服务层,支持9个市州的数据统计
- 实现实时数据获取API,包括最新数据、汇总数据、城市排行等功能
- 开发贵州省专用地图组件,支持点击城市查看详细银龄数据
- 集成模拟数据Hook,提供实时数据更新和趋势分析功能
- 重构数据大屏布局,从全国视图切换为贵州省9个市州视图

📝 docs(admin): 简化控制台标题文案

- 将"量子控制台"简化为"控制台",提升界面简洁性
yourname 9 months ago
parent
commit
40fa1188e5

+ 1 - 1
src/client/admin/pages/Dashboard.tsx

@@ -13,7 +13,7 @@ const DashboardPage: React.FC = () => {
         {/* 页面标题 */}
         {/* 页面标题 */}
         <div className="mb-8">
         <div className="mb-8">
           <h1 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
           <h1 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
-            量子控制台
+          控制台
           </h1>
           </h1>
           <p className="text-slate-400 mt-2">实时系统状态监控面板</p>
           <p className="text-slate-400 mt-2">实时系统状态监控面板</p>
         </div>
         </div>

+ 205 - 0
src/client/big-shadcn/hooks/useGuizhouStats.ts

@@ -0,0 +1,205 @@
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { hc } from 'hono/client';
+import { GuizhouStatsRoutes } from '@/server/api';
+import { useEffect, useRef, useState } from 'react';
+
+const client = hc<GuizhouStatsRoutes>('/api/v1', {
+  fetch: fetch,
+});
+
+// 类型定义
+export interface GuizhouCityData {
+  cityName: string;
+  cityCode: string;
+  dailyNewJobs: number;
+  totalJobs: number;
+  dailyNewTalents: number;
+  totalTalents: number;
+  longitude: number;
+  latitude: number;
+  statDate: string;
+}
+
+export interface GuizhouSummary {
+  totalDailyNewJobs: number;
+  totalJobs: number;
+  totalDailyNewTalents: number;
+  totalTalents: number;
+  cityCount: number;
+}
+
+// 自定义Hook:获取贵州省银龄统计数据
+export const useGuizhouStats = () => {
+  return useQuery({
+    queryKey: ['guizhou-stats'],
+    queryFn: async () => {
+      const response = await client.api.v1['guizhou-stats'].latest.$get();
+      if (!response.ok) {
+        throw new Error('Failed to fetch Guizhou stats');
+      }
+      return response.json();
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟
+    refetchInterval: 30 * 1000, // 30秒刷新
+  });
+};
+
+// 自定义Hook:获取汇总数据
+export const useGuizhouSummary = () => {
+  return useQuery({
+    queryKey: ['guizhou-summary'],
+    queryFn: async () => {
+      const response = await client.api.v1['guizhou-stats'].summary.$get();
+      if (!response.ok) {
+        throw new Error('Failed to fetch Guizhou summary');
+      }
+      return response.json();
+    },
+    staleTime: 5 * 60 * 1000,
+    refetchInterval: 30 * 1000,
+  });
+};
+
+// 自定义Hook:实时数据更新
+export const useRealtimeGuizhouStats = () => {
+  const queryClient = useQueryClient();
+  const [isConnected, setIsConnected] = useState(false);
+  const wsRef = useRef<WebSocket | null>(null);
+
+  useEffect(() => {
+    // 创建WebSocket连接
+    const wsUrl = `ws://${window.location.host}/ws/guizhou-stats`;
+    const ws = new WebSocket(wsUrl);
+    
+    ws.onopen = () => {
+      setIsConnected(true);
+      console.log('Connected to Guizhou stats WebSocket');
+    };
+
+    ws.onmessage = (event) => {
+      try {
+        const data = JSON.parse(event.data);
+        // 更新缓存数据
+        queryClient.setQueryData(['guizhou-stats'], data.latest);
+        queryClient.setQueryData(['guizhou-summary'], data.summary);
+      } catch (error) {
+        console.error('Error processing WebSocket message:', error);
+      }
+    };
+
+    ws.onclose = () => {
+      setIsConnected(false);
+      console.log('Disconnected from Guizhou stats WebSocket');
+    };
+
+    ws.onerror = (error) => {
+      console.error('WebSocket error:', error);
+      setIsConnected(false);
+    };
+
+    wsRef.current = ws;
+
+    return () => {
+      if (wsRef.current) {
+        wsRef.current.close();
+      }
+    };
+  }, [queryClient]);
+
+  return { isConnected };
+};
+
+// 计算数据变化趋势
+export const useTrendData = (currentData: GuizhouCityData[], previousData?: GuizhouCityData[]) => {
+  const [trends, setTrends] = useState<Record<string, number>>({});
+
+  useEffect(() => {
+    if (!currentData || !previousData) {
+      setTrends({});
+      return;
+    }
+
+    const newTrends: Record<string, number> = {};
+    
+    currentData.forEach(current => {
+      const previous = previousData.find(p => p.cityCode === current.cityCode);
+      if (previous) {
+        const jobsTrend = ((current.totalJobs - previous.totalJobs) / Math.max(previous.totalJobs, 1)) * 100;
+        const talentsTrend = ((current.totalTalents - previous.totalTalents) / Math.max(previous.totalTalents, 1)) * 100;
+        newTrends[current.cityCode] = Math.max(jobsTrend, talentsTrend);
+      }
+    });
+
+    setTrends(newTrends);
+  }, [currentData, previousData]);
+
+  return trends;
+};
+
+// 数据模拟Hook(用于开发和测试)
+export const useSimulatedGuizhouStats = () => {
+  const [data, setData] = useState<GuizhouCityData[]>([]);
+  const [summary, setSummary] = useState<GuizhouSummary>({
+    totalDailyNewJobs: 0,
+    totalJobs: 0,
+    totalDailyNewTalents: 0,
+    totalTalents: 0,
+    cityCount: 9,
+  });
+
+  // 贵州省各市州初始数据
+  const initialData: GuizhouCityData[] = [
+    { cityName: '贵阳市', cityCode: '520100', dailyNewJobs: 12, totalJobs: 156, dailyNewTalents: 45, totalTalents: 1234, longitude: 106.7070, latitude: 26.5982, statDate: new Date().toISOString() },
+    { cityName: '遵义市', cityCode: '520300', dailyNewJobs: 8, totalJobs: 134, dailyNewTalents: 38, totalTalents: 1089, longitude: 106.9274, latitude: 27.7254, statDate: new Date().toISOString() },
+    { cityName: '六盘水市', cityCode: '520200', dailyNewJobs: 5, totalJobs: 89, dailyNewTalents: 22, totalTalents: 567, longitude: 104.8300, latitude: 26.5976, statDate: new Date().toISOString() },
+    { cityName: '安顺市', cityCode: '520400', dailyNewJobs: 6, totalJobs: 98, dailyNewTalents: 28, totalTalents: 678, longitude: 105.9452, latitude: 26.2535, statDate: new Date().toISOString() },
+    { cityName: '毕节市', cityCode: '520500', dailyNewJobs: 9, totalJobs: 145, dailyNewTalents: 35, totalTalents: 892, longitude: 105.2997, latitude: 27.3026, statDate: new Date().toISOString() },
+    { cityName: '铜仁市', cityCode: '520600', dailyNewJobs: 7, totalJobs: 112, dailyNewTalents: 31, totalTalents: 745, longitude: 109.1891, latitude: 27.7311, statDate: new Date().toISOString() },
+    { cityName: '黔西南州', cityCode: '522300', dailyNewJobs: 4, totalJobs: 76, dailyNewTalents: 19, totalTalents: 456, longitude: 104.9073, latitude: 25.0892, statDate: new Date().toISOString() },
+    { cityName: '黔东南州', cityCode: '522600', dailyNewJobs: 6, totalJobs: 103, dailyNewTalents: 26, totalTalents: 634, longitude: 107.9785, latitude: 26.5834, statDate: new Date().toISOString() },
+    { cityName: '黔南州', cityCode: '522700', dailyNewJobs: 5, totalJobs: 87, dailyNewTalents: 24, totalTalents: 523, longitude: 107.5170, latitude: 26.2584, statDate: new Date().toISOString() },
+  ];
+
+  useEffect(() => {
+    setData(initialData);
+    
+    // 计算初始汇总数据
+    const summary = initialData.reduce(
+      (acc, city) => ({
+        totalDailyNewJobs: acc.totalDailyNewJobs + city.dailyNewJobs,
+        totalJobs: acc.totalJobs + city.totalJobs,
+        totalDailyNewTalents: acc.totalDailyNewTalents + city.dailyNewTalents,
+        totalTalents: acc.totalTalents + city.totalTalents,
+        cityCount: 9,
+      }),
+      { totalDailyNewJobs: 0, totalJobs: 0, totalDailyNewTalents: 0, totalTalents: 0, cityCount: 9 }
+    );
+    setSummary(summary);
+
+    // 模拟实时数据更新
+    const interval = setInterval(() => {
+      setData(prevData => {
+        return prevData.map(city => ({
+          ...city,
+          dailyNewJobs: Math.max(1, city.dailyNewJobs + Math.floor(Math.random() * 3) - 1),
+          totalJobs: city.totalJobs + Math.floor(Math.random() * 3),
+          dailyNewTalents: Math.max(1, city.dailyNewTalents + Math.floor(Math.random() * 5) - 2),
+          totalTalents: city.totalTalents + Math.floor(Math.random() * 8),
+          statDate: new Date().toISOString(),
+        }));
+      });
+
+      setSummary(prevSummary => ({
+        ...prevSummary,
+        totalDailyNewJobs: prevSummary.totalDailyNewJobs + Math.floor(Math.random() * 5),
+        totalJobs: prevSummary.totalJobs + Math.floor(Math.random() * 10),
+        totalDailyNewTalents: prevSummary.totalDailyNewTalents + Math.floor(Math.random() * 8),
+        totalTalents: prevSummary.totalTalents + Math.floor(Math.random() * 15),
+      }));
+    }, 5000);
+
+    return () => clearInterval(interval);
+  }, []);
+
+  return { data, summary };
+};

+ 311 - 266
src/client/big-shadcn/pages/HomePage.tsx

@@ -6,8 +6,9 @@ import { useQuery } from '@tanstack/react-query';
 import CountUp from 'react-countup';
 import CountUp from 'react-countup';
 import { useInView } from 'react-intersection-observer';
 import { useInView } from 'react-intersection-observer';
 import { MapContainer, TileLayer, CircleMarker, Popup } from 'react-leaflet';
 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';
 import 'leaflet/dist/leaflet.css';
 
 
 // 玻璃拟态卡片组件
 // 玻璃拟态卡片组件
@@ -40,7 +41,7 @@ const AnimatedNumber = ({ value, duration = 2 }) => {
 };
 };
 
 
 // KPI卡片组件
 // KPI卡片组件
-const KpiCard = ({ title, value, unit, trend, color = 'cyan' }) => {
+const KpiCard = ({ title, value, unit, trend, color = 'cyan', icon: Icon }) => {
   const isPositive = trend > 0;
   const isPositive = trend > 0;
   const colorClasses = {
   const colorClasses = {
     cyan: 'from-cyan-400 to-blue-500',
     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={`absolute inset-0 bg-gradient-to-br ${colorClasses[color]} opacity-10`} />
       
       
       <div className="relative z-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">
         <div className="flex items-baseline space-x-2">
           <AnimatedNumber value={value} />
           <AnimatedNumber value={value} />
           <span className="text-lg text-gray-400">{unit}</span>
           <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'}`}>
           <span className={`text-sm ${isPositive ? 'text-green-400' : 'text-red-400'}`}>
             {Math.abs(trend)}%
             {Math.abs(trend)}%
           </span>
           </span>
-          <span className="text-xs text-gray-500">vs 上期</span>
+          <span className="text-xs text-gray-500">vs 昨日</span>
         </div>
         </div>
       </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 (
   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={[35.8617, 104.1954]} 
-        zoom={4} 
+      <MapContainer
+        center={[26.5832, 106.7070]}
+        zoom={7}
         className="h-full w-full"
         className="h-full w-full"
         style={{ background: 'transparent' }}
         style={{ background: 'transparent' }}
+        zoomControl={false}
+        attributionControl={false}
       >
       >
         <TileLayer
         <TileLayer
           url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
           url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
           attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
           attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
         />
         />
         
         
-        {positions.map((pos) => (
+        {cityData.map((city) => (
           <CircleMarker
           <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={{
             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,
               weight: 2,
               opacity: 0.8,
               opacity: 0.8,
               fillOpacity: 0.6,
               fillOpacity: 0.6,
             }}
             }}
+            eventHandlers={{
+              click: () => setSelectedCity(city),
+            }}
           >
           >
             <Popup>
             <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>
               </div>
             </Popup>
             </Popup>
           </CircleMarker>
           </CircleMarker>
         ))}
         ))}
       </MapContainer>
       </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">
       <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>
     </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 }) => {
   const CustomTooltip = ({ active, payload, label }) => {
     if (active && payload && payload.length) {
     if (active && payload && payload.length) {
       return (
       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>
         </GlassCard>
       );
       );
     }
     }
     return null;
     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') {
   if (type === 'pie') {
     return (
     return (
       <div className="h-full w-full">
       <div className="h-full w-full">
         <h3 className="text-lg font-semibold text-cyan-400 mb-4">{title}</h3>
         <h3 className="text-lg font-semibold text-cyan-400 mb-4">{title}</h3>
         <ResponsiveContainer width="100%" height="80%">
         <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 />} />
             <Tooltip content={<CustomTooltip />} />
-            <Area 
-              type="monotone" 
-              dataKey="value" 
-              stroke="#00ff88" 
-              strokeWidth={2}
-              fillOpacity={1} 
-              fill="url(#colorGradient)" 
-            />
-          </AreaChart>
+          </PieChart>
         </ResponsiveContainer>
         </ResponsiveContainer>
       </div>
       </div>
     );
     );
@@ -270,16 +326,22 @@ const RealTimeChart = ({ type = 'line', title, dataKey = 'value' }) => {
       <ResponsiveContainer width="100%" height="80%">
       <ResponsiveContainer width="100%" height="80%">
         <AreaChart data={chartData}>
         <AreaChart data={chartData}>
           <defs>
           <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="5%" stopColor="#00ff88" stopOpacity={0.8}/>
               <stop offset="95%" stopColor="#00ff88" stopOpacity={0.1}/>
               <stop offset="95%" stopColor="#00ff88" stopOpacity={0.1}/>
             </linearGradient>
             </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>
           </defs>
           <CartesianGrid strokeDasharray="3 3" stroke="#ffffff20" />
           <CartesianGrid strokeDasharray="3 3" stroke="#ffffff20" />
           <XAxis 
           <XAxis 
-            dataKey="time" 
+            dataKey="name" 
             stroke="#ffffff60"
             stroke="#ffffff60"
-            tick={{ fill: '#ffffff80' }}
+            tick={{ fill: '#ffffff80', fontSize: 12 }}
+            angle={-45}
+            textAnchor="end"
           />
           />
           <YAxis 
           <YAxis 
             stroke="#ffffff60"
             stroke="#ffffff60"
@@ -288,11 +350,21 @@ const RealTimeChart = ({ type = 'line', title, dataKey = 'value' }) => {
           <Tooltip content={<CustomTooltip />} />
           <Tooltip content={<CustomTooltip />} />
           <Area 
           <Area 
             type="monotone" 
             type="monotone" 
-            dataKey={dataKey} 
+            dataKey="银龄岗" 
+            stackId="1"
             stroke="#00ff88" 
             stroke="#00ff88" 
             strokeWidth={2}
             strokeWidth={2}
             fillOpacity={1} 
             fillOpacity={1} 
-            fill="url(#colorGradient)" 
+            fill="url(#colorJobs)" 
+          />
+          <Area 
+            type="monotone" 
+            dataKey="银龄库" 
+            stackId="1"
+            stroke="#00ffff" 
+            strokeWidth={2}
+            fillOpacity={1} 
+            fill="url(#colorTalents)" 
           />
           />
         </AreaChart>
         </AreaChart>
       </ResponsiveContainer>
       </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 DigitalClock = () => {
   const [time, setTime] = useState(new Date());
   const [time, setTime] = useState(new Date());
@@ -359,7 +385,7 @@ const DigitalClock = () => {
   }, []);
   }, []);
   
   
   return (
   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">
       <div className="text-cyan-400 text-lg font-mono">
         {time.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}
         {time.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}
       </div>
       </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>
-      </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 = () => {
 const HomePage = () => {

+ 5 - 1
src/server/api.ts

@@ -147,6 +147,10 @@ const silverKnowledgeApiRoutes = api.route('/api/v1/silver-knowledges', silverKn
 import aiAgentRoutes from './api/ai-agents/index'
 import aiAgentRoutes from './api/ai-agents/index'
 const aiAgentApiRoutes = api.route('/api/v1/ai-agents', aiAgentRoutes)
 const aiAgentApiRoutes = api.route('/api/v1/ai-agents', aiAgentRoutes)
 
 
+// 注册贵州省银龄统计数据路由
+import guizhouStatsRoutes from './api/guizhou-stats/index'
+const guizhouStatsApiRoutes = api.route('/api/v1/guizhou-stats', guizhouStatsRoutes)
+
 export type AuthRoutes = typeof authRoutes
 export type AuthRoutes = typeof authRoutes
 export type CompanyCertificationRoutes = typeof companyCertificationApiRoutes
 export type CompanyCertificationRoutes = typeof companyCertificationApiRoutes
 export type UserRoutes = typeof userRoutes
 export type UserRoutes = typeof userRoutes
@@ -177,7 +181,7 @@ export type SilverCompaniesRoutes = typeof silverCompaniesApiRoutes
 export type HomeIconRoutes = typeof homeIconApiRoutes
 export type HomeIconRoutes = typeof homeIconApiRoutes
 export type SilverKnowledgeRoutes = typeof silverKnowledgeApiRoutes
 export type SilverKnowledgeRoutes = typeof silverKnowledgeApiRoutes
 export type AIAgentRoutes = typeof aiAgentApiRoutes
 export type AIAgentRoutes = typeof aiAgentApiRoutes
-
+export type GuizhouStatsRoutes = typeof guizhouStatsApiRoutes
 
 
 app.route('/', api)
 app.route('/', api)
 export default app
 export default app

+ 363 - 0
src/server/api/guizhou-stats/index.ts

@@ -0,0 +1,363 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { GuizhouSilverStatsSchema, CreateGuizhouSilverStatsDto, UpdateGuizhouSilverStatsDto } from '@/server/modules/silver-users/guizhou-stats.entity';
+import { GuizhouStatsService } from '@/server/modules/silver-users/guizhou-stats.service';
+import { AppDataSource } from '@/server/data-source';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 查询参数Schema
+const ListQuery = z.object({
+  page: z.coerce.number().int().positive().default(1).openapi({
+    description: '页码',
+    example: 1
+  }),
+  pageSize: z.coerce.number().int().positive().default(10).openapi({
+    description: '每页条数',
+    example: 10
+  }),
+  cityCode: z.string().optional().openapi({
+    description: '城市代码',
+    example: '520100'
+  }),
+  statDate: z.coerce.date().optional().openapi({
+    description: '统计日期',
+    example: '2024-01-01'
+  })
+});
+
+// 列表响应Schema
+const ListResponse = z.object({
+  data: z.array(GuizhouSilverStatsSchema),
+  pagination: z.object({
+    total: z.number().openapi({ example: 100, description: '总记录数' }),
+    current: z.number().openapi({ example: 1, description: '当前页码' }),
+    pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+  })
+});
+
+// 汇总响应Schema
+const SummaryResponse = z.object({
+  totalDailyNewJobs: z.number().openapi({ example: 62, description: '全省今日新增银龄岗' }),
+  totalJobs: z.number().openapi({ example: 1000, description: '全省累计银龄岗' }),
+  totalDailyNewTalents: z.number().openapi({ example: 270, description: '全省今日新增银龄库' }),
+  totalTalents: z.number().openapi({ example: 6817, description: '全省累计银龄库' }),
+  cityCount: z.number().openapi({ example: 9, description: '统计城市数量' })
+});
+
+// 获取列表路由
+const listRoute = createRoute({
+  method: 'get',
+  path: '/',
+  request: {
+    query: ListQuery
+  },
+  responses: {
+    200: {
+      description: '成功获取贵州省银龄统计数据',
+      content: { 'application/json': { schema: ListResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 获取最新统计数据路由
+const latestRoute = createRoute({
+  method: 'get',
+  path: '/latest',
+  responses: {
+    200: {
+      description: '成功获取最新贵州省银龄统计数据',
+      content: { 'application/json': { schema: z.array(GuizhouSilverStatsSchema) } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 获取汇总数据路由
+const summaryRoute = createRoute({
+  method: 'get',
+  path: '/summary',
+  responses: {
+    200: {
+      description: '成功获取贵州省银龄统计汇总数据',
+      content: { 'application/json': { schema: SummaryResponse } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 创建统计数据路由
+const createRouteDef = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: CreateGuizhouSilverStatsDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功创建贵州省银龄统计数据',
+      content: { 'application/json': { schema: GuizhouSilverStatsSchema } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 批量创建统计数据路由
+const batchCreateRoute = createRoute({
+  method: 'post',
+  path: '/batch',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { 
+          schema: z.object({
+            data: z.array(CreateGuizhouSilverStatsDto)
+          })
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功批量创建贵州省银龄统计数据',
+      content: { 'application/json': { schema: z.array(GuizhouSilverStatsSchema) } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 更新统计数据路由
+const updateRouteDef = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.string().openapi({
+        param: { name: 'id', in: 'path' },
+        example: '1',
+        description: '统计ID'
+      })
+    }),
+    body: {
+      content: {
+        'application/json': { schema: UpdateGuizhouSilverStatsDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功更新贵州省银龄统计数据',
+      content: { 'application/json': { schema: GuizhouSilverStatsSchema } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '统计数据不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 删除统计数据路由
+const deleteRouteDef = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.string().openapi({
+        param: { name: 'id', in: 'path' },
+        example: '1',
+        description: '统计ID'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功删除贵州省银龄统计数据',
+      content: { 'application/json': { schema: z.object({ success: z.boolean() }) } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '统计数据不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由初始化
+const app = new OpenAPIHono();
+
+// 获取列表
+app.openapi(listRoute, async (c) => {
+  try {
+    const query = c.req.valid('query');
+    const service = new GuizhouStatsService(AppDataSource);
+    
+    let stats: GuizhouSilverStats[];
+    let total: number;
+    
+    if (query.cityCode) {
+      stats = await service.findByCityCode(query.cityCode);
+      total = stats.length;
+    } else if (query.statDate) {
+      stats = await service.findByDate(query.statDate);
+      total = stats.length;
+    } else {
+      stats = await service.findAll();
+      total = stats.length;
+    }
+    
+    const startIndex = (query.page - 1) * query.pageSize;
+    const endIndex = startIndex + query.pageSize;
+    const paginatedData = stats.slice(startIndex, endIndex);
+    
+    return c.json({
+      data: paginatedData,
+      pagination: {
+        total,
+        current: query.page,
+        pageSize: query.pageSize
+      }
+    }, 200);
+  } catch (error) {
+    const { message = '获取列表失败' } = error as Error;
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+// 获取最新统计数据
+app.openapi(latestRoute, async (c) => {
+  try {
+    const service = new GuizhouStatsService(AppDataSource);
+    const stats = await service.getLatestStats();
+    return c.json(stats, 200);
+  } catch (error) {
+    const { message = '获取最新数据失败' } = error as Error;
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+// 获取汇总数据
+app.openapi(summaryRoute, async (c) => {
+  try {
+    const service = new GuizhouStatsService(AppDataSource);
+    const summary = await service.getSummaryStats();
+    return c.json(summary, 200);
+  } catch (error) {
+    const { message = '获取汇总数据失败' } = error as Error;
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+// 创建统计数据
+app.openapi(createRouteDef, async (c) => {
+  try {
+    const data = await c.req.json();
+    const service = new GuizhouStatsService(AppDataSource);
+    const result = await service.create(data);
+    return c.json(result, 200);
+  } catch (error) {
+    const { code = 500, message = '创建统计数据失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+// 批量创建统计数据
+app.openapi(batchCreateRoute, async (c) => {
+  try {
+    const { data } = await c.req.json();
+    const service = new GuizhouStatsService(AppDataSource);
+    const results = await service.createMany(data);
+    return c.json(results, 200);
+  } catch (error) {
+    const { code = 500, message = '批量创建统计数据失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+// 更新统计数据
+app.openapi(updateRouteDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const data = await c.req.json();
+    const service = new GuizhouStatsService(AppDataSource);
+    const result = await service.update(Number(id), data);
+    
+    if (!result) {
+      return c.json({ code: 404, message: '统计数据不存在' }, 404);
+    }
+    
+    return c.json(result, 200);
+  } catch (error) {
+    const { code = 500, message = '更新统计数据失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+// 删除统计数据
+app.openapi(deleteRouteDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const service = new GuizhouStatsService(AppDataSource);
+    const success = await service.delete(Number(id));
+    
+    if (!success) {
+      return c.json({ code: 404, message: '统计数据不存在' }, 404);
+    }
+    
+    return c.json({ success: true }, 200);
+  } catch (error) {
+    const { code = 500, message = '删除统计数据失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 2 - 1
src/server/data-source.ts

@@ -31,6 +31,7 @@ import { UserPreference } from "./modules/silver-users/user-preference.entity"
 import { SilverJob } from "./modules/silver-jobs/silver-job.entity"
 import { SilverJob } from "./modules/silver-jobs/silver-job.entity"
 import { HomeIcon } from "./modules/home/home-icon.entity"
 import { HomeIcon } from "./modules/home/home-icon.entity"
 import { AIAgent } from "./modules/ai-agents/ai-agent.entity"
 import { AIAgent } from "./modules/ai-agents/ai-agent.entity"
+import { GuizhouSilverStats } from "./modules/silver-users/guizhou-stats.entity"
 
 
 export const AppDataSource = new DataSource({
 export const AppDataSource = new DataSource({
   type: "mysql",
   type: "mysql",
@@ -46,7 +47,7 @@ export const AppDataSource = new DataSource({
     SilverKnowledge, SilverKnowledgeCategory, SilverKnowledgeTag,
     SilverKnowledge, SilverKnowledgeCategory, SilverKnowledgeTag,
     SilverKnowledgeTagRelation, SilverKnowledgeStats, SilverKnowledgeInteraction,
     SilverKnowledgeTagRelation, SilverKnowledgeStats, SilverKnowledgeInteraction,
     ElderlyUniversity, PolicyNews, UserPreference, SilverJob, HomeIcon,
     ElderlyUniversity, PolicyNews, UserPreference, SilverJob, HomeIcon,
-    AIAgent,
+    AIAgent, GuizhouSilverStats,
   ],
   ],
   migrations: [],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 81 - 0
src/server/modules/silver-users/guizhou-stats.entity.ts

@@ -0,0 +1,81 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+@Entity('guizhou_silver_stats')
+export class GuizhouSilverStats {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'city_name', type: 'varchar', length: 50, comment: '城市名称' })
+  cityName!: string;
+
+  @Column({ name: 'city_code', type: 'varchar', length: 10, comment: '城市代码' })
+  cityCode!: string;
+
+  @Column({ name: 'daily_new_jobs', type: 'int', unsigned: true, default: 0, comment: '每日新增银龄岗' })
+  dailyNewJobs!: number;
+
+  @Column({ name: 'total_jobs', type: 'int', unsigned: true, default: 0, comment: '累计银龄岗' })
+  totalJobs!: number;
+
+  @Column({ name: 'daily_new_talents', type: 'int', unsigned: true, default: 0, comment: '每日新增银龄库' })
+  dailyNewTalents!: number;
+
+  @Column({ name: 'total_talents', type: 'int', unsigned: true, default: 0, comment: '累计银龄库' })
+  totalTalents!: number;
+
+  @Column({ name: 'longitude', type: 'decimal', precision: 10, scale: 6, comment: '经度' })
+  longitude!: number;
+
+  @Column({ name: 'latitude', type: 'decimal', precision: 10, scale: 6, comment: '纬度' })
+  latitude!: number;
+
+  @Column({ name: 'stat_date', type: 'date', comment: '统计日期' })
+  statDate!: Date;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}
+
+// Zod Schemas for API
+export const GuizhouSilverStatsSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '统计ID', example: 1 }),
+  cityName: z.string().max(50).openapi({ description: '城市名称', example: '贵阳市' }),
+  cityCode: z.string().max(10).openapi({ description: '城市代码', example: '520100' }),
+  dailyNewJobs: z.number().int().min(0).openapi({ description: '每日新增银龄岗', example: 5 }),
+  totalJobs: z.number().int().min(0).openapi({ description: '累计银龄岗', example: 125 }),
+  dailyNewTalents: z.number().int().min(0).openapi({ description: '每日新增银龄库', example: 12 }),
+  totalTalents: z.number().int().min(0).openapi({ description: '累计银龄库', example: 890 }),
+  longitude: z.coerce.number().openapi({ description: '经度', example: 106.7070 }),
+  latitude: z.coerce.number().openapi({ description: '纬度', example: 26.5982 }),
+  statDate: z.coerce.date().openapi({ description: '统计日期', example: '2024-01-01' }),
+  createdAt: z.date().openapi({ description: '创建时间', example: '2024-01-01T00:00:00Z' }),
+  updatedAt: z.date().openapi({ description: '更新时间', example: '2024-01-01T00:00:00Z' })
+});
+
+export const CreateGuizhouSilverStatsDto = z.object({
+  cityName: z.string().max(50).openapi({ description: '城市名称', example: '贵阳市' }),
+  cityCode: z.string().max(10).openapi({ description: '城市代码', example: '520100' }),
+  dailyNewJobs: z.coerce.number().int().min(0).default(0).openapi({ description: '每日新增银龄岗', example: 5 }),
+  totalJobs: z.coerce.number().int().min(0).default(0).openapi({ description: '累计银龄岗', example: 125 }),
+  dailyNewTalents: z.coerce.number().int().min(0).default(0).openapi({ description: '每日新增银龄库', example: 12 }),
+  totalTalents: z.coerce.number().int().min(0).default(0).openapi({ description: '累计银龄库', example: 890 }),
+  longitude: z.coerce.number().openapi({ description: '经度', example: 106.7070 }),
+  latitude: z.coerce.number().openapi({ description: '纬度', example: 26.5982 }),
+  statDate: z.coerce.date().openapi({ description: '统计日期', example: '2024-01-01' })
+});
+
+export const UpdateGuizhouSilverStatsDto = z.object({
+  cityName: z.string().max(50).optional().openapi({ description: '城市名称', example: '贵阳市' }),
+  cityCode: z.string().max(10).optional().openapi({ description: '城市代码', example: '520100' }),
+  dailyNewJobs: z.coerce.number().int().min(0).optional().openapi({ description: '每日新增银龄岗', example: 5 }),
+  totalJobs: z.coerce.number().int().min(0).optional().openapi({ description: '累计银龄岗', example: 125 }),
+  dailyNewTalents: z.coerce.number().int().min(0).optional().openapi({ description: '每日新增银龄库', example: 12 }),
+  totalTalents: z.coerce.number().int().min(0).optional().openapi({ description: '累计银龄库', example: 890 }),
+  longitude: z.coerce.number().optional().openapi({ description: '经度', example: 106.7070 }),
+  latitude: z.coerce.number().optional().openapi({ description: '纬度', example: 26.5982 }),
+  statDate: z.coerce.date().optional().openapi({ description: '统计日期', example: '2024-01-01' })
+});

+ 184 - 0
src/server/modules/silver-users/guizhou-stats.service.ts

@@ -0,0 +1,184 @@
+import { DataSource, Repository } from 'typeorm';
+import { GuizhouSilverStats } from './guizhou-stats.entity';
+import debug from 'debug';
+
+const logger = debug('backend:service:guizhou-stats');
+
+export class GuizhouStatsService {
+  private repository: Repository<GuizhouSilverStats>;
+
+  constructor(dataSource: DataSource) {
+    this.repository = dataSource.getRepository(GuizhouSilverStats);
+  }
+
+  /**
+   * 获取所有贵州省银龄统计数据
+   */
+  async findAll(): Promise<GuizhouSilverStats[]> {
+    try {
+      return await this.repository.find({
+        order: { statDate: 'DESC', cityName: 'ASC' }
+      });
+    } catch (error) {
+      logger('Error finding all stats:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 按日期获取统计数据
+   */
+  async findByDate(statDate: Date): Promise<GuizhouSilverStats[]> {
+    try {
+      return await this.repository.find({
+        where: { statDate },
+        order: { cityName: 'ASC' }
+      });
+    } catch (error) {
+      logger('Error finding stats by date:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 按城市代码获取统计数据
+   */
+  async findByCityCode(cityCode: string): Promise<GuizhouSilverStats[]> {
+    try {
+      return await this.repository.find({
+        where: { cityCode },
+        order: { statDate: 'DESC' }
+      });
+    } catch (error) {
+      logger('Error finding stats by city code:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取最新统计数据
+   */
+  async getLatestStats(): Promise<GuizhouSilverStats[]> {
+    try {
+      const latestDate = await this.repository
+        .createQueryBuilder('stats')
+        .select('MAX(stats.statDate)', 'latestDate')
+        .getRawOne();
+
+      if (!latestDate?.latestDate) {
+        return [];
+      }
+
+      return await this.repository.find({
+        where: { statDate: latestDate.latestDate },
+        order: { cityName: 'ASC' }
+      });
+    } catch (error) {
+      logger('Error getting latest stats:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 创建统计数据
+   */
+  async create(data: Partial<GuizhouSilverStats>): Promise<GuizhouSilverStats> {
+    try {
+      const stat = this.repository.create(data);
+      return await this.repository.save(stat);
+    } catch (error) {
+      logger('Error creating stats:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 批量创建统计数据
+   */
+  async createMany(data: Partial<GuizhouSilverStats>[]): Promise<GuizhouSilverStats[]> {
+    try {
+      const stats = this.repository.create(data);
+      return await this.repository.save(stats);
+    } catch (error) {
+      logger('Error creating many stats:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 更新统计数据
+   */
+  async update(id: number, data: Partial<GuizhouSilverStats>): Promise<GuizhouSilverStats | null> {
+    try {
+      await this.repository.update(id, data);
+      return await this.repository.findOneBy({ id });
+    } catch (error) {
+      logger('Error updating stats:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 按城市代码和日期更新统计数据
+   */
+  async updateByCityAndDate(cityCode: string, statDate: Date, data: Partial<GuizhouSilverStats>): Promise<GuizhouSilverStats | null> {
+    try {
+      const existing = await this.repository.findOneBy({ cityCode, statDate });
+      if (existing) {
+        await this.repository.update(existing.id, data);
+        return await this.repository.findOneBy({ id: existing.id });
+      }
+      return null;
+    } catch (error) {
+      logger('Error updating stats by city and date:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取统计汇总数据
+   */
+  async getSummaryStats(): Promise<{
+    totalDailyNewJobs: number;
+    totalJobs: number;
+    totalDailyNewTalents: number;
+    totalTalents: number;
+    cityCount: number;
+  }> {
+    try {
+      const result = await this.repository
+        .createQueryBuilder('stats')
+        .select('SUM(stats.dailyNewJobs)', 'totalDailyNewJobs')
+        .addSelect('SUM(stats.totalJobs)', 'totalJobs')
+        .addSelect('SUM(stats.dailyNewTalents)', 'totalDailyNewTalents')
+        .addSelect('SUM(stats.totalTalents)', 'totalTalents')
+        .addSelect('COUNT(DISTINCT stats.cityCode)', 'cityCount')
+        .where('stats.statDate = (SELECT MAX(statDate) FROM guizhou_silver_stats)')
+        .getRawOne();
+
+      return {
+        totalDailyNewJobs: parseInt(result?.totalDailyNewJobs || '0'),
+        totalJobs: parseInt(result?.totalJobs || '0'),
+        totalDailyNewTalents: parseInt(result?.totalDailyNewTalents || '0'),
+        totalTalents: parseInt(result?.totalTalents || '0'),
+        cityCount: parseInt(result?.cityCount || '0'),
+      };
+    } catch (error) {
+      logger('Error getting summary stats:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 删除统计数据
+   */
+  async delete(id: number): Promise<boolean> {
+    try {
+      const result = await this.repository.delete(id);
+      return result.affected > 0;
+    } catch (error) {
+      logger('Error deleting stats:', error);
+      throw error;
+    }
+  }
+}