import { useRef, useState, useEffect } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { Settings, Tag, Upload, Code, RefreshCw, CheckCircle2, Loader2, Pencil, ChevronDown, ChevronUp, AlertTriangle } from 'lucide-react' import { api, type ExtDataConfig } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { SettingsModal } from '@/components/data/SettingsModal' import { ExtDataPullPanel } from './ExtDataPullPanel' import { ExtDataApiPanel } from './ExtDataApiPanel' export function ExtDataStatCard({ config, onDelete, deleting, onEdit }: { config: ExtDataConfig onDelete: () => void deleting: boolean onEdit?: () => void }) { const qc = useQueryClient() const fileRef = useRef(null) const [uploading, setUploading] = useState(false) const [uploadResult, setUploadResult] = useState<{ rows: number; date: string } | null>(null) const [showDelete, setShowDelete] = useState(false) const [settingsOpen, setSettingsOpen] = useState(false) const [dragOver, setDragOver] = useState(false) const [ingestTab, setIngestTab] = useState<'file' | 'api' | 'pull'>('file') const [copied, setCopied] = useState(false) const [fieldsExpanded, setFieldsExpanded] = useState(false) const fieldsRef = useRef(null) const [fieldsOverflow, setFieldsOverflow] = useState(false) useEffect(() => { const el = fieldsRef.current if (!el) return const prev = el.style.maxHeight el.style.maxHeight = 'none' const full = el.scrollHeight el.style.maxHeight = prev setFieldsOverflow(full > 68) }, [config.fields]) const upload = useMutation({ mutationFn: ({ id, file }: { id: string; file: File }) => api.extDataUpload(id, file), onMutate: () => { setUploading(true); setUploadResult(null) }, onSuccess: (data) => { setUploadResult({ rows: data.rows, date: data.date }) qc.invalidateQueries({ queryKey: QK.extData }) qc.invalidateQueries({ queryKey: QK.dataStatus }) setTimeout(() => { setUploadResult(null) setSettingsOpen(false) }, 1500) }, onSettled: () => setUploading(false), }) const doUpload = (file: File) => upload.mutate({ id: config.id, file }) const handleFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) doUpload(file) e.target.value = '' } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setDragOver(false) const file = e.dataTransfer.files[0] if (file) doUpload(file) } return (

{config.label}

{config.mode === 'snapshot' ? '快照' : '时序'}
{config.description || `扩展数据 · ${config.mode === 'snapshot' ? '与标的 JOIN' : '与日K JOIN'}`}
{config.fields .filter(f => f.name !== 'symbol' && f.name !== 'code') .map((f) => ( {f.label || f.name} )) }
{fieldsOverflow && !fieldsExpanded && (
setFieldsExpanded(true)} >
)} {fieldsExpanded && fieldsOverflow && ( )}
标识 {config.id}
最新 {config.latest_sync_date ?? '—'}
{settingsOpen && ( setSettingsOpen(false)}>
{onEdit && ( )}
{ingestTab === 'file' ? ( <>
fileRef.current?.click()} onDragOver={e => { e.preventDefault(); setDragOver(true) }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} className={`relative cursor-pointer rounded-lg border-2 border-dashed transition-colors py-5 flex flex-col items-center justify-center gap-1.5 ${ dragOver ? 'border-accent bg-accent/10' : uploading ? 'border-border/50 bg-elevated/30 pointer-events-none' : 'border-border/40 hover:border-accent/50 hover:bg-accent/[0.03]' }`} > {uploading ? ( <> 上传中… ) : ( <> 拖拽文件到此处上传 支持 CSV / Excel,需包含 symbol 列 )}
) : ingestTab === 'api' ? ( ) : ( qc.invalidateQueries({ queryKey: QK.extData })} /> )} {uploadResult && (
上传成功 · {uploadResult.rows} 行 · {uploadResult.date}
)}
)}
{/* 删除二次确认弹窗 */} {showDelete && (
!deleting && setShowDelete(false)} />

确认删除「{config.label}」?

永久删除该扩展数据配置及其全部数据,包括字段、已上传/拉取的所有记录。

操作不可恢复。

)}
) }