1
0

TeacherDashboard.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. "use client";
  2. import { useState, useEffect } from "react";
  3. import { Download, Users, TrendingUp, Award, UserCheck, Loader2 } from "lucide-react";
  4. import { mockApi, ClassStats, LeaderboardEntry } from "@/lib/mockApi";
  5. import Link from "next/link";
  6. export default function TeacherDashboard() {
  7. const [activeTab, setActiveTab] = useState<"K" | "A" | "S" | "D">("K");
  8. const [stats, setStats] = useState<ClassStats | null>(null);
  9. const [topPerformers, setTopPerformers] = useState<LeaderboardEntry[]>([]);
  10. const [loading, setLoading] = useState(true);
  11. useEffect(() => {
  12. const fetchData = async () => {
  13. const [classStats, leaderboard] = await Promise.all([
  14. mockApi.getClassStats(),
  15. mockApi.getLeaderboard(5),
  16. ]);
  17. setStats(classStats);
  18. setTopPerformers(leaderboard);
  19. setLoading(false);
  20. };
  21. fetchData();
  22. }, []);
  23. // Visual config for each dimension
  24. const dimConfig = {
  25. K: { trend: "+2.1%", label: "软件工程知识", color: "text-chart-3", bg: "bg-primary-container", icon: <UserCheck className="w-6 h-6 text-primary" /> },
  26. 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" /> },
  27. S: { trend: "-1.2%", label: "文档编程实现", color: "text-tertiary", bg: "bg-tertiary-container", icon: <Award className="w-6 h-6 text-tertiary" /> },
  28. D: { trend: "+0.8%", label: "协作态度考核", color: "text-chart-1", bg: "bg-secondary-container", icon: <Users className="w-6 h-6 text-chart-1" /> }
  29. };
  30. if (loading || !stats) {
  31. return (
  32. <div className="flex-1 h-full w-full flex items-center justify-center p-8 text-secondary">
  33. <Loader2 className="w-10 h-10 animate-spin mr-3 text-primary" />
  34. <span className="font-bold animate-pulse">正在加载教学大盘数据...</span>
  35. </div>
  36. );
  37. }
  38. const classAverages = Object.entries(dimConfig).map(([key, cfg]) => ({
  39. key,
  40. score: stats.averages[key as keyof typeof stats.averages],
  41. ...cfg,
  42. }));
  43. const distributions = stats.distributions;
  44. return (
  45. <div className="flex-1 overflow-y-auto p-4 lg:p-8 space-y-8 animate-in fade-in duration-500">
  46. {/* Header */}
  47. <div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-4">
  48. <div>
  49. <h1 className="text-3xl lg:text-4xl font-display font-bold text-on-surface mb-2">
  50. 教学大盘数据总览
  51. </h1>
  52. <p className="text-secondary text-sm lg:text-base">
  53. 全体学生多维度能力评估核心统计基准板
  54. </p>
  55. </div>
  56. <div className="mt-4 md:mt-0 flex gap-3">
  57. <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">
  58. <Download className="w-5 h-5 mr-2" />
  59. 导出班级报告
  60. </button>
  61. </div>
  62. </div>
  63. {/* Aggregate Score Cards */}
  64. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
  65. {classAverages.map((data) => (
  66. <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">
  67. {/* Gradient splash */}
  68. <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>
  69. <div className="flex justify-between items-start relative z-10">
  70. <div>
  71. <p className="text-sm font-bold text-outline uppercase tracking-wider mb-1">大类平均维度 {data.key}</p>
  72. <h3 className="text-lg font-medium text-on-surface">{data.label}</h3>
  73. </div>
  74. <div className={`w-12 h-12 rounded-2xl flex items-center justify-center ${data.bg} shadow-inner`}>
  75. {data.icon}
  76. </div>
  77. </div>
  78. <div className="flex items-baseline mt-6 relative z-10 gap-3">
  79. <span className={`text-5xl font-display font-black tracking-tighter ${data.color}`}>{data.score}</span>
  80. <div className={`flex items-center text-sm font-bold ${data.trend.startsWith('+') ? 'text-metric-high' : 'text-metric-low'}`}>
  81. {data.trend}
  82. {data.trend.startsWith('+') ? ' ↑' : ' ↓'}
  83. </div>
  84. </div>
  85. </div>
  86. ))}
  87. </div>
  88. <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
  89. {/* Disbribution Section (2 cols) */}
  90. <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">
  91. <div className="flex justify-between items-center mb-6">
  92. <h3 className="text-xl font-display font-medium text-on-surface">能力等级分布图</h3>
  93. <nav className="flex space-x-2 bg-background p-1 rounded-lg border border-outline-variant/20">
  94. {(["K", "A", "S", "D"] as const).map(tab => (
  95. <button
  96. key={tab}
  97. onClick={() => setActiveTab(tab)}
  98. 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'}`}
  99. >
  100. {tab} 维度
  101. </button>
  102. ))}
  103. </nav>
  104. </div>
  105. <div className="flex-1 flex flex-col justify-center space-y-6 mt-4">
  106. {distributions[activeTab].map((item, idx) => {
  107. // Dynamic styling based on tier
  108. let barColor = "bg-metric-high";
  109. if (idx === 1) barColor = "bg-metric-mid";
  110. else if (idx === 2) barColor = "bg-metric-warn";
  111. else if (idx === 3) barColor = "bg-metric-low";
  112. return (
  113. <div key={idx} className="group">
  114. <div className="flex justify-between text-sm mb-2">
  115. <span className="font-bold text-on-surface">{item.tier}</span>
  116. <span className="text-secondary font-medium">{item.count} 人 ({item.percent})</span>
  117. </div>
  118. <div className="w-full bg-surface-variant rounded-full h-3.5 overflow-hidden shadow-inner flex">
  119. <div
  120. className={`h-full ${barColor} rounded-full transition-all duration-1000 ease-in-out relative`}
  121. style={{ width: item.percent }}
  122. >
  123. <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>
  124. </div>
  125. </div>
  126. </div>
  127. )
  128. })}
  129. </div>
  130. </div>
  131. {/* Top Performers (1 col) */}
  132. <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">
  133. <h3 className="text-xl font-display font-medium text-on-surface flex items-center mb-6">
  134. <Award className="w-5 h-5 text-metric-warn mr-2" fill="currentColor" />
  135. 班级领军学霸榜
  136. </h3>
  137. <div className="space-y-4 flex-1">
  138. {topPerformers.map((stu, i) => (
  139. <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">
  140. <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">
  141. <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>
  142. <span className="text-lg font-bold text-secondary object-contain">{stu.name[0]}</span>
  143. </div>
  144. <div className="flex-1 min-w-0">
  145. <h4 className="font-bold text-on-surface text-sm truncate">{stu.name}</h4>
  146. <p className="text-xs text-outline font-mono truncate">{stu.id}</p>
  147. </div>
  148. <div className="text-right ml-3">
  149. <div className="text-primary font-black text-xl">{stu.score}</div>
  150. <div className="text-[10px] font-bold text-secondary bg-surface-variant px-1.5 rounded-full mt-1">
  151. 强项: {stu.bestAt}
  152. </div>
  153. </div>
  154. </div>
  155. ))}
  156. </div>
  157. <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">
  158. 查看全体排名及明细 →
  159. </Link>
  160. </div>
  161. </div>
  162. <div className="h-10"></div>
  163. </div>
  164. );
  165. }