初始化模版工程
This commit is contained in:
171
components/nova-sdk/task-panel/Preview/CsvPreview.tsx
Normal file
171
components/nova-sdk/task-panel/Preview/CsvPreview.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
||||
|
||||
export interface CsvPreviewProps {
|
||||
/** CSV 文件的远程 URL */
|
||||
url?: string
|
||||
/** 直接传入的 CSV 内容 */
|
||||
content?: string
|
||||
}
|
||||
|
||||
function parseCsv(text: string): string[][] {
|
||||
const lines = text.split(/\r?\n/)
|
||||
return lines
|
||||
.filter(line => line.trim() !== '')
|
||||
.map(line => {
|
||||
const row: string[] = []
|
||||
let inQuotes = false
|
||||
let cell = ''
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i]
|
||||
if (ch === '"') {
|
||||
if (inQuotes && line[i + 1] === '"') {
|
||||
cell += '"'
|
||||
i++
|
||||
} else {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
} else if (ch === ',' && !inQuotes) {
|
||||
row.push(cell)
|
||||
cell = ''
|
||||
} else {
|
||||
cell += ch
|
||||
}
|
||||
}
|
||||
row.push(cell)
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
export function CsvPreview({ url, content }: CsvPreviewProps) {
|
||||
const [rows, setRows] = useState<string[][]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
// 在异步回调里更新本地 state,避免在 effect 体内同步 setState
|
||||
queueMicrotask(() => {
|
||||
if (cancelled) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
})
|
||||
|
||||
if (content != null) {
|
||||
queueMicrotask(() => {
|
||||
if (cancelled) return
|
||||
setRows(parseCsv(content))
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
queueMicrotask(() => {
|
||||
if (cancelled) return
|
||||
setRows([])
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.text()
|
||||
})
|
||||
.then(text => {
|
||||
if (cancelled) return
|
||||
setRows(parseCsv(text))
|
||||
})
|
||||
.catch(err => {
|
||||
if (cancelled) return
|
||||
setError(err.message || '加载失败')
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [content, url])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="w-6 h-6 border-2 border-muted border-t-primary rounded-full animate-spin mr-2" />
|
||||
<span className="text-sm">加载中...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-destructive text-sm">
|
||||
加载失败:{error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
||||
文件为空
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const [header, ...body] = rows
|
||||
const colCount = Math.max(...rows.map(r => r.length))
|
||||
const paddedHeader = header.concat(Array(colCount - header.length).fill(''))
|
||||
|
||||
return (
|
||||
<ScrollArea className="w-full h-full">
|
||||
<div className="p-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{paddedHeader.map((col, i) => (
|
||||
<TableHead key={i} className="whitespace-nowrap font-medium text-foreground">
|
||||
{col || `列 ${i + 1}`}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{body.map((row, ri) => {
|
||||
const paddedRow = row.concat(Array(colCount - row.length).fill(''))
|
||||
return (
|
||||
<TableRow key={ri}>
|
||||
{paddedRow.map((cell, ci) => (
|
||||
<TableCell key={ci} className="whitespace-nowrap text-sm">
|
||||
{cell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user