TeacherDashboard.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. "use client";
  2. import { useMemo } from "react";
  3. import { Download, Users, TrendingUp, Award, UserCheck, Loader2, RefreshCw, AlertTriangle } from "lucide-react";
  4. import Link from "next/link";
  5. import { useD4Metrics } from "@/hooks/useD4Metrics";
  6. import { D4_EVENT_LABELS, type D4EventName } from "@/lib/d4Metrics";
  7. import { buildRuntimeMetrics, getRuntimeMetric, type RuntimeMetricValue } from "@/lib/metricRuntime";
  8. export default function TeacherDashboard() {
  9. const d4Metrics = useD4Metrics();
  10. const runtimeMetrics = useMemo(
  11. () => buildRuntimeMetrics({ data: d4Metrics.data, error: d4Metrics.error, isLoading: d4Metrics.isLoading }),
  12. [d4Metrics.data, d4Metrics.error, d4Metrics.isLoading],
  13. );
  14. const d4Metric = getRuntimeMetric(runtimeMetrics, "D4");
  15. const d4Values = d4Metric?.subMetrics.flatMap((subMetric) => subMetric.values) ?? [];
  16. const findValue = (name: string): RuntimeMetricValue | undefined => d4Values.find((value) => value.name === name);
  17. const completionRate = findValue("协作任务完成率");
  18. const onTimeRate = findValue("个人任务按时完成率");
  19. const averageResponse = findValue("团队沟通平均响应耗时");
  20. const effectiveCount = findValue("有效协作次数");
  21. const resolvedCount = findValue("协作冲突解决次数");
  22. const conflictRate = findValue("协作冲突解决率");
  23. const detailValues = [completionRate, onTimeRate, averageResponse, effectiveCount, resolvedCount, conflictRate]
  24. .filter((value): value is RuntimeMetricValue => Boolean(value));
  25. const overviewCards = [
  26. {
  27. key: "score",
  28. label: "D4 综合指数",
  29. value: d4Metric?.avgScore ?? "--",
  30. helper: d4Metric?.summary ?? "正在同步 D4 指标",
  31. color: "text-chart-1",
  32. bg: "bg-secondary-container",
  33. icon: <UserCheck className="w-6 h-6 text-chart-1" />,
  34. },
  35. {
  36. key: "completion",
  37. label: "协作任务完成率",
  38. value: completionRate?.displayValue ?? "--",
  39. helper: completionRate?.helperText ?? "D4_TASK_COMPLETED / D4_TASK_ASSIGNED",
  40. color: "text-chart-3",
  41. bg: "bg-primary-container",
  42. icon: <TrendingUp className="w-6 h-6 text-primary" />,
  43. },
  44. {
  45. key: "onTime",
  46. label: "个人任务按时完成率",
  47. value: onTimeRate?.displayValue ?? "--",
  48. helper: onTimeRate?.helperText ?? "按后端 SLA 统计",
  49. color: "text-chart-4",
  50. bg: "bg-chart-5-container",
  51. icon: <Award className="w-6 h-6 text-chart-4" />,
  52. },
  53. {
  54. key: "conflict",
  55. label: "协作冲突解决率",
  56. value: conflictRate?.displayValue ?? "--",
  57. helper: conflictRate?.helperText ?? "D4_COLLAB_CONFLICT_RESOLVED / CREATED",
  58. color: "text-tertiary",
  59. bg: "bg-tertiary-container",
  60. icon: <Users className="w-6 h-6 text-tertiary" />,
  61. },
  62. ];
  63. const eventCounts = Object.entries(d4Metrics.data?.eventCounts ?? {})
  64. .sort(([eventA], [eventB]) => eventA.localeCompare(eventB))
  65. .map(([event, count]) => ({
  66. event,
  67. label: D4_EVENT_LABELS[event as D4EventName] ?? event,
  68. count,
  69. }));
  70. if (d4Metrics.isLoading && !d4Metrics.data) {
  71. return (
  72. <div className="flex-1 h-full w-full flex items-center justify-center p-8 text-secondary">
  73. <Loader2 className="w-10 h-10 animate-spin mr-3 text-primary" />
  74. <span className="font-bold animate-pulse">正在加载 D4 教学大盘数据...</span>
  75. </div>
  76. );
  77. }
  78. if (d4Metrics.error && !d4Metrics.data) {
  79. return (
  80. <div className="flex-1 overflow-y-auto p-4 lg:p-8 animate-in fade-in duration-500">
  81. <div className="bg-surface rounded-3xl p-8 shadow-elevation-1 border border-metric-low/30 max-w-3xl">
  82. <div className="flex items-start gap-4">
  83. <div className="w-12 h-12 rounded-2xl bg-metric-low/10 text-metric-low flex items-center justify-center shrink-0">
  84. <AlertTriangle className="w-6 h-6" />
  85. </div>
  86. <div>
  87. <h1 className="text-2xl font-display font-bold text-on-surface mb-2">D4 指标接口暂不可用</h1>
  88. <p className="text-secondary text-sm leading-relaxed">{d4Metrics.error}</p>
  89. <button
  90. type="button"
  91. onClick={d4Metrics.refetch}
  92. 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"
  93. >
  94. <RefreshCw className="w-4 h-4 mr-2" />
  95. 重试
  96. </button>
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. );
  102. }
  103. return (
  104. <div className="flex-1 overflow-y-auto p-4 lg:p-8 space-y-8 animate-in fade-in duration-500">
  105. <div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-4">
  106. <div>
  107. <h1 className="text-3xl lg:text-4xl font-display font-bold text-on-surface mb-2">
  108. 教学大盘数据总览
  109. </h1>
  110. <p className="text-secondary text-sm lg:text-base">
  111. 当前接入 D4 态度与团队协作指标接口
  112. </p>
  113. </div>
  114. <div className="mt-4 md:mt-0 flex gap-3">
  115. <button
  116. type="button"
  117. onClick={d4Metrics.refetch}
  118. 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"
  119. >
  120. <RefreshCw className="w-4 h-4 mr-2" />
  121. 刷新数据
  122. </button>
  123. <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">
  124. <Download className="w-5 h-5 mr-2" />
  125. 导出班级报告
  126. </button>
  127. </div>
  128. </div>
  129. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
  130. {overviewCards.map((data) => (
  131. <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">
  132. <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>
  133. <div className="flex justify-between items-start relative z-10 gap-4">
  134. <div className="min-w-0">
  135. <p className="text-sm font-bold text-outline uppercase tracking-wider mb-1">D4 指标</p>
  136. <h3 className="text-lg font-medium text-on-surface">{data.label}</h3>
  137. </div>
  138. <div className={`w-12 h-12 rounded-2xl flex items-center justify-center ${data.bg} shadow-inner shrink-0`}>
  139. {data.icon}
  140. </div>
  141. </div>
  142. <div className="mt-6 relative z-10">
  143. <span className={`text-5xl font-display font-black tracking-tighter ${data.color}`}>{data.value}</span>
  144. <p className="text-xs text-secondary mt-3 leading-relaxed min-h-8">{data.helper}</p>
  145. </div>
  146. </div>
  147. ))}
  148. </div>
  149. <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
  150. <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">
  151. <div className="flex justify-between items-center mb-6 gap-4">
  152. <h3 className="text-xl font-display font-medium text-on-surface">D4 子指标明细</h3>
  153. <span className="text-xs font-bold text-primary bg-primary/10 border border-primary/20 px-3 py-1 rounded-full">
  154. 后端实时汇总
  155. </span>
  156. </div>
  157. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  158. {detailValues.map((value) => (
  159. <div key={value.name} className="p-4 rounded-2xl bg-surface border border-outline-variant/30">
  160. <div className="flex items-start justify-between gap-3">
  161. <div>
  162. <p className="text-sm font-bold text-on-surface">{value.name}</p>
  163. <p className="text-xs text-secondary mt-1 leading-relaxed">{value.helperText}</p>
  164. </div>
  165. <span className="text-xl font-black text-primary whitespace-nowrap">{value.displayValue}</span>
  166. </div>
  167. {value.progress != null && (
  168. <div className="mt-4 h-2 rounded-full bg-surface-container-high overflow-hidden">
  169. <div className="h-full rounded-full bg-primary" style={{ width: `${value.progress}%` }}></div>
  170. </div>
  171. )}
  172. </div>
  173. ))}
  174. </div>
  175. </div>
  176. <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">
  177. <h3 className="text-xl font-display font-medium text-on-surface flex items-center mb-6">
  178. <Award className="w-5 h-5 text-metric-warn mr-2" fill="currentColor" />
  179. D4 事件分布
  180. </h3>
  181. <div className="space-y-4 flex-1">
  182. {eventCounts.map((item) => (
  183. <div key={item.event} className="p-3 rounded-2xl bg-surface/70 border border-outline-variant/30">
  184. <div className="flex items-center justify-between gap-3">
  185. <div className="min-w-0">
  186. <h4 className="font-bold text-on-surface text-sm truncate">{item.label}</h4>
  187. <p className="text-xs text-outline font-mono truncate">{item.event}</p>
  188. </div>
  189. <div className="text-primary font-black text-xl">{item.count}</div>
  190. </div>
  191. </div>
  192. ))}
  193. {eventCounts.length === 0 && (
  194. <div className="text-sm text-secondary p-4 rounded-2xl bg-surface/70 border border-outline-variant/30">
  195. 当前时间范围内暂无 D4 事件。
  196. </div>
  197. )}
  198. </div>
  199. {d4Metrics.data && (
  200. <div className="mt-6 pt-5 border-t border-outline-variant/40 text-xs text-secondary space-y-1">
  201. <p>索引:{d4Metrics.data.scan.indexPattern}</p>
  202. <p>扫描事件:{d4Metrics.data.scan.scannedEvents} / {d4Metrics.data.scan.maxEventsToScan}</p>
  203. <p>范围:{new Date(d4Metrics.data.from).toLocaleString()} - {new Date(d4Metrics.data.to).toLocaleString()}</p>
  204. </div>
  205. )}
  206. <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">
  207. 查看能力分析热力图 →
  208. </Link>
  209. </div>
  210. </div>
  211. <div className="h-10"></div>
  212. </div>
  213. );
  214. }