Files
users/components/data-integration/database-structure-viewer.tsx

294 lines
11 KiB
TypeScript
Raw Permalink Normal View History

"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Database, TableIcon, Key, Download, RefreshCw } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton"
interface TableField {
Field: string
Type: string
Null: string
Key: string
Default: string | null
Extra: string
}
interface DatabaseStructure {
[tableName: string]: TableField[]
}
export function DatabaseStructureViewer() {
const [databases, setDatabases] = useState<string[]>([])
const [selectedDatabase, setSelectedDatabase] = useState<string>("")
const [structure, setStructure] = useState<DatabaseStructure | null>(null)
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
// 获取数据库列表
useEffect(() => {
const fetchDatabases = async () => {
try {
setLoading(true)
const response = await fetch("/api/database-structure")
const result = await response.json()
if (result.success) {
const dbList = result.data.map((item: any) => Object.values(item)[0] as string)
setDatabases(dbList)
// 默认选择第一个非系统数据库
const nonSystemDbs = dbList.filter(
(db: string) => !["information_schema", "mysql", "performance_schema", "sys"].includes(db),
)
if (nonSystemDbs.length > 0) {
setSelectedDatabase(nonSystemDbs[0])
}
} else {
setError(result.message || "获取数据库列表失败")
}
} catch (err) {
setError("获取数据库列表失败")
console.error(err)
} finally {
setLoading(false)
}
}
fetchDatabases()
}, [])
// 获取数据库结构
useEffect(() => {
if (!selectedDatabase) return
const fetchDatabaseStructure = async () => {
try {
setLoading(true)
setStructure(null)
const response = await fetch(`/api/database-structure?database=${selectedDatabase}`)
const result = await response.json()
if (result.success) {
setStructure(result.data)
setError(null)
} else {
setError(result.message || "获取数据库结构失败")
}
} catch (err) {
setError("获取数据库结构失败")
console.error(err)
} finally {
setLoading(false)
}
}
fetchDatabaseStructure()
}, [selectedDatabase])
// 获取字段类型的简化描述
const getSimplifiedType = (type: string) => {
if (type.includes("int")) return "Integer"
if (type.includes("varchar") || type.includes("text") || type.includes("char")) return "String"
if (type.includes("datetime") || type.includes("timestamp")) return "DateTime"
if (type.includes("date")) return "Date"
if (type.includes("decimal") || type.includes("float") || type.includes("double")) return "Decimal"
if (type.includes("bool")) return "Boolean"
if (type.includes("json")) return "JSON"
if (type.includes("blob")) return "Binary"
return type
}
// 获取字段类型的标签颜色
const getTypeColor = (type: string) => {
if (type.includes("int")) return "bg-blue-100 text-blue-800"
if (type.includes("varchar") || type.includes("text") || type.includes("char")) return "bg-green-100 text-green-800"
if (type.includes("datetime") || type.includes("timestamp") || type.includes("date"))
return "bg-purple-100 text-purple-800"
if (type.includes("decimal") || type.includes("float") || type.includes("double"))
return "bg-yellow-100 text-yellow-800"
if (type.includes("bool")) return "bg-orange-100 text-orange-800"
if (type.includes("json")) return "bg-indigo-100 text-indigo-800"
if (type.includes("blob")) return "bg-red-100 text-red-800"
return "bg-gray-100 text-gray-800"
}
// 导出数据库结构为JSON
const exportDatabaseStructure = () => {
if (!structure) return
const dataStr = JSON.stringify(structure, null, 2)
const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`
const exportFileDefaultName = `${selectedDatabase}-structure.json`
const linkElement = document.createElement("a")
linkElement.setAttribute("href", dataUri)
linkElement.setAttribute("download", exportFileDefaultName)
linkElement.click()
}
// 刷新数据库结构
const refreshDatabaseStructure = async () => {
if (!selectedDatabase) return
try {
setLoading(true)
setStructure(null)
const response = await fetch(`/api/database-structure?database=${selectedDatabase}`)
const result = await response.json()
if (result.success) {
setStructure(result.data)
setError(null)
} else {
setError(result.message || "刷新数据库结构失败")
}
} catch (err) {
setError("刷新数据库结构失败")
console.error(err)
} finally {
setLoading(false)
}
}
return (
<Card className="w-full">
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={refreshDatabaseStructure}
disabled={loading || !selectedDatabase}
>
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" size="sm" onClick={exportDatabaseStructure} disabled={!structure}>
<Download className="h-4 w-4 mr-2" />
JSON
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{loading && (
<div className="space-y-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-64 w-full" />
</div>
)}
{error && (
<div className="p-4 bg-red-50 text-red-800 rounded-md">
<p>{error}</p>
</div>
)}
{!loading && !error && (
<div className="space-y-4">
<div className="flex space-x-2 overflow-x-auto pb-2">
{databases.map((db) => (
<Button
key={db}
variant={selectedDatabase === db ? "default" : "outline"}
size="sm"
onClick={() => setSelectedDatabase(db)}
>
<Database className="h-4 w-4 mr-2" />
{db}
</Button>
))}
</div>
{structure && (
<div>
<h3 className="text-lg font-medium mb-2">: {selectedDatabase}</h3>
<p className="text-sm text-muted-foreground mb-4"> {Object.keys(structure).length} </p>
<Accordion type="multiple" className="w-full">
{Object.entries(structure).map(([tableName, fields]) => (
<AccordionItem key={tableName} value={tableName}>
<AccordionTrigger className="hover:bg-muted/50 px-4">
<div className="flex items-center">
<TableIcon className="h-4 w-4 mr-2" />
<span>{tableName}</span>
<Badge className="ml-2 bg-gray-100 text-gray-800">{fields.length} </Badge>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={index}>
<TableCell className="font-medium">
{field.Field}
{field.Key === "PRI" && <Key className="h-3 w-3 ml-1 inline text-amber-500" />}
</TableCell>
<TableCell>
<Badge className={getTypeColor(field.Type)}>{getSimplifiedType(field.Type)}</Badge>
<span className="text-xs text-muted-foreground ml-2">{field.Type}</span>
</TableCell>
<TableCell>
{field.Null === "YES" ? (
<Badge variant="outline"></Badge>
) : (
<Badge className="bg-red-100 text-red-800"></Badge>
)}
</TableCell>
<TableCell>
{field.Key === "PRI" && <Badge></Badge>}
{field.Key === "UNI" && <Badge className="bg-blue-100 text-blue-800"></Badge>}
{field.Key === "MUL" && (
<Badge className="bg-purple-100 text-purple-800"></Badge>
)}
</TableCell>
<TableCell>
{field.Default !== null ? (
field.Default
) : (
<span className="text-muted-foreground">NULL</span>
)}
</TableCell>
<TableCell>{field.Extra}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}