CodeReview/src/pages/AdminDashboard.tsx

536 lines
21 KiB
TypeScript
Raw Normal View History

import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Progress } from "@/components/ui/progress";
import {
Database,
HardDrive,
RefreshCw,
Info,
CheckCircle2,
AlertCircle,
FolderOpen,
Clock,
AlertTriangle,
TrendingUp,
Package,
Settings
} from "lucide-react";
import { api, dbMode, isLocalMode } from "@/shared/config/database";
import { DatabaseManager } from "@/components/database/DatabaseManager";
import { DatabaseStatusDetail } from "@/components/database/DatabaseStatus";
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">
<div className="space-y-4 text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-primary mx-auto"></div>
<p className="text-muted-foreground">...</p>
</div>
</div>
);
}
return (
<div className="space-y-6 pb-8">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
<Settings className="h-8 w-8 text-primary" />
</h1>
<p className="text-gray-600 mt-2">
LLM设置使
</p>
</div>
<Button variant="outline" onClick={loadStats}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 数据库模式提示 */}
{!isLocalMode && (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
使 <strong>{dbMode === 'supabase' ? 'Supabase 云端' : '演示'}</strong>
{dbMode === 'demo' && ' 请在 .env 文件中配置 VITE_USE_LOCAL_DB=true 启用本地数据库。'}
</AlertDescription>
</Alert>
)}
{/* 数据库状态卡片 */}
<DatabaseStatusDetail />
{/* 统计概览 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-3xl font-bold mt-2">{stats.totalProjects}</p>
<p className="text-xs text-muted-foreground mt-1">
: {stats.activeProjects}
</p>
</div>
<div className="h-12 w-12 bg-primary/10 rounded-full flex items-center justify-center">
<FolderOpen className="h-6 w-6 text-primary" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-3xl font-bold mt-2">{stats.totalTasks}</p>
<p className="text-xs text-muted-foreground mt-1">
: {stats.completedTasks}
</p>
</div>
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
<Clock className="h-6 w-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-3xl font-bold mt-2">{stats.totalIssues}</p>
<p className="text-xs text-muted-foreground mt-1">
: {stats.resolvedIssues}
</p>
</div>
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-orange-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">使</p>
<p className="text-3xl font-bold mt-2">{stats.storageUsed}</p>
<p className="text-xs text-muted-foreground mt-1">
: {stats.storageQuota}
</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<HardDrive className="h-6 w-6 text-purple-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 主要内容标签页 */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="config"></TabsTrigger>
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="storage"></TabsTrigger>
<TabsTrigger value="operations"></TabsTrigger>
<TabsTrigger value="settings"></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">
{/* 任务完成率 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span></span>
<span className="font-medium">
{stats.totalTasks > 0
? Math.round((stats.completedTasks / stats.totalTasks) * 100)
: 0}%
</span>
</div>
<Progress
value={stats.totalTasks > 0
? (stats.completedTasks / stats.totalTasks) * 100
: 0
}
/>
</div>
<div className="grid grid-cols-2 gap-4 pt-4">
<div className="space-y-1">
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{stats.totalTasks}</p>
</div>
<div className="space-y-1">
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold text-green-600">{stats.completedTasks}</p>
</div>
</div>
</CardContent>
</Card>
{/* 问题解决率 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span></span>
<span className="font-medium">
{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="bg-orange-100"
/>
</div>
<div className="grid grid-cols-2 gap-4 pt-4">
<div className="space-y-1">
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{stats.totalIssues}</p>
</div>
<div className="space-y-1">
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold text-green-600">{stats.resolvedIssues}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 数据库表统计 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Package className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-3">
<FolderOpen className="h-8 w-8 text-primary" />
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{stats.totalProjects}</p>
</div>
</div>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Clock className="h-8 w-8 text-green-600" />
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{stats.totalTasks}</p>
</div>
</div>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-3">
<AlertTriangle className="h-8 w-8 text-orange-600" />
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{stats.totalIssues}</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 存储管理 */}
<TabsContent value="storage" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HardDrive className="h-5 w-5" />
使
</CardTitle>
<CardDescription>
IndexedDB 使
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{storageDetails ? (
<>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>使</span>
<span className="font-medium">{storageDetails.percentage}%</span>
</div>
<Progress value={storageDetails.percentage} />
<div className="flex items-center justify-between text-xs text-muted-foreground">
<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-muted rounded-lg">
<p className="text-sm text-muted-foreground mb-1">使</p>
<p className="text-xl font-bold">{stats.storageUsed}</p>
</div>
<div className="p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="text-xl font-bold">{stats.storageQuota}</p>
</div>
<div className="p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="text-xl font-bold">
{((storageDetails.quota - storageDetails.usage) / 1024 / 1024).toFixed(2)} MB
</p>
</div>
</div>
{storageDetails.percentage > 80 && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
使 80%
</AlertDescription>
</Alert>
)}
</>
) : (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
Storage API
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-start gap-3 p-3 bg-muted rounded-lg">
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
<div>
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">
JSON
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-muted rounded-lg">
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
<div>
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-muted rounded-lg">
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
<div>
<p className="font-medium">使</p>
<p className="text-sm text-muted-foreground">
使
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 数据操作 */}
<TabsContent value="operations" className="space-y-6">
<DatabaseManager />
</TabsContent>
{/* 设置 */}
<TabsContent value="settings" className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<strong></strong> {dbMode === 'local' ? '本地 IndexedDB' : dbMode === 'supabase' ? 'Supabase 云端' : '演示模式'}
</AlertDescription>
</Alert>
<div className="space-y-4 pt-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">
</p>
</div>
<Badge variant="outline"></Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">
</p>
</div>
<Badge variant="outline"></Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">
</p>
</div>
<Badge variant="outline"></Badge>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
<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">
<strong></strong>
</p>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}