|
|
@@ -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>
|
|
|
);
|
|
|
};
|
|
|
|