| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- import { useState, useEffect, useRef, memo, useCallback } from 'react'
- import api, { DatasetInfo, KnowledgeBaseItem, DatasetDownloadTaskResponse } from '../api/client'
- import { Database, Upload, Loader2, FolderOpen, CheckCircle, XCircle } from 'lucide-react'
- const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: {
- d: DatasetInfo
- onPreview: (id: string) => void
- onDelete: (id: string) => void
- }) {
- return (
- <tr style={{
- borderBottom: '1px solid #f1f5f9',
- transition: 'background 0.15s ease',
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
- >
- <td style={{ padding: '12px 12px', fontWeight: 500, fontSize: 13 }}>{d.name}</td>
- <td style={{ padding: '12px 12px', fontSize: 13, color: '#64748b' }}>
- <span style={{
- display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
- background: '#f1f5f9', color: '#64748b',
- }}>
- {d.format}
- </span>
- </td>
- <td style={{ padding: '12px 12px', fontSize: 13 }}>{d.record_count}</td>
- <td style={{ padding: '12px 12px', fontSize: 13, color: '#94a3b8' }}>{d.created_at}</td>
- <td style={{ padding: '12px 12px' }}>
- <button onClick={() => onPreview(d.id)} style={{
- marginRight: 8, padding: '4px 12px', color: '#0ea5e9',
- border: '1px solid #0ea5e9', borderRadius: 6, background: 'transparent',
- cursor: 'pointer', fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#0ea5e9'; e.currentTarget.style.color = '#fff' }}
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#0ea5e9' }}
- >预览</button>
- <button onClick={() => onDelete(d.id)} style={{
- padding: '4px 12px', color: '#f43f5e', border: '1px solid #f43f5e',
- borderRadius: 6, background: 'transparent', cursor: 'pointer',
- fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#f43f5e'; e.currentTarget.style.color = '#fff' }}
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#f43f5e' }}
- >删除</button>
- </td>
- </tr>
- )
- })
- export function Datasets() {
- const [datasets, setDatasets] = useState<DatasetInfo[]>([])
- const [uploading, setUploading] = useState(false)
- const [downloading, setDownloading] = useState(false)
- const [loading, setLoading] = useState(false)
- const [previewData, setPreviewData] = useState<{ columns: string[]; rows: { row_index: number; data: Record<string, unknown> }[] } | null>(null)
- const inputRef = useRef<HTMLInputElement>(null)
- // Sample center modal state
- const [showSampleCenter, setShowSampleCenter] = useState(false)
- const [kbList, setKbList] = useState<KnowledgeBaseItem[]>([])
- const [kbLoading, setKbLoading] = useState(false)
- const [kbImporting, setKbImporting] = useState<string | null>(null)
- const [kbStatus, setKbStatus] = useState('')
- const [kbPage, setKbPage] = useState(1)
- const [kbTotal, setKbTotal] = useState(0)
- // Active downloads tracking
- const [activeDownloads, setActiveDownloads] = useState<Map<string, DatasetDownloadTaskResponse>>(new Map())
- const downloadPollIntervals = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
- useEffect(() => {
- fetchDatasets()
- }, [])
- // Download form
- const [dlDatasetId, setDlDatasetId] = useState('')
- const [dlUseModelscope, setDlUseModelscope] = useState(false)
- const [dlStatus, setDlStatus] = useState('')
- const fetchDatasets = () => {
- setLoading(true)
- api.datasets.list()
- .then(setDatasets)
- .catch(() => setDatasets([]))
- .finally(() => setLoading(false))
- }
- const handleFileUpload = async (file: File) => {
- setUploading(true)
- try {
- await api.datasets.upload(file)
- fetchDatasets()
- } catch (err) {
- console.error('Upload failed:', err)
- } finally {
- setUploading(false)
- }
- }
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const file = e.target.files?.[0]
- if (file) handleFileUpload(file)
- }
- const handleDownload = async () => {
- if (!dlDatasetId.trim()) return
- setDownloading(true)
- setDlStatus('正在提交下载任务...')
- try {
- const res = await api.datasets.download(dlDatasetId, dlUseModelscope)
- setDlStatus(`下载任务已提交: ${res.dataset_id}`)
- setActiveDownloads(prev => new Map(prev).set(res.task_id, res))
- startDatasetDownloadPolling(res.task_id)
- } catch (err) {
- setDlStatus(`下载失败: ${err instanceof Error ? err.message : '未知错误'}`)
- } finally {
- setDownloading(false)
- }
- }
- const startDatasetDownloadPolling = (taskId: string) => {
- const interval = setInterval(() => {
- api.datasets.downloadStatus(taskId)
- .then(res => {
- setActiveDownloads(prev => {
- const next = new Map(prev)
- next.set(taskId, res)
- return next
- })
- if (res.status === 'completed') {
- clearInterval(interval)
- downloadPollIntervals.current.delete(taskId)
- fetchDatasets()
- setDlStatus(`${res.dataset_id} 下载完成 (${res.record_count} 条记录)`)
- } else if (res.status === 'failed') {
- clearInterval(interval)
- downloadPollIntervals.current.delete(taskId)
- setDlStatus(`${res.dataset_id} 下载失败: ${res.error}`)
- }
- })
- .catch(() => {})
- }, 3000)
- downloadPollIntervals.current.set(taskId, interval)
- }
- const handleCancelDatasetDownload = (taskId: string) => {
- api.datasets.cancelDownload(taskId)
- setActiveDownloads(prev => {
- const next = new Map(prev)
- next.delete(taskId)
- return next
- })
- const interval = downloadPollIntervals.current.get(taskId)
- if (interval) {
- clearInterval(interval)
- downloadPollIntervals.current.delete(taskId)
- }
- }
- useEffect(() => {
- return () => {
- downloadPollIntervals.current.forEach(interval => clearInterval(interval))
- }
- }, [])
- const handlePreview = (id: string) => {
- api.datasets.preview(id, 10)
- .then(res => setPreviewData({ columns: res.columns, rows: res.preview_rows }))
- .catch(() => setPreviewData(null))
- }
- const handleDelete = async (id: string) => {
- if (!confirm('确定删除此数据集?')) return
- try {
- await api.datasets.delete(id)
- fetchDatasets()
- setPreviewData(null)
- } catch (err) {
- console.error('Delete failed:', err)
- }
- }
- const fetchKnowledgeBases = useCallback((page = 1) => {
- setKbLoading(true)
- api.sampleCenter.listKnowledgeBases(page, 20)
- .then(res => {
- setKbList(res.items)
- setKbTotal(res.total)
- setKbPage(res.page)
- })
- .catch(err => setKbStatus(`获取知识库列表失败: ${err.message}`))
- .finally(() => setKbLoading(false))
- }, [])
- const handleImportFromKB = async (kb: KnowledgeBaseItem) => {
- setKbImporting(kb.id)
- setKbStatus(`正在导入 "${kb.name}" ...`)
- try {
- await api.sampleCenter.importFromKnowledgeBase(kb.id, kb.name)
- setKbStatus(`"${kb.name}" 导入请求已提交,可在样本中心查看入库进度`)
- // 刷新本地数据集列表
- fetchDatasets()
- } catch (err: unknown) {
- const msg = err instanceof Error ? err.message : '导入失败'
- setKbStatus(`导入失败: ${msg}`)
- } finally {
- setKbImporting(null)
- }
- }
- return (
- <div>
- <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
- <p style={{ color: '#64748b', fontSize: 13, margin: '4px 0 16px' }}>上传和管理训练数据集</p>
- {/* Upload area */}
- <div
- onClick={() => inputRef.current?.click()}
- style={{
- marginTop: 16, border: '2px dashed #cbd5e1', borderRadius: 12,
- padding: 40, textAlign: 'center', color: '#94a3b8', cursor: 'pointer',
- opacity: uploading ? 0.6 : 1, background: '#fff',
- transition: 'all 0.2s ease',
- }}
- onMouseEnter={e => { e.currentTarget.style.borderColor = '#14b8a6'; e.currentTarget.style.background = '#f0fdfa' }}
- onMouseLeave={e => { e.currentTarget.style.borderColor = '#cbd5e1'; e.currentTarget.style.background = '#fff' }}
- >
- <div style={{ marginBottom: 8 }}>
- {uploading ? (
- <Loader2 size={32} color="#14b8a6" strokeWidth={1.5} style={{ animation: 'lucide-spin 1s linear infinite' }} />
- ) : (
- <Upload size={32} color="#14b8a6" strokeWidth={1.5} />
- )}
- </div>
- <div style={{ fontSize: 14, fontWeight: 500 }}>
- {uploading ? '上传中...' : '拖拽文件到此处或点击上传'}
- </div>
- <div style={{ fontSize: 12, color: '#cbd5e1', marginTop: 4 }}>
- 支持 JSONL / CSV / Parquet / JSON 格式
- </div>
- <input
- ref={inputRef}
- type="file"
- accept=".jsonl,.csv,.parquet,.json"
- style={{ display: 'none' }}
- onChange={handleInputChange}
- />
- </div>
- {/* Download section */}
- <div style={{
- marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>从平台下载</h2>
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
- <input
- type="text"
- placeholder="数据集 ID (如 glue, MRPC, stanfordnlp/imdb)"
- value={dlDatasetId}
- onChange={e => setDlDatasetId(e.target.value)}
- style={{
- padding: '10px 14px', flex: 1, maxWidth: 400, borderRadius: 8,
- border: '1px solid #cbd5e1', fontSize: 14, outline: 'none',
- transition: 'border-color 0.2s',
- }}
- onFocus={e => { e.currentTarget.style.borderColor = '#14b8a6' }}
- onBlur={e => { e.currentTarget.style.borderColor = '#cbd5e1' }}
- />
- <label style={{ fontSize: 13, color: '#64748b', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
- <input type="checkbox" checked={dlUseModelscope} onChange={e => setDlUseModelscope(e.target.checked)} />
- {' '}ModelScope
- </label>
- <button
- onClick={handleDownload}
- disabled={downloading}
- style={{
- padding: '10px 20px', borderRadius: 8, border: 'none',
- background: '#14b8a6', color: '#fff', cursor: 'pointer',
- opacity: downloading ? 0.6 : 1, fontSize: 14, fontWeight: 600,
- }}
- >
- {downloading ? '下载中...' : '下载数据集'}
- </button>
- </div>
- {dlStatus && <p style={{
- marginTop: 10, padding: '8px 12px', borderRadius: 6, fontSize: 13,
- background: dlStatus.includes('失败') ? '#fff1f2' : '#f1f5f9',
- color: dlStatus.includes('失败') ? '#e11d48' : '#64748b',
- border: `1px solid ${dlStatus.includes('失败') ? '#fecdd3' : 'transparent'}`,
- }}>{dlStatus}</p>}
- </div>
- {/* Active Downloads */}
- {activeDownloads.size > 0 && (
- <div style={{
- marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>下载任务</h2>
- <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
- {Array.from(activeDownloads.values()).map(dl => (
- <div key={dl.task_id} style={{
- display: 'flex', alignItems: 'center', gap: 12, padding: 12,
- borderRadius: 8, background: '#f8fafc', border: '1px solid #e2e8f0',
- }}>
- {dl.status === 'completed' ? (
- <CheckCircle size={20} color="#059669" />
- ) : dl.status === 'failed' ? (
- <XCircle size={20} color="#f43f5e" />
- ) : (
- <Loader2 size={20} color="#14b8a6" style={{ animation: 'lucide-spin 1s linear infinite' }} />
- )}
- <div style={{ flex: 1 }}>
- <div style={{ fontSize: 13, fontWeight: 500 }}>{dl.dataset_id}</div>
- <div style={{ fontSize: 12, color: '#64748b' }}>
- {dl.status === 'completed' ? `已完成 (${dl.record_count} 条记录)` : dl.status === 'failed' ? `失败: ${dl.error}` :
- dl.status === 'cancelled' ? '已取消' : '下载中...'}
- </div>
- </div>
- {(dl.status === 'pending' || dl.status === 'running' || dl.status === 'downloading') && (
- <button onClick={() => handleCancelDatasetDownload(dl.task_id)} style={{
- padding: '4px 12px', color: '#f43f5e', border: '1px solid #f43f5e',
- borderRadius: 6, background: 'transparent', cursor: 'pointer', fontSize: 12,
- }}>取消</button>
- )}
- </div>
- ))}
- </div>
- </div>
- )}
- {/* Sample Center section */}
- <div style={{
- marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>样本中心</h2>
- <p style={{ fontSize: 13, color: '#64748b', margin: '0 0 12px' }}>
- 从样本中心导入知识库数据作为训练数据集
- </p>
- <button
- onClick={() => { setShowSampleCenter(true); fetchKnowledgeBases(1); }}
- style={{
- padding: '10px 20px', borderRadius: 8, border: 'none',
- background: '#8b5cf6', color: '#fff', cursor: 'pointer', fontSize: 14, fontWeight: 600,
- }}
- >
- <FolderOpen size={16} style={{ display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />
- 从样本中心导入
- </button>
- </div>
- {/* Sample Center Modal */}
- {showSampleCenter && (
- <div
- style={{
- position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
- display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
- }}
- onClick={() => setShowSampleCenter(false)}
- >
- <div
- onClick={e => e.stopPropagation()}
- style={{
- background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 700,
- maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
- }}
- >
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
- <h2 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>样本中心 - 知识库列表</h2>
- <button
- onClick={() => setShowSampleCenter(false)}
- style={{
- border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 20,
- color: '#64748b', padding: '4px 8px', borderRadius: 4,
- }}
- >✕</button>
- </div>
- <p style={{ fontSize: 13, color: '#64748b', margin: '0 0 16px' }}>
- 选择要导入的知识库,数据将转为训练格式
- </p>
- {/* KB list */}
- {kbLoading && (
- <div style={{ textAlign: 'center', padding: 20, color: '#94a3b8' }}>
- <Loader2 size={24} style={{ animation: 'lucide-spin 1s linear infinite' }} />
- <div style={{ marginTop: 8, fontSize: 13 }}>加载中...</div>
- </div>
- )}
- {!kbLoading && kbList.length === 0 && (
- <div style={{ padding: 20, textAlign: 'center', color: '#94a3b8', fontSize: 14 }}>
- 暂无可用的知识库
- </div>
- )}
- {!kbLoading && kbList.length > 0 && (
- <div style={{ border: '1px solid #e2e8f0', borderRadius: 8, overflow: 'hidden' }}>
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
- <thead>
- <tr style={{ background: '#f5f3ff', borderBottom: '2px solid #e2e8f0', textAlign: 'left' }}>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>名称</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>文档数</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>状态</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>字段</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>操作</th>
- </tr>
- </thead>
- <tbody>
- {kbList.map(kb => (
- <tr key={kb.id} style={{ borderBottom: '1px solid #f1f5f9' }}>
- <td style={{ padding: '10px 12px', fontWeight: 500 }}>{kb.name}</td>
- <td style={{ padding: '10px 12px', fontSize: 13 }}>{kb.document_count}</td>
- <td style={{ padding: '10px 12px', fontSize: 13 }}>
- <span style={{
- display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
- background: kb.status === 1 ? '#dcfce7' : '#f1f5f9',
- color: kb.status === 1 ? '#16a34a' : '#64748b',
- }}>
- {kb.status === 1 ? '启用' : '禁用'}
- </span>
- </td>
- <td style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', maxWidth: 200 }}>
- {kb.metadata_schema.slice(0, 3).map(f => f.field_name_cn).join('、')}
- {kb.metadata_schema.length > 3 ? '...' : ''}
- </td>
- <td style={{ padding: '10px 12px' }}>
- <button
- onClick={() => handleImportFromKB(kb)}
- disabled={kbImporting === kb.id}
- style={{
- padding: '4px 12px', color: '#8b5cf6', border: '1px solid #8b5cf6',
- borderRadius: 6, background: kbImporting === kb.id ? '#f5f3ff' : 'transparent',
- cursor: kbImporting === kb.id ? 'not-allowed' : 'pointer',
- fontSize: 12, fontWeight: 500, opacity: kbImporting === kb.id ? 0.7 : 1,
- }}
- >
- {kbImporting === kb.id ? (
- <><Loader2 size={12} style={{ animation: 'lucide-spin 1s linear infinite', display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />导入中</>
- ) : '导入'}
- </button>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- )}
- {/* Pagination */}
- {!kbLoading && kbTotal > 20 && (
- <div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 16, alignItems: 'center' }}>
- <button
- disabled={kbPage <= 1}
- onClick={() => fetchKnowledgeBases(kbPage - 1)}
- style={{
- padding: '4px 12px', borderRadius: 6, border: '1px solid #cbd5e1',
- background: '#fff', cursor: kbPage <= 1 ? 'not-allowed' : 'pointer',
- opacity: kbPage <= 1 ? 0.5 : 1, fontSize: 13,
- }}
- >上一页</button>
- <span style={{ fontSize: 13, color: '#64748b' }}>
- 第 {kbPage} 页 / 共 {Math.ceil(kbTotal / 20)} 页
- </span>
- <button
- disabled={kbPage * 20 >= kbTotal}
- onClick={() => fetchKnowledgeBases(kbPage + 1)}
- style={{
- padding: '4px 12px', borderRadius: 6, border: '1px solid #cbd5e1',
- background: '#fff', cursor: kbPage * 20 >= kbTotal ? 'not-allowed' : 'pointer',
- opacity: kbPage * 20 >= kbTotal ? 0.5 : 1, fontSize: 13,
- }}
- >下一页</button>
- </div>
- )}
- {/* Status */}
- {kbStatus && (
- <p style={{
- marginTop: 12, padding: '8px 12px', borderRadius: 6, fontSize: 13,
- background: kbStatus.includes('失败') ? '#fff1f2' : '#f0fdf4',
- color: kbStatus.includes('失败') ? '#e11d48' : '#16a34a',
- border: `1px solid ${kbStatus.includes('失败') ? '#fecdd3' : '#bbf7d0'}`,
- }}>{kbStatus}</p>
- )}
- </div>
- </div>
- )}
- {/* Dataset list */}
- <div style={{ marginTop: 24 }}>
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
- <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>已上传数据集</h2>
- <button onClick={fetchDatasets} style={{
- padding: '6px 14px', borderRadius: 6, border: '1px solid #cbd5e1',
- background: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 500,
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
- onMouseLeave={e => { e.currentTarget.style.background = '#fff' }}
- >
- 刷新
- </button>
- </div>
- {loading && <p style={{ color: '#94a3b8', fontSize: 13 }}>加载中...</p>}
- {!loading && datasets.length === 0 && (
- <div style={{
- padding: 40, textAlign: 'center', color: '#94a3b8', fontSize: 14,
- background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
- }}>
- <div style={{ marginBottom: 8 }}><Database size={32} color="#94a3b8" strokeWidth={1.5} /></div>
- 暂无数据集,请上传文件或从平台下载
- </div>
- )}
- {!loading && datasets.length > 0 && (
- <div style={{
- background: '#fff', borderRadius: 10, overflow: 'hidden',
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
- <thead>
- <tr style={{ background: '#f0fdfa', borderBottom: '2px solid #f1f5f9', textAlign: 'left' }}>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>名称</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>格式</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>记录数</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>上传时间</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>操作</th>
- </tr>
- </thead>
- <tbody>
- {datasets.map(d => (
- <DatasetRow key={d.id} d={d} onPreview={handlePreview} onDelete={handleDelete} />
- ))}
- </tbody>
- </table>
- </div>
- )}
- </div>
- {/* Preview */}
- {previewData && previewData.rows.length > 0 && (
- <div style={{
- marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>数据预览</h3>
- <div style={{ overflowX: 'auto' }}>
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
- <thead>
- <tr style={{ background: '#f0fdfa', borderBottom: '2px solid #f1f5f9', textAlign: 'left' }}>
- {previewData.columns.map(col => (
- <th key={col} style={{ padding: '8px 12px', fontSize: 12, color: '#64748b', fontWeight: 600, whiteSpace: 'nowrap' }}>{col}</th>
- ))}
- </tr>
- </thead>
- <tbody>
- {previewData.rows.slice(0, 10).map((row, i) => (
- <tr key={i} style={{ borderBottom: '1px solid #f1f5f9', transition: 'background 0.15s ease' }}
- onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
- >
- {previewData.columns.map(col => {
- const cellVal = String(row.data[col] ?? '')
- const isMultiline = cellVal.includes('\n') || cellVal.length > 100
- return (
- <td
- key={col}
- style={{
- padding: '8px 12px',
- maxWidth: isMultiline ? 500 : 200,
- overflow: isMultiline ? 'auto' : 'hidden',
- textOverflow: isMultiline ? undefined : 'ellipsis',
- whiteSpace: isMultiline ? 'pre-wrap' : 'nowrap',
- fontFamily: isMultiline ? 'monospace' : undefined,
- fontSize: isMultiline ? 12 : 13,
- }}
- >
- {cellVal}
- </td>
- )
- })}
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- )}
- </div>
- )
- }
|