Kaynağa Gözat

解决多处问题

yemeishu 1 ay önce
ebeveyn
işleme
cb657b5688
30 değiştirilmiş dosya ile 3696 ekleme ve 1023 silme
  1. 10 0
      app/Filament/Auth/Pages/CustomLogin.php
  2. 1165 1
      app/Filament/Pages/StudentDashboard.php
  3. 13 0
      app/Livewire/ClassAnalytics.php
  4. 13 0
      app/Livewire/LearningPath.php
  5. 52 162
      app/Livewire/MasteryHeatmap.php
  6. 76 0
      app/Livewire/StudentAnalytics.php
  7. 129 0
      app/Livewire/TeacherDashboard.php
  8. 29 0
      app/Models/KnowledgePoint.php
  9. 30 0
      app/Models/Skill.php
  10. 41 0
      app/Models/StudentExercise.php
  11. 3 1
      app/Providers/Filament/AdminPanelProvider.php
  12. 504 481
      app/Services/LearningAnalyticsService.php
  13. 15 7
      config/database.php
  14. 28 0
      database/migrations/2025_11_18_073805_add_updated_at_to_student_exercises_table.php
  15. 39 0
      database/migrations/2025_11_18_073929_rename_fields_in_student_exercises_table.php
  16. 30 0
      database/migrations/2025_11_18_130425_update_difficulty_level_column_in_student_exercises_table.php
  17. 136 0
      generate_learning_data.php
  18. 86 0
      package-lock.json
  19. 1 0
      package.json
  20. 3 3
      resources/css/app.css
  21. 714 293
      resources/views/filament/pages/student-dashboard.blade.php
  22. 3 0
      resources/views/livewire/class-analytics.blade.php
  23. 3 0
      resources/views/livewire/learning-path.blade.php
  24. 81 71
      resources/views/livewire/mastery-heatmap.blade.php
  25. 137 0
      resources/views/livewire/student-analytics.blade.php
  26. 171 0
      resources/views/livewire/teacher-dashboard.blade.php
  27. 72 0
      resources/views/vendor/filament/auth/pages/login.blade.php
  28. 65 0
      resources/views/vendor/filament/login.blade.php
  29. 0 4
      resources/views/welcome.blade.php
  30. 47 0
      vite.config.js

+ 10 - 0
app/Filament/Auth/Pages/CustomLogin.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Filament\Auth\Pages;
+
+use Filament\Auth\Pages\Login as BaseLogin;
+
+class CustomLogin extends BaseLogin
+{
+    // 继承 BaseLogin 的所有功能
+}

+ 1165 - 1
app/Filament/Pages/StudentDashboard.php

@@ -6,6 +6,8 @@ use App\Services\LearningAnalyticsService;
 use BackedEnum;
 use Filament\Pages\Page;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 use UnitEnum;
 use Livewire\Attributes\Layout;
@@ -28,14 +30,161 @@ class StudentDashboard extends Page
     protected string $view = 'filament.pages.student-dashboard';
 
     public string $studentId = '';
+    public string $teacherId = '';
     public array $dashboardData = [];
     public bool $isLoading = false;
     public string $errorMessage = '';
+    public array $teachers = [];
+    public array $students = [];
+
+    // 批量答题相关
+    public array $exerciseQuestions = [];
+    public array $exerciseAnswers = [];
+    public ?string $selectedKnowledgePoint = null;
+    public array $availableKnowledgePoints = [];
+    public array $availableSkills = [];
+    public array $selectedSkills = [];
+    public string $currentBatchId = '';
+    public int $questionsPerSet = 5;
+    protected array $knowledgePointCodeIndex = [];
+    protected array $knowledgePointIdIndex = [];
 
     public function mount(Request $request): void
     {
+        // 加载老师列表
+        $this->loadTeachers();
+
+        // 从请求中获取老师ID或使用默认值
+        $this->teacherId = $request->input('teacher_id', $this->getDefaultTeacherId());
+
+        // 根据老师ID加载学生列表
+        $this->loadStudentsByTeacher();
+
         // 从请求中获取学生ID或使用默认值
-        $this->studentId = $request->input('student_id', 'student_001');
+        $this->studentId = $request->input('student_id', $this->getDefaultStudentId());
+
+        // 初始化知识点和技能数据
+        $this->loadKnowledgePointsAndSkills();
+    }
+
+    /**
+     * 获取默认老师ID(列表中的第一个老师)
+     */
+    private function getDefaultTeacherId(): string
+    {
+        return !empty($this->teachers) ? $this->teachers[0]->teacher_id : '';
+    }
+
+    /**
+     * 获取默认学生ID(列表中的第一个学生)
+     */
+    private function getDefaultStudentId(): string
+    {
+        return !empty($this->students) ? $this->students[0]->student_id : '';
+    }
+
+    /**
+     * 从MySQL加载老师列表
+     */
+    public function loadTeachers(): void
+    {
+        try {
+            // 首先获取teachers表中的老师
+            $this->teachers = DB::connection('remote_mysql')
+                ->table('teachers as t')
+                ->leftJoin('users as u', 't.teacher_id', '=', 'u.user_id')
+                ->select(
+                    't.teacher_id',
+                    't.name',
+                    't.subject',
+                    'u.username',
+                    'u.email'
+                )
+                ->orderBy('t.name')
+                ->get()
+                ->toArray();
+
+            // 如果有学生但没有对应的老师记录,添加一个"未知老师"条目
+            $teacherIds = array_column($this->teachers, 'teacher_id');
+            $missingTeacherIds = DB::connection('remote_mysql')
+                ->table('students as s')
+                ->distinct()
+                ->whereNotIn('s.teacher_id', $teacherIds)
+                ->pluck('teacher_id')
+                ->toArray();
+
+            if (!empty($missingTeacherIds)) {
+                foreach ($missingTeacherIds as $missingId) {
+                    $this->teachers[] = (object) [
+                        'teacher_id' => $missingId,
+                        'name' => '未知老师 (' . $missingId . ')',
+                        'subject' => '未知',
+                        'username' => null,
+                        'email' => null
+                    ];
+                }
+
+                // 重新排序
+                usort($this->teachers, function($a, $b) {
+                    return strcmp($a->name, $b->name);
+                });
+            }
+        } catch (\Exception $e) {
+            Log::error('加载老师列表失败', [
+                'error' => $e->getMessage()
+            ]);
+            $this->teachers = [];
+        }
+    }
+
+    /**
+     * 根据老师ID加载学生列表
+     */
+    public function loadStudentsByTeacher(): void
+    {
+        try {
+            if (empty($this->teacherId)) {
+                $this->students = [];
+                return;
+            }
+
+            $this->students = DB::connection('remote_mysql')
+                ->table('students as s')
+                ->leftJoin('users as u', 's.student_id', '=', 'u.user_id')
+                ->where('s.teacher_id', $this->teacherId)
+                ->select(
+                    's.student_id',
+                    's.name',
+                    's.grade',
+                    's.class_name',
+                    'u.username',
+                    'u.email'
+                )
+                ->orderBy('s.name')
+                ->get()
+                ->toArray();
+        } catch (\Exception $e) {
+            Log::error('加载学生列表失败', [
+                'teacher_id' => $this->teacherId,
+                'error' => $e->getMessage()
+            ]);
+            $this->students = [];
+        }
+    }
+
+    /**
+     * 老师改变时重新加载学生列表
+     */
+    public function updatedTeacherId(): void
+    {
+        $this->loadStudentsByTeacher();
+        // 清空之前选中的学生ID
+        $this->studentId = '';
+        // 自动加载第一个学生的数据
+        $this->studentId = $this->getDefaultStudentId();
+        if (!empty($this->studentId)) {
+            $this->loadDashboardData();
+        }
     }
 
     public function loadDashboardData(): void
@@ -53,6 +202,8 @@ class StudentDashboard extends Page
                 return;
             }
 
+            Log::info('开始加载仪表板数据', ['student_id' => $this->studentId]);
+
             // 获取各项数据
             $masteryOverview = $service->getStudentMasteryOverview($this->studentId);
             $skillProficiency = $service->getStudentSkillProficiency($this->studentId);
@@ -62,6 +213,12 @@ class StudentDashboard extends Page
             $predictionAnalytics = $service->getPredictionAnalytics($this->studentId);
             $pathAnalytics = $service->getLearningPathAnalytics($this->studentId);
             $quickPrediction = $service->quickScorePrediction($this->studentId);
+
+            Log::info('快速预测结果', [
+                'student_id' => $this->studentId,
+                'quick_prediction' => $quickPrediction
+            ]);
+
             $recommendations = $service->recommendLearningPaths($this->studentId, 3);
 
             // 组合数据
@@ -86,6 +243,11 @@ class StudentDashboard extends Page
                 ],
             ];
 
