| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- "use client";
- 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 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">正在加载 D4 教学大盘数据...</span>
- </div>
- );
- }
- 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">
- <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" />
- 导出班级报告
- </button>
- </div>
- </div>
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
- {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">
- <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">
- <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 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 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" />
- 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>
- );
- }
|