// ============================================================================ // Mock API Layer — Single Source of Truth with localStorage persistence // All scores (macro + sub-metric) are pre-generated and stored deterministically // ============================================================================ export interface User { id: string; username: string; role: 'stu' | 'teacher'; name: string; avatar: string; } export interface AuthResponse { token: string; user: User; } // Per-metric score record: metricId → score (0-100) export type MetricScores = Record; export interface MockStudent { id: string; name: string; overall: number; K: number; A: number; S: number; D: number; avatar: string; // Pre-generated sub-metric scores, e.g. { "K1": 88, "K2": 82, "K3": 90, "A1": 85, ... } metricScores: MetricScores; } export interface ClassStats { averages: { K: number; A: number; S: number; D: number }; distributions: Record; } export interface LeaderboardEntry { id: string; name: string; score: number; avatar: string; bestAt: string; } // ── All metric IDs grouped by dimension ──────────────────────────────────────── // This is the structural truth — must match metricsData.ts const METRIC_IDS: Record = { K: ['K1', 'K2', 'K3'], A: ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7'], S: ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7'], D: ['D1', 'D2', 'D3', 'D4'], }; // ── localStorage Keys ────────────────────────────────────────────────────────── const STORAGE_KEY = 'edumetric_students'; // ── Deterministic seeded RNG ─────────────────────────────────────────────────── function createSeededRng(seed: number) { let s = seed; return () => { const x = Math.sin(s++) * 10000; return x - Math.floor(x); }; } // ── Data Generation (runs once, then persisted) ──────────────────────────────── function generateStudents(): MockStudent[] { return Array.from({ length: 40 }).map((_, i) => { const isExcellent = i % 5 === 0; const isStruggling = i % 7 === 0; let baseScore = 80; if (isExcellent) baseScore = 92; else if (isStruggling) baseScore = 65; else baseScore = 75 + (i % 15); const majorCode = i % 2 === 0 ? '125' : '188'; const indexCode = (i + 1).toString().padStart(3, '0'); const studentId = `23${majorCode}0${indexCode}`; // Macro dimension scores const K = baseScore + (i % 4) - 2; const A = baseScore + (i % 5) - 1; const S = baseScore - (i % 3); const D = baseScore + (i % 6); const macros: Record = { K, A, S, D }; // Generate deterministic sub-metric scores per student const rng = createSeededRng(i * 1000 + 42); const metricScores: MetricScores = {}; for (const [dim, ids] of Object.entries(METRIC_IDS)) { const dimScore = macros[dim]; for (const metricId of ids) { const offset = Math.floor(rng() * 9) - 4; metricScores[metricId] = Math.max(0, Math.min(100, dimScore + offset)); } } const surnames = [ '李', '王', '赵', '陈', '刘', '张', '周', '吴', '黄', '孙', '徐', '马', '朱', '胡', '林', '郭', '何', '高', '罗', '郑', '梁', '谢', '宋', '唐', '韩', '冯', '董', '程', '蔡', '袁', '许', '叶', '余', '彭', '苏', '潘', '杜', '曹', '戴', '魏', ]; const givenNames = [ '子涵', '佳怡', '宇航', '梓轩', '星雨', '梦瑶', '浩然', '思源', '雨萱', '天翔', '若琳', '明哲', '语嫣', '博文', '诗涵', '晨阳', '雅静', '俊杰', '欣怡', '泽宇', '芷若', '翰林', '婉清', '凌峰', '乐天', '安琪', '鸿飞', '嘉懿', '瑾瑜', '睿渊', '文昊', '修洁', '黎昕', '烨磊', '晟睿', '靖琪', '致远', '逸飞', '昊然', '皓轩', ]; return { id: studentId, name: surnames[i] + givenNames[i], overall: baseScore, K, A, S, D, avatar: '/images/profile-avatar.png', metricScores, }; }); } // ── Init / Read from localStorage ────────────────────────────────────────────── function initMockData(): MockStudent[] { if (typeof window === 'undefined') { return generateStudents(); } const stored = localStorage.getItem(STORAGE_KEY); if (stored) { try { const parsed = JSON.parse(stored) as MockStudent[]; // Validate that sub-metric scores exist (handle old format) if (parsed.length > 0 && parsed[0].metricScores) { return parsed; } } catch { // Corrupted — regenerate } } const data = generateStudents(); localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); return data; } // ── User Accounts ────────────────────────────────────────────────────────────── function getUsers(): Record { const students = initMockData(); return { stu: { id: students[0].id, username: 'stu', role: 'stu', name: students[0].name, avatar: students[0].avatar, }, teacher: { id: 'tea_001', username: 'teacher', role: 'teacher', name: '刘钦', avatar: '/images/profile-avatar.png', }, }; } // ── Helpers ───────────────────────────────────────────────────────────────────── const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // ── Public Mock API ──────────────────────────────────────────────────────────── export const mockApi = { // ── Auth ──────────────────────────────────────────────────────────────────── login: async (username: string, password: string): Promise => { await delay(600); if (password !== 'passwd') throw new Error('密码错误'); const users = getUsers(); const user = users[username]; if (!user) throw new Error('用户不存在'); return { token: `mock_jwt_token_${user.id}_${Date.now()}`, user, }; }, verifySession: async (token: string | null): Promise => { await delay(300); if (!token) return null; const users = getUsers(); if (token.includes(users.stu.id)) return users.stu; if (token.includes('tea_001')) return users.teacher; return null; }, // ── Students ──────────────────────────────────────────────────────────────── getStudents: async (): Promise => { await delay(400); return initMockData(); }, getStudentById: async (id: string): Promise => { await delay(200); return initMockData().find(s => s.id === id); }, // ── Per-Student Metric Scores ─────────────────────────────────────────────── // Returns the pre-generated sub-metric scores for a specific student getStudentMetrics: async (studentId: string): Promise => { await delay(200); const student = initMockData().find(s => s.id === studentId); return student?.metricScores ?? null; }, // ── Class Statistics (Teacher Dashboard) ──────────────────────────────────── getClassStats: async (): Promise => { await delay(500); const students = initMockData(); const total = students.length || 1; const sums = students.reduce( (acc, stu) => { acc.K += stu.K; acc.A += stu.A; acc.S += stu.S; acc.D += stu.D; return acc; }, { K: 0, A: 0, S: 0, D: 0 }, ); const averages = { K: Math.round(sums.K / total), A: Math.round(sums.A / total), S: Math.round(sums.S / total), D: Math.round(sums.D / total), }; const bucket = (key: 'K' | 'A' | 'S' | 'D') => { let excellent = 0, good = 0, fair = 0, poor = 0; students.forEach(stu => { const score = stu[key]; if (score >= 90) excellent++; else if (score >= 80) good++; else if (score >= 70) fair++; else poor++; }); return [ { tier: '优秀 (90+)', count: excellent, percent: `${Math.round((excellent / total) * 100)}%` }, { tier: '良好 (80-89)', count: good, percent: `${Math.round((good / total) * 100)}%` }, { tier: '一般 (70-79)', count: fair, percent: `${Math.round((fair / total) * 100)}%` }, { tier: '需改进 (<70)', count: poor, percent: `${Math.round((poor / total) * 100)}%` }, ]; }; const distributions: ClassStats['distributions'] = { K: bucket('K'), A: bucket('A'), S: bucket('S'), D: bucket('D'), }; return { averages, distributions }; }, // ── Leaderboard ───────────────────────────────────────────────────────────── getLeaderboard: async (limit = 5): Promise => { await delay(300); const students = initMockData(); return [...students] .sort((a, b) => b.overall - a.overall) .slice(0, limit) .map(stu => { const scores = [ { k: 'K (知识覆盖)', v: stu.K }, { k: 'A (AI交互)', v: stu.A }, { k: 'S (代码产出)', v: stu.S }, { k: 'D (团队协作)', v: stu.D }, ]; scores.sort((a, b) => b.v - a.v); return { id: stu.id, name: stu.name, score: stu.overall, avatar: stu.avatar, bestAt: scores[0].k, }; }); }, // ── Student Self Profile ──────────────────────────────────────────────────── getMyProfile: async (): Promise => { await delay(200); return initMockData()[0]; }, }; // ── Sync helpers (for components that can't use async easily) ───────────────── export const getMockStudentById = (id: string): MockStudent | undefined => { return initMockData().find(s => s.id === id); };