| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- // ============================================================================
- // 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<string, number>;
- 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<string, { tier: string; count: number; percent: string }[]>;
- }
- 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<string, string[]> = {
- 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<string, number> = { 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<string, User> {
- 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<AuthResponse> => {
- 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<User | null> => {
- 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<MockStudent[]> => {
- await delay(400);
- return initMockData();
- },
- getStudentById: async (id: string): Promise<MockStudent | undefined> => {
- 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<MetricScores | null> => {
- await delay(200);
- const student = initMockData().find(s => s.id === studentId);
- return student?.metricScores ?? null;
- },
- // ── Class Statistics (Teacher Dashboard) ────────────────────────────────────
- getClassStats: async (): Promise<ClassStats> => {
- 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<LeaderboardEntry[]> => {
- 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<MockStudent> => {
- 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);
- };
|