Files
zzTaiko/app/components/ChallengeDataLoader.tsx
2025-12-23 14:13:11 +08:00

230 lines
9.5 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { MonthlyChallengeData, DataLoadState } from '@/app/types';
import { FileText, AlertCircle, RefreshCw, Calendar } from 'lucide-react';
interface ChallengeDataLoaderProps {
onDataLoaded?: (data: MonthlyChallengeData[]) => void;
darkMode?: boolean;
}
export default function ChallengeDataLoader({
onDataLoaded,
darkMode = false
}: ChallengeDataLoaderProps) {
const [loadState, setLoadState] = useState<DataLoadState>({
isLoading: true,
error: null,
data: []
});
// 加载数据
const loadChallengeData = async () => {
setLoadState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// 获取challenge-data目录下的所有JSON文件
const response = await fetch('/api/challenge-data');
if (!response.ok) {
throw new Error(`加载数据失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
// 转换数据格式
const monthlyData: MonthlyChallengeData[] = data.files.map((file: any) => ({
yearMonth: file.yearMonth,
fileName: file.fileName,
data: file.data
}));
setLoadState({
isLoading: false,
error: null,
data: monthlyData
});
// 通知父组件数据已加载
if (onDataLoaded) {
onDataLoaded(monthlyData);
}
} catch (error) {
console.error('加载课题数据失败:', error);
setLoadState({
isLoading: false,
error: error instanceof Error ? error.message : '未知错误',
data: []
});
}
};
// 组件挂载时加载数据
useEffect(() => {
loadChallengeData();
}, []);
// 重新加载数据
const handleReload = () => {
loadChallengeData();
};
// 格式化日期
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// 渲染加载状态
if (loadState.isLoading) {
return (
<div className={`flex flex-col items-center justify-center py-12 ${darkMode ? 'text-gray-400' : 'text-gray-600'}`}>
<RefreshCw className="w-8 h-8 animate-spin mb-4" />
<p>...</p>
</div>
);
}
// 渲染错误状态
if (loadState.error) {
return (
<div className={`rounded-xl p-6 ${darkMode ? 'bg-red-900/20 border border-red-800' : 'bg-red-50 border border-red-200'}`}>
<div className="flex items-start gap-3">
<AlertCircle className={`w-6 h-6 mt-0.5 ${darkMode ? 'text-red-400' : 'text-red-500'}`} />
<div className="flex-1">
<h3 className={`font-semibold mb-2 ${darkMode ? 'text-red-300' : 'text-red-800'}`}>
</h3>
<p className={`text-sm mb-4 ${darkMode ? 'text-red-400' : 'text-red-600'}`}>
{loadState.error}
</p>
<button
onClick={handleReload}
className={`px-4 py-2 rounded-lg flex items-center gap-2 ${darkMode ? 'bg-red-800 hover:bg-red-700 text-white' : 'bg-red-100 hover:bg-red-200 text-red-800'}`}
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
}
// 渲染空状态
if (loadState.data.length === 0) {
return (
<div className={`text-center py-12 ${darkMode ? 'text-gray-400' : 'text-gray-600'}`}>
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
<h3 className="text-lg font-medium mb-2"></h3>
<p className="mb-6">challenge-data目录中没有找到JSON文件</p>
<button
onClick={handleReload}
className={`px-4 py-2 rounded-lg flex items-center gap-2 mx-auto ${darkMode ? 'bg-gray-700 hover:bg-gray-600 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-800'}`}
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
);
}
// 渲染数据统计
return (
<div className="space-y-6">
{/* 数据统计 */}
<div className={`grid grid-cols-1 md:grid-cols-3 gap-4 ${darkMode ? 'text-gray-300' : 'text-gray-700'}`}>
<div className={`p-4 rounded-xl ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}>
<div className="flex items-center gap-3">
<Calendar className="w-5 h-5 text-blue-500" />
<div>
<p className="text-sm opacity-75"></p>
<p className="text-2xl font-bold">{loadState.data.length}</p>
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}>
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-green-500" />
<div>
<p className="text-sm opacity-75"></p>
<p className="text-2xl font-bold">
{loadState.data.reduce((sum, month) => sum + month.data.totalChallenges, 0)}
</p>
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}>
<div className="flex items-center gap-3">
<RefreshCw className="w-5 h-5 text-purple-500" />
<div>
<p className="text-sm opacity-75"></p>
<p className="text-sm font-medium">
{formatDate(loadState.data[0].data.generatedAt)}
</p>
</div>
</div>
</div>
</div>
{/* 月份列表 */}
<div>
<h3 className={`text-lg font-semibold mb-4 ${darkMode ? 'text-gray-300' : 'text-gray-800'}`}>
({loadState.data.length})
</h3>
<div className="space-y-3">
{loadState.data.map((monthData) => (
<div
key={monthData.yearMonth}
className={`p-4 rounded-xl border ${darkMode ? 'border-gray-700 bg-gray-800' : 'border-gray-200 bg-white'}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${darkMode ? 'bg-blue-900' : 'bg-blue-100'}`}>
<Calendar className={`w-5 h-5 ${darkMode ? 'text-blue-300' : 'text-blue-600'}`} />
</div>
<div>
<h4 className={`font-semibold ${darkMode ? 'text-white' : 'text-gray-900'}`}>
{monthData.yearMonth}
</h4>
<p className={`text-sm ${darkMode ? 'text-gray-400' : 'text-gray-600'}`}>
{monthData.fileName} {monthData.data.totalChallenges}
</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm ${darkMode ? 'text-gray-400' : 'text-gray-600'}`}>
</p>
<p className={`text-sm font-medium ${darkMode ? 'text-gray-300' : 'text-gray-800'}`}>
{formatDate(monthData.data.generatedAt)}
</p>
</div>
</div>
</div>
))}
</div>
</div>
{/* 操作按钮 */}
<div className="flex justify-end">
<button
onClick={handleReload}
className={`px-4 py-2 rounded-lg flex items-center gap-2 ${darkMode ? 'bg-gray-700 hover:bg-gray-600 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-800'}`}
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
</div>
);
}