| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- "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 Link from "next/link";
- 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) {
- 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>
- </div>
- );
- }
- const classAverages = Object.entries(dimConfig).map(([key, cfg]) => ({
- key,
- score: stats.averages[key as keyof typeof stats.averages],
- ...cfg,
- }));
- const distributions = stats.distributions;
- 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">
- 全体学生多维度能力评估核心统计基准板
- </p>
- </div>
- <div className="mt-4 md:mt-0 flex gap-3">
- <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" />
- 导出班级报告
- </button>
- </div>
- </div>
- {/* Aggregate Score Cards */}
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
- {classAverages.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>
- ))}
- </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>
- )
- })}
- </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>
- </div>
-
- <div className="h-10"></div>
- </div>
- );
- }
|