1
0

MetricsContent.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. "use client";
  2. import {
  3. TrendingUp,
  4. Download,
  5. Info,
  6. CheckCircle2,
  7. AlertTriangle,
  8. User,
  9. } from "lucide-react";
  10. import { useSearchParams } from "next/navigation";
  11. import { metricsData } from "./metricsData";
  12. import { getMockStudentById } from "@/lib/mockApi";
  13. import { useAuth } from "@/components/AuthProvider";
  14. export default function MetricsContent({ metricId }: { metricId: string }) {
  15. const searchParams = useSearchParams();
  16. const { user } = useAuth();
  17. const studentId = searchParams.get("student") || (user?.role === 'stu' ? user.id : null);
  18. const student = studentId ? getMockStudentById(studentId) : null;
  19. const categoryStr = metricId.charAt(0).toUpperCase();
  20. const metricCategoryList = metricsData[categoryStr] || [];
  21. const metricObj = metricCategoryList.find(m => m.id === metricId.toUpperCase());
  22. if (!metricObj) {
  23. return (
  24. <div className="flex-grow flex items-center justify-center p-8">
  25. <h2 className="text-xl text-secondary">未找到指标 {metricId.toUpperCase()} 的详情</h2>
  26. </div>
  27. );
  28. }
  29. // When viewing a specific student, use their pre-generated score for this metric.
  30. // Otherwise use the static class average from metricsData.
  31. let scoreNum = parseFloat(metricObj.avgScore) || 85.0;
  32. if (student) {
  33. const storedScore = student.metricScores?.[metricId.toUpperCase()];
  34. if (storedScore !== undefined) {
  35. scoreNum = storedScore;
  36. }
  37. }
  38. return (
  39. <main className="flex-grow p-4 md:p-8 max-w-7xl mx-auto w-full grid grid-cols-1 lg:grid-cols-12 gap-6">
  40. <aside className="lg:col-span-4 xl:col-span-3 space-y-6">
  41. <div className="bg-surface rounded-2xl p-6 shadow-elevation-1 border border-outline-variant">
  42. <div className="flex items-center gap-2 mb-4">
  43. <span className={`w-2 h-8 rounded-full bg-${metricObj.colorClass}`}></span>
  44. <h2 className="text-lg font-medium text-on-surface">
  45. {metricObj.id} - {metricObj.name}
  46. </h2>
  47. </div>
  48. <div className="flex flex-col items-center justify-center py-8 relative">
  49. {/* Ambient Background Glow */}
  50. <div
  51. className="absolute w-40 h-40 rounded-full opacity-20 blur-2xl"
  52. style={{ backgroundColor: `var(--color-${metricObj.colorClass})` }}
  53. ></div>
  54. <div className="relative w-40 h-40 flex items-center justify-center rounded-full bg-surface shadow-elevation-2 border border-outline-variant/30">
  55. <svg
  56. className="absolute w-full h-full transform -rotate-90 drop-shadow-md"
  57. viewBox="0 0 100 100"
  58. >
  59. {/* Track Circle */}
  60. <circle
  61. cx="50"
  62. cy="50"
  63. r="44"
  64. fill="transparent"
  65. className="stroke-surface-variant flex-shrink-0"
  66. strokeWidth="6"
  67. />
  68. {/* Progress Circle */}
  69. <circle
  70. cx="50"
  71. cy="50"
  72. fill="transparent"
  73. r="44"
  74. style={{ stroke: `var(--color-${metricObj.colorClass})` }}
  75. strokeDasharray={`${(scoreNum / 100) * 276.46} 276.46`}
  76. strokeDashoffset="0"
  77. strokeLinecap="round"
  78. strokeWidth="8"
  79. className="transition-all duration-1000 ease-out"
  80. ></circle>
  81. </svg>
  82. <div className="text-center z-10 flex flex-col items-center justify-center bg-surface w-28 h-28 rounded-full shadow-inner border border-outline-variant/10">
  83. <span
  84. className="text-4xl font-display font-bold tracking-tight"
  85. style={{ color: `var(--color-${metricObj.colorClass})` }}
  86. >
  87. {scoreNum}
  88. </span>
  89. <span className="block text-[0.65rem] text-on-surface-variant uppercase tracking-widest mt-1 font-medium">
  90. 得分
  91. </span>
  92. </div>
  93. </div>
  94. </div>
  95. <div className="space-y-4 mt-2">
  96. <div className="flex justify-between items-center text-sm">
  97. <span className="text-on-surface-variant">
  98. 趋势 (对比上月)
  99. </span>
  100. <span className="flex items-center text-metric-high font-medium bg-metric-high/10 px-2 py-0.5 rounded-md">
  101. <TrendingUp className="w-4 h-4 mr-1" /> +2.1%
  102. </span>
  103. </div>
  104. </div>
  105. <div className="mt-6 pt-6 border-t border-outline-variant">
  106. <button className="w-full bg-primary text-on-primary h-10 rounded-full font-medium text-sm shadow-md hover:shadow-lg transition-all flex items-center justify-center gap-2 cursor-pointer">
  107. <Download className="w-5 h-5" /> 导出报告
  108. </button>
  109. </div>
  110. </div>
  111. <div className={`${metricObj.bgClass} rounded-2xl p-6`}>
  112. <h3 className="font-medium mb-2 flex items-center gap-2">
  113. <Info className="w-5 h-5" /> 指标说明
  114. </h3>
  115. <p className="text-sm opacity-90 leading-relaxed font-light">
  116. {metricObj.description}
  117. </p>
  118. </div>
  119. </aside>
  120. <section className="lg:col-span-8 xl:col-span-9 space-y-6">
  121. <div className="flex flex-col md:flex-row md:items-center justify-between mb-2">
  122. <div>
  123. <h2 className="text-2xl font-medium text-on-background">
  124. 子指标分析
  125. </h2>
  126. <p className="text-on-surface-variant text-sm mt-1">
  127. 关于 {metricObj.id} 的详细数据维度拆解
  128. </p>
  129. </div>
  130. <div className="flex gap-2 mt-4 md:mt-0">
  131. <button className="h-10 px-4 rounded-full border border-outline text-on-surface-variant text-sm font-medium hover:bg-surface-variant hover:text-on-surface focus:ring-2 focus:ring-primary focus:outline-none transition-colors cursor-pointer">
  132. 筛选
  133. </button>
  134. <button className="h-10 px-4 rounded-full border border-outline text-on-surface-variant text-sm font-medium hover:bg-surface-variant hover:text-on-surface focus:ring-2 focus:ring-primary focus:outline-none transition-colors cursor-pointer">
  135. 日期范围
  136. </button>
  137. </div>
  138. </div>
  139. <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
  140. {metricObj.subMetrics.map((sub, i) => {
  141. // Generating some mock variation to make UI look realistic
  142. const subScore = Math.min(100, scoreNum + ((i % 3 === 0 ? 1 : -1) * (i * 2 + 3)));
  143. const isWarn = subScore < 80;
  144. return (
  145. <article key={sub.id} className="bg-surface rounded-xl p-0 shadow-elevation-1 border border-outline-variant overflow-hidden flex flex-col h-full hover:shadow-elevation-2 hover:bg-surface-container transition-all duration-300 group">
  146. <div className="p-5 border-b border-outline-variant bg-surface-variant/20">
  147. <div className="flex justify-between items-start">
  148. <div>
  149. <span className={`text-xs font-bold tracking-wider uppercase text-${metricObj.colorClass}`}>
  150. {sub.id}
  151. </span>
  152. <h3 className="text-lg font-medium text-on-surface mt-1">
  153. {sub.name}
  154. </h3>
  155. </div>
  156. <span className={`w-8 h-8 rounded-full flex items-center justify-center ${isWarn ? 'bg-metric-warn/10 text-metric-warn' : 'bg-metric-high/10 text-metric-high'}`}>
  157. {isWarn ? <AlertTriangle className="w-5 h-5" /> : <CheckCircle2 className="w-5 h-5" />}
  158. </span>
  159. </div>
  160. </div>
  161. <div className="p-5 flex-grow space-y-5">
  162. {sub.values.map((val, idx) => (
  163. <div key={idx} className="flex flex-col gap-1">
  164. <div className="flex items-center justify-between">
  165. <div className="flex items-start gap-3">
  166. <CheckCircle2 className="text-on-surface-variant w-5 h-5 mt-0.5" />
  167. <p className="text-sm font-medium text-on-surface leading-tight">
  168. {val.name}
  169. </p>
  170. </div>
  171. <div className="text-right whitespace-nowrap ml-2">
  172. <span className={`block text-xl font-bold ${isWarn ? 'text-metric-warn' : 'text-metric-high'}`}>
  173. {idx === 0 ? subScore + '%' : (isWarn ? '一般' : '优秀')}
  174. </span>
  175. </div>
  176. </div>
  177. </div>
  178. ))}
  179. <div className="w-full bg-surface-variant rounded-full h-1.5 mt-2">
  180. <div
  181. className={`h-1.5 rounded-full ${isWarn ? 'bg-metric-warn' : 'bg-metric-high'}`}
  182. style={{ width: `${subScore}%` }}
  183. ></div>
  184. </div>
  185. </div>
  186. <div className="px-5 py-3 bg-surface-variant/10 flex justify-end border-t border-outline-variant/50">
  187. <button className="text-primary text-sm font-medium hover:bg-primary/10 px-3 py-1.5 rounded-lg transition-colors cursor-pointer">
  188. 查看详情
  189. </button>
  190. </div>
  191. </article>
  192. );
  193. })}
  194. </div>
  195. </section>
  196. </main>
  197. );
  198. }