Implement live analysis achievements and admin console

这个提交包含在:
cryptocommuniums-afk
2026-03-15 01:39:34 +08:00
父节点 d1b6603061
当前提交 edc66ea5bc
修改 23 个文件,包含 4033 行新增1022 行删除

查看文件

@@ -0,0 +1,316 @@
import { useEffect, useMemo, useState } from "react";
import { useAuth } from "@/_core/hooks/useAuth";
import { trpc } from "@/lib/trpc";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
import { Activity, Database, RefreshCw, Settings2, Shield, Sparkles, Users } from "lucide-react";
export default function AdminConsole() {
const { user } = useAuth();
const utils = trpc.useUtils();
const usersQuery = trpc.admin.users.useQuery({ limit: 100 }, { enabled: user?.role === "admin" });
const tasksQuery = trpc.admin.tasks.useQuery({ limit: 100 }, { enabled: user?.role === "admin" });
const liveSessionsQuery = trpc.admin.liveSessions.useQuery({ limit: 50 }, { enabled: user?.role === "admin" });
const settingsQuery = trpc.admin.settings.useQuery(undefined, { enabled: user?.role === "admin" });
const auditQuery = trpc.admin.auditLogs.useQuery({ limit: 100 }, { enabled: user?.role === "admin" });
const [settingsDrafts, setSettingsDrafts] = useState<Record<string, string>>({});
const refreshAllMutation = trpc.admin.refreshAllNtrp.useMutation({
onSuccess: () => {
toast.success("已提交全量 NTRP 刷新任务");
utils.admin.tasks.invalidate();
},
onError: (error) => toast.error(`提交失败: ${error.message}`),
});
const refreshUserMutation = trpc.admin.refreshUserNtrp.useMutation({
onSuccess: () => {
toast.success("已提交用户 NTRP 刷新任务");
utils.admin.tasks.invalidate();
},
onError: (error) => toast.error(`提交失败: ${error.message}`),
});
const refreshUserNowMutation = trpc.admin.refreshUserNtrpNow.useMutation({
onSuccess: () => {
toast.success("用户 NTRP 已即时刷新");
utils.admin.users.invalidate();
utils.admin.auditLogs.invalidate();
},
onError: (error) => toast.error(`即时刷新失败: ${error.message}`),
});
const updateSettingMutation = trpc.admin.updateSetting.useMutation({
onSuccess: () => {
toast.success("设置已更新");
utils.admin.settings.invalidate();
utils.admin.auditLogs.invalidate();
},
onError: (error) => toast.error(`设置更新失败: ${error.message}`),
});
useEffect(() => {
const drafts: Record<string, string> = {};
(settingsQuery.data || []).forEach((item: any) => {
drafts[item.settingKey] = JSON.stringify(item.value ?? null);
});
setSettingsDrafts(drafts);
}, [settingsQuery.data]);
const totals = useMemo(() => ({
users: (usersQuery.data || []).length,
tasks: (tasksQuery.data || []).length,
sessions: (liveSessionsQuery.data || []).length,
}), [liveSessionsQuery.data, tasksQuery.data, usersQuery.data]);
if (user?.role !== "admin") {
return (
<Alert>
<Shield className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>访</AlertDescription>
</Alert>
);
}
return (
<div className="space-y-6">
<section className="rounded-[28px] border border-border/60 bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.12),_transparent_30%),linear-gradient(180deg,rgba(255,255,255,1),rgba(248,250,252,0.96))] p-5 shadow-sm md:p-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight"></h1>
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
H1
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button onClick={() => refreshAllMutation.mutate()} disabled={refreshAllMutation.isPending} className="gap-2">
<RefreshCw className="h-4 w-4" />
NTRP
</Button>
</div>
</div>
</section>
<div className="grid gap-4 md:grid-cols-3">
<Card className="border-0 shadow-sm">
<CardContent className="pt-5">
<div className="flex items-center gap-3">
<Users className="h-5 w-5 text-emerald-700" />
<div>
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-xl font-semibold">{totals.users}</div>
</div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm">
<CardContent className="pt-5">
<div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-sky-700" />
<div>
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-xl font-semibold">{totals.tasks}</div>
</div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm">
<CardContent className="pt-5">
<div className="flex items-center gap-3">
<Sparkles className="h-5 w-5 text-orange-700" />
<div>
<div className="text-sm text-muted-foreground"></div>
<div className="mt-1 text-xl font-semibold">{totals.sessions}</div>
</div>
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="users" className="space-y-4">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="users"></TabsTrigger>
<TabsTrigger value="tasks"></TabsTrigger>
<TabsTrigger value="sessions"></TabsTrigger>
<TabsTrigger value="settings"></TabsTrigger>
<TabsTrigger value="audit"></TabsTrigger>
</TabsList>
<TabsContent value="users">
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
<CardDescription> NTRP</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{(usersQuery.data || []).map((item: any) => (
<div key={item.id} className="rounded-2xl border border-border/60 bg-muted/20 p-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-medium">{item.name}</span>
<Badge variant="outline">{item.role}</Badge>
<Badge variant="outline">NTRP {Number(item.ntrpRating || 1.5).toFixed(1)}</Badge>
</div>
<div className="mt-2 text-sm text-muted-foreground">
{item.totalSessions || 0} · {item.totalMinutes || 0} · {item.currentStreak || 0}
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={() => refreshUserMutation.mutate({ userId: item.id })}>
</Button>
<Button size="sm" onClick={() => refreshUserNowMutation.mutate({ userId: item.id })}>
</Button>
</div>
</div>
</div>
))}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="tasks">
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{(tasksQuery.data || []).map((task: any) => (
<div key={task.id} className="rounded-2xl border border-border/60 bg-muted/20 p-4">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-medium">{task.title}</span>
<Badge variant="outline">{task.type}</Badge>
<Badge variant={task.status === "failed" ? "destructive" : task.status === "succeeded" ? "secondary" : "outline"}>
{task.status}
</Badge>
</div>
<div className="mt-2 text-sm text-muted-foreground">
{task.userName || task.userId} · {new Date(task.createdAt).toLocaleString("zh-CN")}
</div>
</div>
<div className="min-w-[180px]">
<div className="mb-2 flex items-center justify-between text-xs text-muted-foreground">
<span>{task.message || "无描述"}</span>
<span>{task.progress || 0}%</span>
</div>
<div className="h-2 rounded-full bg-muted">
<div className="h-full rounded-full bg-emerald-500" style={{ width: `${task.progress || 0}%` }} />
</div>
</div>
</div>
</div>
))}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="sessions">
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{(liveSessionsQuery.data || []).map((session: any) => (
<div key={session.id} className="rounded-2xl border border-border/60 bg-muted/20 p-4">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-medium">{session.title}</span>
<Badge variant="outline">{session.userName || session.userId}</Badge>
<Badge variant="outline">{session.sessionMode}</Badge>
</div>
<div className="mt-2 text-sm text-muted-foreground">
{session.dominantAction || "unknown"} · {session.effectiveSegments || 0}/{session.totalSegments || 0}
</div>
</div>
<div className="text-sm text-muted-foreground">
{Math.round(session.overallScore || 0)} · {Math.round((session.durationMs || 0) / 1000)}
</div>
</div>
</div>
))}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="settings">
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Settings2 className="h-4 w-4 text-primary" />
</CardTitle>
<CardDescription> JSON </CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{(settingsQuery.data || []).map((setting: any) => (
<div key={setting.settingKey} className="rounded-2xl border border-border/60 bg-muted/20 p-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end">
<div className="min-w-0 flex-1">
<div className="font-medium">{setting.label}</div>
<div className="mt-1 text-sm text-muted-foreground">{setting.description}</div>
<Input
value={settingsDrafts[setting.settingKey] || ""}
onChange={(event) => setSettingsDrafts((current) => ({ ...current, [setting.settingKey]: event.target.value }))}
className="mt-3 h-11 rounded-2xl"
/>
</div>
<Button
className="gap-2"
onClick={() => {
try {
const parsed = JSON.parse(settingsDrafts[setting.settingKey] || "null");
updateSettingMutation.mutate({ settingKey: setting.settingKey, value: parsed });
} catch {
toast.error("设置值必须是合法 JSON");
}
}}
>
<Database className="h-4 w-4" />
</Button>
</div>
</div>
))}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="audit">
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{(auditQuery.data || []).map((item: any) => (
<div key={item.id} className="rounded-2xl border border-border/60 bg-muted/20 p-4">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-medium">{item.actionType}</span>
<Badge variant="outline">{item.entityType}</Badge>
{item.targetUserId ? <Badge variant="outline"> {item.targetUserId}</Badge> : null}
</div>
<div className="mt-2 text-sm text-muted-foreground">
{item.adminName || item.adminUserId} · {new Date(item.createdAt).toLocaleString("zh-CN")}
</div>
</div>
{item.entityId ? <div className="text-sm text-muted-foreground"> {item.entityId}</div> : null}
</div>
</div>
))}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}