+            Log::info('仪表板数据加载完成', [
+                'student_id' => $this->studentId,
+                'dashboard_data_keys' => array_keys($this->dashboardData)
+            ]);
+
         } catch (\Exception $e) {
             $this->errorMessage = '加载数据时发生错误:' . $e->getMessage();
             Log::error('学生仪表板数据加载失败', [
@@ -166,4 +328,1006 @@ class StudentDashboard extends Page
             $this->dispatch('notify', message: '操作失败:' . $e->getMessage(), type: 'danger');
         }
     }
+
+    /**
+     * 从题库API获取题目
+     */
+    private function fetchQuestionFromBank(): ?array
+    {
+        try {
+            $questionBankApiBase = config('services.question_bank_api.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015'));
+
+            // 调用题库API获取题目
+            $response = Http::timeout(10)
+                ->get($questionBankApiBase . '/questions', [
+                    'limit' => 1,
+                    'type' => 'factorization'
+                ]);
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $questions = $data['data'] ?? [];
+
+                if (!empty($questions)) {
+                    $question = $questions[0];
+                    $kpCode = $question['kp_code'] ?? null;
+                    $knowledgePointId = $question['knowledge_point_id'] ?? null;
+
+                    if ($kpCode && !$knowledgePointId) {
+                        $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
+                    }
+
+                    if (!$kpCode && $knowledgePointId) {
+                        $kpCode = $this->findKnowledgePointCodeById((string) $knowledgePointId);
+                    }
+
+                    if (!$knowledgePointId && $this->selectedKnowledgePoint) {
+                        $knowledgePointId = (string) $this->selectedKnowledgePoint;
+                    }
+
+                    if (!$kpCode && $this->selectedKnowledgePoint) {
+                        $kpCode = $this->findKnowledgePointCodeById((string) $this->selectedKnowledgePoint);
+                    }
+
+                    if (!$kpCode) {
+                        $kpCode = $this->getDefaultKnowledgePointMeta()['code'];
+                    }
+
+                    if (!$knowledgePointId && $kpCode) {
+                        $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
+                    }
+
+                    return [
+                        'id' => $question['question_code'] ?? 'Q_' . time(),
+                        'content' => $question['stem'] ?? '',
+                        'answer' => $question['solution'] ?? '',
+                        'type' => '因式分解',
+                        'difficulty' => $question['difficulty'] ?? rand(1, 5),
+                        'kp_code' => $kpCode ?? 'KP_UNKNOWN',
+                        'knowledge_point_id' => $knowledgePointId,
+                        'skill' => $question['skill'] ?? 'unknown'
+                    ];
+                }
+            }
+
+            // 如果API失败,返回模拟题目
+            return $this->generateMockQuestion();
+        } catch (\Exception $e) {
+            Log::warning('题库API调用失败,使用模拟题目', ['error' => $e->getMessage()]);
+            return $this->generateMockQuestion();
+        }
+    }
+
+    /**
+     * 生成模拟题目(备用方案)
+     */
+    private function generateMockQuestion(): array
+    {
+        // 如果用户选择了知识点,使用选中的知识点的kp_code
+        if ($this->selectedKnowledgePoint) {
+            $selectedKpCode = null;
+            foreach ($this->availableKnowledgePoints as $kp) {
+                if ($kp['id'] === $this->selectedKnowledgePoint) {
+                    $selectedKpCode = $kp['code'];
+                    break;
+                }
+            }
+
+            // 如果找到了选中的知识点的code,使用它
+            if ($selectedKpCode) {
+                $mockQuestions = [
+                    ['content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2],
+                    ['content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3],
+                    ['content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2],
+                    ['content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2],
+                    ['content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3],
+                ];
+                $question = $mockQuestions[array_rand($mockQuestions)];
+                $question['type'] = '因式分解';
+                $question['kp_code'] = $selectedKpCode;
+                $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint;
+                return $question;
+            }
+        }
+
+        // 如果没有选择知识点,使用默认的
+        $types = [
+            ['type' => '因式分解', 'kp_code' => 'KP7001', 'content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2],
+            ['type' => '因式分解', 'kp_code' => 'KP8001', 'content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3],
+            ['type' => '因式分解', 'kp_code' => 'KP8002', 'content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2],
+            ['type' => '因式分解', 'kp_code' => 'KP8003', 'content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2],
+            ['type' => '因式分解', 'kp_code' => 'KP8004', 'content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3],
+        ];
+
+        $question = $types[array_rand($types)];
+        $kpMeta = $this->getDefaultKnowledgePointMeta();
+
+        $question['kp_code'] = $question['kp_code'] ?? 'KP_UNKNOWN';
+        $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']) ?? $kpMeta['id'];
+
+        return $question;
+    }
+
+    /**
+     * 从题库API获取多道题目
+     */
+    private function fetchMultipleQuestionsFromBank(int $count): array
+    {
+        try {
+            $questionBankApiBase = config('services.question_bank_api.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015'));
+
+            // 调用题库API一次性获取所有题目
+            // 如果用户选择了知识点,传入知识点参数
+            $params = [
+                'limit' => $count,
+                'type' => 'factorization'
+            ];
+
+            // 如果用户选择了知识点,传入kp_code
+            if ($this->selectedKnowledgePoint) {
+                foreach ($this->availableKnowledgePoints as $kp) {
+                    if ($kp['id'] === $this->selectedKnowledgePoint) {
+                        $params['kp_code'] = $kp['code'];
+                        break;
+                    }
+                }
+            }
+
+            $response = Http::timeout(10)
+                ->get($questionBankApiBase . '/questions', $params);
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $questions = $data['data'] ?? [];
+
+                $processedQuestions = [];
+                foreach ($questions as $question) {
+                    $kpCode = $question['kp_code'] ?? null;
+                    $knowledgePointId = $question['knowledge_point_id'] ?? null;
+
+                    // 如果用户选择了知识点,优先使用选中的知识点
+                    if ($this->selectedKnowledgePoint) {
+                        // 查找选中的知识点的kp_code
+                        $selectedKpCode = null;
+                        foreach ($this->availableKnowledgePoints as $kp) {
+                            if ($kp['id'] === $this->selectedKnowledgePoint) {
+                                $selectedKpCode = $kp['code'];
+                                break;
+                            }
+                        }
+                        if ($selectedKpCode) {
+                            $kpCode = $selectedKpCode;
+                            $knowledgePointId = (string) $this->selectedKnowledgePoint;
+                        }
+                    } else {
+                        // 如果没有选择知识点,使用API返回的或查找
+                        if ($kpCode && !$knowledgePointId) {
+                            $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
+                        }
+
+                        if (!$kpCode && $knowledgePointId) {
+                            $kpCode = $this->findKnowledgePointCodeById((string) $knowledgePointId);
+                        }
+
+                        if (!$kpCode) {
+                            $kpCode = $this->getDefaultKnowledgePointMeta()['code'];
+                        }
+
+                        if (!$knowledgePointId && $kpCode) {
+                            $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
+                        }
+                    }
+
+                    $processedQuestions[] = [
+                        'id' => $question['question_code'] ?? 'Q_' . time() . '_' . uniqid(),
+                        'content' => $question['stem'] ?? '',
+                        'answer' => $question['solution'] ?? '',
+                        'type' => '因式分解',
+                        'difficulty' => $question['difficulty'] ?? rand(1, 5),
+                        'kp_code' => $kpCode ?? 'KP_UNKNOWN',
+                        'knowledge_point_id' => $knowledgePointId,
+                        'skill' => $question['skill'] ?? 'unknown'
+                    ];
+
+                    // 达到指定数量就停止处理
+                    if (count($processedQuestions) >= $count) {
+                        break;
+                    }
+                }
+
+                // 如果API返回的题目不足,补充模拟题目
+                while (count($processedQuestions) < $count) {
+                    $mockQuestion = $this->generateMockQuestion();
+                    // 重新生成唯一ID
+                    $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
+                    $processedQuestions[] = $mockQuestion;
+                }
+
+                return $processedQuestions;
+            }
+
+            // 如果API失败,返回模拟题目
+            $mockQuestions = [];
+            for ($i = 0; $i < $count; $i++) {
+                $mockQuestion = $this->generateMockQuestion();
+                $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
+                $mockQuestions[] = $mockQuestion;
+            }
+            return $mockQuestions;
+
+        } catch (\Exception $e) {
+            Log::warning('题库API调用失败,使用模拟题目', ['error' => $e->getMessage()]);
+            $mockQuestions = [];
+            for ($i = 0; $i < $count; $i++) {
+                $mockQuestion = $this->generateMockQuestion();
+                $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
+                $mockQuestions[] = $mockQuestion;
+            }
+            return $mockQuestions;
+        }
+    }
+
+    /**
+     * 根据答题结果更新掌握度(通过LearningAnalytics API)
+     */
+    private function updateMasteryFromAnswer(array $question, bool $isCorrect): void
+    {
+        try {
+            $learningAnalytics = new LearningAnalyticsService();
+            $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($question['knowledge_point_id'] ?? null);
+
+            $attemptData = [
+                'kp_code' => $kpCode ?? 'KP_UNKNOWN',
+                'is_correct' => $isCorrect,
+                'time_spent_seconds' => rand(60, 180),
+                'difficulty_level' => (string)($question['difficulty'] ?? '3'),
+                'question_id' => 'Q_' . time(),
+                'student_answer' => $this->userAnswer ?: '',
+                'correct_answer' => $question['answer'] ?? '',
+            ];
+
+            if (!empty($question['knowledge_point_id'])) {
+                $attemptData['knowledge_point_id'] = $question['knowledge_point_id'];
+            }
+
+            // 添加技能点数据(从题目中或当前选择的技能中获取)
+            if (!empty($question['selected_skills'])) {
+                $attemptData['skill_codes'] = $question['selected_skills'];
+            } elseif (!empty($this->selectedSkills)) {
+                $attemptData['skill_codes'] = $this->selectedSkills;
+            } else {
+                $attemptData['skill_codes'] = [];
+            }
+
+            $result = $learningAnalytics->submitAttempt($this->studentId, $attemptData);
+
+            if (isset($result['error'])) {
+                Log::error('LearningAnalytics API 调用失败', [
+                    'student_id' => $this->studentId,
+                    'error' => $result['message'] ?? 'Unknown error',
+                    'attempt_data' => $attemptData
+                ]);
+            } else {
+                Log::info('答题记录已成功提交到 LearningAnalytics', [
+                    'student_id' => $this->studentId,
+                    'attempt_id' => $result['attempt_id'] ?? null,
+                    'mastery_level' => $result['mastery_level'] ?? null,
+                    'knowledge_point_id' => $result['knowledge_point_id'] ?? null,
+                    'skill_codes' => $result['skill_codes'] ?? []
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            Log::error('更新掌握度失败', [
+                'student_id' => $this->studentId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+            // 不再抛出异常,避免影响用户体验
+        }
+    }
+
+    /**
+     * 加载知识点和技能数据
+     */
+    public function loadKnowledgePointsAndSkills(): void
+    {
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+
+            // 从知识图谱API获取知识点数据
+            $kpResponse = Http::timeout(10)
+                ->get($knowledgeApiBase . '/knowledge-points/', [
+                    'page' => 1,
+                    'per_page' => 100
+                ]);
+
+            if ($kpResponse->successful()) {
+                $kpData = $kpResponse->json();
+                $this->availableKnowledgePoints = $kpData['data'] ?? $kpData ?? [];
+
+                // 格式化知识点数据,确保包含必要的字段
+                $this->availableKnowledgePoints = array_map(function($kp) {
+                    return [
+                        'id' => (string)($kp['id'] ?? $kp['kp_id'] ?? uniqid()),
+                        'code' => $kp['kp_code'] ?? $kp['kp_id'] ?? $kp['code'] ?? 'KP_UNKNOWN',
+                        'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kp['name'] ?? $kp['kp_code'] ?? '未知知识点',
+                        'subject' => $kp['category'] ?? '数学'
+                    ];
+                }, $this->availableKnowledgePoints);
+            } else {
+                throw new \Exception('知识图谱API调用失败: ' . $kpResponse->status());
+            }
+
+            // 从知识图谱API获取技能数据
+            $skillResponse = Http::timeout(10)
+                ->get($knowledgeApiBase . '/skills/', [
+                    'page' => 1,
+                    'per_page' => 50
+                ]);
+
+            if ($skillResponse->successful()) {
+                $skillData = $skillResponse->json();
+                $this->availableSkills = $skillData['data'] ?? $skillData ?? [];
+
+                // 格式化技能数据
+                $this->availableSkills = array_map(function($skill) {
+                    return [
+                        'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
+                        'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
+                        'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
+                        'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
+                    ];
+                }, $this->availableSkills);
+            } else {
+                throw new \Exception('技能API调用失败: ' . $skillResponse->status());
+            }
+
+            Log::info('成功从知识图谱API加载数据', [
+                'knowledge_points_count' => count($this->availableKnowledgePoints),
+                'skills_count' => count($this->availableSkills)
+            ]);
+
+        } catch (\Exception $e) {
+            Log::error('从知识图谱API加载知识点和技能数据失败,使用备用数据', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            // 使用模拟数据作为备用
+            $this->availableKnowledgePoints = [
+                ['id' => 'factor_1', 'code' => 'factor_1', 'name' => '因式分解基础', 'subject' => '数学'],
+                ['id' => 'factor_2', 'code' => 'factor_2', 'name' => '提取公因式', 'subject' => '数学'],
+                ['id' => 'factor_3', 'code' => 'factor_3', 'name' => '平方差公式', 'subject' => '数学'],
+                ['id' => 'factor_4', 'code' => 'factor_4', 'name' => '完全平方公式', 'subject' => '数学'],
+                ['id' => 'factor_5', 'code' => 'factor_5', 'name' => '分组分解法', 'subject' => '数学'],
+                ['id' => 'factor_6', 'code' => 'factor_6', 'name' => '立方和差公式', 'subject' => '数学'],
+                ['id' => 'factor_7', 'code' => 'factor_7', 'name' => '十字相乘法', 'subject' => '数学'],
+                ['id' => 'factor_8', 'code' => 'factor_8', 'name' => '综合因式分解', 'subject' => '数学'],
+            ];
+
+            $this->availableSkills = [
+                ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'],
+                ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'],
+                ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'],
+                ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'],
+                ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'],
+                ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'],
+            ];
+        }
+
+        $this->buildKnowledgePointIndexes();
+
+        // 不在这里初始化联动,让用户手动选择后再加载
+        // 避免在页面加载时就调用API
+        Log::info('知识点和技能数据加载完成');
+    }
+
+    /**
+     * 为知识点构建索引映射
+     */
+    private function buildKnowledgePointIndexes(): void
+    {
+        $this->knowledgePointCodeIndex = [];
+        $this->knowledgePointIdIndex = [];
+
+        foreach ($this->availableKnowledgePoints as $kp) {
+            // 使用格式化后的字段:code 对应 kp_code
+            if (!empty($kp['code'])) {
+                $this->knowledgePointCodeIndex[(string) $kp['code']] = $kp;
+            }
+            // 使用格式化后的字段:id
+            if (!empty($kp['id'])) {
+                $this->knowledgePointIdIndex[(string) $kp['id']] = $kp;
+            }
+        }
+    }
+
+    /**
+     * 将技能ID数组转换为技能名称数组
+     */
+    private function convertSkillIdsToNames(array $skillIds): array
+    {
+        $skillNames = [];
+        foreach ($skillIds as $skillId) {
+            foreach ($this->availableSkills as $skill) {
+                if ((string)$skill['id'] === (string)$skillId || (string)$skill['code'] === (string)$skillId) {
+                    $skillNames[] = $skill['name'];
+                    break;
+                }
+            }
+        }
+        return $skillNames;
+    }
+
+    /**
+     * 通过 ID 或 code 查找知识点
+     */
+    private function findKnowledgePointByIdOrCode(?string $identifier): ?array
+    {
+        if (empty($identifier)) {
+            return null;
+        }
+
+        if (isset($this->knowledgePointIdIndex[$identifier])) {
+            return $this->knowledgePointIdIndex[$identifier];
+        }
+
+        if (isset($this->knowledgePointCodeIndex[$identifier])) {
+            return $this->knowledgePointCodeIndex[$identifier];
+        }
+
+        return $this->findKnowledgePointByCode($identifier);
+    }
+
+    /**
+     * 通过知识点 code 获取详情
+     */
+    private function findKnowledgePointByCode(?string $kpCode): ?array
+    {
+        if (empty($kpCode)) {
+            return null;
+        }
+
+        if (isset($this->knowledgePointCodeIndex[$kpCode])) {
+            return $this->knowledgePointCodeIndex[$kpCode];
+        }
+
+        $fetched = $this->fetchKnowledgePointFromApi($kpCode);
+        if ($fetched) {
+            $this->availableKnowledgePoints[] = $fetched;
+            $this->buildKnowledgePointIndexes();
+        }
+
+        return $fetched;
+    }
+
+    /**
+     * 调用知识图谱 API 获取知识点
+     */
+    private function fetchKnowledgePointFromApi(string $kpCode): ?array
+    {
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+            $response = Http::timeout(10)->get($knowledgeApiBase . '/knowledge-points/' . $kpCode);
+
+            if ($response->successful()) {
+                $kp = $response->json();
+                return [
+                    'id' => (string) ($kp['id'] ?? $kp['kp_id'] ?? $kpCode),
+                    'code' => $kp['kp_code'] ?? $kpCode,
+                    'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kpCode,
+                    'subject' => $kp['category'] ?? '数学'
+                ];
+            }
+        } catch (\Exception $e) {
+            Log::warning('获取知识点详情失败', [
+                'kp_code' => $kpCode,
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return null;
+    }
+
+    /**
+     * 获取知识点 ID(根据 code)
+     */
+    private function findKnowledgePointIdByCode(?string $kpCode): ?string
+    {
+        $kp = $this->findKnowledgePointByCode($kpCode);
+        if (!$kp || empty($kp['id'])) {
+            return null;
+        }
+
+        return (string) $kp['id'];
+    }
+
+    /**
+     * 根据 ID 获取知识点 code
+     */
+    private function findKnowledgePointCodeById(?string $kpId): ?string
+    {
+        $kp = $this->findKnowledgePointByIdOrCode($kpId);
+        return $kp['code'] ?? null;
+    }
+
+    /**
+     * 获取一个默认的知识点(用于兜底数据)
+     */
+    private function getDefaultKnowledgePointMeta(): array
+    {
+        if (!empty($this->availableKnowledgePoints)) {
+            $kp = $this->availableKnowledgePoints[array_rand($this->availableKnowledgePoints)];
+            return [
+                'id' => isset($kp['id']) ? (string) $kp['id'] : null,
+                'code' => $kp['code'] ?? null
+            ];
+        }
+
+        return [
+            'id' => null,
+            'code' => 'KP_UNKNOWN'
+        ];
+    }
+
+    /**
+     * 知识点选择变化时更新技能列表
+     */
+    public function updatedSelectedKnowledgePoint(): void
+    {
+        // 清空已选择的技能
+        $this->selectedSkills = [];
+
+        // 如果没有选择知识点,加载所有技能
+        if (empty($this->selectedKnowledgePoint)) {
+            $this->loadAllSkills();
+            return;
+        }
+
+        // 根据选择的知识点获取相关技能
+        $this->loadSkillsForKnowledgePoint($this->selectedKnowledgePoint);
+    }
+
+    /**
+     * 根据知识点加载相关技能
+     */
+    private function loadSkillsForKnowledgePoint(string $knowledgePointId): void
+    {
+        $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+
+        // 根据knowledgePointId查找对应的kp_code
+        $kpCode = null;
+        foreach ($this->availableKnowledgePoints as $kp) {
+            if ($kp['id'] === $knowledgePointId) {
+                $kpCode = $kp['code']; // 使用kp_code作为API参数
+                break;
+            }
+        }
+
+        if (!$kpCode) {
+            Log::warning('未找到知识点对应的kp_code', ['knowledge_point_id' => $knowledgePointId]);
+            $this->availableSkills = [];
+            return;
+        }
+
+        Log::info('准备调用知识点详情API', [
+            'kp_code' => $kpCode,
+            'knowledge_point_id' => $knowledgePointId
+        ]);
+
+        // 直接从知识点详情API获取技能列表
+        $kpDetailResponse = Http::timeout(10)
+            ->get($knowledgeApiBase . '/knowledge-points/' . $kpCode);
+
+        $kpData = $kpDetailResponse->json();
+
+        // 打印完整响应,方便调试
+        Log::info('知识点API完整响应', [
+            'knowledge_point' => $kpCode,
+            'status' => $kpDetailResponse->status(),
+            'response' => $kpData
+        ]);
+
+        // 转换技能数据格式,匹配模板期望的字段名
+        $skills = $kpData['skills'] ?? [];
+        $this->availableSkills = array_map(function($skill) {
+            return [
+                'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? ''),
+                'code' => $skill['skill_code'] ?? '',
+                'name' => $skill['skill_name'] ?? '',
+                'category' => $skill['skill_type'] ?? ''
+            ];
+        }, $skills);
+
+        Log::info('设置技能列表', [
+            'count' => count($this->availableSkills),
+            'skills' => $this->availableSkills
+        ]);
+    }
+
+    /**
+     * 加载所有技能
+     */
+    private function loadAllSkills(): void
+    {
+        // 先保存当前技能列表作为兜底
+        $fallbackSkills = $this->availableSkills;
+
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+
+            $skillResponse = Http::timeout(10)
+                ->get($knowledgeApiBase . '/skills/', [
+                    'page' => 1,
+                    'per_page' => 50
+                ]);
+
+            if ($skillResponse->successful()) {
+                $skillData = $skillResponse->json();
+                $skills = $skillData['data'] ?? $skillData ?? [];
+
+                // 只有当API返回有效数据时才更新技能列表
+                if (!empty($skills) && is_array($skills)) {
+                    $this->availableSkills = array_map(function($skill) {
+                        return [
+                            'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
+                            'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
+                            'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
+                            'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
+                        ];
+                    }, $skills);
+
+                    Log::info('成功加载所有技能', [
+                        'skills_count' => count($this->availableSkills)
+                    ]);
+                    return;
+                }
+            }
+
+            // 如果API调用失败或返回空数据,使用默认技能列表
+            Log::warning('加载所有技能失败或为空,使用默认技能列表');
+            $this->useDefaultSkills();
+
+        } catch (\Exception $e) {
+            Log::error('加载所有技能失败,使用默认技能列表', ['error' => $e->getMessage()]);
+            $this->useDefaultSkills();
+        }
+    }
+
+    /**
+     * 使用默认技能列表
+     */
+    private function useDefaultSkills(): void
+    {
+        $this->availableSkills = [
+            ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'],
+            ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'],
+            ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'],
+            ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'],
+            ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'],
+            ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'],
+        ];
+    }
+
+    /**
+     * 生成批量题目
+     */
+    public function generateBatchQuestions(): void
+    {
+        if (empty($this->studentId)) {
+            $this->dispatch('notify', message: '请先选择学生', type: 'warning');
+            return;
+        }
+
+        try {
+            $this->isLoading = true;
+
+            // 生成批次ID
+            $this->currentBatchId = 'BATCH_' . $this->studentId . '_' . time();
+
+            // 一次性获取所有需要的题目,避免重复调用API
+            $allQuestions = $this->fetchMultipleQuestionsFromBank($this->questionsPerSet);
+
+            // 处理题目
+            $questions = [];
+            foreach ($allQuestions as $question) {
+                if (empty($question['knowledge_point_id']) && !empty($question['kp_code'])) {
+                    $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']);
+                }
+                if (empty($question['knowledge_point_id']) && $this->selectedKnowledgePoint) {
+                    $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint;
+                }
+
+                // 添加选择的知识点和技能信息
+                $question['batch_id'] = $this->currentBatchId;
+                $question['selected_knowledge_point'] = $this->selectedKnowledgePoint;
+                $question['selected_skills'] = $this->selectedSkills;
+                $questions[] = $question;
+            }
+
+            if (!empty($questions)) {
+                $this->exerciseQuestions = $questions;
+
+                // 初始化答题数组
+                $this->exerciseAnswers = [];
+                foreach ($questions as $index => $question) {
+                    $this->exerciseAnswers[$index] = [
+                        'user_answer' => '',
+                        'is_correct' => null,
+                    ];
+                }
+
+                $this->dispatch('notify', message: "成功生成 {$this->questionsPerSet} 道题目", type: 'success');
+            } else {
+                $this->dispatch('notify', message: '生成题目失败,请重试', type: 'danger');
+            }
+
+        } catch (\Exception $e) {
+            Log::error('生成批量题目失败', [
+                'student_id' => $this->studentId,
+                'questions_count' => $this->questionsPerSet,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('notify', message: '生成题目失败:' . $e->getMessage(), type: 'danger');
+        } finally {
+            $this->isLoading = false;
+        }
+    }
+
+    /**
+     * 批量提交答案
+     */
+    public function submitBatchAnswers(): void
+    {
+        if (empty($this->studentId) || empty($this->exerciseQuestions) || empty($this->currentBatchId)) {
+            $this->dispatch('notify', message: '没有可提交的题目', type: 'warning');
+            return;
+        }
+
+        try {
+            $this->isLoading = true;
+
+            $successCount = 0;
+            $failureCount = 0;
+
+            foreach ($this->exerciseQuestions as $index => $question) {
+                $answer = $this->exerciseAnswers[$index] ?? null;
+
+                if (!$answer || $answer['is_correct'] === null) {
+                    continue; // 跳过未答题的题目
+                }
+
+                try {
+                    // 确保knowledge_point_id始终是数字ID
+                    $knowledgePointId = $question['knowledge_point_id'] ?? null;
+
+                    // 如果knowledge_point_id是code,转换为ID
+                    if ($knowledgePointId && !is_numeric($knowledgePointId)) {
+                        $knowledgePointId = $this->findKnowledgePointIdByCode($knowledgePointId);
+                    }
+
+                    // 如果还是没有,使用选中的知识点
+                    if (!$knowledgePointId && $this->selectedKnowledgePoint) {
+                        $selectedValue = $this->selectedKnowledgePoint;
+                        // 如果选中的是code,转换为ID;如果是ID,直接使用
+                        if (!is_numeric($selectedValue)) {
+                            $knowledgePointId = $this->findKnowledgePointIdByCode($selectedValue);
+                        } else {
+                            $knowledgePointId = (string)$selectedValue;
+                        }
+                    }
+
+                    // 如果还是没有,从题目kp_code查找
+                    if (!$knowledgePointId && !empty($question['kp_code'])) {
+                        $knowledgePointId = $this->findKnowledgePointIdByCode($question['kp_code']);
+                    }
+
+                    $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($knowledgePointId) ?? 'KP_UNKNOWN';
+
+                    // 准备数据库存储数据
+                    $exerciseData = [
+                        'student_id' => $this->studentId,
+                        'question_id' => $question['id'] ?? 'Q_' . $this->currentBatchId . '_' . $index,
+                        // 确保knowledge_point_id是整数或null,不能是字符串
+                        'knowledge_point_id' => is_numeric($knowledgePointId) ? (int)$knowledgePointId : null,
+                        'question_content' => $question['content'] ?? '',
+                        'student_answer' => $answer['user_answer'] ?? '',
+                        'correct_answer' => $question['answer'] ?? '',
+                        'is_correct' => $answer['is_correct'],
+                        'submission_status' => 'submitted',
+                        'batch_id' => $this->currentBatchId,
+                        'kp_code' => $kpCode,
+                        'selected_skills' => json_encode($this->selectedSkills),
+                        'skill_scores' => $this->calculateSkillScores($this->selectedSkills, $answer['is_correct']),
+                        'time_spent_seconds' => rand(60, 180),
+                        'difficulty_level' => is_numeric($question['difficulty'] ?? 3) ? (float)$question['difficulty'] : 3,
+                        'created_at' => now(),
+                        'updated_at' => now(),
+                    ];
+
+                    // 保存到 Laravel 数据库
+                    \App\Models\StudentExercise::create($exerciseData);
+
+                    // 提交给 LearningAnalytics 系统
+                    $this->updateMasteryFromBatchAnswer($question, $answer['is_correct']);
+
+                    $successCount++;
+
+                } catch (\Exception $e) {
+                    Log::error('批量答题中的单题提交失败', [
+                        'student_id' => $this->studentId,
+                        'question_index' => $index,
+                        'error' => $e->getMessage()
+                    ]);
+                    $failureCount++;
+                }
+            }
+
+            // 清空批量数据
+            $this->exerciseQuestions = [];
+            $this->exerciseAnswers = [];
+            $this->currentBatchId = '';
+
+            // 提交结果
+            $totalQuestions = $successCount + $failureCount;
+            $this->dispatch('notify',
+                message: "批量提交完成!成功: {$successCount} 题,失败: {$failureCount} 题",
+                type: $failureCount === 0 ? 'success' : 'warning'
+            );
+
+            // 刷新仪表板数据
+            $this->loadDashboardData();
+
+            // 批量更新技能熟练度
+            try {
+                $learningAnalytics = new LearningAnalyticsService();
+                $skillResult = $learningAnalytics->batchUpdateSkillProficiency($this->studentId);
+                if ($skillResult) {
+                    Log::info('技能熟练度批量更新成功', ['student_id' => $this->studentId]);
+                }
+            } catch (\Exception $e) {
+                Log::warning('技能熟练度批量更新失败(不影响答题提交)', [
+                    'student_id' => $this->studentId,
+                    'error' => $e->getMessage()
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            Log::error('批量提交答案失败', [
+                'student_id' => $this->studentId,
+                'batch_id' => $this->currentBatchId,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('notify', message: '批量提交失败:' . $e->getMessage(), type: 'danger');
+        } finally {
+            $this->isLoading = false;
+        }
+    }
+
+    /**
+     * 计算技能评分
+     */
+    private function calculateSkillScores(array $selectedSkills, bool $isCorrect): string
+    {
+        if (empty($selectedSkills)) {
+            return json_encode([]);
+        }
+
+        $scores = [];
+        $baseScore = $isCorrect ? 0.8 : 0.2; // 正确答对给0.8分,错误给0.2分
+        $randomFactor = rand(-10, 10) / 100; // 添加随机因素
+
+        foreach ($selectedSkills as $skillId) {
+            $score = max(0, min(1, $baseScore + $randomFactor));
+            $scores[$skillId] = round($score, 3);
+        }
+
+        return json_encode($scores);
+    }
+
+    /**
+     * 批量答题时更新掌握度
+     */
+    private function updateMasteryFromBatchAnswer(array $question, bool $isCorrect): void
+    {
+        try {
+            $learningAnalytics = new LearningAnalyticsService();
+            $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($question['knowledge_point_id'] ?? null);
+
+            $attemptData = [
+                'kp_code' => $kpCode ?? 'KP_UNKNOWN',
+                'is_correct' => $isCorrect,
+                'time_spent_seconds' => rand(60, 180),
+                'difficulty_level' => (string)($question['difficulty'] ?? '3'),
+                'question_id' => 'Q_' . $this->currentBatchId . '_' . rand(1000, 9999),
+                'student_answer' => '',
+                'correct_answer' => $question['answer'] ?? '',
+            ];
+
+            if (!empty($question['knowledge_point_id'])) {
+                $attemptData['knowledge_point_id'] = $question['knowledge_point_id'];
+            }
+
+            // 添加技能点数据(使用技能ID,ID是唯一的)
+            if (!empty($question['selected_skills'])) {
+                $attemptData['skill_codes'] = $question['selected_skills'];
+            } elseif (!empty($this->selectedSkills)) {
+                $attemptData['skill_codes'] = $this->selectedSkills;
+            } else {
+                $attemptData['skill_codes'] = [];
+            }
+
+            $result = $learningAnalytics->submitAttempt($this->studentId, $attemptData);
+
+            if (isset($result['error'])) {
+                Log::error('LearningAnalytics API 调用失败', [
+                    'student_id' => $this->studentId,
+                    'batch_id' => $this->currentBatchId,
+                    'error' => $result['message'] ?? 'Unknown error',
+                    'attempt_data' => $attemptData
+                ]);
+            } else {
+                Log::info('批量答题记录已成功提交', [
+                    'student_id' => $this->studentId,
+                    'batch_id' => $this->currentBatchId,
+                    'attempt_id' => $result['attempt_id'] ?? null,
+                    'knowledge_point_id' => $result['knowledge_point_id'] ?? null,
+                    'skill_codes' => $result['skill_codes'] ?? []
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            Log::error('更新批量答题掌握度失败', [
+                'student_id' => $this->studentId,
+                'batch_id' => $this->currentBatchId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 清除历史记录
+     */
+    public function clearHistory(): void
+    {
+        $this->questionHistory = [];
+        $this->dispatch('notify', message: '历史记录已清除', type: 'info');
+    }
+
+    /**
+     * 清空学生的所有答题数据
+     */
+    public function clearStudentAllData(): void
+    {
+        if (empty($this->studentId)) {
+            $this->dispatch('notify', message: '请先选择学生', type: 'warning');
+            return;
+        }
+
+        try {
+            $this->isLoading = true;
+
+            $service = new LearningAnalyticsService();
+            $result = $service->clearStudentData($this->studentId);
+
+            if ($result) {
+                $this->dispatch('notify', message: '学生答题数据已清空', type: 'success');
+                // 清空当前仪表板数据
+                $this->dashboardData = [];
+                // 重新加载仪表板数据
+                $this->loadDashboardData();
+            } else {
+                $this->dispatch('notify', message: '清空数据时发生错误,请检查日志', type: 'danger');
+            }
+        } catch (\Exception $e) {
+            Log::error('清空学生数据失败', [
+                'student_id' => $this->studentId,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('notify', message: '清空数据失败:' . $e->getMessage(), type: 'danger');
+        } finally {
+            $this->isLoading = false;
+        }
+    }
 }

+ 13 - 0
app/Livewire/ClassAnalytics.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Livewire;
+
+use Livewire\Component;
+
+class ClassAnalytics extends Component
+{
+    public function render()
+    {
+        return view('livewire.class-analytics');
+    }
+}

+ 13 - 0
app/Livewire/LearningPath.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Livewire;
+
+use Livewire\Component;
+
+class LearningPath extends Component
+{
+    public function render()
+    {
+        return view('livewire.learning-path');
+    }
+}

+ 52 - 162
app/Livewire/MasteryHeatmap.php

@@ -3,189 +3,79 @@
 namespace App\Livewire;
 
 use App\Services\LearningAnalyticsService;
+use App\Models\User;
+use Illuminate\Support\Facades\DB;
 use Livewire\Component;
 
 class MasteryHeatmap extends Component
 {
-    public string $studentId = '';
+    public string $studentId;
     public array $heatmapData = [];
-    public bool $isLoading = false;
-    public string $errorMessage = '';
+    public bool $loading = false;
+    public string $error = '';
 
-    // 热力图配置
-    public array $config = [
-        'cellSize' => 60,
-        'itemWidth' => 100,
-        'itemHeight' => 80,
-        'colorStops' => [
-            ['offset' => 0, 'color' => '#ef4444', 'name' => '薄弱'],
-            ['offset' => 0.5, 'color' => '#f59e0b', 'name' => '一般'],
-            ['offset' => 0.7, 'color' => '#10b981', 'name' => '良好'],
-            ['offset' => 1, 'color' => '#3b82f6', 'name' => '掌握'],
-        ]
-    ];
+    protected LearningAnalyticsService $laService;
 
-    public function mount(string $studentId): void
+    public function mount($studentId)
     {
         $this->studentId = $studentId;
+        $this->laService = new LearningAnalyticsService();
         $this->loadHeatmapData();
     }
 
-    public function loadHeatmapData(): void
+    public function loadHeatmapData()
     {
-        $this->isLoading = true;
-        $this->errorMessage = '';
+        $this->loading = true;
+        $this->error = '';
 
         try {
-            $service = new LearningAnalyticsService();
-            $masteryList = $service->getStudentMasteryList($this->studentId);
+            $masteryData = $this->laService->getStudentMastery($this->studentId) ?: [];
+            $knowledgePoints = $masteryData['data'] ?? [];
+
+            // 构建热力图数据
+            $this->heatmapData = array_map(function($kp) {
+                $mastery = ($kp['mastery_level'] ?? 0) * 100;
+                
+                // 根据掌握度分配颜色
+                if ($mastery >= 80) {
+                    $color = '#10b981'; // green-500
+                    $bgClass = 'bg-green-500';
+                } elseif ($mastery >= 60) {
+                    $color = '#3b82f6'; // blue-500
+                    $bgClass = 'bg-blue-500';
+                } elseif ($mastery >= 40) {
+                    $color = '#f59e0b'; // yellow-500
+                    $bgClass = 'bg-yellow-500';
+                } elseif ($mastery >= 20) {
+                    $color = '#f97316'; // orange-500
+                    $bgClass = 'bg-orange-500';
+                } else {
+                    $color = '#ef4444'; // red-500
+                    $bgClass = 'bg-red-500';
+                }
+
+                return [
+                    'code' => $kp['kp_code'],
+                    'name' => $kp['kp_name'] ?? $kp['kp_code'],
+                    'mastery' => round($mastery, 1),
+                    'total_attempts' => $kp['total_attempts'] ?? 0,
+                    'correct_attempts' => $kp['correct_attempts'] ?? 0,
+                    'color' => $color,
+                    'bgClass' => $bgClass,
+                ];
+            }, $knowledgePoints);
 
-            if ($masteryList && isset($masteryList['data'])) {
-                $this->heatmapData = $this->processHeatmapData($masteryList['data']);
-            } else {
-                $this->heatmapData = [];
-            }
         } catch (\Exception $e) {
-            $this->errorMessage = '加载热力图数据失败:' . $e->getMessage();
-            $this->heatmapData = [];
+            $this->error = '加载热力图数据失败: ' . $e->getMessage();
+            \Log::error('MasteryHeatmap load error', [
+                'student_id' => $this->studentId,
+                'error' => $e->getMessage()
+            ]);
         } finally {
-            $this->isLoading = false;
+            $this->loading = false;
         }
     }
 
-    /**
-     * 处理热力图数据
-     */
-    private function processHeatmapData(array $masteryList): array
-    {
-        $processedData = [];
-        $categories = []; // 知识分类
-
-        foreach ($masteryList as $mastery) {
-            $kpCode = $mastery['kp_code'];
-            $masteryLevel = $mastery['mastery_level'];
-            $trend = $mastery['mastery_trend'];
-
-            // 根据知识点编码提取分类
-            $category = $this->extractCategory($kpCode);
-            if (!in_array($category, $categories)) {
-                $categories[] = $category;
-            }
-
-            // 确定颜色
-            $color = $this->getMasteryColor($masteryLevel);
-            $borderColor = $this->getTrendColor($trend);
-
-            $processedData[] = [
-                'kp_code' => $kpCode,
-                'category' => $category,
-                'mastery_level' => $masteryLevel,
-                'accuracy_rate' => $mastery['accuracy_rate'],
-                'total_attempts' => $mastery['total_attempts'],
-                'trend' => $trend,
-                'color' => $color,
-                'border_color' => $borderColor,
-                'text_color' => $masteryLevel > 0.5 ? '#ffffff' : '#000000',
-            ];
-        }
-
-        // 按分类排序
-        usort($categories, fn($a, $b) => $a <=> $b);
-
-        return [
-            'data' => $processedData,
-            'categories' => $categories,
-        ];
-    }
-
-    /**
-     * 根据知识点编码提取分类
-     */
-    private function extractCategory(string $kpCode): string
-    {
-        // 简单的分类逻辑,实际应根据知识点体系进行分类
-        if (strpos($kpCode, 'KP') === 0) {
-            $number = (int) substr($kpCode, 2);
-            if ($number >= 100 && $number < 200) {
-                return '基础概念';
-            } elseif ($number >= 200 && $number < 300) {
-                return '基础技能';
-            } elseif ($number >= 300 && $number < 400) {
-                return '综合应用';
-            } elseif ($number >= 400 && $number < 500) {
-                return '高级技巧';
-            } else {
-                return '其他';
-            }
-        }
-
-        return '未分类';
-    }
-
-    /**
-     * 根据掌握度获取颜色
-     */
-    private function getMasteryColor(float $masteryLevel): string
-    {
-        if ($masteryLevel < 0.3) {
-            return '#ef4444'; // 红色 - 薄弱
-        } elseif ($masteryLevel < 0.5) {
-            return '#f97316'; // 橙色 - 需要改进
-        } elseif ($masteryLevel < 0.7) {
-            return '#eab308'; // 黄色 - 一般
-        } elseif ($masteryLevel < 0.85) {
-            return '#22c55e'; // 绿色 - 良好
-        } else {
-            return '#3b82f6'; // 蓝色 - 掌握
-        }
-    }
-
-    /**
-     * 根据趋势获取边框颜色
-     */
-    private function getTrendColor(string $trend): string
-    {
-        return match ($trend) {
-            'improving' => '#10b981', // 绿色上升箭头
-            'stable' => '#6b7280',    // 灰色横线
-            'declining' => '#ef4444', // 红色下降箭头
-            'insufficient' => '#d1d5db', // 灰色虚线
-            default => '#9ca3af',
-        };
-    }
-
-    /**
-     * 获取掌握度等级名称
-     */
-    public function getMasteryLevelName(float $masteryLevel): string
-    {
-        if ($masteryLevel < 0.3) {
-            return '薄弱';
-        } elseif ($masteryLevel < 0.5) {
-            return '入门';
-        } elseif ($masteryLevel < 0.7) {
-            return '进阶';
-        } elseif ($masteryLevel < 0.85) {
-            return '熟练';
-        } else {
-            return '精通';
-        }
-    }
-
-    /**
-     * 获取趋势图标
-     */
-    public function getTrendIcon(string $trend): string
-    {
-        return match ($trend) {
-            'improving' => '↗',
-            'stable' => '→',
-            'declining' => '↘',
-            'insufficient' => '⋯',
-            default => '?',
-        };
-    }
-
     public function render()
     {
         return view('livewire.mastery-heatmap');

+ 76 - 0
app/Livewire/StudentAnalytics.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Livewire;
+
+use App\Services\LearningAnalyticsService;
+use App\Models\User;
+use Illuminate\Support\Facades\DB;
+use Livewire\Component;
+
+class StudentAnalytics extends Component
+{
+    public User $student;
+    public string $studentId;
+    public array $masteryData = [];
+    public array $analysis = [];
+    public bool $loading = false;
+    public string $error = '';
+
+    protected LearningAnalyticsService $laService;
+
+    public function mount($studentId)
+    {
+        $this->studentId = $studentId;
+        $this->laService = new LearningAnalyticsService();
+        $this->loadStudentData();
+    }
+
+    public function loadStudentData()
+    {
+        $this->loading = true;
+        $this->error = '';
+
+        try {
+            // 获取学生基本信息
+            $this->student = User::where('user_id', $this->studentId)->firstOrFail();
+
+            // 从 MySQL 获取练习历史
+            $exercises = DB::connection('remote_mysql')
+                ->table('student_exercises')
+                ->where('student_id', $this->studentId)
+                ->orderBy('created_at', 'desc')
+                ->limit(50)
+                ->get()
+                ->toArray();
+
+            // 从 LearningAnalytics 获取掌握度数据
+            $this->masteryData = $this->laService->getStudentMastery($this->studentId) ?: [];
+
+            // 从 LearningAnalytics 获取学习分析
+            $this->analysis = $this->laService->getStudentAnalysis($this->studentId);
+
+            // 合并数据
+            $this->analysis['exercises'] = $exercises;
+            $this->analysis['exercises_count'] = count($exercises);
+
+        } catch (\Exception $e) {
+            $this->error = '加载学生数据失败: ' . $e->getMessage();
+            \Log::error('StudentAnalytics load error', [
+                'student_id' => $this->studentId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        } finally {
+            $this->loading = false;
+        }
+    }
+
+    public function render()
+    {
+        return view('livewire.student-analytics', [
+            'masteryPoints' => $this->masteryData['data'] ?? [],
+            'totalKnowledgePoints' => count($this->masteryData['data'] ?? []),
+            'student' => $this->student,
+        ]);
+    }
+}

+ 129 - 0
app/Livewire/TeacherDashboard.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace App\Livewire;
+
+use App\Services\LearningAnalyticsService;
+use Illuminate\Support\Facades\DB;
+use Livewire\Component;
+
+class TeacherDashboard extends Component
+{
+    public string $teacherId;
+    public array $students = [];
+    public array $stats = [];
+    public bool $loading = false;
+    public string $error = '';
+
+    protected LearningAnalyticsService $laService;
+
+    public function mount($teacherId)
+    {
+        $this->teacherId = $teacherId;
+        $this->laService = new LearningAnalyticsService();
+        $this->loadDashboardData();
+    }
+
+    public function loadDashboardData()
+    {
+        $this->loading = true;
+        $this->error = '';
+
+        try {
+            // 获取老师名下的学生
+            $this->students = DB::connection('remote_mysql')
+                ->table('students as s')
+                ->leftJoin('users as u', 's.student_id', '=', 'u.user_id')
+                ->where('s.teacher_id', $this->teacherId)
+                ->select(
+                    's.student_id',
+                    's.name as student_name',
+                    's.grade',
+                    's.class_name',
+                    'u.username',
+                    'u.email'
+                )
+                ->get()
+                ->map(function($student) {
+                    // 获取每个学生的掌握度数据
+                    $masteryData = $this->laService->getStudentMastery($student->student_id);
+                    
+                    $knowledgePoints = $masteryData['data'] ?? [];
+                    
+                    // 计算统计信息
+                    $totalPoints = count($knowledgePoints);
+                    $masteredPoints = 0;
+                    $avgMastery = 0;
+                    $totalAttempts = 0;
+                    
+                    if (!empty($knowledgePoints)) {
+                        $masterySum = 0;
+                        foreach ($knowledgePoints as $kp) {
+                            $mastery = ($kp['mastery_level'] ?? 0) * 100;
+                            $masterySum += $mastery;
+                            $totalAttempts += $kp['total_attempts'] ?? 0;
+                            
+                            if ($mastery >= 80) {
+                                $masteredPoints++;
+                            }
+                        }
+                        $avgMastery = round($masterySum / $totalPoints, 1);
+                    }
+
+                    return [
+                        'student_id' => $student->student_id,
+                        'name' => $student->student_name,
+                        'grade' => $student->grade,
+                        'class' => $student->class_name,
+                        'email' => $student->email,
+                        'total_knowledge_points' => $totalPoints,
+                        'mastered_points' => $masteredPoints,
+                        'avg_mastery' => $avgMastery,
+                        'total_attempts' => $totalAttempts,
+                        'mastery_data' => $knowledgePoints,
+                    ];
+                })
+                ->toArray();
+
+            // 计算总体统计
+            $this->calculateStats();
+
+        } catch (\Exception $e) {
+            $this->error = '加载数据失败: ' . $e->getMessage();
+            \Log::error('TeacherDashboard load error', [
+                'teacher_id' => $this->teacherId,
+                'error' => $e->getMessage()
+            ]);
+        } finally {
+            $this->loading = false;
+        }
+    }
+
+    private function calculateStats()
+    {
+        $totalStudents = count($this->students);
+        $totalMasteredPoints = 0;
+        $totalAvgMastery = 0;
+        $totalAttempts = 0;
+
+        foreach ($this->students as $student) {
+            $totalMasteredPoints += $student['mastered_points'];
+            $totalAvgMastery += $student['avg_mastery'];
+            $totalAttempts += $student['total_attempts'];
+        }
+
+        $this->stats = [
+            'total_students' => $totalStudents,
+            'total_mastered_points' => $totalMasteredPoints,
+            'avg_mastery' => $totalStudents > 0 ? round($totalAvgMastery / $totalStudents, 1) : 0,
+            'total_attempts' => $totalAttempts,
+        ];
+    }
+
+    public function render()
+    {
+        return view('livewire.teacher-dashboard', [
+            'stats' => $this->stats,
+            'students' => $this->students,
+        ]);
+    }
+}

+ 29 - 0
app/Models/KnowledgePoint.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class KnowledgePoint extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'name',
+        'description',
+        'code',
+        'subject',
+        'grade',
+        'difficulty_level',
+    ];
+
+    protected $casts = [
+        'difficulty_level' => 'integer',
+    ];
+
+    public function skills()
+    {
+        return $this->belongsToMany(Skill::class, 'knowledge_point_skill');
+    }
+}

+ 30 - 0
app/Models/Skill.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Skill extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'name',
+        'description',
+        'code',
+        'category',
+        'max_score',
+        'weight',
+    ];
+
+    protected $casts = [
+        'max_score' => 'float',
+        'weight' => 'float',
+    ];
+
+    public function knowledgePoints()
+    {
+        return $this->belongsToMany(KnowledgePoint::class, 'knowledge_point_skill');
+    }
+}

+ 41 - 0
app/Models/StudentExercise.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class StudentExercise extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'student_id',
+        'question_id',
+        'knowledge_point_id',
+        'question_content',
+        'student_answer',
+        'correct_answer',
+        'is_correct',
+        'submission_status',
+        'batch_id',
+        'kp_code',
+        'selected_skills',
+        'skill_scores',
+        'time_spent_seconds',
+        'difficulty_level',
+        'created_at',
+        'updated_at',
+    ];
+
+    protected $casts = [
+        'is_correct' => 'boolean',
+        'knowledge_point_id' => 'integer',
+        'selected_skills' => 'array',
+        'skill_scores' => 'array',
+        'time_spent_seconds' => 'integer',
+        'difficulty_level' => 'float',
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+    ];
+}

+ 3 - 1
app/Providers/Filament/AdminPanelProvider.php

@@ -14,6 +14,7 @@ use Filament\Http\Middleware\DispatchServingFilamentEvent;
 use Filament\Pages\Dashboard;
 use Filament\Panel;
 use Filament\PanelProvider;
+use App\Filament\Auth\Pages\CustomLogin;
 use Filament\Support\Colors\Color;
 use Filament\Widgets\AccountWidget;
 use Filament\Widgets\FilamentInfoWidget;
@@ -32,7 +33,7 @@ class AdminPanelProvider extends PanelProvider
             ->default()
             ->id('admin')
             ->path('admin')
-            ->login()
+            ->login(CustomLogin::class)
             ->colors([
                 'primary' => Color::hex('#4163ff'),
             ])
@@ -54,6 +55,7 @@ class AdminPanelProvider extends PanelProvider
             ->renderHook('panels::head.end', fn (): string =>
                 view('filament.layout.vite-styles')->render() . view('filament.layout.vite-scripts')->render()
             )
+            ->renderHook('global::head.start', fn (): string => view('filament.layout.vite-styles')->render())
             ->middleware([
                 EncryptCookies::class,
                 AddQueuedCookiesToResponse::class,

+ 504 - 481
app/Services/LearningAnalyticsService.php

@@ -4,832 +4,855 @@ namespace App\Services;
 
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\DB;
 
-/**
- * 学习分析系统API服务
- * 负责与LearningAnalytics系统交互
- */
 class LearningAnalyticsService
 {
-    /**
-     * API基础URL
-     */
-    private string $baseUrl;
-
-    /**
-     * 超时时间(秒)
-     */
-    private int $timeout;
+    protected string $baseUrl;
+    protected int $timeout = 10;
 
     public function __construct()
     {
-        $this->baseUrl = config('services.learning_analytics.url', 'http://localhost:5016');
-        $this->timeout = config('services.learning_analytics.timeout', 30);
+        $this->baseUrl = config('services.learning_analytics.url', env('LEARNING_ANALYTICS_API_BASE', 'http://localhost:5016'));
     }
 
     /**
-     * 获取学生掌握度概览
+     * 获取学生掌握度
      */
-    public function getStudentMasteryOverview(string $studentId): ?array
+    public function getStudentMastery(string $studentId, string $kpCode = null): array
     {
         try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/mastery/student/{$studentId}/overview");
+            $endpoint = $kpCode 
+                ? "/api/v1/mastery/student/{$studentId}/kp/{$kpCode}"
+                : "/api/v1/mastery/student/{$studentId}";
 
+            $response = Http::timeout($this->timeout)->get($this->baseUrl . $endpoint);
+            
             if ($response->successful()) {
                 return $response->json();
             }
 
-            Log::warning("获取学生掌握度概览失败", [
-                'student_id' => $studentId,
+            Log::error('LearningAnalytics API Error', [
+                'endpoint' => $endpoint,
                 'status' => $response->status(),
                 'response' => $response->body()
             ]);
 
-            return null;
+            return [
+                'error' => true,
+                'message' => 'Failed to fetch mastery data'
+            ];
         } catch (\Exception $e) {
-            Log::error("获取学生掌握度概览异常", [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
+            Log::error('LearningAnalytics Service Exception', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
             ]);
 
-            return null;
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
         }
     }
 
     /**
-     * 获取学生掌握度列表
+     * 更新学生掌握度
      */
-    public function getStudentMasteryList(string $studentId, array $filters = []): ?array
+    public function updateMastery(array $data): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/mastery/student/{$studentId}", $filters);
+                ->post($this->baseUrl . '/api/v1/mastery/student/' . $data['student_id'] . '/update', $data);
 
             if ($response->successful()) {
                 return $response->json();
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取学生掌握度列表异常", [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
+            Log::error('LearningAnalytics Update Error', [
+                'data' => $data,
+                'status' => $response->status(),
+                'response' => $response->body()
             ]);
 
-            return null;
-        }
-    }
-
-    /**
-     * 获取学生技能熟练度
-     */
-    public function getStudentSkillProficiency(string $studentId): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/skill/proficiency/student/{$studentId}");
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
+            return [
+                'error' => true,
+                'message' => 'Failed to update mastery'
+            ];
         } catch (\Exception $e) {
-            Log::error("获取学生技能熟练度异常", [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
+            Log::error('LearningAnalytics Update Exception', [
+                'error' => $e->getMessage(),
+                'data' => $data
             ]);
 
-            return null;
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
         }
     }
 
     /**
-     * 获取学生技能摘要
+     * 获取老师名下的所有学生
      */
-    public function getStudentSkillSummary(string $studentId): ?array
+    public function getTeacherStudents(string $teacherId): array
     {
         try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/skill/proficiency/student/{$studentId}/summary");
-
-            if ($response->successful()) {
-                return $response->json();
-            }
+            // 从本地MySQL获取学生
+            $students = DB::connection('remote_mysql')
+                ->table('students as s')
+                ->leftJoin('users as u', 's.student_id', '=', 'u.user_id')
+                ->where('s.teacher_id', $teacherId)
+                ->select(
+                    's.student_id',
+                    's.name',
+                    's.grade',
+                    's.class_name',
+                    'u.username',
+                    'u.email'
+                )
+                ->get()
+                ->toArray();
 
-            return null;
+            return $students;
         } catch (\Exception $e) {
-            Log::error("获取学生技能摘要异常", [
-                'student_id' => $studentId,
+            Log::error('Get Teacher Students Error', [
+                'teacher_id' => $teacherId,
                 'error' => $e->getMessage()
             ]);
 
-            return null;
+            return [];
         }
     }
 
     /**
-     * 创建提分预测
+     * 获取学生学习分析
      */
-    public function createScorePrediction(array $data): ?array
+    public function getStudentAnalysis(string $studentId): array
     {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/api/v1/prediction/score", $data);
+        // 从LearningAnalytics获取掌握度
+        $masteryData = $this->getStudentMastery($studentId);
+        
+        // 从MySQL获取练习历史
+        $exercises = DB::connection('remote_mysql')
+            ->table('student_exercises')
+            ->where('student_id', $studentId)
+            ->orderBy('created_at', 'desc')
+            ->limit(50)
+            ->get()
+            ->toArray();
 
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
-        } catch (\Exception $e) {
-            Log::error("创建提分预测异常", [
-                'data' => $data,
-                'error' => $e->getMessage()
-            ]);
+        // 从MySQL获取掌握度记录
+        $masteryRecords = DB::connection('remote_mysql')
+            ->table('student_mastery')
+            ->where('student_id', $studentId)
+            ->get()
+            ->toArray();
 
-            return null;
-        }
+        return [
+            'student_id' => $studentId,
+            'mastery_from_la' => $masteryData,
+            'exercises' => $exercises,
+            'mastery_records' => $masteryRecords,
+            'total_exercises' => count($exercises),
+            'total_mastery_records' => count($masteryRecords),
+        ];
     }
 
     /**
-     * 获取学生历史预测
+     * 生成学习测试数据
      */
-    public function getStudentPredictions(string $studentId, int $limit = 10): ?array
+    public function generateLearningData(string $studentId, array $params): array
     {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/prediction/student/{$studentId}", [
-                    'limit' => $limit
-                ]);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取学生预测记录异常", [
+        $results = [];
+        
+        foreach ($params as $param) {
+            $data = [
                 'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
+                'kp_code' => $param['kp_code'],
+                'is_correct' => $param['is_correct'],
+                'time_spent_seconds' => $param['time_spent_seconds'] ?? 120,
+                'difficulty_level' => $param['difficulty_level'] ?? 3,
+            ];
+            
+            $result = $this->updateMastery($data);
+            $results[] = $result;
         }
+        
+        return $results;
     }
 
     /**
-     * 生成学习路径
+     * 获取学习推荐
      */
-    public function generateLearningPath(array $data): ?array
+    public function getLearningRecommendations(string $studentId): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/api/v1/learning-path/generate", $data);
+                ->get($this->baseUrl . "/api/v1/learning-path/student/{$studentId}/recommend");
 
             if ($response->successful()) {
                 return $response->json();
             }
 
-            return null;
+            return ['error' => true, 'message' => 'Failed to fetch recommendations'];
         } catch (\Exception $e) {
-            Log::error("生成学习路径异常", [
-                'data' => $data,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
+            return ['error' => true, 'message' => $e->getMessage()];
         }
     }
 
     /**
-     * 获取学生学习路径
+     * 获取知识点列表(从知识图谱API)
      */
-    public function getStudentLearningPaths(string $studentId, int $limit = 10): ?array
+    public function getKnowledgePoints(): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/learning-path/student/{$studentId}", [
-                    'limit' => $limit
-                ]);
+                ->get($this->baseUrl . '/knowledge-points/');
 
             if ($response->successful()) {
-                return $response->json();
+                return $response->json()['data'] ?? [];
             }
 
-            return null;
+            return [];
         } catch (\Exception $e) {
-            Log::error("获取学生学习路径异常", [
-                'student_id' => $studentId,
+            Log::error('LearningAnalytics Knowledge Points Error', [
                 'error' => $e->getMessage()
             ]);
 
-            return null;
+            return [];
         }
     }
 
     /**
-     * 获取预测分析统计
+     * 获取学生技能熟练度
      */
-    public function getPredictionAnalytics(string $studentId): ?array
+    public function getStudentSkillProficiency(string $studentId): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/prediction/student/{$studentId}/analytics");
+                ->get($this->baseUrl . "/api/v1/skill/proficiency/student/{$studentId}");
 
             if ($response->successful()) {
                 return $response->json();
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取预测分析统计异常", [
+            Log::error('LearningAnalytics Skill Proficiency Error', [
                 'student_id' => $studentId,
-                'error' => $e->getMessage()
+                'status' => $response->status(),
+                'response' => $response->body()
             ]);
 
-            return null;
-        }
-    }
-
-    /**
-     * 获取学习路径分析统计
-     */
-    public function getLearningPathAnalytics(string $studentId): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/api/v1/learning-path/student/{$studentId}/analytics");
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
+            return [
+                'error' => true,
+                'message' => 'Failed to fetch skill proficiency'
+            ];
         } catch (\Exception $e) {
-            Log::error("获取学习路径分析统计异常", [
+            Log::error('LearningAnalytics Skill Proficiency Exception', [
                 'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
 
-            return null;
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
         }
     }
 
     /**
-     * 重新计算掌握度
+     * 获取学生掌握度列表(别名方法)
      */
-    public function recalculateMastery(string $studentId, string $kpCode): ?array
+    public function getStudentMasteryList(string $studentId): array
     {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/api/v1/mastery/student/{$studentId}/update?kp_code={$kpCode}");
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
-        } catch (\Exception $e) {
-            Log::error("重新计算掌握度异常", [
-                'student_id' => $studentId,
-                'kp_code' => $kpCode,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
-        }
+        return $this->getStudentMastery($studentId);
     }
 
     /**
-     * 批量更新技能熟练度
+     * 获取知识点依赖关系
      */
-    public function batchUpdateSkillProficiency(string $studentId): ?array
+    public function getKnowledgeDependencies(): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/api/v1/skill/proficiency/student/{$studentId}/batch-update");
+                ->get($this->baseUrl . '/knowledge-dependencies/');
 
             if ($response->successful()) {
-                return $response->json();
+                return $response->json()['data'] ?? [];
             }
 
-            return null;
+            return [];
         } catch (\Exception $e) {
-            Log::error("批量更新技能熟练度异常", [
-                'student_id' => $studentId,
+            Log::error('LearningAnalytics Knowledge Dependencies Error', [
                 'error' => $e->getMessage()
             ]);
 
-            return null;
+            return [];
         }
     }
 
     /**
-     * 快速提分预测
+     * 提交学生答题记录
      */
-    public function quickScorePrediction(string $studentId): ?array
+    public function submitAttempt(string $studentId, array $attemptData): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/api/v1/prediction/student/{$studentId}/quick-prediction");
+                ->post($this->baseUrl . "/api/v1/attempts/student/{$studentId}", $attemptData);
 
             if ($response->successful()) {
                 return $response->json();
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("快速提分预测异常", [
+            Log::error('Submit Attempt Error', [
                 'student_id' => $studentId,
-                'error' => $e->getMessage()
+                'data' => $attemptData,
+                'status' => $response->status(),
+                'response' => $response->body()
             ]);
 
-            return null;
-        }
-    }
-
-    /**
-     * 推荐学习路径
-     */
-    public function recommendLearningPaths(string $studentId, int $limit = 3): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/api/v1/learning-path/student/{$studentId}/recommend", [
-                    'limit' => $limit
-                ]);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
+            return [
+                'error' => true,
+                'message' => 'Failed to submit attempt'
+            ];
         } catch (\Exception $e) {
-            Log::error("推荐学习路径异常", [
+            Log::error('Submit Attempt Exception', [
                 'student_id' => $studentId,
-                'error' => $e->getMessage()
+                'error' => $e->getMessage(),
+                'data' => $attemptData
             ]);
 
-            return null;
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
         }
     }
 
     /**
-     * ==================== 新增:从MathRecSys迁移的功能 ====================
-     */
-
-    /**
-     * 获取学生能力画像
+     * 检查服务健康状态
      */
-    public function getStudentProfile(string $studentId): ?array
+    public function checkHealth(): bool
     {
         try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/student/{$studentId}/profile");
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
+            $response = Http::timeout(5)->get($this->baseUrl . '/health');
+            return $response->successful();
         } catch (\Exception $e) {
-            Log::error("获取学生能力画像异常", [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
+            return false;
         }
     }
 
     /**
-     * 智能分析题目/学习
+     * 获取学生掌握度概览
      */
-    public function smartAnalyze(string $studentId, array $data): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/student/{$studentId}/analysis", $data);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
-        } catch (\Exception $e) {
-            Log::error("智能分析异常", [
+    public function getStudentMasteryOverview(string $studentId): array
+    {
+        try {
+            $mastery = $this->getStudentMastery($studentId);
+            if (isset($mastery['error'])) {
+                return [
+                    'total_knowledge_points' => 0,
+                    'average_mastery_level' => 0,
+                    'mastered_knowledge_points' => 0,
+                    'good_knowledge_points' => 0,
+                    'weak_knowledge_points' => 0,
+                    'weak_knowledge_points_list' => [],
+                    'details' => []
+                ];
+            }
+
+            $data = $mastery['data'] ?? [];
+
+            // 过滤出有实际答题记录的知识点
+            $attemptedData = array_filter($data, function($item) {
+                return ($item['total_attempts'] ?? 0) > 0;
+            });
+
+            $total = count($data);  // 总知识点数(包括未学习的)
+            $attemptedCount = count($attemptedData);  // 已学习的知识点数
+            $average = $attemptedCount > 0
+                ? array_sum(array_column($attemptedData, 'mastery_level')) / $attemptedCount
+                : 0;
+
+            // 分类知识点(只对有记录的知识点进行分类)
+            $mastered = [];     // ≥ 85%
+            $good = [];         // 70-84%
+            $weak = [];         // < 70%
+
+            foreach ($attemptedData as $item) {
+                $level = $item['mastery_level'] ?? 0;
+                if ($level >= 0.85) {
+                    $mastered[] = $item;
+                } elseif ($level >= 0.70) {
+                    $good[] = $item;
+                } else {
+                    $weak[] = $item;
+                }
+            }
+
+            return [
+                'total_knowledge_points' => $total,
+                'average_mastery_level' => $average,
+                'mastered_knowledge_points' => count($mastered),
+                'good_knowledge_points' => count($good),
+                'weak_knowledge_points' => count($weak),
+                'weak_knowledge_points_list' => $weak,
+                'details' => $data
+            ];
+        } catch (\Exception $e) {
+            Log::error('Get Student Mastery Overview Error', [
                 'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return [
+                'total_knowledge_points' => 0,
+                'average_mastery_level' => 0,
+                'mastered_knowledge_points' => 0,
+                'good_knowledge_points' => 0,
+                'weak_knowledge_points' => 0,
+                'weak_knowledge_points_list' => [],
+                'details' => []
+            ];
         }
     }
 
     /**
-     * 获取学习轨迹
+     * 获取学生技能摘要
      */
-    public function getLearningTrajectory(string $studentId): ?array
+    public function getStudentSkillSummary(string $studentId): array
     {
         try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/student/{$studentId}/trajectory");
-
-            if ($response->successful()) {
-                return $response->json();
+            $proficiency = $this->getStudentSkillProficiency($studentId);
+            if (isset($proficiency['error'])) {
+                return [
+                    'total_skills' => 0,
+                    'average_proficiency_level' => 0,
+                    'total_questions_attempted' => 0,
+                    'skill_list' => []
+                ];
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取学习轨迹异常", [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
-        }
-    }
-
-    /**
-     * 获取薄弱知识点
-     */
-    public function getWeakPoints(string $studentId): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/student/{$studentId}/weak-points");
+            $data = $proficiency['data'] ?? [];
+            $totalSkills = count($data);
+            $averageLevel = $totalSkills > 0 ? array_sum(array_column($data, 'proficiency_level')) / $totalSkills : 0;
 
-            if ($response->successful()) {
-                return $response->json();
+            // 计算总答题数
+            $totalQuestions = 0;
+            foreach ($data as $skill) {
+                $totalQuestions += $skill['total_questions_attempted'] ?? 0;
             }
 
-            return null;
+            return [
+                'total_skills' => $totalSkills,
+                'average_proficiency_level' => $averageLevel,
+                'total_questions_attempted' => $totalQuestions,
+                'skill_list' => $data
+            ];
         } catch (\Exception $e) {
-            Log::error("获取薄弱知识点异常", [
+            Log::error('Get Student Skill Summary Error', [
                 'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return [
+                'total_skills' => 0,
+                'average_proficiency_level' => 0,
+                'total_questions_attempted' => 0,
+                'skill_list' => []
+            ];
         }
     }
 
     /**
-     * 获取学习进度
+     * 获取学生预测数据
      */
-    public function getLearningProgress(string $studentId): ?array
+    public function getStudentPredictions(string $studentId, int $count = 5): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/student/{$studentId}/progress");
+                ->get($this->baseUrl . "/api/v1/prediction/student/{$studentId}?count={$count}");
 
             if ($response->successful()) {
-                return $response->json();
+                $data = $response->json();
+                $predictions = $data['predictions'] ?? $data['data'] ?? [];
+                return [
+                    'predictions' => $predictions
+                ];
             }
 
-            return null;
+            return [
+                'predictions' => []
+            ];
         } catch (\Exception $e) {
-            Log::error("获取学习进度异常", [
+            Log::error('Get Student Predictions Error', [
                 'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return [
+                'predictions' => []
+            ];
         }
     }
 
     /**
-     * 获取个性化推荐
+     * 获取学生学习路径
      */
-    public function getRecommendations(string $studentId, array $data = []): ?array
+    public function getStudentLearningPaths(string $studentId, int $count = 3): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/student/{$studentId}/recommendations", $data);
+                ->get($this->baseUrl . "/api/v1/learning-path/student/{$studentId}?count={$count}");
 
             if ($response->successful()) {
-                return $response->json();
+                $data = $response->json()['data'] ?? [];
+                return [
+                    'paths' => $data
+                ];
             }
 
-            return null;
+            return [
+                'paths' => []
+            ];
         } catch (\Exception $e) {
-            Log::error("获取个性化推荐异常", [
+            Log::error('Get Student Learning Paths Error', [
                 'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return [
+                'paths' => []
+            ];
         }
     }
 
     /**
-     * 获取班级整体分析
+     * 获取预测分析数据
      */
-    public function getClassAnalysis(string $classId): ?array
+    public function getPredictionAnalytics(string $studentId): array
     {
         try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/class/{$classId}/analysis");
+            $predictions = $this->getStudentPredictions($studentId, 10);
 
-            if ($response->successful()) {
-                return $response->json();
+            if (empty($predictions)) {
+                return ['accuracy' => 0, 'trend' => 'stable', 'confidence' => 0];
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取班级分析异常", [
-                'class_id' => $classId,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
-        }
-    }
-
-    /**
-     * 批量分析学生
-     */
-    public function batchAnalyzeStudents(string $classId, array $data): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/class/{$classId}/batch-analysis", $data);
-
-            if ($response->successful()) {
-                return $response->json();
+            $accuracy = 0;
+            $confidence = 0;
+            if (!empty($predictions)) {
+                $accuracy = rand(75, 95); // 模拟准确率
+                $confidence = rand(70, 90); // 模拟置信度
             }
 
-            return null;
+            $trend = 'improving'; // improving, stable, declining
+
+            return [
+                'accuracy' => $accuracy,
+                'trend' => $trend,
+                'confidence' => $confidence,
+                'sample_size' => count($predictions)
+            ];
         } catch (\Exception $e) {
-            Log::error("批量分析学生异常", [
-                'class_id' => $classId,
+            Log::error('Get Prediction Analytics Error', [
+                'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return ['accuracy' => 0, 'trend' => 'stable', 'confidence' => 0];
         }
     }
 
     /**
-     * 获取班级排名
+     * 获取学习路径分析数据
      */
-    public function getClassRanking(string $classId, string $metric = 'mastery'): ?array
+    public function getLearningPathAnalytics(string $studentId): array
     {
         try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/class/{$classId}/ranking", [
-                    'metric' => $metric
-                ]);
+            $paths = $this->getStudentLearningPaths($studentId, 5);
 
-            if ($response->successful()) {
-                return $response->json();
+            if (empty($paths)) {
+                return [
+                    'active_paths' => 0,
+                    'completed_paths' => 0,
+                    'average_efficiency_score' => 0,
+                    'completion_rate' => 0,
+                    'average_time' => 0,
+                    'total_paths' => 0
+                ];
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取班级排名异常", [
-                'class_id' => $classId,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
-        }
-    }
-
-    /**
-     * 与其他班级对比
-     */
-    public function compareWithOtherClasses(string $classId): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/class/{$classId}/comparison");
+            $activePaths = 0;
+            $completedPaths = 0;
+            $efficiencyScores = [];
 
-            if ($response->successful()) {
-                return $response->json();
+            foreach ($paths as $path) {
+                if (($path['status'] ?? '') === 'active') {
+                    $activePaths++;
+                }
+                if (($path['status'] ?? '') === 'completed') {
+                    $completedPaths++;
+                }
+                if (isset($path['efficiency_score'])) {
+                    $efficiencyScores[] = $path['efficiency_score'];
+                }
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取班级对比异常", [
-                'class_id' => $classId,
-                'error' => $e->getMessage()
-            ]);
-
-            return null;
-        }
-    }
+            $averageEfficiency = !empty($efficiencyScores)
+                ? array_sum($efficiencyScores) / count($efficiencyScores)
+                : rand(60, 85) / 100;
 
-    /**
-     * 提分潜力估算
-     */
-    public function estimateScoreGain(array $data): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/ability/gain-estimation", $data);
+            $completionRate = count($paths) > 0
+                ? ($completedPaths / count($paths)) * 100
+                : 0;
 
-            if ($response->successful()) {
-                return $response->json();
-            }
+            $averageTime = rand(30, 60); // 模拟平均时间(分钟)
 
-            return null;
+            return [
+                'active_paths' => $activePaths,
+                'completed_paths' => $completedPaths,
+                'average_efficiency_score' => $averageEfficiency,
+                'completion_rate' => $completionRate,
+                'average_time' => $averageTime,
+                'total_paths' => count($paths)
+            ];
         } catch (\Exception $e) {
-            Log::error("提分潜力估算异常", [
-                'data' => $data,
+            Log::error('Get Learning Path Analytics Error', [
+                'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return [
+                'active_paths' => 0,
+                'completed_paths' => 0,
+                'average_efficiency_score' => 0,
+                'completion_rate' => 0,
+                'average_time' => 0,
+                'total_paths' => 0
+            ];
         }
     }
 
     /**
-     * 评估学生能力
+     * 快速分数预测
      */
-    public function evaluateStudentAbility(array $data): ?array
+    public function quickScorePrediction(string $studentId): array
     {
+        Log::info('开始调用快速预测API', ['student_id' => $studentId]);
+
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/ability/evaluate-student-ability", $data);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
+                ->post($this->baseUrl . "/api/v1/prediction/student/{$studentId}/quick-prediction", [
+                    'student_id' => $studentId
+                ]);
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("评估学生能力异常", [
-                'data' => $data,
-                'error' => $e->getMessage()
+            Log::info('快速预测API响应', [
+                'student_id' => $studentId,
+                'status' => $response->status(),
+                'body' => $response->body()
             ]);
 
-            return null;
-        }
-    }
+            if ($response->successful()) {
+                $data = $response->json();
+                Log::info('快速预测API返回数据', ['student_id' => $studentId, 'data' => $data]);
 
-    /**
-     * 获取学生能力档案
-     */
-    public function getAbilityProfile(string $studentId): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/ability/student/{$studentId}/ability-profile");
+                // API直接返回数据,没有嵌套在'data'键中
+                $quickPredictionData = $data['quick_prediction'] ?? [];
 
-            if ($response->successful()) {
-                return $response->json();
+                return [
+                    'quick_prediction' => [
+                        'current_score' => $quickPredictionData['current_score'] ?? 70,
+                        'predicted_score' => $quickPredictionData['predicted_score'] ?? 75,
+                        'improvement_potential' => $quickPredictionData['improvement_potential'] ?? 5,
+                        'estimated_study_hours' => $quickPredictionData['estimated_study_hours'] ?? 15,
+                        'confidence_level' => $quickPredictionData['confidence_level'] ?? 0.75,
+                        'priority_topics' => $quickPredictionData['priority_topics'] ?? [],
+                        'recommended_actions' => $quickPredictionData['recommended_actions'] ?? [],
+                        'weak_knowledge_points_count' => $quickPredictionData['weak_knowledge_points_count'] ?? 0,
+                        'total_knowledge_points' => $quickPredictionData['total_knowledge_points'] ?? 0
+                    ],
+                    'predicted_score' => $quickPredictionData['predicted_score'] ?? 75,
+                    'confidence' => $quickPredictionData['confidence_level'] ? $quickPredictionData['confidence_level'] * 100 : 75,
+                    'time_estimate' => $quickPredictionData['estimated_study_hours'] ?? 15
+                ];
             }
 
-            return null;
-        } catch (\Exception $e) {
-            Log::error("获取学生能力档案异常", [
+            Log::warning('快速预测API调用失败', [
                 'student_id' => $studentId,
-                'error' => $e->getMessage()
+                'status' => $response->status(),
+                'response' => $response->body()
             ]);
 
-            return null;
-        }
-    }
-
-    /**
-     * 比较学生能力
-     */
-    public function compareAbilities(array $data): ?array
-    {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/ability/compare-abilities", $data);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            return null;
+            return [
+                'quick_prediction' => [
+                    'improvement_potential' => 0,
+                    'estimated_study_hours' => 0,
+                    'confidence_level' => 0
+                ],
+                'predicted_score' => 0,
+                'confidence' => 0,
+                'time_estimate' => 0
+            ];
         } catch (\Exception $e) {
-            Log::error("比较学生能力异常", [
-                'data' => $data,
+            Log::error('Quick Score Prediction Error', [
+                'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return [
+                'quick_prediction' => [
+                    'improvement_potential' => 0,
+                    'estimated_study_hours' => 0,
+                    'confidence_level' => 0
+                ],
+                'predicted_score' => 0,
+                'confidence' => 0,
+                'time_estimate' => 0
+            ];
         }
     }
 
     /**
-     * 学习问题诊断
+     * 推荐学习路径
      */
-    public function diagnosticAnalyze(array $data): ?array
+    public function recommendLearningPaths(string $studentId, int $count = 3): array
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/diagnostic/analyze", $data);
+                ->get($this->baseUrl . "/api/v1/learning-path/recommend/{$studentId}?count={$count}");
 
             if ($response->successful()) {
-                return $response->json();
+                $data = $response->json()['data'] ?? [];
+                return [
+                    'recommendations' => $data
+                ];
             }
 
-            return null;
+            return [
+                'recommendations' => []
+            ];
         } catch (\Exception $e) {
-            Log::error("学习问题诊断异常", [
-                'data' => $data,
+            Log::error('Recommend Learning Paths Error', [
+                'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return [
+                'recommendations' => []
+            ];
         }
     }
 
     /**
-     * 学习障碍检测
+     * 重新计算掌握度
      */
-    public function detectLearningBarriers(array $data): ?array
+    public function recalculateMastery(string $studentId, string $kpCode): bool
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/diagnostic/learning-barrier-detection", $data);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
+                ->post($this->baseUrl . "/api/v1/mastery/recalculate/{$studentId}", [
+                    'student_id' => $studentId,
+                    'kp_code' => $kpCode
+                ]);
 
-            return null;
+            return $response->successful();
         } catch (\Exception $e) {
-            Log::error("学习障碍检测异常", [
-                'data' => $data,
+            Log::error('Recalculate Mastery Error', [
+                'student_id' => $studentId,
+                'kp_code' => $kpCode,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return false;
         }
     }
 
     /**
-     * 错误概念检测
+     * 批量更新技能熟练度
      */
-    public function detectMisconceptions(array $data): ?array
+    public function batchUpdateSkillProficiency(string $studentId): bool
     {
         try {
             $response = Http::timeout($this->timeout)
-                ->post("{$this->baseUrl}/diagnostic/misconception-detection", $data);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
+                ->post($this->baseUrl . "/api/v1/skill/proficiency/student/{$studentId}/batch-update", [
+                    'student_id' => $studentId
+                ]);
 
-            return null;
+            return $response->successful();
         } catch (\Exception $e) {
-            Log::error("错误概念检测异常", [
-                'data' => $data,
+            Log::error('Batch Update Skill Proficiency Error', [
+                'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return null;
+            return false;
         }
     }
 
     /**
-     * 获取诊断历史
+     * 清空学生所有答题数据
      */
-    public function getDiagnosisHistory(string $studentId): ?array
+    public function clearStudentData(string $studentId): bool
     {
         try {
+            // 清空LearningAnalytics中的数据(通过API)
             $response = Http::timeout($this->timeout)
-                ->get("{$this->baseUrl}/diagnostic/student/{$studentId}/learning-diagnosis-history");
+                ->delete($this->baseUrl . "/api/v1/student/{$studentId}/clear");
 
-            if ($response->successful()) {
-                return $response->json();
+            if (!$response->successful()) {
+                Log::error('Clear LearningAnalytics Data Failed', [
+                    'student_id' => $studentId,
+                    'status' => $response->status(),
+                    'response' => $response->body()
+                ]);
             }
 
-            return null;
+            // 清空MySQL中的数据
+            $this->clearStudentMySQLData($studentId);
+
+            Log::info('Student Data Cleared Successfully', [
+                'student_id' => $studentId,
+                'api_success' => $response->successful()
+            ]);
+
+            return true;
         } catch (\Exception $e) {
-            Log::error("获取诊断历史异常", [
+            Log::error('Clear Student Data Error', [
                 'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
 
-            return null;
+            // 即使API失败,也要尝试清空本地数据
+            try {
+                $this->clearStudentMySQLData($studentId);
+                return true;
+            } catch (\Exception $localError) {
+                Log::error('Clear Local Data Also Failed', [
+                    'student_id' => $studentId,
+                    'error' => $localError->getMessage()
+                ]);
+                return false;
+            }
         }
     }
 
     /**
-     * 检查服务健康状态
+     * 清空学生MySQL中的答题数据
      */
-    public function checkHealth(): bool
+    private function clearStudentMySQLData(string $studentId): void
     {
         try {
-            $response = Http::timeout(5)
-                ->get("{$this->baseUrl}/health");
+            // 清空student_exercises表
+            DB::connection('remote_mysql')
+                ->table('student_exercises')
+                ->where('student_id', $studentId)
+                ->delete();
 
-            return $response->successful();
+            // 清空student_mastery表
+            DB::connection('remote_mysql')
+                ->table('student_mastery')
+                ->where('student_id', $studentId)
+                ->delete();
+
+            Log::info('Student MySQL Data Cleared', [
+                'student_id' => $studentId
+            ]);
         } catch (\Exception $e) {
-            Log::warning("学习分析系统健康检查失败", [
+            Log::error('Clear Student MySQL Data Error', [
+                'student_id' => $studentId,
                 'error' => $e->getMessage()
             ]);
-
-            return false;
+            throw $e; // 重新抛出异常,让上层处理
         }
     }
 }

+ 15 - 7
config/database.php

@@ -10,7 +10,7 @@ return [
     |--------------------------------------------------------------------------
     |
     | Here you may specify which of the database connections below you wish
-    | to use as your default connection for database operations. This is
+    | to use as your default connection for all database operations. This is
     | the connection which will be utilized unless another connection
     | is explicitly specified when you execute a query / statement.
     |
@@ -58,8 +58,6 @@ return [
             'prefix_indexes' => true,
             'strict' => true,
             'engine' => null,
-
-            // 远程MySQL优化配置
             'options' => extension_loaded('pdo_mysql') ? array_filter([
                 PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
                 PDO::MYSQL_ATTR_INIT_COMMAND => "SET sql_mode='STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'",
@@ -69,10 +67,6 @@ return [
                 PDO::MYSQL_ATTR_MULTI_STATEMENTS => false,
                 PDO::ATTR_EMULATE_PREPARES => false,
             ]) : [],
-
-            // 连接重试配置
-            'retry_times' => env('DB_RETRY_TIMES', 3),
-            'retry_delay' => env('DB_RETRY_DELAY', 1000), // 毫秒
         ],
 
         'mariadb' => [
@@ -125,6 +119,20 @@ return [
             // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
         ],
 
+        'remote_mysql' => [
+            'driver' => 'mysql',
+            'host' => '120.78.197.180',
+            'port' => '3306',
+            'database' => 'math',
+            'username' => 'root',
+            'password' => 'bamasoso902',
+            'charset' => 'utf8mb4',
+            'collation' => 'utf8mb4_unicode_ci',
+            'prefix' => '',
+            'strict' => true,
+            'engine' => null,
+        ],
+
     ],
 
     /*

+ 28 - 0
database/migrations/2025_11_18_073805_add_updated_at_to_student_exercises_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('student_exercises', function (Blueprint $table) {
+            $table->timestamp('updated_at')->nullable()->after('created_at');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('student_exercises', function (Blueprint $table) {
+            $table->dropColumn('updated_at');
+        });
+    }
+};

+ 39 - 0
database/migrations/2025_11_18_073929_rename_fields_in_student_exercises_table.php

@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('student_exercises', function (Blueprint $table) {
+            // 重命名字段使其与代码逻辑一致
+            $table->renameColumn('question_text', 'question_content');
+            $table->renameColumn('kp_name', 'kp_code');
+            $table->renameColumn('difficulty', 'difficulty_level');
+
+            // 添加 question_id 字段
+            $table->string('question_id', 64)->nullable()->after('student_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('student_exercises', function (Blueprint $table) {
+            $table->renameColumn('question_content', 'question_text');
+            $table->renameColumn('kp_code', 'kp_name');
+            $table->renameColumn('difficulty_level', 'difficulty');
+
+            // 删除 question_id 字段
+            $table->dropColumn('question_id');
+        });
+    }
+};

+ 30 - 0
database/migrations/2025_11_18_130425_update_difficulty_level_column_in_student_exercises_table.php

@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('student_exercises', function (Blueprint $table) {
+            // 修改difficulty_level字段为DECIMAL(3,2)以匹配题库
+            $table->decimal('difficulty_level', 3, 2)->comment('题目难度值')->change();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('student_exercises', function (Blueprint $table) {
+            // 回滚到整数类型(如果需要)
+            $table->integer('difficulty_level')->comment('题目难度值')->change();
+        });
+    }
+};

+ 136 - 0
generate_learning_data.php

@@ -0,0 +1,136 @@
+<?php
+
+require __DIR__ . '/vendor/autoload.php';
+
+use App\Services\LearningAnalyticsService;
+
+echo "=" . str_repeat("=", 60) . "\n";
+echo "🚀 LearningAnalytics 学习数据生成器\n";
+echo "=" . str_repeat("=", 60) . "\n\n";
+
+// 初始化服务
+$service = new LearningAnalyticsService();
+
+// 配置
+$kpCodes = ['KP1001', 'KP1101', 'KP9003', 'KP8001', 'KP9002']; // 因式分解相关知识点
+$studentsPerTeacher = 5;
+$attemptsPerStudent = 30;
+
+try {
+    // 从LearningAnalytics获取知识点
+    echo "📚 获取知识点列表...\n";
+    $knowledgePoints = $service->getKnowledgePoints();
+    echo "✅ 获取到 " . count($knowledgePoints) . " 个知识点\n\n";
+
+    // 获取老师列表
+    echo "👥 获取老师列表...\n";
+    $teachers = DB::connection('remote_mysql')
+        ->table('teachers')
+        ->get()
+        ->toArray();
+    
+    echo "✅ 找到 " . count($teachers) . " 个老师\n\n";
+
+    $totalStudents = 0;
+    $totalAttempts = 0;
+
+    foreach ($teachers as $teacher) {
+        echo "🎓 处理老师: {$teacher->teacher_id} - {$teacher->name}\n";
+        echo str_repeat("-", 60) . "\n";
+
+        // 获取该老师的学生
+        $students = DB::connection('remote_mysql')
+            ->table('students')
+            ->where('teacher_id', $teacher->teacher_id)
+            ->limit($studentsPerTeacher)
+            ->get()
+            ->toArray();
+
+        echo "  📝 找到 " . count($students) . " 个学生\n";
+
+        foreach ($students as $student) {
+            echo "\n  👤 学生: {$student->student_id} - {$student->name}\n";
+            echo "      班级: {$student->grade} {$student->class_name}\n";
+
+            // 生成学习数据
+            $attempts = [];
+            $mastery = 0.3; // 初始掌握度
+            
+            // 生成30次答题记录
+            for ($i = 0; $i < $attemptsPerStudent; $i++) {
+                // 随机选择知识点
+                $kpCode = $kpCodes[array_rand($kpCodes)];
+                
+                // 模拟学习进度:前10次错误较多,后面逐渐变好
+                $isCorrect = false;
+                if ($i < 10) {
+                    $isCorrect = (rand(1, 100) <= 30); // 30% 正确率
+                } elseif ($i < 20) {
+                    $isCorrect = (rand(1, 100) <= 60); // 60% 正确率
+                } else {
+                    $isCorrect = (rand(1, 100) <= 80); // 80% 正确率
+                }
+                
+                // 模拟答题时间(逐渐变快)
+                $baseTime = 180;
+                $timeSpent = max(60, $baseTime - ($i * 4) + rand(-20, 20));
+                
+                // 更新掌握度
+                if ($isCorrect) {
+                    $mastery = $mastery + (1 - $mastery) * 0.15;
+                } else {
+                    $mastery = $mastery - ($mastery * 0.08);
+                }
+                $mastery = max(0.1, min(0.95, $mastery)); // 限制在0.1-0.95之间
+                
+                $attempts[] = [
+                    'kp_code' => $kpCode,
+                    'is_correct' => $isCorrect,
+                    'time_spent_seconds' => $timeSpent,
+                    'difficulty_level' => rand(2, 4),
+                    'mastery' => $mastery
+                ];
+            }
+
+            // 提交到LearningAnalytics
+            echo "      📤 提交学习数据到LearningAnalytics...\n";
+            $successCount = 0;
+            foreach ($attempts as $attempt) {
+                $result = $service->updateMastery([
+                    'student_id' => $student->student_id,
+                    'kp_code' => $attempt['kp_code'],
+                    'is_correct' => $attempt['is_correct'],
+                    'time_spent_seconds' => $attempt['time_spent_seconds'],
+                    'difficulty_level' => $attempt['difficulty_level']
+                ]);
+
+                if (isset($result['error']) && !$result['error']) {
+                    $successCount++;
+                }
+            }
+
+            $finalMastery = $mastery;
+            echo "      ✅ 成功提交 {$successCount} 条记录\n";
+            echo "      📊 最终掌握度: " . number_format($finalMastery * 100, 2) . "%\n";
+
+            $totalStudents++;
+            $totalAttempts += $successCount;
+        }
+
+        echo "\n";
+    }
+
+    echo "=" . str_repeat("=", 60) . "\n";
+    echo "📈 生成完成!\n";
+    echo "=" . str_repeat("=", 60) . "\n";
+    echo "✅ 处理学生数: {$totalStudents}\n";
+    echo "✅ 总答题记录: {$totalAttempts}\n";
+    echo "✅ 平均每人: " . number_format($totalAttempts / max(1, $totalStudents), 1) . " 次\n";
+    echo "\n🎉 所有学习数据已生成并同步到LearningAnalytics!\n";
+
+} catch (Exception $e) {
+    echo "❌ 错误: " . $e->getMessage() . "\n";
+    echo "📍 位置: " . $e->getFile() . ":" . $e->getLine() . "\n";
+    echo "\n" . str_repeat("=", 60) . "\n";
+    exit(1);
+}

+ 86 - 0
package-lock.json

@@ -19,6 +19,7 @@
                 "laravel-vite-plugin": "^2.0.0",
                 "postcss": "^8.5.6",
                 "tailwindcss": "^3.4.18",
+                "terser": "^5.44.1",
                 "vite": "^7.0.7"
             }
         },
@@ -876,6 +877,17 @@
                 "node": ">=6.0.0"
             }
         },
+        "node_modules/@jridgewell/source-map": {
+            "version": "0.3.11",
+            "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+            "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jridgewell/gen-mapping": "^0.3.5",
+                "@jridgewell/trace-mapping": "^0.3.25"
+            }
+        },
         "node_modules/@jridgewell/sourcemap-codec": {
             "version": "1.5.5",
             "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -1292,6 +1304,19 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/acorn": {
+            "version": "8.15.0",
+            "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
+            "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+            "dev": true,
+            "license": "MIT",
+            "bin": {
+                "acorn": "bin/acorn"
+            },
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
         "node_modules/ansi-regex": {
             "version": "6.2.2",
             "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -1479,6 +1504,7 @@
                 }
             ],
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "baseline-browser-mapping": "^2.8.25",
                 "caniuse-lite": "^1.0.30001754",
@@ -1493,6 +1519,13 @@
                 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
             }
         },
+        "node_modules/buffer-from": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
+            "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/call-bind-apply-helpers": {
             "version": "1.0.2",
             "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1902,6 +1935,7 @@
             "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
             "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "graphlib": "^2.1.8",
                 "lodash": "^4.17.15"
@@ -2914,6 +2948,7 @@
                 }
             ],
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "nanoid": "^3.3.11",
                 "picocolors": "^1.1.1",
@@ -3302,6 +3337,16 @@
                 "is-arrayish": "^0.3.1"
             }
         },
+        "node_modules/source-map": {
+            "version": "0.6.1",
+            "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
+            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+            "dev": true,
+            "license": "BSD-3-Clause",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/source-map-js": {
             "version": "1.2.1",
             "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3312,6 +3357,17 @@
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/source-map-support": {
+            "version": "0.5.21",
+            "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz",
+            "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "buffer-from": "^1.0.0",
+                "source-map": "^0.6.0"
+            }
+        },
         "node_modules/string-width": {
             "version": "5.1.2",
             "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
@@ -3516,6 +3572,33 @@
                 "jiti": "bin/jiti.js"
             }
         },
+        "node_modules/terser": {
+            "version": "5.44.1",
+            "resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.1.tgz",
+            "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
+            "dev": true,
+            "license": "BSD-2-Clause",
+            "peer": true,
+            "dependencies": {
+                "@jridgewell/source-map": "^0.3.3",
+                "acorn": "^8.15.0",
+                "commander": "^2.20.0",
+                "source-map-support": "~0.5.20"
+            },
+            "bin": {
+                "terser": "bin/terser"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/terser/node_modules/commander": {
+            "version": "2.20.3",
+            "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
+            "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/thenify": {
             "version": "3.3.1",
             "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
@@ -3586,6 +3669,7 @@
             "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "engines": {
                 "node": ">=12"
             },
@@ -3673,6 +3757,7 @@
             "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "esbuild": "^0.25.0",
                 "fdir": "^6.5.0",
@@ -3777,6 +3862,7 @@
             "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "engines": {
                 "node": ">=12"
             },

+ 1 - 0
package.json

@@ -15,6 +15,7 @@
         "laravel-vite-plugin": "^2.0.0",
         "postcss": "^8.5.6",
         "tailwindcss": "^3.4.18",
+        "terser": "^5.44.1",
         "vite": "^7.0.7"
     },
     "name": "filamentadmin",

+ 3 - 3
resources/css/app.css

@@ -1,12 +1,12 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
-
+/* 使用系统字体栈,避免依赖外部CDN */
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
 
 @layer base {
     :root {
-        --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
+        /* 使用系统字体:PingFang SC (macOS), 苹方-简 (iOS), 微软雅黑 (Windows), 文泉驿微米黑 (Linux) */
+        --font-sans: 'PingFang SC', 'PingFang SC Regular', 'Hiragino Sans GB', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
     }
 
     body {

+ 714 - 293
resources/views/filament/pages/student-dashboard.blade.php

@@ -1,46 +1,115 @@
-<div class="p-6 space-y-6">
-    {{-- 页面标题 --}}
-    <div class="flex items-center justify-between">
-        <div>
-            <h1 class="text-3xl font-bold text-gray-900">学生仪表板</h1>
-            <p class="mt-1 text-sm text-gray-500">
-                全面展示学生的学习分析数据,包括掌握度、技能熟练度、提分预测和学习路径
-            </p>
-        </div>
-        <div class="flex items-center space-x-4">
-            <input
-                type="text"
-                wire:model.live="studentId"
-                placeholder="输入学生ID"
-                class="block w-40 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
-            />
-            <button
-                wire:click="loadDashboardData"
-                wire:loading.attr="disabled"
-                class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
-            >
-                <svg wire:loading class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-                </svg>
-                刷新数据
-            </button>
+<div class="min-h-screen bg-gray-50 p-8">
+    {{-- 页面标题区域 --}}
+    <div class="mb-8">
+        <div class="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
+            <div class="flex items-center justify-between mb-6">
+                <div>
+                    <h1 class="text-3xl font-bold text-gray-900 flex items-center">
+                        <svg class="w-8 h-8 mr-3 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                        </svg>
+                        学生仪表板
+                    </h1>
+                    <p class="mt-2 text-sm text-gray-600 ml-11">
+                        全面展示学生的学习分析数据,包括掌握度、技能熟练度、提分预测和学习路径
+                    </p>
+                </div>
+            </div>
+
+            {{-- 选择器区域 --}}
+            <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
+                <div class="flex items-end space-x-6">
+                    <div class="flex-1">
+                        <label for="teacher-select" class="block text-sm font-medium text-gray-700 mb-2">
+                            <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
+                            </svg>
+                            选择老师
+                        </label>
+                        <select
+                            id="teacher-select"
+                            wire:model.live="teacherId"
+                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm py-2.5 bg-gray-50"
+                        >
+                            <option value="">请选择老师</option>
+                            @foreach ($teachers as $teacher)
+                                <option value="{{ $teacher->teacher_id }}">
+                                    {{ $teacher->name }} ({{ $teacher->subject }})
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    <div class="flex-1">
+                        <label for="student-select" class="block text-sm font-medium text-gray-700 mb-2">
+                            <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
+                            </svg>
+                            选择学生
+                        </label>
+                        <select
+                            id="student-select"
+                            wire:model.live="studentId"
+                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm py-2.5 bg-gray-50"
+                            @disabled(empty($teacherId))
+                        >
+                            <option value="">请选择学生</option>
+                            @foreach ($students as $student)
+                                <option value="{{ $student->student_id }}">
+                                    {{ $student->name }} - {{ $student->grade }}{{ $student->class_name }}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    <div class="pb-0.5 flex space-x-2">
+                        <button
+                            wire:click="loadDashboardData"
+                            wire:loading.attr="disabled"
+                            class="inline-flex items-center px-6 py-2.5 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
+                        >
+                            <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                            </svg>
+                            刷新数据
+                        </button>
+
+                        <button
+                            wire:click="clearStudentAllData"
+                            wire:loading.attr="disabled"
+                            class="inline-flex items-center px-6 py-2.5 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
+                        >
+                            <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                            </svg>
+                            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
+                            </svg>
+                            清空答题数据
+                        </button>
+                    </div>
+                </div>
+            </div>
         </div>
     </div>
 
     {{-- 错误提示 --}}
     @if ($errorMessage)
-        <div class="rounded-md bg-red-50 p-4">
-            <div class="flex">
-                <div class="flex-shrink-0">
-                    <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
-                        <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
-                    </svg>
-                </div>
-                <div class="ml-3">
-                    <h3 class="text-sm font-medium text-red-800">加载错误</h3>
-                    <div class="mt-2 text-sm text-red-700">
-                        <p>{{ $errorMessage }}</p>
+        <div class="mb-8">
+            <div class="bg-red-50 border border-red-200 rounded-xl p-4">
+                <div class="flex items-start">
+                    <div class="flex-shrink-0">
+                        <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
+                            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
+                        </svg>
+                    </div>
+                    <div class="ml-3">
+                        <h3 class="text-sm font-medium text-red-800">加载错误</h3>
+                        <div class="mt-2 text-sm text-red-700">
+                            <p>{{ $errorMessage }}</p>
+                        </div>
                     </div>
                 </div>
             </div>
@@ -49,171 +118,188 @@
 
     {{-- 加载状态 --}}
     @if ($isLoading)
-        <div class="flex items-center justify-center py-12">
-            <svg class="animate-spin h-8 w-8 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-            </svg>
-            <span class="ml-3 text-gray-600">正在加载数据...</span>
+        <div class="mb-8">
+            <div class="bg-white rounded-xl shadow-sm p-12 border border-gray-200">
+                <div class="flex flex-col items-center justify-center">
+                    <svg class="animate-spin h-12 w-12 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                    </svg>
+                    <p class="mt-4 text-sm text-gray-600">正在加载数据,请稍候...</p>
+                </div>
+            </div>
         </div>
     @else
+        {{-- 加载完成后显示内容 --}}
         {{-- 快速概览卡片 --}}
-        <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
-            {{-- 掌握度概览 --}}
-            @if (isset($dashboardData['mastery']['overview']))
-                <div class="bg-white overflow-hidden shadow rounded-lg">
-                    <div class="p-5">
-                        <div class="flex items-center">
-                            <div class="flex-shrink-0">
-                                <div class="w-8 h-8 bg-indigo-500 rounded-md flex items-center justify-center">
-                                    <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
-                                    </svg>
-                                </div>
-                            </div>
-                            <div class="ml-5 w-0 flex-1">
-                                <dl>
-                                    <dt class="text-sm font-medium text-gray-500 truncate">平均掌握度</dt>
-                                    <dd class="flex items-baseline">
-                                        <div class="text-2xl font-semibold text-gray-900">
-                                            {{ number_format($dashboardData['mastery']['overview']['average_mastery_level'] * 100, 1) }}%
+        {{-- 快速概览卡片 --}}
+        <div class="mb-8">
+            <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
+                {{-- 掌握度概览 --}}
+                @if (isset($dashboardData['mastery']['overview']))
+                    <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
+                        <div class="p-6">
+                            <div class="flex items-center justify-between">
+                                <div class="flex items-center">
+                                    <div class="flex-shrink-0">
+                                        <div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
+                                            <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                                            </svg>
                                         </div>
-                                    </dd>
-                                </dl>
+                                    </div>
+                                    <div class="ml-4">
+                                        <p class="text-sm font-medium text-gray-600">平均掌握度</p>
+                                        <p class="text-2xl font-bold text-gray-900 mt-1">
+                                            {{ number_format($dashboardData['mastery']['overview']['average_mastery_level'] * 100, 1) }}%
+                                        </p>
+                                    </div>
+                                </div>
                             </div>
                         </div>
-                    </div>
-                    <div class="bg-gray-50 px-5 py-3">
-                        <div class="text-sm">
-                            <div class="flex justify-between text-xs text-gray-600">
-                                <span>已掌握: {{ $dashboardData['mastery']['overview']['mastered_knowledge_points'] }}</span>
-                                <span>薄弱点: {{ $dashboardData['mastery']['overview']['weak_knowledge_points'] }}</span>
+                        <div class="bg-gray-50 px-6 py-4 border-t border-gray-100">
+                            <div class="flex justify-between text-xs">
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
+                                    已掌握: {{ $dashboardData['mastery']['overview']['mastered_knowledge_points'] }}
+                                </span>
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
+                                    薄弱: {{ $dashboardData['mastery']['overview']['weak_knowledge_points'] }}
+                                </span>
                             </div>
                         </div>
                     </div>
-                </div>
-            @endif
+                @endif
 
-            {{-- 技能熟练度概览 --}}
-            @if (isset($dashboardData['skill']['summary']))
-                <div class="bg-white overflow-hidden shadow rounded-lg">
-                    <div class="p-5">
-                        <div class="flex items-center">
-                            <div class="flex-shrink-0">
-                                <div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
-                                    <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
-                                    </svg>
-                                </div>
-                            </div>
-                            <div class="ml-5 w-0 flex-1">
-                                <dl>
-                                    <dt class="text-sm font-medium text-gray-500 truncate">技能熟练度</dt>
-                                    <dd class="flex items-baseline">
-                                        <div class="text-2xl font-semibold text-gray-900">
-                                            {{ number_format($dashboardData['skill']['summary']['average_proficiency_level'] * 100, 1) }}%
+                {{-- 技能熟练度概览 --}}
+                @if (isset($dashboardData['skill']['summary']))
+                    <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
+                        <div class="p-6">
+                            <div class="flex items-center justify-between">
+                                <div class="flex items-center">
+                                    <div class="flex-shrink-0">
+                                        <div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg">
+                                            <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                                            </svg>
                                         </div>
-                                    </dd>
-                                </dl>
+                                    </div>
+                                    <div class="ml-4">
+                                        <p class="text-sm font-medium text-gray-600">技能熟练度</p>
+                                        <p class="text-2xl font-bold text-gray-900 mt-1">
+                                            {{ number_format($dashboardData['skill']['summary']['average_proficiency_level'] * 100, 1) }}%
+                                        </p>
+                                    </div>
+                                </div>
                             </div>
                         </div>
-                    </div>
-                    <div class="bg-gray-50 px-5 py-3">
-                        <div class="text-sm">
-                            <div class="flex justify-between text-xs text-gray-600">
-                                <span>技能总数: {{ $dashboardData['skill']['summary']['total_skills'] }}</span>
-                                <span>练习题: {{ $dashboardData['skill']['summary']['total_questions_attempted'] }}</span>
+                        <div class="bg-gray-50 px-6 py-4 border-t border-gray-100">
+                            <div class="flex justify-between text-xs">
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
+                                    技能: {{ $dashboardData['skill']['summary']['total_skills'] }}
+                                </span>
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
+                                    练习: {{ $dashboardData['skill']['summary']['total_questions_attempted'] }}
+                                </span>
                             </div>
                         </div>
                     </div>
-                </div>
-            @endif
+                @endif
 
-            {{-- 提分潜力 --}}
-            @if (isset($dashboardData['prediction']['quick']))
-                <div class="bg-white overflow-hidden shadow rounded-lg">
-                    <div class="p-5">
-                        <div class="flex items-center">
-                            <div class="flex-shrink-0">
-                                <div class="w-8 h-8 bg-yellow-500 rounded-md flex items-center justify-center">
-                                    <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
-                                    </svg>
-                                </div>
-                            </div>
-                            <div class="ml-5 w-0 flex-1">
-                                <dl>
-                                    <dt class="text-sm font-medium text-gray-500 truncate">预期提分</dt>
-                                    <dd class="flex items-baseline">
-                                        <div class="text-2xl font-semibold text-gray-900">
-                                            +{{ $dashboardData['prediction']['quick']['quick_prediction']['improvement_potential'] ?? 0 }}
+                {{-- 提分潜力 --}}
+                @if (isset($dashboardData['prediction']['quick']))
+                    <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
+                        <div class="p-6">
+                            <div class="flex items-center justify-between">
+                                <div class="flex items-center">
+                                    <div class="flex-shrink-0">
+                                        <div class="w-12 h-12 bg-gradient-to-br from-yellow-500 to-orange-500 rounded-xl flex items-center justify-center shadow-lg">
+                                            <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
+                                            </svg>
                                         </div>
-                                        <span class="ml-2 text-sm text-gray-600">分</span>
-                                    </dd>
-                                </dl>
+                                    </div>
+                                    <div class="ml-4">
+                                        <p class="text-sm font-medium text-gray-600">预期提分</p>
+                                        <p class="text-2xl font-bold text-gray-900 mt-1">
+                                            +{{ $dashboardData['prediction']['quick']['quick_prediction']['improvement_potential'] ?? 0 }}
+                                            <span class="text-lg text-gray-500">分</span>
+                                        </p>
+                                    </div>
+                                </div>
                             </div>
                         </div>
-                    </div>
-                    <div class="bg-gray-50 px-5 py-3">
-                        <div class="text-sm">
-                            <div class="flex justify-between text-xs text-gray-600">
-                                <span>预计学习: {{ $dashboardData['prediction']['quick']['quick_prediction']['estimated_study_hours'] ?? 0 }}小时</span>
-                                <span>置信度: {{ number_format(($dashboardData['prediction']['quick']['quick_prediction']['confidence_level'] ?? 0) * 100, 0) }}%</span>
+                        <div class="bg-gray-50 px-6 py-4 border-t border-gray-100">
+                            <div class="flex justify-between text-xs">
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
+                                    学习: {{ $dashboardData['prediction']['quick']['quick_prediction']['estimated_study_hours'] ?? 0 }}小时
+                                </span>
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
+                                    置信度: {{ number_format(($dashboardData['prediction']['quick']['quick_prediction']['confidence_level'] ?? 0) * 100, 0) }}%
+                                </span>
                             </div>
                         </div>
                     </div>
-                </div>
-            @endif
+                @endif
 
-            {{-- 学习路径 --}}
-            @if (isset($dashboardData['learning_path']['analytics']))
-                <div class="bg-white overflow-hidden shadow rounded-lg">
-                    <div class="p-5">
-                        <div class="flex items-center">
-                            <div class="flex-shrink-0">
-                                <div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
-                                    <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
-                                    </svg>
-                                </div>
-                            </div>
-                            <div class="ml-5 w-0 flex-1">
-                                <dl>
-                                    <dt class="text-sm font-medium text-gray-500 truncate">活跃路径</dt>
-                                    <dd class="flex items-baseline">
-                                        <div class="text-2xl font-semibold text-gray-900">
-                                            {{ $dashboardData['learning_path']['analytics']['active_paths'] ?? 0 }}
+                {{-- 学习路径 --}}
+                @if (isset($dashboardData['learning_path']['analytics']))
+                    <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
+                        <div class="p-6">
+                            <div class="flex items-center justify-between">
+                                <div class="flex items-center">
+                                    <div class="flex-shrink-0">
+                                        <div class="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
+                                            <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
+                                            </svg>
                                         </div>
-                                    </dd>
-                                </dl>
+                                    </div>
+                                    <div class="ml-4">
+                                        <p class="text-sm font-medium text-gray-600">活跃路径</p>
+                                        <p class="text-2xl font-bold text-gray-900 mt-1">
+                                            {{ $dashboardData['learning_path']['analytics']['active_paths'] ?? 0 }}
+                                        </p>
+                                    </div>
+                                </div>
                             </div>
                         </div>
-                    </div>
-                    <div class="bg-gray-50 px-5 py-3">
-                        <div class="text-sm">
-                            <div class="flex justify-between text-xs text-gray-600">
-                                <span>已完成: {{ $dashboardData['learning_path']['analytics']['completed_paths'] ?? 0 }}</span>
-                                <span>效率: {{ number_format(($dashboardData['learning_path']['analytics']['average_efficiency_score'] ?? 0) * 100, 0) }}%</span>
+                        <div class="bg-gray-50 px-6 py-4 border-t border-gray-100">
+                            <div class="flex justify-between text-xs">
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
+                                    已完成: {{ $dashboardData['learning_path']['analytics']['completed_paths'] ?? 0 }}
+                                </span>
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
+                                    效率: {{ number_format(($dashboardData['learning_path']['analytics']['average_efficiency_score'] ?? 0) * 100, 0) }}%
+                                </span>
                             </div>
                         </div>
                     </div>
-                </div>
-            @endif
+                @endif
+            </div>
         </div>
 
         {{-- 主要内容区域 --}}
-        <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
-            {{-- 掌握度分析 --}}
-            <div class="bg-white shadow rounded-lg">
-                <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
-                    <h3 class="text-lg font-medium text-gray-900">知识点掌握度</h3>
-                    <button
-                        wire:click="batchUpdateSkills"
-                        class="inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200"
-                    >
-                        刷新数据
-                    </button>
-                </div>
+        <div class="mb-8">
+            <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
+                {{-- 掌握度分析 --}}
+                <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                    <div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                            </svg>
+                            知识点掌握度
+                        </h3>
+                        <button
+                            wire:click="batchUpdateSkills"
+                            class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-lg text-indigo-700 bg-indigo-100 hover:bg-indigo-200 transition-colors duration-150"
+                        >
+                            <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
+                            </svg>
+                            刷新
+                        </button>
+                    </div>
                 <div class="p-6">
                     @if (isset($dashboardData['mastery']['overview']['total_knowledge_points']))
                         <div class="space-y-4">
@@ -238,20 +324,20 @@
                                     <div class="text-xs text-gray-500">薄弱 (<50%)</div>
                                 </div>
                             </div>
-                            @if (!empty($dashboardData['mastery']['overview']['weak_knowledge_points']))
+                            @if (!empty($dashboardData['mastery']['overview']['weak_knowledge_points_list']))
                                 <div class="mt-6">
                                     <h4 class="text-sm font-medium text-gray-900 mb-3">薄弱知识点</h4>
                                     <div class="space-y-2">
-                                        @foreach (array_slice($dashboardData['mastery']['overview']['weak_knowledge_points'], 0, 5) as $weak)
+                                        @foreach (array_slice($dashboardData['mastery']['overview']['weak_knowledge_points_list'], 0, 5) as $weak)
                                             <div class="flex items-center justify-between p-3 bg-red-50 rounded-lg">
                                                 <div class="flex items-center">
                                                     <div class="w-2 h-2 bg-red-500 rounded-full mr-3"></div>
-                                                    <span class="text-sm font-medium text-gray-900">{{ $weak['kp_code'] }}</span>
+                                                    <span class="text-sm font-medium text-gray-900">{{ $weak['kp_code'] ?? $weak['knowledge_point_code'] ?? 'N/A' }}</span>
                                                 </div>
                                                 <div class="flex items-center space-x-3">
-                                                    <span class="text-sm text-gray-600">{{ number_format($weak['mastery_level'] * 100, 1) }}%</span>
+                                                    <span class="text-sm text-gray-600">{{ number_format(($weak['mastery_level'] ?? $weak['mastery'] ?? 0) * 100, 1) }}%</span>
                                                     <button
-                                                        wire:click="recalculateMastery('{{ $weak['kp_code'] }}')"
+                                                        wire:click="recalculateMastery('{{ $weak['kp_code'] ?? $weak['knowledge_point_code'] ?? '' }}')"
                                                         class="text-xs text-indigo-600 hover:text-indigo-800"
                                                     >
                                                         重新计算
@@ -271,34 +357,48 @@
                 </div>
             </div>
 
-            {{-- 技能熟练度 --}}
-            <div class="bg-white shadow rounded-lg">
-                <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
-                    <h3 class="text-lg font-medium text-gray-900">技能熟练度</h3>
-                    <button
-                        wire:click="batchUpdateSkills"
-                        class="inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded text-green-700 bg-green-100 hover:bg-green-200"
-                    >
-                        批量更新
-                    </button>
-                </div>
+                {{-- 技能熟练度 --}}
+                <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                    <div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                            </svg>
+                            技能熟练度
+                        </h3>
+                        <button
+                            wire:click="batchUpdateSkills"
+                            class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-lg text-green-700 bg-green-100 hover:bg-green-200 transition-colors duration-150"
+                        >
+                            <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
+                            </svg>
+                            批量更新
+                        </button>
+                    </div>
                 <div class="p-6">
-                    @if (isset($dashboardData['skill']['proficiency']['data']))
+                    @php
+                        $skillData = $dashboardData['skill']['proficiency']['data'] ?? [];
+                        if (!is_array($skillData)) {
+                            $skillData = [];
+                        }
+                    @endphp
+                    @if (!empty($skillData))
                         <div class="space-y-4">
-                            @foreach (array_slice($dashboardData['skill']['proficiency']['data'], 0, 5) as $skill)
+                            @foreach (array_slice($skillData, 0, 5) as $skill)
                                 <div class="flex items-center justify-between">
                                     <div class="flex items-center flex-1">
                                         <div class="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
                                         <div class="flex-1">
-                                            <div class="text-sm font-medium text-gray-900">{{ $skill['skill_name'] }}</div>
+                                            <div class="text-sm font-medium text-gray-900">{{ $skill['skill_name'] ?? 'N/A' }}</div>
                                             <div class="w-full bg-gray-200 rounded-full h-1.5 mt-1">
-                                                <div class="bg-green-500 h-1.5 rounded-full" style="width: {{ $skill['proficiency_level'] * 100 }}%"></div>
+                                                <div class="bg-green-500 h-1.5 rounded-full" style="width: {{ (($skill['proficiency_level'] ?? 0) * 100) }}%"></div>
                                             </div>
                                         </div>
                                     </div>
                                     <div class="ml-4 text-right">
-                                        <div class="text-sm font-semibold text-gray-900">{{ number_format($skill['proficiency_level'] * 100, 1) }}%</div>
-                                        <div class="text-xs text-gray-500">{{ $skill['total_questions_attempted'] }}题</div>
+                                        <div class="text-sm font-semibold text-gray-900">{{ number_format(($skill['proficiency_level'] ?? 0) * 100, 1) }}%</div>
+                                        <div class="text-xs text-gray-500">{{ $skill['total_questions_attempted'] ?? 0 }}题</div>
                                     </div>
                                 </div>
                             @endforeach
@@ -312,45 +412,186 @@
             </div>
         </div>
 
-        {{-- 技能熟练度雷达图 --}}
-        <div class="bg-white shadow rounded-lg">
-            <div class="px-6 py-4 border-b border-gray-200">
-                <h3 class="text-lg font-medium text-gray-900">技能熟练度雷达图</h3>
-            </div>
-            <div class="p-6">
-                <livewire:skill-proficiency-radar :student-id="$studentId" />
+        {{-- 技能熟练度雷达图 - 已隐藏(功能已实现但暂时隐藏以简化界面) --}}
+        {{-- <div class="mb-8">
+            <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                <div class="px-6 py-5 border-b border-gray-100">
+                    <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                        <svg class="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                        </svg>
+                        技能熟练度雷达图
+                    </h3>
+                </div>
+                <div class="p-6">
+                    <livewire:skill-proficiency-radar :student-id="$studentId" />
+                </div>
             </div>
-        </div>
+        </div> --}}
 
-        {{-- 提分预测和学习路径 --}}
-        <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
-            {{-- 提分预测 --}}
-            <div class="bg-white shadow rounded-lg">
-                <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
-                    <h3 class="text-lg font-medium text-gray-900">提分预测</h3>
-                    <button
-                        wire:click="generateQuickPrediction"
-                        class="inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded text-yellow-700 bg-yellow-100 hover:bg-yellow-200"
-                    >
-                        快速预测
-                    </button>
-                </div>
+        {{-- 提分预测和学习路径 - 已实现 --}}
+        <div class="mb-8">
+            <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
+                {{-- 提分预测 --}}
+                <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                    <div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
+                            </svg>
+                            提分预测
+                        </h3>
+                        <button
+                            wire:click="generateQuickPrediction"
+                            class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-lg text-yellow-700 bg-yellow-100 hover:bg-yellow-200 transition-colors duration-150"
+                        >
+                            <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                            </svg>
+                            快速预测
+                        </button>
+                    </div>
                 <div class="p-6">
-                    @if (isset($dashboardData['prediction']['list']['predictions']))
+                    {{-- 快速预测结果 --}}
+                    @if (isset($dashboardData['prediction']['quick']) && !empty($dashboardData['prediction']['quick']['quick_prediction']))
+                        @php
+                            $quickPrediction = $dashboardData['prediction']['quick']['quick_prediction'];
+                        @endphp
+
+                        {{-- 主要预测信息卡片 --}}
+                        <div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 mb-6 border border-blue-200">
+                            <div class="flex items-center justify-between mb-4">
+                                <div>
+                                    <h4 class="text-lg font-semibold text-gray-900">智能提分预测</h4>
+                                    <p class="text-sm text-gray-600 mt-1">基于当前学习数据的AI分析</p>
+                                </div>
+                                <div class="text-right">
+                                    <div class="text-2xl font-bold text-green-600">
+                                        +{{ number_format($quickPrediction['improvement_potential'] ?? 0, 1) }}分
+                                    </div>
+                                    <div class="text-sm text-gray-500">预期提分</div>
+                                </div>
+                            </div>
+
+                            <div class="grid grid-cols-3 gap-4 mb-4">
+                                <div class="text-center">
+                                    <div class="text-lg font-semibold text-gray-900">
+                                        {{ $quickPrediction['current_score'] ?? 0 }}分
+                                    </div>
+                                    <div class="text-xs text-gray-500">当前分数</div>
+                                </div>
+                                <div class="text-center">
+                                    <div class="text-lg font-semibold text-blue-600">
+                                        {{ $quickPrediction['predicted_score'] ?? 0 }}分
+                                    </div>
+                                    <div class="text-xs text-gray-500">预测分数</div>
+                                </div>
+                                <div class="text-center">
+                                    <div class="text-lg font-semibold text-purple-600">
+                                        {{ $quickPrediction['estimated_study_hours'] ?? 0 }}h
+                                    </div>
+                                    <div class="text-xs text-gray-500">建议学习时间</div>
+                                </div>
+                            </div>
+
+                            <div class="flex items-center justify-between">
+                                <div class="flex items-center space-x-2">
+                                    <span class="text-sm text-gray-600">置信度:</span>
+                                    <div class="flex items-center">
+                                        <div class="w-20 bg-gray-200 rounded-full h-2 mr-2">
+                                            <div class="bg-blue-600 h-2 rounded-full" style="width: {{ ($quickPrediction['confidence_level'] ?? 0) * 100 }}%"></div>
+                                        </div>
+                                        <span class="text-sm font-medium text-gray-900">{{ number_format(($quickPrediction['confidence_level'] ?? 0) * 100, 0) }}%</span>
+                                    </div>
+                                </div>
+                                <div class="text-xs text-gray-500">
+                                    {{ $quickPrediction['weak_knowledge_points_count'] ?? 0 }}个薄弱知识点
+                                </div>
+                            </div>
+                        </div>
+
+                        {{-- 优先学习主题 --}}
+                        @if (!empty($quickPrediction['priority_topics']))
+                            <div class="mb-6">
+                                <h5 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                    <svg class="w-4 h-4 mr-2 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                    </svg>
+                                    优先学习主题 (前5个)
+                                </h5>
+                                <div class="space-y-2">
+                                    @foreach (array_slice($quickPrediction['priority_topics'], 0, 5) as $topic)
+                                        <div class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+                                            <div class="flex items-center space-x-3">
+                                                <div class="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
+                                                    <span class="text-xs font-medium text-yellow-800">{{ substr($topic['kp_code'] ?? '', -2) }}</span>
+                                                </div>
+                                                <div>
+                                                    <div class="text-sm font-medium text-gray-900">{{ $topic['kp_code'] ?? 'N/A' }}</div>
+                                                    <div class="text-xs text-gray-500">掌握度: {{ number_format(($topic['mastery_level'] ?? 0) * 100, 1) }}%</div>
+                                                </div>
+                                            </div>
+                                            <div class="text-right">
+                                                <div class="text-sm font-medium text-gray-900">优先级</div>
+                                                <div class="text-xs text-gray-500">{{ number_format($topic['priority_score'] ?? 0, 2) }}</div>
+                                            </div>
+                                        </div>
+                                    @endforeach
+                                </div>
+                            </div>
+                        @endif
+
+                        {{-- 学习建议 --}}
+                        @if (!empty($quickPrediction['recommended_actions']))
+                            <div>
+                                <h5 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                    <svg class="w-4 h-4 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                    </svg>
+                                    个性化学习建议
+                                </h5>
+                                <div class="space-y-3">
+                                    @foreach (array_slice($quickPrediction['recommended_actions'], 0, 3) as $recommendation)
+                                        <div class="p-3 bg-blue-50 rounded-lg border border-blue-200">
+                                            <div class="flex items-center justify-between mb-2">
+                                                <span class="text-sm font-medium text-gray-900">{{ $recommendation['kp_code'] ?? 'N/A' }}</span>
+                                                <span class="text-xs text-gray-500">
+                                                    {{ number_format(($recommendation['current_mastery'] ?? 0) * 100, 0) }}% → {{ number_format(($recommendation['target_mastery'] ?? 0) * 100, 0) }}%
+                                                </span>
+                                            </div>
+                                            <div class="flex flex-wrap gap-1">
+                                                @foreach (($recommendation['actions'] ?? []) as $action)
+                                                    <span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800">
+                                                        {{ $action }}
+                                                    </span>
+                                                @endforeach
+                                            </div>
+                                        </div>
+                                    @endforeach
+                                </div>
+                            </div>
+                        @endif
+
+                    {{-- 历史预测记录 --}}
+                    @elseif (!empty($dashboardData['prediction']['list']['predictions']))
+                        @php
+                            $historicalPredictions = $dashboardData['prediction']['list']['predictions'];
+                        @endphp
                         <div class="space-y-4">
-                            @foreach (array_slice($dashboardData['prediction']['list']['predictions'], 0, 3) as $prediction)
+                            <h5 class="text-sm font-medium text-gray-900 mb-3">历史预测记录</h5>
+                            @foreach (array_slice($historicalPredictions, 0, 3) as $prediction)
                                 <div class="p-4 border border-gray-200 rounded-lg">
                                     <div class="flex items-center justify-between mb-2">
-                                        <div class="text-sm font-medium text-gray-900">{{ $prediction['target_entity'] }}</div>
-                                        <span class="text-xs text-gray-500">{{ date('m-d', strtotime($prediction['prediction_date'])) }}</span>
+                                        <div class="text-sm font-medium text-gray-900">{{ $prediction['target_entity'] ?? 'N/A' }}</div>
+                                        <span class="text-xs text-gray-500">{{ $prediction['prediction_date'] ?? date('m-d') }}</span>
                                     </div>
                                     <div class="flex items-center justify-between">
                                         <div class="text-sm text-gray-600">
-                                            当前: {{ $prediction['current_score'] }}分 →
-                                            <span class="font-semibold text-gray-900">{{ $prediction['predicted_score'] }}分</span>
+                                            当前: {{ $prediction['current_score'] ?? 0 }}分 →
+                                            <span class="font-semibold text-gray-900">{{ $prediction['predicted_score'] ?? 0 }}分</span>
                                         </div>
                                         <div class="text-sm font-semibold text-green-600">
-                                            +{{ number_format($prediction['predicted_score'] - $prediction['current_score'], 1) }}分
+                                            +{{ number_format(($prediction['predicted_score'] ?? 0) - ($prediction['current_score'] ?? 0), 1) }}分
                                         </div>
                                     </div>
                                 </div>
@@ -358,76 +599,74 @@
                         </div>
                     @else
                         <div class="text-center py-8 text-gray-500">
-                            暂无预测数据
+                            <div class="mb-4">
+                                <svg class="w-12 h-12 mx-auto text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                                </svg>
+                            </div>
+                            <p class="text-sm font-medium text-gray-900 mb-1">暂无预测数据</p>
+                            <p class="text-xs text-gray-500 mb-4">点击"快速预测"按钮生成AI预测分析</p>
+                            <button
+                                wire:click="generateQuickPrediction"
+                                class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 transition-colors duration-200"
+                            >
+                                <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                                </svg>
+                                生成快速预测
+                            </button>
                         </div>
                     @endif
                 </div>
             </div>
+        </div>
 
-            {{-- 学习路径 --}}
-            <div class="bg-white shadow rounded-lg">
-                <div class="px-6 py-4 border-b border-gray-200">
-                    <h3 class="text-lg font-medium text-gray-900">学习路径</h3>
+        {{-- 掌握度热力图 - 已隐藏(功能已实现但暂时隐藏以简化界面) --}}
+        {{-- <div class="mb-8">
+            <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                <div class="px-6 py-5 border-b border-gray-100">
+                    <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                        <svg class="w-5 h-5 mr-2 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"></path>
+                        </svg>
+                        知识点掌握度热力图
+                    </h3>
                 </div>
                 <div class="p-6">
-                    @if (isset($dashboardData['learning_path']['list']['paths']))
-                        <div class="space-y-4">
-                            @foreach (array_slice($dashboardData['learning_path']['list']['paths'], 0, 3) as $path)
-                                <div class="p-4 border border-gray-200 rounded-lg">
-                                    <div class="flex items-center justify-between mb-2">
-                                        <div class="text-sm font-medium text-gray-900">{{ $path['target_kp_code'] }}</div>
-                                        <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
-                                            {{ $path['status'] === 'active' ? 'bg-green-100 text-green-800' : '' }}
-                                            {{ $path['status'] === 'completed' ? 'bg-blue-100 text-blue-800' : '' }}
-                                            {{ $path['status'] === 'abandoned' ? 'bg-gray-100 text-gray-800' : '' }}">
-                                            {{ $path['status'] }}
-                                        </span>
-                                    </div>
-                                    <div class="w-full bg-gray-200 rounded-full h-2 mb-2">
-                                        <div class="bg-purple-600 h-2 rounded-full" style="width: {{ $path['completion_percentage'] }}%"></div>
-                                    </div>
-                                    <div class="flex items-center justify-between text-xs text-gray-600">
-                                        <span>完成度: {{ number_format($path['completion_percentage'], 0) }}%</span>
-                                        <span>预计: {{ $path['estimated_hours'] }}小时</span>
-                                    </div>
-                                </div>
-                            @endforeach
-                        </div>
-                    @else
-                        <div class="text-center py-8 text-gray-500">
-                            暂无学习路径数据
-                        </div>
-                    @endif
+                    <livewire:mastery-heatmap :student-id="$studentId" />
                 </div>
             </div>
-        </div>
+        </div> --}}
 
-        {{-- 掌握度热力图 --}}
-        <div class="bg-white shadow rounded-lg">
-            <div class="px-6 py-4 border-b border-gray-200">
-                <h3 class="text-lg font-medium text-gray-900">知识点掌握度热力图</h3>
-            </div>
-            <div class="p-6">
-                <livewire:mastery-heatmap :student-id="$studentId" />
-            </div>
-        </div>
-
-        {{-- 知识点依赖关系图 --}}
-        <div class="bg-white shadow rounded-lg">
-            <div class="px-6 py-4 border-b border-gray-200">
-                <h3 class="text-lg font-medium text-gray-900">知识点依赖关系图</h3>
-            </div>
-            <div class="p-6">
-                <livewire:knowledge-dependency-graph :student-id="$studentId" />
+        {{-- 知识点依赖关系图 - 已隐藏(功能已实现但暂时隐藏以简化界面) --}}
+        {{-- <div class="mb-8">
+            <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                <div class="px-6 py-5 border-b border-gray-100">
+                    <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                        <svg class="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
+                        </svg>
+                        知识点依赖关系图
+                    </h3>
+                </div>
+                <div class="p-6">
+                    <livewire:knowledge-dependency-graph :student-id="$studentId" />
+                </div>
             </div>
-        </div>
+        </div> --}}
 
-        {{-- 推荐学习路径 --}}
-        @if (isset($dashboardData['learning_path']['recommendations']['recommendations']))
-            <div class="bg-white shadow rounded-lg">
-                <div class="px-6 py-4 border-b border-gray-200">
-                    <h3 class="text-lg font-medium text-gray-900">推荐学习路径</h3>
-                </div>
+        {{-- 推荐学习路径 - 已隐藏(功能已实现但暂时隐藏以简化界面) --}}
+        {{-- @if (isset($dashboardData['learning_path']['recommendations']['recommendations']))
+            <div class="mb-8">
+                <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                    <div class="px-6 py-5 border-b border-gray-100">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
+                            </svg>
+                            推荐学习路径
+                        </h3>
+                    </div>
                 <div class="p-6">
                     <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
                         @foreach ($dashboardData['learning_path']['recommendations']['recommendations'] as $recommendation)
@@ -461,21 +700,203 @@
                     </div>
                 </div>
             </div>
-        @endif
-    @endif
+        @endif --}}
+
+        {{-- 练习题目模块 --}}
+        <div class="mb-8">
+            <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                <div class="px-6 py-5 border-b border-gray-100">
+                    <div class="flex items-center justify-between mb-4">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                            </svg>
+                            模拟判卷
+                        </h3>
 
-    {{-- 通知脚本 --}}
-    <script>
-        document.addEventListener('notify', (event) => {
-            const message = event.detail.message;
-            const type = event.detail.type || 'info';
+                </div>
+                <div class="p-6">
+                        <div class="space-y-6">
+                            {{-- 知识点和技能选择区域 --}}
+                            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+                                {{-- 知识点选择 --}}
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                        <svg class="w-4 h-4 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
+                                        </svg>
+                                        选择知识点
+                                    </h4>
+                                    <select
+                                        wire:model.live="selectedKnowledgePoint"
+                                        class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50"
+                                    >
+                                        <option value="">随机知识点</option>
+                                        @foreach ($availableKnowledgePoints as $kp)
+                                            <option value="{{ $kp['id'] ?? $kp['code'] ?? $kp }}">{{ $kp['name'] ?? $kp['code'] ?? $kp }}</option>
+                                        @endforeach
+                                    </select>
+                                </div>
 
-            // 这里可以使用您喜欢的通知库,如toast、notyf等
-            if (window.Alpine && window.Alpine.store) {
-                Alpine.store('notifications').add(message, type);
-            } else {
-                alert(message);
-            }
-        });
-    </script>
+                                {{-- 技能选择 --}}
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                        <svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                                        </svg>
+                                        选择技能(可多选)
+                                    </h4>
+                                    <div class="space-y-2 max-h-32 overflow-y-auto">
+                                        @foreach ($availableSkills as $skill)
+                                            <label class="flex items-center">
+                                                <input
+                                                    type="checkbox"
+                                                    wire:model.live="selectedSkills"
+                                                    value="{{ $skill['id'] ?? $skill['code'] ?? $skill }}"
+                                                    class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
+                                                />
+                                                <span class="ml-2 text-sm text-gray-700">{{ $skill['name'] ?? $skill['code'] ?? $skill }}</span>
+                                            </label>
+                                        @endforeach
+                                    </div>
+                                </div>
+                            </div>
+
+                            {{-- 题目数量和生成按钮 --}}
+                            <div class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
+                                <div class="flex items-center space-x-2">
+                                    <label class="text-sm font-medium text-gray-700">题目数量:</label>
+                                    <input
+                                        type="number"
+                                        wire:model.live="questionsPerSet"
+                                        min="1"
+                                        max="10"
+                                        class="w-16 rounded-lg border-gray-300 text-sm text-center bg-gray-50"
+                                    />
+                                </div>
+                                <button
+                                    wire:click="generateBatchQuestions"
+                                    wire:loading.attr="disabled"
+                                    class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
+                                >
+                                    <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                                    </svg>
+                                    生成题目组
+                                </button>
+                                <div class="text-sm text-gray-500">
+                                    当前批次ID: {{ $currentBatchId ?: '未生成' }}
+                                </div>
+                            </div>
+
+                            {{-- 题目列表 --}}
+                            @if (!empty($exerciseQuestions))
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-4">题目列表 (请标记对错)</h4>
+                                    <div class="space-y-4 max-h-96 overflow-y-auto">
+                                        @foreach ($exerciseQuestions as $index => $question)
+                                            <div class="border border-gray-200 rounded-lg p-4">
+                                                <div class="flex items-start justify-between mb-3">
+                                                    <div class="flex-1">
+                                                        <div class="flex items-center mb-2">
+                                                            <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-indigo-100 text-indigo-800 text-xs font-medium mr-2">
+                                                                {{ $index + 1 }}
+                                                            </span>
+                                                            <span class="text-sm text-gray-500">{{ $question['type'] ?? '数学题' }}</span>
+                                                            <span class="mx-2 text-gray-300">|</span>
+                                                            <span class="text-sm text-gray-500">难度: {{ $question['difficulty'] ?? 3 }}/5</span>
+                                                        </div>
+                                                        <h5 class="text-base font-medium text-gray-900">{{ $question['content'] }}</h5>
+                                                    </div>
+                                                </div>
+
+                                                <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+                                                    <div>
+                                                        <label class="block text-sm font-medium text-gray-700 mb-1">学生答案(可选)</label>
+                                                        <input
+                                                            type="text"
+                                                            wire:model.live="exerciseAnswers.{{ $index }}.user_answer"
+                                                            placeholder="输入答案..."
+                                                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50"
+                                                        />
+                                                    </div>
+                                                    <div>
+                                                        <label class="block text-sm font-medium text-gray-700 mb-1">答题结果</label>
+                                                        <div class="flex space-x-4">
+                                                            <label class="flex items-center">
+                                                                <input
+                                                                    type="radio"
+                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
+                                                                    value="1"
+                                                                    class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300"
+                                                                />
+                                                                <span class="ml-2 text-sm text-green-700">正确</span>
+                                                            </label>
+                                                            <label class="flex items-center">
+                                                                <input
+                                                                    type="radio"
+                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
+                                                                    value="0"
+                                                                    class="h-4 w-4 text-red-600 focus:ring-red-500 border-gray-300"
+                                                                />
+                                                                <span class="ml-2 text-sm text-red-700">错误</span>
+                                                            </label>
+                                                        </div>
+                                                    </div>
+                                                </div>
+
+                                                <div class="mt-3 p-2 bg-green-50 border border-green-200 rounded">
+                                                    <span class="text-xs text-green-800">正确答案: {{ $question['answer'] ?? 'N/A' }}</span>
+                                                </div>
+                                            </div>
+                                        @endforeach
+                                    </div>
+
+                                    {{-- 批量提交按钮 --}}
+                                    <div class="mt-6 pt-4 border-t border-gray-200">
+                                        <button
+                                            wire:click="submitBatchAnswers"
+                                            wire:loading.attr="disabled"
+                                            class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
+                                        >
+                                            <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                                            </svg>
+                                            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                            </svg>
+                                            批量提交答案 ({{ count($exerciseQuestions) }} 题)
+                                        </button>
+                                    </div>
+                                </div>
+                            @else
+                                {{-- 批量模式空状态 --}}
+                                <div class="text-center py-12">
+                                    <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
+                                    </svg>
+                                    <h3 class="mt-4 text-lg font-medium text-gray-900">批量练习模式</h3>
+                                    <p class="mt-2 text-sm text-gray-500 max-w-md mx-auto">
+                                        选择知识点和技能,设置题目数量,点击"生成题目组"开始批量答题练习
+                                    </p>
+                                </div>
+                            @endif
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
 </div>
+@endif
+
+{{-- 通知脚本 --}}
+<script>
+    document.addEventListener('notify', (event) => {
+        const message = event.detail.message;
+        const type = event.detail.type || 'info';
+        alert(message);
+    });
+</script>

+ 3 - 0
resources/views/livewire/class-analytics.blade.php

@@ -0,0 +1,3 @@
+<div>
+    {{-- The best athlete wants his opponent at his best. --}}
+</div>

+ 3 - 0
resources/views/livewire/learning-path.blade.php

@@ -0,0 +1,3 @@
+<div>
+    {{-- If your happiness depends on money, you will never be happy with yourself. --}}
+</div>

+ 81 - 71
resources/views/livewire/mastery-heatmap.blade.php

@@ -1,85 +1,95 @@
-<div>
-    {{-- 标题和控制 --}}
-    <div class="flex items-center justify-between">
-        <div>
-            <h3 class="text-lg font-medium text-gray-900">掌握度热力图</h3>
-            <p class="mt-1 text-sm text-gray-500">
-                以网格形式展示学生对各知识点的掌握情况
-            </p>
-        </div>
-        @if (!empty($heatmapData['data']))
-            <div class="text-sm text-gray-600">
-                共 {{ count($heatmapData['data']) }} 个知识点
-            </div>
-        @endif
+<div class="space-y-6">
+    {{-- Header --}}
+    <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+        <h3 class="text-lg font-semibold text-gray-900">知识点掌握度热力图</h3>
+        <p class="text-sm text-gray-500 mt-1">点击知识点查看详情</p>
     </div>
 
-    {{-- 热力图容器 --}}
-    <div class="relative bg-white rounded-lg border border-gray-200 p-6" style="min-height: 400px;">
-        @if ($isLoading)
-            <div class="flex items-center justify-center h-96">
-                <div class="flex flex-col items-center">
-                    <svg class="animate-spin h-8 w-8 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-                    </svg>
-                    <p class="mt-3 text-sm text-gray-600">正在加载热力图数据...</p>
-                </div>
-            </div>
-        @elseif ($errorMessage)
-            <div class="flex items-center justify-center h-96">
-                <div class="text-center">
-                    <svg class="mx-auto h-12 w-12 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
-                    </svg>
-                    <h3 class="mt-2 text-sm font-medium text-gray-900">加载失败</h3>
-                    <p class="mt-1 text-sm text-red-600">{{ $errorMessage }}</p>
+    {{-- Loading State --}}
+    @if($loading)
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
+            <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
+            <p class="mt-4 text-gray-500">加载热力图数据中...</p>
+        </div>
+    {{-- Error State --}}
+    @elseif($error)
+        <div class="bg-red-50 border border-red-200 rounded-lg p-6">
+            <div class="flex">
+                <svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
+                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
+                </svg>
+                <div class="ml-3">
+                    <h3 class="text-sm font-medium text-red-800">加载失败</h3>
+                    <p class="mt-2 text-sm text-red-700">{{ $error }}</p>
                 </div>
             </div>
-        @elseif (empty($heatmapData['data']))
-            <div class="flex items-center justify-center h-96">
-                <div class="text-center">
+        </div>
+    {{-- Heatmap --}}
+    @else
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+            @if(empty($heatmapData))
+                <div class="text-center py-12 text-gray-500">
                     <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"></path>
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"></path>
                     </svg>
-                    <h3 class="mt-2 text-sm font-medium text-gray-900">暂无热力图数据</h3>
-                    <p class="mt-1 text-sm text-gray-500">该学生还没有掌握度数据</p>
+                    <p class="mt-2">暂无掌握度数据</p>
                 </div>
-            </div>
-        @else
-            {{-- 简单的文本展示代替热力图 --}}
-            <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
-                @foreach ($heatmapData['data'] as $index => $item)
-                    <div class="p-4 border rounded-lg" style="background-color: {{ $this->getMasteryColor($item['mastery_level'] ?? 0) }};">
-                        <div class="text-sm font-medium text-white">{{ $item['kp_code'] ?? 'N/A' }}</div>
-                        <div class="text-xs text-white mt-1">{{ number_format(($item['mastery_level'] ?? 0) * 100, 1) }}%</div>
+            @else
+                {{-- Legend --}}
+                <div class="mb-6 flex items-center justify-center space-x-6">
+                    <div class="flex items-center">
+                        <div class="w-4 h-4 rounded bg-red-500 mr-2"></div>
+                        <span class="text-sm text-gray-600">0-20%</span>
                     </div>
-                @endforeach
-            </div>
+                    <div class="flex items-center">
+                        <div class="w-4 h-4 rounded bg-orange-500 mr-2"></div>
+                        <span class="text-sm text-gray-600">20-40%</span>
+                    </div>
+                    <div class="flex items-center">
+                        <div class="w-4 h-4 rounded bg-yellow-500 mr-2"></div>
+                        <span class="text-sm text-gray-600">40-60%</span>
+                    </div>
+                    <div class="flex items-center">
+                        <div class="w-4 h-4 rounded bg-blue-500 mr-2"></div>
+                        <span class="text-sm text-gray-600">60-80%</span>
+                    </div>
+                    <div class="flex items-center">
+                        <div class="w-4 h-4 rounded bg-green-500 mr-2"></div>
+                        <span class="text-sm text-gray-600">80-100%</span>
+                    </div>
+                </div>
 
-            {{-- 分类图例 --}}
-            @if (!empty($heatmapData['categories']))
-                <div class="mt-4 flex flex-wrap gap-4 justify-center">
-                    @foreach ($heatmapData['categories'] as $category)
-                        <div class="flex items-center space-x-2">
-                            <div class="w-3 h-3 rounded-full bg-indigo-500"></div>
-                            <span class="text-xs text-gray-600">{{ $category }}</span>
+                {{-- Heatmap Grid --}}
+                <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
+                    @foreach($heatmapData as $item)
+                        <div 
+                            class="relative group cursor-pointer rounded-lg p-4 {{ $item['bgClass'] }} text-white transition-transform hover:scale-105"
+                            title="{{ $item['name'] }}: {{ $item['mastery'] }}%"
+                        >
+                            <div class="text-center">
+                                <p class="font-semibold text-lg">{{ $item['mastery'] }}%</p>
+                                <p class="text-xs mt-1 opacity-90">{{ $item['code'] }}</p>
+                                <p class="text-xs mt-1 opacity-75 truncate">{{ $item['name'] }}</p>
+                                <div class="mt-2 pt-2 border-t border-white/20">
+                                    <p class="text-xs opacity-75">答题: {{ $item['total_attempts'] }}</p>
+                                    <p class="text-xs opacity-75">正确: {{ $item['correct_attempts'] }}</p>
+                                </div>
+                            </div>
+                            
+                            {{-- Tooltip --}}
+                            <div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-10">
+                                {{ $item['name'] }}<br>
+                                掌握度: {{ $item['mastery'] }}%<br>
+                                答题: {{ $item['total_attempts'] }} 次
+                                <div class="absolute top-full left-1/2 transform -translate-x-1/2">
+                                    <div class="w-2 h-2 bg-gray-900 rotate-45"></div>
+                                </div>
+                            </div>
                         </div>
                     @endforeach
                 </div>
             @endif
-        @endif
-    </div>
-
-    {{-- ECharts 脚本(简化版) --}}
-    <script>
-        document.addEventListener('livewire:initialized', () => {
-            console.log('热力图已初始化');
-        });
-
-        // 监听 Livewire 更新
-        document.addEventListener('livewire:update', () => {
-            console.log('热力图数据已更新');
-        });
-    </script>
+        </div>
+    @endif
 </div>

+ 137 - 0
resources/views/livewire/student-analytics.blade.php

@@ -0,0 +1,137 @@
+<div class="space-y-6">
+    {{-- Header --}}
+    <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+        <div class="flex items-center justify-between">
+            <div>
+                <h2 class="text-2xl font-bold text-gray-900">{{ $student->name ?? '学生' }}</h2>
+                <p class="text-sm text-gray-500 mt-1">
+                    学号: {{ $studentId }} | 
+                    邮箱: {{ $student->email ?? 'N/A' }}
+                </p>
+            </div>
+            <div class="flex items-center space-x-4">
+                <div class="text-right">
+                    <p class="text-sm text-gray-500">知识点数量</p>
+                    <p class="text-2xl font-bold text-indigo-600">{{ $totalKnowledgePoints }}</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    {{-- Loading State --}}
+    @if($loading)
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
+            <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
+            <p class="mt-4 text-gray-500">加载学习数据中...</p>
+        </div>
+    {{-- Error State --}}
+    @elseif($error)
+        <div class="bg-red-50 border border-red-200 rounded-lg p-6">
+            <div class="flex">
+                <svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
+                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
+                </svg>
+                <div class="ml-3">
+                    <h3 class="text-sm font-medium text-red-800">加载失败</h3>
+                    <p class="mt-2 text-sm text-red-700">{{ $error }}</p>
+                </div>
+            </div>
+        </div>
+    {{-- Data --}}
+    @else
+        {{-- Mastery Overview --}}
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+            <h3 class="text-lg font-semibold text-gray-900 mb-4">掌握度概览</h3>
+            
+            @if(empty($masteryPoints))
+                <div class="text-center py-12 text-gray-500">
+                    <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                    </svg>
+                    <p class="mt-2">暂无掌握度数据</p>
+                </div>
+            @else
+                <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+                    @foreach($masteryPoints as $point)
+                        @php
+                            $mastery = $point['mastery_level'] * 100;
+                            if ($mastery >= 80) {
+                                $colorClass = 'green';
+                                $badgeClass = 'bg-green-100 text-green-800';
+                                $barClass = 'bg-green-600';
+                            } elseif ($mastery >= 60) {
+                                $colorClass = 'yellow';
+                                $badgeClass = 'bg-yellow-100 text-yellow-800';
+                                $barClass = 'bg-yellow-600';
+                            } else {
+                                $colorClass = 'red';
+                                $badgeClass = 'bg-red-100 text-red-800';
+                                $barClass = 'bg-red-600';
+                            }
+                        @endphp
+                        <div class="border border-gray-200 rounded-lg p-4">
+                            <div class="flex items-center justify-between mb-2">
+                                <h4 class="font-medium text-gray-900">{{ $point['kp_code'] }}</h4>
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $badgeClass }}">
+                                    {{ number_format($mastery, 1) }}%
+                                </span>
+                            </div>
+                            <p class="text-sm text-gray-600 mb-2">{{ $point['kp_name'] ?? 'N/A' }}</p>
+                            <div class="w-full bg-gray-200 rounded-full h-2">
+                                <div class="{{ $barClass }} h-2 rounded-full"
+                                     style="width: {{ $mastery }}%"></div>
+                            </div>
+                            <div class="mt-2 flex justify-between text-xs text-gray-500">
+                                <span>答题: {{ $point['total_attempts'] ?? 0 }} 次</span>
+                                <span>正确: {{ $point['correct_attempts'] ?? 0 }} 次</span>
+                            </div>
+                        </div>
+                    @endforeach
+                </div>
+            @endif
+        </div>
+
+        {{-- Exercise History --}}
+        @if(!empty($analysis['exercises']))
+            <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+                <h3 class="text-lg font-semibold text-gray-900 mb-4">最近练习</h3>
+                <div class="overflow-x-auto">
+                    <table class="min-w-full divide-y divide-gray-200">
+                        <thead class="bg-gray-50">
+                            <tr>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">题目</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">结果</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用时</th>
+                            </tr>
+                        </thead>
+                        <tbody class="bg-white divide-y divide-gray-200">
+                            @foreach(array_slice($analysis['exercises'], 0, 10) as $exercise)
+                                <tr>
+                                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+                                        {{ \Carbon\Carbon::parse($exercise->created_at)->format('m-d H:i') }}
+                                    </td>
+                                    <td class="px-6 py-4 text-sm text-gray-900">{{ $exercise->question_id }}</td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        @if($exercise->is_correct)
+                                            <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
+                                                正确
+                                            </span>
+                                        @else
+                                            <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
+                                                错误
+                                            </span>
+                                        @endif
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+                                        {{ $exercise->time_spent_seconds ?? 0 }}秒
+                                    </td>
+                                </tr>
+                            @endforeach
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        @endif
+    @endif
+</div>

+ 171 - 0
resources/views/livewire/teacher-dashboard.blade.php

@@ -0,0 +1,171 @@
+<div class="space-y-6">
+    {{-- Stats Cards --}}
+    <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+            <div class="flex items-center">
+                <div class="p-3 rounded-full bg-indigo-100 text-indigo-600">
+                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
+                    </svg>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm text-gray-500">学生总数</p>
+                    <p class="text-2xl font-bold text-gray-900">{{ $stats['total_students'] ?? 0 }}</p>
+                </div>
+            </div>
+        </div>
+
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+            <div class="flex items-center">
+                <div class="p-3 rounded-full bg-green-100 text-green-600">
+                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                    </svg>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm text-gray-500">已掌握知识点</p>
+                    <p class="text-2xl font-bold text-gray-900">{{ $stats['total_mastered_points'] ?? 0 }}</p>
+                </div>
+            </div>
+        </div>
+
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+            <div class="flex items-center">
+                <div class="p-3 rounded-full bg-blue-100 text-blue-600">
+                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
+                    </svg>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm text-gray-500">平均掌握度</p>
+                    <p class="text-2xl font-bold text-gray-900">{{ $stats['avg_mastery'] ?? 0 }}%</p>
+                </div>
+            </div>
+        </div>
+
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+            <div class="flex items-center">
+                <div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
+                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
+                    </svg>
+                </div>
+                <div class="ml-4">
+                    <p class="text-sm text-gray-500">总答题数</p>
+                    <p class="text-2xl font-bold text-gray-900">{{ $stats['total_attempts'] ?? 0 }}</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    {{-- Loading State --}}
+    @if($loading)
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
+            <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
+            <p class="mt-4 text-gray-500">加载学生数据中...</p>
+        </div>
+    {{-- Error State --}}
+    @elseif($error)
+        <div class="bg-red-50 border border-red-200 rounded-lg p-6">
+            <div class="flex">
+                <svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
+                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
+                </svg>
+                <div class="ml-3">
+                    <h3 class="text-sm font-medium text-red-800">加载失败</h3>
+                    <p class="mt-2 text-sm text-red-700">{{ $error }}</p>
+                </div>
+            </div>
+        </div>
+    {{-- Students Table --}}
+    @else
+        <div class="bg-white rounded-lg shadow-sm border border-gray-200">
+            <div class="px-6 py-4 border-b border-gray-200">
+                <h3 class="text-lg font-semibold text-gray-900">学生列表</h3>
+            </div>
+            @if(empty($students))
+                <div class="text-center py-12 text-gray-500">
+                    <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
+                    </svg>
+                    <p class="mt-2">暂无学生数据</p>
+                </div>
+            @else
+                <div class="overflow-x-auto">
+                    <table class="min-w-full divide-y divide-gray-200">
+                        <thead class="bg-gray-50">
+                            <tr>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">学生</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">班级</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">平均掌握度</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">已掌握</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">答题数</th>
+                                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
+                            </tr>
+                        </thead>
+                        <tbody class="bg-white divide-y divide-gray-200">
+                            @foreach($students as $student)
+                                <tr class="hover:bg-gray-50">
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <div class="flex items-center">
+                                            <div class="flex-shrink-0 h-10 w-10">
+                                                <div class="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
+                                                    <span class="text-sm font-medium text-indigo-600">
+                                                        {{ substr($student['name'], 0, 1) }}
+                                                    </span>
+                                                </div>
+                                            </div>
+                                            <div class="ml-4">
+                                                <div class="text-sm font-medium text-gray-900">{{ $student['name'] }}</div>
+                                                <div class="text-sm text-gray-500">{{ $student['student_id'] }}</div>
+                                            </div>
+                                        </div>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+                                        {{ $student['grade'] }} {{ $student['class'] }}
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <div class="flex items-center">
+                                            <div class="flex-1">
+                                                <div class="w-full bg-gray-200 rounded-full h-2">
+                                                    @php
+                                                        $mastery = $student['avg_mastery'];
+                                                        if ($mastery >= 80) {
+                                                            $barClass = 'bg-green-500';
+                                                        } elseif ($mastery >= 60) {
+                                                            $barClass = 'bg-blue-500';
+                                                        } elseif ($mastery >= 40) {
+                                                            $barClass = 'bg-yellow-500';
+                                                        } else {
+                                                            $barClass = 'bg-red-500';
+                                                        }
+                                                    @endphp
+                                                    <div class="{{ $barClass }} h-2 rounded-full" style="width: {{ $mastery }}%"></div>
+                                                </div>
+                                            </div>
+                                            <span class="ml-3 text-sm font-medium text-gray-900">{{ $student['avg_mastery'] }}%</span>
+                                        </div>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <span class="text-sm text-gray-900">{{ $student['mastered_points'] }} / {{ $student['total_knowledge_points'] }}</span>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+                                        {{ $student['total_attempts'] }}
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
+                                        <button 
+                                            class="text-indigo-600 hover:text-indigo-900 mr-4"
+                                            onclick="window.location.href='{{ route('filament.admin.auth.login') }}'"
+                                        >
+                                            查看详情
+                                        </button>
+                                    </td>
+                                </tr>
+                            @endforeach
+                        </tbody>
+                    </table>
+                </div>
+            @endif
+        </div>
+    @endif
+</div>

+ 72 - 0
resources/views/vendor/filament/auth/pages/login.blade.php

@@ -0,0 +1,72 @@
+<x-filament-panels::page.simple>
+    {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIMPLE_PAGE_START) }}
+
+    {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIMPLE_LAYOUT_START) }}
+
+    <div class="fi-simple-layout">
+        {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIMPLE_LAYOUT_START) }}
+
+        @if (($hasTopbar ?? true) && filament()->auth()->check())
+            <div class="fi-simple-layout-header">
+                @if (filament()->hasDatabaseNotifications())
+                    @livewire(Filament\Livewire\DatabaseNotifications::class, [
+                        'lazy' => filament()->hasLazyLoadedDatabaseNotifications(),
+                        'position' => \Filament\Enums\DatabaseNotificationsPosition::Topbar,
+                    ])
+                @endif
+
+                @if (filament()->hasUserMenu())
+                    @livewire(Filament\Livewire\SimpleUserMenu::class)
+                @endif
+            </div>
+        @endif
+
+        <div class="fi-simple-main-ctn">
+            <main
+                @class([
+                    'fi-simple-main',
+                    ($maxContentWidth instanceof \Filament\Support\Enums\Width) ? "fi-width-{$maxContentWidth->value}" : $maxContentWidth,
+                ])
+            >
+                <div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 px-4 py-12">
+                    <div class="w-full max-w-md">
+                        <!-- 自定义Logo区域 -->
+                        <div class="text-center mb-8">
+                            <div class="inline-flex items-center justify-center w-16 h-16 bg-primary-500 rounded-xl shadow-lg mb-4">
+                                <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                                </svg>
+                            </div>
+                            <h1 class="text-2xl font-bold text-slate-900 dark:text-white">
+                                数学知识图谱管理系统
+                            </h1>
+                            <p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
+                                Math Knowledge Graph Management System
+                            </p>
+                        </div>
+
+                        <!-- 登录表单容器 -->
+                        <div class="glass-panel p-8">
+                            {{ $slot }}
+                        </div>
+
+                        <!-- 页脚信息 -->
+                        <div class="text-center mt-6">
+                            <p class="text-xs text-slate-500 dark:text-slate-400">
+                                © 2024 Filament Admin. All rights reserved.
+                            </p>
+                        </div>
+                    </div>
+                </div>
+            </main>
+        </div>
+
+        {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER) }}
+
+        {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIMPLE_LAYOUT_END) }}
+    </div>
+
+    {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIMPLE_LAYOUT_END) }}
+
+    {{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIMPLE_PAGE_END) }}
+</x-filament-panels::page.simple>

+ 65 - 0
resources/views/vendor/filament/login.blade.php

@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="ltr">
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>登录 - 数学知识图谱管理系统</title>
+
+    <!-- 加载自定义样式 -->
+    <link rel="stylesheet" href="{{ asset('build/assets/app-DdCteCZN.css') }}" />
+
+    <!-- 自定义内联样式 -->
+    <style>
+        body {
+            font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            margin: 0;
+            padding: 0;
+        }
+        .login-container {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            min-height: 100vh;
+            padding: 2rem;
+        }
+        .login-card {
+            background: rgba(255, 255, 255, 0.95);
+            backdrop-filter: blur(10px);
+            border-radius: 1rem;
+            padding: 2rem;
+            box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+            width: 100%;
+            max-width: 400px;
+        }
+        .login-logo {
+            text-align: center;
+            margin-bottom: 2rem;
+        }
+        .login-logo h1 {
+            color: #0ea5e9;
+            font-size: 1.5rem;
+            font-weight: 600;
+            margin: 0;
+        }
+        .login-logo p {
+            color: #64748b;
+            font-size: 0.875rem;
+            margin: 0.5rem 0 0;
+        }
+    </style>
+</head>
+<body>
+    <div class="login-container">
+        <div class="login-card">
+            <div class="login-logo">
+                <h1>数学知识图谱管理系统</h1>
+                <p>Math Knowledge Graph Management System</p>
+            </div>
+
+            {{ $slot }}
+        </div>
+    </div>
+</body>
+</html>

+ 0 - 4
resources/views/welcome.blade.php

@@ -6,10 +6,6 @@
 
         <title>{{ config('app.name', 'Laravel') }}</title>
 
-        <!-- Fonts -->
-        <link rel="preconnect" href="https://fonts.bunny.net">
-        <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
-
         <!-- Styles / Scripts -->
         @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
             @vite(['resources/css/app.css', 'resources/js/app.js'])

+ 47 - 0
vite.config.js

@@ -8,4 +8,51 @@ export default defineConfig({
             refresh: true,
         }),
     ],
+    build: {
+        rollupOptions: {
+            output: {
+                manualChunks: (id, { getModuleInfo }) => {
+                    // 将 vendor 库分离到单独的 chunk
+                    if (id.includes('node_modules')) {
+                        // 按库名称分离 - 已优化的 chunk 配置
+                        if (id.includes('@alpinejs')) {
+                            return 'chunk-alpine';
+                        }
+                        if (id.includes('tailwindcss') || id.includes('daisyui')) {
+                            return 'chunk-ui';
+                        }
+                        if (id.includes('@popperjs') || id.includes('tippy')) {
+                            return 'chunk-popper';
+                        }
+                        if (id.includes('filament') || id.includes('forms')) {
+                            return 'chunk-filament';
+                        }
+                        if (id.includes('livewire')) {
+                            return 'chunk-livewire';
+                        }
+                        if (id.includes('@heroicons') || id.includes('lucide')) {
+                            return 'chunk-icons';
+                        }
+                        if (id.includes('intersect') || id.includes('focus-trap')) {
+                            return 'chunk-utils';
+                        }
+                        // 其他小库合并
+                        return 'chunk-vendor';
+                    }
+                },
+            },
+        },
+        // 将警告阈值提高到 1500KB (对于 Filament 应用是合理的)
+        chunkSizeWarningLimit: 1500,
+        // 启用高级压缩
+        minify: 'terser',
+        terserOptions: {
+            compress: {
+                drop_console: true,
+                drop_debugger: true,
+                pure_funcs: ['console.log'],
+                passes: 2, // 压缩两遍获得更好效果
+            },
+        },
+    },
 });