yemeishu 2 недель назад
Родитель
Сommit
78d377397d
55 измененных файлов с 6099 добавлено и 900 удалено
  1. 180 0
      app/Filament/Pages/ExamAnalysis.php
  2. 83 27
      app/Filament/Pages/ExamDetail.php
  3. 3 1
      app/Filament/Pages/Integrations/KnowledgeGraphExplorer.php
  4. 5 3
      app/Filament/Pages/IntelligentExamGeneration.php
  5. 25 0
      app/Filament/Pages/OCRPaperGrading.php
  6. 47 0
      app/Filament/Pages/QuestionDetail.php
  7. 14 51
      app/Filament/Pages/QuestionGeneration.php
  8. 2 12
      app/Filament/Pages/StudentManagement.php
  9. 118 0
      app/Filament/Resources/MenuPermissionResource.php
  10. 24 0
      app/Filament/Resources/MenuPermissionResource/Pages/CreateMenuPermission.php
  11. 24 0
      app/Filament/Resources/MenuPermissionResource/Pages/EditMenuPermission.php
  12. 255 0
      app/Filament/Resources/MenuPermissionResource/Pages/ListMenuPermissions.php
  13. 79 24
      app/Filament/Resources/TeacherResource.php
  14. 18 6
      app/Filament/Resources/TeacherResource/Pages/EditTeacher.php
  15. 22 2
      app/Filament/Resources/TeacherResource/Pages/ListTeachers.php
  16. 41 3
      app/Filament/Resources/TeacherResource/Pages/ViewTeacher.php
  17. 142 0
      app/Http/Controllers/ExamPdfController.php
  18. 54 0
      app/Http/Controllers/MenuVisibilityController.php
  19. 90 37
      app/Livewire/UploadExam/UploadForm.php
  20. 144 0
      app/Models/MenuConfig.php
  21. 90 0
      app/Models/MenuPermission.php
  22. 5 0
      app/Providers/Filament/AdminPanelProvider.php
  23. 249 0
      app/Services/ChatGPTAnalysisService.php
  24. 32 26
      app/Services/LearningAnalyticsService.php
  25. 119 0
      app/Services/MenuPermissionService.php
  26. 5 0
      app/Services/PromptService.php
  27. 18 4
      app/Services/QuestionBankService.php
  28. 3 0
      config/services.php
  29. 39 0
      debug_improved_matching.php
  30. 1203 49
      public/data/edges.json
  31. 963 262
      public/data/tree.json
  32. 1 0
      public/favicon.svg
  33. 35 0
      query_paper.php
  34. 35 0
      query_paper_details.php
  35. 66 0
      resources/css/app.css
  36. 245 0
      resources/views/components/exam/paper-body.blade.php
  37. 125 0
      resources/views/filament/components/menu-visibility-toggle.blade.php
  38. 84 0
      resources/views/filament/pages/exam-analysis-compact.blade.php
  39. 130 0
      resources/views/filament/pages/exam-analysis-standard.blade.php
  40. 145 5
      resources/views/filament/pages/exam-detail.blade.php
  41. 22 30
      resources/views/filament/pages/integrations/knowledge-graph-explorer.blade.php
  42. 70 16
      resources/views/filament/pages/intelligent-exam-generation-simple.blade.php
  43. 47 7
      resources/views/filament/pages/ocr-paper-grading.blade.php
  44. 35 29
      resources/views/filament/pages/question-detail.blade.php
  45. 7 2
      resources/views/filament/pages/question-management-simple.blade.php
  46. 5 2
      resources/views/filament/pages/question-management.blade.php
  47. 39 25
      resources/views/filament/pages/student-management.blade.php
  48. 70 0
      resources/views/filament/resources/menu-permission-resource/header.blade.php
  49. 252 0
      resources/views/filament/resources/teacher/pages/edit-teacher.blade.php
  50. 225 0
      resources/views/filament/resources/teacher/pages/view-teacher.blade.php
  51. 7 2
      resources/views/livewire/upload-exam/upload-form.blade.php
  52. 135 0
      resources/views/pdf/exam-grading.blade.php
  53. 112 275
      resources/views/pdf/exam-paper.blade.php
  54. 6 0
      routes/web.php
  55. 105 0
      test_grading_panel.sh

+ 180 - 0
app/Filament/Pages/ExamAnalysis.php

@@ -6,6 +6,7 @@ use App\Models\OCRRecord;
 use App\Models\OCRQuestionResult;
 use App\Services\LearningAnalyticsService;
 use App\Services\OCRService;
+use App\Services\ChatGPTAnalysisService;
 use BackedEnum;
 use Filament\Notifications\Notification;
 use Filament\Pages\Page;
@@ -33,6 +34,12 @@ class ExamAnalysis extends Page
     public bool $loading = true;
     public string $recordType = ''; // 'ocr' 或 'generated'
 
+    // ChatGPT识别相关
+    public ?string $imageUrl = null; // 试卷图片URL
+    public bool $useChatGPT = false; // 是否使用ChatGPT识别
+    public bool $isAnalyzing = false; // 是否正在分析
+    public array $chatGPTResult = []; // ChatGPT分析结果
+
     public function mount()
     {
         // 允许使用 recordId(OCR记录)或 paperId(系统生成卷子)
@@ -1196,4 +1203,177 @@ class ExamAnalysis extends Page
             ]);
         }
     }
+
+    /**
+     * 使用ChatGPT分析试卷图片
+     */
+    public function analyzeWithChatGPT(): void
+    {
+        $this->validate([
+            'imageUrl' => 'required|url',
+        ]);
+
+        if (empty($this->imageUrl)) {
+            Notification::make()
+                ->title('请先上传试卷图片')
+                ->danger()
+                ->send();
+            return;
+        }
+
+        $this->isAnalyzing = true;
+        $this->chatGPTResult = [];
+
+        try {
+            // 确定要分析的试卷ID
+            $targetPaperId = null;
+            if ($this->recordId) {
+                // OCR记录,使用记录ID作为paper_id
+                $targetPaperId = 'ocr_' . $this->recordId;
+            } elseif ($this->paperId) {
+                // 系统生成卷子
+                $targetPaperId = $this->paperId;
+            }
+
+            if (!$targetPaperId) {
+                throw new \Exception('未找到有效的试卷ID');
+            }
+
+            $chatGPTService = app(ChatGPTAnalysisService::class);
+            $result = $chatGPTService->analyzeExamPaper($targetPaperId, $this->imageUrl);
+
+            if ($result['success']) {
+                $this->chatGPTResult = $result['data'];
+
+                // 保存分析结果到数据库
+                $saved = $chatGPTService->saveAnalysisResult($targetPaperId, $result['data']);
+
+                // 同时提交到学习分析服务,更新掌握度
+                $this->submitToLearningAnalysis($result['data']);
+
+                if ($saved) {
+                    Notification::make()
+                        ->title('ChatGPT分析完成')
+                        ->body('已成功分析 ' . count($result['data']['questions'] ?? []) . ' 道题目')
+                        ->success()
+                        ->send();
+
+                    // 刷新分析数据
+                    $this->loadAnalysisData();
+                } else {
+                    Notification::make()
+                        ->title('分析完成但保存失败')
+                        ->body('请手动刷新查看结果')
+                        ->warning()
+                        ->send();
+                }
+            } else {
+                throw new \Exception($result['error'] ?? '未知错误');
+            }
+
+        } catch (\Exception $e) {
+            \Log::error('ChatGPT分析失败', [
+                'paper_id' => $targetPaperId ?? 'unknown',
+                'error' => $e->getMessage()
+            ]);
+
+            Notification::make()
+                ->title('ChatGPT分析失败')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
+        } finally {
+            $this->isAnalyzing = false;
+        }
+    }
+
+    /**
+     * 将ChatGPT分析结果提交到学习分析服务
+     */
+    private function submitToLearningAnalysis(array $analysisData): void
+    {
+        try {
+            if (!isset($analysisData['questions'])) {
+                return;
+            }
+
+            $studentId = $this->studentInfo['student_id'] ?? null;
+            if (!$studentId) {
+                \Log::warning('未找到学生ID,跳过学习分析提交');
+                return;
+            }
+
+            // 转换ChatGPT结果为学习分析服务期望的格式
+            $answers = [];
+            foreach ($analysisData['questions'] as $question) {
+                // 获取知识点名称(ChatGPT返回的是name,不是id)
+                $kpCode = null;
+                if (isset($question['knowledge_points']) && !empty($question['knowledge_points'])) {
+                    $kpCode = $question['knowledge_points'][0]['name'] ?? $question['knowledge_points'][0]['id'] ?? null;
+                }
+
+                $answers[] = [
+                    'question_id' => $question['q'] ?? null,
+                    'student_answer' => $question['student_answer'] ?? null,
+                    'correct_answer' => $question['correct_answer'] ?? null,
+                    'is_correct' => $question['is_correct'] ?? false,
+                    'score' => $question['is_correct'] ? 1 : 0,
+                    'max_score' => 1,
+                    'kp_code' => $kpCode,
+                ];
+            }
+
+            $submissionData = [
+                'paper_id' => $this->paperId ?? ('ocr_' . $this->recordId),
+                'answers' => $answers,
+            ];
+
+            \Log::info('提交ChatGPT分析结果到学习分析服务', [
+                'student_id' => $studentId,
+                'paper_id' => $submissionData['paper_id'],
+                'question_count' => count($answers),
+                'sample_kp_code' => $answers[0]['kp_code'] ?? 'N/A'
+            ]);
+
+            // 调用学习分析服务
+            $learningService = app(\App\Services\LearningAnalyticsService::class);
+            $response = $learningService->submitBatchAttempts($studentId, $submissionData);
+
+            \Log::info('ChatGPT分析结果已提交到学习分析服务', [
+                'student_id' => $studentId,
+                'paper_id' => $submissionData['paper_id'],
+                'question_count' => count($answers),
+                'response_success' => !isset($response['error'])
+            ]);
+
+        } catch (\Exception $e) {
+            \Log::error('提交ChatGPT分析结果到学习分析服务失败', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        }
+    }
+
+    /**
+     * 切换识别模式
+     */
+    public function updatedUseChatGPT($value): void
+    {
+        if ($value) {
+            Notification::make()
+                ->title('ChatGPT识别模式')
+                ->body('将使用ChatGPT进行试卷智能分析,无需OCR识别,可直接分析图片中的学生答案')
+                ->info()
+                ->send();
+        }
+    }
+
+    /**
+     * 重置ChatGPT分析表单
+     */
+    public function resetChatGPTForm(): void
+    {
+        $this->reset(['imageUrl', 'useChatGPT', 'chatGPTResult']);
+        $this->isAnalyzing = false;
+    }
 }

+ 83 - 27
app/Filament/Pages/ExamDetail.php

@@ -38,6 +38,9 @@ class ExamDetail extends Page
     // 是否正在加载
     public bool $isLoading = false;
 
+    // 是否显示预览
+    public bool $showPreview = false;
+
     public function mount()
     {
         // 从URL查询参数获取paperId
@@ -93,6 +96,9 @@ class ExamDetail extends Page
         } else {
             $this->paperDetail = [];
         }
+
+        // 切换试卷时关闭预览,避免展示旧数据
+        $this->showPreview = false;
     }
 
     /**
@@ -322,40 +328,90 @@ class ExamDetail extends Page
     }
 
     /**
-     * 导出PDF
-     * 注意:当前外部API可能不稳定,可能导致导出失败
+     * 预览卷子
+     * 显示试卷预览,可以直接打印
      */
-    public function exportPdf()
+    public function previewPaper()
     {
-        try {
-            $questionBankService = app(QuestionBankService::class);
-            $pdfUrl = $questionBankService->exportExamToPdf($this->paperId);
-
-            if ($pdfUrl) {
-                Notification::make()
-                    ->title('PDF导出成功')
-                    ->body('试卷已导出为PDF格式')
-                    ->success()
-                    ->send();
-            } else {
-                Notification::make()
-                    ->title('PDF导出暂时不可用')
-                    ->body('外部题库服务暂时不可用,请稍后重试或联系管理员')
-                    ->warning()
-                    ->send();
-            }
-        } catch (\Exception $e) {
-            Log::error('PDF导出异常', [
-                'paper_id' => $this->paperId,
-                'error' => $e->getMessage()
-            ]);
+        $previewUrl = $this->getPreviewUrl();
+        $gradingUrl = $this->getGradingUrl();
 
+        if (!$previewUrl || !$gradingUrl) {
             Notification::make()
-                ->title('PDF导出失败')
-                ->body('导出过程中发生错误:' . $e->getMessage())
+                ->title('无法预览试卷')
+                ->body('当前试卷 ID 不存在或尚未加载,无法展示试卷或判卷')
                 ->danger()
                 ->send();
+            return;
         }
+
+        $this->showPreview = true;
+
+        // 通知前端刷新 iframe,保持与智能出卷页面一致
+        $this->dispatch('refresh-preview', previewUrl: $previewUrl, gradingUrl: $gradingUrl);
+
+        Notification::make()
+            ->title('已打开试卷与判卷预览')
+            ->body('预览区域会依次展示试卷正文和判卷页,便于连贯查看')
+            ->success()
+            ->send();
+    }
+
+    /**
+     * 打印试卷
+     */
+    public function printPaper()
+    {
+        $previewUrl = $this->getPreviewUrl();
+        $gradingUrl = $this->getGradingUrl();
+
+        if (!$previewUrl || !$gradingUrl) {
+            Notification::make()
+                ->title('无法打印试卷或判卷')
+                ->body('当前试卷 ID 不存在或尚未加载,无法启动打印')
+                ->danger()
+                ->send();
+            return;
+        }
+
+        $this->showPreview = true;
+
+        // 使用智能出卷 PDF 预览进行打印,保证样式一致
+        $this->dispatch('print-paper', url: $previewUrl);
+        $this->dispatch('print-paper', url: $gradingUrl);
+
+        Notification::make()
+            ->title('打印功能已启动')
+            ->body('已同时打开试卷与判卷的打印窗口,如未弹出请检查浏览器弹窗设置')
+            ->info()
+            ->send();
+    }
+
+    private function getPreviewUrl(): ?string
+    {
+        $paperId = $this->paperDetail['paper_id'] ?? $this->paperId;
+
+        if (empty($paperId)) {
+            return null;
+        }
+
+        return route('filament.admin.auth.intelligent-exam.pdf', [
+            'paper_id' => $paperId,
+            'answer' => 'false',
+        ]);
+    }
+
+    private function getGradingUrl(): ?string
+    {
+        $paperId = $this->paperDetail['paper_id'] ?? $this->paperId;
+
+        if (empty($paperId)) {
+            return null;
+        }
+
+        return route('filament.admin.auth.intelligent-exam.grading', [
+            'paper_id' => $paperId,
+        ]);
     }
 
     /**

+ 3 - 1
app/Filament/Pages/Integrations/KnowledgeGraphExplorer.php

@@ -18,6 +18,7 @@ class KnowledgeGraphExplorer extends Page
     protected string $view = 'filament.pages.integrations.knowledge-graph-explorer';
 
     public ?string $selectedKpCode = null;
+    protected ?string $subheading = '点击任意节点查看详细信息,包括子知识点和技能点';
 
     public function mount(Request $request): void
     {
@@ -27,7 +28,8 @@ class KnowledgeGraphExplorer extends Page
     public function getBreadcrumbs(): array
     {
         return [
-            'knowledge-graph-explorer' => '知识图谱浏览',
+            url('/admin') => '首页',
+            0 => '知识图谱浏览', // 当前页不做链接,避免重复点击
         ];
     }
 }

+ 5 - 3
app/Filament/Pages/IntelligentExamGeneration.php

@@ -31,10 +31,11 @@ class IntelligentExamGeneration extends Page
     // 基本配置
     public ?string $paperName = '';
     public ?string $paperDescription = '';
+    public ?string $selectedGrade = '初中'; // 初中/七年级/八年级/九年级
     public ?string $difficultyCategory = '基础'; // 基础/进阶/竞赛
     public int $totalQuestions = 20;
     public int $totalScore = 100;
-    public bool $includeAnswer = true; // 是否生成答案
+    public bool $includeAnswer = false; // 是否生成答案(默认不生成参考答案)
 
     // 知识点和技能点选择
     public array $selectedKpCodes = [];
@@ -587,6 +588,7 @@ class IntelligentExamGeneration extends Page
             // 准备出卷参数
             $examParams = [
                 'student_id' => $this->selectedStudentId,
+                'grade' => $this->selectedGrade,
                 'total_questions' => $this->totalQuestions,
                 'kp_codes' => $this->selectedKpCodes,
                 'skills' => $this->selectedSkills,
@@ -1429,14 +1431,14 @@ Cache::put('generated_exam_' . $paperId, $examData, now()->addHour());
         // 调用PDF导出API
         return redirect()->route('filament.admin.auth.intelligent-exam.pdf', [
             'paper_id' => $this->generatedPaperId,
-            'answer' => $this->includeAnswer ? 'true' : 'false'
+            'answer' => 'false'
         ]);
     }
 
     public function resetForm()
     {
         $this->reset([
-            'paperName', 'paperDescription', 'selectedKpCodes', 'selectedSkills',
+            'paperName', 'paperDescription', 'selectedGrade', 'selectedKpCodes', 'selectedSkills',
             'selectedTeacherId', 'selectedStudentId', 'filterByStudentWeakness', 'generatedQuestions', 'generatedPaperId'
         ]);
 

+ 25 - 0
app/Filament/Pages/OCRPaperGrading.php

@@ -4,11 +4,13 @@ namespace App\Filament\Pages;
 
 use App\Models\OCRRecord;
 use App\Services\ExamPaperService;
+use App\Services\ChatGPTAnalysisService;
 use App\Jobs\RegradeOCRSubmission;
 use App\Filament\Traits\HasUserRole;
 use Filament\Pages\Page;
 use Filament\Notifications\Notification;
 use Livewire\Attributes\Computed;
+use Illuminate\Support\Facades\Log;
 
 class OCRPaperGrading extends Page
 {
@@ -39,6 +41,15 @@ class OCRPaperGrading extends Page
     public ?int $selectedRecordId = null;
     public ?OCRRecord $selectedRecord = null;
 
+    // ChatGPT识别相关
+    public bool $useChatGPT = false;
+
+    // 获取识别模式文本
+    public function getRecognitionModeText(): string
+    {
+        return $this->useChatGPT ? 'ChatGPT智能识别' : 'OCR识别 + AI判分';
+    }
+
     public function mount(): void
     {
         $this->initializeUserRole();
@@ -121,4 +132,18 @@ class OCRPaperGrading extends Page
             ->limit(10)
             ->get();
     }
+
+    /**
+     * 切换识别模式
+     */
+    public function updatedUseChatGPT($value): void
+    {
+        if ($value) {
+            Notification::make()
+                ->title('ChatGPT识别模式')
+                ->body('将使用ChatGPT进行试卷智能分析,无需OCR识别')
+                ->info()
+                ->send();
+        }
+    }
 }

+ 47 - 0
app/Filament/Pages/QuestionDetail.php

@@ -261,6 +261,53 @@ class QuestionDetail extends Page
         return $kpMap[$kpCode] ?? $kpCode;
     }
 
