import { useState, useEffect, useRef } from 'react'; import { motion } from 'framer-motion'; import { Upload, FileSpreadsheet, Database, History, CheckCircle, AlertCircle, X, Download, Calendar, Settings } from 'lucide-react'; import { adminApi, createSyncTaskWebSocket } from '@/services/adminApi'; interface ImportTask { id: string; type: 'stock' | 'sector' | 'trade' | 'kline'; name: string; fileName: string; status: 'pending' | 'processing' | 'completed' | 'error'; progress: number; totalRecords: number; importedRecords: number; errorMessage?: string; createdAt: string; } const importTemplates = [ { type: 'stock', name: '股票基础数据', format: 'CSV/Excel', fields: ['code', 'name', 'industry', 'market_cap'] }, { type: 'sector', name: '版块数据', format: 'CSV/Excel', fields: ['code', 'name', 'parent_code'] }, { type: 'trade', name: '交易数据', format: 'CSV/Excel', fields: ['code', 'date', 'open', 'high', 'low', 'close', 'volume'] }, { type: 'kline', name: 'K线数据', format: 'CSV/Excel', fields: ['code', 'date', 'open', 'high', 'low', 'close', 'volume'] }, ]; export default function DataImport() { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [dragActive, setDragActive] = useState(false); const [selectedType, setSelectedType] = useState('stock'); const [showTemplateModal, setShowTemplateModal] = useState(false); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); const wsRef = useRef(null); useEffect(() => { fetchTasks(); }, []); useEffect(() => { return () => { if (wsRef.current) { wsRef.current.close(); } }; }, []); const fetchTasks = async () => { setLoading(true); try { const data = await adminApi.getImportTasks(); setTasks(data.map(t => ({ id: t.id, type: 'stock' as const, name: t.name, fileName: t.fileName, status: t.status as ImportTask['status'], progress: t.progress, totalRecords: t.totalRecords, importedRecords: t.importedRecords, createdAt: t.createdAt, }))); } catch (error) { console.error('Failed to fetch tasks:', error); } finally { setLoading(false); } }; const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === 'dragenter' || e.type === 'dragover') { setDragActive(true); } else if (e.type === 'dragleave') { setDragActive(false); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleFile(e.dataTransfer.files[0]); } }; const handleFile = async (file: File) => { setUploading(true); try { const result = await adminApi.uploadImportFile(file, selectedType); // 添加新任务到列表 const newTask: ImportTask = { id: result.taskId, type: selectedType as ImportTask['type'], name: importTemplates.find(t => t.type === selectedType)?.name || '数据导入', fileName: file.name, status: 'pending', progress: 0, totalRecords: 0, importedRecords: 0, createdAt: new Date().toLocaleString('zh-CN'), }; setTasks(prev => [newTask, ...prev]); // 连接 WebSocket 获取进度 connectWebSocket(result.taskId, newTask.id); } catch (error: any) { alert('上传失败: ' + error.message); } finally { setUploading(false); } }; const connectWebSocket = (serverTaskId: string, localTaskId: string) => { if (wsRef.current) { wsRef.current.close(); } const ws = createSyncTaskWebSocket(serverTaskId); ws.onmessage = (event) => { const data = JSON.parse(event.data); setTasks(prev => prev.map(t => t.id === localTaskId ? { ...t, status: data.status, progress: data.progress, totalRecords: data.totalRecords || t.totalRecords, importedRecords: data.processedRecords || t.importedRecords, errorMessage: data.error, } : t )); if (data.status === 'completed' || data.status === 'failed') { ws.close(); } }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; wsRef.current = ws; }; const handleDeleteTask = (taskId: string) => { setTasks(prev => prev.filter(t => t.id !== taskId)); }; const getStatusIcon = (status: ImportTask['status']) => { switch (status) { case 'completed': return ; case 'error': return ; case 'processing': return
; default: return
; } }; const downloadTemplate = (type: string) => { const template = importTemplates.find(t => t.type === type); if (!template) return; const headers = template.fields.join(','); const sample = template.fields.map(() => '示例数据').join(','); const content = `${headers}\n${sample}`; const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `${type}_template.csv`; link.click(); }; return (
{/* Header */}

数据导入

批量导入股票、交易历史等数据

{/* Upload Area */} e.target.files?.[0] && handleFile(e.target.files[0])} className="hidden" />
{uploading ? (
) : ( )}

{uploading ? '上传中...' : '拖拽文件到此处上传'}

支持 CSV、Excel 格式文件

{/* Data Type Selector */}
{importTemplates.map(template => ( ))}
{/* Import Tasks */}

导入记录

{loading ? (
加载中...
) : tasks.length === 0 ? (
暂无导入记录
) : ( tasks.map((task) => (
{getStatusIcon(task.status)}

{task.name}

{task.fileName}

{task.createdAt} 类型: {task.type} {task.status !== 'pending' && task.totalRecords > 0 && ( 记录: {task.importedRecords.toLocaleString()} / {task.totalRecords.toLocaleString()} )}
{task.errorMessage && (

{task.errorMessage}

)}
{task.status === 'processing' && (
导入进度 {task.progress.toFixed(0)}%
)} {task.status === 'completed' && (
导入成功
)}
)) )}
{/* Import Guide */}

导入说明

文件格式

支持 CSV (.csv) 和 Excel (.xlsx, .xls) 格式,文件大小不超过 100MB

数据验证

系统会自动验证数据格式,错误数据将被跳过并生成报告

数据去重

已存在的记录将被更新,新记录将被插入,不会重复导入

自动计算

导入交易数据后,系统会自动重新计算动量指标和排名

{/* Template Modal */} {showTemplateModal && ( setShowTemplateModal(false)} > e.stopPropagation()} >

下载导入模板

{importTemplates.map(template => (

{template.name}

字段: {template.fields.join(', ')}

))}
)}
); }