yourname 8 mesiacov pred
rodič
commit
0e408fdccb

+ 41 - 0
docs/homepage-deployment-guide.md

@@ -0,0 +1,41 @@
+# 银龄平台移动端首页部署指南
+
+## 🚀 部署准备
+
+### 环境要求
+- Node.js 18+
+- Redis 6.0+
+- MySQL 8.0+
+- 移动端浏览器支持
+
+### 依赖安装
+```bash
+# 安装Redis客户端
+npm install ioredis
+
+# 安装性能监控
+npm install --save-dev @artilleryio/artillery
+
+# 安装测试工具
+npm install --save-dev jest @testing-library/react
+```
+
+## 📦 部署步骤
+
+### 1. 环境配置
+创建 `.env` 文件:
+```bash
+# 数据库配置
+DATABASE_HOST=localhost
+DATABASE_PORT=3306
+DATABASE_USER=your_user
+DATABASE_PASSWORD=your_password
+DATABASE_NAME=silver_platform
+
+# Redis配置
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_PASSWORD=your_redis_password
+
+# CDN配置
+CDN_URL=https://your-c

+ 12 - 6
src/client/api.ts

@@ -1,14 +1,15 @@
 import { hc } from 'hono/client';
-import type { 
-  AuthRoutes, 
-  UserRoutes, 
-  RoleRoutes, 
-  FileRoutes, 
+import type {
+  AuthRoutes,
+  UserRoutes,
+  RoleRoutes,
+  FileRoutes,
   SilverJobsRoutes,
   SilverUsersRoutes,
   ElderlyUniversityRoutes,
   PolicyNewsRoutes,
-  UserPreferenceRoutes
+  UserPreferenceRoutes,
+  HomeRoutes
 } from '@/server/api';
 import axios from 'axios';
 
@@ -87,3 +88,8 @@ export const policyNewsClient = hc<PolicyNewsRoutes>('/', {
 export const userPreferenceClient = hc<UserPreferenceRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1['user-preferences'];
+
+// 首页API客户端
+export const homeClient = hc<HomeRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.home;

+ 113 - 0
src/client/api/home.ts

@@ -0,0 +1,113 @@
+import { hc } from 'hono/client';
+import type { HomeRoutes } from '@/server/api';
+import { axiosFetch } from './utils';
+
+export const homeClient = hc<HomeRoutes>('/api/v1', {
+  fetch: axiosFetch,
+}).api.v1.home;
+
+// 类型定义
+export type HomeData = {
+  banners: PolicyNews[];
+  recommendedJobs: Job[];
+  hotKnowledge: SilverKnowledge[];
+  timeBankActivities: TimeBankActivity[];
+  userStats: UserStats;
+};
+
+// 导出类型以便前端使用
+export type { PolicyNews } from '@/server/modules/silver-users/policy-news.entity';
+export type { Job } from '@/server/modules/silver-jobs/job.entity';
+export type { SilverKnowledge } from '@/server/modules/silver-users/silver-knowledge.entity';
+export type { SilverTimeBank } from '@/server/modules/silver-users/silver-time-bank.entity';
+
+interface PolicyNews {
+  id: number;
+  newsTitle: string;
+  newsContent: string;
+  images: string | null;
+  summary: string | null;
+  category: string | null;
+  viewCount: number;
+  createdAt: Date;
+}
+
+interface Job {
+  id: number;
+  title: string;
+  description: string;
+  salaryRange: string | null;
+  location: string | null;
+  company: {
+    id: number;
+    name: string;
+    logo: string | null;
+  } | null;
+  createdAt: Date;
+}
+
+interface SilverKnowledge {
+  id: number;
+  title: string;
+  content: string;
+  coverImage: string | null;
+  viewCount: number;
+  category: {
+    id: number;
+    name: string;
+  } | null;
+  createdAt: Date;
+}
+
+interface TimeBankActivity {
+  id: number;
+  workType: number;
+  organization: string;
+  workHours: number;
+  earnedPoints: number;
+  workDate: Date;
+}
+
+interface UserStats {
+  pointBalance: number;
+  timeBankHours: number;
+  publishedCount: number;
+  favoriteCount: number;
+}
+
+// API方法
+export const homeApi = {
+  /**
+   * 获取首页数据
+   */
+  async getHomeData(limit?: number) {
+    const response = await homeClient.$get({
+      query: limit ? { limit } : undefined
+    });
+    
+    if (response.status !== 200) {
+      throw new Error('获取首页数据失败');
+    }
+    
+    return response.json();
+  },
+
+  /**
+   * 搜索功能
+   */
+  async search(keyword: string, type: 'jobs' | 'knowledge' | 'companies' | 'all' = 'all', limit: number = 10) {
+    const response = await homeClient.search.$get({
+      query: {
+        keyword,
+        type,
+        limit
+      }
+    });
+    
+    if (response.status !== 200) {
+      throw new Error('搜索失败');
+    }
+    
+    return response.json();
+  }
+};

+ 176 - 0
src/client/mobile/components/EnhancedCarousel.tsx

@@ -0,0 +1,176 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { SkeletonLoader } from './SkeletonLoader';
+
+interface CarouselItem {
+  id: number;
+  title: string;
+  description: string;
+  image: string;
+  fallbackImage?: string;
+  link: string;
+}
+
+interface EnhancedCarouselProps {
+  items: CarouselItem[];
+  autoPlayInterval?: number;
+  className?: string;
+}
+
+// 增强版轮播图组件
+export const EnhancedCarousel: React.FC<EnhancedCarouselProps> = ({
+  items,
+  autoPlayInterval = 5000,
+  className = ''
+}) => {
+  const [currentSlide, setCurrentSlide] = useState(0);
+  const [isLoading, setIsLoading] = useState(true);
+  const [imageErrors, setImageErrors] = useState<Set<number>>(new Set());
+  const navigate = useNavigate();
+  const autoplayRef = useRef<NodeJS.Timeout | null>(null);
+
+  // 自动播放控制
+  const startAutoplay = () => {
+    if (autoplayRef.current) {
+      clearInterval(autoplayRef.current);
+    }
+    
+    autoplayRef.current = setInterval(() => {
+      setCurrentSlide((prev) => (prev + 1) % items.length);
+    }, autoPlayInterval);
+  };
+
+  const stopAutoplay = () => {
+    if (autoplayRef.current) {
+      clearInterval(autoplayRef.current);
+    }
+  };
+
+  // 启动/停止自动播放
+  useEffect(() => {
+    if (items.length > 1) {
+      startAutoplay();
+    }
+    
+    return () => stopAutoplay();
+  }, [items.length, autoPlayInterval]);
+
+  // 图片加载处理
+  useEffect(() => {
+    if (items.length > 0) {
+      setIsLoading(false);
+    }
+  }, [items]);
+
+  const handleImageError = (index: number) => {
+    setImageErrors(prev => new Set(prev).add(index));
+  };
+
+  const handleSlideClick = (link: string) => {
+    navigate(link);
+  };
+
+  const goToSlide = (index: number) => {
+    setCurrentSlide(index);
+  };
+
+  const handlePrev = () => {
+    setCurrentSlide((prev) => (prev - 1 + items.length) % items.length);
+  };
+
+  const handleNext = () => {
+    setCurrentSlide((prev) => (prev + 1) % items.length);
+  };
+
+  if (isLoading) {
+    return <div className="h-48 bg-gray-200 rounded-lg animate-pulse" />;
+  }
+
+  if (!items || items.length === 0) {
+    return <div className="h-48 bg-gray-100 rounded-lg flex items-center justify-center text-gray-500">暂无图片</div>;
+  }
+
+  return (
+    <div 
+      className={`relative bg-white overflow-hidden rounded-lg ${className}`}
+      onMouseEnter={stopAutoplay}
+      onMouseLeave={startAutoplay}
+    >
+      <div className="relative h-48">
+        <div
+          className="flex transition-transform duration-300 ease-in-out"
+          style={{ transform: `translateX(-${currentSlide * 100}%)` }}
+        >
+          {items.map((item, index) => (
+            <div
+              key={item.id}
+              className="w-full flex-shrink-0 relative cursor-pointer"
+              onClick={() => handleSlideClick(item.link)}
+            >
+              <img
+                src={imageErrors.has(index) ? item.fallbackImage || '/images/placeholder-banner.jpg' : item.image}
+                alt={item.title}
+                className="w-full h-48 object-cover"
+                onError={() => handleImageError(index)}
+                loading="lazy"
+              />
+              <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent">
+                <div className="absolute bottom-4 left-4 right-4 text-white">
+                  <h3 className="text-lg font-bold mb-1 line-clamp-2">{item.title}</h3>
+                  <p className="text-sm opacity-90 line-clamp-2">{item.description}</p>
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+
+        {/* 轮播指示器 */}
+        {items.length > 1 && (
+          <div className="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex space-x-2">
+            {items.map((_, index) => (
+              <button
+                key={index}
+                onClick={() => goToSlide(index)}
+                className={`w-2 h-2 rounded-full transition-all duration-200 ${
+                  index === currentSlide ? 'bg-white w-6' : 'bg-white/50 hover:bg-white/80'
+                }`}
+                aria-label={`跳转到第${index + 1}张`}
+              />
+            ))}
+          </div>
+        )}
+
+        {/* 左右箭头 */}
+        {items.length > 1 && (
+          <>
+            <button
+              onClick={handlePrev}
+              className="absolute left-2 top-1/2 -translate-y-1/2 bg-black/30 hover:bg-black/50 text-white p-2 rounded-full transition-all duration-200"
+              aria-label="上一张"
+            >
+              <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
+              </svg>
+            </button>
+            <button
+              onClick={handleNext}
+              className="absolute right-2 top-1/2 -translate-y-1/2 bg-black/30 hover:bg-black/50 text-white p-2 rounded-full transition-all duration-200"
+              aria-label="下一张"
+            >
+              <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+              </svg>
+            </button>
+          </>
+        )}
+      </div>
+
+      {/* 进度指示器 */}
+      {items.length > 1 && (
+        <div className="absolute bottom-4 right-4 bg-black/50 text-white text-xs px-2 py-1 rounded">
+          {currentSlide + 1} / {items.length}
+        </div>
+      )}
+    </div>
+  );
+};

+ 125 - 0
src/client/mobile/components/PullToRefresh.tsx

@@ -0,0 +1,125 @@
+import React, { useState, useRef, useEffect } from 'react';
+
+interface PullToRefreshProps {
+  onRefresh: () => Promise<void>;
+  children: React.ReactNode;
+  className?: string;
+}
+
+export const PullToRefresh: React.FC<PullToRefreshProps> = ({
+  onRefresh,
+  children,
+  className = ''
+}) => {
+  const [touchStart, setTouchStart] = useState(0);
+  const [touchY, setTouchY] = useState(0);
+  const [pulling, setPulling] = useState(false);
+  const [refreshing, setRefreshing] = useState(false);
+  const containerRef = useRef<HTMLDivElement>(null);
+
+  const handleTouchStart = (e: React.TouchEvent) => {
+    if (containerRef.current?.scrollTop === 0) {
+      setTouchStart(e.touches[0].clientY);
+      setTouchY(e.touches[0].clientY);
+    }
+  };
+
+  const handleTouchMove = (e: React.TouchEvent) => {
+    if (touchStart > 0) {
+      const currentY = e.touches[0].clientY;
+      setTouchY(currentY);
+      
+      const pullDistance = currentY - touchStart;
+      if (pullDistance > 0) {
+        setPulling(true);
+        e.preventDefault();
+      }
+    }
+  };
+
+  const handleTouchEnd = async () => {
+    if (pulling) {
+      const pullDistance = touchY - touchStart;
+      
+      if (pullDistance > 80) { // 阈值:80px
+        setRefreshing(true);
+        try {
+          await onRefresh();
+        } finally {
+          setRefreshing(false);
+        }
+      }
+      
+      setTouchStart(0);
+      setTouchY(0);
+      setPulling(false);
+    }
+  };
+
+  const pullDistance = Math.max(0, touchY - touchStart);
+  const pullPercentage = Math.min(pullDistance / 80, 1);
+
+  return (
+    <div
+      ref={containerRef}
+      className={`relative ${className}`}
+      onTouchStart={handleTouchStart}
+      onTouchMove={handleTouchMove}
+      onTouchEnd={handleTouchEnd}
+    >
+      {/* 下拉刷新指示器 */}
+      <div
+        className="absolute top-0 left-0 right-0 flex justify-center items-center transition-all duration-200"
+        style={{
+          height: pulling ? Math.min(pullDistance, 60) : 0,
+          opacity: pulling ? 1 : 0
+        }}
+      >
+        {refreshing ? (
+          <div className="flex items-center">
+            <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
+            <span className="ml-2 text-sm text-blue-600">刷新中...</span>
+          </div>
+        ) : (
+          <div
+            className="text-center"
+            style={{ transform: `scale(${pullPercentage})` }}
+          >
+            <div className="text-blue-600 text-sm">
+              下拉可以刷新
+            </div>
+          </div>
+        )}
+      </div>
+
+      {/* 内容区域 */}
+      <div
+        style={{
+          transform: pulling ? `translateY(${Math.min(pullDistance, 60)}px)` : 'translateY(0)',
+          transition: pulling ? 'none' : 'transform 0.3s ease-out'
+        }}
+      >
+        {children}
+      </div>
+    </div>
+  );
+};
+
+// 基于浏览器API的下拉刷新(适用于现代浏览器)
+export const usePullToRefresh = (onRefresh: () => Promise<void>) => {
+  const [isRefreshing, setIsRefreshing] = useState(false);
+
+  const handleRefresh = async () => {
+    setIsRefreshing(true);
+    try {
+      await onRefresh();
+    } finally {
+      setIsRefreshing(false);
+    }
+  };
+
+  return {
+    isRefreshing,
+    handleRefresh
+  };
+};

+ 60 - 0
src/client/mobile/components/SkeletonLoader.tsx

@@ -0,0 +1,60 @@
+import React from 'react';
+
+// 骨架屏加载组件
+export const SkeletonLoader: React.FC<{ className?: string }> = ({ className }) => {
+  return (
+    <div className={`animate-pulse ${className}`}>
+      <div className="bg-gray-200 rounded"></div>
+    </div>
+  );
+};
+
+// 轮播图骨架屏
+export const BannerSkeleton: React.FC = () => (
+  <div className="relative h-48 bg-gray-200 rounded-lg overflow-hidden">
+    <div className="absolute inset-0 bg-gradient-to-t from-gray-300 to-gray-200"></div>
+  </div>
+);
+
+// 用户统计卡片骨架屏
+export const UserStatsSkeleton: React.FC = () => (
+  <div className="grid grid-cols-2 gap-4 p-4">
+    {[1, 2, 3, 4].map((i) => (
+      <div key={i} className="bg-white rounded-lg p-4 shadow-sm">
+        <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
+        <div className="h-6 bg-gray-300 rounded w-1/2"></div>
+      </div>
+    ))}
+  </div>
+);
+
+// 列表项骨架屏
+export const ListItemSkeleton: React.FC<{ count?: number }> = ({ count = 3 }) => (
+  <div className="space-y-3 p-4">
+    {Array.from({ length: count }).map((_, i) => (
+      <div key={i} className="flex items-center p-3 bg-gray-50 rounded-lg">
+        <div className="w-12 h-12 bg-gray-200 rounded-full mr-3"></div>
+        <div className="flex-1">
+          <div className="h-4 bg-gray-200 rounded mb-2 w-3/4"></div>
+          <div className="h-3 bg-gray-200 rounded mb-1 w-1/2"></div>
+          <div className="flex items-center mt-2">
+            <div className="h-4 bg-gray-200 rounded w-16 mr-2"></div>
+            <div className="h-4 bg-gray-200 rounded w-12"></div>
+          </div>
+        </div>
+      </div>
+    ))}
+  </div>
+);
+
+// 分类卡片骨架屏
+export const CategoryCardSkeleton: React.FC = () => (
+  <div className="grid grid-cols-3 gap-4 p-4">
+    {Array.from({ length: 6 }).map((_, i) => (
+      <div key={i} className="flex flex-col items-center">
+        <div className="w-12 h-12 bg-gray-200 rounded-full mb-2"></div>
+        <div className="h-3 bg-gray-200 rounded w-10"></div>
+      </div>
+    ))}
+  </div>
+);

+ 65 - 0
src/client/mobile/components/UserStatsCard.tsx

@@ -0,0 +1,65 @@
+import React from 'react';
+
+interface UserStatsCardProps {
+  stats: {
+    pointBalance: number;
+    timeBankHours: number;
+    publishedCount: number;
+    favoriteCount: number;
+  };
+}
+
+const UserStatsCard: React.FC<UserStatsCardProps> = ({ stats }) => {
+  const statsData = [
+    {
+      title: '积分余额',
+      value: stats.pointBalance,
+      icon: '🎯',
+      color: 'bg-blue-500',
+      unit: '分'
+    },
+    {
+      title: '时间银行',
+      value: stats.timeBankHours,
+      icon: '⏰',
+      color: 'bg-green-500',
+      unit: '小时'
+    },
+    {
+      title: '知识分享',
+      value: stats.publishedCount,
+      icon: '📚',
+      color: 'bg-purple-500',
+      unit: '篇'
+    },
+    {
+      title: '我的收藏',
+      value: stats.favoriteCount,
+      icon: '⭐',
+      color: 'bg-yellow-500',
+      unit: '个'
+    }
+  ];
+
+  return (
+    <div className="bg-white rounded-lg shadow-sm p-4 mb-4">
+      <h3 className="text-lg font-bold text-gray-900 mb-4">我的数据</h3>
+      <div className="grid grid-cols-2 gap-4">
+        {statsData.map((stat, index) => (
+          <div key={index} className="text-center">
+            <div className={`w-12 h-12 ${stat.color} rounded-full flex items-center justify-center text-white text-xl mx-auto mb-2`}>
+              {stat.icon}
+            </div>
+            <div className="text-2xl font-bold text-gray-900">
+              {stat.value}
+            </div>
+            <div className="text-sm text-gray-600">{stat.title}</div>
+            <div className="text-xs text-gray-500">{stat.unit}</div>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+export default UserStatsCard;

+ 77 - 0
src/client/mobile/hooks/useHomeData.ts

@@ -0,0 +1,77 @@
+import { useQuery } from '@tanstack/react-query';
+import { homeClient } from '@/client/api';
+import { useAuth } from './AuthProvider';
+
+// 查询键
+export const homeKeys = {
+  all: ['home'] as const,
+  banners: () => [...homeKeys.all, 'banners'] as const,
+  jobs: () => [...homeKeys.all, 'jobs'] as const,
+  knowledge: () => [...homeKeys.all, 'knowledge'] as const,
+  timeBank: () => [...homeKeys.all, 'timeBank'] as const,
+  search: (keyword: string) => [...homeKeys.all, 'search', keyword] as const,
+};
+
+/**
+ * 获取首页完整数据的Hook
+ */
+export const useHomeData = () => {
+  const { user } = useAuth();
+  
+  return useQuery({
+    queryKey: homeKeys.all,
+    queryFn: async () => {
+      const response = await homeClient.$get();
+      if (response.status !== 200) {
+        throw new Error('获取首页数据失败');
+      }
+      return response.json();
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟
+    gcTime: 10 * 60 * 1000, // 10分钟 (gcTime替代cacheTime)
+    enabled: !!user, // 需要登录
+  });
+};
+
+/**
+ * 搜索功能的Hook
+ */
+export const useSearch = (keyword: string, type: 'jobs' | 'knowledge' | 'companies' | 'all' = 'all', limit = 10) => {
+  const { user } = useAuth();
+  
+  return useQuery({
+    queryKey: homeKeys.search(keyword),
+    queryFn: async () => {
+      const response = await homeClient.search.$get({
+        query: {
+          keyword,
+          type,
+          limit
+        }
+      });
+      if (response.status !== 200) {
+        throw new Error('搜索失败');
+      }
+      return response.json();
+    },
+    enabled: !!keyword && !!user,
+    staleTime: 3 * 60 * 1000,
+  });
+};
+
+/**
+ * 下拉刷新的Hook
+ */
+export const usePullToRefresh = () => {
+  const { data, refetch, isFetching } = useHomeData();
+  
+  const onRefresh = async () => {
+    await refetch();
+  };
+  
+  return {
+    data,
+    onRefresh,
+    isRefreshing: isFetching,
+  };
+};

+ 354 - 187
src/client/mobile/pages/HomePage.tsx

@@ -1,112 +1,144 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
 import { useNavigate } from 'react-router-dom';
+import { useHomeData } from '../hooks/useHomeData';
 import { useAuth } from '../hooks/AuthProvider';
+import { EnhancedCarousel } from '../components/EnhancedCarousel';
+import UserStatsCard from '../components/UserStatsCard';
+import { SkeletonLoader, BannerSkeleton, ListItemSkeleton, UserStatsSkeleton } from '../components/SkeletonLoader';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 
-// 图片加载组件
-const LazyImage: React.FC<{
-  src: string;
-  alt: string;
-  className?: string;
-  fallback?: string;
-  onClick?: () => void;
-}> = ({ src, alt, className, fallback, onClick }) => {
-  const [imageSrc, setImageSrc] = useState(fallback || '/images/placeholder.jpg');
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    const img = new Image();
-    img.src = src;
-    img.onload = () => {
-      setImageSrc(src);
-      setIsLoading(false);
-    };
-    img.onerror = () => {
-      setIsLoading(false);
-    };
-  }, [src]);
+// 创建QueryClient实例
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      staleTime: 5 * 60 * 1000,
+      gcTime: 10 * 60 * 1000,
+      refetchOnWindowFocus: false,
+    },
+  },
+});
 
-  return (
-    <img
-      src={imageSrc}
-      alt={alt}
-      className={`${className} ${isLoading ? 'animate-pulse bg-gray-200' : ''} ${onClick ? 'cursor-pointer' : ''}`}
-      onClick={onClick}
-    />
-  );
-};
+// 数据转换工具
+const transformPolicyNews = (news: any[]) => 
+  news.map(item => ({
+    id: item.id,
+    title: item.newsTitle,
+    description: item.summary || item.newsContent.substring(0, 100) + '...',
+    image: item.images?.split(',')[0] || '/images/banner1.jpg',
+    fallbackImage: '/images/placeholder-banner.jpg',
+    link: `/policy-news/${item.id}`
+  }));
 
-// 首页组件 - 银龄智慧平台主入口
-const HomePage: React.FC = () => {
+const transformJobs = (jobs: any[]) =>
+  jobs.map(job => ({
+    id: job.id,
+    title: job.title,
+    company: job.company?.name || '未知公司',
+    salary: job.salaryRange || '面议',
+    image: job.company?.logo || `https://picsum.photos/seed/${job.id}/200/200`,
+    tags: [job.location ? job.location.split(' ')[0] : '全国', '热门']
+  }));
+
+const transformKnowledge = (knowledge: any[]) =>
+  knowledge.map(item => ({
+    id: item.id,
+    title: item.title,
+    category: item.category?.name || '其他',
+    coverImage: item.coverImage || `https://picsum.photos/seed/${item.id}/200/200`,
+    viewCount: item.viewCount || 0
+  }));
+
+const transformTimeBank = (activities: any[]) =>
+  activities.map(activity => ({
+    id: activity.id,
+    workType: activity.workType === 1 ? '志愿服务' : '技能培训',
+    organization: activity.organization,
+    workHours: activity.workHours,
+    earnedPoints: activity.earnedPoints,
+    workDate: new Date(activity.workDate).toLocaleDateString()
+  }));
+
+// 主HomePage组件
+const HomeContent: React.FC = () => {
   const navigate = useNavigate();
   const { user } = useAuth();
   const [searchQuery, setSearchQuery] = useState('');
-  const [currentSlide, setCurrentSlide] = useState(0);
-
-  // 轮播图数据 - 使用更稳定的图片源
-  const carouselItems = [
-    {
-      id: 1,
-      title: '银龄智慧平台上线啦',
-      description: '为银龄群体打造的专业服务平台',
-      image: '/images/banner1.jpg',
-      fallbackImage: '/images/placeholder-banner.jpg',
-      link: '/home'
-    },
-    {
-      id: 2,
-      title: '时间银行互助养老',
-      description: '存储时间,收获温暖',
-      image: '/images/banner2.jpg',
-      fallbackImage: '/images/placeholder-banner.jpg',
-      link: '/time-bank'
-    }
-  ];
-
-  // 自动轮播功能
-  useEffect(() => {
-    const interval = setInterval(() => {
-      setCurrentSlide((prev) => (prev + 1) % carouselItems.length);
-    }, 5000);
-    return () => clearInterval(interval);
-  }, [carouselItems.length]);
-
-  // 服务分类数据
-  const serviceCategories = [
-    { name: '银龄岗', icon: '💼', path: '/silver-jobs', color: 'bg-blue-500' },
-    { name: '银龄库', icon: '👥', path: '/silver-talents', color: 'bg-green-500' },
-    { name: '银龄智库', icon: '📚', path: '/silver-wisdom', color: 'bg-purple-500' },
-    { name: '老年大学', icon: '🎓', path: '/elderly-university', color: 'bg-orange-500' },
-    { name: '时间银行', icon: '⏰', path: '/time-bank', color: 'bg-red-500' },
-    { name: '政策资讯', icon: '📰', path: '/policy-news', color: 'bg-indigo-500' }
-  ];
-
-  // 推荐数据
-  const recommendedItems = [
-    {
-      id: 1,
-      title: '教育培训师',
-      company: '智慧学堂',
-      salary: '面议',
-      image: 'https://picsum.photos/id/20/200/200',
-      tags: ['兼职', '教育']
-    },
-    {
-      id: 2,
-      title: '健康咨询师',
-      company: '社区服务中心',
-      salary: '3000-5000元/月',
-      image: 'https://picsum.photos/id/21/200/200',
-      tags: ['健康', '咨询']
+  const searchRef = useRef<HTMLInputElement>(null);
+  const [isPulling, setIsPulling] = useState(false);
+
+  // 获取首页数据
+  const {
+    data: homeData,
+    isLoading,
+    isError,
+    error,
+    refetch
+  } = useHomeData();
+
+  // 下拉刷新处理
+  const handlePullToRefresh = async () => {
+    setIsPulling(true);
+    try {
+      await refetch();
+    } finally {
+      setIsPulling(false);
     }
-  ];
+  };
 
+  // 搜索处理
   const handleSearch = () => {
     if (searchQuery.trim()) {
-      // 实现搜索逻辑
-      console.log('搜索:', searchQuery);
+      navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
+    }
+  };
+
+  const handleKeyPress = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') {
+      handleSearch();
     }
   };
 
