mockApi.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. // ============================================================================
  2. // Mock API Layer — Single Source of Truth with localStorage persistence
  3. // All scores (macro + sub-metric) are pre-generated and stored deterministically
  4. // ============================================================================
  5. export interface User {
  6. id: string;
  7. username: string;
  8. role: 'stu' | 'teacher';
  9. name: string;
  10. avatar: string;
  11. }
  12. export interface AuthResponse {
  13. token: string;
  14. user: User;
  15. }
  16. // Per-metric score record: metricId → score (0-100)
  17. export type MetricScores = Record<string, number>;
  18. export interface MockStudent {
  19. id: string;
  20. name: string;
  21. overall: number;
  22. K: number;
  23. A: number;
  24. S: number;
  25. D: number;
  26. avatar: string;
  27. // Pre-generated sub-metric scores, e.g. { "K1": 88, "K2": 82, "K3": 90, "A1": 85, ... }
  28. metricScores: MetricScores;
  29. }
  30. export interface ClassStats {
  31. averages: { K: number; A: number; S: number; D: number };
  32. distributions: Record<string, { tier: string; count: number; percent: string }[]>;
  33. }
  34. export interface LeaderboardEntry {
  35. id: string;
  36. name: string;
  37. score: number;
  38. avatar: string;
  39. bestAt: string;
  40. }
  41. // ── All metric IDs grouped by dimension ────────────────────────────────────────
  42. // This is the structural truth — must match metricsData.ts
  43. const METRIC_IDS: Record<string, string[]> = {
  44. K: ['K1', 'K2', 'K3'],
  45. A: ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7'],
  46. S: ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7'],
  47. D: ['D1', 'D2', 'D3', 'D4'],
  48. };
  49. // ── localStorage Keys ──────────────────────────────────────────────────────────
  50. const STORAGE_KEY = 'edumetric_students';
  51. // ── Deterministic seeded RNG ───────────────────────────────────────────────────
  52. function createSeededRng(seed: number) {
  53. let s = seed;
  54. return () => {
  55. const x = Math.sin(s++) * 10000;
  56. return x - Math.floor(x);
  57. };
  58. }
  59. // ── Data Generation (runs once, then persisted) ────────────────────────────────
  60. function generateStudents(): MockStudent[] {
  61. return Array.from({ length: 40 }).map((_, i) => {
  62. const isExcellent = i % 5 === 0;
  63. const isStruggling = i % 7 === 0;
  64. let baseScore = 80;
  65. if (isExcellent) baseScore = 92;
  66. else if (isStruggling) baseScore = 65;
  67. else baseScore = 75 + (i % 15);
  68. const majorCode = i % 2 === 0 ? '125' : '188';
  69. const indexCode = (i + 1).toString().padStart(3, '0');
  70. const studentId = `23${majorCode}0${indexCode}`;
  71. // Macro dimension scores
  72. const K = baseScore + (i % 4) - 2;
  73. const A = baseScore + (i % 5) - 1;
  74. const S = baseScore - (i % 3);
  75. const D = baseScore + (i % 6);
  76. const macros: Record<string, number> = { K, A, S, D };
  77. // Generate deterministic sub-metric scores per student
  78. const rng = createSeededRng(i * 1000 + 42);
  79. const metricScores: MetricScores = {};
  80. for (const [dim, ids] of Object.entries(METRIC_IDS)) {
  81. const dimScore = macros[dim];
  82. for (const metricId of ids) {
  83. const offset = Math.floor(rng() * 9) - 4;
  84. metricScores[metricId] = Math.max(0, Math.min(100, dimScore + offset));
  85. }
  86. }
  87. const surnames = [
  88. '李', '王', '赵', '陈', '刘', '张', '周', '吴', '黄', '孙',
  89. '徐', '马', '朱', '胡', '林', '郭', '何', '高', '罗', '郑',
  90. '梁', '谢', '宋', '唐', '韩', '冯', '董', '程', '蔡', '袁',
  91. '许', '叶', '余', '彭', '苏', '潘', '杜', '曹', '戴', '魏',
  92. ];
  93. const givenNames = [
  94. '子涵', '佳怡', '宇航', '梓轩', '星雨', '梦瑶', '浩然', '思源', '雨萱', '天翔',
  95. '若琳', '明哲', '语嫣', '博文', '诗涵', '晨阳', '雅静', '俊杰', '欣怡', '泽宇',
  96. '芷若', '翰林', '婉清', '凌峰', '乐天', '安琪', '鸿飞', '嘉懿', '瑾瑜', '睿渊',
  97. '文昊', '修洁', '黎昕', '烨磊', '晟睿', '靖琪', '致远', '逸飞', '昊然', '皓轩',
  98. ];
  99. return {
  100. id: studentId,
  101. name: surnames[i] + givenNames[i],
  102. overall: baseScore,
  103. K, A, S, D,
  104. avatar: '/images/profile-avatar.png',
  105. metricScores,
  106. };
  107. });
  108. }
  109. // ── Init / Read from localStorage ──────────────────────────────────────────────
  110. function initMockData(): MockStudent[] {
  111. if (typeof window === 'undefined') {
  112. return generateStudents();
  113. }
  114. const stored = localStorage.getItem(STORAGE_KEY);
  115. if (stored) {
  116. try {
  117. const parsed = JSON.parse(stored) as MockStudent[];
  118. // Validate that sub-metric scores exist (handle old format)
  119. if (parsed.length > 0 && parsed[0].metricScores) {
  120. return parsed;
  121. }
  122. } catch {
  123. // Corrupted — regenerate
  124. }
  125. }
  126. const data = generateStudents();
  127. localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
  128. return data;
  129. }
  130. // ── User Accounts ──────────────────────────────────────────────────────────────
  131. function getUsers(): Record<string, User> {
  132. const students = initMockData();
  133. return {
  134. stu: {
  135. id: students[0].id,
  136. username: 'stu',
  137. role: 'stu',
  138. name: students[0].name,
  139. avatar: students[0].avatar,
  140. },
  141. teacher: {
  142. id: 'tea_001',
  143. username: 'teacher',
  144. role: 'teacher',
  145. name: '刘钦',
  146. avatar: '/images/profile-avatar.png',
  147. },
  148. };
  149. }
  150. // ── Helpers ─────────────────────────────────────────────────────────────────────
  151. const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
  152. // ── Public Mock API ────────────────────────────────────────────────────────────
  153. export const mockApi = {
  154. // ── Auth ────────────────────────────────────────────────────────────────────
  155. login: async (username: string, password: string): Promise<AuthResponse> => {
  156. await delay(600);
  157. if (password !== 'passwd') throw new Error('密码错误');
  158. const users = getUsers();
  159. const user = users[username];
  160. if (!user) throw new Error('用户不存在');
  161. return {
  162. token: `mock_jwt_token_${user.id}_${Date.now()}`,
  163. user,
  164. };
  165. },
  166. verifySession: async (token: string | null): Promise<User | null> => {
  167. await delay(300);
  168. if (!token) return null;
  169. const users = getUsers();
  170. if (token.includes(users.stu.id)) return users.stu;
  171. if (token.includes('tea_001')) return users.teacher;
  172. return null;
  173. },
  174. // ── Students ────────────────────────────────────────────────────────────────
  175. getStudents: async (): Promise<MockStudent[]> => {
  176. await delay(400);
  177. return initMockData();
  178. },
  179. getStudentById: async (id: string): Promise<MockStudent | undefined> => {
  180. await delay(200);
  181. return initMockData().find(s => s.id === id);
  182. },
  183. // ── Per-Student Metric Scores ───────────────────────────────────────────────
  184. // Returns the pre-generated sub-metric scores for a specific student
  185. getStudentMetrics: async (studentId: string): Promise<MetricScores | null> => {
  186. await delay(200);
  187. const student = initMockData().find(s => s.id === studentId);
  188. return student?.metricScores ?? null;
  189. },
  190. // ── Class Statistics (Teacher Dashboard) ────────────────────────────────────
  191. getClassStats: async (): Promise<ClassStats> => {
  192. await delay(500);
  193. const students = initMockData();
  194. const total = students.length || 1;
  195. const sums = students.reduce(
  196. (acc, stu) => {
  197. acc.K += stu.K;
  198. acc.A += stu.A;
  199. acc.S += stu.S;
  200. acc.D += stu.D;
  201. return acc;
  202. },
  203. { K: 0, A: 0, S: 0, D: 0 },
  204. );
  205. const averages = {
  206. K: Math.round(sums.K / total),
  207. A: Math.round(sums.A / total),
  208. S: Math.round(sums.S / total),
  209. D: Math.round(sums.D / total),
  210. };
  211. const bucket = (key: 'K' | 'A' | 'S' | 'D') => {
  212. let excellent = 0, good = 0, fair = 0, poor = 0;
  213. students.forEach(stu => {
  214. const score = stu[key];
  215. if (score >= 90) excellent++;
  216. else if (score >= 80) good++;
  217. else if (score >= 70) fair++;
  218. else poor++;
  219. });
  220. return [
  221. { tier: '优秀 (90+)', count: excellent, percent: `${Math.round((excellent / total) * 100)}%` },
  222. { tier: '良好 (80-89)', count: good, percent: `${Math.round((good / total) * 100)}%` },
  223. { tier: '一般 (70-79)', count: fair, percent: `${Math.round((fair / total) * 100)}%` },
  224. { tier: '需改进 (<70)', count: poor, percent: `${Math.round((poor / total) * 100)}%` },
  225. ];
  226. };
  227. const distributions: ClassStats['distributions'] = {
  228. K: bucket('K'),
  229. A: bucket('A'),
  230. S: bucket('S'),
  231. D: bucket('D'),
  232. };
  233. return { averages, distributions };
  234. },
  235. // ── Leaderboard ─────────────────────────────────────────────────────────────
  236. getLeaderboard: async (limit = 5): Promise<LeaderboardEntry[]> => {
  237. await delay(300);
  238. const students = initMockData();
  239. return [...students]
  240. .sort((a, b) => b.overall - a.overall)
  241. .slice(0, limit)
  242. .map(stu => {
  243. const scores = [
  244. { k: 'K (知识覆盖)', v: stu.K },
  245. { k: 'A (AI交互)', v: stu.A },
  246. { k: 'S (代码产出)', v: stu.S },
  247. { k: 'D (团队协作)', v: stu.D },
  248. ];
  249. scores.sort((a, b) => b.v - a.v);
  250. return {
  251. id: stu.id,
  252. name: stu.name,
  253. score: stu.overall,
  254. avatar: stu.avatar,
  255. bestAt: scores[0].k,
  256. };
  257. });
  258. },
  259. // ── Student Self Profile ────────────────────────────────────────────────────
  260. getMyProfile: async (): Promise<MockStudent> => {
  261. await delay(200);
  262. return initMockData()[0];
  263. },
  264. };
  265. // ── Sync helpers (for components that can't use async easily) ─────────────────
  266. export const getMockStudentById = (id: string): MockStudent | undefined => {
  267. return initMockData().find(s => s.id === id);
  268. };