CodeReview/frontend/src/pages/AdminDashboard.tsx

464 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Progress } from "@/components/ui/progress";
import {
HardDrive,
RefreshCw,
Info,
CheckCircle2,
AlertCircle,
FolderOpen,
Clock,
AlertTriangle,
TrendingUp,
Package,
Settings
} from "lucide-react";
import { api, dbMode } from "@/shared/config/database";
import { DatabaseManager } from "@/components/database/DatabaseManager";
import { SystemConfig } from "@/components/system/SystemConfig";
import { toast } from "sonner";
export default function AdminDashboard() {
const [stats, setStats] = useState({
totalProjects: 0,
activeProjects: 0,
totalTasks: 0,
completedTasks: 0,
totalIssues: 0,
resolvedIssues: 0,
storageUsed: '计算中...',
storageQuota: '未知'
});
const [loading, setLoading] = useState(true);
const [storageDetails, setStorageDetails] = useState<{
usage: number;
quota: number;
percentage: number;
} | null>(null);
useEffect(() => {
loadStats();
}, []);
const loadStats = async () => {
try {
setLoading(true);
const projectStats = await api.getProjectStats();
// 获取存储使用量IndexedDB
let storageUsed = '未知';
let storageQuota = '未知';
let details = null;
if ('storage' in navigator && 'estimate' in navigator.storage) {
try {
const estimate = await navigator.storage.estimate();
const usedMB = ((estimate.usage || 0) / 1024 / 1024).toFixed(2);
const quotaMB = ((estimate.quota || 0) / 1024 / 1024).toFixed(2);
const percentage = estimate.quota ? ((estimate.usage || 0) / estimate.quota * 100) : 0;
storageUsed = `${usedMB} MB`;
storageQuota = `${quotaMB} MB`;
details = {
usage: estimate.usage || 0,
quota: estimate.quota || 0,
percentage: Math.round(percentage)
};
} catch (e) {
console.error('Failed to estimate storage:', e);
}
}
setStats({
totalProjects: projectStats.total_projects || 0,
activeProjects: projectStats.active_projects || 0,
totalTasks: projectStats.total_tasks || 0,
completedTasks: projectStats.completed_tasks || 0,
totalIssues: projectStats.total_issues || 0,
resolvedIssues: projectStats.resolved_issues || 0,
storageUsed,
storageQuota
});
setStorageDetails(details);
} catch (error) {
console.error('Failed to load stats:', error);
toast.error("加载统计数据失败");
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="space-y-4 text-center">
<div className="animate-spin rounded-none h-16 w-16 border-8 border-black border-t-transparent mx-auto"></div>
<p className="text-black font-mono font-bold uppercase">...</p>
</div>
</div>
);
}
return (
<div className="space-y-6 px-6 py-4 bg-background min-h-screen font-mono relative overflow-hidden">
{/* Decorative Background */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none" />
{/* 页面标题 */}
<div className="relative z-10 flex items-center justify-between border-b-4 border-black pb-6 bg-white/50 backdrop-blur-sm p-4 retro-border">
<div>
<h1 className="text-3xl font-display font-bold text-black uppercase tracking-tighter flex items-center gap-3">
<Settings className="h-8 w-8 text-black" />
</h1>
<p className="text-gray-600 mt-2 font-mono border-l-2 border-primary pl-2">
LLM设置使
</p>
</div>
<Button variant="outline" onClick={loadStats} className="retro-btn bg-white text-black border-2 border-black hover:bg-gray-100 rounded-none h-10 font-bold uppercase shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)]">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 主要内容标签页 */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full grid-cols-5 bg-transparent border-2 border-black p-0 h-auto gap-0 mb-6">
<TabsTrigger value="config" className="rounded-none border-r-2 border-black data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase h-10 text-xs"></TabsTrigger>
<TabsTrigger value="overview" className="rounded-none border-r-2 border-black data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase h-10 text-xs"></TabsTrigger>
<TabsTrigger value="storage" className="rounded-none border-r-2 border-black data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase h-10 text-xs"></TabsTrigger>
<TabsTrigger value="operations" className="rounded-none border-r-2 border-black data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase h-10 text-xs"></TabsTrigger>
<TabsTrigger value="settings" className="rounded-none data-[state=active]:bg-black data-[state=active]:text-white font-mono font-bold uppercase h-10 text-xs"></TabsTrigger>
</TabsList>
{/* 系统配置 */}
<TabsContent value="config" className="space-y-6">
<SystemConfig />
</TabsContent>
{/* 数据概览 */}
<TabsContent value="overview" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 任务完成率 */}
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
<div className="p-4 border-b-2 border-black bg-gray-50">
<h3 className="text-lg font-display font-bold uppercase flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
</h3>
<p className="text-xs text-gray-500 font-mono mt-1"></p>
</div>
<div className="p-6 space-y-4 font-mono">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm font-bold">
<span></span>
<span>
{stats.totalTasks > 0
? Math.round((stats.completedTasks / stats.totalTasks) * 100)
: 0}%
</span>
</div>
<Progress
value={stats.totalTasks > 0
? (stats.completedTasks / stats.totalTasks) * 100
: 0
}
className="h-4 border-2 border-black rounded-none bg-gray-200 [&>div]:bg-green-600"
/>
</div>
<div className="grid grid-cols-2 gap-4 pt-4 border-t-2 border-black border-dashed">
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase font-bold"></p>
<p className="text-2xl font-bold">{stats.totalTasks}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase font-bold"></p>
<p className="text-2xl font-bold text-green-600">{stats.completedTasks}</p>
</div>
</div>
</div>
</div>
{/* 问题解决率 */}
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
<div className="p-4 border-b-2 border-black bg-gray-50">
<h3 className="text-lg font-display font-bold uppercase flex items-center gap-2">
<CheckCircle2 className="h-5 w-5" />
</h3>
<p className="text-xs text-gray-500 font-mono mt-1"></p>
</div>
<div className="p-6 space-y-4 font-mono">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm font-bold">
<span></span>
<span>
{stats.totalIssues > 0
? Math.round((stats.resolvedIssues / stats.totalIssues) * 100)
: 0}%
</span>
</div>
<Progress
value={stats.totalIssues > 0
? (stats.resolvedIssues / stats.totalIssues) * 100
: 0
}
className="h-4 border-2 border-black rounded-none bg-gray-200 [&>div]:bg-orange-500"
/>
</div>
<div className="grid grid-cols-2 gap-4 pt-4 border-t-2 border-black border-dashed">
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase font-bold"></p>
<p className="text-2xl font-bold">{stats.totalIssues}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase font-bold"></p>
<p className="text-2xl font-bold text-green-600">{stats.resolvedIssues}</p>
</div>
</div>
</div>
</div>
</div>
{/* 数据库表统计 */}
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
<div className="p-4 border-b-2 border-black bg-gray-50">
<h3 className="text-lg font-display font-bold uppercase flex items-center gap-2">
<Package className="h-5 w-5" />
</h3>
<p className="text-xs text-gray-500 font-mono mt-1"></p>
</div>
<div className="p-6 font-mono">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div className="p-4 border-2 border-black bg-blue-50 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] transition-all">
<div className="flex items-center gap-3">
<FolderOpen className="h-8 w-8 text-primary" />
<div>
<p className="text-xs text-gray-600 uppercase font-bold"></p>
<p className="text-2xl font-bold">{stats.totalProjects}</p>
</div>
</div>
</div>
<div className="p-4 border-2 border-black bg-green-50 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] transition-all">
<div className="flex items-center gap-3">
<Clock className="h-8 w-8 text-green-600" />
<div>
<p className="text-xs text-gray-600 uppercase font-bold"></p>
<p className="text-2xl font-bold">{stats.totalTasks}</p>
</div>
</div>
</div>
<div className="p-4 border-2 border-black bg-orange-50 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] transition-all">
<div className="flex items-center gap-3">
<AlertTriangle className="h-8 w-8 text-orange-600" />
<div>
<p className="text-xs text-gray-600 uppercase font-bold"></p>
<p className="text-2xl font-bold">{stats.totalIssues}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</TabsContent>
{/* 存储管理 */}
<TabsContent value="storage" className="space-y-6">
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
<div className="p-4 border-b-2 border-black bg-gray-50">
<h3 className="text-lg font-display font-bold uppercase flex items-center gap-2">
<HardDrive className="h-5 w-5" />
使
</h3>
<p className="text-xs text-gray-500 font-mono mt-1">
IndexedDB 使
</p>
</div>
<div className="p-6 space-y-6 font-mono">
{storageDetails ? (
<>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm font-bold">
<span>使</span>
<span>{storageDetails.percentage}%</span>
</div>
<Progress value={storageDetails.percentage} className="h-4 border-2 border-black rounded-none bg-gray-200 [&>div]:bg-primary" />
<div className="flex items-center justify-between text-xs text-gray-500 font-bold">
<span>{stats.storageUsed} 使</span>
<span>{stats.storageQuota} </span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4">
<div className="p-4 bg-gray-100 border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<p className="text-xs text-gray-600 uppercase font-bold mb-1">使</p>
<p className="text-xl font-bold">{stats.storageUsed}</p>
</div>
<div className="p-4 bg-gray-100 border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<p className="text-xs text-gray-600 uppercase font-bold mb-1"></p>
<p className="text-xl font-bold">{stats.storageQuota}</p>
</div>
<div className="p-4 bg-gray-100 border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<p className="text-xs text-gray-600 uppercase font-bold mb-1"></p>
<p className="text-xl font-bold">
{((storageDetails.quota - storageDetails.usage) / 1024 / 1024).toFixed(2)} MB
</p>
</div>
</div>
{storageDetails.percentage > 80 && (
<div className="bg-red-50 border-2 border-red-500 p-4 flex items-start gap-3 shadow-[4px_4px_0px_0px_rgba(239,68,68,1)]">
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
<div>
<p className="font-bold text-red-800 uppercase"></p>
<p className="text-sm text-red-700 font-medium">
使 80%
</p>
</div>
</div>
)}
</>
) : (
<div className="bg-blue-50 border-2 border-blue-500 p-4 flex items-start gap-3 shadow-[4px_4px_0px_0px_rgba(59,130,246,1)]">
<Info className="h-5 w-5 text-blue-600 mt-0.5" />
<div>
<p className="font-bold text-blue-800 uppercase"></p>
<p className="text-sm text-blue-700 font-medium">
Storage API
</p>
</div>
</div>
)}
</div>
</div>
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
<div className="p-4 border-b-2 border-black bg-gray-50">
<h3 className="text-lg font-display font-bold uppercase"></h3>
</div>
<div className="p-6 space-y-3 font-mono">
<div className="flex items-start gap-3 p-3 bg-gray-50 border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
<div>
<p className="font-bold text-black uppercase text-sm"></p>
<p className="text-xs text-gray-600 font-medium">
JSON
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
<div>
<p className="font-bold text-black uppercase text-sm"></p>
<p className="text-xs text-gray-600 font-medium">
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
<div>
<p className="font-bold text-black uppercase text-sm">使</p>
<p className="text-xs text-gray-600 font-medium">
使
</p>
</div>
</div>
</div>
</div>
</TabsContent>
{/* 数据操作 */}
<TabsContent value="operations" className="space-y-6">
<DatabaseManager />
</TabsContent>
{/* 设置 */}
<TabsContent value="settings" className="space-y-6">
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
<div className="p-4 border-b-2 border-black bg-gray-50">
<h3 className="text-lg font-display font-bold uppercase"></h3>
<p className="text-xs text-gray-500 font-mono mt-1"></p>
</div>
<div className="p-6 space-y-4 font-mono">
<div className="bg-blue-50 border-2 border-blue-500 p-4 flex items-start gap-3 shadow-[4px_4px_0px_0px_rgba(59,130,246,1)]">
<Info className="h-5 w-5 text-blue-600 mt-0.5" />
<div>
<p className="font-bold text-blue-800 uppercase text-sm"></p>
<p className="text-sm text-blue-700 font-medium mt-1">
{
dbMode === 'api' ? '后端 PostgreSQL 数据库' :
dbMode === 'local' ? '本地 IndexedDB' :
dbMode === 'supabase' ? 'Supabase 云端(已废弃)' :
'演示模式'
}
</p>
</div>
</div>
<div className="space-y-4 pt-4">
<div className="flex items-center justify-between p-4 border-2 border-black bg-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<div>
<p className="font-bold text-black uppercase text-sm"></p>
<p className="text-xs text-gray-500 font-medium">
</p>
</div>
<Badge variant="outline" className="rounded-none border-black bg-gray-100 font-mono text-xs"></Badge>
</div>
<div className="flex items-center justify-between p-4 border-2 border-black bg-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<div>
<p className="font-bold text-black uppercase text-sm"></p>
<p className="text-xs text-gray-500 font-medium">
</p>
</div>
<Badge variant="outline" className="rounded-none border-black bg-gray-100 font-mono text-xs"></Badge>
</div>
<div className="flex items-center justify-between p-4 border-2 border-black bg-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
<div>
<p className="font-bold text-black uppercase text-sm"></p>
<p className="text-xs text-gray-500 font-medium">
</p>
</div>
<Badge variant="outline" className="rounded-none border-black bg-gray-100 font-mono text-xs"></Badge>
</div>
</div>
</div>
</div>
<div className="retro-card bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-0">
<div className="p-4 border-b-2 border-black bg-gray-50">
<h3 className="text-lg font-display font-bold uppercase"></h3>
</div>
<div className="p-6 space-y-3 text-sm text-gray-600 font-mono font-medium">
<p>
使 IndexedDB
</p>
<ul className="list-disc list-inside space-y-2 ml-2">
<li></li>
<li>线访</li>
<li></li>
<li></li>
<li></li>
</ul>
<p className="pt-2 border-t-2 border-black border-dashed mt-4">
<strong className="text-black uppercase"></strong>
</p>
</div>
</div>
</TabsContent>
</Tabs>
</div>
);
}