|
|
@@ -1,68 +1,136 @@
|
|
|
"use client";
|
|
|
|
|
|
-import { useState, useEffect } from "react";
|
|
|
-import { Download, Users, TrendingUp, Award, UserCheck, Loader2 } from "lucide-react";
|
|
|
-import { mockApi, ClassStats, LeaderboardEntry } from "@/lib/mockApi";
|
|
|
+import { useMemo } from "react";
|
|
|
+import { Download, Users, TrendingUp, Award, UserCheck, Loader2, RefreshCw, AlertTriangle } from "lucide-react";
|
|
|
import Link from "next/link";
|
|
|
+import { useD4Metrics } from "@/hooks/useD4Metrics";
|
|
|
+import { D4_EVENT_LABELS, type D4EventName } from "@/lib/d4Metrics";
|
|
|
+import { buildRuntimeMetrics, getRuntimeMetric, type RuntimeMetricValue } from "@/lib/metricRuntime";
|
|
|
|
|
|
export default function TeacherDashboard() {
|
|
|
- const [activeTab, setActiveTab] = useState<"K" | "A" | "S" | "D">("K");
|
|
|
- const [stats, setStats] = useState<ClassStats | null>(null);
|
|
|
- const [topPerformers, setTopPerformers] = useState<LeaderboardEntry[]>([]);
|
|
|
- const [loading, setLoading] = useState(true);
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- const fetchData = async () => {
|
|
|
- const [classStats, leaderboard] = await Promise.all([
|
|
|
- mockApi.getClassStats(),
|
|
|
- mockApi.getLeaderboard(5),
|
|
|
- ]);
|
|
|
- setStats(classStats);
|
|
|
- setTopPerformers(leaderboard);
|
|
|
- setLoading(false);
|
|
|
- };
|
|
|
- fetchData();
|
|
|
- }, []);
|
|
|
-
|
|
|
- // Visual config for each dimension
|
|
|
- const dimConfig = {
|
|
|
- K: { trend: "+2.1%", label: "软件工程知识", color: "text-chart-3", bg: "bg-primary-container", icon: <UserCheck className="w-6 h-6 text-primary" /> },
|
|
|
- A: { trend: "+4.5%", label: "AI输出获取", color: "text-chart-4", bg: "bg-chart-5-container", icon: <TrendingUp className="w-6 h-6 text-chart-4" /> },
|
|
|
- S: { trend: "-1.2%", label: "文档编程实现", color: "text-tertiary", bg: "bg-tertiary-container", icon: <Award className="w-6 h-6 text-tertiary" /> },
|
|
|
- D: { trend: "+0.8%", label: "协作态度考核", color: "text-chart-1", bg: "bg-secondary-container", icon: <Users className="w-6 h-6 text-chart-1" /> }
|
|
|
- };
|
|
|
-
|
|
|
- if (loading || !stats) {
|
|
|
+ const d4Metrics = useD4Metrics();
|
|
|
+
|
|
|
+ const runtimeMetrics = useMemo(
|
|
|
+ () => buildRuntimeMetrics({ data: d4Metrics.data, error: d4Metrics.error, isLoading: d4Metrics.isLoading }),
|
|
|
+ [d4Metrics.data, d4Metrics.error, d4Metrics.isLoading],
|
|
|
+ );
|
|
|
+
|
|
|
+ const d4Metric = getRuntimeMetric(runtimeMetrics, "D4");
|
|
|
+ const d4Values = d4Metric?.subMetrics.flatMap((subMetric) => subMetric.values) ?? [];
|
|
|
+
|
|
|
+ const findValue = (name: string): RuntimeMetricValue | undefined => d4Values.find((value) => value.name === name);
|
|
|
+
|
|
|
+ const completionRate = findValue("协作任务完成率");
|
|
|
+ const onTimeRate = findValue("个人任务按时完成率");
|
|
|
+ const averageResponse = findValue("团队沟通平均响应耗时");
|
|
|
+ const effectiveCount = findValue("有效协作次数");
|
|
|
+ const resolvedCount = findValue("协作冲突解决次数");
|
|
|
+ const conflictRate = findValue("协作冲突解决率");
|
|
|
+ const detailValues = [completionRate, onTimeRate, averageResponse, effectiveCount, resolvedCount, conflictRate]
|
|
|
+ .filter((value): value is RuntimeMetricValue => Boolean(value));
|
|
|
+
|
|
|
+ const overviewCards = [
|
|
|
+ {
|
|
|
+ key: "score",
|
|
|
+ label: "D4 综合指数",
|
|
|
+ value: d4Metric?.avgScore ?? "--",
|
|
|
+ helper: d4Metric?.summary ?? "正在同步 D4 指标",
|
|
|
+ color: "text-chart-1",
|
|
|
+ bg: "bg-secondary-container",
|
|
|
+ icon: <UserCheck className="w-6 h-6 text-chart-1" />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "completion",
|
|
|
+ label: "协作任务完成率",
|
|
|
+ value: completionRate?.displayValue ?? "--",
|
|
|
+ helper: completionRate?.helperText ?? "D4_TASK_COMPLETED / D4_TASK_ASSIGNED",
|
|
|
+ color: "text-chart-3",
|
|
|
+ bg: "bg-primary-container",
|
|
|
+ icon: <TrendingUp className="w-6 h-6 text-primary" />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "onTime",
|
|
|
+ label: "个人任务按时完成率",
|
|
|
+ value: onTimeRate?.displayValue ?? "--",
|
|
|
+ helper: onTimeRate?.helperText ?? "按后端 SLA 统计",
|
|
|
+ color: "text-chart-4",
|
|
|
+ bg: "bg-chart-5-container",
|
|
|
+ icon: <Award className="w-6 h-6 text-chart-4" />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "conflict",
|
|
|
+ label: "协作冲突解决率",
|
|
|
+ value: conflictRate?.displayValue ?? "--",
|
|
|
+ helper: conflictRate?.helperText ?? "D4_COLLAB_CONFLICT_RESOLVED / CREATED",
|
|
|
+ color: "text-tertiary",
|
|
|
+ bg: "bg-tertiary-container",
|
|
|
+ icon: <Users className="w-6 h-6 text-tertiary" />,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const eventCounts = Object.entries(d4Metrics.data?.eventCounts ?? {})
|
|
|
+ .sort(([eventA], [eventB]) => eventA.localeCompare(eventB))
|
|
|
+ .map(([event, count]) => ({
|
|
|
+ event,
|
|
|
+ label: D4_EVENT_LABELS[event as D4EventName] ?? event,
|
|
|
+ count,
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (d4Metrics.isLoading && !d4Metrics.data) {
|
|
|
return (
|
|
|
<div className="flex-1 h-full w-full flex items-center justify-center p-8 text-secondary">
|
|
|
<Loader2 className="w-10 h-10 animate-spin mr-3 text-primary" />
|
|
|
- <span className="font-bold animate-pulse">正在加载教学大盘数据...</span>
|
|
|
+ <span className="font-bold animate-pulse">正在加载 D4 教学大盘数据...</span>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- const classAverages = Object.entries(dimConfig).map(([key, cfg]) => ({
|
|
|
- key,
|
|
|
- score: stats.averages[key as keyof typeof stats.averages],
|
|
|
- ...cfg,
|
|
|
- }));
|
|
|
-
|
|
|
- const distributions = stats.distributions;
|
|
|
+ if (d4Metrics.error && !d4Metrics.data) {
|
|
|
+ return (
|
|
|
+ <div className="flex-1 overflow-y-auto p-4 lg:p-8 animate-in fade-in duration-500">
|
|
|
+ <div className="bg-surface rounded-3xl p-8 shadow-elevation-1 border border-metric-low/30 max-w-3xl">
|
|
|
+ <div className="flex items-start gap-4">
|
|
|
+ <div className="w-12 h-12 rounded-2xl bg-metric-low/10 text-metric-low flex items-center justify-center shrink-0">
|
|
|
+ <AlertTriangle className="w-6 h-6" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-display font-bold text-on-surface mb-2">D4 指标接口暂不可用</h1>
|
|
|
+ <p className="text-secondary text-sm leading-relaxed">{d4Metrics.error}</p>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={d4Metrics.refetch}
|
|
|
+ className="mt-6 inline-flex items-center px-4 py-2 rounded-full bg-primary text-on-primary text-sm font-bold shadow hover:bg-primary/90 transition-colors"
|
|
|
+ >
|
|
|
+ <RefreshCw className="w-4 h-4 mr-2" />
|
|
|
+ 重试
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<div className="flex-1 overflow-y-auto p-4 lg:p-8 space-y-8 animate-in fade-in duration-500">
|
|
|
-
|
|
|
- {/* Header */}
|
|
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-4">
|
|
|
<div>
|
|
|
<h1 className="text-3xl lg:text-4xl font-display font-bold text-on-surface mb-2">
|
|
|
教学大盘数据总览
|
|
|
</h1>
|
|
|
<p className="text-secondary text-sm lg:text-base">
|
|
|
- 全体学生多维度能力评估核心统计基准板
|
|
|
+ 当前接入 D4 态度与团队协作指标接口
|
|
|
</p>
|
|
|
</div>
|
|
|
<div className="mt-4 md:mt-0 flex gap-3">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={d4Metrics.refetch}
|
|
|
+ className="flex items-center px-5 py-2.5 border border-outline-variant text-on-surface rounded-full hover:bg-surface-container-high transition-all font-medium text-sm cursor-pointer"
|
|
|
+ >
|
|
|
+ <RefreshCw className="w-4 h-4 mr-2" />
|
|
|
+ 刷新数据
|
|
|
+ </button>
|
|
|
<button className="flex items-center px-5 py-2.5 bg-primary text-on-primary rounded-full shadow-lg hover:shadow-primary/20 hover:bg-white transition-all font-medium text-sm cursor-pointer border border-primary/20">
|
|
|
<Download className="w-5 h-5 mr-2" />
|
|
|
导出班级报告
|
|
|
@@ -70,116 +138,97 @@ export default function TeacherDashboard() {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- {/* Aggregate Score Cards */}
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
- {classAverages.map((data) => (
|
|
|
+ {overviewCards.map((data) => (
|
|
|
<div key={data.key} className="bg-surface rounded-3xl p-6 shadow-elevation-1 border border-outline-variant/30 hover:shadow-elevation-2 transition-all flex flex-col relative overflow-hidden group">
|
|
|
- {/* Gradient splash */}
|
|
|
- <div className={`absolute -right-8 -top-8 w-32 h-32 rounded-full opacity-20 blur-3xl ${data.bg} transition-transform group-hover:scale-110 duration-500`}></div>
|
|
|
-
|
|
|
- <div className="flex justify-between items-start relative z-10">
|
|
|
- <div>
|
|
|
- <p className="text-sm font-bold text-outline uppercase tracking-wider mb-1">大类平均维度 {data.key}</p>
|
|
|
- <h3 className="text-lg font-medium text-on-surface">{data.label}</h3>
|
|
|
- </div>
|
|
|
- <div className={`w-12 h-12 rounded-2xl flex items-center justify-center ${data.bg} shadow-inner`}>
|
|
|
- {data.icon}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div className="flex items-baseline mt-6 relative z-10 gap-3">
|
|
|
- <span className={`text-5xl font-display font-black tracking-tighter ${data.color}`}>{data.score}</span>
|
|
|
- <div className={`flex items-center text-sm font-bold ${data.trend.startsWith('+') ? 'text-metric-high' : 'text-metric-low'}`}>
|
|
|
- {data.trend}
|
|
|
- {data.trend.startsWith('+') ? ' ↑' : ' ↓'}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div className={`absolute -right-8 -top-8 w-32 h-32 rounded-full opacity-20 blur-3xl ${data.bg} transition-transform group-hover:scale-110 duration-500`}></div>
|
|
|
+
|
|
|
+ <div className="flex justify-between items-start relative z-10 gap-4">
|
|
|
+ <div className="min-w-0">
|
|
|
+ <p className="text-sm font-bold text-outline uppercase tracking-wider mb-1">D4 指标</p>
|
|
|
+ <h3 className="text-lg font-medium text-on-surface">{data.label}</h3>
|
|
|
+ </div>
|
|
|
+ <div className={`w-12 h-12 rounded-2xl flex items-center justify-center ${data.bg} shadow-inner shrink-0`}>
|
|
|
+ {data.icon}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="mt-6 relative z-10">
|
|
|
+ <span className={`text-5xl font-display font-black tracking-tighter ${data.color}`}>{data.value}</span>
|
|
|
+ <p className="text-xs text-secondary mt-3 leading-relaxed min-h-8">{data.helper}</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
-
|
|
|
- {/* Disbribution Section (2 cols) */}
|
|
|
<div className="lg:col-span-2 bg-surface-container rounded-3xl p-6 lg:p-8 shadow-elevation-1 border border-outline-variant/30 flex flex-col">
|
|
|
- <div className="flex justify-between items-center mb-6">
|
|
|
- <h3 className="text-xl font-display font-medium text-on-surface">能力等级分布图</h3>
|
|
|
- <nav className="flex space-x-2 bg-background p-1 rounded-lg border border-outline-variant/20">
|
|
|
- {(["K", "A", "S", "D"] as const).map(tab => (
|
|
|
- <button
|
|
|
- key={tab}
|
|
|
- onClick={() => setActiveTab(tab)}
|
|
|
- className={`px-4 py-1.5 rounded-md text-sm font-bold transition-all ${activeTab === tab ? 'bg-primary text-on-primary shadow' : 'text-secondary hover:text-on-surface'}`}
|
|
|
- >
|
|
|
- {tab} 维度
|
|
|
- </button>
|
|
|
- ))}
|
|
|
- </nav>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div className="flex-1 flex flex-col justify-center space-y-6 mt-4">
|
|
|
- {distributions[activeTab].map((item, idx) => {
|
|
|
- // Dynamic styling based on tier
|
|
|
- let barColor = "bg-metric-high";
|
|
|
- if (idx === 1) barColor = "bg-metric-mid";
|
|
|
- else if (idx === 2) barColor = "bg-metric-warn";
|
|
|
- else if (idx === 3) barColor = "bg-metric-low";
|
|
|
-
|
|
|
- return (
|
|
|
- <div key={idx} className="group">
|
|
|
- <div className="flex justify-between text-sm mb-2">
|
|
|
- <span className="font-bold text-on-surface">{item.tier}</span>
|
|
|
- <span className="text-secondary font-medium">{item.count} 人 ({item.percent})</span>
|
|
|
- </div>
|
|
|
- <div className="w-full bg-surface-variant rounded-full h-3.5 overflow-hidden shadow-inner flex">
|
|
|
- <div
|
|
|
- className={`h-full ${barColor} rounded-full transition-all duration-1000 ease-in-out relative`}
|
|
|
- style={{ width: item.percent }}
|
|
|
- >
|
|
|
- <div className="absolute inset-0 bg-white/20 w-full h-full skew-x-12 -translate-x-full group-hover:animate-[shimmer_1.5s_infinite]"></div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div className="flex justify-between items-center mb-6 gap-4">
|
|
|
+ <h3 className="text-xl font-display font-medium text-on-surface">D4 子指标明细</h3>
|
|
|
+ <span className="text-xs font-bold text-primary bg-primary/10 border border-primary/20 px-3 py-1 rounded-full">
|
|
|
+ 后端实时汇总
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
+ {detailValues.map((value) => (
|
|
|
+ <div key={value.name} className="p-4 rounded-2xl bg-surface border border-outline-variant/30">
|
|
|
+ <div className="flex items-start justify-between gap-3">
|
|
|
+ <div>
|
|
|
+ <p className="text-sm font-bold text-on-surface">{value.name}</p>
|
|
|
+ <p className="text-xs text-secondary mt-1 leading-relaxed">{value.helperText}</p>
|
|
|
+ </div>
|
|
|
+ <span className="text-xl font-black text-primary whitespace-nowrap">{value.displayValue}</span>
|
|
|
+ </div>
|
|
|
+ {value.progress != null && (
|
|
|
+ <div className="mt-4 h-2 rounded-full bg-surface-container-high overflow-hidden">
|
|
|
+ <div className="h-full rounded-full bg-primary" style={{ width: `${value.progress}%` }}></div>
|
|
|
</div>
|
|
|
- )
|
|
|
- })}
|
|
|
- </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- {/* Top Performers (1 col) */}
|
|
|
<div className="bg-gradient-to-br from-surface to-surface-container-high rounded-3xl p-6 lg:p-8 shadow-elevation-2 border border-outline-variant/30 flex flex-col">
|
|
|
- <h3 className="text-xl font-display font-medium text-on-surface flex items-center mb-6">
|
|
|
- <Award className="w-5 h-5 text-metric-warn mr-2" fill="currentColor" />
|
|
|
- 班级领军学霸榜
|
|
|
- </h3>
|
|
|
-
|
|
|
- <div className="space-y-4 flex-1">
|
|
|
- {topPerformers.map((stu, i) => (
|
|
|
- <div key={stu.id} className="flex items-center p-3 rounded-2xl hover:bg-white/50 dark:hover:bg-black/20 transition-all border border-transparent hover:border-outline-variant/30 group">
|
|
|
- <div className="relative shrink-0 w-12 h-12 rounded-xl bg-background border border-outline-variant/30 overflow-hidden flex items-center justify-center mr-4">
|
|
|
- <span className="absolute top-0 left-0 text-[10px] font-black tracking-tighter bg-surface px-1.5 opacity-80 rounded-br-lg text-primary">{i+1}</span>
|
|
|
- <span className="text-lg font-bold text-secondary object-contain">{stu.name[0]}</span>
|
|
|
- </div>
|
|
|
- <div className="flex-1 min-w-0">
|
|
|
- <h4 className="font-bold text-on-surface text-sm truncate">{stu.name}</h4>
|
|
|
- <p className="text-xs text-outline font-mono truncate">{stu.id}</p>
|
|
|
- </div>
|
|
|
- <div className="text-right ml-3">
|
|
|
- <div className="text-primary font-black text-xl">{stu.score}</div>
|
|
|
- <div className="text-[10px] font-bold text-secondary bg-surface-variant px-1.5 rounded-full mt-1">
|
|
|
- 强项: {stu.bestAt}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
-
|
|
|
- <Link href="/students" className="w-full mt-6 py-3 border border-outline-variant/50 rounded-xl text-sm font-bold text-primary hover:bg-primary-container/50 transition-colors block text-center">
|
|
|
- 查看全体排名及明细 →
|
|
|
- </Link>
|
|
|
- </div>
|
|
|
+ <h3 className="text-xl font-display font-medium text-on-surface flex items-center mb-6">
|
|
|
+ <Award className="w-5 h-5 text-metric-warn mr-2" fill="currentColor" />
|
|
|
+ D4 事件分布
|
|
|
+ </h3>
|
|
|
+
|
|
|
+ <div className="space-y-4 flex-1">
|
|
|
+ {eventCounts.map((item) => (
|
|
|
+ <div key={item.event} className="p-3 rounded-2xl bg-surface/70 border border-outline-variant/30">
|
|
|
+ <div className="flex items-center justify-between gap-3">
|
|
|
+ <div className="min-w-0">
|
|
|
+ <h4 className="font-bold text-on-surface text-sm truncate">{item.label}</h4>
|
|
|
+ <p className="text-xs text-outline font-mono truncate">{item.event}</p>
|
|
|
+ </div>
|
|
|
+ <div className="text-primary font-black text-xl">{item.count}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ {eventCounts.length === 0 && (
|
|
|
+ <div className="text-sm text-secondary p-4 rounded-2xl bg-surface/70 border border-outline-variant/30">
|
|
|
+ 当前时间范围内暂无 D4 事件。
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {d4Metrics.data && (
|
|
|
+ <div className="mt-6 pt-5 border-t border-outline-variant/40 text-xs text-secondary space-y-1">
|
|
|
+ <p>索引:{d4Metrics.data.scan.indexPattern}</p>
|
|
|
+ <p>扫描事件:{d4Metrics.data.scan.scannedEvents} / {d4Metrics.data.scan.maxEventsToScan}</p>
|
|
|
+ <p>范围:{new Date(d4Metrics.data.from).toLocaleString()} - {new Date(d4Metrics.data.to).toLocaleString()}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
|
|
|
+ <Link href="/analysis" className="w-full mt-6 py-3 border border-outline-variant/50 rounded-xl text-sm font-bold text-primary hover:bg-primary-container/50 transition-colors block text-center">
|
|
|
+ 查看能力分析热力图 →
|
|
|
+ </Link>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
-
|
|
|
+
|
|
|
<div className="h-10"></div>
|
|
|
</div>
|
|
|
);
|