You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

404 lines
15 KiB

import { useState, useRef } from 'react';
import { motion } from 'framer-motion';
import {
Upload,
FileSpreadsheet,
Database,
History,
CheckCircle,
AlertCircle,
X,
Download,
Calendar,
Settings
} from 'lucide-react';
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<ImportTask[]>([]);
const [dragActive, setDragActive] = useState(false);
const [selectedType, setSelectedType] = useState<string>('stock');
const [showTemplateModal, setShowTemplateModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
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 = (file: File) => {
const newTask: ImportTask = {
id: Date.now().toString(),
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]);
// 模拟开始导入
setTimeout(() => {
startImport(newTask.id);
}, 500);
};
const startImport = async (taskId: string) => {
setTasks(prev => prev.map(t =>
t.id === taskId ? { ...t, status: 'processing' } : t
));
// 模拟导入进度
const totalSteps = 10;
for (let i = 1; i <= totalSteps; i++) {
await new Promise(resolve => setTimeout(resolve, 500));
setTasks(prev => prev.map(t =>
t.id === taskId ? {
...t,
progress: (i / totalSteps) * 100,
totalRecords: 10000,
importedRecords: Math.round((i / totalSteps) * 10000),
} : t
));
}
// 模拟随机成功或失败
const success = Math.random() > 0.2;
setTasks(prev => prev.map(t =>
t.id === taskId ? {
...t,
status: success ? 'completed' : 'error',
progress: success ? 100 : 60,
errorMessage: success ? undefined : '部分数据格式错误,请检查文件格式',
} : t
));
};
const handleDeleteTask = (taskId: string) => {
setTasks(prev => prev.filter(t => t.id !== taskId));
};
const getStatusIcon = (status: ImportTask['status']) => {
switch (status) {
case 'completed':
return <CheckCircle className="w-5 h-5 text-green-400" />;
case 'error':
return <AlertCircle className="w-5 h-5 text-red-400" />;
case 'processing':
return <div className="w-5 h-5 border-2 border-[#ff6b35] border-t-transparent rounded-full animate-spin" />;
default:
return <div className="w-5 h-5 rounded-full border-2 border-[#666]" />;
}
};
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 (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-white"></h1>
<p className="text-[#b0b0b0] mt-1"></p>
</div>
<button
onClick={() => setShowTemplateModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors"
>
<Download className="w-4 h-4" />
</button>
</div>
{/* Upload Area */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`bg-[#111111] border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
dragActive ? 'border-[#ff6b35] bg-[#ff6b35]/5' : 'border-[#2a2a2a]'
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<input
ref={fileInputRef}
type="file"
accept=".csv,.xlsx,.xls"
onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])}
className="hidden"
/>
<div className="w-16 h-16 bg-[#ff6b35]/20 rounded-full flex items-center justify-center mx-auto mb-4">
<Upload className="w-8 h-8 text-[#ff6b35]" />
</div>
<p className="text-white text-lg mb-2"></p>
<p className="text-[#666] text-sm mb-4"> CSVExcel </p>
{/* Data Type Selector */}
<div className="flex flex-wrap justify-center gap-2 mb-4">
{importTemplates.map(template => (
<button
key={template.type}
onClick={() => setSelectedType(template.type)}
className={`px-4 py-2 rounded-lg text-sm transition-colors ${
selectedType === template.type
? 'bg-[#ff6b35] text-white'
: 'bg-[#2a2a2a] text-[#b0b0b0] hover:bg-[#333]'
}`}
>
{template.name}
</button>
))}
</div>
<button
onClick={() => fileInputRef.current?.click()}
className="px-6 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors"
>
</button>
</motion.div>
{/* Import Tasks */}
{tasks.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-[#111111] border border-[#2a2a2a] rounded-xl overflow-hidden"
>
<div className="px-5 py-4 border-b border-[#2a2a2a] flex items-center justify-between">
<div className="flex items-center gap-3">
<History className="w-5 h-5 text-[#ff6b35]" />
<h2 className="text-white font-semibold"></h2>
</div>
<button
onClick={() => setTasks([])}
className="text-sm text-[#666] hover:text-white transition-colors"
>
</button>
</div>
<div className="divide-y divide-[#2a2a2a]">
{tasks.map((task) => (
<div key={task.id} className="p-5 hover:bg-[#1a1a1a] transition-colors">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
{getStatusIcon(task.status)}
<div>
<h3 className="text-white font-medium">{task.name}</h3>
<p className="text-sm text-[#666] mt-1">{task.fileName}</p>
<div className="flex items-center gap-4 mt-2 text-xs text-[#666]">
<span>{task.createdAt}</span>
<span>: {task.type}</span>
{task.status !== 'pending' && (
<span>
: {task.importedRecords.toLocaleString()} / {task.totalRecords.toLocaleString()}
</span>
)}
</div>
{task.errorMessage && (
<p className="text-sm text-red-400 mt-2">{task.errorMessage}</p>
)}
</div>
</div>
<button
onClick={() => handleDeleteTask(task.id)}
className="p-2 hover:bg-[#2a2a2a] rounded-lg transition-colors"
>
<X className="w-4 h-4 text-[#666]" />
</button>
</div>
{task.status === 'processing' && (
<div className="mt-4">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-[#b0b0b0]"></span>
<span className="text-[#ff6b35]">{task.progress.toFixed(0)}%</span>
</div>
<div className="h-2 bg-[#2a2a2a] rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-[#ff6b35] to-[#ff9f43]"
initial={{ width: 0 }}
animate={{ width: `${task.progress}%` }}
transition={{ duration: 0.3 }}
/>
</div>
</div>
)}
{task.status === 'completed' && (
<div className="mt-4 flex items-center gap-2 text-green-400 text-sm">
<CheckCircle className="w-4 h-4" />
</div>
)}
</div>
))}
</div>
</motion.div>
)}
{/* Import Guide */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
>
<h2 className="text-white font-semibold mb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-blue-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<FileSpreadsheet className="w-4 h-4 text-blue-400" />
</div>
<div>
<h3 className="text-white font-medium"></h3>
<p className="text-sm text-[#666] mt-1">
CSV (.csv) Excel (.xlsx, .xls) 100MB
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-green-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<Database className="w-4 h-4 text-green-400" />
</div>
<div>
<h3 className="text-white font-medium"></h3>
<p className="text-sm text-[#666] mt-1">
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-purple-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<Calendar className="w-4 h-4 text-purple-400" />
</div>
<div>
<h3 className="text-white font-medium"></h3>
<p className="text-sm text-[#666] mt-1">
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-orange-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<Settings className="w-4 h-4 text-orange-400" />
</div>
<div>
<h3 className="text-white font-medium"></h3>
<p className="text-sm text-[#666] mt-1">
</p>
</div>
</div>
</div>
</motion.div>
{/* Template Modal */}
{showTemplateModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
onClick={() => setShowTemplateModal(false)}
>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl w-full max-w-lg"
onClick={e => e.stopPropagation()}
>
<div className="p-5 border-b border-[#2a2a2a] flex items-center justify-between">
<h2 className="text-white font-semibold"></h2>
<button
onClick={() => setShowTemplateModal(false)}
className="p-2 hover:bg-[#2a2a2a] rounded-lg transition-colors"
>
<X className="w-5 h-5 text-[#666]" />
</button>
</div>
<div className="p-5 space-y-3">
{importTemplates.map(template => (
<div
key={template.type}
className="flex items-center justify-between p-4 bg-[#0a0a0a] rounded-lg"
>
<div>
<h3 className="text-white font-medium">{template.name}</h3>
<p className="text-sm text-[#666] mt-1">
: {template.fields.join(', ')}
</p>
</div>
<button
onClick={() => downloadTemplate(template.type)}
className="px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors"
>
</button>
</div>
))}
</div>
</motion.div>
</motion.div>
)}
</div>
);
}