+    public function getSkillNames(): array
+    {
+        $skillCodes = $this->questionData['skills'] ?? [];
+        if (empty($skillCodes) || !is_array($skillCodes)) {
+            return [];
+        }
+
+        static $skillMap = null;
+        if ($skillMap === null) {
+            $skillMap = [];
+            try {
+                $service = app(KnowledgeGraphService::class);
+                // 优先拉全量技能映射
+                $resp = $service->listSkills(1, 1000);
+                $skills = $resp['data'] ?? $resp ?? [];
+                if (is_array($skills)) {
+                    foreach ($skills as $skill) {
+                        $code = $skill['code'] ?? null;
+                        if (!$code) {
+                            continue;
+                        }
+                        $skillMap[$code] = $skill['name'] ?? $code;
+                    }
+                }
+                // 如果仍为空,尝试按知识点单独获取
+                if (empty($skillMap) && !empty($this->questionData['kp_code'])) {
+                    $kpSkills = $service->getSkillsByKnowledgePoint($this->questionData['kp_code']);
+                    foreach ($kpSkills as $skill) {
+                        $code = $skill['code'] ?? null;
+                        if (!$code) {
+                            continue;
+                        }
+                        $skillMap[$code] = $skill['name'] ?? $code;
+                    }
+                }
+            } catch (\Throwable $e) {
+                Log::warning('Load skills failed', [
+                    'error' => $e->getMessage(),
+                ]);
+            }
+        }
+
+        return array_map(function ($code) use ($skillMap) {
+            return $skillMap[$code] ?? $code;
+        }, $skillCodes);
+    }
+
     protected function loadHistorySummary(): void
     {
         if (!$this->studentId || !$this->questionId) {

+ 14 - 51
app/Filament/Pages/QuestionGeneration.php

@@ -154,7 +154,7 @@ class QuestionGeneration extends Page
 
             \Log::info("[QuestionGen] 开始异步生成,callback URL: " . $callbackUrl);
 
-            // 异步请求生成题目
+            // 生成参数(交由服务发送,带超时与重试)
             $params = [
                 'kp_code' => $this->generateKpCode,
                 'skills' => $this->selectedSkills,
@@ -167,57 +167,20 @@ class QuestionGeneration extends Page
             // 添加回调 URL
             $params['callback_url'] = $callbackUrl;
 
-            // 获取 base URL(通过公共方法或配置)
-            $baseUrl = config('services.question_bank.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015'));
-            $baseUrl = rtrim($baseUrl, '/');
-            if (!str_ends_with($baseUrl, '/api')) {
-                $baseUrl .= '/api';
-            }
+            // 通过服务请求,期望快速返回 task_id(超时与重试由服务配置)
+            $response = $service->generateIntelligentQuestions($params, $callbackUrl);
 
-            // 直接发送异步请求,不使用队列
-            try {
-                // 先立即跳转,避免阻塞
-                $redirectUrl = "/admin/question-management";
-                $this->js("window.location.href = '{$redirectUrl}';");
-
-                // 使用 stream 方式快速发送请求
-                $ch = curl_init();
-                curl_setopt($ch, CURLOPT_URL, $baseUrl . '/ai/generate-intelligent-questions');
-                curl_setopt($ch, CURLOPT_POST, true);
-                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
-                curl_setopt($ch, CURLOPT_HTTPHEADER, [
-                    'Content-Type: application/json',
-                    'Accept: application/json'
-                ]);
-                curl_setopt($ch, CURLOPT_TIMEOUT, 1); // 1秒超时,只确保请求发出
-                curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
-                curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
-                curl_setopt($ch, CURLOPT_FORBID_REUSE, true);
-                curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
-
-                // 异步执行,不等待响应
-                curl_exec($ch);
-                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-                curl_close($ch);
-
-                \Log::info("[QuestionGen] 异步请求已发送", [
-                    'http_code' => $httpCode
-                ]);
-
-                return; // 立即返回
-
-            } catch (\Exception $e) {
-                $this->isGenerating = false;
-                \Log::error("[QuestionGen] 发送异步请求失败", [
-                    'error' => $e->getMessage()
-                ]);
-
-                Notification::make()
-                    ->title('❌ 请求发送失败')
-                    ->body('请检查网络连接并重试')
-                    ->danger()
-                    ->send();
-            }
+            \Log::info("[QuestionGen] 生成请求已发送", [
+                'response' => $response,
+                'kp_code' => $this->generateKpCode,
+                'skills' => $this->selectedSkills,
+                'count' => $this->questionCount,
+            ]);
+
+            // 快速跳转到题目管理页,等待回调提示
+            $redirectUrl = "/admin/question-management";
+            $this->js("window.location.href = '{$redirectUrl}';");
+            return;
         } catch (\Illuminate\Http\Client\ConnectionException $e) {
             $this->isGenerating = false;
             \Log::error("[QuestionGen] 连接异常: " . $e->getMessage());

+ 2 - 12
app/Filament/Pages/StudentManagement.php

@@ -23,12 +23,10 @@ class StudentManagement extends Page implements HasTable
 {
     use HasUserRole, InteractsWithTable;
 
+    protected static ?string $title = '师生管理';
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-academic-cap';
-
     protected static ?string $navigationLabel = '师生管理';
-
     protected static string|UnitEnum|null $navigationGroup = '操作';
-
     protected static ?int $navigationSort = 1;
 
     protected string $view = 'filament.pages.student-management';
@@ -42,15 +40,7 @@ class StudentManagement extends Page implements HasTable
         $this->initializeUserRole();
     }
 
-    public function getTitle(): string
-    {
-        return '师生管理';
-    }
-
-    public function getBreadcrumb(): string
-    {
-        return '师生管理';
-    }
+    protected ?string $subheading = '集中管理老师与学生信息,支持筛选与快速操作';
 
 
     public function filterByTeacher(?string $teacherId): void

+ 118 - 0
app/Filament/Resources/MenuPermissionResource.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace App\Filament\Resources;
+
+use App\Filament\Resources\MenuPermissionResource\Pages;
+use App\Models\MenuPermission;
+use Filament\Resources\Resource;
+use Filament\Tables;
+use Filament\Tables\Table;
+use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Columns\ToggleColumn;
+use Filament\Tables\Columns\BadgeColumn;
+use Filament\Tables\Filters\SelectFilter;
+use Filament\Tables\Filters\TernaryFilter;
+use Filament\Actions;
+use Filament\Actions\EditAction;
+use Filament\Actions\DeleteAction;
+use Filament\Actions\BulkAction;
+use BackedEnum;
+use UnitEnum;
+
+class MenuPermissionResource extends Resource
+{
+    protected static ?string $model = MenuPermission::class;
+
+    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-squares-2x2';
+    protected static ?string $navigationLabel = '菜单权限管理';
+    protected static string|UnitEnum|null $navigationGroup = '系统管理';
+    protected static ?int $navigationSort = 100;
+
+    public static function table(Table $table): Table
+    {
+        return $table
+            ->columns([
+                TextColumn::make('user_id')
+                    ->label('用户ID')
+                    ->searchable()
+                    ->sortable(),
+                TextColumn::make('menu_key')
+                    ->label('菜单标识')
+                    ->searchable()
+                    ->sortable(),
+                TextColumn::make('menu_label')
+                    ->label('菜单名称')
+                    ->searchable(),
+                TextColumn::make('menu_group')
+                    ->label('菜单分组')
+                    ->badge()
+                    ->color('gray'),
+                TextColumn::make('menu_url')
+                    ->label('菜单URL')
+                    ->limit(50)
+                    ->copyable(),
+                ToggleColumn::make('is_visible')
+                    ->label('是否可见')
+                    ->sortable(),
+                TextColumn::make('sort_order')
+                    ->label('排序')
+                    ->sortable(),
+                TextColumn::make('created_at')
+                    ->label('创建时间')
+                    ->dateTime('Y-m-d H:i')
+                    ->sortable()
+                    ->toggleable(isToggledHiddenByDefault: true),
+            ])
+            ->filters([
+                SelectFilter::make('menu_group')
+                    ->label('菜单分组')
+                    ->options([
+                        '管理' => '管理',
+                        '工具' => '工具',
+                        '分析' => '分析',
+                    ])
+                    ->multiple(),
+                TernaryFilter::make('is_visible')
+                    ->label('可见状态')
+                    ->placeholder('全部')
+                    ->trueLabel('可见')
+                    ->falseLabel('隐藏'),
+            ])
+            ->actions([
+                EditAction::make(),
+                DeleteAction::make(),
+            ])
+            ->bulkActions([
+                Actions\BulkActionGroup::make([
+                    Actions\DeleteBulkAction::make(),
+                    BulkAction::make('toggle_visibility')
+                        ->label('批量切换可见性')
+                        ->icon('heroicon-m-eye')
+                        ->action(function ($records) {
+                            foreach ($records as $record) {
+                                $record->update(['is_visible' => !$record->is_visible]);
+                            }
+                        }),
+                ]),
+            ])
+            ->defaultSort('sort_order')
+            ->paginated([10, 25, 50, 100])
+            ->poll('30s');
+    }
+
+    public static function getRelations(): array
+    {
+        return [
+            //
+        ];
+    }
+
+    public static function getPages(): array
+    {
+        return [
+            'index' => Pages\ListMenuPermissions::route('/'),
+            'create' => Pages\CreateMenuPermission::route('/create'),
+            'edit' => Pages\EditMenuPermission::route('/{record}/edit'),
+        ];
+    }
+}

+ 24 - 0
app/Filament/Resources/MenuPermissionResource/Pages/CreateMenuPermission.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Filament\Resources\MenuPermissionResource\Pages;
+
+use App\Filament\Resources\MenuPermissionResource;
+use Filament\Actions;
+use Filament\Resources\Pages\CreateRecord;
+
+class CreateMenuPermission extends CreateRecord
+{
+    protected static string $resource = MenuPermissionResource::class;
+
+    protected function mutateFormDataBeforeCreate(array $data): array
+    {
+        // 如果没有提供排序,默认使用当前用户的最大排序+1
+        if (empty($data['sort_order'])) {
+            $maxOrder = \App\Models\MenuPermission::where('user_id', $data['user_id'])
+                ->max('sort_order');
+            $data['sort_order'] = ($maxOrder ?? -1) + 1;
+        }
+
+        return $data;
+    }
+}

+ 24 - 0
app/Filament/Resources/MenuPermissionResource/Pages/EditMenuPermission.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Filament\Resources\MenuPermissionResource\Pages;
+
+use App\Filament\Resources\MenuPermissionResource;
+use Filament\Actions;
+use Filament\Resources\Pages\EditRecord;
+
+class EditMenuPermission extends EditRecord
+{
+    protected static string $resource = MenuPermissionResource::class;
+
+    protected function getHeaderActions(): array
+    {
+        return [
+            Actions\DeleteAction::make(),
+        ];
+    }
+
+    protected function mutateFormDataBeforeSave(array $data): array
+    {
+        return $data;
+    }
+}

+ 255 - 0
app/Filament/Resources/MenuPermissionResource/Pages/ListMenuPermissions.php

@@ -0,0 +1,255 @@
+<?php
+
+namespace App\Filament\Resources\MenuPermissionResource\Pages;
+
+use App\Filament\Resources\MenuPermissionResource;
+use App\Models\Teacher;
+use Filament\Actions;
+use Filament\Resources\Pages\ListRecords;
+use Filament\Support\Enums\FontWeight;
+use Filament\Tables;
+use Filament\Tables\Table;
+use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Columns\ToggleColumn;
+use Filament\Tables\Columns\BadgeColumn;
+use Filament\Tables\Filters\SelectFilter;
+use Filament\Tables\Filters\TernaryFilter;
+use Filament\Forms;
+use Filament\Forms\Components\Select;
+use Filament\Forms\Components\Toggle;
+use Filament\Forms\Components\TextInput;
+use Filament\Forms\Components\Textarea;
+
+class ListMenuPermissions extends ListRecords
+{
+    public ?string $selectedTeacherId = null;
+
+    protected static string $resource = MenuPermissionResource::class;
+
+    public function mount(): void
+    {
+        parent::mount();
+
+        // 默认选择第一个老师
+        if (!$this->selectedTeacherId) {
+            $firstTeacher = Teacher::first();
+            if ($firstTeacher) {
+                $this->selectedTeacherId = $firstTeacher->teacher_id;
+            }
+        }
+    }
+
+    /**
+     * 更新选中的老师
+     */
+    public function updatedSelectedTeacherId(): void
+    {
+        // 当老师选择改变时,刷新表格数据
+        $this->resetPage();
+    }
+
+    public function table(Table $table): Table
+    {
+        return $table
+            ->query(
+                $this->selectedTeacherId
+                    ? \App\Models\MenuPermission::where('user_id', $this->selectedTeacherId)
+                    : \App\Models\MenuPermission::where('id', 0) // 返回空结果
+            )
+            ->columns([
+                TextColumn::make('user_id')
+                    ->label('老师ID')
+                    ->searchable()
+                    ->sortable(),
+                TextColumn::make('menu_key')
+                    ->label('菜单标识')
+                    ->searchable()
+                    ->sortable(),
+                TextColumn::make('menu_label')
+                    ->label('菜单名称')
+                    ->searchable(),
+                TextColumn::make('menu_group')
+                    ->label('菜单分组')
+                    ->badge()
+                    ->color('gray'),
+                TextColumn::make('menu_url')
+                    ->label('菜单URL')
+                    ->limit(50)
+                    ->copyable(),
+                ToggleColumn::make('is_visible')
+                    ->label('是否可见')
+                    ->sortable(),
+                TextColumn::make('sort_order')
+                    ->label('排序')
+                    ->sortable(),
+                TextColumn::make('created_at')
+                    ->label('创建时间')
+                    ->dateTime('Y-m-d H:i')
+                    ->sortable()
+                    ->toggleable(isToggledHiddenByDefault: true),
+            ])
+            ->filters([
+                SelectFilter::make('menu_group')
+                    ->label('菜单分组')
+                    ->options([
+                        '管理' => '管理',
+                        '工具' => '工具',
+                        '分析' => '分析',
+                    ])
+                    ->multiple(),
+                TernaryFilter::make('is_visible')
+                    ->label('可见状态')
+                    ->placeholder('全部')
+                    ->trueLabel('可见')
+                    ->falseLabel('隐藏'),
+            ])
+            ->actions([
+                Actions\EditAction::make(),
+                Actions\DeleteAction::make(),
+            ])
+            ->bulkActions([
+                Actions\BulkActionGroup::make([
+                    Actions\DeleteBulkAction::make(),
+                    Actions\BulkAction::make('toggle_visibility')
+                        ->label('批量切换可见性')
+                        ->icon('heroicon-m-eye')
+                        ->action(function ($records) {
+                            foreach ($records as $record) {
+                                $record->update(['is_visible' => !$record->is_visible]);
+                            }
+                        }),
+                ]),
+            ])
+            ->defaultSort('sort_order')
+            ->paginated([10, 25, 50, 100])
+            ->poll('30s');
+    }
+
+    protected function getHeaderActions(): array
+    {
+        $teacher = $this->selectedTeacherId
+            ? Teacher::where('teacher_id', $this->selectedTeacherId)->first()
+            : null;
+
+        return [
+            Actions\CreateAction::make()
+                ->visible(fn () => $this->selectedTeacherId !== null),
+
+            Actions\Action::make('初始化选中老师菜单')
+                ->label('为当前老师初始化菜单')
+                ->icon('heroicon-o-plus-circle')
+                ->color('success')
+                ->visible(fn () => $this->selectedTeacherId !== null)
+                ->action(function () use ($teacher) {
+                    if (!$teacher) {
+                        \Filament\Notifications\Notification::make()
+                            ->title('错误')
+                            ->body('请先选择老师')
+                            ->danger()
+                            ->send();
+                        return;
+                    }
+
+                    $menuKeys = [
+                        'dashboard' => ['label' => '仪表盘', 'group' => '管理', 'sort' => 0],
+                        'exam-history' => ['label' => '考试历史', 'group' => '管理', 'sort' => 1],
+                        'exam-analysis' => ['label' => '考试分析', 'group' => '分析', 'sort' => 2],
+                        'ocr-paper-grading' => ['label' => 'OCR试卷批改', 'group' => '工具', 'sort' => 3],
+                        'intelligent-exam-generation' => ['label' => '智能出卷', 'group' => '工具', 'sort' => 4],
+                        'knowledge-graph' => ['label' => '知识图谱', 'group' => '分析', 'sort' => 5],
+                        'student-management' => ['label' => '学生管理', 'group' => '管理', 'sort' => 6],
+                        'teacher-management' => ['label' => '老师管理', 'group' => '管理', 'sort' => 7],
+                    ];
+
+                    foreach ($menuKeys as $key => $data) {
+                        \App\Models\MenuPermission::updateOrCreate(
+                            [
+                                'user_id' => $teacher->teacher_id,
+                                'menu_key' => $key,
+                            ],
+                            [
+                                'menu_label' => $data['label'],
+                                'menu_group' => $data['group'],
+                                'is_visible' => true,
+                                'sort_order' => $data['sort'],
+                            ]
+                        );
+                    }
+
+                    // 刷新表格数据
+                    $this->resetPage();
+
+                    \Filament\Notifications\Notification::make()
+                        ->title('菜单初始化完成')
+                        ->body('已为老师 ' . $teacher->teacher_id . ' 初始化菜单权限')
+                        ->success()
+                        ->send();
+                }),
+
+            Actions\Action::make('初始化所有老师菜单')
+                ->label('为所有老师初始化菜单')
+                ->icon('heroicon-o-users')
+                ->color('primary')
+                ->action(function () {
+                    $teachers = Teacher::all();
+
+                    $menuKeys = [
+                        'dashboard' => ['label' => '仪表盘', 'group' => '管理', 'sort' => 0],
+                        'exam-history' => ['label' => '考试历史', 'group' => '管理', 'sort' => 1],
+                        'exam-analysis' => ['label' => '考试分析', 'group' => '分析', 'sort' => 2],
+                        'ocr-paper-grading' => ['label' => 'OCR试卷批改', 'group' => '工具', 'sort' => 3],
+                        'intelligent-exam-generation' => ['label' => '智能出卷', 'group' => '工具', 'sort' => 4],
+                        'knowledge-graph' => ['label' => '知识图谱', 'group' => '分析', 'sort' => 5],
+                        'student-management' => ['label' => '学生管理', 'group' => '管理', 'sort' => 6],
+                        'teacher-management' => ['label' => '老师管理', 'group' => '管理', 'sort' => 7],
+                    ];
+
+                    foreach ($teachers as $teacher) {
+                        foreach ($menuKeys as $key => $data) {
+                            \App\Models\MenuPermission::updateOrCreate(
+                                [
+                                    'user_id' => $teacher->teacher_id,
+                                    'menu_key' => $key,
+                                ],
+                                [
+                                    'menu_label' => $data['label'],
+                                    'menu_group' => $data['group'],
+                                    'is_visible' => true,
+                                    'sort_order' => $data['sort'],
+                                ]
+                            );
+                        }
+                    }
+
+                    \Filament\Notifications\Notification::make()
+                        ->title('菜单初始化完成')
+                        ->body('已为 ' . $teachers->count() . ' 位老师初始化菜单权限')
+                        ->success()
+                        ->send();
+                }),
+        ];
+    }
+
+    protected function getTableContentGrid(): ?array
+    {
+        return [
+            'md' => 2,
+            'xl' => 3,
+        ];
+    }
+
+    protected function getTableHeader(): \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|null
+    {
+        $teacher = $this->selectedTeacherId
+            ? Teacher::where('teacher_id', $this->selectedTeacherId)->first()
+            : null;
+
+        if ($teacher) {
+            return view('filament.resources.menu-permission-resource.header', [
+                'teacher' => $teacher,
+            ]);
+        }
+
+        return null;
+    }
+}

+ 79 - 24
app/Filament/Resources/TeacherResource.php

@@ -8,12 +8,18 @@ use App\Models\User;
 use Filament\Forms\Components\TextInput;
 use Filament\Forms\Components\Select;
 use Filament\Forms\Components\Textarea;
+use Filament\Forms\Components\Placeholder;
 use Filament\Resources\Resource;
 use Filament\Schemas\Schema;
 use Filament\Tables;
 use Filament\Tables\Table;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Hash;
+use Filament\Actions\Action;
+use Filament\Actions\BulkAction;
+use Filament\Actions\BulkActionGroup;
+use Filament\Actions\DeleteAction;
+use Filament\Actions\EditAction;
 
 class TeacherResource extends Resource
 {
@@ -24,47 +30,54 @@ class TeacherResource extends Resource
     public static function form(Schema $schema): Schema
     {
         return $schema->schema([
+            // 基本信息字段
             TextInput::make('teacher_id')
                 ->label('教师ID')
                 ->disabled()
-                ->hidden(fn (?Teacher $record) => blank($record))
-                ->formatStateUsing(fn (?Teacher $record): string => $record?->teacher_id ?? ''),
+                ->columnSpanFull(),
+
             TextInput::make('name')
                 ->label('教师姓名')
                 ->required()
                 ->maxLength(128)
-                ->placeholder('请输入教师姓名'),
+                ->placeholder('请输入教师姓名')
+                ->columnSpanFull()
+                ->extraAttributes(['style' => 'height: 3rem']),
+
             TextInput::make('subject')
                 ->label('教授科目')
                 ->required()
                 ->maxLength(64)
-                ->placeholder('例如:数学、语文、英语等'),
+                ->placeholder('例如:数学、语文、英语等')
+                ->columnSpanFull()
+                ->extraAttributes(['style' => 'height: 3rem']),
+
+            // 登录信息字段
             TextInput::make('user.username')
                 ->label('手机号(登录用户名)')
-                ->required()
-                ->regex('/^1[3-9]\d{9}$/')
-                ->placeholder('请输入11位手机号(将作为登录用户名)')
-                ->helperText('请输入11位手机号码,将作为登录用户名')
-                ->maxLength(11)
-                ->minLength(11),
-            TextInput::make('user.email')
-                ->label('邮箱地址(可选)')
-                ->email()
-                ->nullable()
-                ->placeholder('请输入邮箱地址(可选)'),
+                ->disabled()
+                ->columnSpanFull()
+                ->extraAttributes(['style' => 'height: 3rem']),
+
             TextInput::make('user.password_hash')
                 ->label('密码')
-                ->required()
                 ->password()
                 ->revealable()
-                ->dehydrateStateUsing(fn ($state) => Hash::make($state))
-                ->placeholder('请输入密码'),
+                ->dehydrateStateUsing(function ($state) {
+                    return $state ? Hash::make($state) : null;
+                })
+                ->placeholder('留空则不修改密码')
+                ->helperText('留空则不修改原密码')
+                ->columnSpanFull()
+                ->extraAttributes(['style' => 'height: 3rem']),
+
+            // 备注字段
             Textarea::make('remark')
                 ->label('备注')
-                ->rows(3)
+                ->rows(4)
                 ->placeholder('请输入备注信息(可选)')
                 ->columnSpanFull(),
-        ])->columns(2);
+        ])->columns(1);
     }
 
     public static function table(Table $table): Table
@@ -124,8 +137,50 @@ class TeacherResource extends Resource
                     ->options(fn () => self::subjectOptions())
                     ->placeholder('全部科目'),
             ])
-            ->actions([])
-            ->bulkActions([])
+            ->actions([
+                Action::make('view')
+                    ->label('查看')
+                    ->icon('heroicon-o-eye')
+                    ->url(fn ($record) => static::getUrl('view', ['record' => $record])),
+                EditAction::make()
+                    ->label('编辑')
+                    ->url(fn ($record) => static::getUrl('edit', ['record' => $record])),
+                Action::make('delete')
+                    ->label('删除')
+                    ->icon('heroicon-o-trash')
+                    ->color('danger')
+                    ->requiresConfirmation()
+                    ->modalDescription('确定要删除这位教师吗?此操作不可恢复。')
+                    ->action(function ($record) {
+                        $record->delete();
+                        \Filament\Notifications\Notification::make()
+                            ->success()
+                            ->title('删除成功')
+                            ->body("教师 {$record->name} 已被删除")
+                            ->send();
+                    }),
+            ])
+            ->bulkActions([
+                BulkActionGroup::make([
+                    BulkAction::make('deleteBulk')
+                        ->label('批量删除')
+                        ->icon('heroicon-o-trash')
+                        ->color('danger')
+                        ->requiresConfirmation()
+                        ->modalDescription('确定要删除选中的教师吗?此操作不可恢复。')
+                        ->action(function ($records) {
+                            $count = $records->count();
+                            foreach ($records as $record) {
+                                $record->delete();
+                            }
+                            \Filament\Notifications\Notification::make()
+                                ->success()
+                                ->title('批量删除成功')
+                                ->body("已删除 {$count} 位教师")
+                                ->send();
+                        }),
+                ]),
+            ])
             ->emptyStateHeading('暂无教师记录')
             ->emptyStateDescription('开始添加第一位教师吧')
             ->emptyStateActions([]);
@@ -141,7 +196,7 @@ class TeacherResource extends Resource
         ];
     }
 
-    protected static function subjectOptions(): array
+    public static function subjectOptions(): array
     {
         return [
             'math' => '数学',
@@ -156,4 +211,4 @@ class TeacherResource extends Resource
             'other' => '其他',
         ];
     }
-}
+}

+ 18 - 6
app/Filament/Resources/TeacherResource/Pages/EditTeacher.php

@@ -8,10 +8,11 @@ use Filament\Resources\Pages\EditRecord;
 class EditTeacher extends EditRecord
 {
     protected static string $resource = TeacherResource::class;
+    protected string $view = 'filament.resources.teacher.pages.edit-teacher';
 
     protected function getRedirectUrl(): string
     {
-        return $this->getResource()::getUrl('index');
+        return $this->getResource()::getUrl('view', ['record' => $this->record]);
     }
 
     protected function getSavedNotificationTitle(): ?string
@@ -29,15 +30,26 @@ class EditTeacher extends EditRecord
 
     public function getTitle(): string
     {
-        return '编辑教师';
+        return '编辑教师 - ' . $this->record->name;
     }
 
     public function getBreadcrumbs(): array
+    {
+        return [];
+    }
+
+    protected function getHeaderActions(): array
+    {
+        return [];
+    }
+
+    protected function getFormActions(): array
     {
         return [
-            '#' => '师生管理',
-            static::getResource()::getUrl('index') => '教师管理',
-            static::getResource()::getUrl('edit', ['record' => $this->record]) => '编辑教师',
+            $this->getSaveFormAction()
+                ->label('保存'),
+            $this->getCancelFormAction()
+                ->label('取消'),
         ];
     }
-}
+}

+ 22 - 2
app/Filament/Resources/TeacherResource/Pages/ListTeachers.php

@@ -3,6 +3,7 @@
 namespace App\Filament\Resources\TeacherResource\Pages;
 
 use App\Filament\Resources\TeacherResource;
+use Filament\Actions;
 use Filament\Resources\Pages\ListRecords;
 
 class ListTeachers extends ListRecords
@@ -12,7 +13,11 @@ class ListTeachers extends ListRecords
     protected function getHeaderActions(): array
     {
         return [
-            \Filament\Actions\CreateAction::make(),
+            Actions\CreateAction::make()
+                ->label('创建老师')
+                ->icon('heroicon-o-plus')
+                ->size('lg')
+                ->color('primary'),
         ];
     }
 
@@ -24,8 +29,23 @@ class ListTeachers extends ListRecords
     public function getBreadcrumbs(): array
     {
         return [
-            '#' => '师生管理',
             static::getResource()::getUrl('index') => '教师管理',
         ];
     }
+
+    protected function getHeaderWidgets(): array
+    {
+        return [];
+    }
+
+    public function getHeading(): string
+    {
+        return '教师管理';
+    }
+
+    public function getSubheading(): string
+    {
+        $count = \App\Models\Teacher::count();
+        return "共 {$count} 位教师";
+    }
 }

+ 41 - 3
app/Filament/Resources/TeacherResource/Pages/ViewTeacher.php

@@ -3,23 +3,61 @@
 namespace App\Filament\Resources\TeacherResource\Pages;
 
 use App\Filament\Resources\TeacherResource;
+use App\Models\Teacher;
+use Filament\Actions;
 use Filament\Resources\Pages\ViewRecord;
 
 class ViewTeacher extends ViewRecord
 {
     protected static string $resource = TeacherResource::class;
 
+    protected string $view = 'filament.resources.teacher.pages.view-teacher';
+
     public function getTitle(): string
     {
-        return '教师详情';
+        return '教师详情 - ' . $this->record->name;
     }
 
     public function getBreadcrumbs(): array
     {
         return [
-            '#' => '师生管理',
             static::getResource()::getUrl('index') => '教师管理',
-            static::getResource()::getUrl('view', ['record' => $this->record]) => '教师详情',
+            static::getResource()::getUrl('view', ['record' => $this->record]) => $this->record->name,
+        ];
+    }
+
+    protected function getHeaderActions(): array
+    {
+        return [
+            Actions\EditAction::make()
+                ->label('编辑信息')
+                ->icon('heroicon-o-pencil-square'),
+            Actions\Action::make('viewStudents')
+                ->label('查看学生')
+                ->icon('heroicon-o-users')
+                ->color('primary')
+                ->url(fn (Teacher $record) => '/admin/students?tableFilters[teacher_id][value]=' . $record->teacher_id),
+        ];
+    }
+
+    public function getViewData(): array
+    {
+        $teacher = $this->record;
+
+        // 加载用户信息,使用正确的关联关系
+        $user = \App\Models\User::where('id', $teacher->user_id)->first();
+        $teacher->user = $user;
+
+        // 加载学生数量
+        $studentsCount = $teacher->students()->count();
+
+        // 加载考试数量
+        $examCount = \App\Models\Paper::where('teacher_id', $teacher->teacher_id)->count();
+
+        return [
+            'teacher' => $teacher,
+            'studentsCount' => $studentsCount,
+            'examCount' => $examCount,
         ];
     }
 }

+ 142 - 0
app/Http/Controllers/ExamPdfController.php

@@ -437,6 +437,7 @@ class ExamPdfController extends Controller
                     'question_type' => $pq->question_type ?? 'answer', // 包含题目类型
                     'stem' => $pq->question_text ?? '题目内容缺失', // 如果有存储题目文本
                     'difficulty' => $pq->difficulty ?? 0.5,
+                    'score' => $pq->score ?? 5, // 包含已计算的分值
                     'tags' => '',
                     'content' => $pq->question_text ?? '',
                 ];
@@ -567,4 +568,145 @@ class ExamPdfController extends Controller
             'includeAnswer' => $includeAnswer
         ]);
     }
+
+    /**
+     * 判卷视图:题目前带方框,题后附“正确答案+解题思路”
+     */
+    public function showGrading(Request $request, $paper_id)
+    {
+        // 复用现有逻辑获取题目分类
+        $includeAnswer = true;
+        // 直接调用 show 的前置逻辑(简化复用)
+        $request->merge(['answer' => 'true']);
+        // 复用 show() 内逻辑获取 questions/paper
+        // 为避免重复代码,简单调用 showData 方法(拆分为私有方法?暂直接重用现有方法流程)
+        // 这里直接复制 show 的主体以保持兼容
+
+        // 使用 Eloquent 模型获取试卷数据
+        $paper = \App\Models\Paper::where('paper_id', $paper_id)->first();
+        if (!$paper) {
+            $cached = Cache::get('generated_exam_' . $paper_id);
+            if (!$cached) {
+                abort(404, '试卷未找到');
+            }
+            $paper = (object)[
+                'paper_id' => $paper_id,
+                'paper_name' => $cached['paper_name'] ?? 'Demo Paper',
+                'student_id' => $cached['student_id'] ?? null,
+                'teacher_id' => $cached['teacher_id'] ?? null,
+            ];
+            $questionsData = $cached['questions'] ?? [];
+            $totalQuestions = $cached['total_questions'] ?? count($questionsData);
+            $difficultyCategory = $cached['difficulty_category'] ?? '中等';
+            if (!empty($questionsData)) {
+                $questionBankService = app(QuestionBankService::class);
+                $questionIds = array_column($questionsData, 'id');
+                $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
+                $responseData = $questionsResponse['data'] ?? [];
+                if (!empty($responseData)) {
+                    $responseDataMap = [];
+                    foreach ($responseData as $respQ) {
+                        $responseDataMap[$respQ['id']] = $respQ;
+                    }
+                    $questionsData = array_map(function($q) use ($responseDataMap) {
+                        if (isset($responseDataMap[$q['id']])) {
+                            $apiData = $responseDataMap[$q['id']];
+                            $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? '';
+                            list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent);
+                            $q['stem'] = $stem;
+                            $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
+                            $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
+                            $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
+                            $q['options'] = $apiData['options'] ?? $extractedOptions;
+                        }
+                        return $q;
+                    }, $questionsData);
+                }
+            }
+            if (count($questionsData) > $totalQuestions) {
+                $questionsData = $this->selectBestQuestionsForPdf($questionsData, $totalQuestions, $difficultyCategory);
+            }
+        } else {
+            $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id)
+                ->orderBy('question_number')
+                ->get();
+            $questionsData = [];
+            foreach ($paperQuestions as $pq) {
+                $questionsData[] = [
+                    'id' => $pq->question_bank_id,
+                    'kp_code' => $pq->knowledge_point,
+                    'question_type' => $pq->question_type ?? 'answer',
+                    'stem' => $pq->question_text ?? '题目内容缺失',
+                    'difficulty' => $pq->difficulty ?? 0.5,
+                    'score' => $pq->score ?? 5,
+                    'tags' => '',
+                    'content' => $pq->question_text ?? '',
+                ];
+            }
+            if (!empty($questionsData)) {
+                $questionBankService = app(QuestionBankService::class);
+                $questionIds = array_column($questionsData, 'id');
+                $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
+                $responseData = $questionsResponse['data'] ?? [];
+                if (!empty($responseData)) {
+                    $responseDataMap = [];
+                    foreach ($responseData as $respQ) {
+                        $responseDataMap[$respQ['id']] = $respQ;
+                    }
+                    $questionsData = array_map(function($q) use ($responseDataMap, $paperQuestions) {
+                        if (isset($responseDataMap[$q['id']])) {
+                            $apiData = $responseDataMap[$q['id']];
+                            $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失';
+                            list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent);
+                            $q['stem'] = $stem;
+                            $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
+                            $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
+                            $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
+                            $q['options'] = $apiData['options'] ?? $extractedOptions;
+                        }
+                        if (!isset($q['question_type']) || empty($q['question_type'])) {
+                            $dbQuestion = $paperQuestions->firstWhere('question_bank_id', $q['id']);
+                            if ($dbQuestion && $dbQuestion->question_type) {
+                                $q['question_type'] = $dbQuestion->question_type;
+                            }
+                        }
+                        return $q;
+                    }, $questionsData);
+                }
+            }
+        }
+
+        $questions = ['choice' => [], 'fill' => [], 'answer' => []];
+        foreach ($questionsData as $q) {
+            $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失';
+            list($content, $extractedOptions) = $this->separateStemAndOptions($rawContent);
+            $options = $q['options'] ?? $extractedOptions;
+            $answer = $q['answer'] ?? '';
+            $solution = $q['solution'] ?? '';
+            $type = $q['question_type'] ?? $this->determineQuestionType($q);
+            if (!isset($questions[$type])) {
+                $type = 'answer';
+            }
+            $qData = (object)[
+                'id' => $q['id'] ?? $q['question_bank_id'] ?? null,
+                'content' => $content,
+                'answer' => $answer,
+                'solution' => $solution,
+                'difficulty' => $q['difficulty'] ?? 0.5,
+                'kp_code' => $q['kp_code'] ?? '',
+                'tags' => $q['tags'] ?? '',
+                'options' => $options,
+                'score' => $q['score'] ?? $this->getQuestionScore($type),
+            ];
+            $questions[$type][] = $qData;
+        }
+
+        return view('pdf.exam-grading', [
+            'paper' => $paper,
+            'questions' => $questions,
+            'student' => $this->getStudentInfo($paper->student_id),
+            'teacher' => $this->getTeacherInfo($paper->teacher_id),
+            'includeAnswer' => true,
+        ]);
+    }
 }

+ 54 - 0
app/Http/Controllers/MenuVisibilityController.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\MenuPermission;
+use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use Filament\Facades\Filament;
+
+class MenuVisibilityController extends Controller
+{
+    public function toggle(Request $request): JsonResponse
+    {
+        // 验证请求
+        $validated = $request->validate([
+            'menu_key' => 'required|string',
+            'user_id' => 'required',
+            'is_visible' => 'required|boolean',
+        ]);
+
+        // 获取当前用户(使用Laravel的auth辅助函数)
+        $user = auth()->user();
+
+        if (!$user) {
+            return response()->json(['error' => '未登录'], 401);
+        }
+
+        // 检查是否是管理员
+        $isAdmin = $user->role === 'admin' ||
+                   $user->username === '17689974321';
+
+        if (!$isAdmin) {
+            return response()->json(['error' => '权限不足'], 403);
+        }
+
+        try {
+            // 更新菜单可见性
+            MenuPermission::setMenuVisibility(
+                $validated['user_id'],
+                $validated['menu_key'],
+                $validated['is_visible']
+            );
+
+            return response()->json([
+                'success' => true,
+                'message' => '菜单已' . ($validated['is_visible'] ? '显示' : '隐藏')
+            ]);
+        } catch (\Exception $e) {
+            return response()->json([
+                'error' => '操作失败: ' . $e->getMessage()
+            ], 500);
+        }
+    }
+}

+ 90 - 37
app/Livewire/UploadExam/UploadForm.php

@@ -7,6 +7,7 @@ use Livewire\WithFileUploads;
 use Livewire\Attributes\On;
 use Filament\Notifications\Notification;
 use App\Jobs\ProcessOCRRecord;
+use App\Services\ChatGPTAnalysisService;
 use Illuminate\Support\Facades\Storage;
 
 class UploadForm extends Component