+  // 错误状态
+  if (isError) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
+        <div className="text-center bg-white p-8 rounded-lg shadow-sm">
+          <div className="text-red-500 text-6xl mb-4">⚠️</div>
+          <h3 className="text-lg font-bold text-gray-900 mb-2">加载失败</h3>
+          <p className="text-gray-600 mb-4">{error?.message || '获取首页数据失败'}</p>
+          <button
+            onClick={() => refetch()}
+            className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors"
+          >
+            重新加载
+          </button>
+        </div>
+      </div>
+    );
+  }
+
+  // 加载状态
+  if (isLoading || !homeData) {
+    return (
+      <div className="min-h-screen bg-gray-50">
+        <HeaderSkeleton />
+        <div className="p-4 space-y-4">
+          <BannerSkeleton />
+          {user && <UserStatsSkeleton />}
+          <CategorySkeleton />
+          <ListItemSkeleton count={3} />
+          <ListItemSkeleton count={4} />
+        </div>
+      </div>
+    );
+  }
+
+  // 数据转换
+  const banners = transformPolicyNews(homeData.banners || []);
+  const recommendedJobs = transformJobs(homeData.recommendedJobs || []);
+  const hotKnowledge = transformKnowledge(homeData.hotKnowledge || []);
+  const timeBankActivities = transformTimeBank(homeData.timeBankActivities || []);
+
   return (
     <div className="min-h-screen bg-gray-50">
       {/* 顶部导航栏 */}
@@ -116,12 +148,17 @@ const HomePage: React.FC = () => {
             <h1 className="text-xl font-bold text-blue-600">银龄智慧平台</h1>
             {user ? (
               <div className="flex items-center space-x-2">
-                <span className="text-sm text-gray-600">欢迎,{user.username}</span>
+                <span className="text-sm text-gray-600 hidden sm:block">欢迎,{user.username}</span>
+                <img 
+                  src={user.avatar || '/images/avatar-placeholder.jpg'} 
+                  alt="用户头像"
+                  className="w-8 h-8 rounded-full object-cover"
+                />
               </div>
             ) : (
               <button 
                 onClick={() => navigate('/login')}
-                className="text-blue-600 text-sm px-3 py-1 rounded-full border border-blue-600"
+                className="text-blue-600 text-sm px-3 py-1 rounded-full border border-blue-600 hover:bg-blue-50 transition-colors"
               >
                 登录
               </button>
@@ -134,117 +171,247 @@ const HomePage: React.FC = () => {
               <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
             </svg>
             <input
+              ref={searchRef}
               type="text"
-              placeholder="搜索岗位、人才、知识..."
+              placeholder="搜索岗位、知识、企业..."
               value={searchQuery}
               onChange={(e) => setSearchQuery(e.target.value)}
-              onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
+              onKeyPress={handleKeyPress}
               className="flex-1 bg-transparent outline-none text-sm"
             />
+            <button
+              onClick={handleSearch}
+              className="text-blue-600 text-xs px-2 hover:text-blue-800 transition-colors"
+            >
+              搜索
+            </button>
           </div>
         </div>
       </header>
 
-      {/* 滚动轮播图 */}
-      <div className="relative bg-white overflow-hidden">
-        <div className="relative h-48">
-          <div
-            className="flex transition-transform duration-300 ease-in-out"
-            style={{ transform: `translateX(-${currentSlide * 100}%)` }}
-          >
-            {carouselItems.map((item) => (
-              <div
-                key={item.id}
-                className="w-full flex-shrink-0 relative cursor-pointer"
-                onClick={() => navigate(item.link)}
-              >
-                <LazyImage
-                  src={item.image}
-                  fallback={item.fallbackImage}
-                  alt={item.title}
-                  className="w-full h-48 object-cover"
-                />
-                <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent">
-                  <div className="absolute bottom-4 left-4 text-white">
-                    <h3 className="text-lg font-bold">{item.title}</h3>
-                    <p className="text-sm">{item.description}</p>
-                  </div>
-                </div>
-              </div>
-            ))}
+      {/* 下拉刷新指示器 */}
+      {isPulling && (
+        <div className="bg-blue-50 text-blue-600 text-center py-2">
+          <div className="flex items-center justify-center">
+            <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
+            正在刷新...
           </div>
-          
-          {/* 轮播指示器 */}
-          <div className="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex space-x-2">
-            {carouselItems.map((_, index) => (
-              <div
+        </div>
+      )}
+
+      {/* 主要内容 */}
+      <div className="pb-16">
+        {/* 轮播图 */}
+        {banners.length > 0 && (
+          <div className="bg-white">
+            <EnhancedCarousel 
+              items={banners} 
+              autoPlayInterval={5000}
+              className="rounded-b-lg"
+            />
+          </div>
+        )}
+
+        {/* 用户统计 */}
+        {user && homeData.userStats && (
+          <div className="px-4 mt-2">
+            <UserStatsCard stats={homeData.userStats} />
+          </div>
+        )}
+
+        {/* 服务分类 */}
+        <div className="bg-white mt-2 p-4">
+          <div className="grid grid-cols-3 gap-3">
+            {[
+              { name: '银龄岗', icon: '💼', path: '/silver-jobs', color: 'bg-blue-500' },
+              { name: '银龄库', icon: '👥', path: '/silver-talents', color: 'bg-green-500' },
+              { name: '银龄智库', icon: '📚', path: '/silver-wisdom', color: 'bg-purple-500' },
+              { name: '老年大学', icon: '🎓', path: '/elderly-university', color: 'bg-orange-500' },
+              { name: '时间银行', icon: '⏰', path: '/time-bank', color: 'bg-red-500' },
+              { name: '政策资讯', icon: '📰', path: '/policy-news', color: 'bg-indigo-500' }
+            ].map((category, index) => (
+              <button
                 key={index}
-                className={`w-2 h-2 rounded-full ${index === currentSlide ? 'bg-white' : 'bg-white/50'}`}
-              />
+                onClick={() => navigate(category.path)}
+                className="flex flex-col items-center p-2 rounded-lg hover:bg-gray-50 transition-colors"
+              >
+                <div className={`${category.color} text-white w-12 h-12 rounded-full flex items-center justify-center text-xl mb-1`}>
+                  {category.icon}
+                </div>
+                <span className="text-xs text-gray-700">{category.name}</span>
+              </button>
             ))}
           </div>
         </div>
-      </div>
 
-      {/* 服务分类区域 */}
-      <div className="bg-white mt-2 p-4">
-        <div className="grid grid-cols-3 gap-4">
-          {serviceCategories.map((category, index) => (
-            <button
-              key={index}
-              onClick={() => navigate(category.path)}
-              className="flex flex-col items-center p-3 rounded-lg hover:bg-gray-50 transition-colors"
-            >
-              <div className={`${category.color} text-white w-12 h-12 rounded-full flex items-center justify-center text-xl mb-2`}>
-                {category.icon}
-              </div>
-              <span className="text-sm text-gray-700">{category.name}</span>
-            </button>
-          ))}
-        </div>
-      </div>
+        {/* 推荐岗位 */}
+        {recommendedJobs.length > 0 && (
+          <div className="bg-white mt-2 p-4">
+            <div className="flex justify-between items-center mb-3">
+              <h2 className="text-lg font-bold text-gray-900">推荐岗位</h2>
+              <button 
+                onClick={() => navigate('/silver-jobs')}
+                className="text-blue-600 text-sm"
+              >
+                查看更多 →
+              </button>
+            </div>
+            
+            <div className="space-y-3">
+              {recommendedJobs.slice(0, 3).map((item) => (
+                <div
+                  key={item.id}
+                  className="flex items-center p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors"
+                  onClick={() => navigate(`/silver-jobs/${item.id}`)}
+                >
+                  <img
+                    src={item.image}
+                    alt={item.title}
+                    className="w-12 h-12 rounded-full object-cover mr-3"
+                    loading="lazy"
+                  />
+                  <div className="flex-1">
+                    <h4 className="font-medium text-gray-900 text-sm line-clamp-1">{item.title}</h4>
+                    <p className="text-xs text-gray-600 line-clamp-1">{item.company}</p>
+                    <div className="flex items-center mt-1">
+                      <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
+                        {item.salary}
+                      </span>
+                      {item.tags.slice(0, 1).map((tag, i) => (
+                        <span key={i} className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded ml-2">
+                          {tag}
+                        </span>
+                      ))}
+                    </div>
+                  </div>
+                </div>
+              ))}
+            </div>
+          </div>
+        )}
 
-      {/* 推荐区域 */}
-      <div className="bg-white mt-2 p-4">
-        <div className="flex justify-between items-center mb-4">
-          <h2 className="text-lg font-bold text-gray-900">推荐岗位</h2>
-          <button 
-            onClick={() => navigate('/silver-jobs')}
-            className="text-blue-600 text-sm"
-          >
-            查看更多 →
-          </button>
-        </div>
-        
-        <div className="space-y-3">
-          {recommendedItems.map((item) => (
-            <div
-              key={item.id}
-              className="flex items-center p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors"
-              onClick={() => navigate(`/silver-jobs/${item.id}`)}
-            >
-              <LazyImage
-                src={item.image}
-                alt={item.title}
-                className="w-12 h-12 rounded-full object-cover mr-3"
-              />
-              <div className="flex-1">
-                <h4 className="font-medium text-gray-900">{item.title}</h4>
-                <p className="text-sm text-gray-600">{item.company}</p>
-                <div className="flex items-center mt-1">
-                  <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
-                    {item.salary}
-                  </span>
-                  <span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded ml-2">
-                    {item.tags[0]}
-                  </span>
+        {/* 热门知识 */}
+        {hotKnowledge.length > 0 && (
+          <div className="bg-white mt-2 p-4">
+            <div className="flex justify-between items-center mb-3">
+              <h2 className="text-lg font-bold text-gray-900">热门知识</h2>
+              <button 
+                onClick={() => navigate('/silver-wisdom')}
+                className="text-blue-600 text-sm"
+              >
+                查看更多 →
+              </button>
+            </div>
+            
+            <div className="grid grid-cols-2 gap-3">
+              {hotKnowledge.slice(0, 4).map((item) => (
+                <div
+                  key={item.id}
+                  className="bg-gray-50 rounded-lg p-2 cursor-pointer hover:bg-gray-100 transition-colors"
+                  onClick={() => navigate(`/silver-wisdom/${item.id}`)}
+                >
+                  <img
+                    src={item.coverImage}
+                    alt={item.title}
+                    className="w-full h-20 object-cover rounded mb-1"
+                    loading="lazy"
+                  />
+                  <h4 className="text-xs font-medium text-gray-900 line-clamp-2">{item.title}</h4>
+                  <div className="flex items-center justify-between mt-1">
+                    <span className="text-xs text-gray-500">{item.category}</span>
+                    <span className="text-xs text-gray-500">{item.viewCount}阅读</span>
+                  </div>
                 </div>
-              </div>
+              ))}
             </div>
-          ))}
-        </div>
+          </div>
+        )}
+
+        {/* 时间银行活动 */}
+        {timeBankActivities.length > 0 && (
+          <div className="bg-white mt-2 p-4">
+            <div className="flex justify-between items-center mb-3">
+              <h2 className="text-lg font-bold text-gray-900">时间银行活动</h2>
+              <button 
+                onClick={() => navigate('/time-bank')}
+                className="text-blue-600 text-sm"
+              >
+                查看更多 →
+              </button>
+            </div>
+            
+            <div className="space-y-3">
+              {timeBankActivities.slice(0, 2).map((activity) => (
+                <div
+                  key={activity.id}
+                  className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
+                >
+                  <div>
+                    <h4 className="font-medium text-gray-900 text-sm">{activity.organization}</h4>
+                    <p className="text-xs text-gray-600">
+                      {activity.workType} · {activity.workHours}小时
+                    </p>
+                  </div>
+                  <div className="text-right">
+                    <div className="text-lg font-bold text-blue-600">{activity.earnedPoints}</div>
+                    <div className="text-xs text-gray-500">积分奖励</div>
+                  </div>
+                </div>
+              ))}
+            </div>
+          </div>
+        )}
+
+        {/* 底部空间 */}
+        <div className="h-8"></div>
       </div>
+
+      {/* 刷新按钮 */}
+      <button
+        onClick={handlePullToRefresh}
+        className="fixed bottom-20 right-4 bg-blue-600 text-white p-3 rounded-full shadow-lg"
+        disabled={isPulling}
+      >
+        <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
+        </svg>
+      </button>
+    </div>
+  );
+};
+
+// Header骨架屏
+const HeaderSkeleton: React.FC = () => (
+  <div className="bg-white shadow-sm px-4 py-3">
+    <div className="flex items-center justify-between mb-3">
+      <div className="h-6 bg-gray-200 rounded w-32 animate-pulse"></div>
+      <div className="h-8 bg-gray-200 rounded px-4 animate-pulse"></div>
     </div>
+    <div className="flex items-center bg-gray-100 rounded-full px-4 py-2">
+      <div className="w-5 h-5 bg-gray-300 rounded mr-2 animate-pulse"></div>
+      <div className="h-4 bg-gray-200 rounded flex-1 animate-pulse"></div>
+    </div>
+  </div>
+);
+
+// 分类骨架屏
+const CategorySkeleton: React.FC = () => (
+  <div className="grid grid-cols-3 gap-3 p-4">
+    {Array.from({ length: 6 }).map((_, i) => (
+      <div key={i} className="flex flex-col items-center">
+        <div className="w-12 h-12 bg-gray-200 rounded-full mb-1 animate-pulse"></div>
+        <div className="h-3 bg-gray-200 rounded w-10 animate-pulse"></div>
+      </div>
+    ))}
+  </div>
+);
+
+// 主组件包装
+const HomePage: React.FC = () => {
+  return (
+    <QueryClientProvider client={queryClient}>
+      <NewHomePage />
+    </QueryClientProvider>
   );
 };
 

+ 332 - 0
src/client/mobile/pages/NewHomePage.tsx

@@ -0,0 +1,332 @@
+import React, { useState, useRef, useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useHomeData } from '../hooks/useHomeData';
+import { EnhancedCarousel } from '../components/EnhancedCarousel';
+import { UserStatsCard } from '../components/UserStatsCard';
+import { SkeletonLoader, BannerSkeleton, ListItemSkeleton, UserStatsSkeleton } from '../components/SkeletonLoader';
+import { useAuth } from '../hooks/AuthProvider';
+
+// 数据转换工具
+const transformPolicyNews = (news: any[]) => 
+  news.map(item => ({
+    id: item.id,
+    title: item.newsTitle,
+    description: item.summary || item.newsContent.substring(0, 100) + '...',
+    image: item.images?.split(',')[0] || '/images/banner1.jpg',
+    fallbackImage: '/images/placeholder-banner.jpg',
+    link: `/policy-news/${item.id}`
+  }));
+
+const transformJobs = (jobs: any[]) =>
+  jobs.map(job => ({
+    id: job.id,
+    title: job.title,
+    company: job.company?.name || '未知公司',
+    salary: job.salaryRange || '面议',
+    image: job.company?.logo || `https://picsum.photos/seed/${job.id}/200/200`,
+    tags: [job.location ? job.location.split(' ')[0] : '全国', '热门']
+  }));
+
+const transformKnowledge = (knowledge: any[]) =>
+  knowledge.map(item => ({
+    id: item.id,
+    title: item.title,
+    category: item.category?.name || '其他',
+    coverImage: item.coverImage || `https://picsum.photos/seed/${item.id}/200/200`,
+    viewCount: item.viewCount || 0
+  }));
+
+const NewHomePage: React.FC = () => {
+  const navigate = useNavigate();
+  const { user } = useAuth();
+  const [searchQuery, setSearchQuery] = useState('');
+  const searchRef = useRef<HTMLInputElement>(null);
+
+  // 获取首页数据
+  const {
+    data: homeData,
+    isLoading,
+    isError,
+    error,
+    refetch
+  } = useHomeData();
+
+  // 处理搜索
+  const handleSearch = useCallback(() => {
+    if (searchQuery.trim()) {
+      navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
+    }
+  }, [searchQuery, navigate]);
+
+  const handleKeyPress = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') {
+      handleSearch();
+    }
+  };
+
+  // 下拉刷新
+  const handlePullToRefresh = useCallback(() => {
+    refetch();
+  }, [refetch]);
+
+  // 错误状态处理
+  if (isError) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
+        <div className="text-center">
+          <div className="text-red-500 mb-4">加载失败</div>
+          <div className="text-gray-600 mb-4">{error?.message || '获取首页数据失败'}</div>
+          <button
+            onClick={() => refetch()}
+            className="bg-blue-600 text-white px-4 py-2 rounded-lg"
+          >
+            重新加载
+          </button>
+        </div>
+      </div>
+    );
+  }
+
+  // 骨架屏加载状态
+  if (isLoading || !homeData) {
+    return (
+      <div className="min-h-screen bg-gray-50">
+        <div className="sticky top-0 z-10 bg-white shadow-sm">
+          <div className="px-4 py-3">
+            <div className="flex items-center justify-between mb-3">
+              <div className="h-6 bg-gray-200 rounded w-32 animate-pulse"></div>
+              <div className="h-8 bg-gray-200 rounded px-4 animate-pulse"></div>
+            </div>
+            <div className="flex items-center bg-gray-100 rounded-full px-4 py-2">
+              <div className="w-5 h-5 bg-gray-300 rounded mr-2 animate-pulse"></div>
+              <div className="h-4 bg-gray-200 rounded flex-1 animate-pulse"></div>
+            </div>
+          </div>
+        </div>
+        
+        <div className="p-4 space-y-4">
+          <BannerSkeleton />
+          <UserStatsSkeleton />
+          <ListItemSkeleton count={3} />
+          <ListItemSkeleton count={3} />
+          <ListItemSkeleton count={3} />
+        </div>
+      </div>
+    );
+  }
+
+  // 数据转换
+  const banners = transformPolicyNews(homeData.banners || []);
+  const recommendedJobs = transformJobs(homeData.recommendedJobs || []);
+  const hotKnowledge = transformKnowledge(homeData.hotKnowledge || []);
+  const timeBankActivities = homeData.timeBankActivities || [];
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      {/* 顶部导航栏 */}
+      <header className="bg-white shadow-sm sticky top-0 z-10">
+        <div className="px-4 py-3">
+          <div className="flex items-center justify-between mb-3">
+            <h1 className="text-xl font-bold text-blue-600">银龄智慧平台</h1>
+            {user ? (
+              <div className="flex items-center space-x-2">
+                <span className="text-sm text-gray-600">欢迎,{user.username}</span>
+                <img 
+                  src={user.avatar || '/images/avatar-placeholder.jpg'} 
+                  alt="用户头像"
+                  className="w-8 h-8 rounded-full"
+                />
+              </div>
+            ) : (
+              <button 
+                onClick={() => navigate('/login')}
+                className="text-blue-600 text-sm px-3 py-1 rounded-full border border-blue-600"
+              >
+                登录
+              </button>
+            )}
+          </div>
+          
+          {/* 搜索栏 */}
+          <div className="flex items-center bg-gray-100 rounded-full px-4 py-2">
+            <svg className="w-5 h-5 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
+            </svg>
+            <input
+              ref={searchRef}
+              type="text"
+              placeholder="搜索岗位、知识、企业..."
+              value={searchQuery}
+              onChange={(e) => setSearchQuery(e.target.value)}
+              onKeyPress={handleKeyPress}
+              className="flex-1 bg-transparent outline-none text-sm"
+            />
+            <button
+              onClick={handleSearch}
+              className="text-blue-600 text-xs px-2"
+            >
+              搜索
+            </button>
+          </div>
+        </div>
+      </header>
+
+      {/* 下拉刷新区域 */}
+      <div className="relative">
+        {/* 轮播图 */}
+        <div className="bg-white">
+          <EnhancedCarousel 
+            items={banners} 
+            autoPlayInterval={5000}
+          />
+        </div>
+
+        {/* 用户统计卡片 */}
+        {user && (
+          <UserStatsCard stats={homeData.userStats} />
+        )}
+
+        {/* 服务分类 */}
+        <div className="bg-white mt-2 p-4">
+          <div className="grid grid-cols-3 gap-4">
+            {[
+              { name: '银龄岗', icon: '💼', path: '/silver-jobs', color: 'bg-blue-500' },
+              { name: '银龄库', icon: '👥', path: '/silver-talents', color: 'bg-green-500' },
+              { name: '银龄智库', icon: '📚', path: '/silver-wisdom', color: 'bg-purple-500' },
+              { name: '老年大学', icon: '🎓', path: '/elderly-university', color: 'bg-orange-500' },
+              { name: '时间银行', icon: '⏰', path: '/time-bank', color: 'bg-red-500' },
+              { name: '政策资讯', icon: '📰', path: '/policy-news', color: 'bg-indigo-500' }
+            ].map((category, index) => (
+              <button
+                key={index}
+                onClick={() => navigate(category.path)}
+                className="flex flex-col items-center p-3 rounded-lg hover:bg-gray-50 transition-colors"
+              >
+                <div className={`${category.color} text-white w-12 h-12 rounded-full flex items-center justify-center text-xl mb-2`}>
+                  {category.icon}
+                </div>
+                <span className="text-sm text-gray-700">{category.name}</span>
+              </button>
+            ))}
+          </div>
+        </div>
+
+        {/* 推荐岗位 */}
+        <div className="bg-white mt-2 p-4">
+          <div className="flex justify-between items-center mb-4">
+            <h2 className="text-lg font-bold text-gray-900">推荐岗位</h2>
+            <button 
+              onClick={() => navigate('/silver-jobs')}
+              className="text-blue-600 text-sm"
+            >
+              查看更多 →
+            </button>
+          </div>
+          
+          <div className="space-y-3">
+            {recommendedJobs.slice(0, 3).map((item) => (
+              <div
+                key={item.id}
+                className="flex items-center p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors"
+                onClick={() => navigate(`/silver-jobs/${item.id}`)}
+              >
+                <img
+                  src={item.image}
+                  alt={item.title}
+                  className="w-12 h-12 rounded-full object-cover mr-3"
+                  loading="lazy"
+                />
+                <div className="flex-1">
+                  <h4 className="font-medium text-gray-900 line-clamp-1">{item.title}</h4>
+                  <p className="text-sm text-gray-600 line-clamp-1">{item.company}</p>
+                  <div className="flex items-center mt-1">
+                    <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
+                      {item.salary}
+                    </span>
+                    {item.tags.slice(0, 1).map((tag, i) => (
+                      <span key={i} className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded ml-2">
+                        {tag}
+                      </span>
+                    ))}
+                  </div>
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {/* 热门知识 */}
+        <div className="bg-white mt-2 p-4">
+          <div className="flex justify-between items-center mb-4">
+            <h2 className="text-lg font-bold text-gray-900">热门知识</h2>
+            <button 
+              onClick={() => navigate('/silver-wisdom')}
+              className="text-blue-600 text-sm"
+            >
+              查看更多 →
+            </button>
+          </div>
+          
+          <div className="grid grid-cols-2 gap-4">
+            {hotKnowledge.slice(0, 4).map((item) => (
+              <div
+                key={item.id}
+                className="bg-gray-50 rounded-lg p-3 cursor-pointer hover:bg-gray-100 transition-colors"
+                onClick={() => navigate(`/silver-wisdom/${item.id}`)}
+              >
+                <img
+                  src={item.coverImage}
+                  alt={item.title}
+                  className="w-full h-20 object-cover rounded mb-2"
+                  loading="lazy"
+                />
+                <h4 className="text-sm font-medium text-gray-900 line-clamp-2">{item.title}</h4>
+                <div className="flex items-center justify-between mt-1">
+                  <span className="text-xs text-gray-500">{item.category}</span>
+                  <span className="text-xs text-gray-500">{item.viewCount}阅读</span>
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {/* 时间银行活动 */}
+        {timeBankActivities.length > 0 && (
+          <div className="bg-white mt-2 p-4">
+            <div className="flex justify-between items-center mb-4">
+              <h2 className="text-lg font-bold text-gray-900">时间银行活动</h2>
+              <button 
+                onClick={() => navigate('/time-bank')}
+                className="text-blue-600 text-sm"
+              >
+                查看更多 →
+              </button>
+            </div>
+            
+            <div className="space-y-3">
+              {timeBankActivities.slice(0, 2).map((activity) => (
+                <div
+                  key={activity.id}
+                  className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
+                >
+                  <div>
+                    <h4 className="font-medium text-gray-900">{activity.organization}</h4>
+                    <p className="text-sm text-gray-600">
+                      {activity.workType === 1 ? '志愿服务' : '技能培训'} · {activity.workHours}小时
+                    </p>
+                  </div>
+                  <div className="text-right">
+                    <div className="text-lg font-bold text-blue-600">{activity.earnedPoints}</div>
+                    <div className="text-xs text-gray-500">积分奖励</div>
+                  </div>
+                </div>
+              ))}
+            </div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default NewHomePage;

+ 3 - 0
src/server/api.ts

@@ -9,6 +9,7 @@ import silverUsersRoutes from './api/silver-users/index'
 import elderlyUniversityRoutes from './api/elderly-universities/index'
 import policyNewsRoutes from './api/policy-news/index'
 import userPreferenceRoutes from './api/user-preferences/index'
+import homeRoutes from './api/home/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 
@@ -63,6 +64,7 @@ const silverUsersApiRoutes = api.route('/api/v1/silver-users', silverUsersRoutes
 const elderlyUniversityApiRoutes = api.route('/api/v1/elderly-universities', elderlyUniversityRoutes)
 const policyNewsApiRoutes = api.route('/api/v1/policy-news', policyNewsRoutes)
 const userPreferenceApiRoutes = api.route('/api/v1/user-preferences', userPreferenceRoutes)
+const homeApiRoutes = api.route('/api/v1/home', homeRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -73,5 +75,6 @@ export type SilverUsersRoutes = typeof silverUsersApiRoutes
 export type ElderlyUniversityRoutes = typeof elderlyUniversityApiRoutes
 export type PolicyNewsRoutes = typeof policyNewsApiRoutes
 export type UserPreferenceRoutes = typeof userPreferenceApiRoutes
+export type HomeRoutes = typeof homeApiRoutes
 
 export default api

+ 136 - 0
src/server/api/home/index.ts

@@ -0,0 +1,136 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { HomeService } from '@/server/modules/home/home.service';
+import { AppDataSource } from '@/server/data-source';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { AuthContext } from '@/server/types/context';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+
+// 响应Schema
+const HomeResponse = z.object({
+  banners: z.array(z.any()),
+  recommendedJobs: z.array(z.any()),
+  hotKnowledge: z.array(z.any()),
+  timeBankActivities: z.array(z.any()),
+  userStats: z.object({
+    pointBalance: z.number(),
+    timeBankHours: z.number(),
+    publishedCount: z.number(),
+    favoriteCount: z.number()
+  })
+});
+
+// 搜索请求Schema
+const SearchQuery = z.object({
+  keyword: z.string().min(1).openapi({ description: '搜索关键词', example: '教育培训' }),
+  type: z.enum(['jobs', 'knowledge', 'companies', 'all']).default('all').openapi({ 
+    description: '搜索类型', 
+    example: 'all' 
+  }),
+  limit: z.coerce.number().int().positive().max(50).default(10).openapi({
+    description: '返回数量限制',
+    example: 10
+  })
+});
+
+// 搜索响应Schema
+const SearchResponse = z.object({
+  jobs: z.array(z.any()).optional(),
+  knowledge: z.array(z.any()).optional(),
+  companies: z.array(z.any()).optional()
+});
+
+// 路由定义:获取首页数据
+const getHomeRoute = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    query: z.object({
+      limit: z.coerce.number().int().positive().max(20).optional().openapi({
+        description: '返回数量限制',
+        example: 10
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取首页数据',
+      content: { 'application/json': { schema: HomeResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由定义:搜索功能
+const searchRoute = createRoute({
+  method: 'get',
+  path: '/search',
+  middleware: [authMiddleware],
+  request: {
+    query: SearchQuery
+  },
+  responses: {
+    200: {
+      description: '搜索成功',
+      content: { 'application/json': { schema: SearchResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>();
+
+// 获取首页数据
+app.openapi(getHomeRoute, async (c) => {
+  try {
+    const { limit = 10 } = c.req.valid('query');
+    const user = c.get('user');
+    const userId = user?.id;
+
+    const homeService = new HomeService(AppDataSource);
+    const homeData = await homeService.getHomeData(userId);
+
+    // 限制返回数量
+    homeData.banners = homeData.banners.slice(0, Math.min(limit, 5));
+    homeData.recommendedJobs = homeData.recommendedJobs.slice(0, Math.min(limit, 6));
+    homeData.hotKnowledge = homeData.hotKnowledge.slice(0, Math.min(limit, 4));
+    homeData.timeBankActivities = homeData.timeBankActivities.slice(0, Math.min(limit, 3));
+
+    return c.json(homeData as any, 200);
+  } catch (error) {
+    const { message = '获取首页数据失败' } = error as Error;
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+// 搜索功能
+app.openapi(searchRoute, async (c) => {
+  try {
+    const { keyword, type, limit } = c.req.valid('query');
+    const searchService = new HomeService(AppDataSource);
+    const results = await searchService.search(keyword, type, limit);
+
+    return c.json(results as any, 200);
+  } catch (error) {
+    const { message = '搜索失败' } = error as Error;
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+export default app;

+ 320 - 0
src/server/modules/home/home.service.optimized.ts

@@ -0,0 +1,320 @@
+import { DataSource, Repository } from 'typeorm';
+import { PolicyNews } from '../silver-users/policy-news.entity';
+import { Job } from '../silver-jobs/job.entity';
+import { SilverKnowledge } from '../silver-users/silver-knowledge.entity';
+import { SilverTimeBank } from '../silver-users/silver-time-bank.entity';
+import { UserEntity } from '../users/user.entity';
+import { SilverUserProfile } from '../silver-users/silver-user-profile.entity';
+import { Favorite } from '../silver-jobs/favorite.entity';
+import { ViewRecord } from '../silver-jobs/view-record.entity';
+import { SilverKnowledgeInteraction } from '../silver-users/silver-knowledge-interaction.entity';
+import { UserPreference } from '../silver-users/user-preference.entity';
+import Redis from 'ioredis';
+
+export interface HomeData {
+  banners: PolicyNews[];
+  recommendedJobs: Job[];
+  hotKnowledge: SilverKnowledge[];
+  timeBankActivities: SilverTimeBank[];
+  userStats: {
+    pointBalance: number;
+    timeBankHours: number;
+    publishedCount: number;
+    favoriteCount: number;
+  };
+}
+
+export interface CacheConfig {
+  ttl: number;
+  keyPrefix: string;
+}
+
+export class OptimizedHomeService {
+  private redis: Redis;
+  private policyNewsRepo: Repository<PolicyNews>;
+  private jobRepo: Repository<Job>;
+  private knowledgeRepo: Repository<SilverKnowledge>;
+  private timeBankRepo: Repository<SilverTimeBank>;
+  private userRepo: Repository<UserEntity>;
+  private userProfileRepo: Repository<SilverUserProfile>;
+  private favoriteRepo: Repository<Favorite>;
+  private viewRecordRepo: Repository<ViewRecord>;
+  private knowledgeInteractionRepo: Repository<SilverKnowledgeInteraction>;
+  private userPreferenceRepo: Repository<UserPreference>;
+
+  constructor(dataSource: DataSource, redis: Redis) {
+    this.redis = redis;
+    this.policyNewsRepo = dataSource.getRepository(PolicyNews);
+    this.jobRepo = dataSource.getRepository(Job);
+    this.knowledgeRepo = dataSource.getRepository(SilverKnowledge);
+    this.timeBankRepo = dataSource.getRepository(SilverTimeBank);
+    this.userRepo = dataSource.getRepository(UserEntity);
+    this.userProfileRepo = dataSource.getRepository(SilverUserProfile);
+    this.favoriteRepo = dataSource.getRepository(Favorite);
+    this.viewRecordRepo = dataSource.getRepository(ViewRecord);
+    this.knowledgeInteractionRepo = dataSource.getRepository(SilverKnowledgeInteraction);
+    this.userPreferenceRepo = dataSource.getRepository(UserPreference);
+  }
+
+  // 缓存配置
+  private readonly cacheConfig = {
+    banners: { ttl: 300, keyPrefix: 'home:banners' }, // 5分钟
+    jobs: { ttl: 600, keyPrefix: 'home:jobs' }, // 10分钟
+    knowledge: { ttl: 900, keyPrefix: 'home:knowledge' }, // 15分钟
+    timeBank: { ttl: 1200, keyPrefix: 'home:timebank' }, // 20分钟
+    userStats: { ttl: 60, keyPrefix: 'home:userstats' } // 1分钟
+  };
+
+  /**
+   * 获取带缓存的首页数据
+   */
+  async getHomeData(userId?: number): Promise<HomeData> {
+    const cacheKey = `home:data:${userId || 'anonymous'}`;
+    const cached = await this.redis.get(cacheKey);
+    
+    if (cached) {
+      return JSON.parse(cached);
+    }
+
+    const [
+      banners,
+      recommendedJobs,
+      hotKnowledge,
+      timeBankActivities,
+      userStats
+    ] = await Promise.all([
+      this.getCachedBanners(),
+      this.getCachedRecommendedJobs(userId),
+      this.getCachedHotKnowledge(userId),
+      this.getCachedTimeBankActivities(userId),
+      this.getCachedUserStats(userId)
+    ]);
+
+    const result = {
+      banners,
+      recommendedJobs,
+      hotKnowledge,
+      timeBankActivities,
+      userStats
+    };
+
+    // 缓存结果,设置较短的TTL
+    await this.redis.setex(cacheKey, 60, JSON.stringify(result));
+    
+    return result;
+  }
+
+  /**
+   * 获取带缓存的轮播图
+   */
+  async getCachedBanners(limit: number = 5): Promise<PolicyNews[]> {
+    const cacheKey = `${this.cacheConfig.banners.keyPrefix}:${limit}`;
+    const cached = await this.redis.get(cacheKey);
+    
+    if (cached) {
+      return JSON.parse(cached);
+    }
+
+    const banners = await this.policyNewsRepo.find({
+      where: { isFeatured: 1 },
+      order: { createdAt: 'DESC' },
+      take: limit
+    });
+
+    await this.redis.setex(cacheKey, this.cacheConfig.banners.ttl, JSON.stringify(banners));
+    return banners;
+  }
+
+  /**
+   * 获取带缓存的推荐岗位(使用连接池优化)
+   */
+  async getCachedRecommendedJobs(userId?: number, limit: number = 6): Promise<Job[]> {
+    const cacheKey = `${this.cacheConfig.jobs.keyPrefix}:${userId || 'anonymous'}:${limit}`;
+    const cached = await this.redis.get(cacheKey);
+    
+    if (cached) {
+      return JSON.parse(cached);
+    }
+
+    let query = this.jobRepo
+      .createQueryBuilder('job')
+      .leftJoinAndSelect('job.company', 'company')
+      .where('job.status = :status', { status: 1 })
+      .orderBy('job.createdAt', 'DESC')
+      .limit(limit);
+
+    if (userId) {
+      const userProfile = await this.userProfileRepo.findOne({ where: { userId } });
+      const viewedJobs = await this.viewRecordRepo.find({ 
+        where: { userId }, 
+        select: ['jobId'] 
+      });
+      const viewedJobIds = viewedJobs.map(v => v.jobId);
+
+      if (viewedJobIds.length > 0) {
+        query = query.andWhere('job.id NOT IN (:...viewedJobIds)', { viewedJobIds });
+      }
+
+      if (userProfile?.personalSkills) {
+        const skills = userProfile.personalSkills.split(',').map((s: string) => s.trim());
+        query = query.orWhere('job.requirements LIKE :skill', { skill: `%${skills[0]}%` });
+      }
+    }
+
+    const jobs = await query.getMany();
+    
+    // 缓存结果
+    await this.redis.setex(cacheKey, this.cacheConfig.jobs.ttl, JSON.stringify(jobs));
+    return jobs;
+  }
+
+  /**
+   * 获取带缓存的热门知识
+   */
+  async getCachedHotKnowledge(userId?: number, limit: number = 4): Promise<SilverKnowledge[]> {
+    const cacheKey = `${this.cacheConfig.knowledge.keyPrefix}:${userId || 'anonymous'}:${limit}`;
+    const cached = await this.redis.get(cacheKey);
+    
+    if (cached) {
+      return JSON.parse(cached);
+    }
+
+    let query = this.knowledgeRepo
+      .createQueryBuilder('knowledge')
+      .leftJoinAndSelect('knowledge.category', 'category')
+      .where('knowledge.status = :status', { status: 1 })
+      .orderBy('knowledge.viewCount', 'DESC')
+      .addOrderBy('knowledge.createdAt', 'DESC')
+      .limit(limit);
+
+    const knowledge = await query.getMany();
+    
+    await this.redis.setex(cacheKey, this.cacheConfig.knowledge.ttl, JSON.stringify(knowledge));
+    return knowledge;
+  }
+
+  /**
+   * 获取带缓存的时间银行活动
+   */
+  async getCachedTimeBankActivities(userId?: number, limit: number = 3): Promise<SilverTimeBank[]> {
+    const cacheKey = `${this.cacheConfig.timeBank.keyPrefix}:${limit}`;
+    const cached = await this.redis.get(cacheKey);
+    
+    if (cached) {
+      return JSON.parse(cached);
+    }
+
+    const activities = await this.timeBankRepo.find({
+      where: { status: 1 },
+      order: { workDate: 'DESC' },
+      take: limit
+    });
+
+    await this.redis.setex(cacheKey, this.cacheConfig.timeBank.ttl, JSON.stringify(activities));
+    return activities;
+  }
+
+  /**
+   * 获取带缓存的用户统计
+   */
+  async getCachedUserStats(userId?: number) {
+    if (!userId) {
+      return {
+        pointBalance: 0,
+        timeBankHours: 0,
+        publishedCount: 0,
+        favoriteCount: 0
+      };
+    }
+
+    const cacheKey = `${this.cacheConfig.userStats.keyPrefix}:${userId}`;
+    const cached = await this.redis.get(cacheKey);
+    
+    if (cached) {
+      return JSON.parse(cached);
+    }
+
+    const [
+      userProfile,
+      timeBankStats,
+      publishedCount,
+      favoriteCount
+    ] = await Promise.all([
+      this.userProfileRepo.findOne({ where: { userId } }),
+      this.timeBankRepo
+        .createQueryBuilder('timeBank')
+        .select('SUM(timeBank.workHours)', 'totalHours')
+        .where('timeBank.userId = :userId', { userId })
+        .getRawOne(),
+      this.knowledgeRepo.count({ where: { userId } }),
+      this.favoriteRepo.count({ where: { userId } })
+    ]);
+
+    const stats = {
+      pointBalance: userProfile?.totalPoints || 0,
+      timeBankHours: parseFloat(timeBankStats?.totalHours || '0'),
+      publishedCount,
+      favoriteCount
+    };
+
+    await this.redis.setex(cacheKey, this.cacheConfig.userStats.ttl, JSON.stringify(stats));
+    return stats;
+  }
+
+  /**
+   * 清除缓存
+   */
+  async clearCache() {
+    const keys = await this.redis.keys('home:*');
+    if (keys.length > 0) {
+      await this.redis.del(...keys);
+    }
+  }
+
+  /**
+   * 搜索缓存
+   */
+  async search(keyword: string, type: 'jobs' | 'knowledge' | 'companies' | 'all', limit: number = 10) {
+    const cacheKey = `home:search:${keyword}:${type}:${limit}`;
+    const cached = await this.redis.get(cacheKey);
+    
+    if (cached) {
+      return JSON.parse(cached);
+    }
+
+    const results: {
+      jobs: Job[];
+      knowledge: SilverKnowledge[];
+      companies: any[];
+    } = {
+      jobs: [],
+      knowledge: [],
+      companies: []
+    };
+
+    if (type === 'jobs' || type === 'all') {
+      results.jobs = await this.jobRepo
+        .createQueryBuilder('job')
+        .leftJoinAndSelect('job.company', 'company')
+        .where('job.status = :status', { status: 1 })
+        .andWhere('(job.title LIKE :keyword OR job.description LIKE :keyword OR company.name LIKE :keyword)')
+        .setParameter('keyword', `%${keyword}%`)
+        .limit(limit)
+        .getMany();
+    }
+
+    if (type === 'knowledge' || type === 'all') {
+      results.knowledge = await this.knowledgeRepo
+        .createQueryBuilder('knowledge')
+        .leftJoinAndSelect('knowledge.category', 'category')
+        .where('knowledge.status = :status', { status: 1 })
+        .andWhere('(knowledge.title LIKE :keyword OR knowledge.content LIKE :keyword)')
+        .setParameter('keyword', `%${keyword}%`)
+        .limit(limit)
+        .getMany();
+    }
+
+    await this.redis.setex(cacheKey, 300, JSON.stringify(results)); // 5分钟缓存
+    return results;
+  }
+}

+ 295 - 0
src/server/modules/home/home.service.ts

@@ -0,0 +1,295 @@
+import { DataSource, Repository } from 'typeorm';
+import { PolicyNews } from '../silver-users/policy-news.entity';
+import { Job } from '../silver-jobs/job.entity';
+import { SilverKnowledge } from '../silver-users/silver-knowledge.entity';
+import { SilverTimeBank } from '../silver-users/silver-time-bank.entity';
+import { UserEntity } from '../users/user.entity';
+import { SilverUserProfile } from '../silver-users/silver-user-profile.entity';
+import { Favorite } from '../silver-jobs/favorite.entity';
+import { ViewRecord } from '../silver-jobs/view-record.entity';
+import { SilverKnowledgeInteraction } from '../silver-users/silver-knowledge-interaction.entity';
+import { UserPreference } from '../silver-users/user-preference.entity';
+
+export interface HomeData {
+  banners: PolicyNews[];
+  recommendedJobs: Job[];
+  hotKnowledge: SilverKnowledge[];
+  timeBankActivities: SilverTimeBank[];
+  userStats: {
+    pointBalance: number;
+    timeBankHours: number;
+    publishedCount: number;
+    favoriteCount: number;
+  };
+}
+
+export interface RecommendationOptions {
+  userId?: number;
+  location?: {
+    latitude: number;
+    longitude: number;
+  };
+  limit?: number;
+  category?: string;
+}
+
+export class HomeService {
+  private policyNewsRepo: Repository<PolicyNews>;
+  private jobRepo: Repository<Job>;
+  private knowledgeRepo: Repository<SilverKnowledge>;
+  private timeBankRepo: Repository<SilverTimeBank>;
+  private userRepo: Repository<UserEntity>;
+  private userProfileRepo: Repository<SilverUserProfile>;
+  private favoriteRepo: Repository<Favorite>;
+  private viewRecordRepo: Repository<ViewRecord>;
+  private knowledgeInteractionRepo: Repository<SilverKnowledgeInteraction>;
+  private userPreferenceRepo: Repository<UserPreference>;
+
+  constructor(dataSource: DataSource) {
+    this.policyNewsRepo = dataSource.getRepository(PolicyNews);
+    this.jobRepo = dataSource.getRepository(Job);
+    this.knowledgeRepo = dataSource.getRepository(SilverKnowledge);
+    this.timeBankRepo = dataSource.getRepository(SilverTimeBank);
+    this.userRepo = dataSource.getRepository(UserEntity);
+    this.userProfileRepo = dataSource.getRepository(SilverUserProfile);
+    this.favoriteRepo = dataSource.getRepository(Favorite);
+    this.viewRecordRepo = dataSource.getRepository(ViewRecord);
+    this.knowledgeInteractionRepo = dataSource.getRepository(SilverKnowledgeInteraction);
+    this.userPreferenceRepo = dataSource.getRepository(UserPreference);
+  }
+
+  /**
+   * 获取首页完整数据
+   */
+  async getHomeData(userId?: number): Promise<HomeData> {
+    const [
+      banners,
+      recommendedJobs,
+      hotKnowledge,
+      timeBankActivities,
+      userStats
+    ] = await Promise.all([
+      this.getBanners(),
+      this.getRecommendedJobs(userId),
+      this.getHotKnowledge(userId),
+      this.getTimeBankActivities(userId),
+      this.getUserStats(userId)
+    ]);
+
+    return {
+      banners,
+      recommendedJobs,
+      hotKnowledge,
+      timeBankActivities,
+      userStats
+    };
+  }
+
+  /**
+   * 获取轮播图(精选政策新闻)
+   */
+  async getBanners(limit: number = 5): Promise<PolicyNews[]> {
+    return await this.policyNewsRepo.find({
+      where: { isFeatured: 1 },
+      order: { createdAt: 'DESC' },
+      take: limit
+    });
+  }
+
+  /**
+   * 获取推荐岗位(基于用户画像和地理位置)
+   */
+  async getRecommendedJobs(userId?: number, limit: number = 6): Promise<Job[]> {
+    if (!userId) {
+      // 未登录用户:返回最新热门岗位
+      return await this.jobRepo
+        .createQueryBuilder('job')
+        .leftJoinAndSelect('job.company', 'company')
+        .where('job.status = :status', { status: 1 })
+        .orderBy('job.viewCount', 'DESC')
+        .addOrderBy('job.createdAt', 'DESC')
+        .limit(limit)
+        .getMany();
+    }
+
+    // 登录用户:基于用户偏好推荐
+    const userProfile = await this.userProfileRepo.findOne({ where: { userId } });
+    const viewedJobs = await this.viewRecordRepo.find({ 
+      where: { userId }, 
+      select: ['jobId'] 
+    });
+    const viewedJobIds = viewedJobs.map(v => v.jobId);
+
+    let query = this.jobRepo
+      .createQueryBuilder('job')
+      .leftJoinAndSelect('job.company', 'company')
+      .where('job.status = :status', { status: 1 });
+
+    if (viewedJobIds.length > 0) {
+      query = query.andWhere('job.id NOT IN (:...viewedJobIds)', { viewedJobIds });
+    }
+
+    // 基于用户专业背景匹配
+    if (userProfile?.personalSkills) {
+      const skills = userProfile.personalSkills.split(',').map((s: string) => s.trim());
+      for (let i = 0; i < skills.length; i++) {
+        query = query.orWhere(`job.requirements LIKE :skill${i}`, { [`skill${i}`]: `%${skills[i]}%` });
+      }
+    }
+
+    return await query
+      .orderBy('job.createdAt', 'DESC')
+      .limit(limit)
+      .getMany();
+  }
+
+  /**
+   * 获取热门知识(基于用户兴趣和互动)
+   */
+  async getHotKnowledge(userId?: number, limit: number = 4): Promise<SilverKnowledge[]> {
+    if (!userId) {
+      // 未登录用户:返回浏览量最高的知识
+      return await this.knowledgeRepo
+        .createQueryBuilder('knowledge')
+        .leftJoinAndSelect('knowledge.category', 'category')
+        .where('knowledge.status = :status', { status: 1 })
+        .orderBy('knowledge.viewCount', 'DESC')
+        .limit(limit)
+        .getMany();
+    }
+
+    // 登录用户:基于用户兴趣推荐
+    const userInteractions = await this.knowledgeInteractionRepo.find({ 
+      where: { userId }, 
+      select: ['knowledgeId'] 
+    });
+    const viewedKnowledgeIds = userInteractions.map(i => i.knowledgeId);
+
+    // 获取用户浏览过的知识分类,进行相似推荐
+    let categoryQuery = this.knowledgeRepo
+      .createQueryBuilder('knowledge')
+      .select('DISTINCT knowledge.categoryId', 'categoryId')
+      .where('knowledge.id IN (:...viewedKnowledgeIds)', { viewedKnowledgeIds });
+
+    const userCategories = await categoryQuery.getRawMany();
+    const preferredCategories = userCategories.map((c: any) => c.categoryId);
+
+    let query = this.knowledgeRepo
+      .createQueryBuilder('knowledge')
+      .leftJoinAndSelect('knowledge.category', 'category')
+      .where('knowledge.status = :status', { status: 1 });
+
+    if (viewedKnowledgeIds.length > 0) {
+      query = query.andWhere('knowledge.id NOT IN (:...viewedKnowledgeIds)', { viewedKnowledgeIds });
+    }
+
+    if (preferredCategories.length > 0) {
+      query = query.andWhere('knowledge.categoryId IN (:...preferredCategories)', { preferredCategories });
+    }
+
+    return await query
+      .orderBy('knowledge.viewCount', 'DESC')
+      .addOrderBy('knowledge.createdAt', 'DESC')
+      .limit(limit)
+      .getMany();
+  }
+
+  /**
+   * 获取时间银行活动
+   */
+  async getTimeBankActivities(userId?: number, limit: number = 3): Promise<SilverTimeBank[]> {
+    return await this.timeBankRepo
+      .createQueryBuilder('timeBank')
+      .where('timeBank.status = :status', { status: 1 })
+      .orderBy('timeBank.workDate', 'DESC')
+      .limit(limit)
+      .getMany();
+  }
+
+  /**
+   * 获取用户统计数据
+   */
+  async getUserStats(userId?: number) {
+    if (!userId) {
+      return {
+        pointBalance: 0,
+        timeBankHours: 0,
+        publishedCount: 0,
+        favoriteCount: 0
+      };
+    }
+
+    const [
+      userProfile,
+      timeBankStats,
+      publishedCount,
+      favoriteCount
+    ] = await Promise.all([
+      this.userProfileRepo.findOne({ where: { userId } }),
+      this.timeBankRepo
+        .createQueryBuilder('timeBank')
+        .select('SUM(timeBank.workHours)', 'totalHours')
+        .where('timeBank.userId = :userId', { userId })
+        .getRawOne(),
+      this.knowledgeRepo.count({ where: { userId } }),
+      this.favoriteRepo.count({ where: { userId } })
+    ]);
+
+    return {
+      pointBalance: userProfile?.totalPoints || 0,
+      timeBankHours: parseFloat(timeBankStats?.totalHours || '0'),
+      publishedCount,
+      favoriteCount
+    };
+  }
+
+  /**
+   * 搜索功能(全局搜索岗位、知识、企业)
+   */
+  async search(keyword: string, type: 'jobs' | 'knowledge' | 'companies' | 'all', limit: number = 10) {
+    const results: {
+      jobs: Job[];
+      knowledge: SilverKnowledge[];
+      companies: any[];
+    } = {
+      jobs: [],
+      knowledge: [],
+      companies: []
+    };
+
+    if (type === 'jobs' || type === 'all') {
+      results.jobs = await this.jobRepo
+        .createQueryBuilder('job')
+        .leftJoinAndSelect('job.company', 'company')
+        .where('job.status = :status', { status: 1 })
+        .andWhere('(job.title LIKE :keyword OR job.description LIKE :keyword OR company.name LIKE :keyword)')
+        .setParameter('keyword', `%${keyword}%`)
+        .limit(limit)
+        .getMany();
+    }
+
+    if (type === 'knowledge' || type === 'all') {
+      results.knowledge = await this.knowledgeRepo
+        .createQueryBuilder('knowledge')
+        .leftJoinAndSelect('knowledge.category', 'category')
+        .where('knowledge.status = :status', { status: 1 })
+        .andWhere('(knowledge.title LIKE :keyword OR knowledge.content LIKE :keyword)')
+        .setParameter('keyword', `%${keyword}%`)
+        .limit(limit)
+        .getMany();
+    }
+
+    if (type === 'companies' || type === 'all') {
+      const Company = this.jobRepo.manager.getRepository('Company');
+      results.companies = await Company
+        .createQueryBuilder('company')
+        .where('company.status = 1')
+        .andWhere('(company.name LIKE :keyword OR company.description LIKE :keyword)')
+        .setParameter('keyword', `%${keyword}%`)
+        .limit(limit)
+        .getMany();
+    }
+
+    return type === 'all' ? results : { [type]: results[type] };
+  }
+}

+ 15 - 0
test-home-api.http

@@ -0,0 +1,15 @@
+### 测试首页API
+GET http://localhost:3000/api/v1/home
+Authorization: Bearer your-jwt-token-here
+
+### 测试首页API带限制
+GET http://localhost:3000/api/v1/home?limit=3
+Authorization: Bearer your-jwt-token-here
+
+### 测试搜索功能
+GET http://localhost:3000/api/v1/home/search?keyword=老师&type=jobs&limit=5
+Authorization: Bearer your-jwt-token-here
+
+### 测试全局搜索
+GET http://localhost:3000/api/v1/home/search?keyword=培训&type=all&limit=10
+Authorization: Bearer your-jwt-token-here

+ 134 - 0
test/home-api.test.ts

@@ -0,0 +1,134 @@
+import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
+import { DataSource } from 'typeorm';
+import { AppDataSource } from '@/server/data-source';
+import { HomeService } from '@/server/modules/home/home.service';
+import { PolicyNews } from '@/server/modules/silver-users/policy-news.entity';
+import { Job } from '@/server/modules/silver-jobs/job.entity';
+import { SilverKnowledge } from '@/server/modules/silver-users/silver-knowledge.entity';
+import { SilverTimeBank } from '@/server/modules/silver-users/silver-time-bank.entity';
+import { UserEntity } from '@/server/modules/users/user.entity';
+import { SilverUserProfile } from '@/server/modules/silver-users/silver-user-profile.entity';
+
+describe('HomeService', () => {
+  let service: HomeService;
+  let dataSource: DataSource;
+
+  beforeAll(async () => {
+    if (!AppDataSource.isInitialized) {
+      await AppDataSource.initialize();
+    }
+    dataSource = AppDataSource;
+    service = new HomeService(dataSource);
+  });
+
+  afterAll(async () => {
+    if (AppDataSource.isInitialized) {
+      await AppDataSource.destroy();
+    }
+  });
+
+  describe('getHomeData', () => {
+    it('should return home data for anonymous users', async () => {
+      const result = await service.getHomeData();
+      
+      expect(result).toHaveProperty('banners');
+      expect(result).toHaveProperty('recommendedJobs');
+      expect(result).toHaveProperty('hotKnowledge');
+      expect(result).toHaveProperty('timeBankActivities');
+      expect(result).toHaveProperty('userStats');
+      
+      expect(Array.isArray(result.banners)).toBe(true);
+      expect(Array.isArray(result.recommendedJobs)).toBe(true);
+      expect(Array.isArray(result.hotKnowledge)).toBe(true);
+      expect(Array.isArray(result.timeBankActivities)).toBe(true);
+      expect(typeof result.userStats).toBe('object');
+    });
+
+    it('should return user-specific data when userId provided', async () => {
+      const mockUserId = 1;
+      const result = await service.getHomeData(mockUserId);
+      
+      // 验证用户统计数据存在
+      expect(result.userStats).toHaveProperty('pointBalance');
+      expect(result.userStats).toHaveProperty('timeBankHours');
+      expect(result.userStats).toHaveProperty('publishedCount');
+      expect(result.userStats).toHaveProperty('favoriteCount');
+    });
+
+    it('should handle empty results gracefully', async () => {
+      const result = await service.getHomeData();
+      
+      // 确保返回空数组而不是null
+      expect(result.banners).toBeInstanceOf(Array);
+      expect(result.recommendedJobs).toBeInstanceOf(Array);
+      expect(result.hotKnowledge).toBeInstanceOf(Array);
+      expect(result.timeBankActivities).toBeInstanceOf(Array);
+    });
+  });
+
+  describe('search', () => {
+    it('should return search results for valid keyword', async () => {
+      const keyword = '老师';
+      const result = await service.search(keyword, 'all', 5);
+      
+      expect(result).toHaveProperty('jobs');
+      expect(result).toHaveProperty('knowledge');
+      expect(result).toHaveProperty('companies');
+    });
+
+    it('should limit results based on limit parameter', async () => {
+      const keyword = '培训';
+      const limit = 3;
+      const result = await service.search(keyword, 'jobs', limit);
+      
+      expect(result.jobs.length).toBeLessThanOrEqual(limit);
+    });
+  });
+
+  describe('getBanners', () => {
+    it('should return banners with default limit', async () => {
+      const banners = await service.getBanners();
+      expect(banners.length).toBeLessThanOrEqual(5);
+    });
+
+    it('should return banners with custom limit', async () => {
+      const limit = 3;
+      const banners = await service.getBanners(limit);
+      expect(banners.length).toBeLessThanOrEqual(limit);
+    });
+  });
+
+  describe('getRecommendedJobs', () => {
+    it('should return jobs for anonymous users', async () => {
+      const jobs = await service.getRecommendedJobs(undefined, 5);
+      expect(jobs).toBeInstanceOf(Array);
+      expect(jobs.length).toBeLessThanOrEqual(5);
+    });
+
+    it('should exclude viewed jobs for logged-in users', async () => {
+      const userId = 1;
+      const jobs = await service.getRecommendedJobs(userId, 5);
+      expect(jobs).toBeInstanceOf(Array);
+    });
+  });
+
+  describe('performance', () => {
+    it('should complete data retrieval within 3 seconds', async () => {
+      const startTime = Date.now();
+      const result = await service.getHomeData();
+      const endTime = Date.now();
+      
+      expect(endTime - startTime).toBeLessThan(3000);
+    });
+
+    it('should handle concurrent requests', async () => {
+      const promises = Array.from({ length: 10 }, () => service.getHomeData());
+      const results = await Promise.all(promises);
+      
+      expect(results).toHaveLength(10);
+      results.forEach(result => {
+        expect(result).toHaveProperty('banners');
+      });
+    });
+  });
+});