chore: 以本地为准,上传全部并替换 GitHub

This commit is contained in:
卡若
2026-02-03 11:36:53 +08:00
parent 1219166526
commit b404bf546e
131 changed files with 37618 additions and 3930 deletions

View File

@@ -0,0 +1,399 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { useParams, useRouter } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
ArrowLeft,
Database,
Send,
Loader2,
Brain,
RefreshCw,
Settings,
Table,
BarChart3,
Zap,
Clock,
HardDrive,
FolderTree,
} from "lucide-react"
interface DataSource {
id: string
name: string
nameCn: string
description: string
type: string
status: string
database?: string
host?: string
recordCount: number
collections?: number
latency?: number
dataCategory?: string
}
interface Collection {
name: string
count: number
indexes: number
size: string
}
interface ChatMessage {
role: "user" | "assistant"
content: string
timestamp: string
}
export default function DataSourceDetailPage() {
const params = useParams()
const router = useRouter()
const sourceId = params.id as string
const [loading, setLoading] = useState(true)
const [source, setSource] = useState<DataSource | null>(null)
const [collections, setCollections] = useState<Collection[]>([])
const [activeTab, setActiveTab] = useState("overview")
// AI查询
const [query, setQuery] = useState("")
const [querying, setQuerying] = useState(false)
const [messages, setMessages] = useState<ChatMessage[]>([])
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
loadDataSource()
}, [sourceId])
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages])
const loadDataSource = async () => {
setLoading(true)
try {
const res = await fetch('/api/data-sources')
const data = await res.json()
if (data.success) {
const found = data.sources.find((s: DataSource) => s.id === sourceId)
if (found) {
setSource(found)
// 加载集合信息
if (found.database) {
loadCollections(found.database)
}
}
}
} catch (error) {
console.error('加载失败:', error)
} finally {
setLoading(false)
}
}
const loadCollections = async (dbName: string) => {
try {
const res = await fetch(`/api/data-sources?action=collections&db=${dbName}`)
const data = await res.json()
if (data.success) {
setCollections(data.collections || [])
}
} catch (error) {
console.error('加载集合失败:', error)
}
}
// AI查询
const handleQuery = async () => {
if (!query.trim() || querying) return
const userMsg: ChatMessage = {
role: "user",
content: query,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
}
setMessages(prev => [...prev, userMsg])
setQuery("")
setQuerying(true)
try {
// 构造针对特定数据库的查询
const enhancedQuery = source?.database
? `${source.database} 数据库中查询: ${query}`
: query
const response = await fetch("/api/ai-chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: enhancedQuery })
})
const data = await response.json()
setMessages(prev => [...prev, {
role: "assistant",
content: data.success ? data.response.content : `查询失败: ${data.error}`,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}])
} catch (error: any) {
setMessages(prev => [...prev, {
role: "assistant",
content: `错误: ${error.message}`,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}])
} finally {
setQuerying(false)
}
}
const formatNumber = (num: number): string => {
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
return num.toLocaleString()
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
)
}
if (!source) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 p-6">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
<div className="mt-20 text-center text-gray-500"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
<div className="p-6 space-y-4">
{/* 顶部导航 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 mr-1" />
</Button>
<div>
<h1 className="text-xl font-bold text-gray-900">{source.nameCn || source.name}</h1>
<p className="text-sm text-gray-500 font-mono">{source.database || source.name}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge className={source.status === 'connected' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}>
{source.status === 'connected' ? '已连接' : '待配置'}
</Badge>
<Button variant="outline" size="sm">
<Settings className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-100 flex items-center justify-center">
<HardDrive className="h-5 w-5 text-blue-600" />
</div>
<div>
<div className="text-xl font-bold">{formatNumber(source.recordCount)}</div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-green-100 flex items-center justify-center">
<FolderTree className="h-5 w-5 text-green-600" />
</div>
<div>
<div className="text-xl font-bold">{source.collections || 0}</div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-100 flex items-center justify-center">
<Zap className="h-5 w-5 text-purple-600" />
</div>
<div>
<div className="text-xl font-bold">{source.latency || 0}ms</div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-orange-100 flex items-center justify-center">
<Clock className="h-5 w-5 text-orange-600" />
</div>
<div>
<div className="text-xl font-bold"></div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
</div>
{/* 标签页 */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-white/80">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="query">AI查询</TabsTrigger>
<TabsTrigger value="collections"></TabsTrigger>
<TabsTrigger value="stats"></TabsTrigger>
</TabsList>
{/* 概览 */}
<TabsContent value="overview" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-6">
<h3 className="font-medium text-gray-900 mb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-medium">{source.type.toUpperCase()}</div>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-medium">{source.dataCategory || '其他'}</div>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-mono text-sm">{source.host || 'localhost'}</div>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-mono text-sm">{source.database || '-'}</div>
</div>
</div>
<div className="mt-4 p-3 rounded-lg bg-blue-50">
<div className="text-xs text-blue-600 mb-1"></div>
<div className="text-sm text-gray-700">{source.description}</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* AI查询 */}
<TabsContent value="query" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-2 border-b">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Brain className="h-4 w-4 text-purple-500" />
AI - {source.nameCn || source.name}
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{/* 消息区域 */}
<div className="h-[300px] overflow-y-auto p-4 space-y-3">
{messages.length === 0 && (
<div className="text-center text-gray-400 py-10">
<Brain className="h-10 w-10 mx-auto mb-2 opacity-50" />
<p className="text-sm">AI将在此数据库中搜索</p>
<p className="text-xs mt-1">: "查询前10条数据" "统计用户分布"</p>
</div>
)}
{messages.map((msg, i) => (
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] rounded-xl px-4 py-2 text-sm ${
msg.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white'
: 'bg-gray-100 text-gray-800'
}`}>
<div className="whitespace-pre-wrap">{msg.content}</div>
<div className={`text-xs mt-1 ${msg.role === 'user' ? 'text-blue-100' : 'text-gray-400'}`}>
{msg.timestamp}
</div>
</div>
</div>
))}
{querying && (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-xl px-4 py-2 flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-sm text-gray-500">...</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* 输入框 */}
<div className="p-3 border-t flex gap-2">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleQuery()}
placeholder={`${source.database || source.name} 中查询...`}
className="flex-1 bg-gray-50 border-0"
/>
<Button onClick={handleQuery} disabled={querying || !query.trim()}>
{querying ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 集合列表 */}
<TabsContent value="collections" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="space-y-2">
{collections.length === 0 ? (
<div className="text-center text-gray-400 py-10">
<Table className="h-10 w-10 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
) : (
collections.map((coll, i) => (
<div key={i} className="flex items-center justify-between p-3 rounded-lg bg-gray-50 hover:bg-gray-100">
<div className="flex items-center gap-3">
<Table className="h-4 w-4 text-gray-400" />
<span className="font-mono text-sm">{coll.name}</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>{formatNumber(coll.count)} </span>
<span>{coll.indexes} </span>
<span>{coll.size}</span>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 统计 */}
<TabsContent value="stats" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="text-center text-gray-400 py-10">
<BarChart3 className="h-10 w-10 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
)
}