@@ -16,14 +17,16 @@ class UploadForm extends Component
     public ?string $teacherId = null;
     public ?string $studentId = null;
     public ?string $selectedPaperId = null;
+    public ?string $mode = 'ocr'; // 'ocr' 或 'chatgpt'
     public $uploadedImages = [];
     public bool $isUploading = false;
 
-    public function mount($teacherId = null, $studentId = null, $selectedPaperId = null)
+    public function mount($teacherId = null, $studentId = null, $selectedPaperId = null, $mode = 'ocr')
     {
         $this->teacherId = $teacherId;
         $this->studentId = $studentId;
         $this->selectedPaperId = $selectedPaperId;
+        $this->mode = $mode;
     }
 
     public function handleSubmit()
@@ -51,48 +54,21 @@ class UploadForm extends Component
                 ];
             }
 
-            // 获取试卷名称
-            $paperTitle = '待OCR识别';
-            if ($this->selectedPaperId) {
-                $paper = \App\Models\Paper::where('paper_id', $this->selectedPaperId)->first();
-                if ($paper) {
-                    $paperTitle = $paper->paper_name;
-                }
-            }
-
-            // 创建 OCR 记录
-            $ocrRecord = \App\Models\OCRRecord::create([
-                'user_id' => $this->studentId,
-                'student_id' => $this->studentId,  // 同时设置 student_id
-                'paper_title' => $paperTitle,
-                'paper_type' => null,
-                'file_path' => $savedImages[0]['path'],
-                'image_count' => count($savedImages),
-                'status' => 'processing',
-                'analysis_id' => $this->selectedPaperId, // 存储关联的试卷ID
-            ]);
-
-            // 派发 OCR 处理任务
-            ProcessOCRRecord::dispatch($ocrRecord->id);
-
-            Notification::make()
-                ->title('上传成功')
-                ->body('图片已上传,正在进行 OCR 识别...')
-                ->success()
-                ->send();
+            // 获取图片URL
+            $imageUrl = asset('storage/' . $savedImages[0]['path']);
 
-            // 判断是否为系统生成的试卷
-            if ($this->selectedPaperId && str_starts_with($this->selectedPaperId, 'paper_')) {
-                // 系统生成的试卷,跳转到专门的分析页面
-                $this->redirect('/admin/ocr-paper-analysis/' . $ocrRecord->id);
+            // 根据模式处理
+            if ($this->mode === 'chatgpt') {
+                // ChatGPT模式:直接调用ChatGPT分析
+                $this->handleChatGPTAnalysis($imageUrl);
             } else {
-                // 上传的试卷,跳转到原来的 OCR 详情页
-                $this->redirect('/admin/ocr-record-view/' . $ocrRecord->id);
+                // OCR模式:保持原有逻辑
+                $this->handleOCRProcessing($savedImages);
             }
 
         } catch (\Exception $e) {
             Notification::make()
-                ->title('上传失败')
+                ->title('处理失败')
                 ->body($e->getMessage())
                 ->danger()
                 ->send();
@@ -101,6 +77,83 @@ class UploadForm extends Component
         }
     }
 
+    /**
+     * 处理OCR识别
+     */
+    private function handleOCRProcessing(array $savedImages)
+    {
+        // 获取试卷名称
+        $paperTitle = '待OCR识别';
+        if ($this->selectedPaperId) {
+            $paper = \App\Models\Paper::where('paper_id', $this->selectedPaperId)->first();
+            if ($paper) {
+                $paperTitle = $paper->paper_name;
+            }
+        }
+
+        // 创建 OCR 记录
+        $ocrRecord = \App\Models\OCRRecord::create([
+            'user_id' => $this->studentId,
+            'student_id' => $this->studentId,
+            'paper_title' => $paperTitle,
+            'paper_type' => null,
+            'file_path' => $savedImages[0]['path'],
+            'image_count' => count($savedImages),
+            'status' => 'processing',
+            'analysis_id' => $this->selectedPaperId,
+        ]);
+
+        // 派发 OCR 处理任务
+        ProcessOCRRecord::dispatch($ocrRecord->id);
+
+        Notification::make()
+            ->title('上传成功')
+            ->body('图片已上传,正在进行 OCR 识别...')
+            ->success()
+            ->send();
+
+        // 跳转
+        if ($this->selectedPaperId && str_starts_with($this->selectedPaperId, 'paper_')) {
+            $this->redirect('/admin/ocr-paper-analysis/' . $ocrRecord->id);
+        } else {
+            $this->redirect('/admin/ocr-record-view/' . $ocrRecord->id);
+        }
+    }
+
+    /**
+     * 处理ChatGPT分析
+     */
+    private function handleChatGPTAnalysis(string $imageUrl)
+    {
+        if (!$this->selectedPaperId) {
+            throw new \Exception('请先选择试卷');
+        }
+
+        // 调用ChatGPT分析
+        $chatGPTService = app(ChatGPTAnalysisService::class);
+        $result = $chatGPTService->analyzeExamPaper($this->selectedPaperId, $imageUrl);
+
+        if ($result['success']) {
+            // 保存分析结果
+            $saved = $chatGPTService->saveAnalysisResult($this->selectedPaperId, $result['data']);
+
+            if ($saved) {
+                Notification::make()
+                    ->title('ChatGPT分析完成')
+                    ->body('已成功分析 ' . count($result['data']['questions'] ?? []) . ' 道题目')
+                    ->success()
+                    ->send();
+
+                // 跳转到分析页面
+                $this->redirect('/admin/exam-analysis?paperId=' . $this->selectedPaperId);
+            } else {
+                throw new \Exception('分析结果保存失败');
+            }
+        } else {
+            throw new \Exception($result['error'] ?? 'ChatGPT分析失败');
+        }
+    }
+
     public function resetForm()
     {
         $this->uploadedImages = [];

+ 144 - 0
app/Models/MenuConfig.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class MenuConfig extends Model
+{
+    use HasFactory;
+
+    protected $table = 'menu_configs';
+
+    protected $fillable = [
+        'menu_key',
+        'menu_label',
+        'menu_group',
+        'menu_url',
+        'sort_order',
+        'is_managed',
+        'is_active',
+        'description',
+    ];
+
+    protected $casts = [
+        'is_managed' => 'boolean',
+        'is_active' => 'boolean',
+        'sort_order' => 'integer',
+    ];
+
+    /**
+     * 获取所有启用的菜单配置
+     */
+    public static function getActiveConfigs()
+    {
+        return self::where('is_active', true)
+            ->orderBy('sort_order')
+            ->get();
+    }
+
+    /**
+     * 获取所有启用的受管理菜单配置
+     */
+    public static function getManagedConfigs()
+    {
+        return self::where('is_active', true)
+            ->where('is_managed', true)
+            ->orderBy('sort_order')
+            ->get();
+    }
+
+    /**
+     * 检查菜单是否受管理
+     */
+    public static function isManaged(string $menuKey): bool
+    {
+        $config = self::where('menu_key', $menuKey)->first();
+        return $config && $config->is_managed && $config->is_active;
+    }
+
+    /**
+     * 切换菜单管理状态
+     */
+    public static function toggleManaged(string $menuKey): bool
+    {
+        $config = self::where('menu_key', $menuKey)->first();
+        if ($config) {
+            $config->is_managed = !$config->is_managed;
+            return $config->save();
+        }
+        return false;
+    }
+
+    /**
+     * 初始化默认菜单配置
+     */
+    public static function initializeDefaultConfigs(): void
+    {
+        $defaultMenus = [
+            'dashboard' => [
+                'label' => '仪表盘',
+                'group' => '管理',
+                'url' => '/admin',
+                'sort' => 0,
+            ],
+            'exam-history' => [
+                'label' => '考试历史',
+                'group' => '管理',
+                'url' => '/admin/exam-history',
+                'sort' => 1,
+            ],
+            'exam-analysis' => [
+                'label' => '考试分析',
+                'group' => '分析',
+                'url' => '/admin/exam-analysis',
+                'sort' => 2,
+            ],
+            'ocr-paper-grading' => [
+                'label' => 'OCR试卷批改',
+                'group' => '工具',
+                'url' => '/admin/ocr-paper-grading',
+                'sort' => 3,
+            ],
+            'intelligent-exam-generation' => [
+                'label' => '智能出卷',
+                'group' => '工具',
+                'url' => '/admin/intelligent-exam-generation',
+                'sort' => 4,
+            ],
+            'knowledge-graph' => [
+                'label' => '知识图谱',
+                'group' => '分析',
+                'url' => '/admin/knowledge-graph',
+                'sort' => 5,
+            ],
+            'student-management' => [
+                'label' => '学生管理',
+                'group' => '管理',
+                'url' => '/admin/student-management',
+                'sort' => 6,
+            ],
+            'teacher-management' => [
+                'label' => '老师管理',
+                'group' => '管理',
+                'url' => '/admin/teacher-management',
+                'sort' => 7,
+            ],
+        ];
+
+        foreach ($defaultMenus as $key => $data) {
+            self::updateOrCreate(
+                ['menu_key' => $key],
+                [
+                    'menu_label' => $data['label'],
+                    'menu_group' => $data['group'],
+                    'menu_url' => $data['url'],
+                    'sort_order' => $data['sort'],
+                    'is_managed' => true,
+                    'is_active' => true,
+                ]
+            );
+        }
+    }
+}

+ 90 - 0
app/Models/MenuPermission.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+
+class MenuPermission extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'user_id',
+        'menu_key',
+        'menu_label',
+        'menu_group',
+        'menu_url',
+        'is_visible',
+        'sort_order',
+        'additional_data',
+    ];
+
+    protected $casts = [
+        'is_visible' => 'boolean',
+        'sort_order' => 'integer',
+        'additional_data' => 'array',
+    ];
+
+    /**
+     * 获取用户的菜单权限
+     */
+    public static function getUserMenus(string $userId, bool $onlyVisible = true)
+    {
+        $query = self::where('user_id', $userId);
+
+        if ($onlyVisible) {
+            $query->where('is_visible', true);
+        }
+
+        return $query->orderBy('sort_order')->get();
+    }
+
+    /**
+     * 检查菜单是否可见
+     */
+    public static function isMenuVisible(string $userId, string $menuKey): bool
+    {
+        $permission = self::where('user_id', $userId)
+            ->where('menu_key', $menuKey)
+            ->first();
+
+        return $permission ? $permission->is_visible : true; // 默认为可见
+    }
+
+    /**
+     * 设置菜单可见性
+     */
+    public static function setMenuVisibility(string $userId, string $menuKey, bool $visible): void
+    {
+        self::updateOrCreate(
+            [
+                'user_id' => $userId,
+                'menu_key' => $menuKey,
+            ],
+            [
+                'is_visible' => $visible,
+                'menu_label' => self::getDefaultMenuLabel($menuKey),
+            ]
+        );
+    }
+
+    /**
+     * 获取默认菜单标签
+     */
+    private static function getDefaultMenuLabel(string $menuKey): string
+    {
+        $labels = [
+            'dashboard' => '仪表盘',
+            'exam-history' => '考试历史',
+            'exam-analysis' => '考试分析',
+            'ocr-paper-grading' => 'OCR试卷批改',
+            'intelligent-exam-generation' => '智能出卷',
+            'knowledge-graph' => '知识图谱',
+            'student-management' => '学生管理',
+            'teacher-management' => '老师管理',
+        ];
+
+        return $labels[$menuKey] ?? ucfirst($menuKey);
+    }
+}

+ 5 - 0
app/Providers/Filament/AdminPanelProvider.php

@@ -30,6 +30,7 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
 use Illuminate\Routing\Middleware\SubstituteBindings;
 use Illuminate\Session\Middleware\StartSession;
 use Illuminate\View\Middleware\ShareErrorsFromSession;
+use Filament\Support\Facades\FilamentView;
 
 class AdminPanelProvider extends PanelProvider
 {
@@ -70,6 +71,10 @@ class AdminPanelProvider extends PanelProvider
             )
             ->renderHook('global::head.start', fn (): string => view('filament.layout.vite-styles')->render())
             ->renderHook('panels::body.end', fn (): string => view('filament.layout.math-renderer')->render())
