初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View 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>
)
}