|
|
|
|
|
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">支持 CSV、Excel 格式文件</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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|