+            ->renderHook(
+                'panels::page.header.actions.after',
+                fn () => view('filament.components.menu-visibility-toggle')
+            )
             ->middleware([
                 EncryptCookies::class,
                 AddQueuedCookiesToResponse::class,

+ 249 - 0
app/Services/ChatGPTAnalysisService.php

@@ -0,0 +1,249 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\DB;
+
+class ChatGPTAnalysisService
+{
+    protected string $learningAnalyticsUrl;
+    protected int $timeout;
+
+    public function __construct()
+    {
+        $this->learningAnalyticsUrl = config('services.learning_analytics.base_url', env('LEARNING_ANALYTICS_URL', 'http://localhost:5016'));
+        $this->timeout = 180; // ChatGPT分析超时
+    }
+
+    /**
+     * 使用ChatGPT分析试卷图片
+     *
+     * @param string $paperId 试卷ID
+     * @param string $imageUrl 试卷图片URL
+     * @return array 分析结果
+     */
+    public function analyzeExamPaper(string $paperId, string $imageUrl): array
+    {
+        try {
+            Log::info('开始ChatGPT分析', [
+                'paper_id' => $paperId,
+                'image_url' => $imageUrl
+            ]);
+
+            // 获取题目数据
+            $questionsData = $this->getPaperQuestions($paperId);
+            if (empty($questionsData)) {
+                throw new \Exception('未找到试卷题目');
+            }
+
+            // 调用LearningAnalytics的ChatGPT分析API
+            $requestData = [
+                'paper_id' => $paperId,
+                'questions' => $questionsData,
+                'image_url' => $imageUrl,
+                'student_id' => $this->getStudentId($paperId),
+            ];
+
+            Log::info('调用LearningAnalytics ChatGPT分析API', [
+                'url' => $this->learningAnalyticsUrl . '/api/v1/chatgpt/analyze',
+                'question_count' => count($questionsData)
+            ]);
+
+            $response = Http::timeout($this->timeout)
+                ->post($this->learningAnalyticsUrl . '/api/v1/chatgpt/analyze', $requestData);
+
+            if (!$response->successful()) {
+                throw new \Exception('LearningAnalytics API调用失败: ' . $response->body());
+            }
+
+            $responseData = $response->json();
+
+            Log::info('ChatGPT分析完成', [
+                'paper_id' => $paperId,
+                'questions_count' => count($responseData['questions'] ?? []),
+                'success' => true
+            ]);
+
+            return [
+                'success' => true,
+                'data' => $responseData,
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('ChatGPT分析失败', [
+                'paper_id' => $paperId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return [
+                'success' => false,
+                'error' => $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 获取试卷题目
+     */
+    private function getPaperQuestions(string $paperId): array
+    {
+        try {
+            // 处理OCR记录
+            if (str_starts_with($paperId, 'ocr_')) {
+                $recordId = substr($paperId, 4);
+                $ocrQuestions = DB::table('ocr_question_results')
+                    ->where('ocr_record_id', $recordId)
+                    ->orderBy('question_number')
+                    ->get()
+                    ->map(function ($q) {
+                        // 优先使用人工校准的答案
+                        $studentAnswer = !empty(trim($q->manual_answer ?? ''))
+                            ? trim($q->manual_answer)
+                            : trim($q->student_answer ?? '');
+
+                        return [
+                            'question_number' => $q->question_number,
+                            'question_id' => $q->question_number,
+                            'content' => $q->question_text,
+                            'correct_answer' => '', // ChatGPT会从图片识别
+                            'knowledge_point' => $q->kp_code,
+                            'question_type' => 'unknown',
+                            'score' => $q->score_total ?? 5
+                        ];
+                    })
+                    ->toArray();
+
+                return $ocrQuestions;
+            }
+
+            // 处理系统生成卷子
+            $questions = DB::table('paper_questions')
+                ->where('paper_id', $paperId)
+                ->orderBy('question_number')
+                ->get()
+                ->map(function ($q) {
+                    return [
+                        'question_number' => $q->question_number,
+                        'question_id' => $q->question_bank_id,
+                        'content' => $q->question_text,
+                        'correct_answer' => '', // ChatGPT会从图片识别
+                        'knowledge_point' => $q->knowledge_point,
+                        'question_type' => $q->question_type,
+                        'score' => $q->score
+                    ];
+                })
+                ->toArray();
+
+            return $questions;
+        } catch (\Exception $e) {
+            Log::error('获取试卷题目失败', [
+                'paper_id' => $paperId,
+                'error' => $e->getMessage()
+            ]);
+            return [];
+        }
+    }
+
+    /**
+     * 获取学生ID
+     */
+    private function getStudentId(string $paperId): ?string
+    {
+        try {
+            // 处理OCR记录
+            if (str_starts_with($paperId, 'ocr_')) {
+                $recordId = substr($paperId, 4);
+                $record = DB::table('ocr_records')->where('id', $recordId)->first();
+                return $record->student_id ?? null;
+            }
+
+            // 处理系统生成卷子
+            $paper = DB::table('papers')->where('paper_id', $paperId)->first();
+            return $paper->student_id ?? null;
+        } catch (\Exception $e) {
+            Log::error('获取学生ID失败', [
+                'paper_id' => $paperId,
+                'error' => $e->getMessage()
+            ]);
+            return null;
+        }
+    }
+
+    /**
+     * 保存ChatGPT分析结果到数据库
+     */
+    public function saveAnalysisResult(string $paperId, array $analysisData): bool
+    {
+        try {
+            return DB::transaction(function () use ($paperId, $analysisData) {
+                // 更新papers表的analysis_id
+                DB::table('papers')
+                    ->where('paper_id', $paperId)
+                    ->update([
+                        'analysis_id' => $analysisData['analysis_id'] ?? ('chatgpt_' . uniqid()),
+                        'updated_at' => now()
+                    ]);
+
+                // 保存详细分析结果到answer_analysis表
+                if (isset($analysisData['questions'])) {
+                    foreach ($analysisData['questions'] as $questionData) {
+                        $questionNumber = $questionData['q'] ?? null;
+                        if (!$questionNumber) continue;
+
+                        // 查找对应的paper_question记录
+                        $paperQuestion = null;
+                        if (str_starts_with($paperId, 'ocr_')) {
+                            // OCR记录使用question_number作为ID
+                            $paperQuestion = (object)[
+                                'question_bank_id' => 'ocr_q' . $questionNumber,
+                                'score' => 5
+                            ];
+                        } else {
+                            $paperQuestion = DB::table('paper_questions')
+                                ->where('paper_id', $paperId)
+                                ->where('question_number', $questionNumber)
+                                ->first();
+                        }
+
+                        if ($paperQuestion) {
+                            DB::table('answer_analysis')->updateOrInsert(
+                                [
+                                    'paper_id' => $paperId,
+                                    'question_id' => $paperQuestion->question_bank_id,
+                                    'question_number' => $questionNumber,
+                                ],
+                                [
+                                    'student_answer' => $questionData['student_answer'] ?? null,
+                                    'correct_answer' => $questionData['correct_answer'] ?? null,
+                                    'is_correct' => $questionData['is_correct'] ?? false,
+                                    'score_obtained' => $questionData['is_correct'] ? ($paperQuestion->score ?? 0) : 0,
+                                    'max_score' => $paperQuestion->score ?? 0,
+                                    'analysis_result' => json_encode($questionData, JSON_UNESCAPED_UNICODE),
+                                    'updated_at' => now()
+                                ]
+                            );
+                        }
+                    }
+                }
+
+                Log::info('ChatGPT分析结果保存成功', [
+                    'paper_id' => $paperId,
+                    'questions_count' => count($analysisData['questions'] ?? [])
+                ]);
+
+                return true;
+            });
+
+        } catch (\Exception $e) {
+            Log::error('保存ChatGPT分析结果失败', [
+                'paper_id' => $paperId,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+}

+ 32 - 26
app/Services/LearningAnalyticsService.php

@@ -1215,6 +1215,7 @@ class LearningAnalyticsService
     {
         try {
             $studentId = $params['student_id'] ?? null;
+            $grade = $params['grade'] ?? null; // 用户选择的年级
             $totalQuestions = $params['total_questions'] ?? 20;
             $kpCodes = $params['kp_codes'] ?? [];
             $skills = $params['skills'] ?? [];
@@ -1244,35 +1245,40 @@ class LearningAnalyticsService
             // 如果仍然没有知识点(例如新学生无薄弱点),根据年级从知识图谱获取知识点
             if (empty($kpCodes)) {
                 $filters = [];
-                if ($studentId) {
+
+                // 优先使用用户选择的年级,其次使用学生的年级
+                $effectiveGrade = $grade;
+                if (!$effectiveGrade && $studentId) {
                     $student = \App\Models\Student::find($studentId);
                     if ($student && $student->grade) {
-                        $grade = $student->grade;
-                        $standardizedGrade = $grade;
-
-                        // 标准化年级名称并更新数据库
-                        if ($grade === '初一') {
-                            $standardizedGrade = '七年级';
-                        } elseif ($grade === '初二') {
-                            $standardizedGrade = '八年级';
-                        } elseif ($grade === '初三') {
-                            $standardizedGrade = '九年级';
-                        }
-
-                        if ($standardizedGrade !== $grade) {
-                            $student->grade = $standardizedGrade;
-                            $student->save();
-                            Log::info('Standardized student grade', ['student_id' => $studentId, 'old' => $grade, 'new' => $standardizedGrade]);
-                            $grade = $standardizedGrade;
-                        }
-
-                        // 映射年级到学段 (phase)
-                        if (str_contains($grade, '初') || str_contains($grade, '七年级') || str_contains($grade, '八年级') || str_contains($grade, '九年级')) {
-                            $filters['phase'] = '初中';
-                        } elseif (str_contains($grade, '高')) {
-                            $filters['phase'] = '高中';
-                        }
+                        $effectiveGrade = $student->grade;
+                    }
+                }
+
+                if ($effectiveGrade) {
+                    $standardizedGrade = $effectiveGrade;
+
+                    // 标准化年级名称(不更新数据库)
+                    if ($standardizedGrade === '初一') {
+                        $standardizedGrade = '七年级';
+                    } elseif ($standardizedGrade === '初二') {
+                        $standardizedGrade = '八年级';
+                    } elseif ($standardizedGrade === '初三') {
+                        $standardizedGrade = '九年级';
+                    }
+
+                    // 映射年级到学段 (phase)
+                    if ($standardizedGrade === '初中' || str_contains($standardizedGrade, '七年级') || str_contains($standardizedGrade, '八年级') || str_contains($standardizedGrade, '九年级')) {
+                        $filters['phase'] = '初中';
+                    } elseif (str_contains($standardizedGrade, '高')) {
+                        $filters['phase'] = '高中';
                     }
+
+                    Log::info('Using grade for knowledge points filter', [
+                        'original_grade' => $effectiveGrade,
+                        'standardized_grade' => $standardizedGrade,
+                        'filters' => $filters
+                    ]);
                 }
 
                 // 调用API获取过滤后的知识点

+ 119 - 0
app/Services/MenuPermissionService.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\MenuPermission;
+use Filament\Facades\Filament;
+use Filament\Navigation\NavigationItem;
+
+class MenuPermissionService
+{
+    /**
+     * 过滤当前用户的菜单项
+     */
+    public static function filterUserNavigationItems(array $items): array
+    {
+        $user = Filament::getCurrentPanel()->auth()->user();
+        if (!$user) {
+            return $items;
+        }
+
+        $userId = $user->teacher_id ?? $user->id;
+
+        return array_filter($items, function ($item) use ($userId) {
+            $url = $item->getUrl();
+            $menuKey = self::extractMenuKeyFromUrl($url);
+
+            if (!$menuKey) {
+                return true; // 如果无法识别菜单键,默认显示
+            }
+
+            return MenuPermission::isMenuVisible($userId, $menuKey);
+        });
+    }
+
+    /**
+     * 从URL中提取菜单键
+     */
+    private static function extractMenuKeyFromUrl(string $url): ?string
+    {
+        // 提取URL中的路径作为菜单键
+        $path = parse_url($url, PHP_URL_PATH);
+
+        if (!$path) {
+            return null;
+        }
+
+        // 移除开头的斜杠并获取第一段路径
+        $path = ltrim($path, '/');
+        $segments = explode('/', $path);
+
+        // 获取第一个路径段作为菜单键
+        $menuKey = $segments[0] ?? null;
+
+        // 特殊处理一些路径
+        $menuMappings = [
+            'admin' => null, // 跳过admin前缀
+            'dashboard' => 'dashboard',
+            'exam-history' => 'exam-history',
+            'exam-analysis' => 'exam-analysis',
+            'ocr-paper-grading' => 'ocr-paper-grading',
+            'intelligent-exam-generation' => 'intelligent-exam-generation',
+            'knowledge-graph' => 'knowledge-graph',
+            'student-management' => 'student-management',
+            'teacher-management' => 'teacher-management',
+        ];
+
+        return $menuMappings[$menuKey] ?? $menuKey;
+    }
+
+    /**
+     * 为新用户初始化默认菜单权限
+     */
+    public static function initializeDefaultMenus(string $userId): void
+    {
+        $defaultMenus = [
+            'dashboard' => ['label' => '仪表盘', 'group' => '管理', 'sort' => 0],
+            'exam-history' => ['label' => '考试历史', 'group' => '管理', 'sort' => 1],
+            'exam-analysis' => ['label' => '考试分析', 'group' => '分析', 'sort' => 2],
+            'ocr-paper-grading' => ['label' => 'OCR试卷批改', 'group' => '工具', 'sort' => 3],
+            'intelligent-exam-generation' => ['label' => '智能出卷', 'group' => '工具', 'sort' => 4],
+            'knowledge-graph' => ['label' => '知识图谱', 'group' => '分析', 'sort' => 5],
+            'student-management' => ['label' => '学生管理', 'group' => '管理', 'sort' => 6],
+            'teacher-management' => ['label' => '老师管理', 'group' => '管理', 'sort' => 7],
+        ];
+
+        foreach ($defaultMenus as $key => $data) {
+            MenuPermission::updateOrCreate(
+                [
+                    'user_id' => $userId,
+                    'menu_key' => $key,
+                ],
+                [
+                    'menu_label' => $data['label'],
+                    'menu_group' => $data['group'],
+                    'is_visible' => true,
+                    'sort_order' => $data['sort'],
+                ]
+            );
+        }
+    }
+
+    /**
+     * 获取用户的菜单统计信息
+     */
+    public static function getUserMenuStats(string $userId): array
+    {
+        $stats = MenuPermission::where('user_id', $userId)
+            ->selectRaw('COUNT(*) as total')
+            ->selectRaw('SUM(CASE WHEN is_visible = 1 THEN 1 ELSE 0 END) as visible')
+            ->selectRaw('SUM(CASE WHEN is_visible = 0 THEN 1 ELSE 0 END) as hidden')
+            ->first();
+
+        return [
+            'total' => $stats->total ?? 0,
+            'visible' => $stats->visible ?? 0,
+            'hidden' => $stats->hidden ?? 0,
+        ];
+    }
+}

+ 5 - 0
app/Services/PromptService.php

@@ -93,6 +93,11 @@ class PromptService
 【技能覆盖】
 {skill_coverage}
 
+【图示处理】
+- 如果原题涉及图形/示意图/坐标系/几何草图,必须在题干内内嵌一段完整的 <svg> 标签来还原图形;不要使用外链图片、base64 或占位符。
+- SVG 要包含明确的宽高(建议 260~360 像素),只使用基础图元(line、rect、circle、polygon、path、text),并给出必要的坐标、角点和标注文本。
+- 确保题干文本描述与 SVG 一致,例如“如图所示”后紧跟 SVG,且 SVG 放在题干末尾即可被前端直接渲染。
+
 【质量标准】
 - 准确性:100%正确
 - 多样性:避免重复

+ 18 - 4
app/Services/QuestionBankService.php

@@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Log;
 class QuestionBankService
 {
     protected string $baseUrl;
+    protected int $timeout;
+    protected int $retry;
+    protected int $retryDelay;
 
     public function __construct()
     {
@@ -19,6 +22,17 @@ class QuestionBankService
         if (!str_ends_with($this->baseUrl, '/api')) {
             $this->baseUrl .= '/api';
         }
+
+        // 读取超时与重试配置
+        $this->timeout = (int) config('services.question_bank.timeout', 60);
+        $this->retry = (int) config('services.question_bank.retry', 2);
+        $this->retryDelay = (int) config('services.question_bank.retry_delay', 500);
+    }
+
+    private function http()
+    {
+        return Http::timeout($this->timeout)
+            ->retry($this->retry, $this->retryDelay);
     }
 
     /**
@@ -71,7 +85,7 @@ class QuestionBankService
     public function listQuestions(int $page = 1, int $perPage = 50, array $filters = []): array
     {
         try {
-            $response = Http::timeout(10)
+            $response = $this->http()
                 ->get($this->baseUrl . '/questions', [
                     'page' => $page,
                     'per_page' => $perPage,
@@ -101,7 +115,7 @@ class QuestionBankService
     public function getQuestion(string $questionCode): ?array
     {
         try {
-            $response = Http::timeout(10)
+            $response = $this->http()
                 ->get($this->baseUrl . "/questions/{$questionCode}");
 
             if ($response->successful()) {
@@ -187,7 +201,7 @@ class QuestionBankService
             return ['data' => []];
         }
         try {
-            $response = Http::timeout(15)
+            $response = $this->http()
                 ->get($this->baseUrl . '/questions', [
                     'ids' => implode(',', $ids),
                 ]);
@@ -220,7 +234,7 @@ class QuestionBankService
 
             // 注意:这里的请求实际上是同步的,会等待响应
             // 真正的异步应该使用 Http::async()
-            $response = Http::timeout(10) // 10秒超时应该足够获取响应
+            $response = $this->http()
                 ->post($this->baseUrl . '/ai/generate-intelligent-questions', $params);
 
             if ($response->successful()) {

+ 3 - 0
config/services.php

@@ -42,6 +42,9 @@ return [
     'question_bank' => [
         'base_url' => env('QUESTION_BANK_API_BASE', 'http://localhost:5015/api'),
         'callback_domain' => env('QUESTION_BANK_CALLBACK_DOMAIN', null),
+        'timeout' => env('QUESTION_BANK_TIMEOUT', 60), // AI 生成可能耗时,默认 60s
+        'retry' => env('QUESTION_BANK_RETRY', 2),
+        'retry_delay' => env('QUESTION_BANK_RETRY_DELAY', 500), // 毫秒
     ],
 
     'learning_analytics' => [

+ 39 - 0
debug_improved_matching.php

@@ -0,0 +1,39 @@
+<?php
+
+use App\Services\OCRDataParser;
+use App\Models\PaperQuestion;
+use App\Models\OCRRecord;
+
+require __DIR__ . '/vendor/autoload.php';
+
+$app = require_once __DIR__ . '/bootstrap/app.php';
+$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
+$kernel->bootstrap();
+
+$recordId = 4;
+$ocrRecord = OCRRecord::find($recordId);
+
+$rawOcrData = \Illuminate\Support\Facades\DB::table('ocr_raw_data')
+    ->where('ocr_record_id', $recordId)
+    ->value('raw_response');
+
+if (!$rawOcrData) {
+    echo "No raw data found for record {$recordId}\n";
+    exit;
+}
+
+$rawOcrData = json_decode($rawOcrData, true);
+$paperQuestions = PaperQuestion::where('paper_id', $ocrRecord->analysis_id)
+    ->orderBy('question_number')
+    ->get();
+
+$parser = new OCRDataParser();
+
+echo "Testing matchWithSystemPaper with improved logic...\n";
+$results = $parser->matchWithSystemPaper($rawOcrData, $paperQuestions);
+
+foreach ($results as $qNum => $result) {
+    echo "Question {$qNum}:\n";
+    echo "  Y-Range: {$result['coordinates']['y_min']} - {$result['coordinates']['y_max']}\n";
+    echo "  Answer: " . mb_substr($result['student_answer'], 0, 50) . "...\n";
+}

+ 1203 - 49
public/data/edges.json

@@ -1,110 +1,1264 @@
 [
   {
-    "source": "P01",
-    "target": "P02",
+    "source": "R01",
+    "target": "R02",
     "type": "successor",
-    "comment": "知识点之间存在必要的学习衔接,用于帮助学生更顺畅地理解后续内容。"
+    "comment": "理解整数和自然数后,进一步学习有理数分类。"
   },
   {
-    "source": "P02",
-    "target": "P03",
+    "source": "R02",
+    "target": "R03",
     "type": "successor",
-    "comment": "知识点之间存在必要的学习衔接,用于帮助学生更顺畅地理解后续内容。"
+    "comment": "掌握有理数的种类后才能理解加减法的规则。"
   },
   {
-    "source": "P03",
-    "target": "P04",
+    "source": "R02",
+    "target": "R04",
     "type": "successor",
-    "comment": "知识点之间存在必要的学习衔接,用于帮助学生更顺畅地理解后续内容。"
+    "comment": "乘除法基于有理数分类与符号理解。"
   },
   {
-    "source": "P04",
-    "target": "P05",
+    "source": "R03",
+    "target": "R04",
     "type": "successor",
-    "comment": "知识点之间存在必要的学习衔接,用于帮助学生更顺畅地理解后续内容。"
+    "comment": "加减法是乘除法学习的基础。"
   },
   {
-    "source": "P05",
-    "target": "P06",
+    "source": "R04",
+    "target": "R05",
     "type": "successor",
-    "comment": "因式分解建立在乘法公式的理解基础上,掌握乘法公式有助于学生快速看出可分解结构。"
+    "comment": "幂与指数运算基于乘法定义。"
   },
   {
-    "source": "P06",
-    "target": "F01",
+    "source": "R03",
+    "target": "A01",
+    "type": "prerequisite",
+    "comment": "理解有理数运算后才能学习代数式。"
+  },
+  {
+    "source": "A01",
+    "target": "A02",
+    "type": "successor",
+    "comment": "代数式概念是整式概念的基础。"
+  },
+  {
+    "source": "A02",
+    "target": "A03",
+    "type": "successor",
+    "comment": "整式分类后进入同类项合并学习。"
+  },
+  {
+    "source": "A03",
+    "target": "A04",
+    "type": "successor",
+    "comment": "掌握同类项后学习整式加减(去括号)。"
+  },
+  {
+    "source": "A04",
+    "target": "A05",
+    "type": "successor",
+    "comment": "整式加减是多项式乘法的基础。"
+  },
+  {
+    "source": "A05",
+    "target": "A06",
+    "type": "successor",
+    "comment": "特殊乘法公式在整式乘法基础上发展而来。"
+  },
+  {
+    "source": "A06",
+    "target": "A07",
+    "type": "successor",
+    "comment": "因式分解的公式法依赖乘法公式。"
+  },
+  {
+    "source": "A07",
+    "target": "A08",
     "type": "successor",
-    "comment": "分式化简通常需要先把分子分母因式分解,这是学生顺利完成约分的关键步骤。"
+    "comment": "基础因式分解掌握后才能学习复杂技巧,如分组分解、十字相乘。"
+  },
+  {
+    "source": "A08",
+    "target": "A09",
+    "type": "successor",
+    "comment": "综合因式分解依赖多种因式分解方法的整合。"
+  },
+  {
+    "source": "A07",
+    "target": "F02",
+    "type": "crosslink",
+    "comment": "分式约分必须依赖因式分解。"
+  },
+  {
+    "source": "A08",
+    "target": "F06",
+    "type": "crosslink",
+    "comment": "复杂分式化简需要高级因式分解技巧。"
   },
   {
     "source": "F01",
     "target": "F02",
     "type": "successor",
-    "comment": "完成分式化简后,学生才能进行分式的加减乘除运算,这是题目的常见流程。"
+    "comment": "理解分式基础后进入分式约分。"
   },
   {
     "source": "F02",
-    "target": "E05",
+    "target": "F03",
+    "type": "successor",
+    "comment": "约分完成后才能进行通分。"
+  },
+  {
+    "source": "F03",
+    "target": "F04",
     "type": "successor",
-    "comment": "处理含分式的一元二次方程前,需要先完成分式运算的化简与统一。"
+    "comment": "通分后才能进行分式加减。"
   },
   {
-    "source": "E01",
-    "target": "E02",
+    "source": "F02",
+    "target": "F05",
     "type": "successor",
-    "comment": "理解一元一次方程后,学生才能掌握方程组的求解方法,如代入法或加减法。"
+    "comment": "分式乘除依赖因式约分。"
   },
   {
-    "source": "E02",
-    "target": "E05",
+    "source": "F04",
+    "target": "F06",
     "type": "successor",
-    "comment": "部分二次方程可通过构造方程组模型来理解,因此方程组经验有助于学生理解二次方程。"
+    "comment": "分式运算基础完成后进入综合化简。"
   },
   {
-    "source": "H01",
-    "target": "H02",
+    "source": "R07",
+    "target": "RS01",
+    "type": "prerequisite",
+    "comment": "平方根建立在平方运算基础之上。"
+  },
+  {
+    "source": "RS01",
+    "target": "RS03",
     "type": "successor",
-    "comment": "掌握函数的基本概念后,学生才能理解一次函数的表示与图像特征。"
+    "comment": "平方根性质引出根式基本性质。"
   },
   {
-    "source": "H02",
-    "target": "H03",
+    "source": "RS03",
+    "target": "RS04",
     "type": "successor",
-    "comment": "理解一次函数的变化特征有助于学生更好地理解二次函数的曲线规律。"
+    "comment": "根式的基本性质用于根式化简。"
   },
   {
-    "source": "H03",
-    "target": "H04",
+    "source": "RS04",
+    "target": "RS05",
     "type": "successor",
-    "comment": "理解二次函数图像后,学生才能准确判断最值与开口方向。"
+    "comment": "根式化简后进行混合运算。"
   },
   {
-    "source": "E05",
-    "target": "H04",
+    "source": "A01",
+    "target": "M01A",
+    "type": "crosslink",
+    "comment": "代数式基础用于实际情境的数量关系表达。"
+  },
+  {
+    "source": "M01A",
+    "target": "M01B",
     "type": "successor",
-    "comment": "求二次函数的最值往往依赖方程求顶点或开口方向,因此二次方程能力影响最值判断。"
+    "comment": "从表达数量关系进入建模关系分析。"
   },
   {
-    "source": "H04",
-    "target": "F05",
+    "source": "M01B",
+    "target": "M01C",
     "type": "successor",
-    "comment": "压轴综合题往往以二次函数最值作为关键突破点,因此掌握最值是完成压轴题的重要能力。"
+    "comment": "复杂代数式建模基于简单建模能力。"
+  },
+  {
+    "source": "A04",
+    "target": "E01A",
+    "type": "prerequisite",
+    "comment": "整式运算能力是学习一元一次方程基础。"
+  },
+  {
+    "source": "A06",
+    "target": "E05C",
+    "type": "crosslink",
+    "comment": "部分二次方程可通过因式分解求解。"
+  },
+  {
+    "source": "RS04",
+    "target": "H03",
+    "type": "crosslink",
+    "comment": "根式化简常用于二次函数顶点式相关计算。"
   },
   {
-    "source": "P06",
-    "target": "E05",
+    "source": "A05",
+    "target": "H02",
     "type": "crosslink",
-    "comment": "部分一元二次方程可通过因式分解直接求解,因此因式分解能力影响方程求解效率。"
+    "comment": "一次函数代数式运算依赖整式运算。"
   },
   {
-    "source": "P06",
+    "source": "A07",
     "target": "H04",
     "type": "crosslink",
-    "comment": "二次函数的解析式常需因式分解才能看出顶点与最值特征,因此因式分解是函数最值的重要辅助。"
+    "comment": "二次函数最值常通过因式分解观察图像特征。"
+  },
+  {
+    "source": "A04",
+    "target": "E01A",
+    "type": "prerequisite",
+    "comment": "整式加减能力是一元一次方程移项与化简的基础。"
+  },
+  {
+    "source": "A03",
+    "target": "E01B",
+    "type": "prerequisite",
+    "comment": "同类项合并直接用于方程化简。"
+  },
+  {
+    "source": "R03",
+    "target": "E01A",
+    "type": "prerequisite",
+    "comment": "有理数加减是方程等式变形的根基。"
+  },
+  {
+    "source": "E01A",
+    "target": "E01B",
+    "type": "successor",
+    "comment": "掌握等式性质后才能熟练移项与合并同类项。"
+  },
+  {
+    "source": "E01B",
+    "target": "E01C",
+    "type": "successor",
+    "comment": "进行方程检验必须在化简后进行。"
+  },
+  {
+    "source": "E01A",
+    "target": "E02A",
+    "type": "prerequisite",
+    "comment": "方程组代入法需要一元一次方程的基础。"
+  },
+  {
+    "source": "E01B",
+    "target": "E02B",
+    "type": "prerequisite",
+    "comment": "加减法消元依赖方程移项与合并技巧。"
+  },
+  {
+    "source": "E02A",
+    "target": "E02C",
+    "type": "successor",
+    "comment": "掌握代入法后进入实际问题的建模应用。"
+  },
+  {
+    "source": "E02B",
+    "target": "E02C",
+    "type": "successor",
+    "comment": "加减法方法熟练后可用于应用方程组求解。"
+  },
+  {
+    "source": "E01A",
+    "target": "E03A",
+    "type": "prerequisite",
+    "comment": "不等式性质基于等式变形思想。"
+  },
+  {
+    "source": "E03A",
+    "target": "E03B",
+    "type": "successor",
+    "comment": "不等式性质掌握后进入不等式求解。"
+  },
+  {
+    "source": "E03B",
+    "target": "E03C",
+    "type": "successor",
+    "comment": "解集的表示必须在求解后进行。"
+  },
+  {
+    "source": "E03C",
+    "target": "E03D",
+    "type": "successor",
+    "comment": "不等式组的解集建立在单个不等式解集基础上。"
+  },
+  {
+    "source": "F03",
+    "target": "E04B",
+    "type": "prerequisite",
+    "comment": "分式方程去分母必须先掌握通分。"
   },
   {
     "source": "F02",
-    "target": "E04",
+    "target": "E04B",
+    "type": "prerequisite",
+    "comment": "分式约分用于方程去分母后化简。"
+  },
+  {
+    "source": "E04B",
+    "target": "E04C",
+    "type": "successor",
+    "comment": "去分母后需检验增根,这是分式方程核心步骤。"
+  },
+  {
+    "source": "E04A",
+    "target": "E04B",
+    "type": "successor",
+    "comment": "理解分式方程基本概念后进入求解过程。"
+  },
+  {
+    "source": "A06",
+    "target": "E05C",
+    "type": "prerequisite",
+    "comment": "因式分解是部分一元二次方程求解的基础方法。"
+  },
+  {
+    "source": "R05",
+    "target": "E05A",
+    "type": "prerequisite",
+    "comment": "指数运算性质用于二次项处理与配平方化简。"
+  },
+  {
+    "source": "E01B",
+    "target": "E05A",
+    "type": "prerequisite",
+    "comment": "配方法需要熟练的一次项移项技巧。"
+  },
+  {
+    "source": "E05A",
+    "target": "E05B",
+    "type": "successor",
+    "comment": "求根公式由配方法推导而来,因此配方法是求根公式的基础。"
+  },
+  {
+    "source": "E05B",
+    "target": "E05E",
+    "type": "successor",
+    "comment": "根的分布判断依赖判别式与求根公式。"
+  },
+  {
+    "source": "E05B",
+    "target": "E05D",
+    "type": "successor",
+    "comment": "韦达定理建立在求根公式与二次方程根的性质之上。"
+  },
+  {
+    "source": "E05C",
+    "target": "E05D",
+    "type": "crosslink",
+    "comment": "部分韦达定理题可通过构造因式快速求解。"
+  },
+  {
+    "source": "E05D",
+    "target": "H04",
+    "type": "crosslink",
+    "comment": "二次函数最值常依赖顶点坐标与韦达定理的结合。"
+  },
+  {
+    "source": "E01A",
+    "target": "APP_E1",
+    "type": "prerequisite",
+    "comment": "行程问题建模以简单方程为基础。"
+  },
+  {
+    "source": "APP_E1",
+    "target": "APP_E4",
+    "type": "crosslink",
+    "comment": "行程模型常可转化为几何方程模型。"
+  },
+  {
+    "source": "E02C",
+    "target": "APP_E2",
+    "type": "crosslink",
+    "comment": "工程问题通常建立方程组。"
+  },
+  {
+    "source": "F06",
+    "target": "APP_E3",
+    "type": "crosslink",
+    "comment": "溶液浓度问题常涉及复杂分式化简。"
+  },
+  {
+    "source": "E05A",
+    "target": "APP_E4",
+    "type": "crosslink",
+    "comment": "几何方程建模常需用到配方法求关键点位置。"
+  },
+  {
+    "source": "E05B",
+    "target": "APP_E4",
+    "type": "crosslink",
+    "comment": "复杂几何模型需应用求根公式判断距离与位置关系。"
+  },
+  {
+    "source": "H02",
+    "target": "E02C",
+    "type": "crosslink",
+    "comment": "函数代数结构可用于建立二元一次方程组模型。"
+  },
+  {
+    "source": "A09",
+    "target": "E05A",
+    "type": "crosslink",
+    "comment": "复杂二次方程常需先进行代数式综合化简才能求解。"
+  },
+  {
+    "source": "RS03",
+    "target": "E05B",
+    "type": "crosslink",
+    "comment": "根式化简用于求根公式中根号部分的处理。"
+  },
+  {
+    "source": "G01A",
+    "target": "G01B",
+    "type": "successor",
+    "comment": "点线面的概念是角的定义基础。"
+  },
+  {
+    "source": "G01B",
+    "target": "G01C",
+    "type": "successor",
+    "comment": "角的分类后学习角的度量。"
+  },
+  {
+    "source": "G01C",
+    "target": "G01D",
+    "type": "successor",
+    "comment": "掌握角度量后可理解对顶角与邻补角关系。"
+  },
+  {
+    "source": "G01D",
+    "target": "G02A",
+    "type": "prerequisite",
+    "comment": "理解对顶角等概念后才能进入平行线判定。"
+  },
+  {
+    "source": "G02A",
+    "target": "G02B",
+    "type": "successor",
+    "comment": "平行线判定后学习其性质。"
+  },
+  {
+    "source": "G02B",
+    "target": "G02C",
+    "type": "crosslink",
+    "comment": "平移是平行线性质的几何变换基础。"
+  },
+  {
+    "source": "G02B",
+    "target": "G03A",
+    "type": "prerequisite",
+    "comment": "平行线性质是理解三角形角关系的基础。"
+  },
+  {
+    "source": "G03A",
+    "target": "G03B",
+    "type": "successor",
+    "comment": "三角形分类后学习三角形内角和与外角定理。"
+  },
+  {
+    "source": "G03B",
+    "target": "G03C",
+    "type": "successor",
+    "comment": "内角关系掌握后可学习三角形不等式与稳定性。"
+  },
+  {
+    "source": "G03C",
+    "target": "G03D",
+    "type": "successor",
+    "comment": "三角形基本性质掌握后才能深入研究角平分线性质。"
+  },
+  {
+    "source": "G03D",
+    "target": "G03E",
+    "type": "successor",
+    "comment": "角平分线与中线、重心性质直接关联。"
+  },
+  {
+    "source": "G03B",
+    "target": "G03F",
+    "type": "prerequisite",
+    "comment": "三角形角与边关系是全等三角形判定基础。"
+  },
+  {
+    "source": "G03F",
+    "target": "G03G",
+    "type": "successor",
+    "comment": "全等判定后进入几何证明与辅助线构造应用。"
+  },
+  {
+    "source": "G03F",
+    "target": "G04A",
+    "type": "prerequisite",
+    "comment": "四边形性质可以用全等三角形证明其定理。"
+  },
+  {
+    "source": "G04A",
+    "target": "G04B",
+    "type": "successor",
+    "comment": "了解四边形分类后进入平行四边形判定。"
+  },
+  {
+    "source": "G04B",
+    "target": "G04C",
+    "type": "successor",
+    "comment": "判定后学习平行四边形性质。"
+  },
+  {
+    "source": "G04C",
+    "target": "G04D",
+    "type": "successor",
+    "comment": "矩形是平行四边形的特例,需先学性质。"
+  },
+  {
+    "source": "G04C",
+    "target": "G04E",
+    "type": "successor",
+    "comment": "菱形性质依赖平行四边形性质。"
+  },
+  {
+    "source": "G04D",
+    "target": "G04F",
+    "type": "successor",
+    "comment": "正方形兼具矩形与菱形所有性质,是综合性质。"
+  },
+  {
+    "source": "G04E",
+    "target": "G04F",
+    "type": "successor",
+    "comment": "正方形也是菱形的特例。"
+  },
+  {
+    "source": "G03F",
+    "target": "G05A",
+    "type": "crosslink",
+    "comment": "圆的性质中大量使用全等三角形(如等弧等弦)。"
+  },
+  {
+    "source": "G05A",
+    "target": "G05B",
+    "type": "successor",
+    "comment": "弦与圆心距关系基于圆的基本结构。"
+  },
+  {
+    "source": "G05B",
+    "target": "G05C",
+    "type": "successor",
+    "comment": "切线性质基于弦和圆心距关系。"
+  },
+  {
+    "source": "G05C",
+    "target": "G05D",
+    "type": "successor",
+    "comment": "圆周角定理依赖切线与弦所构图形分析。"
+  },
+  {
+    "source": "G05D",
+    "target": "G05E",
+    "type": "successor",
+    "comment": "掌握圆周角定理后可学习扇形弧长与面积计算。"
+  },
+  {
+    "source": "G03D",
+    "target": "G06A",
+    "type": "crosslink",
+    "comment": "角平分线常作为辅助线使用于几何证明。"
+  },
+  {
+    "source": "G03F",
+    "target": "G06A",
+    "type": "crosslink",
+    "comment": "全等构造中常通过辅助线使用延长、平移等技巧。"
+  },
+  {
+    "source": "G02C",
+    "target": "G06A",
+    "type": "crosslink",
+    "comment": "平移用于构造平行辅助线。"
+  },
+  {
+    "source": "G06A",
+    "target": "G06B",
+    "type": "successor",
+    "comment": "使用辅助线需要结合数形结合解决复杂应用。"
+  },
+  {
+    "source": "G06B",
+    "target": "G06C",
+    "type": "successor",
+    "comment": "数形结合能力是几何证明与推理的核心基础。"
+  },
+  {
+    "source": "G03F",
+    "target": "SIM01A",
+    "type": "crosslink",
+    "comment": "相似三角形概念依赖全等三角形的边角对应关系。"
+  },
+  {
+    "source": "G02B",
+    "target": "SIM02D",
+    "type": "crosslink",
+    "comment": "平行线分线段比例是相似三角形的重要来源。"
+  },
+  {
+    "source": "G03B",
+    "target": "PY01A",
+    "type": "prerequisite",
+    "comment": "理解三角形角关系后才能学习直角三角形性质。"
+  },
+  {
+    "source": "G05D",
+    "target": "SIM03A",
+    "type": "crosslink",
+    "comment": "圆周角定理常用于相似三角形辅助关系构造。"
+  },
+  {
+    "source": "G06C",
+    "target": "SIM03D",
+    "type": "crosslink",
+    "comment": "几何综合证明能力直接影响相似压轴题的推理链。"
+  },
+  {
+    "source": "G04C",
+    "target": "M04C1",
+    "type": "crosslink",
+    "comment": "三角形面积计算需用到平行四边形面积转化。"
+  },
+  {
+    "source": "G05E",
+    "target": "M04C3",
+    "type": "crosslink",
+    "comment": "扇形面积公式来自圆的弧长和圆周角关系。"
+  },
+  {
+    "source": "G03B",
+    "target": "H01",
+    "type": "crosslink",
+    "comment": "三角形角度计算常用于函数图像斜率判断与倾斜角。"
+  },
+  {
+    "source": "G05D",
+    "target": "H01",
+    "type": "crosslink",
+    "comment": "圆周角定理常用于函数图像几何性质的推导与证明。"
+  },
+  {
+    "source": "M04A1",
+    "target": "M04B1",
+    "type": "prerequisite",
+    "comment": "长度单位的理解是计算三角形周长的基础。"
+  },
+  {
+    "source": "M04A1",
+    "target": "M04B2",
+    "type": "prerequisite",
+    "comment": "掌握线段长度后才能正确计算四边形周长。"
+  },
+  {
+    "source": "M04A1",
+    "target": "M04B3",
+    "type": "prerequisite",
+    "comment": "圆周长计算需要长度单位理解。"
+  },
+  {
+    "source": "G03A",
+    "target": "M04B1",
+    "type": "crosslink",
+    "comment": "三角形周长计算依赖三边概念与分类。"
+  },
+  {
+    "source": "G04A",
+    "target": "M04B2",
+    "type": "crosslink",
+    "comment": "四边形周长计算依赖四边形基本分类与性质。"
+  },
+  {
+    "source": "G05A",
+    "target": "M04B3",
+    "type": "crosslink",
+    "comment": "圆的基本性质是圆周长公式的来源。"
+  },
+  {
+    "source": "M04A2",
+    "target": "M04C1",
+    "type": "prerequisite",
+    "comment": "面积单位是三角形面积计算的基础。"
+  },
+  {
+    "source": "M04A2",
+    "target": "M04C2",
+    "type": "prerequisite",
+    "comment": "面积单位理解是平行四边形和梯形面积公式的基础。"
+  },
+  {
+    "source": "M04A2",
+    "target": "M04C3",
+    "type": "prerequisite",
+    "comment": "扇形面积计算依赖面积单位与比例理解。"
+  },
+  {
+    "source": "M04A2",
+    "target": "M04C4",
+    "type": "prerequisite",
+    "comment": "组合图形面积需要单位面积理解。"
+  },
+  {
+    "source": "G03B",
+    "target": "M04C1",
+    "type": "crosslink",
+    "comment": "三角形内角与高的位置关系是面积公式实际使用的关键。"
+  },
+  {
+    "source": "G04C",
+    "target": "M04C2",
+    "type": "crosslink",
+    "comment": "平行四边形性质与面积公式联系紧密。"
+  },
+  {
+    "source": "G05D",
+    "target": "M04C3",
+    "type": "crosslink",
+    "comment": "扇形面积公式来源于圆周角定理与弧长比例关系。"
+  },
+  {
+    "source": "M04C1",
+    "target": "M04C4",
+    "type": "successor",
+    "comment": "掌握基础三角形面积后才能处理组合图形面积。"
+  },
+  {
+    "source": "M04C2",
+    "target": "M04C4",
+    "type": "successor",
+    "comment": "平行四边形与梯形面积用于组合图形的拆分与构造。"
+  },
+  {
+    "source": "M04C3",
+    "target": "M04C4",
+    "type": "crosslink",
+    "comment": "扇形面积常出现在组合图形面积问题中。"
+  },
+  {
+    "source": "M04A3",
+    "target": "M04D1",
+    "type": "prerequisite",
+    "comment": "立方体与长方体体积单位依赖体积单位理解。"
+  },
+  {
+    "source": "M04A3",
+    "target": "M04D3",
+    "type": "prerequisite",
+    "comment": "圆柱体积公式依赖体积单位理解。"
+  },
+
+  {
+    "source": "M04D1",
+    "target": "M04D2",
+    "type": "successor",
+    "comment": "棱柱体积是长方体体积的推广。"
+  },
+  {
+    "source": "M04D1",
+    "target": "M04D4",
+    "type": "crosslink",
+    "comment": "长方体展开图是理解一般立体展开的基础。"
+  },
+  {
+    "source": "G05A",
+    "target": "M04D3",
+    "type": "crosslink",
+    "comment": "圆柱体积公式来源于圆面积公式。"
+  },
+  {
+    "source": "M04C3",
+    "target": "M04D3",
+    "type": "crosslink",
+    "comment": "扇形面积计算用于圆柱侧面积构造理解。"
+  },
+  {
+    "source": "G02C",
+    "target": "M04E1",
+    "type": "crosslink",
+    "comment": "平移是图形变换的基础,且长度保持性对度量重要。"
+  },
+  {
+    "source": "G03B",
+    "target": "M04E2",
+    "type": "crosslink",
+    "comment": "旋转常用于三角形角度变化分析。"
+  },
+  {
+    "source": "G01D",
+    "target": "M04E3",
+    "type": "prerequisite",
+    "comment": "对顶角、邻补角等角关系是轴对称角度变化理解基础。"
+  },
+  {
+    "source": "M04E1",
+    "target": "M04E4",
+    "type": "successor",
+    "comment": "平移掌握后进入三大变换组合应用。"
+  },
+  {
+    "source": "M04E2",
+    "target": "M04E4",
+    "type": "successor",
+    "comment": "旋转是图形变换综合的重要部分。"
+  },
+  {
+    "source": "M04E3",
+    "target": "M04E4",
+    "type": "successor",
+    "comment": "掌握轴对称后进入综合变换。"
+  },
+  {
+    "source": "M04C4",
+    "target": "M04F1",
+    "type": "crosslink",
+    "comment": "复杂组合图形常通过代数建模处理几何量关系。"
+  },
+  {
+    "source": "M04D3",
+    "target": "M04F1",
+    "type": "crosslink",
+    "comment": "结合立体几何量与代数求未知量。"
+  },
+  {
+    "source": "M04E4",
+    "target": "M04F2",
+    "type": "crosslink",
+    "comment": "几何变换作为中考图形综合计算的重要技巧。"
+  },
+  {
+    "source": "SIM02D",
+    "target": "M04F2",
+    "type": "crosslink",
+    "comment": "相似三角形中常通过面积比例求组合图形面积。"
+  },
+  {
+    "source": "PY01A",
+    "target": "M04F2",
+    "type": "crosslink",
+    "comment": "勾股定理在图形度量综合计算中极为常用。"
+  },
+  {
+    "source": "G03F",
+    "target": "SIM01A",
+    "type": "prerequisite",
+    "comment": "全等三角形中的对应边和对应角概念是相似三角形的基础。"
+  },
+  {
+    "source": "G02B",
+    "target": "SIM01A",
+    "type": "crosslink",
+    "comment": "平行线的对应角、内错角关系常用于构造相似三角形。"
+  },
+  {
+    "source": "SIM01A",
+    "target": "SIM01B",
+    "type": "successor",
+    "comment": "理解相似概念后,首先学习AA相似判定。"
+  },
+  {
+    "source": "SIM01A",
+    "target": "SIM01C",
+    "type": "successor",
+    "comment": "相似的边角对应关系用于SAS比例判定。"
+  },
+  {
+    "source": "SIM01A",
+    "target": "SIM01D",
+    "type": "successor",
+    "comment": "SS比例判定基于相似概念中的边比关系。"
+  },
+  {
+    "source": "G02B",
+    "target": "SIM01B",
+    "type": "crosslink",
+    "comment": "平行线形成对应角相等,因此产生AA相似。"
+  },
+  {
+    "source": "G03B",
+    "target": "SIM01B",
+    "type": "crosslink",
+    "comment": "三角形内角和的使用在AA相似判定中极其常见。"
+  },
+  {
+    "source": "SIM01B",
+    "target": "SIM01E",
+    "type": "successor",
+    "comment": "掌握基础相似判定后进行综合构造与转换。"
+  },
+  {
+    "source": "SIM01C",
+    "target": "SIM01E",
+    "type": "successor",
+    "comment": "边比+夹角的判定常用于中考构造相似。"
+  },
+  {
+    "source": "SIM01D",
+    "target": "SIM01E",
+    "type": "successor",
+    "comment": "SS比例判定后进入构造性质应用。"
+  },
+  {
+    "source": "SIM01B",
+    "target": "SIM02A",
+    "type": "prerequisite",
+    "comment": "一旦判定相似,首先得到边比性质。"
+  },
+  {
+    "source": "SIM02A",
+    "target": "SIM02B",
+    "type": "successor",
+    "comment": "边比平方得到面积比,是相似性质核心。"
+  },
+  {
+    "source": "SIM02B",
+    "target": "SIM02C",
+    "type": "successor",
+    "comment": "面积比推广到体积比,尽管为初步认识。"
+  },
+  {
+    "source": "G02B",
+    "target": "SIM02D",
+    "type": "prerequisite",
+    "comment": "平行线分线段比例是相似性质的重要来源。"
+  },
+  {
+    "source": "SIM02A",
+    "target": "SIM02D",
+    "type": "crosslink",
+    "comment": "比例线段定理可由相似三角形性质推导。"
+  },
+  {
+    "source": "SIM02A",
+    "target": "SIM02E",
+    "type": "successor",
+    "comment": "掌握边比后进入复杂比例构造与代数结合。"
+  },
+  {
+    "source": "SIM02D",
+    "target": "SIM02E",
+    "type": "crosslink",
+    "comment": "比例线段是几何综合比例链的关键。"
+  },
+  {
+    "source": "G03B",
+    "target": "PY01A",
+    "type": "prerequisite",
+    "comment": "三角形角关系是理解直角三角形的基础。"
+  },
+  {
+    "source": "PY01A",
+    "target": "PY01B",
+    "type": "successor",
+    "comment": "勾股逆定理基于勾股正定理。"
+  },
+  {
+    "source": "PY01A",
+    "target": "PY01C",
+    "type": "successor",
+    "comment": "勾股数来自勾股定理的特殊整数解。"
+  },
+  {
+    "source": "PY01A",
+    "target": "PY01D",
+    "type": "successor",
+    "comment": "直角三角形性质依赖勾股定理。"
+  },
+  {
+    "source": "PY01A",
+    "target": "PY02A",
+    "type": "prerequisite",
+    "comment": "距离公式的本质就是勾股定理。"
+  },
+  {
+    "source": "PY01A",
+    "target": "PY02B",
+    "type": "prerequisite",
+    "comment": "最短路径问题多通过反射法构造直角应用勾股定理。"
+  },
+  {
+    "source": "PY01D",
+    "target": "PY02C",
+    "type": "prerequisite",
+    "comment": "复杂图形中的直角关系常依赖直角三角形性质。"
+  },
+  {
+    "source": "SIM02A",
+    "target": "PY02D",
+    "type": "crosslink",
+    "comment": "几何结构中常结合相似三角形与勾股定理求量。"
+  },
+  {
+    "source": "PY01A",
+    "target": "PY02D",
+    "type": "crosslink",
+    "comment": "勾股定理与相似性质是中考压轴题的核心组合。"
+  },
+  {
+    "source": "SIM02D",
+    "target": "PY02D",
+    "type": "crosslink",
+    "comment": "平行线比例定理常用于构造相似与勾股联合结构。"
+  },
+  {
+    "source": "G06A",
+    "target": "SIM03A",
+    "type": "prerequisite",
+    "comment": "辅助线构造是相似构造的关键技能。"
+  },
+  {
+    "source": "SIM01E",
+    "target": "SIM03A",
+    "type": "successor",
+    "comment": "掌握相似判定与构造后进入压轴构造策略。"
+  },
+  {
+    "source": "SIM02A",
+    "target": "SIM03B",
+    "type": "prerequisite",
+    "comment": "比例关系链构建基于相似性质。"
+  },
+  {
+    "source": "SIM02D",
+    "target": "SIM03B",
+    "type": "crosslink",
+    "comment": "平行线比例定理常用于构建多段比例链。"
+  },
+  {
+    "source": "PY01A",
+    "target": "SIM03C",
+    "type": "crosslink",
+    "comment": "构造直角常用于建立相似结构或比例关系。"
+  },
+  {
+    "source": "G06A",
+    "target": "SIM03C",
+    "type": "crosslink",
+    "comment": "通过作垂线等构造直角是几何技巧基础。"
+  },
+  {
+    "source": "SIM03A",
+    "target": "SIM03D",
+    "type": "successor",
+    "comment": "辅助线技巧用于几何综合压轴题。"
+  },
+  {
+    "source": "SIM03B",
+    "target": "SIM03D",
+    "type": "successor",
+    "comment": "比例链是中考压轴几何题的核心结构。"
+  },
+  {
+    "source": "SIM03C",
+    "target": "SIM03D",
+    "type": "successor",
+    "comment": "直角构造与比例链结合用于综合推理。"
+  },
+  {
+    "source": "PY02D",
+    "target": "SIM03D",
+    "type": "crosslink",
+    "comment": "相似 + 勾股联合构成中考压轴题最重要模型。"
+  },
+  {
+    "source": "M04C1",
+    "target": "PY02A",
+    "type": "crosslink",
+    "comment": "面积相关问题常需要使用勾股定理解高或距离。"
+  },
+  {
+    "source": "M04C4",
+    "target": "SIM03D",
+    "type": "crosslink",
+    "comment": "组合图形面积题中大量运用相似与勾股联合推理。"
+  },
+  {
+    "source": "G05D",
+    "target": "SIM03D",
+    "type": "crosslink",
+    "comment": "圆周角定理常用于构造相似或直角结构。"
+  },
+  {
+    "source": "ST01A",
+    "target": "ST01B",
+    "type": "successor",
+    "comment": "获取数据后才能进行分类整理。"
+  },
+  {
+    "source": "ST01B",
+    "target": "ST01C",
+    "type": "successor",
+    "comment": "频数与频率基于数据分类整理。"
+  },
+  {
+    "source": "ST01C",
+    "target": "ST02A",
+    "type": "prerequisite",
+    "comment": "频数与频率是平均数和加权平均数的重要基础。"
+  },
+  {
+    "source": "ST01C",
+    "target": "ST02B",
+    "type": "prerequisite",
+    "comment": "中位数离不开对数据排序与频率分布的理解。"
+  },
+  {
+    "source": "ST01C",
+    "target": "ST02C",
+    "type": "prerequisite",
+    "comment": "众数来源于频数最高的分类。"
+  },
+  {
+    "source": "ST02A",
+    "target": "ST03A",
+    "type": "prerequisite",
+    "comment": "极差分析在平均数基础上理解数据波动。"
+  },
+  {
+    "source": "ST03A",
+    "target": "ST03B",
+    "type": "successor",
+    "comment": "方差是对数据波动程度更深入的度量。"
+  },
+  {
+    "source": "ST03B",
+    "target": "ST03C",
+    "type": "successor",
+    "comment": "标准差基于方差的平方根定义。"
+  },
+  {
+    "source": "ST01C",
+    "target": "ST04A",
+    "type": "crosslink",
+    "comment": "条形图展示频数分布。"
+  },
+  {
+    "source": "ST01C",
+    "target": "ST04B",
+    "type": "crosslink",
+    "comment": "折线图展示趋势变化,基于频数或频率。"
+  },
+  {
+    "source": "ST01C",
+    "target": "ST04C",
+    "type": "crosslink",
+    "comment": "扇形图展示频率比例。"
+  },
+  {
+    "source": "ST04A",
+    "target": "ST04D",
+    "type": "successor",
+    "comment": "掌握基本统计图后进入综合分析图表。"
+  },
+  {
+    "source": "ST04B",
+    "target": "ST04D",
+    "type": "successor",
+    "comment": "折线图趋势分析是综合图表解读的重要组成。"
+  },
+  {
+    "source": "ST04C",
+    "target": "ST04D",
+    "type": "successor",
+    "comment": "扇形图比例为综合图表解读提供关键信息。"
+  },
+  {
+    "source": "ST01A",
+    "target": "ST05A",
+    "type": "prerequisite",
+    "comment": "概率研究之前必须理解随机性来源于数据过程。"
+  },
+  {
+    "source": "ST05A",
+    "target": "ST05B",
+    "type": "successor",
+    "comment": "随机事件概念后学习古典概率(等可能模型)。"
+  },
+  {
+    "source": "ST05A",
+    "target": "ST05C",
+    "type": "successor",
+    "comment": "实验概率来自随机事件的重复试验。"
+  },
+  {
+    "source": "ST05B",
+    "target": "ST05D",
+    "type": "prerequisite",
+    "comment": "树状图和列表法构建样本空间用于古典概率计算。"
+  },
+  {
+    "source": "ST05C",
+    "target": "ST05D",
+    "type": "crosslink",
+    "comment": "实验概率可通过树状图辅助分析复杂事件的频率。"
+  },
+  {
+    "source": "ST02A",
+    "target": "ST06A",
+    "type": "crosslink",
+    "comment": "平均数是统计综合题的核心基础。"
+  },
+  {
+    "source": "ST02B",
+    "target": "ST06A",
+    "type": "crosslink",
+    "comment": "中位数常用于数据趋势判断。"
+  },
+  {
+    "source": "ST03C",
+    "target": "ST06A",
+    "type": "crosslink",
+    "comment": "标准差用于判断数据稳定性(新课标核心)。"
+  },
+  {
+    "source": "ST04D",
+    "target": "ST06A",
+    "type": "prerequisite",
+    "comment": "综合统计图分析常出现在数据统计综合题中。"
+  },
+  {
+    "source": "ST05B",
+    "target": "ST06B",
+    "type": "prerequisite",
+    "comment": "古典概率用于概率综合题第一阶段分析。"
+  },
+  {
+    "source": "ST05D",
+    "target": "ST06B",
+    "type": "prerequisite",
+    "comment": "树状图+列表法用于概率综合题中的多阶段概率。"
+  },
+  {
+    "source": "ST05C",
+    "target": "ST06B",
+    "type": "crosslink",
+    "comment": "实验概率用于概率综合题中的概率估计与修正。"
+  },
+  {
+    "source": "ST06A",
+    "target": "ST06C",
+    "type": "successor",
+    "comment": "统计判断能力用于现实情境中的概率估计模型。"
+  },
+  {
+    "source": "ST06B",
+    "target": "ST06C",
+    "type": "successor",
+    "comment": "概率分析能力用于综合的统计概率融合问题。"
+  },
+
+  {
+    "source": "E02C",
+    "target": "ST06A",
+    "type": "crosslink",
+    "comment": "统计量求解中经常将平均数等量转化为方程求解。"
+  },
+  {
+    "source": "H01",
+    "target": "ST06A",
+    "type": "crosslink",
+    "comment": "折线图趋势判断与函数图像的变化率密切相关。"
+  },
+  {
+    "source": "M04C4",
+    "target": "ST06C",
     "type": "crosslink",
-    "comment": "处理应用题(如行程、浓度)前,学生通常需要先完成相关的分式运算化简。"
+    "comment": "几何面积变化常结合概率出现于中考融合题。"
   }
 ]

Разница между файлами не показана из-за своего большого размера
+ 963 - 262
public/data/tree.json


+ 1 - 0
public/favicon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="#3b82f6"/><text x="50" y="50" font-family="Arial" font-size="40" fill="white" text-anchor="middle" dominant-baseline="central">M</text></svg>

+ 35 - 0
query_paper.php

@@ -0,0 +1,35 @@
+<?php
+require __DIR__ . '/vendor/autoload.php';
+
+$app = require_once __DIR__ . '/bootstrap/app.php';
+$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
+$kernel->bootstrap();
+
+$pdo = new PDO('mysql:host=120.78.197.180;dbname=math;charset=utf8mb4', 'root', 'bamasoso902');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+$stmt = $pdo->prepare('SELECT * FROM papers WHERE paper_id = ?');
+$stmt->execute(['paper_1765263438_26997949']);
+$paper = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if ($paper) {
+    echo "=== 试卷基本信息 ===\n";
+    print_r($paper);
+    
+    echo "\n=== 试卷题目 ===\n";
+    $stmt2 = $pdo->prepare('SELECT * FROM paper_questions WHERE paper_id = ? ORDER BY question_number');
+    $stmt2->execute(['paper_1765263438_26997949']);
+    $questions = $stmt2->fetchAll(PDO::FETCH_ASSOC);
+    
+    echo "题目数量: " . count($questions) . "\n\n";
+    foreach ($questions as $q) {
+        echo "第{$q['question_number']}题:\n";
+        echo "  ID: {$q['question_bank_id']}\n";
+        echo "  内容: " . substr($q['question_text'], 0, 100) . "...\n";
+        echo "  知识点: {$q['knowledge_point']}\n";
+        echo "  题目类型: {$q['question_type']}\n";
+        echo "  分值: {$q['score']}\n\n";
+    }
+} else {
+    echo "未找到试卷\n";
+}

+ 35 - 0
query_paper_details.php

@@ -0,0 +1,35 @@
+<?php
+require __DIR__ . '/vendor/autoload.php';
+
+$app = require_once __DIR__ . '/bootstrap/app.php';
+$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
+$kernel->bootstrap();
+
+$pdo = new PDO('mysql:host=120.78.197.180;dbname=math;charset=utf8mb4', 'root', 'bamasoso902');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+$stmt = $pdo->prepare('SELECT * FROM papers WHERE paper_id = ?');
+$stmt->execute(['paper_1765263438_26997949']);
+$paper = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if ($paper) {
+    echo "=== 试卷基本信息 ===\n";
+    print_r($paper);
+    
+    echo "\n=== 试卷题目详情 ===\n";
+    $stmt2 = $pdo->prepare('SELECT * FROM paper_questions WHERE paper_id = ? ORDER BY question_number');
+    $stmt2->execute(['paper_1765263438_26997949']);
+    $questions = $stmt2->fetchAll(PDO::FETCH_ASSOC);
+    
+    echo "题目数量: " . count($questions) . "\n\n";
+    foreach ($questions as $i => $q) {
+        echo "=== 第{$q['question_number']}题 ===\n";
+        echo "Question ID: {$q['question_bank_id']}\n";
+        echo "Question Text:\n{$q['question_text']}\n";
+        echo "Knowledge Point: {$q['knowledge_point']}\n";
+        echo "Question Type: {$q['question_type']}\n";
+        echo "Score: {$q['score']}\n";
+        echo "Correct Answer: {$q['correct_answer']}\n";
+        echo "\n";
+    }
+}

+ 66 - 0
resources/css/app.css

@@ -183,6 +183,72 @@
         @apply transform hover:scale-105;
     }
 
+    /* Filament 页面头部统一样式 */
+    .fi-header {
+        @apply mb-6 flex flex-wrap items-start justify-between gap-3 rounded-xl bg-white/90 px-5 py-4 shadow-sm ring-1 ring-slate-200/80 backdrop-blur-sm;
+    }
+
+    .fi-header-heading {
+        @apply text-2xl font-bold text-slate-900 leading-tight;
+    }
+
+    .fi-header-subheading {
+        @apply mt-1 text-sm text-slate-600;
+    }
+
+    /* Filament 内置面包屑全局样式 */
+    .fi-breadcrumbs {
+        @apply mb-4 flex items-center rounded-lg bg-base-100 px-4 py-2 shadow-sm ring-1 ring-base-200;
+    }
+
+    .fi-breadcrumbs-list {
+        @apply breadcrumbs text-sm text-base-content/70;
+    }
+
+    .fi-breadcrumbs-item-label {
+        @apply inline-flex items-center gap-1 text-base-content/70 transition-colors duration-150 hover:text-primary-600;
+    }
+
+    .fi-breadcrumbs-item .fi-breadcrumbs-item-separator {
+        @apply text-base-300;
+    }
+
+    /* 当前节点加粗,禁用 hover 色 */
+    .fi-breadcrumbs-item:last-child .fi-breadcrumbs-item-label {
+        @apply font-semibold text-base-content cursor-default hover:text-base-content;
+    }
+
+    /* 头部栏里的面包屑去掉额外下边距 */
+    .fi-header nav.fi-breadcrumbs {
+        @apply mb-0;
+    }
+
+    /* 面包屑样式增强(全局) */
+    nav[aria-label="Breadcrumb"],
+    .breadcrumb-modern {
+        @apply mb-4 flex items-center rounded-xl bg-white/80 px-4 py-2 shadow-sm ring-1 ring-slate-200/80 backdrop-blur-sm;
+    }
+
+    nav[aria-label="Breadcrumb"] ol,
+    .breadcrumb-modern ol {
+        @apply flex items-center gap-3 text-sm text-slate-600;
+    }
+
+    nav[aria-label="Breadcrumb"] a,
+    .breadcrumb-modern a {
+        @apply inline-flex items-center gap-1 text-slate-600 transition-colors duration-150 hover:text-primary-600;
+    }
+
+    nav[aria-label="Breadcrumb"] .crumb-current,
+    .breadcrumb-modern .crumb-current {
+        @apply inline-flex items-center gap-1 font-semibold text-slate-900;
+    }
+
+    nav[aria-label="Breadcrumb"] .crumb-divider,
+    .breadcrumb-modern .crumb-divider {
+        @apply h-4 w-px bg-slate-200;
+    }
+
     /* 图片预览框架 */
     .mockup-window {
         @apply border border-slate-300 rounded-t-lg overflow-hidden;

+ 245 - 0
resources/views/components/exam/paper-body.blade.php

@@ -0,0 +1,245 @@
+@php
+    $choiceQuestions = $questions['choice'] ?? [];
+    $fillQuestions = $questions['fill'] ?? [];
+    $answerQuestions = $questions['answer'] ?? [];
+    $gradingMode = $grading ?? false;
+
+    // 计算填空空格数量
+    $countBlanks = function($text) {
+        $count = 0;
+        $count += preg_match_all('/_{2,}/u', $text, $m);
+        $count += preg_match_all('/(\s*)/u', $text, $m);
+        $count += preg_match_all('/\(\s*\)/', $text, $m);
+        return max(1, $count);
+    };
+
+    // 计算步骤数量
+    $countSteps = function($text) {
+        $matches = [];
+        $cnt = preg_match_all('/第\s*\d+\s*步/u', $text ?? '', $matches);
+        return max(1, $cnt);
+    };
+
+    $renderBoxes = function($num) {
+        return str_repeat('<span style="display:inline-block;width:14px;height:14px;line-height:14px;border:1px solid #333;margin-right:4px;vertical-align:middle;"></span>', $num);
+    };
+@endphp
+
+<!-- 一、选择题 -->
+<div class="section-title">一、选择题
+    @if(count($choiceQuestions) > 0)
+        @php
+            $choiceTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $choiceQuestions));
+        @endphp
+        (本大题共 {{ count($choiceQuestions) }} 小题,共 {{ $choiceTotal }} 分)
+    @else
+        (本大题共 0 小题,共 0 分)
+    @endif
+</div>
+@if(count($choiceQuestions) > 0)
+    @foreach($choiceQuestions as $index => $q)
+        @php
+            $questionNumber = $index + 1;
+            $cleanContent = preg_replace('/^\d+[\.、]\s*/', '', $q->content);
+            $cleanContent = trim($cleanContent);
+            $options = $q->options ?? [];
+            if (empty($options)) {
+                $pattern = '/([A-D])[\.、:.:]\s*(.+?)(?=\s*[A-D][\.、:.:]|$)/su';
+                if (preg_match_all($pattern, $cleanContent, $matches, PREG_SET_ORDER)) {
+                    foreach ($matches as $match) {
+                        $optionText = trim($match[2]);
+                        if (!empty($optionText)) {
+                            $options[] = $optionText;
+                        }
+                    }
+                }
+            }
+            $stemLine = $cleanContent;
+            if (!empty($options)) {
+                if (preg_match('/^(.+?)(?=[A-D][\.、:.:])/su', $cleanContent, $stemMatch)) {
+                    $stemLine = trim($stemMatch[1]);
+                }
+            }
+            // 将题干中的空括号/下划线替换为短波浪线;如无占位符,则在末尾追加短波浪线
+            $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
+            $renderedStem = preg_replace(['/((\s*))/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $stemLine);
+            if ($renderedStem === $stemLine) {
+                $renderedStem .= ' ' . $blankSpan;
+            }
+            $renderedStem = \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
+        @endphp
+        <div class="question">
+            <div class="question-grid">
+                <div class="question-lead">
+                    @if($gradingMode)
+                        <span class="grading-boxes">{!! $renderBoxes(1) !!}</span>
+                    @endif
+                    <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
+                </div>
+                <div class="question-main">
+                    <span class="question-stem">{!! $renderedStem !!}</span>
+                </div>
+                @if(!empty($options))
+                    @php
+                        $optCount = count($options);
+                        $optionsClass = $optCount <= 4 ? 'options-grid-2' : 'options';
+                    @endphp
+                    <div class="question-lead spacer"></div>
+                    <div class="{{ $optionsClass }}">
+                        @foreach($options as $optIndex => $opt)
+                            @php $label = chr(65 + $optIndex); @endphp
+                            <div class="option option-compact">
+                                <strong>{{ $label }}.</strong>&nbsp;{!! \App\Services\MathFormulaProcessor::processFormulas($opt) !!}
+                            </div>
+                        @endforeach
+                    </div>
+                @endif
+                @if($gradingMode)
+                    <div class="question-lead spacer"></div>
+                    <div class="answer-meta">
+                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
+                        <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->solution ?? '') !!}</span></div>
+                    </div>
+                @endif
+            </div>
+        </div>
+    @endforeach
+@else
+    <div class="question">
+        <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
+            该题型正在生成中或暂无题目,请稍后刷新页面查看
+        </div>
+    </div>
+@endif
+
+<!-- 二、填空题 -->
+<div class="section-title">二、填空题
+    @if(count($fillQuestions) > 0)
+        @php
+            $fillTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $fillQuestions));
+        @endphp
+        (本大题共 {{ count($fillQuestions) }} 小题,共 {{ $fillTotal }} 分)
+    @else
+        (本大题共 0 小题,共 0 分)
+    @endif
+</div>
+@if(count($fillQuestions) > 0)
+    @foreach($fillQuestions as $index => $q)
+        @php
+            $questionNumber = count($choiceQuestions) + $index + 1;
+            $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
+            $renderedContent = preg_replace(['/((\s*))/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $q->content);
+            if ($renderedContent === $q->content) {
+                $renderedContent .= ' ' . $blankSpan;
+            }
+            $renderedContent = \App\Services\MathFormulaProcessor::processFormulas($renderedContent);
+        @endphp
+        <div class="question">
+            <div class="question-grid">
+                <div class="question-lead">
+                    @if($gradingMode)
+                        <span class="grading-boxes">{!! $renderBoxes($countBlanks($q->content ?? '')) !!}</span>
+                    @endif
+                    <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
+                </div>
+                <div class="question-main">
+                    <span class="question-stem">{!! $renderedContent !!}</span>
+                </div>
+                @if($gradingMode)
+                    <div class="question-lead spacer"></div>
+                    <div class="answer-meta">
+                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
+                        <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->solution ?? '') !!}</span></div>
+                    </div>
+                @endif
+            </div>
+        </div>
+    @endforeach
+@else
+    <div class="question">
+        <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
+            该题型正在生成中或暂无题目,请稍后刷新页面查看
+        </div>
+    </div>
+@endif
+
+<!-- 三、解答题 -->
+<div class="section-title">三、解答题
+    @if(count($answerQuestions) > 0)
+        (本大题共 {{ count($answerQuestions) }} 小题,共 {{ array_sum(array_column($answerQuestions, 'score')) }} 分。解答应写出文字说明、证明过程或演算步骤)
+    @else
+        (本大题共 0 小题,共 0 分)
+    @endif
+</div>
+@if(count($answerQuestions) > 0)
+    @foreach($answerQuestions as $index => $q)
+        @php
+            $questionNumber = count($choiceQuestions) + count($fillQuestions) + $index + 1;
+        @endphp
+        <div class="question">
+            <div class="question-grid">
+                <div class="question-lead">
+                    <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
+                </div>
+                <div class="question-main">
+                    @unless($gradingMode)
+                        <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)</span>
+                    @endunless
+                    <span class="question-stem">{!! \App\Services\MathFormulaProcessor::processFormulas($q->content) !!}</span>
+                </div>
+                @unless($gradingMode)
+                    <div class="question-lead spacer"></div>
+                    <div class="answer-area boxy">
+                        <span class="answer-label">作答</span>
+                    </div>
+                @endunless
+                @if($gradingMode)
+                    @php
+                        $solutionRaw = $q->solution ?? '';
+                        $solutionProcessed = \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
+                        // 去掉分步得分等分值标记
+                        $solutionProcessed = preg_replace('/(\s*\d+\s*分\s*)/u', '', $solutionProcessed);
+                        // 断行处理:在【解题思路】【详细解答】【最终答案】前后加换行
+                        $solutionProcessed = preg_replace('/【(解题思路|详细解答|最终答案)】/u', '<br><strong class="solution-heading">【$1】</strong><br>', $solutionProcessed);
+                        // 为每个“第 N 步”前添加方框;若没有,则在【详细解答】段落开头添加一个方框
+                        if (preg_match('/第\s*\d+\s*步/u', $solutionProcessed)) {
+                            $solutionProcessed = preg_replace_callback('/第\s*\d+\s*步/u', function($m) use ($renderBoxes) {
+                                return '<br><span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $m[0] . '</span></span>';
+                            }, $solutionProcessed);
+                        } else {
+                            // 在【详细解答】标题后追加方框;若无标题则在正文最前补一个
+                            $injected = false;
+                            $count = 0;
+                            $solutionProcessed = preg_replace(
+                                '/(<strong class="solution-heading">【详细解答】<\/strong><br>)/u',
+                                '$1' . '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">&nbsp;</span></span> ',
+                                $solutionProcessed,
+                                1,
+                                $count
+                            );
+                            if (!empty($count)) {
+                                $injected = true;
+                            }
+                            if (!$injected) {
+                                $solutionProcessed = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">&nbsp;</span></span> ' . ltrim($solutionProcessed);
+                            }
+                        }
+                        // 最终统一换行渲染
+                        $solutionProcessed = nl2br($solutionProcessed);
+                    @endphp
+                    <div class="question-lead spacer"></div>
+                    <div class="answer-meta">
+                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
+                        <div class="answer-line"><span class="solution-content">{!! $solutionProcessed !!}</span></div>
+                    </div>
+                @endif
+            </div>
+        </div>
+    @endforeach
+@else
+    <div class="question">
+        <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
+            该题型正在生成中或暂无题目,请稍后刷新页面查看
+        </div>
+    </div>
+@endif

+ 125 - 0
resources/views/filament/components/menu-visibility-toggle.blade.php

@@ -0,0 +1,125 @@
+@php
+    use App\Models\MenuPermission;
+    use App\Models\MenuConfig;
+    use Filament\Facades\Filament;
+
+    // 获取当前用户
+    $user = Filament::getCurrentPanel()->auth()->user();
+
+    // 检查是否是管理员
+    $isAdmin = $user && (
+        $user->role === 'admin' ||
+        $user->username === '17689974321'
+    );
+
+    if (!$isAdmin) {
+        return;
+    }
+
+    // 获取当前路径
+    $currentPath = request()->path();
+    $menuKey = null;
+
+    // 根据路径映射到菜单键
+    $menuMappings = [
+        'admin/ocr-paper-grading' => 'ocr-paper-grading',
+        'admin/exam-analysis' => 'exam-analysis',
+        'admin/dashboard' => 'dashboard',
+        'admin/exam-history' => 'exam-history',
+        'admin/intelligent-exam-generation' => 'intelligent-exam-generation',
+        'admin/knowledge-graph' => 'knowledge-graph',
+        'admin/student-management' => 'student-management',
+        'admin/teacher-management' => 'teacher-management',
+    ];
+
+    // 查找匹配的菜单键
+    foreach ($menuMappings as $path => $key) {
+        if (str_contains($currentPath, $path)) {
+            $menuKey = $key;
+            break;
+        }
+    }
+
+    if (!$menuKey) {
+        return;
+    }
+
+    // 检查该菜单是否纳入管理系统
+    $menuConfig = MenuConfig::where('menu_key', $menuKey)->first();
+    if (!$menuConfig || !$menuConfig->is_managed || !$menuConfig->is_active) {
+        return;
+    }
+
+    // 获取用户ID
+    $userId = $user->teacher_id ?? $user->id;
+
+    // 获取当前菜单可见性
+    $isVisible = MenuPermission::isMenuVisible($userId, $menuKey);
+
+    // 获取菜单显示名称
+    $menuLabel = $menuConfig->menu_label ?? $menuKey;
+@endphp
+
+<div x-data="menuToggle()" class="flex items-center gap-3">
+    <!-- 菜单状态指示 -->
+    <div class="flex items-center gap-2 text-sm">
+        <span class="text-gray-600 dark:text-gray-400">菜单:</span>
+        <span x-text="isVisible ? '显示' : '隐藏'" class="font-medium"
+              :class="isVisible ? 'text-success-600' : 'text-danger-600'">
+        </span>
+    </div>
+
+    <!-- DaisyUI Toggle 开关 -->
+    <input
+        type="checkbox"
+        class="toggle toggle-primary toggle-sm"
+        :checked="{{ $isVisible ? 'true' : 'false' }}"
+        @change="toggleMenu('{{ $menuKey }}', '{{ $userId }}', $event.target.checked)"
+    />
+</div>
+
+<script>
+    function menuToggle() {
+        return {
+            isVisible: {{ $isVisible ? 'true' : 'false' }},
+
+            async toggleMenu(menuKey, userId, visible) {
+                try {
+                    const response = await fetch('{{ route('filament.admin.auth.toggle-menu-visibility') }}', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json',
+                            'X-CSRF-TOKEN': '{{ csrf_token() }}',
+                        },
+                        body: JSON.stringify({
+                            menu_key: menuKey,
+                            user_id: userId,
+                            is_visible: visible
+                        })
+                    });
+
+                    if (response.ok) {
+                        this.isVisible = visible;
+
+                        // 显示成功通知
+                        if (window.Filament && Filament.notify) {
+                            Filament.notify('success', '菜单已' + (visible ? '显示' : '隐藏'));
+                        }
+                    } else {
+                        throw new Error('操作失败');
+                    }
+                } catch (error) {
+                    console.error('Error:', error);
+
+                    // 显示错误通知
+                    if (window.Filament && Filament.notify) {
+                        Filament.notify('danger', '操作失败,请重试');
+                    }
+
+                    // 恢复原状态
+                    this.isVisible = !visible;
+                }
+            }
+        }
+    }
+</script>

+ 84 - 0
resources/views/filament/pages/exam-analysis-compact.blade.php

@@ -9,6 +9,90 @@
             <!-- 快速统计 -->
             <x-exam-analysis.quick-stats :recordData="$recordData" />
 
+            <!-- ChatGPT智能识别 -->
+            <div class="bg-white rounded-lg shadow-sm border border-gray-200">
+                <div class="p-4 border-b border-gray-200">
+                    <div class="flex items-center justify-between">
+                        <h2 class="text-base font-semibold text-gray-900">🤖 ChatGPT智能识别</h2>
+                        <div class="flex items-center gap-2">
+                            <input type="checkbox" wire:model="useChatGPT" id="useChatGPT-compact" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
+                            <label for="useChatGPT-compact" class="text-sm text-gray-700">启用</label>
+                        </div>
+                    </div>
+                </div>
+
+                @if($useChatGPT)
+                    <div class="p-4">
+                        <div class="space-y-3">
+                            <div>
+                                <label class="block text-sm font-medium text-gray-700 mb-1">
+                                    试卷图片URL <span class="text-red-500">*</span>
+                                </label>
+                                <input
+                                    type="url"
+                                    wire:model="imageUrl"
+                                    placeholder="请输入试卷图片URL"
+                                    class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+                                />
+                            </div>
+
+                            @if(!empty($imageUrl))
+                                <div class="flex items-center gap-2">
+                                    <button
+                                        wire:click="analyzeWithChatGPT"
+                                        wire:loading.attr="disabled"
+                                        class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
+                                    >
+                                        @if($isAnalyzing)
+                                            <svg class="animate-spin 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>
+                                            分析中...
+                                        @else
+                                            <svg class="w-4 h-4" 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>
+                                            智能识别
+                                        @endif
+                                    </button>
+
+                                    <button
+                                        wire:click="resetChatGPTForm"
+                                        class="px-3 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400"
+                                    >
+                                        重置
+                                    </button>
+                                </div>
+
+                                <!-- 图片预览 -->
+                                <div class="mt-2">
+                                    <div class="border border-gray-300 rounded-md overflow-hidden max-w-sm">
+                                        <img src="{{ $imageUrl }}" alt="试卷图片" class="w-full h-auto" onerror="this.style.display='none'">
+                                    </div>
+                                </div>
+                            @endif
+
+                            <!-- ChatGPT分析结果 -->
+                            @if(!empty($chatGPTResult))
+                                <div class="mt-3 p-3 bg-green-50 border border-green-200 rounded-md">
+                                    <h3 class="text-sm font-semibold text-green-900 mb-1">✅ 分析完成</h3>
+                                    <div class="text-xs text-green-800">
+                                        <p>已分析 {{ count($chatGPTResult['questions'] ?? []) }} 道题目</p>
+                                        <p>得分:{{ $chatGPTResult['score_obtained'] ?? 0 }}/{{ $chatGPTResult['total_score'] ?? 0 }}</p>
+                                        <p>掌握度:{{ number_format(($chatGPTResult['summary']['overall_mastery'] ?? 0) * 100, 1) }}%</p>
+                                    </div>
+                                </div>
+                            @endif
+                        </div>
+                    </div>
+                @else
+                    <div class="p-4 text-center text-gray-500">
+                        <p class="text-sm">启用ChatGPT直接从图片识别答案</p>
+                    </div>
+                @endif
+            </div>
+
             <!-- 学习分析概览 -->
             <x-exam-analysis.learning-analysis :analysisData="$analysisData" />
 

+ 130 - 0
resources/views/filament/pages/exam-analysis-standard.blade.php

@@ -29,6 +29,136 @@
                 </div>
             </div>
 
+            <!-- ChatGPT智能识别 -->
+            <div class="bg-white rounded-lg shadow-sm border border-gray-200">
+                <div class="p-6 border-b border-gray-200">
+                    <div class="flex items-center justify-between">
+                        <h2 class="text-lg font-semibold text-gray-900">🤖 ChatGPT智能识别</h2>
+                        <div class="flex items-center gap-2">
+                            <input type="checkbox" wire:model="useChatGPT" id="useChatGPT" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
+                            <label for="useChatGPT" class="text-sm text-gray-700">启用ChatGPT识别</label>
+                        </div>
+                    </div>
+                </div>
+
+                @if($useChatGPT)
+                    <div class="p-6">
+                        <div class="space-y-4">
+                            <div>
+                                <label class="block text-sm font-medium text-gray-700 mb-2">
+                                    试卷图片URL <span class="text-red-500">*</span>
+                                </label>
+                                <input
+                                    type="url"
+                                    wire:model="imageUrl"
+                                    placeholder="请输入试卷图片的完整URL地址"
+                                    class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+                                />
+                                <p class="text-xs text-gray-500 mt-1">
+                                    支持JPG、PNG格式图片,建议图片清晰、光线充足
+                                </p>
+                            </div>
+
+                            @if(!empty($imageUrl))
+                                <div class="flex items-center gap-4">
+                                    <button
+                                        wire:click="analyzeWithChatGPT"
+                                        wire:loading.attr="disabled"
+                                        class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
+                                    >
+                                        @if($isAnalyzing)
+                                            <svg class="animate-spin 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>
+                                            分析中...
+                                        @else
+                                            <svg class="w-5 h-5" 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>
+                                            开始智能识别
+                                        @endif
+                                    </button>
+
+                                    <button
+                                        wire:click="resetChatGPTForm"
+                                        class="px-4 py-2 bg-gray-200 text-gray-700 font-medium rounded-lg hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400"
+                                    >
+                                        重置
+                                    </button>
+                                </div>
+
+                                <!-- 图片预览 -->
+                                <div class="mt-4">
+                                    <p class="text-sm font-medium text-gray-700 mb-2">图片预览</p>
+                                    <div class="border border-gray-300 rounded-lg overflow-hidden max-w-2xl">
+                                        <img src="{{ $imageUrl }}" alt="试卷图片" class="w-full h-auto" onerror="this.style.display='none'">
+                                    </div>
+                                </div>
+                            @endif
+
+                            <!-- ChatGPT分析结果 -->
+                            @if(!empty($chatGPTResult))
+                                <div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
+                                    <h3 class="text-sm font-semibold text-green-900 mb-2">✅ ChatGPT分析完成</h3>
+                                    <div class="text-sm text-green-800">
+                                        <p>已成功分析 {{ count($chatGPTResult['questions'] ?? []) }} 道题目</p>
+                                        <p>总体得分:{{ $chatGPTResult['score_obtained'] ?? 0 }} / {{ $chatGPTResult['total_score'] ?? 0 }}</p>
+                                        <p>整体掌握度:{{ number_format(($chatGPTResult['summary']['overall_mastery'] ?? 0) * 100, 1) }}%</p>
+                                    </div>
+                                </div>
+
+                                <!-- ChatGPT分析详情 -->
+                                @if(isset($chatGPTResult['questions']))
+                                    <div class="mt-4">
+                                        <h4 class="text-sm font-semibold text-gray-700 mb-3">题目分析详情</h4>
+                                        <div class="space-y-3 max-h-96 overflow-y-auto">
+                                            @foreach($chatGPTResult['questions'] as $q)
+                                                <div class="border rounded-lg p-3 {{ $q['is_correct'] ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50' }}">
+                                                    <div class="flex items-start justify-between mb-2">
+                                                        <span class="text-sm font-medium text-gray-900">第{{ $q['q'] }}题</span>
+                                                        <span class="text-xs px-2 py-1 rounded-full {{ $q['is_correct'] ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
+                                                            {{ $q['is_correct'] ? '正确' : '错误' }}
+                                                        </span>
+                                                    </div>
+                                                    <div class="text-xs text-gray-600 space-y-1">
+                                                        <p><span class="font-medium">学生答案:</span>{{ $q['student_answer'] ?? '未识别到答案' }}</p>
+                                                        <p><span class="font-medium">正确答案:</span>{{ $q['correct_answer'] ?? 'N/A' }}</p>
+                                                        @if(isset($q['knowledge_points'][0]))
+                                                            <p><span class="font-medium">知识点:</span>{{ $q['knowledge_points'][0]['name'] ?? $q['knowledge_points'][0]['id'] }}</p>
+                                                            <p><span class="font-medium">掌握度:</span>{{ number_format($q['knowledge_points'][0]['mastery'] ?? 0, 1) }}%</p>
+                                                        @endif
+                                                    </div>
+                                                </div>
+                                            @endforeach
+                                        </div>
+                                    </div>
+                                @endif
+
+                                <!-- 学习建议 -->
+                                @if(isset($chatGPTResult['summary']['suggested_study_path']))
+                                    <div class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
+                                        <h4 class="text-sm font-semibold text-blue-900 mb-2">📚 个性化学习建议</h4>
+                                        <div class="text-sm text-blue-800 space-y-1">
+                                            @foreach($chatGPTResult['summary']['suggested_study_path'] as $path)
+                                                <p>• {{ $path }}</p>
+                                            @endforeach
+                                        </div>
+                                    </div>
+                                @endif
+                            @endif
+                        </div>
+                    </div>
+                @else
+                    <div class="p-6 text-center text-gray-500">
+                        <svg class="w-12 h-12 mx-auto text-gray-300 mb-3" 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>
+                        <p class="text-sm">启用ChatGPT识别功能,直接从图片中识别学生答案并智能分析</p>
+                    </div>
+                @endif
+            </div>
+
             <!-- 学习统计概览 -->
             @if(!empty($analysisData))
             <div class="bg-white rounded-lg shadow-sm border border-gray-200">

+ 145 - 5
resources/views/filament/pages/exam-detail.blade.php

@@ -76,13 +76,14 @@
                             </button>
 
                             <button
-                                wire:click="exportPdf"
+                                wire:click="previewPaper"
                                 class="btn btn-outline btn-sm"
-                                title="注意:PDF导出功能依赖外部题库API,可能不稳定">
+                                title="预览试卷卷子,可以直接打印">
                                 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
                                 </svg>
-                                导出PDF
+                                预览卷子
                             </button>
 
                             <button
@@ -145,7 +146,10 @@
                                     <div class="bg-base-200 p-4 rounded-lg mb-3">
                                         <div class="text-sm text-gray-500 mb-2">题干:</div>
                                         <div class="prose prose-sm max-w-none">
-                                            {!! nl2br(e($question['stem'])) !!}
+                                            @php
+                                                $stemHtml = nl2br(\App\Services\MathFormulaProcessor::processFormulas($question['stem'] ?? ''));
+                                            @endphp
+                                            {!! $stemHtml !!}
                                         </div>
                                     </div>
 
@@ -203,6 +207,97 @@
                     @endforelse
                 </div>
             </div>
+
+            <!-- 试卷预览区域 -->
+            @if($showPreview && !empty($paperDetail))
+                @php
+                    $paperPreviewUrl = route('filament.admin.auth.intelligent-exam.pdf', [
+                        'paper_id' => $paperDetail['paper_id'] ?? ($paperId ?? ''),
+                        'answer' => 'false',
+                    ]);
+                    $gradingPreviewUrl = route('filament.admin.auth.intelligent-exam.grading', [
+                        'paper_id' => $paperDetail['paper_id'] ?? ($paperId ?? ''),
+                    ]);
+                @endphp
+                <div class="bg-white p-6 rounded-lg border shadow-sm">
+                    <div class="flex items-center justify-between mb-4">
+                        <div class="flex items-center gap-3">
+                            <div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
+                                <svg class="w-6 h-6 text-emerald-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>
+                            </div>
+                            <div>
+                                <h3 class="text-lg font-semibold text-gray-900">试卷预览 - {{ $paperDetail['paper_name'] }}</h3>
+                                <p class="text-sm text-gray-500">预览区默认展示试卷正文,底部自动附加判卷页,打印会连续输出两份</p>
+                            </div>
+                        </div>
+                        <div class="flex items-center gap-3">
+                            <a
+                                href="{{ $paperPreviewUrl }}"
+                                target="_blank"
+                                class="btn btn-ghost btn-sm"
+                                title="新窗口打开试卷预览">
+                                试卷新窗口预览
+                            </a>
+                            <a
+                                href="{{ $gradingPreviewUrl }}"
+                                target="_blank"
+                                class="btn btn-ghost btn-sm"
+                                title="新窗口打开判卷预览">
+                                判卷新窗口预览
+                            </a>
+                            <button
+                                wire:click="printPaper"
+                                class="btn btn-primary btn-sm">
+                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
+                                </svg>
+                                打印试卷+判卷
+                            </button>
+                            <button
+                                wire:click="$set('showPreview', false)"
+                                class="btn btn-outline btn-sm">
+                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                                </svg>
+                                关闭预览
+                            </button>
+                        </div>
+                    </div>
+
+                    @if(!empty($paperPreviewUrl))
+                        <div class="bg-gray-100 border rounded-lg overflow-hidden mb-4">
+                            <iframe
+                                id="examPreviewFrame"
+                                src="{{ $paperPreviewUrl }}"
+                                class="w-full"
+                                style="min-height: 900px; background: white;"
+                                title="试卷预览(智能出卷)">
+                            </iframe>
+                        </div>
+                        <div class="bg-amber-50 border rounded-lg overflow-hidden">
+                            <div class="px-4 py-2 text-sm text-amber-700 bg-amber-100 border-b">
+                                判卷页面(将随试卷一起打印,含答题方框与答案解析)
+                            </div>
+                            <iframe
+                                id="examGradingFrame"
+                                src="{{ $gradingPreviewUrl }}"
+                                class="w-full"
+                                style="min-height: 800px; background: white;"
+                                title="判卷预览">
+                            </iframe>
+                        </div>
+                    @else
+                        <div class="alert alert-error">
+                            <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 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                            </svg>
+                            <span>未找到试卷 ID,无法加载智能出卷预览。</span>
+                        </div>
+                    @endif
+                </div>
+            @endif
         @endif
     </div>
 
@@ -296,4 +391,49 @@
         </div>
     </div>
     @endif
+
+    <!-- 打印/预览功能脚本(统一使用智能出卷的 PDF 页面) -->
+    <script>
+        document.addEventListener('livewire:init', () => {
+            Livewire.on('print-paper', (payload) => {
+                const url = typeof payload === 'string' ? payload : payload?.url;
+                if (!url) return;
+
+                const printWindow = window.open(url, '_blank');
+                if (!printWindow) return;
+
+                const handleLoad = () => {
+                    try {
+                        printWindow.focus();
+                        printWindow.print();
+                    } catch (e) {
+                        console.warn('打印失败,请检查浏览器弹窗权限', e);
+                    }
+                };
+
+                if (printWindow.document?.readyState === 'complete') {
+                    handleLoad();
+                } else {
+                    printWindow.addEventListener('load', handleLoad, { once: true });
+                }
+            });
+
+            Livewire.on('refresh-preview', (payload) => {
+                const previewUrl = typeof payload === 'string'
+                    ? payload
+                    : payload?.previewUrl || payload?.url;
+                const gradingUrl = typeof payload === 'object' ? payload?.gradingUrl : null;
+
+                const frame = document.getElementById('examPreviewFrame');
+                if (frame && previewUrl) {
+                    frame.src = previewUrl;
+                }
+
+                const gradingFrame = document.getElementById('examGradingFrame');
+                if (gradingFrame && gradingUrl) {
+                    gradingFrame.src = gradingUrl;
+                }
+            });
+        });
+    </script>
 </x-filament-panels::page>

+ 22 - 30
resources/views/filament/pages/integrations/knowledge-graph-explorer.blade.php

@@ -1,36 +1,28 @@
 <x-filament-panels::page>
-    <div class="space-y-6">
-        {{-- 页面标题 --}}
-        <div class="rounded-xl border border-slate-200 bg-white shadow-sm p-5">
-            <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
-                <div class="space-y-2 text-slate-900">
-                    <h2 class="text-3xl font-bold">知识图谱浏览</h2>
-                    <p class="text-lg text-slate-600">点击任意节点查看详细信息,包括子知识点和技能点</p>
-                </div>
-                <div class="flex items-center gap-2">
-                    <a
-                        href="{{ url('admin/knowledge-graph-integration') }}"
-                        class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
-                    >
-                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
-                        </svg>
-                        综合视图
-                    </a>
-                    <a
-                        href="{{ url('admin/knowledge-mindmap') }}"
-                        class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
-                    >
-                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
-                        </svg>
-                        原始脑图
-                    </a>
-                </div>
-            </div>
+    <div class="space-y-4">
+        {{-- 操作切换条(保留单一标题区) --}}
+        <div class="flex items-center gap-2">
+            <a
+                href="{{ url('admin/knowledge-graph-integration') }}"
+                class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+            >
+                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
+                </svg>
+                综合视图
+            </a>
+            <a
+                href="{{ url('admin/knowledge-mindmap') }}"
+                class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+            >
+                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
+                </svg>
+                原始脑图
+            </a>
         </div>
 
-        {{-- 完全复用原版知识图谱代码 --}}
+        {{-- 知识图谱可视化区域 --}}
         <div
             class="rounded-2xl border border-slate-200 shadow-sm bg-white text-slate-900 knowledge-mindmap-card"
             x-data="{

+ 70 - 16
resources/views/filament/pages/intelligent-exam-generation-simple.blade.php

@@ -114,6 +114,21 @@
                     console.log('Window教师变更事件:', data);
                 });
             });
+
+            window.printBoth = function() {
+                const examFrame = document.getElementById('pdfFrame');
+                const gradingFrame = document.getElementById('gradingFrame');
+                if (examFrame?.contentWindow) {
+                    examFrame.contentWindow.focus();
+                    examFrame.contentWindow.print();
+                }
+                setTimeout(() => {
+                    if (gradingFrame?.contentWindow) {
+                        gradingFrame.contentWindow.focus();
+                        gradingFrame.contentWindow.print();
+                    }
+                }, 600);
+            };
         </script>
     @endpush
 
@@ -151,7 +166,17 @@
                 </div>
             </div>
             <div class="space-y-4">
-                <div class="grid grid-cols-3 gap-4">
+                <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
+                    <div class="selection-card border rounded-lg p-4">
+                        <label class="block text-sm font-medium text-gray-700 mb-2">年级</label>
+                        <select wire:model="selectedGrade" class="form-select w-full px-3 py-2 rounded-lg text-sm">
+                            <option value="初中">初中整体</option>
+                            <option value="七年级">七年级</option>
+                            <option value="八年级">八年级</option>
+                            <option value="九年级">九年级</option>
+                        </select>
+                    </div>
+
                     <div class="selection-card border rounded-lg p-4">
                         <label class="block text-sm font-medium text-gray-700 mb-2">难度分类</label>
                         <select wire:model="difficultyCategory" class="form-select w-full px-3 py-2 rounded-lg text-sm">
@@ -195,20 +220,6 @@
                     />
                 </div>
 
-                <div class="selection-card border rounded-lg p-4">
-                    <label class="flex items-center gap-3 cursor-pointer">
-                        <input
-                            type="checkbox"
-                            wire:model="includeAnswer"
-                            class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
-                            checked
-                        />
-                        <div>
-                            <span class="block text-sm font-medium text-gray-700">生成参考答案</span>
-                            <span class="text-xs text-gray-500">开启后将在PDF中单独生成参考答案页,方便打印后核对</span>
-                        </div>
-                    </label>
-                </div>
             </div>
         </div>
 
@@ -737,13 +748,56 @@
                     <div class="p-6 bg-gray-100">
                         <iframe
                             id="pdfFrame"
-                            src="{{ route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $generatedPaperId, 'answer' => $includeAnswer ? 'true' : 'false']) }}"
+                            src="{{ route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $generatedPaperId, 'answer' => 'false']) }}"
                             class="w-full border-0 rounded-lg shadow-lg"
                             style="height: 1200px; background: white;"
                             title="试卷预览">
                         </iframe>
                     </div>
                 </div>
+
+                <!-- 判卷预览区域 -->
+                <div id="gradingPreview" class="mt-8 bg-white rounded-xl border-2 border-gray-200 shadow-lg overflow-hidden">
+                    <div class="p-5 border-b bg-gradient-to-r from-amber-50 to-amber-100 flex justify-between items-center">
+                        <div class="flex items-center gap-3">
+                            <div class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center">
+                                <svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
+                                </svg>
+                            </div>
+                            <h3 class="text-lg font-bold text-gray-900">判卷预览</h3>
+                        </div>
+                        <div class="flex gap-3">
+                            <button
+                                onclick="document.getElementById('gradingFrame').contentWindow.print()"
+                                class="px-4 py-2 bg-amber-600 text-white font-semibold rounded-lg hover:bg-amber-700 transition-all shadow-md flex items-center gap-2"
+                            >
+                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
+                                </svg>
+                                打印判卷
+                            </button>
+                            <button
+                                onclick="printBoth()"
+                                class="px-4 py-2 border-2 border-amber-600 text-amber-700 font-semibold rounded-lg hover:bg-amber-50 transition-all shadow-sm flex items-center gap-2"
+                            >
+                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 17l4 4 4-4m0-10l-4-4-4 4" />
+                                </svg>
+                                连续打印试卷+判卷
+                            </button>
+                        </div>
+                    </div>
+                    <div class="p-6 bg-gray-100">
+                        <iframe
+                            id="gradingFrame"
+                            src="{{ route('filament.admin.auth.intelligent-exam.grading', ['paper_id' => $generatedPaperId]) }}"
+                            class="w-full border-0 rounded-lg shadow-lg"
+                            style="height: 1200px; background: white;"
+                            title="判卷预览">
+                        </iframe>
+                    </div>
+                </div>
             @endif
         </div>
     </div>

+ 47 - 7
resources/views/filament/pages/ocr-paper-grading.blade.php

@@ -12,12 +12,24 @@
         <!-- 上传区域 -->
         <div class="bg-white p-6 rounded-lg shadow">
             <div class="flex items-center gap-3 mb-6">
-                <div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
-                    <x-heroicon-o-document-plus class="w-6 h-6 text-blue-600" />
+                <div class="w-12 h-12 {{ $useChatGPT ? 'bg-purple-100' : 'bg-blue-100' }} rounded-xl flex items-center justify-center">
+                    @if($useChatGPT)
+                        <x-heroicon-o-sparkles class="w-6 h-6 text-purple-600" />
+                    @else
+                        <x-heroicon-o-document-plus class="w-6 h-6 text-blue-600" />
+                    @endif
                 </div>
                 <div>
-                    <h2 class="text-xl font-bold text-gray-900">上传试卷进行OCR识别</h2>
-                    <p class="text-sm text-gray-500">支持JPG、PNG格式图片</p>
+                    <h2 class="text-xl font-bold text-gray-900">
+                        上传试卷进行{{ $useChatGPT ? 'ChatGPT智能识别' : 'OCR识别' }}
+                    </h2>
+                    <p class="text-sm text-gray-500">
+                        @if($useChatGPT)
+                            支持JPG、PNG格式图片,ChatGPT将直接识别学生答案
+                        @else
+                            支持JPG、PNG格式图片
+                        @endif
+                    </p>
                 </div>
             </div>
 
@@ -104,13 +116,41 @@
                     </div>
                 @endif
 
-                {{-- 图片上传组件 --}}
+                {{-- 识别模式选择 --}}
+                @if(!empty($selectedPaperId))
+                    <div class="p-4 bg-gray-50 border border-gray-200 rounded-lg">
+                        <h3 class="text-sm font-medium text-gray-900 mb-3">选择识别方式</h3>
+                        <div class="flex gap-4">
+                            <label class="flex items-center gap-2 cursor-pointer">
+                                <input
+                                    type="radio"
+                                    wire:model.live="useChatGPT"
+                                    value="0"
+                                    class="w-4 h-4 text-blue-600"
+                                />
+                                <span class="text-sm text-gray-700">OCR识别 + AI判分(传统方式)</span>
+                            </label>
+                            <label class="flex items-center gap-2 cursor-pointer">
+                                <input
+                                    type="radio"
+                                    wire:model.live="useChatGPT"
+                                    value="1"
+                                    class="w-4 h-4 text-blue-600"
+                                />
+                                <span class="text-sm text-gray-700">ChatGPT智能识别(无需OCR)</span>
+                            </label>
+                        </div>
+                    </div>
+                @endif
+
+                {{-- 上传组件 - 复用同一个UploadForm --}}
                 @if(!empty($selectedPaperId))
                     @livewire(\App\Livewire\UploadExam\UploadForm::class, [
                         'teacherId' => $teacherId,
                         'studentId' => $studentId,
-                        'selectedPaperId' => $selectedPaperId
-                    ], key('upload-form-' . $selectedPaperId))
+                        'selectedPaperId' => $selectedPaperId,
+                        'mode' => $useChatGPT ? 'chatgpt' : 'ocr'
+                    ], key('upload-form-' . $selectedPaperId . '-' . $useChatGPT))
                 @endif
             </div>
         </div>

+ 35 - 29
resources/views/filament/pages/question-detail.blade.php

@@ -1,24 +1,5 @@
 <div class="min-h-screen bg-gray-50">
         <div class="container mx-auto p-6">
-            {{-- 面包屑导航 --}}
-            <nav class="flex text-sm" aria-label="Breadcrumb">
-                <ol class="flex items-center space-x-4">
-                    @foreach ($this->getBreadcrumbs() as $key => $breadcrumb)
-                        <li class="flex items-center">
-                            @if ($key !== array_key_last($this->getBreadcrumbs()))
-                                <a href="{{ $breadcrumb['url'] }}" class="text-gray-500 hover:text-gray-700">
-                                    {{ $breadcrumb['name'] }}
-                                </a>
-                                <svg class="flex-shrink-0 h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
-                                    <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10l-1.293 1.414a1 1 0 001.414 0z" clip-rule="evenodd" />
-                                </svg>
-                            @else
-                                <span class="text-gray-900">{{ $breadcrumb['name'] }}</span>
-                            @endif
-                        </li>
-                    @endforeach
-                </ol>
-            </nav>
 
             @if (empty($this->questionData))
                 <div class="bg-white rounded-lg shadow p-12 text-center">
@@ -79,11 +60,14 @@
                             <div class="prose prose-sm max-w-none mb-6">
                                 @php
                                     $displayStem = $this->questionData['display_stem'] ?? $this->questionData['stem'] ?? '';
+                                    $stemHtml = \App\Services\MathFormulaProcessor::processFormulas($displayStem);
                                 @endphp
-                                @if (!empty($displayStem))
+                                @if (!empty($stemHtml))
                                     <div class="p-4 bg-gray-50 rounded-lg">
                                         <h4 class="text-sm font-medium text-gray-700 mb-2">题目</h4>
-                                        <div class="text-gray-900">{{ $displayStem }}</div>
+                                        <div class="text-gray-900 leading-relaxed space-y-3 question-stem prose-sm max-w-none">
+                                            {!! $stemHtml !!}
+                                        </div>
                                     </div>
                                 @endif
 
@@ -109,8 +93,11 @@
                                         @endforeach
                                     @endif
 
-                                    @if (!empty($this->questionData['skills']) && is_array($this->questionData['skills']))
-                                        @foreach($this->questionData['skills'] as $skill)
+                                    @php
+                                        $skillNames = $this->getSkillNames();
+                                    @endphp
+                                    @if (!empty($skillNames))
+                                        @foreach($skillNames as $skill)
                                             <span class="inline-flex items-center px-2 py-1 rounded text-xs bg-teal-50 text-teal-600">
                                                 {{ $skill }}
                                             </span>
@@ -165,7 +152,7 @@
                                         </div>
                                         <div class="p-4 bg-green-50 rounded-lg">
                                             <h4 class="text-sm font-medium text-green-700 mb-2">正确答案</h4>
-                                            <p class="text-gray-900">{{ $this->questionData['answer'] ?? '暂无' }}</p>
+                                            <p class="text-gray-900">{!! \App\Services\MathFormulaProcessor::processFormulas($this->questionData['answer'] ?? '暂无') !!}</p>
                                         </div>
                                     </div>
                                 @else
@@ -173,7 +160,7 @@
                                     @if (!empty($this->questionData['answer']))
                                         <div class="p-4 bg-green-50 rounded-lg mb-4">
                                             <h4 class="text-sm font-medium text-green-700 mb-2">正确答案</h4>
-                                            <p class="text-gray-900 text-lg font-mono">{{ $this->questionData['answer'] }}</p>
+                                            <p class="text-gray-900 text-lg font-mono leading-relaxed">{!! \App\Services\MathFormulaProcessor::processFormulas($this->questionData['answer']) !!}</p>
                                         </div>
                                     @endif
                                 @endif
@@ -182,13 +169,32 @@
                                 @if (!empty($this->questionData['solution']))
                                     <div class="p-4 bg-blue-50 rounded-lg">
                                         <h4 class="text-sm font-medium text-blue-700 mb-2">解题思路</h4>
-                                        <div class="prose prose-sm max-w-none text-gray-900">
-                                            {!! $this->questionData['solution'] !!}
+                                        @php
+                                            $solutionRaw = $this->questionData['solution'];
+                                            $solutionProcessed = \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
+                                            $solutionProcessed = preg_replace('/【(解题思路|详细解答|最终答案)】/u', '<br><strong>【$1】</strong><br>', $solutionProcessed);
+                                            $solutionProcessed = preg_replace('/(第\s*\d+\s*步:)/u', '<br>$1', $solutionProcessed);
+                                            $solutionProcessed = nl2br($solutionProcessed);
+                                        @endphp
+                                        <div class="prose prose-sm max-w-none text-gray-900 leading-relaxed space-y-3 solution-content">
+                                            {!! $solutionProcessed !!}
                                         </div>
                                     </div>
                                 @endif
-                            </div>
-                            </div>
+</div>
+</div>
+
+<style>
+    .question-stem svg,
+    .solution-content svg {
+        max-width: 100%;
+        height: auto;
+        display: block;
+    }
+    .solution-content {
+        white-space: pre-line;
+    }
+</style>
 
                             {{-- AI分析(如果是错题) --}}
                             @if ($this->sourceType === 'mistake' && !empty($this->questionData['mistake_info']['ai_analysis']))

+ 7 - 2
resources/views/filament/pages/question-management-simple.blade.php

@@ -257,9 +257,14 @@
                                         onclick="this.parentElement.querySelector('span').classList.toggle('line-clamp-3')">展开</button>
                             @endif
                         </td>
-                        <td class="px-6 py-4 whitespace-nowrap text-sm">
+                        <td class="px-6 py-4 whitespace-nowrap text-sm space-x-3">
                             <a href="{{ url('/admin/question-detail') }}?question_id={{ $question['id'] }}"
-                               class="text-indigo-600 hover:text-indigo-900 font-medium">查看详情</a>
+                               class="text-indigo-600 hover:text-indigo-900 font-medium">查看</a>
+                            <button
+                                wire:click="deleteQuestion('{{ $question['question_code'] ?? '' }}')"
+                                wire:confirm="确定要删除这道题目吗?此操作不可恢复。"
+                                class="text-red-600 hover:text-red-800 font-medium"
+                            >删除</button>
                         </td>
                     </tr>
                 @empty

+ 5 - 2
resources/views/filament/pages/question-management.blade.php

@@ -204,11 +204,14 @@
                                 @endif
                             </div>
                         </td>
-                        <td>
+                        <td class="space-x-2">
+                            <a href="{{ url('/admin/question-detail') }}?question_id={{ urlencode($question['id'] ?? $question['question_code'] ?? '') }}" class="btn btn-ghost btn-xs text-primary">
+                                查看
+                            </a>
                             <button 
                                 wire:click="deleteQuestion('{{ $question['question_code'] }}')" 
                                 wire:confirm="确定要删除这道题目吗?此操作不可恢复。"
-                                class="btn btn-ghost btn-xs text-error"
+                                class="btn btn-outline btn-xs text-error border-error"
                             >
                                 删除
                             </button>

+ 39 - 25
resources/views/filament/pages/student-management.blade.php

@@ -1,30 +1,44 @@
 <x-filament-panels::page>
     <div class="space-y-6">
-        <!-- 快速操作按钮 -->
-        <div class="flex justify-end gap-2">
-            <a href="{{ route('filament.admin.resources.students.create') }}"
-               class="btn btn-primary">
-                <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
-                </svg>
-                添加新学生
-            </a>
-            @if(!$this->isTeacher)
-            <a href="{{ route('filament.admin.resources.teachers.create') }}"
-               class="btn btn-success">
-                <svg class="h-4 w-4" 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>
-                添加新老师
-            </a>
-            @endif
-            <a href="{{ route('filament.admin.pages.student-dashboard') }}"
-               class="btn btn-ghost">
-                <svg class="h-4 w-4" 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>
-                学生仪表板
-            </a>
+        <!-- 顶部操作条 -->
+        <div class="flex flex-wrap items-center justify-between gap-3">
+            <div class="flex items-center gap-2">
+                <a href="{{ route('filament.admin.resources.students.create') }}"
+                   class="btn btn-primary">
+                    <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
+                    </svg>
+                    添加新学生
+                </a>
+                @if(!$this->isTeacher)
+                <a href="{{ route('filament.admin.resources.teachers.create') }}"
+                   class="btn btn-success">
+                    <svg class="h-4 w-4" 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>
+                    添加新老师
+                </a>
+                @endif
+            </div>
+            <div class="flex items-center gap-2">
+                <a href="{{ route('filament.admin.pages.student-dashboard') }}"
+                   class="btn btn-ghost">
+                    <svg class="h-4 w-4" 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-2z"></path>
+                    </svg>
+                    学生仪表板
+                </a>
+                @if($this->selectedTeacherId)
+                    <span class="rounded-lg bg-primary-50 px-3 py-1 text-sm text-primary-700">
+                        当前筛选:{{ $this->selectedTeacherName ?? '指定老师' }}
+                    </span>
+                    <button
+                        wire:click="resetTeacherFilter"
+                        class="btn btn-xs btn-ghost text-slate-500">
+                        重置筛选
+                    </button>
+                @endif
+            </div>
         </div>
 
         <!-- 老师概览 -->

+ 70 - 0
resources/views/filament/resources/menu-permission-resource/header.blade.php

@@ -0,0 +1,70 @@
+@php
+use App\Models\Teacher;
+use Illuminate\View\View;
+
+// 获取所有老师列表
+$teachers = Teacher::all();
+@endphp
+
+<div class="flex items-center justify-between p-4 bg-white border-b">
+    <div class="flex items-center gap-3">
+        <div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
+            <x-heroicon-o-squares-2x2 class="w-6 h-6 text-blue-600" />
+        </div>
+        <div>
+            <h3 class="text-lg font-semibold text-gray-900">菜单权限管理</h3>
+            <p class="text-sm text-gray-500">
+                为老师 <span class="font-mono text-blue-600">{{ $teacher->teacher_id ?? 'N/A' }}</span> 管理菜单可见性
+            </p>
+        </div>
+    </div>
+
+    <div class="flex items-center gap-4">
+        <div class="flex items-center gap-2">
+            <label for="teacher-select" class="text-sm font-medium text-gray-700">
+                选择老师:
+            </label>
+            <select
+                id="teacher-select"
+                class="filament-input block w-64 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+                wire:change="$set('selectedTeacherId', $event.target.value)"
+            >
+                @foreach($teachers as $t)
+                    <option value="{{ $t->teacher_id }}" {{ $t->teacher_id === ($teacher?->teacher_id ?? '') ? 'selected' : '' }}>
+                        {{ $t->name ?? $t->teacher_id }}
+                    </option>
+                @endforeach
+            </select>
+        </div>
+
+        <div class="flex items-center gap-2 text-sm text-gray-500">
+            <x-heroicon-o-user class="w-4 h-4" />
+            <span>共 {{ $teachers->count() }} 位老师</span>
+        </div>
+    </div>
+</div>
+
+<style>
+    /* 自定义选择框样式 */
+    #teacher-select {
+        appearance: none;
+        background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
+        background-position: right 0.5rem center;
+        background-repeat: no-repeat;
+        background-size: 1.5em 1.5em;
+        padding-right: 2.5rem;
+    }
+
+    /* 悬停效果 */
+    #teacher-select:hover {
+        border-color: #3b82f6;
+    }
+
+    /* 聚焦效果 */
+    #teacher-select:focus {
+        border-color: #3b82f6;
+        box-shadow: 0 0 0 1px #3b82f6;
+        outline: 2px solid transparent;
+        outline-offset: 2px;
+    }
+</style>

+ 252 - 0
resources/views/filament/resources/teacher/pages/edit-teacher.blade.php

@@ -0,0 +1,252 @@
+<x-filament-panels::page>
+    @php
+        $teacher = $this->record->loadMissing('students');
+        $user = $teacher->user ?? \App\Models\User::find($teacher->user_id);
+        $subjectOptions = \App\Filament\Resources\TeacherResource::subjectOptions();
+        $subjectLabel = $subjectOptions[$teacher->subject] ?? $teacher->subject ?? '未设置';
+        $studentsCount = $teacher->students->count();
+    @endphp
+
+    <div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
+        {{-- 主表单区域 --}}
+        <div class="xl:col-span-2 space-y-6">
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
+                        <div>
+                            <p class="text-sm text-base-content/60">教师资料</p>
+                            <h2 class="card-title text-2xl">编辑教师信息</h2>
+                            <p class="text-base-content/60 text-sm mt-1">保持登录与基本资料一致,保存后立即生效</p>
+                        </div>
+                        <div class="flex flex-wrap gap-2">
+                            <a href="{{ \App\Filament\Resources\TeacherResource::getUrl('view', ['record' => $teacher]) }}"
+                               class="btn btn-ghost btn-sm gap-2">
+                                <x-heroicon-o-arrow-left class="w-4 h-4" />
+                                返回详情
+                            </a>
+                            <a href="/admin/students?tableFilters[teacher_id][value]={{ $teacher->teacher_id }}"
+                               class="btn btn-outline btn-primary btn-sm gap-2">
+                                <x-heroicon-o-users class="w-4 h-4" />
+                                查看学生
+                            </a>
+                        </div>
+                    </div>
+
+                    {{-- 快速信息 --}}
+                    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
+                        <div class="bg-base-200/60 rounded-xl p-4 flex items-center gap-3">
+                            <div class="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
+                                <x-heroicon-o-identification class="w-5 h-5 text-primary" />
+                            </div>
+                            <div>
+                                <p class="text-sm text-base-content/60">教师ID</p>
+                                <p class="font-semibold font-mono">{{ $teacher->teacher_id }}</p>
+                            </div>
+                        </div>
+                        <div class="bg-base-200/60 rounded-xl p-4 flex items-center gap-3">
+                            <div class="w-10 h-10 rounded-lg bg-secondary/20 flex items-center justify-center">
+                                <x-heroicon-o-academic-cap class="w-5 h-5 text-secondary" />
+                            </div>
+                            <div>
+                                <p class="text-sm text-base-content/60">教授科目</p>
+                                <p class="font-semibold">{{ $subjectLabel }}</p>
+                            </div>
+                        </div>
+                        <div class="bg-base-200/60 rounded-xl p-4 flex items-center gap-3">
+                            <div class="w-10 h-10 rounded-lg bg-info/20 flex items-center justify-center">
+                                <x-heroicon-o-chart-bar class="w-5 h-5 text-info" />
+                            </div>
+                            <div>
+                                <p class="text-sm text-base-content/60">管理学生</p>
+                                <p class="font-semibold">{{ $studentsCount }} 人</p>
+                            </div>
+                        </div>
+                    </div>
+
+                    {{-- 表单 --}}
+                    <form wire:submit="save" class="mt-6 space-y-6">
+                        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                            <div class="form-control">
+                                <label class="label">
+                                    <span class="label-text font-medium">教师ID</span>
+                                    <span class="label-text-alt text-base-content/60">系统生成</span>
+                                </label>
+                                <input type="text"
+                                       wire:model="data.teacher_id"
+                                       class="input input-bordered bg-base-200"
+                                       disabled />
+                            </div>
+
+                            <div class="form-control">
+                                <label class="label">
+                                    <span class="label-text font-medium">手机号(登录名)</span>
+                                    <span class="label-text-alt text-base-content/60">不可修改</span>
+                                </label>
+                                <input type="text"
+                                       wire:model="data.user.username"
+                                       class="input input-bordered bg-base-200"
+                                       disabled />
+                            </div>
+                        </div>
+
+                        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                            <div class="form-control">
+                                <label class="label">
+                                    <span class="label-text font-medium">姓名 <span class="text-error">*</span></span>
+                                </label>
+                                <input type="text"
+                                       wire:model="data.name"
+                                       placeholder="请输入教师姓名"
+                                       class="input input-bordered focus:input-primary" />
+                                @error('data.name')
+                                    <label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
+                                @enderror
+                            </div>
+
+                            <div class="form-control">
+                                <label class="label">
+                                    <span class="label-text font-medium">教授科目 <span class="text-error">*</span></span>
+                                </label>
+                                <select wire:model="data.subject"
+                                        class="select select-bordered focus:select-primary">
+                                    <option value="">请选择科目</option>
+                                    @foreach($subjectOptions as $value => $label)
+                                        <option value="{{ $value }}">{{ $label }}</option>
+                                    @endforeach
+                                </select>
+                                @error('data.subject')
+                                    <label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
+                                @enderror
+                            </div>
+                        </div>
+
+                        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                            <div class="form-control md:col-span-2">
+                                <label class="label">
+                                    <span class="label-text font-medium">登录密码</span>
+                                    <span class="label-text-alt text-base-content/60">留空则不修改</span>
+                                </label>
+                                <input type="password"
+                                       wire:model.defer="data.user.password_hash"
+                                       placeholder="输入新密码"
+                                       class="input input-bordered focus:input-primary" />
+                                @error('data.user.password_hash')
+                                    <label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
+                                @enderror
+                            </div>
+                        </div>
+
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text font-medium">备注</span>
+                                <span class="label-text-alt text-base-content/60">选填</span>
+                            </label>
+                            <textarea wire:model.defer="data.remark"
+                                      class="textarea textarea-bordered focus:textarea-primary"
+                                      placeholder="记录教学侧重点、班主任情况等"
+                                      rows="3"></textarea>
+                            @error('data.remark')
+                                <label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
+                            @enderror
+                        </div>
+
+                        <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pt-2 border-t border-base-200">
+                            <div class="text-sm text-base-content/70 flex items-center gap-2">
+                                <x-heroicon-o-shield-check class="w-4 h-4" />
+                                未填写密码时将保留原密码,保存后立即同步到登录账号。
+                            </div>
+                            <div class="flex flex-wrap gap-3">
+                                <button type="button" wire:click="$refresh" class="btn btn-ghost">
+                                    重置
+                                </button>
+                                <button type="submit" class="btn btn-primary gap-2">
+                                    <span wire:loading.remove wire:target="save" class="flex items-center gap-2">
+                                        <x-heroicon-o-check class="w-5 h-5" />
+                                        保存修改
+                                    </span>
+                                    <span wire:loading wire:target="save" class="flex items-center gap-2">
+                                        <span class="loading loading-spinner loading-sm"></span>
+                                        保存中...
+                                    </span>
+                                </button>
+                            </div>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+
+        {{-- 侧边栏 --}}
+        <div class="space-y-6">
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body items-center text-center">
+                    <div class="avatar placeholder mb-4">
+                        <div class="bg-gradient-to-br from-primary to-secondary text-primary-content rounded-full w-24">
+                            <span class="text-3xl">{{ mb_substr($teacher->name, 0, 1) }}</span>
+                        </div>
+                    </div>
+                    <h3 class="text-xl font-bold">{{ $teacher->name }}</h3>
+                    <p class="text-base-content/60">{{ $subjectLabel }}</p>
+                    <div class="badge badge-outline mt-2">{{ $teacher->teacher_id }}</div>
+                    <div class="mt-3 space-y-1 text-sm text-base-content/70">
+                        <p>账号:{{ $user?->username ?? '未设置' }}</p>
+                        <p>最后登录:{{ $user?->last_login?->format('Y-m-d H:i:s') ?? '从未登录' }}</p>
+                    </div>
+                </div>
+            </div>
+
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <div class="flex items-center gap-3 mb-3">
+                        <div class="w-10 h-10 rounded-lg bg-warning/20 flex items-center justify-center">
+                            <x-heroicon-o-light-bulb class="w-5 h-5 text-warning" />
+                        </div>
+                        <h3 class="card-title text-base">填写提示</h3>
+                    </div>
+                    <ul class="space-y-2 text-sm text-base-content/70">
+                        <li class="flex items-start gap-2">
+                            <x-heroicon-o-information-circle class="w-4 h-4 flex-shrink-0 mt-0.5" />
+                            <span>手机号和教师ID均不可修改,如需变更请新建账号。</span>
+                        </li>
+                        <li class="flex items-start gap-2">
+                            <x-heroicon-o-information-circle class="w-4 h-4 flex-shrink-0 mt-0.5" />
+                            <span>密码留空则保持原值,避免误覆盖。</span>
+                        </li>
+                        <li class="flex items-start gap-2">
+                            <x-heroicon-o-information-circle class="w-4 h-4 flex-shrink-0 mt-0.5" />
+                            <span>备注可记录授课重点或行政职务,方便统一管理。</span>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <div class="flex items-center gap-3 mb-4">
+                        <div class="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
+                            <x-heroicon-o-bolt class="w-5 h-5 text-primary" />
+                        </div>
+                        <h3 class="card-title text-base">快捷导航</h3>
+                    </div>
+                    <div class="flex flex-col gap-2">
+                        <a href="{{ \App\Filament\Resources\TeacherResource::getUrl('index') }}"
+                           class="btn btn-ghost btn-sm gap-2 justify-start">
+                            <x-heroicon-o-list-bullet class="w-4 h-4" />
+                            返回教师列表
+                        </a>
+                        <a href="{{ \App\Filament\Resources\TeacherResource::getUrl('view', ['record' => $teacher]) }}"
+                           class="btn btn-outline btn-sm gap-2 justify-start">
+                            <x-heroicon-o-eye class="w-4 h-4" />
+                            查看教师详情
+                        </a>
+                        <a href="/admin/students?tableFilters[teacher_id][value]={{ $teacher->teacher_id }}"
+                           class="btn btn-primary btn-sm gap-2 justify-start">
+                            <x-heroicon-o-users class="w-4 h-4" />
+                            跳转学生列表
+                        </a>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</x-filament-panels::page>

+ 225 - 0
resources/views/filament/resources/teacher/pages/view-teacher.blade.php

@@ -0,0 +1,225 @@
+<x-filament-panels::page>
+    @php
+        $teacher = $this->record;
+    @endphp
+
+    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
+        {{-- 左侧主信息 --}}
+        <div class="lg:col-span-2 space-y-6">
+            {{-- 基本信息卡片 --}}
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <div class="flex items-center justify-between mb-6">
+                        <h2 class="card-title text-xl">基本信息</h2>
+                        <a href="{{ \App\Filament\Resources\TeacherResource::getUrl('edit', ['record' => $teacher]) }}"
+                           class="btn btn-sm btn-outline btn-primary gap-2">
+                            <x-heroicon-o-pencil-square class="w-4 h-4" />
+                            编辑
+                        </a>
+                    </div>
+
+                    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                        {{-- 教师ID --}}
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text text-base-content/60">教师ID</span>
+                            </label>
+                            <div class="flex items-center gap-2">
+                                <span class="badge badge-primary badge-lg font-mono">{{ $teacher->teacher_id }}</span>
+                                <button onclick="navigator.clipboard.writeText('{{ $teacher->teacher_id }}')"
+                                        class="btn btn-ghost btn-xs tooltip" data-tip="复制">
+                                    <x-heroicon-o-clipboard-document class="w-4 h-4" />
+                                </button>
+                            </div>
+                        </div>
+
+                        {{-- 姓名 --}}
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text text-base-content/60">姓名</span>
+                            </label>
+                            <p class="text-lg font-semibold">{{ $teacher->name }}</p>
+                        </div>
+
+                        {{-- 教授科目 --}}
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text text-base-content/60">教授科目</span>
+                            </label>
+                            <span class="badge badge-info badge-lg">{{ $teacher->subject }}</span>
+                        </div>
+
+                        {{-- 学生数量 --}}
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text text-base-content/60">管理学生数</span>
+                            </label>
+                            <span class="badge badge-warning badge-lg">{{ $studentsCount }}</span>
+                        </div>
+                    </div>
+
+                    @if($teacher->remark)
+                    <div class="divider"></div>
+                    <div class="form-control">
+                        <label class="label">
+                            <span class="label-text text-base-content/60">备注</span>
+                        </label>
+                        <div class="bg-base-200 rounded-lg p-4">
+                            <p class="text-sm whitespace-pre-wrap">{{ $teacher->remark }}</p>
+                        </div>
+                    </div>
+                    @endif
+                </div>
+            </div>
+
+            {{-- 登录信息卡片 --}}
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <h2 class="card-title text-xl mb-4">
+                        <x-heroicon-o-key class="w-6 h-6" />
+                        登录信息
+                    </h2>
+
+                    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                        {{-- 手机号 --}}
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text text-base-content/60">手机号(登录名)</span>
+                            </label>
+                            <div class="flex items-center gap-2">
+                                @if($teacher->user && $teacher->user->username)
+                                    <span class="font-mono">●●●●●●{{ substr($teacher->user->username, -4) }}</span>
+                                    <button onclick="navigator.clipboard.writeText('{{ $teacher->user->username }}')"
+                                            class="btn btn-ghost btn-xs tooltip" data-tip="复制完整号码">
+                                        <x-heroicon-o-clipboard-document class="w-4 h-4" />
+                                    </button>
+                                @else
+                                    <span class="text-base-content/40">未设置</span>
+                                @endif
+                            </div>
+                            <label class="label">
+                                <span class="label-text-alt">点击复制完整号码</span>
+                            </label>
+                        </div>
+
+                        
+                        {{-- 创建时间 --}}
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text text-base-content/60">创建时间</span>
+                            </label>
+                            <span class="badge badge-success">{{ is_string($teacher->created_at) ? $teacher->created_at : $teacher->created_at?->format('Y-m-d H:i:s') }}</span>
+                        </div>
+
+                        {{-- 最后登录 --}}
+                        <div class="form-control">
+                            <label class="label">
+                                <span class="label-text text-base-content/60">最后登录</span>
+                            </label>
+                            <span class="text-sm">{{ $teacher->user?->last_login?->format('Y-m-d H:i:s') ?? '从未登录' }}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            {{-- 统计信息卡片 --}}
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <h2 class="card-title text-xl mb-4">
+                        <x-heroicon-o-chart-bar class="w-6 h-6" />
+                        教学统计
+                    </h2>
+
+                    <div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200 w-full">
+                        <div class="stat">
+                            <div class="stat-figure text-primary">
+                                <x-heroicon-o-users class="w-8 h-8" />
+                            </div>
+                            <div class="stat-title">学生数量</div>
+                            <div class="stat-value text-primary">{{ $studentsCount }}</div>
+                            <div class="stat-desc">位学生在管理中</div>
+                        </div>
+
+                        <div class="stat">
+                            <div class="stat-figure text-secondary">
+                                <x-heroicon-o-document-text class="w-8 h-8" />
+                            </div>
+                            <div class="stat-title">创建考试</div>
+                            <div class="stat-value text-secondary">{{ $examCount ?? 0 }}</div>
+                            <div class="stat-desc">场考试已创建</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        {{-- 右侧信息 --}}
+        <div class="lg:col-span-1 space-y-6">
+            {{-- 头像卡片 --}}
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body items-center text-center">
+                    <div class="avatar placeholder mb-4">
+                        <div class="bg-gradient-to-br from-primary to-secondary text-primary-content rounded-full w-24">
+                            <span class="text-3xl">{{ mb_substr($teacher->name, 0, 1) }}</span>
+                        </div>
+                    </div>
+                    <h3 class="text-xl font-bold">{{ $teacher->name }}</h3>
+                    <p class="text-base-content/60">{{ $teacher->subject }}</p>
+                    <div class="badge badge-outline mt-2">{{ $teacher->teacher_id }}</div>
+                </div>
+            </div>
+
+            {{-- 快捷操作 --}}
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <h3 class="card-title text-base mb-4">
+                        <x-heroicon-o-bolt class="w-5 h-5" />
+                        快捷操作
+                    </h3>
+                    <div class="flex flex-col gap-2">
+                        <a href="{{ \App\Filament\Resources\TeacherResource::getUrl('edit', ['record' => $teacher]) }}"
+                           class="btn btn-primary gap-2">
+                            <x-heroicon-o-pencil class="w-4 h-4" />
+                            编辑信息
+                        </a>
+
+                        <a href="/admin/students?tableFilters[teacher_id][value]={{ $teacher->teacher_id }}"
+                           class="btn btn-secondary gap-2">
+                            <x-heroicon-o-users class="w-4 h-4" />
+                            查看学生
+                        </a>
+
+                        <a href="{{ \App\Filament\Resources\TeacherResource::getUrl('index') }}"
+                           class="btn btn-ghost gap-2">
+                            <x-heroicon-o-arrow-left class="w-4 h-4" />
+                            返回列表
+                        </a>
+                    </div>
+                </div>
+            </div>
+
+            {{-- 教学功能 --}}
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <h3 class="card-title text-base mb-4">
+                        <x-heroicon-o-academic-cap class="w-5 h-5" />
+                        教学功能
+                    </h3>
+                    <div class="grid grid-cols-2 gap-2">
+                        <a href="{{ route('filament.admin.pages.intelligent-exam-generation') }}?teacher_id={{ $teacher->teacher_id }}"
+                           class="btn btn-outline gap-2">
+                            <x-heroicon-o-document-plus class="w-4 h-4" />
+                            智能出卷
+                        </a>
+
+                        <a href="{{ route('filament.admin.pages.question-management') }}?teacher_id={{ $teacher->teacher_id }}"
+                           class="btn btn-outline gap-2">
+                            <x-heroicon-o-folder class="w-4 h-4" />
+                            题库管理
+                        </a>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</x-filament-panels::page>

+ 7 - 2
resources/views/livewire/upload-exam/upload-form.blade.php

@@ -45,8 +45,13 @@
                     重置
                 </button>
                 <button type="submit" wire:loading.attr="disabled" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
-                    <span wire:loading.remove>开始 OCR 识别</span>
-                    <span wire:loading>上传中...</span>
+                    @if($mode === 'chatgpt')
+                        <span wire:loading.remove>开始 GPT 识别</span>
+                        <span wire:loading>分析中...</span>
+                    @else
+                        <span wire:loading.remove>开始 OCR 识别</span>
+                        <span wire:loading>上传中...</span>
+                    @endif
                 </button>
             </div>
         </form>

+ 135 - 0
resources/views/pdf/exam-grading.blade.php

@@ -0,0 +1,135 @@
+@php
+    // 复用题目数据并开启判卷模式(显示方框+答案+思路)
+    $grading = true;
+@endphp
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>{{ $paper->paper_name ?? '判卷预览' }}</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
+    <style>
+        @page { size: A4; margin: 2cm; }
+        :root {
+            --question-gap: 6px;
+        }
+        body {
+            font-family: "SimSun", "Songti SC", serif;
+            line-height: 1.65;
+            color: #000;
+            background: #fff;
+            font-size: 14px;
+        }
+        .page {
+            max-width: 720px;
+            margin: 0 auto;
+            padding: 0 12px;
+        }
+        .header { text-align: center; margin-bottom: 1.5rem; border-bottom: 2px solid #000; padding-bottom: 1rem; }
+        .section-title { font-size: 16px; font-weight: bold; margin-top: 16px; margin-bottom: 10px; }
+        .question { margin-bottom: 14px; page-break-inside: avoid; }
+        .question-grid {
+            display: grid;
+            grid-template-columns: auto 1fr;
+            column-gap: 4px;
+            row-gap: 6px;
+            align-items: flex-start;
+        }
+        .question-lead {
+            display: flex;
+            gap: 4px;
+            align-items: flex-start;
+            font-weight: 600;
+            font-size: 14px;
+            line-height: 1.6;
+            margin-top: 2px;
+        }
+        .question-lead.spacer { visibility: hidden; }
+        .question-number { white-space: nowrap; margin-right: 2px; }
+        .grading-boxes { gap: 4px; flex-wrap: wrap; align-items: center; }
+        .grading-boxes span { vertical-align: middle; }
+        .question-main { font-size: 14px; line-height: 1.65; font-family: inherit; display: block; }
+        .question-score { margin-right: 6px; font-weight: 600; }
+        .question-stem { display: inline-block; font-size: 14px; font-family: inherit; }
+        .options { display: grid; row-gap: 8px; margin-top: 8px; }
+        .options-grid-2 {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 8px 20px;
+        }
+        .option { display: flex; align-items: flex-start; font-size: 14px; line-height: 1.6; }
+        .option strong { margin-right: 4px; }
+        .option-compact { font-size: 14px; line-height: 1.6; }
+        .answer-area { position: relative; margin-top: 4px; }
+        .answer-area.boxy {
+            min-height: 150px;
+            border: 1.5px solid #444;
+            border-radius: 6px;
+            padding: 14px;
+        }
+        .answer-label {
+            position: absolute;
+            top: -10px;
+            left: 10px;
+            font-size: 10px;
+            background: #fff;
+            padding: 0 4px;
+            color: #555;
+            letter-spacing: 1px;
+        }
+        .answer-meta {
+            font-size: 12px;
+            color: #2f2f2f;
+            line-height: 1.75;
+            margin-top: 4px;
+        }
+        .answer-line + .answer-line { margin-top: 4px; }
+        .solution-step {
+            align-items: center;
+            gap: 6px;
+        }
+        .step-box { display: inline-block; }
+        .step-label { white-space: nowrap; }
+        .solution-heading { font-weight: 700; }
+        .solution-content { display: inline-block; line-height: 1.75; }
+        svg, .math-render svg { max-width: 100%; height: auto; display: block; }
+    </style>
+</head>
+<body>
+    <div class="page">
+    <div class="header">
+        <div style="font-size:22px;font-weight:bold;">判卷专用</div>
+        <div style="font-size:18px;">{{ $paper->paper_name ?? '未命名试卷' }}</div>
+        <div style="display:flex;justify-content:space-between;font-size:14px;margin-top:8px;">
+            <span>老师:{{ $teacher['name'] ?? '________' }}</span>
+            <span>年级:{{ $student['grade'] ?? '________' }}</span>
+            <span>姓名:{{ $student['name'] ?? '________' }}</span>
+            <span>得分:________</span>
+        </div>
+    </div>
+
+    @include('components.exam.paper-body', ['questions' => $questions, 'grading' => true])
+    </div>
+
+    <!-- KaTeX -->
+    <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
+    <script>
+        document.addEventListener('DOMContentLoaded', function() {
+            try {
+                renderMathInElement(document.body, {
+                    delimiters: [
+                        {left: '$$', right: '$$', display: true},
+                        {left: '$', right: '$', display: false},
+                        {left: '\\\\(', right: '\\\\)', display: false},
+                        {left: '\\\\[', right: '\\\\]', display: true}
+                    ],
+                    throwOnError: false,
+                    strict: false,
+                    trust: true
+                });
+            } catch(e) {}
+        });
+    </script>
+</body>
+</html>

+ 112 - 275
resources/views/pdf/exam-paper.blade.php

@@ -9,11 +9,20 @@
             size: A4;
             margin: 2cm;
         }
+        :root {
+            --question-gap: 6px;
+        }
         body {
             font-family: "SimSun", "Songti SC", serif; /* 宋体,适合试卷 */
-            line-height: 1.6;
+            line-height: 1.65;
             color: #000;
             background: #fff;
+            font-size: 14px;
+        }
+        .page {
+            max-width: 720px;
+            margin: 0 auto;
+            padding: 0 12px;
         }
         .header {
             text-align: center;
@@ -52,68 +61,76 @@
             align-items: center;
             justify-content: center;
         }
-        .section-title {
-            font-size: 16px;
-            font-weight: bold;
-            margin-top: 20px;
-            margin-bottom: 10px;
-        }
-        .question {
-            margin-bottom: 15px;
-            page-break-inside: avoid;
+        .section-title { font-size: 16px; font-weight: bold; margin-top: 20px; margin-bottom: 10px; }
+        .question { margin-bottom: 15px; page-break-inside: avoid; }
+        .question-grid {
+            display: grid;
+            grid-template-columns: auto 1fr;
+            column-gap: 4px;
+            row-gap: 6px;
+            align-items: flex-start;
         }
-        .question-content {
-            font-size: 14px;
-            margin-bottom: 8px;
+        .question-lead {
             display: flex;
-            align-items: baseline;
-        }
-        .omr-marker {
-            display: inline-block;
-            width: 20px;
-            height: 20px;
-            border: 1px solid #000;
-            border-radius: 50%;
-            margin-right: 10px;
-            position: relative;
-            top: 4px;
+            gap: 4px;
+            align-items: flex-start;
+            font-weight: 600;
+            font-size: 14px;
+            line-height: 1.6;
+            margin-top: 2px;
         }
-        .options {
+        .question-lead.spacer { visibility: hidden; }
+        .question-number { white-space: nowrap; margin-right: 2px; }
+        .grading-boxes { gap: 4px; flex-wrap: wrap; align-items: center; }
+        .grading-boxes span { vertical-align: middle; }
+        .question-main { font-size: 14px; line-height: 1.65; font-family: inherit; display: block; }
+        .question-score { margin-right: 6px; font-weight: 600; }
+        .question-score-inline {
             display: block;
-            margin-left: 35px; /* 对齐题目内容 */
-            margin-top: 10px;
+            font-size: 13px;
+            color: #555;
+            margin: 0 0 4px 0;
+            white-space: nowrap;
         }
+        .question-stem { display: inline-block; font-size: 14px; font-family: inherit; }
+        .question-content { font-size: 14px; margin-bottom: 8px; line-height: 1.6; }
+        .options { display: grid; row-gap: 8px; margin-top: 8px; }
         .options-grid-4 {
             display: grid;
             grid-template-columns: repeat(4, 1fr);
             gap: 8px 12px;
-            margin-left: 35px;
-            margin-top: 10px;
         }
         .options-grid-2 {
             display: grid;
             grid-template-columns: 1fr 1fr;
             gap: 8px 20px;
-            margin-left: 35px;
-            margin-top: 10px;
         }
         .option {
             width: 100%;
             font-size: 14px;
-            line-height: 1.5;
+            line-height: 1.6;
             word-wrap: break-word;
             display: flex;
             align-items: flex-start;
         }
-        .option-inline {
-            display: inline-flex;
-            align-items: baseline;
-            margin-right: 20px;
+        .option strong { margin-right: 4px; }
+        .option-inline { display: inline-flex; align-items: baseline; margin-right: 20px; }
+        .option-compact { font-size: 14px; line-height: 1.6; }
+        .answer-meta {
+            font-size: 12px;
+            color: #2f2f2f;
+            line-height: 1.75;
+            margin-top: 4px;
         }
-        .option-compact {
-            font-size: 13px;
-            line-height: 1.4;
+        .answer-line + .answer-line { margin-top: 4px; }
+        .solution-step {
+            align-items: center;
+            gap: 6px;
         }
+        .step-box { display: inline-block; }
+        .step-label { white-space: nowrap; }
+        .solution-heading { font-weight: 700; }
+        .solution-content { display: inline-block; line-height: 1.75; }
         .fill-line {
             display: inline-block;
             border-bottom: 1px solid #000;
@@ -125,6 +142,56 @@
             border: 1px dashed #eee;
             margin-top: 10px;
         }
+        .answer-area {
+            position: relative;
+            margin-top: 12px;
+        }
+        .answer-area .answer-label {
+            position: absolute;
+            top: -10px;
+            left: 10px;
+            font-size: 10px;
+            background: #fff;
+            padding: 0 4px;
+            color: #555;
+            letter-spacing: 1px;
+        }
+        .answer-area.wavy {
+            height: 28px;
+            border-bottom: 1.5px dashed #555;
+            background-image: repeating-linear-gradient(
+                -45deg,
+                rgba(0, 0, 0, 0.35),
+                rgba(0, 0, 0, 0.35) 4px,
+                transparent 4px,
+                transparent 8px
+            );
+            background-size: 16px 16px;
+            background-repeat: repeat-x;
+            background-position: bottom;
+        }
+        .answer-area.boxy {
+            min-height: 150px;
+            border: 1.5px solid #444;
+            border-radius: 6px;
+            padding: 14px;
+        }
+        /* 让内嵌 SVG 按比例缩放展示 */
+        svg, .math-render svg {
+            max-width: 100%;
+            height: auto;
+            display: block;
+        }
+        .wavy-underline {
+            display: inline-block;
+            min-width: 80px;
+            height: 22px;
+            border-bottom: 1.2px dashed #444;
+            vertical-align: middle;
+        }
+        .wavy-underline.short {
+            min-width: 60px;
+        }
         @media print {
             .no-print {
                 display: none;
@@ -140,6 +207,7 @@
         装&nbsp;&nbsp;&nbsp;&nbsp;订&nbsp;&nbsp;&nbsp;&nbsp;线&nbsp;&nbsp;&nbsp;&nbsp;内&nbsp;&nbsp;&nbsp;&nbsp;不&nbsp;&nbsp;&nbsp;&nbsp;要&nbsp;&nbsp;&nbsp;&nbsp;答&nbsp;&nbsp;&nbsp;&nbsp;题
     </div>
 
+    <div class="page">
     <div class="header">
         <div class="school-name">数学智能测试卷</div>
         <div class="paper-title">{{ $paper->paper_name ?? '未命名试卷' }}</div>
@@ -151,239 +219,7 @@
         </div>
     </div>
 
-    <!-- 一、选择题 -->
-    <div class="section-title">一、选择题
-        @if(count($questions['choice']) > 0)
-            @php
-                $choiceTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $questions['choice']));
-            @endphp
-            (本大题共 {{ count($questions['choice']) }} 小题,共 {{ $choiceTotal }} 分)
-        @else
-            (本大题共 0 小题,共 0 分)
-        @endif
-    </div>
-    @if(count($questions['choice']) > 0)
-        @foreach($questions['choice'] as $index => $q)
-            @php
-                $questionNumber = $index + 1; // 选择题从1开始编号
-
-                // 清理和预处理题干内容:移除题号前缀
-                $cleanContent = preg_replace('/^\d+[\.\、]\s*/', '', $q->content);
-                $cleanContent = trim($cleanContent);
-
-                // 优先使用控制器传递的选项
-                $options = $q->options ?? [];
-
-                // 如果没有从控制器获取到选项,尝试从内容提取
-                if (empty($options)) {
-                    // 支持多种选项格式:A. / A、/ A:/ A.
-                    $pattern = '/([A-D])[\.、:.:]\s*(.+?)(?=\s*[A-D][\.、:.:]|$)/su';
-                    if (preg_match_all($pattern, $cleanContent, $matches, PREG_SET_ORDER)) {
-                        foreach ($matches as $match) {
-                            $optionText = trim($match[2]);
-                            if (!empty($optionText)) {
-                                $options[] = $optionText;
-                            }
-                        }
-                    }
-                }
-
-                // 提取纯题干(选项前的部分)
-                $stemLine = $cleanContent;
-                if (!empty($options)) {
-                    // 找到第一个选项标记的位置
-                    if (preg_match('/^(.+?)(?=[A-D][\.、:.:])/su', $cleanContent, $stemMatch)) {
-                        $stemLine = trim($stemMatch[1]);
-                    }
-                }
-
-                // 移除题干末尾可能的括号
-                $stemLine = preg_replace('/()\s*$/', '', $stemLine);
-                $stemLine = trim($stemLine);
-            @endphp
-            <div class="question">
-                <div class="question-content">
-                    <span class="omr-marker"></span>
-                    <span class="font-bold" style="margin-right: 8px;">{{ $questionNumber }}.</span>
-                    <span style="margin-left: 4px;">({{ $q->score ?? 5 }}分) @math($stemLine)</span>
-                </div>
-
-                @if(!empty($options))
-                    @php
-                        // 确保有4个选项(A、B、C、D)
-                        $standardOptions = ['A', 'B', 'C', 'D'];
-                        $displayOptions = [];
-                        foreach ($standardOptions as $idx => $letter) {
-                            if (isset($options[$idx]) && !empty($options[$idx])) {
-                                $displayOptions[$letter] = $options[$idx];
-                            } else {
-                                // 补充缺失的选项
-                                $displayOptions[$letter] = '(待补充选项' . $letter . ')';
-                            }
-                        }
-
-                        // 计算选项长度,决定布局
-                        $maxOptionLength = 0;
-                        $totalOptionLength = 0;
-                        foreach ($displayOptions as $letter => $option) {
-                            $text = strip_tags($option);
-                            $length = mb_strlen($text);
-                            $maxOptionLength = max($maxOptionLength, $length);
-                            $totalOptionLength += $length;
-                        }
-
-                        // 三种布局选择:
-                        // 1. 一行4列:极短选项(单个选项≤10字符)
-                        // 2. 一行2列:短选项(单个选项≤25字符)
-                        // 3. 垂直布局:长选项(单个选项>25字符)
-                        if ($maxOptionLength <= 10) {
-                            $layoutType = 'inline-4';
-                            $maxOptionLength = 12; // 给一点余量
-                        } elseif ($maxOptionLength <= 25) {
-                            $layoutType = 'grid-2';
-                            $maxOptionLength = 30; // 给一点余量
-                        } else {
-                            $layoutType = 'vertical';
-                        }
-                    @endphp
-
-                    @if($layoutType === 'inline-4')
-                        {{-- 极短选项:一行4列布局 --}}
-                        <div style="margin-left: 35px; margin-top: 10px;">
-                            <span style="font-size: 14px;">
-                                @foreach($displayOptions as $letter => $option)
-                                    <span class="option-inline option-compact">
-                                        <span style="font-weight: bold; margin-right: 4px;">{{ $letter }}.</span>
-                                        <span>@math($option)</span>
-                                    </span>
-                                @endforeach
-                            </span>
-                        </div>
-                    @elseif($layoutType === 'grid-2')
-                        {{-- 短选项:一行2列布局 --}}
-                        <div class="options-grid-2">
-                            @foreach($displayOptions as $letter => $option)
-                                <div class="option">
-                                    <span style="font-weight: bold; margin-right: 8px;">{{ $letter }}.</span>
-                                    <span>@math($option)</span>
-                                </div>
-                            @endforeach
-                        </div>
-                    @else
-                        {{-- 长选项:垂直布局 --}}
-                        <div class="options">
-                            @foreach($displayOptions as $letter => $option)
-                                <div class="option">
-                                    <span style="font-weight: bold; margin-right: 8px;">{{ $letter }}.</span>
-                                    <span>@math($option)</span>
-                                </div>
-                            @endforeach
-                        </div>
-                    @endif
-                @else
-                    {{-- 如果没有任何选项,显示占位符 --}}
-                    <div class="options-grid-2">
-                        <div class="option" style="font-style: italic; color: #999;">
-                            <span style="font-weight: bold; margin-right: 8px;">A.</span>
-                            <span>(待补充选项A)</span>
-                        </div>
-                        <div class="option" style="font-style: italic; color: #999;">
-                            <span style="font-weight: bold; margin-right: 8px;">B.</span>
-                            <span>(待补充选项B)</span>
-                        </div>
-                        <div class="option" style="font-style: italic; color: #999;">
-                            <span style="font-weight: bold; margin-right: 8px;">C.</span>
-                            <span>(待补充选项C)</span>
-                        </div>
-                        <div class="option" style="font-style: italic; color: #999;">
-                            <span style="font-weight: bold; margin-right: 8px;">D.</span>
-                            <span>(待补充选项D)</span>
-                        </div>
-                    </div>
-                @endif
-            </div>
-        @endforeach
-    @else
-        <div class="question">
-            <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
-                该题型正在生成中或暂无题目,请稍后刷新页面查看
-            </div>
-        </div>
-    @endif
-
-    <!-- 二、填空题 -->
-    <div class="section-title">二、填空题
-        @if(count($questions['fill']) > 0)
-            @php
-                $fillTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $questions['fill']));
-            @endphp
-            (本大题共 {{ count($questions['fill']) }} 小题,共 {{ $fillTotal }} 分)
-        @else
-            (本大题共 0 小题,共 0 分)
-        @endif
-    </div>
-    @if(count($questions['fill']) > 0)
-        @foreach($questions['fill'] as $index => $q)
-            @php
-                $questionNumber = count($questions['choice']) + $index + 1; // 填空题接着选择题编号
-            @endphp
-            <div class="question">
-                <div class="question-content">
-                    <span class="omr-marker"></span>
-                    <span class="font-bold" style="margin-right: 8px;">{{ $questionNumber }}.</span>
-                    <span style="margin-left: 4px;">({{ $q->score ?? 5 }}分) @math(str_replace('__________', '<span class="fill-line"></span>', $q->content))</span>
-                </div>
-            </div>
-        @endforeach
-    @else
-        <div class="question">
-            <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
-                该题型正在生成中或暂无题目,请稍后刷新页面查看
-            </div>
-        </div>
-    @endif
-
-    <!-- 三、解答题 -->
-    <div class="section-title">三、解答题
-        @if(count($questions['answer']) > 0)
-            (本大题共 {{ count($questions['answer']) }} 小题,共 {{ array_sum(array_column($questions['answer'], 'score')) }} 分。解答应写出文字说明、证明过程或演算步骤)
-        @else
-            (本大题共 0 小题,共 0 分)
-        @endif
-    </div>
-    @if(count($questions['answer']) > 0)
-        @foreach($questions['answer'] as $index => $q)
-            @php
-                $questionNumber = count($questions['choice']) + count($questions['fill']) + $index + 1; // 解答题接着前面所有题型编号
-            @endphp
-            <div class="question">
-                <div class="question-content">
-                    <span class="omr-marker"></span>
-                    <span class="font-bold" style="margin-right: 8px;">{{ $questionNumber }}.</span>
-                    <span style="margin-left: 4px;">({{ $q->score ?? 10 }}分) @math($q->content)</span>
-                </div>
-                <div class="answer-space"></div>
-            </div>
-        @endforeach
-    @else
-        <div class="question">
-            <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
-                该题型正在生成中或暂无题目,请稍后刷新页面查看
-            </div>
-        </div>
-    @endif
-
-    <!-- 试卷总分统计 -->
-    @php
-        $totalChoiceScore = array_sum(array_map(fn($q) => $q->score ?? 5, $questions['choice']));
-        $totalFillScore = array_sum(array_map(fn($q) => $q->score ?? 5, $questions['fill']));
-        $totalAnswerScore = array_sum(array_map(fn($q) => $q->score ?? 10, $questions['answer']));
-        $grandTotal = $totalChoiceScore + $totalFillScore + $totalAnswerScore;
-    @endphp
-    <div style="margin-top: 30px; padding: 15px; border-top: 2px solid #000; text-align: right; font-size: 14px;">
-        <strong>试卷总分:{{ $grandTotal }} 分</strong>
-        (选择题 {{ $totalChoiceScore }} 分 + 填空题 {{ $totalFillScore }} 分 + 解答题 {{ $totalAnswerScore }} 分)
-    </div>
+    @include('components.exam.paper-body', ['questions' => $questions])
 
     {{-- 参考答案(仅在开启时显示) --}}
     @if($includeAnswer)
@@ -420,9 +256,10 @@
                                 @else
                                     <span style="margin-left: 8px; color: #999; font-style: italic;">(待补充)</span>
                                 @endif
-                            </div>
-                        @endforeach
-                    </div>
+            </div>
+        @endforeach
+    </div>
+    </div>
                 </div>
             @endif
 

+ 6 - 0
routes/web.php

@@ -2,6 +2,7 @@
 
 use Illuminate\Support\Facades\Route;
 use App\Http\Controllers\NotificationController;
+use App\Http\Controllers\MenuVisibilityController;
 
 Route::get('/', function () {
     return redirect()->route('filament.admin.pages.dashboard');
@@ -13,6 +14,11 @@ Route::get('/test-math', function() { return view('test-math'); });
 Route::get('/test-case', function() { return view('test-case'); });
 Route::view('/knowledge-mindmap-public', 'public.knowledge-mindmap');
 Route::get('/admin/intelligent-exam/pdf/{paper_id}', [\App\Http\Controllers\ExamPdfController::class, 'show'])->name('filament.admin.auth.intelligent-exam.pdf');
+Route::get('/admin/intelligent-exam/grading/{paper_id}', [\App\Http\Controllers\ExamPdfController::class, 'showGrading'])->name('filament.admin.auth.intelligent-exam.grading');
 
 // 检查通知的路由
 Route::get('/admin/question-management/check-notifications', [NotificationController::class, 'checkNotifications']);
+
+// 菜单可见性切换路由
+Route::post('/admin/toggle-menu-visibility', [MenuVisibilityController::class, 'toggle'])
+    ->name('filament.admin.auth.toggle-menu-visibility');

+ 105 - 0
test_grading_panel.sh

@@ -0,0 +1,105 @@
+#!/bin/bash
+
+# =============================================
+# 测试脚本:验证评分面板修复
+# =============================================
+
+echo "=========================================="
+echo " 验证评分面板修复"
+echo "=========================================="
+echo ""
+
+# 颜色定义
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+echo -e "${BLUE}步骤 1: 清理缓存${NC}"
+echo "执行: php artisan view:clear"
+php artisan view:clear
+if [ $? -eq 0 ]; then
+    echo -e "${GREEN}✅ 视图缓存清理成功${NC}"
+else
+    echo "❌ 清理失败"
+fi
+echo ""
+
+echo -e "${BLUE}步骤 2: 验证文件修改${NC}"
+
+# 检查 GradingPanel.php
+if grep -q "public function mount" app/Livewire/UploadExam/GradingPanel.php; then
+    echo -e "${GREEN}✅ GradingPanel.php - mount() 方法已添加${NC}"
+else
+    echo "❌ GradingPanel.php - mount() 方法未找到"
+fi
+
+if grep -q "updatedSelectedPaperId" app/Livewire/UploadExam/GradingPanel.php; then
+    echo -e "${GREEN}✅ GradingPanel.php - updatedSelectedPaperId() 方法已添加${NC}"
+else
+    echo "❌ GradingPanel.php - updatedSelectedPaperId() 方法未找到"
+fi
+
+# 检查视图文件
+if grep -q ":selectedPaperId" resources/views/filament/pages/upload-exam-paper.blade.php; then
+    echo -e "${GREEN}✅ upload-exam-paper.blade.php - selectedPaperId 参数已传递${NC}"
+else
+    echo "❌ upload-exam-paper.blade.php - selectedPaperId 参数未传递"
+fi
+
+echo ""
+
+echo -e "${BLUE}步骤 3: 验证数据库数据${NC}"
+echo "正在查询吴同学的试卷数据..."
+
+# 使用 tinker 验证数据(如果可用)
+php artisan tinker --execute="
+\$paper = \App\Models\Paper::where('paper_name', 'like', '%吴%')->latest()->first();
+if (\$paper) {
+    \$count = \$paper->questions()->count();
+    echo \"✅ 找到试卷: \" . \$paper->paper_name . \"\\n\";
+    echo \"✅ 题目数量: \" . \$count . \" 道题\\n\";
+    if (\$count > 0) {
+        echo \"✅ 数据完整性验证通过\\n\";
+    } else {
+        echo \"⚠️  警告: 试卷存在但没有题目数据\\n\";
+    }
+} else {
+    echo \"❌ 未找到吴同学的试卷\\n\";
+}
+" 2>/dev/null
+
+echo ""
+
+echo -e "${BLUE}步骤 4: 手动测试指南${NC}"
+echo "请按照以下步骤手动验证修复:"
+echo ""
+echo "1. 打开浏览器访问: http://fa.test/admin/upload-exam-paper"
+echo ""
+echo "2. 点击【选择已有试卷评分】按钮"
+echo ""
+echo "3. 选择老师(例如:默认老师)"
+echo ""
+echo "4. 选择学生(例如:吴同学)"
+echo ""
+echo "5. 选择最新的一份试卷"
+echo ""
+echo "6. 检查评分面板:"
+echo "   ✅ 应该显示试卷的题目列表"
+echo "   ❌ 不应显示'暂无题目数据'"
+echo ""
+echo "7. 验证题目信息:"
+echo "   - 每道题应显示题目内容"
+echo "   - 显示题目类型和分值"
+echo "   - 如果有题库ID,应显示参考答案"
+echo ""
+
+echo "=========================================="
+echo " 验证完成!"
+echo "=========================================="
+echo ""
+echo "如果仍有问题,请检查:"
+echo "  1. Laravel Herd 是否正在运行"
+echo "  2. FilamentAdmin 是否可以正常访问"
+echo "  3. 查看浏览器开发者工具的控制台错误"
+echo "  4. 查看 Laravel 日志: storage/logs/laravel.log"
+echo ""

Некоторые файлы не были показаны из-за большого количества измененных файлов