Pārlūkot izejas kodu

主要是卷子识别相关

yemeishu 1 mēnesi atpakaļ
vecāks
revīzija
a2a40acc59
62 mainītis faili ar 5626 papildinājumiem un 217 dzēšanām
  1. 111 0
      app/Console/Commands/CleanupPapers.php
  2. 1150 0
      app/Filament/Pages/ExamAnalysis.php
  3. 36 6
      app/Filament/Pages/ExamHistory.php
  4. 2 2
      app/Filament/Pages/KnowledgeGraphManagement.php
  5. 1 1
      app/Filament/Pages/KnowledgeGraphVisualization.php
  6. 1 1
      app/Filament/Pages/KnowledgeMindmap.php
  7. 1 1
      app/Filament/Pages/KnowledgePoints.php
  8. 2 2
      app/Filament/Pages/KnowledgeRelationManagement.php
  9. 1 1
      app/Filament/Pages/OCRRecordList.php
  10. 95 29
      app/Filament/Pages/OCRRecordView.php
  11. 2 2
      app/Filament/Pages/PromptManagement.php
  12. 2 2
      app/Filament/Pages/QuestionGeneration.php
  13. 2 2
      app/Filament/Pages/QuestionManagement.php
  14. 2 2
      app/Filament/Pages/SimulatedGrading.php
  15. 2 2
      app/Filament/Pages/StudentAnalysis.php
  16. 2 2
      app/Filament/Pages/StudentKnowledgeGraphPage.php
  17. 1 1
      app/Filament/Pages/StudentManagement.php
  18. 511 7
      app/Filament/Pages/UploadExamPaper.php
  19. 14 0
      app/Models/OCRRecord.php
  20. 17 0
      app/Models/Paper.php
  21. 8 0
      app/Models/PaperQuestion.php
  22. 231 0
      app/Services/LatexCleanerService.php
  23. 299 0
      app/Services/LearningAnalyticsService.php
  24. 34 2
      app/Services/MathFormulaProcessor.php
  25. 79 3
      app/Services/OCRService.php
  26. 184 26
      app/Services/QuestionBankService.php
  27. 18 0
      app/View/Components/ExamAnalysis/Header.php
  28. 17 0
      app/View/Components/ExamAnalysis/LearningAnalysis.php
  29. 17 0
      app/View/Components/ExamAnalysis/Loading.php
  30. 17 0
      app/View/Components/ExamAnalysis/QuestionDetails.php
  31. 17 0
      app/View/Components/ExamAnalysis/QuickStats.php
  32. 17 0
      app/View/Components/ExamAnalysis/Recommendations.php
  33. 20 0
      config/components.php
  34. 36 0
      config/view.php
  35. 28 0
      database/migrations/2025_11_24_120700_add_remember_token_to_users_table.php
  36. 33 0
      database/migrations/2025_11_24_135849_add_paper_type_to_ocr_records_table.php
  37. 30 0
      database/migrations/2025_11_25_030248_add_analysis_id_to_papers_table.php
  38. 30 0
      database/migrations/2025_11_25_064604_add_analysis_id_to_ocr_records_table.php
  39. 248 0
      resources/views/components/exam-analysis/ARCHITECTURE.md
  40. 80 0
      resources/views/components/exam-analysis/CHANGELOG.md
  41. 223 0
      resources/views/components/exam-analysis/OCR_INTEGRATION.md
  42. 88 0
      resources/views/components/exam-analysis/README.md
  43. 27 0
      resources/views/components/exam-analysis/header.blade.php
  44. 46 0
      resources/views/components/exam-analysis/learning-analysis.blade.php
  45. 151 0
      resources/views/components/exam-analysis/question-details.blade.php
  46. 35 0
      resources/views/components/exam-analysis/quick-stats.blade.php
  47. 15 0
      resources/views/components/exam-analysis/recommendations.blade.php
  48. 4 0
      resources/views/components/loading.blade.php
  49. 78 0
      resources/views/examples/exam-analysis-components-example.blade.php
  50. 221 0
      resources/views/filament/components/exam-analysis/manual-questions.blade.php
  51. 263 0
      resources/views/filament/components/exam-analysis/mastery-analysis.blade.php
  52. 36 0
      resources/views/filament/components/exam-analysis/paper-image.blade.php
  53. 52 0
      resources/views/filament/components/exam-analysis/paper-info.blade.php
  54. 60 0
      resources/views/filament/components/exam-analysis/processing-timeline.blade.php
  55. 24 0
      resources/views/filament/pages/exam-analysis-compact.blade.php
  56. 143 0
      resources/views/filament/pages/exam-analysis-standard.blade.php
  57. 9 5
      resources/views/filament/pages/exam-history-simple.blade.php
  58. 41 2
      resources/views/filament/pages/ocr-record-view-new.blade.php
  59. 394 113
      resources/views/filament/pages/upload-exam-paper.blade.php
  60. 3 3
      resources/views/pdf/exam-paper.blade.php
  61. 55 0
      tests/test_latex_cleaner.php
  62. 260 0
      系统生成卷子分析数据获取修复报告.md

+ 111 - 0
app/Console/Commands/CleanupPapers.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Services\QuestionBankService;
+
+class CleanupPapers extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'exams:cleanup {--check : 检查数据完整性} {--cleanup : 清理不一致的试卷} {--fix : 修复题目数量统计}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '检查和清理试卷数据完整性问题';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $questionBankService = app(QuestionBankService::class);
+
+        if ($this->option('check')) {
+            return $this->checkDataIntegrity($questionBankService);
+        }
+
+        if ($this->option('cleanup')) {
+            return $this->cleanupInconsistentPapers($questionBankService);
+        }
+
+        if ($this->option('fix')) {
+            return $this->fixPaperQuestionCounts($questionBankService);
+        }
+
+        // 默认执行所有操作
+        $this->info('开始执行试卷数据完整性检查和清理...');
+
+        $this->checkDataIntegrity($questionBankService);
+        $this->cleanupInconsistentPapers($questionBankService);
+        $this->fixPaperQuestionCounts($questionBankService);
+
+        $this->info('所有操作完成!');
+        return Command::SUCCESS;
+    }
+
+    private function checkDataIntegrity(QuestionBankService $service): int
+    {
+        $this->info('检查数据完整性...');
+
+        $result = $service->checkDataIntegrity();
+        $count = $result['inconsistent_count'];
+
+        if ($count === 0) {
+            $this->info('✅ 未发现数据不一致问题');
+        } else {
+            $this->warn("⚠️  发现 {$count} 个数据不一致的试卷:");
+
+            $this->table(
+                ['试卷ID', '试卷名称', '预期题目数', '学生ID'],
+                array_map(function($paper) {
+                    return [
+                        $paper->paper_id,
+                        $paper->paper_name ?? '未命名',
+                        $paper->question_count ?? 0,
+                        $paper->student_id ?? 'unknown'
+                    ];
+                }, $result['papers'])
+            );
+        }
+
+        return $count;
+    }
+
+    private function cleanupInconsistentPapers(QuestionBankService $service): int
+    {
+        $this->info('清理不一致的试卷记录...');
+
+        $deletedCount = $service->cleanupInconsistentPapers();
+
+        if ($deletedCount === 0) {
+            $this->info('✅ 没有需要清理的试卷');
+        } else {
+            $this->info("🗑️  已清理 {$deletedCount} 个不一致的试卷记录");
+        }
+
+        return $deletedCount;
+    }
+
+    private function fixPaperQuestionCounts(QuestionBankService $service): int
+    {
+        $this->info('修复试卷题目数量统计...');
+
+        $fixedCount = $service->fixPaperQuestionCounts();
+
+        if ($fixedCount === 0) {
+            $this->info('✅ 所有试卷的题目数量统计都正确');
+        } else {
+            $this->info("🔧 已修复 {$fixedCount} 个试卷的题目数量统计");
+        }
+
+        return $fixedCount;
+    }
+}

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

@@ -0,0 +1,1150 @@
+<?php
+
+namespace App\Filament\Pages;
+
+use App\Models\OCRRecord;
+use App\Models\OCRQuestionResult;
+use App\Services\LearningAnalyticsService;
+use App\Services\OCRService;
+use BackedEnum;
+use Filament\Notifications\Notification;
+use Filament\Pages\Page;
+use Livewire\Attributes\Url;
+use UnitEnum;
+
+class ExamAnalysis extends Page
+{
+    protected static ?string $title = '试卷分析详情';
+    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
+    protected static ?string $navigationLabel = '试卷分析';
+    protected static string|UnitEnum|null $navigationGroup = '管理';
+    protected static ?int $navigationSort = 15;
+
+    #[Url]
+    public ?string $recordId = null;  // OCR记录ID
+
+    #[Url]
+    public ?string $paperId = null;   // 系统生成卷子ID
+
+    public array $recordData = [];
+    public array $analysisData = []; // 整体掌握度分析数据
+    public array $paperAnalysisData = []; // 本次试卷的分析结果
+    public array $studentInfo = [];
+    public bool $loading = true;
+    public string $recordType = ''; // 'ocr' 或 'generated'
+
+    public function mount()
+    {
+        // 允许使用 recordId(OCR记录)或 paperId(系统生成卷子)
+        if (!$this->recordId && !$this->paperId) {
+            Notification::make()
+                ->title('错误')
+                ->body('缺少记录ID或试卷ID')
+                ->danger()
+                ->send();
+
+            $this->redirectRoute('filament.admin.pages.upload-exam-paper');
+            return;
+        }
+
+        // 根据记录类型选择不同的视图
+        if ($this->recordId) {
+            // OCR记录使用紧凑布局
+            $this->view = 'filament.pages.exam-analysis-compact';
+        } elseif ($this->paperId) {
+            // 系统生成卷子使用标准布局
+            $this->view = 'filament.pages.exam-analysis-standard';
+        }
+
+        $this->loadAnalysisData();
+    }
+
+    protected function loadAnalysisData()
+    {
+        try {
+            // 处理OCR记录
+            if ($this->recordId) {
+                $this->recordType = 'ocr';
+                $record = OCRRecord::with('student')->find($this->recordId);
+
+                if (!$record) {
+                    Notification::make()
+                        ->title('错误')
+                        ->body('未找到指定的上传记录')
+                        ->danger()
+                        ->send();
+
+                    $this->redirectRoute('filament.admin.pages.upload-exam-paper');
+                    return;
+                }
+
+                $this->recordData = $record->toArray();
+                $this->studentInfo = $record->student ? $record->student->toArray() : [];
+
+                // OCR记录:添加题目统计信息
+                $ocrQuestionsCount = OCRQuestionResult::where('ocr_record_id', $this->recordId)->count();
+                $this->recordData['total_questions'] = $ocrQuestionsCount;
+                $this->recordData['questions'] = $this->getQuestions(); // 提前加载题目数据
+
+                // OCR记录如果已完成处理,加载分析数据
+                if ($record->status === 'completed' && $record->student_id) {
+                    $this->loadLearningAnalysis($record->student_id, $this->recordId);
+                } else {
+                    $this->analysisData = [];
+                }
+            }
+            // 处理系统生成卷子
+            elseif ($this->paperId) {
+                $this->recordType = 'generated';
+                $paper = \App\Models\Paper::with('student')->find($this->paperId);
+
+                if (!$paper) {
+                    Notification::make()
+                        ->title('错误')
+                        ->body('未找到指定的试卷')
+                        ->danger()
+                        ->send();
+
+                    $this->redirectRoute('filament.admin.pages.upload-exam-paper');
+                    return;
+                }
+
+                // 构造基础数据,视图会安全处理缺失字段
+                $this->recordData = [
+                    'id' => $paper->paper_id,
+                    'paper_id' => $paper->paper_id,
+                    'student_id' => $paper->student_id,
+                    'paper_type' => 'system_generated',
+                    'paper_name' => $paper->paper_name,
+                    'status' => $paper->status,
+                    'total_questions' => $paper->question_count,
+                    'created_at' => $paper->created_at,
+                    'analysis_id' => $paper->analysis_id, // AI分析记录ID
+                ];
+                $this->studentInfo = $paper->student ? $paper->student->toArray() : [];
+
+                // 获取试卷题目列表(包含题库API的详细数据)
+                $this->recordData['questions'] = $this->getQuestions();
+
+                // 系统生成卷子也尝试加载学习分析数据
+                if ($paper->student_id) {
+                    $this->loadLearningAnalysis($paper->student_id, $this->paperId);
+                } else {
+                    $this->analysisData = [];
+                }
+            }
+
+            $this->loading = false;
+
+        } catch (\Exception $e) {
+            \Log::error('加载试卷分析数据失败', [
+                'record_id' => $this->recordId,
+                'paper_id' => $this->paperId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            Notification::make()
+                ->title('错误')
+                ->body('加载分析数据失败:' . $e->getMessage())
+                ->danger()
+                ->send();
+
+            $this->loading = false;
+        }
+    }
+
+    /**
+     * 加载学习分析数据
+     */
+    protected function loadLearningAnalysis($studentId, $identifier)
+    {
+        try {
+            // 1. 尝试从数据库加载本次试卷的分析结果
+            $paperId = null;
+            if ($this->recordType === 'ocr') {
+                $paperId = $this->recordData['exam_id'] ?? null;
+            } elseif ($this->recordType === 'generated') {
+                $paperId = $this->recordData['paper_id'] ?? null;
+            }
+
+            if ($paperId) {
+                // 从 paper_analysis_results 表读取分析结果
+                $paperAnalysis = \DB::table('paper_analysis_results')
+                    ->where('paper_id', $paperId)
+                    ->first();
+
+                if ($paperAnalysis && !empty($paperAnalysis->analysis_data)) {
+                    $this->paperAnalysisData = json_decode($paperAnalysis->analysis_data, true);
+
+                    \Log::info('本次试卷分析结果已从数据库加载', [
+                        'paper_id' => $paperId,
+                        'student_id' => $studentId,
+                        'data_keys' => array_keys($this->paperAnalysisData ?? [])
+                    ]);
+                } else {
+                    // 如果数据库中没有分析结果,回退到API调用(临时解决方案)
+                    \Log::info('数据库中未找到分析结果,回退到API调用', [
+                        'paper_id' => $paperId,
+                        'student_id' => $studentId
+                    ]);
+                    $this->loadLearningAnalysisFromAPI($studentId, $paperId);
+                    return;
+                }
+            }
+
+            // 2. 尝试从知识点记录表获取整体掌握度数据
+            $knowledgePointRecords = \DB::table('knowledge_point_records')
+                ->where('student_id', $studentId)
+                ->get();
+
+            // 处理知识点掌握度数据
+            if ($knowledgePointRecords->count() > 0) {
+                // 如果找到了知识点记录数据,使用它
+                $this->processKnowledgePointRecords($knowledgePointRecords);
+            } else {
+                // 如果没有找到知识点记录,回退到API调用
+                \Log::info('知识点记录表为空,回退到API调用', [
+                    'student_id' => $studentId
+                ]);
+                $this->loadLearningAnalysisFromAPI($studentId, $paperId ?? null);
+            }
+        } catch (\Exception $e) {
+            \Log::warning('加载分析数据失败,回退到API调用', [
+                'identifier' => $identifier,
+                'student_id' => $studentId,
+                'type' => $this->recordType,
+                'error' => $e->getMessage()
+            ]);
+
+            // 数据库查询失败时回退到API调用
+            $this->loadLearningAnalysisFromAPI($studentId, $paperId ?? null);
+        }
+    }
+
+    /**
+     * 从API加载学习分析数据(回退方案)
+     */
+    protected function loadLearningAnalysisFromAPI($studentId, $paperId = null)
+    {
+        try {
+            $learningService = app(\App\Services\LearningAnalyticsService::class);
+
+            // 1. 加载本次试卷的分析结果
+            $analysisId = null;
+            if ($this->recordType === 'ocr' && isset($this->recordData['analysis_id'])) {
+                $analysisId = $this->recordData['analysis_id'];
+            } elseif ($this->recordType === 'generated' && isset($this->recordData['analysis_id'])) {
+                $analysisId = $this->recordData['analysis_id'];
+            }
+
+            if ($analysisId) {
+                $paperAnalysisResponse = $learningService->getAnalysisResult($analysisId);
+                if (!empty($paperAnalysisResponse) && isset($paperAnalysisResponse['data'])) {
+                    $this->paperAnalysisData = $paperAnalysisResponse['data'];
+
+                    // 将API的分析结果同步到题目数据
+                    if (isset($this->paperAnalysisData['question_results'])) {
+                        $this->syncApiAnalysisToQuestions($this->paperAnalysisData['question_results']);
+                    }
+
+                    \Log::info('本次试卷分析结果已从API加载', [
+                        'analysis_id' => $analysisId,
+                        'student_id' => $studentId,
+                        'data_keys' => array_keys($this->paperAnalysisData),
+                        'question_results_count' => count($this->paperAnalysisData['question_results'] ?? [])
+                    ]);
+                }
+            }
+
+            // 2. 调用学习分析API获取整体掌握度数据
+            $masteryResponse = $learningService->getStudentMastery($studentId);
+
+            // 转换为页面期望的格式
+            if (!empty($masteryResponse) && isset($masteryResponse['data'])) {
+                $masteryList = $masteryResponse['data'];
+
+                // 计算整体掌握度
+                $totalMastery = 0;
+                $count = count($masteryList);
+                $weakAreas = [];
+                $knowledgePoints = [];
+
+                foreach ($masteryList as $mastery) {
+                    $masteryLevel = $mastery['mastery_level'] ?? 0;
+                    $totalMastery += $masteryLevel;
+
+                    // 识别薄弱知识点(掌握度 < 0.6)
+                    if ($masteryLevel < 0.6) {
+                        $weakAreas[] = [
+                            'kp_code' => $mastery['kp_code'],
+                            'mastery' => $masteryLevel
+                        ];
+                    }
+
+                    // 构造知识点数据
+                    $totalAttempts = $mastery['total_attempts'] ?? 0;
+                    $correctAttempts = $mastery['correct_attempts'] ?? 0;
+                    $accuracyRate = $totalAttempts > 0 ? $correctAttempts / $totalAttempts : 0;
+
+                    $knowledgePoints[] = [
+                        'kp_code' => $mastery['kp_code'],
+                        'name' => $mastery['kp_code'], // TODO: 从知识图谱服务获取名称
+                        'mastery' => $masteryLevel,
+                        'mastery_level' => $masteryLevel, // 添加模板需要的字段
+                        'total_attempts' => $totalAttempts,
+                        'correct_attempts' => $correctAttempts,
+                        'accuracy_rate' => $accuracyRate
+                    ];
+                }
+
+                $overallMastery = $count > 0 ? $totalMastery / $count : 0;
+
+                // 生成学习建议
+                $recommendations = $this->generateRecommendations($overallMastery, $weakAreas, $knowledgePoints);
+
+                // 只显示与当前试卷相关的知识点
+                $currentPaperKps = $this->getCurrentPaperKnowledgePoints();
+                $currentPaperKpCodes = array_column($currentPaperKps, 'kp_code');
+                $filteredKnowledgePoints = array_filter($knowledgePoints, function($kp) use ($currentPaperKpCodes) {
+                    return in_array($kp['kp_code'], $currentPaperKpCodes);
+                });
+
+                $this->analysisData = [
+                    'overall_mastery' => $overallMastery,
+                    'weak_areas' => array_filter($weakAreas, function($weak) use ($currentPaperKpCodes) {
+                        return in_array($weak['kp_code'], $currentPaperKpCodes);
+                    }),
+                    'knowledge_points' => $filteredKnowledgePoints, // 只显示当前试卷相关知识点
+                    'recommendations' => $recommendations,
+                    'total_knowledge_points' => count($filteredKnowledgePoints),
+                    'mastery_distribution' => $this->calculateMasteryDistribution($masteryList)
+                ];
+
+                \Log::info('学习分析数据已从API加载', [
+                    'student_id' => $studentId,
+                    'overall_mastery' => $overallMastery,
+                    'knowledge_points_count' => count($knowledgePoints),
+                    'filtered_knowledge_points_count' => count($filteredKnowledgePoints),
+                    'current_paper_kp_codes' => $currentPaperKpCodes,
+                    'weak_areas_count' => count($weakAreas)
+                ]);
+            } else {
+                \Log::info('API返回数据为空', [
+                    'student_id' => $studentId,
+                    'paper_id' => $paperId,
+                    'type' => $this->recordType
+                ]);
+                $this->analysisData = [];
+            }
+        } catch (\Exception $apiError) {
+            \Log::warning('API调用失败', [
+                'student_id' => $studentId,
+                'paper_id' => $paperId,
+                'type' => $this->recordType,
+                'error' => $apiError->getMessage()
+            ]);
+
+            // API调用失败时设置空数组,避免页面报错
+            $this->analysisData = [];
+            $this->paperAnalysisData = [];
+        }
+    }
+
+    /**
+     * 处理知识点记录数据
+     */
+    protected function processKnowledgePointRecords($knowledgePointRecords)
+    {
+        // 根据实际的knowledge_point_records表结构处理数据
+        $masteryList = [];
+        $weakAreas = [];
+        $knowledgePoints = [];
+        $totalMastery = 0;
+        $count = 0;
+
+        foreach ($knowledgePointRecords as $record) {
+            // knowledge_point_records表有不同的字段结构
+            $kpCode = $record->knowledge_point ?? '';
+            $masteryLevel = $record->mastery_after ?? $record->mastery_before ?? 0;
+
+            if (!empty($kpCode)) {
+                $masteryList[] = [
+                    'kp_code' => $kpCode,
+                    'mastery_level' => $masteryLevel
+                ];
+
+                $totalMastery += $masteryLevel;
+                $count++;
+
+                // 识别薄弱知识点(掌握度 < 0.6)
+                if ($masteryLevel < 0.6) {
+                    $weakAreas[] = [
+                        'kp_code' => $kpCode,
+                        'mastery' => $masteryLevel
+                    ];
+                }
+
+                // 构造知识点数据
+                $knowledgePoints[] = [
+                    'kp_code' => $kpCode,
+                    'name' => $kpCode,
+                    'mastery' => $masteryLevel,
+                    'mastery_level' => $masteryLevel, // 添加模板需要的字段
+                    'total_attempts' => 1, // 默认值
+                    'correct_attempts' => $masteryLevel > 0.5 ? 1 : 0, // 估算值
+                    'accuracy_rate' => $masteryLevel
+                ];
+            }
+        }
+
+        $overallMastery = $count > 0 ? $totalMastery / $count : 0;
+
+        // 生成学习建议
+        $recommendations = $this->generateRecommendations($overallMastery, $weakAreas, $knowledgePoints);
+
+        // 只显示与当前试卷相关的知识点
+        $currentPaperKps = $this->getCurrentPaperKnowledgePoints();
+        $currentPaperKpCodes = array_column($currentPaperKps, 'kp_code');
+        $filteredKnowledgePoints = array_filter($knowledgePoints, function($kp) use ($currentPaperKpCodes) {
+            return in_array($kp['kp_code'], $currentPaperKpCodes);
+        });
+
+        $this->analysisData = [
+            'overall_mastery' => $overallMastery,
+            'weak_areas' => array_filter($weakAreas, function($weak) use ($currentPaperKpCodes) {
+                return in_array($weak['kp_code'], $currentPaperKpCodes);
+            }),
+            'knowledge_points' => $filteredKnowledgePoints, // 只显示当前试卷相关知识点
+            'recommendations' => $recommendations,
+            'total_knowledge_points' => count($filteredKnowledgePoints),
+            'mastery_distribution' => $this->calculateMasteryDistribution($masteryList)
+        ];
+
+        \Log::info('学习分析数据已从knowledge_point_records加载', [
+            'student_id' => $knowledgePointRecords->first()->student_id ?? 'unknown',
+            'overall_mastery' => $overallMastery,
+            'knowledge_points_count' => count($knowledgePoints),
+            'filtered_knowledge_points_count' => count($filteredKnowledgePoints),
+            'current_paper_kp_codes' => $currentPaperKpCodes,
+            'weak_areas_count' => count($weakAreas)
+        ]);
+    }
+
+    /**
+     * 生成学习建议
+     */
+    protected function generateRecommendations($overallMastery, $weakAreas, $knowledgePoints)
+    {
+        $recommendations = [];
+
+        if ($overallMastery >= 0.8) {
+            $recommendations[] = '整体掌握情况良好,继续保持!可以尝试更有挑战性的题目。';
+        } elseif ($overallMastery >= 0.6) {
+            $recommendations[] = '基础掌握较好,建议加强薄弱知识点的练习。';
+        } else {
+            $recommendations[] = '需要系统复习基础知识,建议从简单题目开始逐步提升。';
+        }
+
+        // 针对薄弱知识点的建议
+        if (count($weakAreas) > 0) {
+            $weakKpCodes = array_slice(array_column($weakAreas, 'kp_code'), 0, 3);
+            $recommendations[] = '重点加强以下知识点: ' . implode('、', $weakKpCodes);
+        }
+
+        // 根据答题次数给建议
+        $lowAttempts = array_filter($knowledgePoints, function($kp) {
+            return ($kp['total_attempts'] ?? 0) < 5;
+        });
+
+        if (count($lowAttempts) > 0) {
+            $recommendations[] = '部分知识点练习次数较少,建议增加练习量以巩固掌握。';
+        }
+
+        return $recommendations;
+    }
+
+    /**
+     * 计算掌握度分布
+     */
+    protected function calculateMasteryDistribution($masteryList)
+    {
+        $distribution = [
+            'high' => 0,    // >= 0.7
+            'medium' => 0,  // 0.4 - 0.7
+            'low' => 0      // < 0.4
+        ];
+
+        foreach ($masteryList as $mastery) {
+            $level = $mastery['mastery_level'] ?? 0;
+            if ($level >= 0.7) {
+                $distribution['high']++;
+            } elseif ($level >= 0.4) {
+                $distribution['medium']++;
+            } else {
+                $distribution['low']++;
+            }
+        }
+
+        return $distribution;
+    }
+
+    public function getPaperTypeLabel(): string
+    {
+        if ($this->recordType === 'ocr' && isset($this->recordData['paper_type'])) {
+            return match($this->recordData['paper_type']) {
+                'unit_test' => '单元测试',
+                'midterm' => '期中考试',
+                'final' => '期末考试',
+                'homework' => '家庭作业',
+                'quiz' => '随堂测验',
+                'other' => '其他',
+                default => '未分类',
+            };
+        }
+        return $this->recordData['paper_type'] ?? '未知';
+    }
+
+    public function getStatusBadge(): string
+    {
+        $status = $this->recordData['status'] ?? 'unknown';
+        return match($status) {
+            'pending' => '<span class="badge badge-ghost">待处理</span>',
+            'processing' => '<span class="badge badge-info gap-2"><span class="loading loading-spinner loading-xs"></span>处理中</span>',
+            'completed' => '<span class="badge badge-success">已完成</span>',
+            'failed' => '<span class="badge badge-error">失败</span>',
+            default => '<span class="badge badge-ghost">未知</span>',
+        };
+    }
+
+    /**
+     * 判断是否为 OCR 场景
+     */
+    public function isOcrRecord(): bool
+    {
+        return $this->recordType === 'ocr';
+    }
+
+    /**
+     * 获取OCR记录的题目数据
+     * 从OCRQuestionResult表加载并格式化为组件期望的格式
+     */
+    protected function getOcrQuestions(): array
+    {
+        $recordId = $this->recordId ?? null;
+        if (!$recordId) {
+            \Log::warning('OCR记录缺少recordId', ['recordId' => $recordId]);
+            return [];
+        }
+
+        try {
+            // 从OCRQuestionResult表加载题目数据
+            $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $recordId)
+                ->orderBy('question_number')
+                ->get();
+
+            \Log::info('加载OCR题目数据', [
+                'record_id' => $recordId,
+                'questions_count' => $ocrQuestions->count()
+            ]);
+
+            if ($ocrQuestions->isEmpty()) {
+                \Log::warning('OCR记录没有题目数据', ['record_id' => $recordId]);
+                return [];
+            }
+
+            // 创建API分析结果的映射(如果有)
+            $analysisMap = [];
+            if (!empty($this->paperAnalysisData['question_results'])) {
+                foreach ($this->paperAnalysisData['question_results'] as $result) {
+                    if (isset($result['question_id'])) {
+                        $analysisMap[$result['question_id']] = $result;
+                    }
+                }
+            }
+
+            $questions = [];
+
+            foreach ($ocrQuestions as $oq) {
+                // 获取API分析结果(如果有)
+                $aiAnalysis = null;
+                if (isset($analysisMap[$oq->question_number])) {
+                    $analysis = $analysisMap[$oq->question_number];
+                    $aiAnalysis = [
+                        'analysis' => $analysis['reason'] ?? '',
+                        'mistake_type' => $analysis['mistake_type'] ?? '',
+                        'mistake_category' => $analysis['mistake_category'] ?? '',
+                        'suggestions' => [$analysis['suggestions'] ?? ''],
+                        'correct_solution' => $analysis['correct_solution'] ?? '',
+                    ];
+                } elseif (!empty($oq->ai_feedback)) {
+                    // 如果没有API分析,使用OCR记录的AI反馈
+                    $aiAnalysis = [
+                        'analysis' => $oq->ai_feedback,
+                        'mistake_type' => '',
+                        'mistake_category' => '',
+                        'suggestions' => [$oq->ai_feedback],
+                        'correct_solution' => '',
+                    ];
+                }
+
+                // 学生答案:优先使用老师校准的答案(manual_answer),如果没有则使用OCR识别的答案
+                $ocrAnswer = trim($oq->student_answer ?? '');
+                $manualAnswer = trim($oq->manual_answer ?? '');
+                $studentAnswer = !empty($manualAnswer) ? $manualAnswer : $ocrAnswer;
+
+                // 判断是否有答案(OCR识别或老师校准)
+                $hasAnswer = !empty($studentAnswer);
+                $displayAnswer = $hasAnswer ? ($studentAnswer ?: '空') : '未作答';
+
+                // 从AI分析结果中获取正确答案和判断
+                $correctAnswer = null;
+                $isCorrect = false;
+                if (isset($analysisMap[$oq->question_number])) {
+                    $analysis = $analysisMap[$oq->question_number];
+                    $correctAnswer = $analysis['correct_answer'] ?? $analysis['correct_solution'] ?? null;
+                    $isCorrect = $analysis['is_correct'] ?? false;
+                }
+
+                // 显示答案对比(如果有AI分析的正确答案)
+                $answerComparison = null;
+                if (!empty($correctAnswer) && $studentAnswer !== $correctAnswer && $hasAnswer) {
+                    $answerComparison = [
+                        'student' => $studentAnswer,
+                        'correct' => $correctAnswer
+                    ];
+                }
+
+                $questions[] = [
+                    'id' => $oq->id,
+                    'question_number' => $oq->question_number,
+                    'question_bank_id' => 'ocr_' . $oq->question_number, // OCR题目没有题库ID,用前缀标识
+                    'question_type' => 'unknown',
+                    'question_text' => $oq->question_text ?? '题目内容缺失',
+                    'content' => $oq->question_text ?? '题目内容缺失',
+                    'stem' => $oq->question_text ?? '题目内容缺失',
+                    'answer' => $correctAnswer ?? '', // 正确答案(从AI分析获取)
+                    'reference_answer' => $correctAnswer ?? '',
+                    'score_total' => $oq->score_total ?? null, // OCR题目的分数可能为空
+                    'score_obtained' => $oq->score_obtained ?? null, // OCR题目的分数可能为空
+                    'student_answer' => $displayAnswer, // 学生答案:未作答/空/实际答案(校准后)
+                    'is_correct' => $isCorrect,
+                    'kp_code' => $oq->kp_code ?? null, // OCR题目的知识点可能为空
+                    'ai_analysis' => $aiAnalysis,
+                    'answer_comparison' => $answerComparison, // 答案对比信息
+                ];
+            }
+
+            \Log::info('OCR题目数据格式化完成', [
+                'record_id' => $recordId,
+                'formatted_questions_count' => count($questions),
+                'has_ai_analysis_count' => count(array_filter($questions, fn($q) => !empty($q['ai_analysis'])))
+            ]);
+
+            return $questions;
+
+        } catch (\Exception $e) {
+            \Log::error('获取OCR题目数据失败', [
+                'record_id' => $recordId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return [];
+        }
+    }
+
+    /**
+     * 获取题目列表(根据场景返回不同数据,包含AI分析解析)
+     */
+    public function getQuestions(): array
+    {
+        // OCR记录:从OCRQuestionResult表加载题目数据
+        if ($this->recordType === 'ocr') {
+            return $this->getOcrQuestions();
+        }
+
+        // 系统生成卷子:从PaperQuestion表加载题目数据
+        $paperId = $this->recordData['paper_id'] ?? null;
+        if (!$paperId) {
+            return [];
+        }
+
+        try {
+            // 直接查询题目数据
+            $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paperId)
+                ->orderBy('question_number')
+                ->get();
+
+            if ($paperQuestions->isEmpty()) {
+                return [];
+            }
+
+            // 获取题库题目详情
+            $questionBankService = app(\App\Services\QuestionBankService::class);
+            $questionBankIds = $paperQuestions->pluck('question_bank_id')->unique()->filter()->toArray();
+            $questionDetails = collect([]);
+
+            \Log::info('准备调用题库服务', [
+                'question_bank_ids' => $questionBankIds,
+                'ids_count' => count($questionBankIds)
+            ]);
+
+            if (!empty($questionBankIds)) {
+                try {
+                    \Log::info('开始调用题库服务getQuestionsByIds');
+                    $details = $questionBankService->getQuestionsByIds($questionBankIds);
+                    \Log::info('题库服务调用结果', [
+                        'response' => $details,
+                        'has_data' => isset($details['data']),
+                        'data_type' => gettype($details['data'] ?? null)
+                    ]);
+
+                    if (isset($details['data']) && is_array($details['data'])) {
+                        $questionDetails = collect($details['data'])->keyBy('id');
+                        \Log::info('题库数据处理成功', [
+                            'details_count' => $questionDetails->count(),
+                            'first_key' => $questionDetails->keys()->first()
+                        ]);
+                    } else {
+                        \Log::warning('题库服务返回数据格式不正确', ['details' => $details]);
+                    }
+                } catch (\Exception $e) {
+                    \Log::error('获取题库数据失败', [
+                        'error' => $e->getMessage(),
+                        'trace' => $e->getTraceAsString()
+                    ]);
+                }
+            } else {
+                \Log::info('没有有效的题库ID');
+            }
+
+            // 构建题目数据 - 合并paper_questions表和题库API数据
+            $questions = [];
+
+            // 创建API分析结果的映射(如果有)
+            $analysisMap = [];
+            if (!empty($this->paperAnalysisData['question_results'])) {
+                foreach ($this->paperAnalysisData['question_results'] as $result) {
+                    if (isset($result['question_id'])) {
+                        $analysisMap[$result['question_id']] = $result;
+                    }
+                }
+            }
+
+            foreach ($paperQuestions as $pq) {
+                // 从题库API获取详细数据
+                $detail = $questionDetails->get($pq->question_bank_id);
+
+                \Log::info('构建题目数据', [
+                    'question_bank_id' => $pq->question_bank_id,
+                    'has_detail' => $detail ? 'Yes' : 'No',
+                    'detail_id' => $detail['id'] ?? 'null',
+                    'stem_preview' => $detail ? substr($detail['stem'] ?? '', 50) : 'null',
+                    'kp_code_from_db' => $pq->knowledge_point,
+                    'kp_code_from_api' => $detail['kp_code'] ?? 'null',
+                    'has_analysis' => isset($analysisMap[$pq->question_bank_id]) ? 'Yes' : 'No'
+                ]);
+
+                // 优先使用题库API返回的stem,如果没有则使用paper_questions中的question_text
+                $questionText = $detail['stem'] ?? $detail['content'] ?? $pq->question_text ?? '题目内容缺失';
+                // 优先使用题库API返回的kp_code,如果没有则使用paper_questions中的knowledge_point
+                $kpCode = $detail['kp_code'] ?? $pq->knowledge_point ?? 'N/A';
+
+                // 获取API分析结果(如果有)
+                $aiAnalysis = null;
+                if (isset($analysisMap[$pq->question_bank_id])) {
+                    $analysis = $analysisMap[$pq->question_bank_id];
+                    $aiAnalysis = [
+                        'analysis' => $analysis['reason'] ?? '',
+                        'mistake_type' => $analysis['mistake_type'] ?? '',
+                        'mistake_category' => $analysis['mistake_category'] ?? '',
+                        'suggestions' => [$analysis['suggestions'] ?? ''],
+                        'correct_solution' => $analysis['correct_solution'] ?? '',
+                    ];
+                }
+
+                $questions[] = [
+                    'id' => $pq->id,
+                    'question_number' => $pq->question_number,
+                    'question_bank_id' => $pq->question_bank_id,
+                    'question_type' => $pq->question_type,
+                    'question_text' => $questionText,
+                    'content' => $questionText,
+                    'stem' => $questionText,
+                    'answer' => $detail['answer'] ?? '',
+                    'reference_answer' => $detail['answer'] ?? '',
+                    'score_total' => $pq->score ?? 5,
+                    'score_obtained' => $pq->score_obtained ?? 0,
+                    'student_answer' => $pq->student_answer ?? '未作答',
+                    'is_correct' => $pq->is_correct ?? false,
+                    'kp_code' => $kpCode,
+                    'ai_analysis' => $aiAnalysis,
+                ];
+            }
+
+            return $questions;
+
+        } catch (\Exception $e) {
+            \Log::error('获取题目列表失败', [
+                'paper_id' => $paperId,
+                'error' => $e->getMessage()
+            ]);
+            return [];
+        }
+    }
+
+    /**
+     * 重新处理OCR
+     */
+    public function reprocessOCR()
+    {
+        if (!$this->recordId) {
+            return;
+        }
+
+        try {
+            $record = OCRRecord::find($this->recordId);
+            if (!$record) {
+                throw new \Exception('记录不存在');
+            }
+
+            $ocrService = app(OCRService::class);
+            $ocrService->reprocess($record);
+
+            Notification::make()
+                ->title('已提交重新处理')
+                ->body('OCR识别任务已重新加入队列')
+                ->success()
+                ->send();
+
+            $this->loadAnalysisData(); // 刷新数据
+
+        } catch (\Exception $e) {
+            Notification::make()
+                ->title('操作失败')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
+        }
+    }
+
+    /**
+     * 重新提交分析
+     */
+    public function reanalyze()
+    {
+        if (!$this->recordId) {
+            return;
+        }
+
+        try {
+            $record = OCRRecord::find($this->recordId);
+            if (!$record) {
+                throw new \Exception('记录不存在');
+            }
+
+            // 获取当前的题目数据(包含可能的人工校准)
+            $questions = OCRQuestionResult::where('ocr_record_id', $this->recordId)
+                ->orderBy('question_number')
+                ->get()
+                ->map(function ($q) {
+                    return [
+                        'question_number' => $q->question_number,
+                        'content' => $q->question_text,
+                        'student_answer' => $q->student_answer,
+                        'manual_answer' => $q->manual_answer,
+                        'answer_verified' => $q->answer_verified,
+                        'confidence' => $q->score_confidence,
+                        'kp_code' => $q->kp_code,
+                        'score_value' => $q->score_value,
+                    ];
+                })->toArray();
+
+            if (empty($questions)) {
+                throw new \Exception('没有可分析的题目数据');
+            }
+
+            // 构造分析请求数据
+            $analysisData = [
+                'exam_id' => $record->exam_id,
+                'student_id' => $record->student_id,
+                'ocr_record_id' => $record->id,
+                'teacher_name' => auth()->user()->name ?? 'Teacher',
+                'analysis_type' => 'mastery',
+                'questions' => array_map(function($q) {
+                    // 优先使用人工校准的答案
+                    $studentAnswer = $q['student_answer'] ?? '';
+                    if (isset($q['manual_answer']) && !empty($q['manual_answer'])) {
+                        $studentAnswer = $q['manual_answer'];
+                    }
+
+                    return [
+                        'question_id' => $q['question_number'],
+                        'question_number' => (string)$q['question_number'],
+                        'kp_code' => $q['kp_code'] ?? null,
+                        'score_value' => $q['score_value'] ?? 0,
+                        'student_answer' => $studentAnswer,
+                        'ocr_confidence' => $q['confidence'] ?? 0,
+                        'question_text' => $q['content'] ?? '',
+                        'teacher_validated' => $q['answer_verified'] ?? false,
+                    ];
+                }, $questions)
+            ];
+
+            // 调用分析服务
+            $learningService = app(LearningAnalyticsService::class);
+            $result = $learningService->submitOCRAnalysis($analysisData);
+
+            if (isset($result['success']) && $result['success']) {
+                $record->update([
+                    'ai_analyzed_at' => now(),
+                    'ai_analysis_count' => ($record->ai_analysis_count ?? 0) + 1
+                ]);
+
+                Notification::make()
+                    ->title('分析请求已提交')
+                    ->body('系统正在重新分析试卷,请稍后刷新查看结果')
+                    ->success()
+                    ->send();
+
+                $this->loadAnalysisData(); // 刷新数据
+            } else {
+                throw new \Exception($result['message'] ?? '提交分析失败');
+            }
+
+        } catch (\Exception $e) {
+            Notification::make()
+                ->title('操作失败')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
+        }
+    }
+
+    /**
+     * 从当前试卷数据中提取知识点信息
+     */
+    protected function extractKnowledgePointsFromCurrentPaper(): array
+    {
+        $knowledgePoints = [];
+
+        $questions = $this->getQuestions();
+
+        foreach ($questions as $question) {
+            $kpCode = $question['kp_code'] ?? null;
+            if ($kpCode && $kpCode !== 'N/A') {
+                $isCorrect = $question['is_correct'] ?? false;
+
+                if (!isset($knowledgePoints[$kpCode])) {
+                    $knowledgePoints[$kpCode] = [
+                        'kp_code' => $kpCode,
+                        'name' => $kpCode,
+                        'total_attempts' => 0,
+                        'correct_attempts' => 0,
+                        'mastery_level' => 0,
+                        'accuracy_rate' => 0
+                    ];
+                }
+
+                $knowledgePoints[$kpCode]['total_attempts']++;
+                if ($isCorrect) {
+                    $knowledgePoints[$kpCode]['correct_attempts']++;
+                }
+            }
+        }
+
+        // 计算准确率和掌握度
+        foreach ($knowledgePoints as &$kp) {
+            if ($kp['total_attempts'] > 0) {
+                $kp['accuracy_rate'] = $kp['correct_attempts'] / $kp['total_attempts'];
+                $kp['mastery_level'] = $kp['accuracy_rate'];
+            }
+        }
+
+        return array_values($knowledgePoints);
+    }
+
+    /**
+     * 获取当前试卷的知识点掌握情况
+     */
+    protected function getCurrentPaperKnowledgePoints(): array
+    {
+        // 首先尝试从当前试卷数据中提取
+        $currentPaperKps = $this->extractKnowledgePointsFromCurrentPaper();
+
+        // 如果有历史分析数据,合并以提供更准确的掌握度
+        if (!empty($this->analysisData['knowledge_points'])) {
+            $historicalKps = $this->analysisData['knowledge_points'];
+
+            foreach ($currentPaperKps as &$currentKp) {
+                $kpCode = $currentKp['kp_code'];
+
+                // 查找历史数据
+                $historicalData = collect($historicalKps)->firstWhere('kp_code', $kpCode);
+
+                if ($historicalData && isset($historicalData['mastery_level'])) {
+                    // 使用历史数据的掌握度,但保留试卷的实际表现
+                    $currentKp['historical_mastery_level'] = $historicalData['mastery_level'];
+                    $currentKp['total_attempts'] = $historicalData['total_attempts'] ?? $currentKp['total_attempts'];
+                    $currentKp['correct_attempts'] = $historicalData['correct_attempts'] ?? $currentKp['correct_attempts'];
+                    $currentKp['accuracy_rate'] = $historicalData['accuracy_rate'] ?? $currentKp['accuracy_rate'];
+
+                    // 优先使用历史的掌握度,但考虑试卷表现做微调
+                    $paperPerformance = $currentKp['accuracy_rate'];
+                    $historicalPerformance = $historicalData['mastery_level'] ?? 0;
+
+                    // 综合计算:70%历史 + 30%本试卷
+                    $currentKp['mastery_level'] = ($historicalPerformance * 0.7 + $paperPerformance * 0.3);
+                }
+            }
+        }
+
+        return $currentPaperKps;
+    }
+
+    /**
+     * 将API的分析结果同步到题目数据和数据库
+     */
+    protected function syncApiAnalysisToQuestions(array $questionResults)
+    {
+        try {
+            \Log::info('开始同步API分析结果到题目数据', [
+                'paper_id' => $this->paperId,
+                'question_results_count' => count($questionResults)
+            ]);
+
+            // 创建question_id到分析结果的映射
+            $analysisMap = [];
+            foreach ($questionResults as $result) {
+                $questionId = $result['question_id'] ?? null;
+                if ($questionId) {
+                    $analysisMap[$questionId] = $result;
+                }
+            }
+
+            // 获取当前试卷的题目
+            $paper = \App\Models\Paper::find($this->paperId);
+            if ($paper) {
+                $paperQuestions = $paper->questions()->get();
+                $updatedCount = 0;
+
+                foreach ($paperQuestions as $pq) {
+                    // 查找对应的API分析结果
+                    if (isset($analysisMap[$pq->question_bank_id])) {
+                        $analysis = $analysisMap[$pq->question_bank_id];
+
+                        // 更新数据库字段
+                        $pq->is_correct = $analysis['correct'] ?? false;
+                        $pq->score_obtained = $analysis['score'] ?? 0;
+                        $pq->save();
+
+                        $updatedCount++;
+
+                        \Log::info('更新题目分析结果', [
+                            'question_bank_id' => $pq->question_bank_id,
+                            'is_correct' => $pq->is_correct,
+                            'score_obtained' => $pq->score_obtained
+                        ]);
+                    }
+                }
+
+                \Log::info('API分析结果同步完成', [
+                    'updated_count' => $updatedCount,
+                    'total_questions' => $paperQuestions->count()
+                ]);
+
+                // 刷新recordData中的questions数据
+                $this->recordData['questions'] = $this->getQuestions();
+            }
+        } catch (\Exception $e) {
+            \Log::error('同步API分析结果失败', [
+                'paper_id' => $this->paperId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 提交OCR记录到AI分析API(与系统卷子使用统一接口)
+     */
+    protected function submitOcrForAnalysis($record)
+    {
+        try {
+            // 获取OCR题目结果(使用校准后的答案)
+            $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $record->id)
+                ->orderBy('question_number')
+                ->get();
+
+            if ($ocrQuestions->isEmpty()) {
+                \Log::warning('OCR记录没有题目,无法提交分析', ['record_id' => $record->id]);
+                return;
+            }
+
+            // 构建答题数据(与系统卷子格式一致)
+            $answers = [];
+            foreach ($ocrQuestions as $oq) {
+                // 使用校准后的答案(manual_answer),如果没有则使用OCR识别的答案
+                $studentAnswer = !empty(trim($oq->manual_answer ?? ''))
+                    ? trim($oq->manual_answer)
+                    : trim($oq->student_answer ?? '');
+
+                $answers[] = [
+                    'question_bank_id' => 'ocr_q' . $oq->question_number,  // OCR题目没有题库ID,生成临时ID
+                    'question_text' => $oq->question_text ?? '',  // 添加题目内容
+                    'student_answer' => $studentAnswer,
+                    'is_correct' => null,  // 让AI分析判断
+                    'score' => null,  // OCR题目可能没有分数
+                    'max_score' => $oq->score_total ?? null,
+                    'kp_code' => $oq->kp_code ?? null,
+                ];
+            }
+
+            // 使用与系统卷子相同的接口提交
+            $submissionData = [
+                'paper_id' => 'ocr_' . $record->id,  // OCR记录ID作为paper_id
+                'answers' => $answers,
+            ];
+
+            \Log::info('提交OCR数据到AI分析(统一接口)', [
+                'record_id' => $record->id,
+                'student_id' => $record->student_id,
+                'question_count' => count($answers),
+                'api_endpoint' => '/api/v1/attempts/batch/student/' . $record->student_id
+            ]);
+
+            // 调用学习分析服务(与系统卷子使用相同的方法)
+            $learningService = app(\App\Services\LearningAnalyticsService::class);
+            $response = $learningService->submitBatchAttempts($record->student_id, $submissionData);
+
+            if (!empty($response) && !isset($response['error'])) {
+                // 从响应中获取analysis_id(如果API返回)
+                $analysisId = $response['analysis_id'] ?? $response['data']['analysis_id'] ?? ('batch_' . $record->id . '_' . time());
+
+                // 更新OCR记录的analysis_id
+                $record->analysis_id = $analysisId;
+                $record->save();
+
+                \Log::info('OCR分析提交成功(统一接口)', [
+                    'record_id' => $record->id,
+                    'analysis_id' => $analysisId,
+                    'response_keys' => array_keys($response)
+                ]);
+
+                // 更新recordData
+                $this->recordData['analysis_id'] = $analysisId;
+            } else {
+                \Log::error('OCR分析提交失败', [
+                    'record_id' => $record->id,
+                    'response' => $response
+                ]);
+            }
+        } catch (\Exception $e) {
+            \Log::error('提交OCR分析异常', [
+                'record_id' => $record->id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        }
+    }
+}

+ 36 - 6
app/Filament/Pages/ExamHistory.php

@@ -15,7 +15,7 @@ class ExamHistory extends Page
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-duplicate';
     protected static ?string $navigationLabel = '卷子历史';
     protected static string|UnitEnum|null $navigationGroup = '管理';
-    protected static ?int $navigationSort = 3;
+    protected static ?int $navigationSort = 14;
 
     protected string $view = 'filament.pages.exam-history-simple';
 
@@ -37,7 +37,8 @@ class ExamHistory extends Page
     {
         try {
             // 从本地数据库读取试卷列表 - 使用 papers 表
-            $query = \App\Models\Paper::query();
+            // 只显示有对应题目数据的试卷
+            $query = \App\Models\Paper::whereHas('questions');
 
             // 应用搜索过滤
             if ($this->search) {
@@ -56,7 +57,8 @@ class ExamHistory extends Page
 
             // 分页
             $total = $query->count();
-            $papers = $query->orderBy('created_at', 'desc')
+            $papers = $query->withCount('questions')
+                ->orderBy('created_at', 'desc')
                 ->skip(($this->currentPage - 1) * $this->perPage)
                 ->take($this->perPage)
                 ->get()
@@ -64,7 +66,7 @@ class ExamHistory extends Page
                     return [
                         'id' => $paper->paper_id,
                         'paper_name' => $paper->paper_name,
-                        'question_count' => $paper->question_count,
+                        'question_count' => $paper->questions_count, // 使用实际的题目数量
                         'total_score' => $paper->total_score,
                         'difficulty_category' => $paper->difficulty_category,
                         'status' => $paper->status,
@@ -115,8 +117,36 @@ class ExamHistory extends Page
             return;
         }
 
-        $questionBankService = app(QuestionBankService::class);
-        $this->selectedExamDetail = $questionBankService->getExamById($this->selectedExamId) ?? [];
+        // 从本地数据库获取试卷详情,而不是外部API
+        $paper = \App\Models\Paper::with(['questions' => function($query) {
+            $query->orderBy('question_number');
+        }])->find($this->selectedExamId);
+
+        if ($paper) {
+            $this->selectedExamDetail = [
+                'paper_id' => $paper->paper_id,
+                'paper_name' => $paper->paper_name,
+                'question_count' => $paper->questions->count(), // 使用实际题目数量
+                'total_score' => $paper->total_score,
+                'difficulty_category' => $paper->difficulty_category,
+                'status' => $paper->status,
+                'created_at' => $paper->created_at,
+                'updated_at' => $paper->updated_at,
+                'questions' => $paper->questions->map(function($question) {
+                    return [
+                        'id' => $question->id,
+                        'question_number' => $question->question_number,
+                        'question_bank_id' => $question->question_bank_id,
+                        'question_type' => $question->question_type,
+                        'score' => $question->score,
+                        'knowledge_point' => $question->knowledge_point,
+                        'difficulty' => $question->difficulty,
+                    ];
+                })->toArray(),
+            ];
+        } else {
+            $this->selectedExamDetail = [];
+        }
     }
 
     public function exportPdf(string $examId)

+ 2 - 2
app/Filament/Pages/KnowledgeGraphManagement.php

@@ -16,9 +16,9 @@ use Illuminate\Support\Facades\Storage;
 class KnowledgeGraphManagement extends Page
 {
     protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
-    protected static string|\UnitEnum|null $navigationGroup = '资源';
+    protected static string|\UnitEnum|null $navigationGroup = '管理';
     protected static ?string $navigationLabel = '知识图谱管理';
-    protected static ?int $navigationSort = 6;
+    protected static ?int $navigationSort = 17;
     protected static ?string $title = '知识图谱管理';
     protected string $view = 'filament.pages.knowledge-graph-management';
 

+ 1 - 1
app/Filament/Pages/KnowledgeGraphVisualization.php

@@ -10,7 +10,7 @@ class KnowledgeGraphVisualization extends Page
 {
     protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-share';
     protected static string|\UnitEnum|null $navigationGroup = '资源';
-    protected static ?int $navigationSort = 2;
+    protected static ?int $navigationSort = 28;
     protected static ?string $navigationLabel = '知识图谱可视化';
     protected static ?string $title = '知识图谱可视化';
     protected string $view = 'filament.pages.knowledge-graph-visualization-simple';

+ 1 - 1
app/Filament/Pages/KnowledgeMindmap.php

@@ -12,7 +12,7 @@ class KnowledgeMindmap extends Page
 
     protected static string|UnitEnum|null $navigationGroup = '资源';
 
-    protected static ?int $navigationSort = 3;
+    protected static ?int $navigationSort = 29;
 
     protected static ?string $navigationLabel = '知识图谱脑图';
 

+ 1 - 1
app/Filament/Pages/KnowledgePoints.php

@@ -19,7 +19,7 @@ class KnowledgePoints extends Page
 
     protected static ?string $navigationLabel = '知识点总览';
 
-    protected static ?int $navigationSort = 5;
+    protected static ?int $navigationSort = 30;
 
     protected string $view = 'filament.pages.knowledge-points';
 

+ 2 - 2
app/Filament/Pages/KnowledgeRelationManagement.php

@@ -8,8 +8,8 @@ use Filament\Pages\Page;
 class KnowledgeRelationManagement extends Page
 {
     protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-link';
-    protected static string|\UnitEnum|null $navigationGroup = '资源';
-    protected static ?int $navigationSort = 4;
+    protected static string|\UnitEnum|null $navigationGroup = '管理';
+    protected static ?int $navigationSort = 18;
     protected static ?string $navigationLabel = '关联关系管理';
     protected static ?string $title = '关联关系管理';
     protected string $view = 'filament.pages.knowledge-relation-management';

+ 1 - 1
app/Filament/Pages/OCRRecordList.php

@@ -20,7 +20,7 @@ class OCRRecordList extends Page
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-camera';
     protected static ?string $navigationLabel = 'OCR识别记录';
     protected static string|UnitEnum|null $navigationGroup = '管理';
-    protected static ?int $navigationSort = 2;
+    protected static ?int $navigationSort = 13;
     protected static ?string $slug = 'ocr-records';
     protected string $view = 'filament.pages.ocr-record-list';
 

+ 95 - 29
app/Filament/Pages/OCRRecordView.php

@@ -23,6 +23,9 @@ class OCRRecordView extends Page
     public string $recordId = '';
     public array $manualAnswers = [];
     public bool $hasAnalysisResults = false;
+    
+    // 新增:判卷相关
+    public array $questionGrades = []; // 存储每道题的评分 [question_id => ['score' => x, 'is_correct' => true/false]]
 
     #[Computed]
     public function record(): ?OCRRecord
@@ -52,6 +55,14 @@ class OCRRecordView extends Page
                 if ($question->manual_answer) {
                     $this->manualAnswers[$question->id] = $question->manual_answer;
                 }
+                
+                // 加载已有的评分
+                if ($question->ai_score !== null || $question->score_value !== null) {
+                    $this->questionGrades[$question->id] = [
+                        'score' => $question->ai_score ?? $question->score_value,
+                        'is_correct' => $question->is_correct,
+                    ];
+                }
             }
 
             // 检查是否已有AI分析结果
@@ -72,7 +83,7 @@ class OCRRecordView extends Page
 
     /**
      * Submit all questions for AI analysis.
-     * Updates manual answers in batch, then sends data to LearningAnalytics.
+     * Updates manual answers in batch, then sends data to LearningAnalytics using unified interface.
      */
     public function submitForAnalysis(): void
     {
@@ -97,40 +108,95 @@ class OCRRecordView extends Page
             }
         }
 
-        // Call LearningAnalytics API
-        $client = new \App\Services\LearningAnalyticsClient();
-        $results = $client->analyze($record);
+        // 使用统一接口提交分析
+        try {
+            $learningService = app(\App\Services\LearningAnalyticsService::class);
 
-        if ($results === null) {
-            Notification::make()
-                ->title('分析失败')
-                ->body('无法连接到分析服务或服务返回错误,请检查日志。')
-                ->danger()
-                ->send();
-            return;
-        }
+            // 准备答题数据(与系统卷子格式一致)
+            $answers = [];
+            foreach ($record->questions as $question) {
+                // 使用校准后的答案(manual_answer),如果没有则使用OCR识别的答案
+                $studentAnswer = !empty(trim($question->manual_answer ?? ''))
+                    ? trim($question->manual_answer)
+                    : trim($question->student_answer ?? '');
+
+                $answers[] = [
+                    'question_bank_id' => 'ocr_q' . $question->question_number,
+                    'question_text' => $question->question_text ?? '',
+                    'student_answer' => $studentAnswer,
+                    'is_correct' => null,  // 让AI判断
+                    'score' => null,
+                    'max_score' => $question->score_total ?? null,
+                    'kp_code' => $question->kp_code ?? null,
+                ];
+            }
+
+            // 提交到统一接口
+            $submissionData = [
+                'paper_id' => 'ocr_' . $record->id,
+                'answers' => $answers,
+            ];
+
+            \Log::info('OCRRecordView提交分析(统一接口)', [
+                'record_id' => $record->id,
+                'student_id' => $record->student_id,
+                'question_count' => count($answers)
+            ]);
+
+            $response = $learningService->submitBatchAttempts($record->student_id, $submissionData);
+
+            if (!empty($response) && !isset($response['error'])) {
+                // 从响应中获取analysis_id
+                $analysisId = $response['analysis_id'] ?? $response['data']['analysis_id'] ?? ('batch_' . $record->id . '_' . time());
+
+                // 更新OCR记录的analysis_id和状态
+                $record->update([
+                    'analysis_id' => $analysisId,
+                    'ai_analyzed_at' => now(),
+                    'ai_analysis_count' => count($answers),
+                ]);
 
-        $aiUpdated = 0;
+                \Log::info('OCR分析提交成功', [
+                    'record_id' => $record->id,
+                    'analysis_id' => $analysisId
+                ]);
 
-        // LearningAnalyticsClient已经处理了数据更新,这里只需要更新记录状态
-        if (is_array($results) && count($results) > 0) {
-            $aiUpdated = count($results);
+                // 重新检查分析结果状态
+                $this->checkAnalysisResults($record);
+
+                Notification::make()
+                    ->title('分析完成')
+                    ->body("已更新 {$updatedCount} 道题的答案,已提交 " . count($answers) . " 道题目进行AI分析")
+                    ->success()
+                    ->send();
+
+                // 跳转到分析页面
+                $this->redirect("/admin/exam-analysis?recordId={$record->id}");
+            } else {
+                \Log::error('OCR分析提交失败', [
+                    'record_id' => $record->id,
+                    'response' => $response
+                ]);
 
-            // 更新记录状态为已分析
-            $record->update([
-                'ai_analyzed_at' => now(),
-                'ai_analysis_count' => $aiUpdated,
+                Notification::make()
+                    ->title('分析失败')
+                    ->body('提交AI分析失败:' . ($response['message'] ?? '未知错误'))
+                    ->danger()
+                    ->send();
+            }
+        } catch (\Exception $e) {
+            \Log::error('提交OCR分析异常', [
+                'record_id' => $record->id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
             ]);
 
-            // 重新检查分析结果状态
-            $this->checkAnalysisResults($record);
+            Notification::make()
+                ->title('分析失败')
+                ->body('提交分析时发生异常:' . $e->getMessage())
+                ->danger()
+                ->send();
         }
-
-        Notification::make()
-            ->title('分析完成')
-            ->body("已更新 {$updatedCount} 道题的答案,AI 分析完成 {$aiUpdated} 道题目")
-            ->success()
-            ->send();
     }
 
     public function startRecognition(): void
@@ -161,7 +227,7 @@ class OCRRecordView extends Page
             ->success()
             ->send();
     }
-
+    
     public function getStatusBadgeConfig(string $status): array
     {
         return match ($status) {

+ 2 - 2
app/Filament/Pages/PromptManagement.php

@@ -15,11 +15,11 @@ class PromptManagement extends Page
 {
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chat-bubble-left-right';
 
-    protected static string|UnitEnum|null $navigationGroup = '题库系统';
+    protected static string|UnitEnum|null $navigationGroup = '管理';
 
     protected static ?string $navigationLabel = '提示词管理';
 
-    protected static ?int $navigationSort = 3;
+    protected static ?int $navigationSort = 12;
 
     protected ?string $heading = '提示词管理';
 

+ 2 - 2
app/Filament/Pages/QuestionGeneration.php

@@ -15,8 +15,8 @@ class QuestionGeneration extends Page
     protected static ?string $title = '题目生成';
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-sparkles';
     protected static ?string $navigationLabel = '题目生成';
-    protected static string|UnitEnum|null $navigationGroup = '题库系统';
-    protected static ?int $navigationSort = 1;
+    protected static string|UnitEnum|null $navigationGroup = '资源';
+    protected static ?int $navigationSort = 21;
     protected string $view = 'filament.pages.question-generation';
 
     public ?string $generateKpCode = null;

+ 2 - 2
app/Filament/Pages/QuestionManagement.php

@@ -15,8 +15,8 @@ class QuestionManagement extends Page
     protected static ?string $title = '题库管理';
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
     protected static ?string $navigationLabel = '题库管理';
-    protected static string|UnitEnum|null $navigationGroup = '题库系统';
-    protected static ?int $navigationSort = 2;
+    protected static string|UnitEnum|null $navigationGroup = '管理';
+    protected static ?int $navigationSort = 11;
     protected string $view = 'filament.pages.question-management-simple';
 
     public ?string $search = null;

+ 2 - 2
app/Filament/Pages/SimulatedGrading.php

@@ -20,11 +20,11 @@ class SimulatedGrading extends Page
 
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-check';
 
-    protected static string|UnitEnum|null $navigationGroup = '学习分析';
+    protected static string|UnitEnum|null $navigationGroup = '资源';
 
     protected static ?string $navigationLabel = '专题测试';
 
-    protected static ?int $navigationSort = 2;
+    protected static ?int $navigationSort = 23;
 
     protected ?string $heading = '专题测试';
 

+ 2 - 2
app/Filament/Pages/StudentAnalysis.php

@@ -13,8 +13,8 @@ class StudentAnalysis extends Page
     protected static ?string $title = '学生掌握度分析';
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
     protected static ?string $navigationLabel = '学生分析';
-    protected static string|UnitEnum|null $navigationGroup = '学习分析';
-    protected static ?int $navigationSort = 1;
+    protected static string|UnitEnum|null $navigationGroup = '资源';
+    protected static ?int $navigationSort = 24;
 
     protected string $view = 'filament.pages.student-analysis-simple';
 

+ 2 - 2
app/Filament/Pages/StudentKnowledgeGraphPage.php

@@ -13,9 +13,9 @@ class StudentKnowledgeGraphPage extends Page
 
     protected static ?string $navigationLabel = '学生知识图谱';
 
-    protected static UnitEnum | string | null $navigationGroup = '学习分析';
+    protected static UnitEnum | string | null $navigationGroup = '资源';
 
-    protected static ?int $navigationSort = 2;
+    protected static ?int $navigationSort = 25;
 
     protected string $view = 'filament.pages.student-knowledge-graph-page';
 

+ 1 - 1
app/Filament/Pages/StudentManagement.php

@@ -30,7 +30,7 @@ class StudentManagement extends Page implements HasTable
 
     protected static string|UnitEnum|null $navigationGroup = '管理';
 
-    protected static ?int $navigationSort = 1;
+    protected static ?int $navigationSort = 16;
 
     protected string $view = 'filament.pages.student-management';
 

+ 511 - 7
app/Filament/Pages/UploadExamPaper.php

@@ -31,12 +31,22 @@ class UploadExamPaper extends Page
     public ?string $studentId = null;
     public $uploadedImage = null;
     public bool $isUploading = false;
+    public ?string $paperType = null; // 试卷类型:unit_test, midterm, final, homework, quiz, other
+
+    // 新增:模式选择
+    public string $mode = 'upload'; // 'upload' 或 'select_paper'
+    public ?string $selectedPaperId = null;
+    public array $questionGrades = []; // 存储每道题的评分
 
     public function mount()
     {
         $this->teacherId = null;
         $this->studentId = null;
         $this->uploadedImage = null;
+        $this->paperType = null;
+        $this->mode = 'upload';
+        $this->selectedPaperId = null;
+        $this->questionGrades = [];
     }
 
     #[Computed]
@@ -126,17 +136,266 @@ class UploadExamPaper extends Page
     #[Computed]
     public function recentRecords(): array
     {
-        return OCRRecord::with('student')
-            ->latest()
-            ->take(5)
+        // 1. 获取OCR记录(图片上传)
+        $ocrQuery = OCRRecord::with('student')->latest();
+        
+        // 如果选择了学生,则筛选该学生的记录
+        if (!empty($this->studentId)) {
+            $ocrQuery->where('student_id', $this->studentId);
+        }
+        
+        $ocrRecords = $ocrQuery->take(5)
+            ->get()
+            ->map(function($record) {
+                return [
+                    'type' => 'ocr_upload',
+                    'id' => $record->id,
+                    'record_id' => $record->id,
+                    'paper_id' => null,
+                    'student_id' => $record->student_id,
+                    'student_name' => $record->student?->name ?? $record->student_id,
+                    'paper_type' => $record->paper_type_label,
+                    'paper_name' => $record->image_filename ?: '未命名图片',
+                    'status' => $record->status,
+                    'total_questions' => $record->total_questions,
+                    'processed_questions' => $record->processed_questions ?? 0,
+                    'created_at' => $record->created_at->format('Y-m-d H:i'),
+                    'is_completed' => $record->status === 'completed',
+                ];
+            })->toArray();
+
+        // 2. 获取所有Paper记录(包括草稿和已评分)
+        $paperQuery = \App\Models\Paper::with('student')->latest();
+        
+        // 如果选择了学生,则筛选该学生的记录
+        if (!empty($this->studentId)) {
+            $paperQuery->where('student_id', $this->studentId);
+        }
+        
+        $allPapers = $paperQuery->take(5)
             ->get()
-            ->toArray();
+            ->map(function($paper) {
+                $type = $paper->status === 'completed' ? 'graded_paper' : 'generated';
+                $paperType = $paper->status === 'completed' ? '已评分试卷' : '系统生成试卷';
+                $iconColor = $paper->status === 'completed' ? 'text-green-500' : 'text-blue-500';
+
+                return [
+                    'type' => $type,
+                    'id' => $paper->paper_id,
+                    'record_id' => null,
+                    'paper_id' => $paper->paper_id,
+                    'student_id' => $paper->student_id,
+                    'student_name' => $paper->student?->name ?? $paper->student_id,
+                    'paper_type' => $paperType,
+                    'paper_name' => $paper->paper_name ?? '未命名试卷',
+                    'status' => $paper->status,
+                    'total_questions' => $paper->question_count,
+                    'created_at' => $paper->updated_at->format('Y-m-d H:i'),
+                    'is_completed' => $paper->status === 'completed',
+                    'icon_color' => $iconColor,
+                ];
+            })->toArray();
+
+        // 3. 合并并按时间排序
+        $allRecords = array_merge($ocrRecords, $allPapers);
+        usort($allRecords, function($a, $b) {
+            return strcmp($b['created_at'], $a['created_at']);
+        });
+
+        return array_slice($allRecords, 0, 10);
+    }
+    
+    /**
+     * 获取学生的试卷列表
+     */
+    #[Computed]
+    public function studentPapers(): array
+    {
+        if (empty($this->studentId)) {
+            return [];
+        }
+
+        try {
+            return \App\Models\Paper::where('student_id', $this->studentId)
+                ->withCount('questions') // 添加题目计数
+                ->orderBy('created_at', 'desc')
+                ->take(20)
+                ->get()
+                ->map(function($paper) {
+                    return [
+                        'paper_id' => $paper->paper_id,
+                        'paper_name' => $paper->paper_name ?? '未命名试卷',
+                        'total_questions' => $paper->questions_count ?? $paper->question_count ?? 0,
+                        'total_score' => $paper->total_score ?? 0,
+                        'created_at' => $paper->created_at->format('Y-m-d H:i'),
+                    ];
+                })
+                ->toArray();
+        } catch (\Exception $e) {
+            \Log::error('获取学生试卷列表失败', [
+                'student_id' => $this->studentId,
+                'error' => $e->getMessage()
+            ]);
+            return [];
+        }
+    }
+
+    #[Computed]
+    public function paperTypes(): array
+    {
+        return [
+            '' => '请选择试卷形式',
+            'unit_test' => '单元测试',
+            'midterm' => '期中考试',
+            'final' => '期末考试',
+            'homework' => '家庭作业',
+            'quiz' => '随堂测验',
+            'other' => '其他',
+        ];
+    }
+
+    /**
+     * 获取选中试卷的题目列表
+     */
+    #[Computed]
+    public function selectedPaperQuestions(): array
+    {
+        if (empty($this->selectedPaperId)) {
+            return [];
+        }
+
+        try {
+            // 首先检查试卷是否存在
+            $paper = \App\Models\Paper::find($this->selectedPaperId);
+            if (!$paper) {
+                \Log::warning('未找到指定试卷', ['paper_id' => $this->selectedPaperId]);
+                return [];
+            }
+
+            // 使用关联关系查询题目
+            $paperWithQuestions = \App\Models\Paper::with(['questions' => function($query) {
+                $query->orderBy('question_number');
+            }])->find($this->selectedPaperId);
+
+            $questions = $paperWithQuestions ? $paperWithQuestions->questions : collect([]);
+
+            // 处理数据不一致的情况:如果题目为空但试卷显示有题目
+            if ($questions->isEmpty() && ($paper->question_count ?? 0) > 0) {
+                \Log::warning('试卷显示有题目但实际题目数据缺失', [
+                    'paper_id' => $this->selectedPaperId,
+                    'expected_questions' => $paper->question_count,
+                    'actual_questions' => 0
+                ]);
+
+                // 返回占位题目,让用户知道有数据缺失
+                return [
+                    [
+                        'id' => 'missing_data',
+                        'question_number' => 1,
+                        'question_bank_id' => null,
+                        'question_type' => 'info',
+                        'content' => "⚠️ 数据异常:试卷显示应有 {$paper->question_count} 道题目,但未找到题目数据。这通常是试卷创建过程中断导致的。请联系管理员或重新创建试卷。",
+                        'answer' => '',
+                        'score' => 0,
+                        'is_missing_data' => true
+                    ]
+                ];
+            }
+
+            if ($questions->isEmpty()) {
+                \Log::info('试卷确实没有题目', ['paper_id' => $this->selectedPaperId]);
+                return [
+                    [
+                        'id' => 'no_questions',
+                        'question_number' => 1,
+                        'question_bank_id' => null,
+                        'question_type' => 'info',
+                        'content' => '该试卷暂无题目数据',
+                        'answer' => '',
+                        'score' => 0,
+                        'is_empty' => true
+                    ]
+                ];
+            }
+
+            // 获取题目详情
+            $questionBankService = app(\App\Services\QuestionBankService::class);
+            $questionIds = $questions->pluck('question_bank_id')->filter()->unique()->toArray();
+
+            if (empty($questionIds)) {
+                \Log::info('题目没有关联题库ID', ['paper_id' => $this->selectedPaperId]);
+                // 返回基本的题目信息,不包含题库详情
+                return $questions->map(function($q) {
+                    return [
+                        'id' => $q->id,
+                        'question_number' => $q->question_number,
+                        'question_bank_id' => $q->question_bank_id,
+                        'question_type' => $q->question_type,
+                        'content' => '题目内容未关联到题库',
+                        'answer' => '',
+                        'score' => $q->score ?? 5,
+                    ];
+                })->toArray();
+            }
+
+            $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
+            $questionDetails = collect($questionsResponse['data'] ?? [])->keyBy('id');
+
+            return $questions->map(function($q) use ($questionDetails) {
+                $detail = $questionDetails->get($q->question_bank_id);
+                return [
+                    'id' => $q->id,
+                    'question_number' => $q->question_number,
+                    'question_bank_id' => $q->question_bank_id,
+                    'question_type' => $q->question_type,
+                    'content' => $detail['stem'] ?? '题目内容缺失',
+                    'answer' => $detail['answer'] ?? '',
+                    'score' => $q->score ?? 5,
+                    'kp_code' => $q->knowledge_point, // 从本地数据库获取知识点代码
+                ];
+            })->toArray();
+        } catch (\Exception $e) {
+            \Log::error('获取试卷题目失败', [
+                'paper_id' => $this->selectedPaperId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+            return [
+                [
+                    'id' => 'error',
+                    'question_number' => 1,
+                    'question_bank_id' => null,
+                    'question_type' => 'error',
+                    'content' => '获取题目数据时发生错误:' . $e->getMessage(),
+                    'answer' => '',
+                    'score' => 0,
+                    'is_error' => true
+                ]
+            ];
+        }
     }
 
     public function updatedTeacherId($value): void
     {
         // 当教师选择变化时,清空之前选择的学生
         $this->studentId = null;
+        $this->selectedPaperId = null;
+        $this->questionGrades = [];
+    }
+    
+    public function updatedStudentId($value): void
+    {
+        // 当学生选择变化时,清空已选试卷
+        $this->selectedPaperId = null;
+        $this->questionGrades = [];
+    }
+    
+    public function updatedMode($value): void
+    {
+        // 切换模式时重置相关字段
+        $this->uploadedImage = null;
+        $this->selectedPaperId = null;
+        $this->questionGrades = [];
     }
 
     public function submitUpload(): void
@@ -177,6 +436,7 @@ class UploadExamPaper extends Page
                 'student_id' => $this->studentId,
                 'image_path' => $path,
                 'image_filename' => $filename,
+                'paper_type' => $this->paperType,
                 'status' => 'pending',
                 'total_questions' => 0,
                 'processed_questions' => 0,
@@ -192,15 +452,16 @@ class UploadExamPaper extends Page
             $this->teacherId = null;
             $this->studentId = null;
             $this->uploadedImage = null;
+            $this->paperType = null;
 
             Notification::make()
                 ->title('上传成功')
-                ->body("卷子已上传并开始OCR处理。记录ID: {$record->id}")
+                ->body("卷子已上传并开始OCR处理,正在跳转到校准页面...")
                 ->success()
                 ->send();
 
-            // 刷新最近记录
-            unset($this->recentRecords);
+            // 跳转到OCR记录详情页面进行校准和提交分析
+            $this->redirect("/admin/ocr-record-view/{$record->id}");
 
         } catch (\Exception $e) {
             Notification::make()
@@ -230,4 +491,247 @@ class UploadExamPaper extends Page
     {
         $this->uploadedImage = null;
     }
+    
+    /**
+     * 提交手动评分
+     */
+    public function submitManualGrading(): void
+    {
+        if (!$this->selectedPaperId) {
+            Notification::make()
+                ->title('请选择试卷')
+                ->danger()
+                ->send();
+            return;
+        }
+
+        if (empty($this->questionGrades)) {
+            Notification::make()
+                ->title('请至少为一道题目评分')
+                ->danger()
+                ->send();
+            return;
+        }
+
+        try {
+            // 准备数据发送到 LearningAnalytics
+            $analyticsData = [];
+            
+            // 获取题目详情以便查找kp_code
+            $questionsMap = collect($this->selectedPaperQuestions)->keyBy('id');
+            
+            // 收集需要从API补充信息的题目ID
+            $missingKpCodeQuestionIds = [];
+
+            foreach ($this->questionGrades as $questionId => $grade) {
+                $question = $questionsMap->get($questionId);
+                if (!$question) {
+                    continue;
+                }
+                
+                // 优先使用本地存储的 kp_code
+                if (empty($question['kp_code'])) {
+                    $missingKpCodeQuestionIds[] = $questionId;
+                }
+            }
+            
+            // 如果有缺失 kp_code 的题目,尝试从 API 获取
+            $apiDetailsMap = collect([]);
+            if (!empty($missingKpCodeQuestionIds)) {
+                $questionBankIds = collect($missingKpCodeQuestionIds)
+                    ->map(fn($qId) => $questionsMap->get($qId)['question_bank_id'] ?? null)
+                    ->filter()
+                    ->toArray();
+                    
+                if (!empty($questionBankIds)) {
+                    $questionBankService = app(\App\Services\QuestionBankService::class);
+                    $questionsDetails = $questionBankService->getQuestionsByIds($questionBankIds);
+                    $apiDetailsMap = collect($questionsDetails['data'] ?? [])->keyBy('id');
+                }
+            }
+
+            foreach ($this->questionGrades as $questionId => $grade) {
+                $question = $questionsMap->get($questionId);
+                if (!$question) {
+                    continue;
+                }
+                
+                $kpCode = $question['kp_code'];
+                
+                // 如果本地没有,尝试从API结果中获取
+                if (empty($kpCode)) {
+                    $detail = $apiDetailsMap->get($question['question_bank_id']);
+                    $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null;
+                }
+
+                $analyticsData[] = [
+                    'question_bank_id' => $question['question_bank_id'],
+                    'student_answer' => $grade['student_answer'] ?? '',
+                    'is_correct' => $grade['is_correct'] ?? null,
+                    'score' => $grade['score'] ?? null,
+                    'max_score' => $question['score'],
+                    'kp_code' => $kpCode, // 添加 kp_code
+                ];
+            }
+
+            // 调用 LearningAnalytics 服务
+            $learningAnalyticsService = app(\App\Services\LearningAnalyticsService::class);
+
+            // 步骤0: 保存学生答案到本地数据库 (重要:确保数据持久化)
+            foreach ($this->questionGrades as $questionId => $grade) {
+                \App\Models\PaperQuestion::where('id', $questionId)->update([
+                    'student_answer' => $grade['student_answer'] ?? '',
+                    'is_correct' => $grade['is_correct'] ?? false,
+                    'score_obtained' => $grade['score'] ?? 0,
+                ]);
+            }
+
+            \Log::info('学生答案已保存到数据库', [
+                'student_id' => $this->studentId,
+                'paper_id' => $this->selectedPaperId,
+                'updated_count' => count($this->questionGrades)
+            ]);
+
+            // 步骤1: 保存答题记录到 LearningAnalytics
+            \Log::info('准备调用submitBatchAttempts API', [
+                'student_id' => $this->studentId,
+                'paper_id' => $this->selectedPaperId,
+                'analytics_data_sample' => array_slice($analyticsData, 0, 2) // 记录前2题的数据作为样本
+            ]);
+
+            $result = $learningAnalyticsService->submitBatchAttempts($this->studentId, [
+                'paper_id' => $this->selectedPaperId,
+                'answers' => $analyticsData,
+            ]);
+
+            // 检查API返回结果
+            if (is_array($result) && isset($result['error']) && $result['error']) {
+                throw new \Exception($result['message'] ?? 'API调用失败');
+            }
+
+            if ($result === null || (is_array($result) && empty($result))) {
+                throw new \Exception('API返回空数据');
+            }
+
+            \Log::info('答题记录已保存到学习分析服务', [
+                'student_id' => $this->studentId,
+                'paper_id' => $this->selectedPaperId,
+                'count' => count($analyticsData)
+            ]);
+
+            // 步骤2: 触发 AI 分析(包含掌握度更新和学习报告生成)
+            try {
+                $paper = \App\Models\Paper::find($this->selectedPaperId);
+                
+                // 构造 AI 分析请求数据
+                $analysisQuestions = [];
+                foreach ($this->questionGrades as $questionId => $grade) {
+                    $question = $questionsMap->get($questionId);
+                    if (!$question) {
+                        continue;
+                    }
+                    
+                    $kpCode = $question['kp_code'];
+                    if (empty($kpCode)) {
+                        $detail = $apiDetailsMap->get($question['question_bank_id']);
+                        $kpCode = $detail['kp_code'] ?? $detail['knowledge_point_code'] ?? null;
+                    }
+                    
+                    $analysisQuestions[] = [
+                        'question_id' => $question['question_bank_id'],
+                        'question_number' => (string)$question['question_number'],
+                        'question_text' => $question['content'] ?? '',
+                        'student_answer' => $grade['student_answer'] ?? '',
+                        'correct_answer' => $question['answer'] ?? '',
+                        'kp_code' => $kpCode,
+                        'score_value' => $grade['score'] ?? 0,
+                        'max_score' => $question['score'],
+                        'is_correct' => $grade['is_correct'] ?? false,
+                        'teacher_validated' => true, // 手动评分即为教师验证
+                        'ocr_confidence' => 1.0, // 手动评分置信度为1
+                    ];
+                }
+                
+                $analysisData = [
+                    'exam_id' => $this->selectedPaperId,
+                    'student_id' => $this->studentId,
+                    'ocr_record_id' => 0, // 系统生成卷子没有OCR记录ID
+                    'paper_id' => $this->selectedPaperId,
+                    'teacher_name' => auth()->user()->name ?? 'Teacher',
+                    'analysis_type' => 'mastery',
+                    'questions' => $analysisQuestions,
+                ];
+                
+                // 调用统一的 AI 分析接口
+                \Log::info('准备调用submitOCRAnalysis API', [
+                    'paper_id' => $this->selectedPaperId,
+                    'student_id' => $this->studentId,
+                    'analysis_data_sample' => [
+                        'question_count' => count($analysisQuestions),
+                        'first_question' => $analysisQuestions[0] ?? null
+                    ]
+                ]);
+
+                $analysisResult = $learningAnalyticsService->submitOCRAnalysis($analysisData);
+
+                \Log::info('AI分析已触发', [
+                    'paper_id' => $this->selectedPaperId,
+                    'student_id' => $this->studentId,
+                    'analysis_result_keys' => is_array($analysisResult) ? array_keys($analysisResult) : 'not_array',
+                    'analysis_result' => $analysisResult
+                ]);
+                
+                // 保存 analysis_id 到 Paper 表
+                if (isset($analysisResult['analysis_id'])) {
+                    \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([
+                        'analysis_id' => $analysisResult['analysis_id'],
+                    ]);
+                    
+                    \Log::info('已保存 analysis_id', [
+                        'paper_id' => $this->selectedPaperId,
+                        'analysis_id' => $analysisResult['analysis_id']
+                    ]);
+                }
+                
+            } catch (\Exception $analysisError) {
+                // AI 分析失败不影响主流程
+                \Log::warning('触发AI分析失败', [
+                    'paper_id' => $this->selectedPaperId,
+                    'error' => $analysisError->getMessage()
+                ]);
+            }
+
+            // 更新Paper表状态为已完成评分
+            \App\Models\Paper::where('paper_id', $this->selectedPaperId)->update([
+                'status' => 'completed',
+                'completed_at' => now(),
+            ]);
+
+            Notification::make()
+                ->title('提交成功')
+                ->body('评分已提交,AI分析正在进行中')
+                ->success()
+                ->send();
+
+            // 刷新最近记录列表
+            unset($this->recentRecords);
+
+            // 重置表单
+            $this->selectedPaperId = null;
+            $this->questionGrades = [];
+
+        } catch (\Exception $e) {
+            \Log::error('提交手动评分失败', [
+                'error' => $e->getMessage(),
+                'student_id' => $this->studentId,
+                'paper_id' => $this->selectedPaperId,
+            ]);
+
+            Notification::make()
+                ->title('提交失败')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
+        }
+    }
 }

+ 14 - 0
app/Models/OCRRecord.php

@@ -21,6 +21,7 @@ class OCRRecord extends Model
         'image_width',
         'image_height',
         'qr_code_data',
+        'paper_type',
         'status',
         'error_message',
         'total_questions',
@@ -87,4 +88,17 @@ class OCRRecord extends Model
         }
         return intval(($this->processed_questions / $this->total_questions) * 100);
     }
+
+    public function getPaperTypeLabelAttribute(): string
+    {
+        return match($this->paper_type) {
+            'unit_test' => '单元测试',
+            'midterm' => '期中考试',
+            'final' => '期末考试',
+            'homework' => '家庭作业',
+            'quiz' => '随堂测验',
+            'other' => '其他',
+            default => '未分类',
+        };
+    }
 }

+ 17 - 0
app/Models/Paper.php

@@ -28,6 +28,7 @@ class Paper extends Model
         'analysis_summary',
         'feedback',
         'completed_at',
+        'analysis_id', // AI分析记录ID
     ];
     
     protected $casts = [
@@ -45,4 +46,20 @@ class Paper extends Model
     {
         return $this->hasMany(PaperQuestion::class, 'paper_id', 'paper_id');
     }
+
+    /**
+     * 获取关联的学生
+     */
+    public function student()
+    {
+        return $this->belongsTo(Student::class, 'student_id', 'student_id');
+    }
+
+    /**
+     * 获取关联的教师
+     */
+    public function teacher()
+    {
+        return $this->belongsTo(Teacher::class, 'teacher_id', 'teacher_id');
+    }
 }

+ 8 - 0
app/Models/PaperQuestion.php

@@ -45,4 +45,12 @@ class PaperQuestion extends Model
     {
         return $this->belongsTo(Paper::class, 'paper_id', 'paper_id');
     }
+
+    /**
+     * 获取对应的题库题目
+     */
+    public function questionBank()
+    {
+        return $this->belongsTo(\App\Models\Question::class, 'question_bank_id', 'id');
+    }
 }

+ 231 - 0
app/Services/LatexCleanerService.php

@@ -0,0 +1,231 @@
+<?php
+
+namespace App\Services;
+
+/**
+ * LaTeX 清理服务
+ * 专门用于清理 OCR 识别返回的 LaTeX 公式中的常见错误
+ * 在数据存入数据库之前进行预处理
+ */
+class LatexCleanerService
+{
+    /**
+     * 清理 LaTeX 文本
+     * 
+     * @param string $latex 原始 LaTeX 文本
+     * @return string 清理后的 LaTeX 文本
+     */
+    public function clean(string $latex): string
+    {
+        if (empty($latex)) {
+            return $latex;
+        }
+
+        // 1. 基础清理
+        $latex = $this->basicCleanup($latex);
+        
+        // 2. 空格规范化
+        $latex = $this->normalizeWhitespace($latex);
+        
+        // 3. 清理错误的定界符
+        $latex = $this->cleanDelimiters($latex);
+        
+        // 4. 修复常见的 LaTeX 命令
+        $latex = $this->fixCommonCommands($latex);
+        
+        // 5. 清理括号匹配问题
+        $latex = $this->fixBraces($latex);
+        
+        return trim($latex);
+    }
+
+    /**
+     * 基础清理
+     */
+    protected function basicCleanup(string $latex): string
+    {
+        // 递归解码 HTML 实体
+        $decoded = html_entity_decode($latex, ENT_QUOTES, 'UTF-8');
+        while ($decoded !== $latex) {
+            $latex = $decoded;
+            $decoded = html_entity_decode($latex, ENT_QUOTES, 'UTF-8');
+        }
+        
+        // 移除 HTML 标签
+        $latex = strip_tags($latex);
+        
+        return $latex;
+    }
+
+    /**
+     * 空格规范化 - 处理 OCR 常见的空格问题
+     */
+    protected function normalizeWhitespace(string $latex): string
+    {
+        // 1. 移除 LaTeX 命令后的空格: \frac { -> \frac{
+        $latex = preg_replace('/\\\\([a-zA-Z]+)\s+\{/', '\\\\$1{', $latex);
+        
+        // 2. 移除花括号内的前导和尾随空格: { 1 } -> {1}
+        $latex = preg_replace('/\{\s+/', '{', $latex);
+        $latex = preg_replace('/\s+\}/', '}', $latex);
+        
+        // 2.1 移除闭合花括号后紧跟开放花括号之间的空格: } { -> }{
+        $latex = preg_replace('/\}\s+\{/', '}{', $latex);
+        
+        // 3. 移除上标/下标符号周围的空格: x ^ { a } -> x^{a}
+        $latex = preg_replace('/\s*\^\s*\{\s*/', '^{', $latex);
+        $latex = preg_replace('/\s*_\s*\{\s*/', '_{', $latex);
+        
+        // 4. 移除 \left 和 \right 后的空格: \left ( -> \left(, \right ) -> \right)
+        $latex = preg_replace('/\\\\(left|right)\s+/', '\\\\$1', $latex);
+        
+        // 4.1 特殊处理 \right 和 ) 之间的空格
+        $latex = preg_replace('/\\\\right\s+\)/', '\\right)', $latex);
+        $latex = preg_replace('/\\\\left\s+\(/', '\\left(', $latex);
+        
+        // 5. 移除括号内侧的空格: ( x ) -> (x)
+        $latex = preg_replace('/\(\s+/', '(', $latex);
+        $latex = preg_replace('/\s+\)/', ')', $latex);
+        
+        // 6. 规范化多个连续空格为单个空格
+        $latex = preg_replace('/\s+/', ' ', $latex);
+        
+        return $latex;
+    }
+
+    /**
+     * 清理错误的定界符 - OCR 常见错误
+     */
+    protected function cleanDelimiters(string $latex): string
+    {
+        // 1. 移除花括号内的 $: {a$} -> {a}
+        $latex = preg_replace('/\{([^}]*)\$+([^}]*)\}/', '{$1$2}', $latex);
+        
+        // 2. 移除末尾的多余 $$$
+        $latex = preg_replace('/\$+\s*$/', '', $latex);
+        
+        // 3. 移除开头的多余 $$$
+        $latex = preg_replace('/^\s*\$+/', '', $latex);
+        
+        // 4. 移除连续的 $$$ (3个或更多) -> $$
+        $latex = preg_replace('/\$\$\$+/', '$$', $latex);
+        
+        // 5. 修复不匹配的定界符
+        // 如果只有一个 $,可能是 OCR 错误,移除它
+        $dollarCount = substr_count($latex, '$');
+        if ($dollarCount === 1) {
+            $latex = str_replace('$', '', $latex);
+        }
+        
+        return $latex;
+    }
+
+    /**
+     * 修复常见的 LaTeX 命令
+     */
+    protected function fixCommonCommands(string $latex): string
+    {
+        // 常见的 LaTeX 命令列表
+        $commands = [
+            'frac', 'sqrt', 'sum', 'int', 'lim', 'prod',
+            'sin', 'cos', 'tan', 'log', 'ln', 'exp',
+            'alpha', 'beta', 'gamma', 'delta', 'theta', 'pi', 'sigma', 'omega',
+            'leq', 'geq', 'neq', 'approx', 'infty', 'partial',
+            'times', 'div', 'pm', 'mp', 'cdot',
+            'left', 'right', 'big', 'Big', 'bigg', 'Bigg'
+        ];
+        
+        // 为缺少反斜杠的命令添加反斜杠
+        foreach ($commands as $cmd) {
+            // 匹配单词边界的命令(不是已经有反斜杠的)
+            $pattern = '/(?<!\\\\)\b' . preg_quote($cmd, '/') . '\b/';
+            $latex = preg_replace($pattern, '\\\\' . $cmd, $latex);
+        }
+        
+        // 规范化反斜杠(处理多重转义)
+        $latex = preg_replace('/\\\\+([a-zA-Z])/', '\\\\$1', $latex);
+        
+        return $latex;
+    }
+
+    /**
+     * 修复括号匹配问题
+     */
+    protected function fixBraces(string $latex): string
+    {
+        // 统计花括号数量
+        $openCount = substr_count($latex, '{');
+        $closeCount = substr_count($latex, '}');
+        
+        // 如果不匹配,尝试修复
+        if ($openCount > $closeCount) {
+            // 缺少闭合括号,在末尾添加
+            $latex .= str_repeat('}', $openCount - $closeCount);
+        } elseif ($closeCount > $openCount) {
+            // 多余的闭合括号,移除末尾的
+            $diff = $closeCount - $openCount;
+            for ($i = 0; $i < $diff; $i++) {
+                $latex = preg_replace('/\}\s*$/', '', $latex, 1);
+            }
+        }
+        
+        return $latex;
+    }
+
+    /**
+     * 批量清理文本数组
+     * 
+     * @param array $texts 文本数组
+     * @param array $keys 需要清理的键名
+     * @return array 清理后的数组
+     */
+    public function cleanArray(array $texts, array $keys = ['content', 'question_text', 'student_answer', 'answer']): array
+    {
+        foreach ($texts as &$item) {
+            if (is_array($item)) {
+                foreach ($keys as $key) {
+                    if (isset($item[$key]) && is_string($item[$key])) {
+                        $item[$key] = $this->clean($item[$key]);
+                    }
+                }
+            }
+        }
+        
+        return $texts;
+    }
+
+    /**
+     * 验证清理后的 LaTeX 是否有效
+     * 
+     * @param string $latex 清理后的 LaTeX
+     * @return array ['valid' => bool, 'errors' => array]
+     */
+    public function validate(string $latex): array
+    {
+        $errors = [];
+        
+        // 检查括号匹配
+        if (substr_count($latex, '{') !== substr_count($latex, '}')) {
+            $errors[] = '花括号不匹配';
+        }
+        
+        if (substr_count($latex, '(') !== substr_count($latex, ')')) {
+            $errors[] = '圆括号不匹配';
+        }
+        
+        if (substr_count($latex, '[') !== substr_count($latex, ']')) {
+            $errors[] = '方括号不匹配';
+        }
+        
+        // 检查定界符匹配
+        $dollarCount = substr_count($latex, '$');
+        if ($dollarCount % 2 !== 0) {
+            $errors[] = '$ 定界符不匹配';
+        }
+        
+        return [
+            'valid' => empty($errors),
+            'errors' => $errors
+        ];
+    }
+}

+ 299 - 0
app/Services/LearningAnalyticsService.php

@@ -27,9 +27,20 @@ class LearningAnalyticsService
             $endpoint = $kpCode 
                 ? "/api/v1/mastery/student/{$studentId}/kp/{$kpCode}"
                 : "/api/v1/mastery/student/{$studentId}";
+            
+            Log::info('LearningAnalytics Request: Get Student Mastery', [
+                'endpoint' => $endpoint,
+                'student_id' => $studentId,
+                'kp_code' => $kpCode
+            ]);
 
             $response = Http::timeout($this->timeout)->get($this->baseUrl . $endpoint);
             
+            Log::info('LearningAnalytics Response: Get Student Mastery', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+            
             if ($response->successful()) {
                 return $response->json();
             }
@@ -63,9 +74,19 @@ class LearningAnalyticsService
     public function updateMastery(array $data): array
     {
         try {
+            Log::info('LearningAnalytics Request: Update Mastery', [
+                'url' => $this->baseUrl . '/api/v1/mastery/student/' . $data['student_id'] . '/update',
+                'data' => $data
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->post($this->baseUrl . '/api/v1/mastery/student/' . $data['student_id'] . '/update', $data);
 
+            Log::info('LearningAnalytics Response: Update Mastery', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
             if ($response->successful()) {
                 return $response->json();
             }
@@ -186,9 +207,18 @@ class LearningAnalyticsService
     public function getLearningRecommendations(string $studentId): array
     {
         try {
+            Log::info('LearningAnalytics Request: Get Learning Recommendations', [
+                'url' => $this->baseUrl . "/api/v1/learning-path/student/{$studentId}/recommend"
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->get($this->baseUrl . "/api/v1/learning-path/student/{$studentId}/recommend");
 
+            Log::info('LearningAnalytics Response: Get Learning Recommendations', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
             if ($response->successful()) {
                 return $response->json();
             }
@@ -206,9 +236,19 @@ class LearningAnalyticsService
     {
         try {
             $kgBaseUrl = config('services.knowledge_api.base_url', 'http://localhost:5011');
+            Log::info('LearningAnalytics Request: Get Knowledge Points', [
+                'url' => $kgBaseUrl . '/knowledge-points/',
+                'filters' => $filters
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->get($kgBaseUrl . '/knowledge-points/', $filters);
 
+            Log::info('LearningAnalytics Response: Get Knowledge Points', [
+                'status' => $response->status(),
+                'count' => count($response->json()['data'] ?? [])
+            ]);
+
             if ($response->successful()) {
                 return $response->json()['data'] ?? [];
             }
@@ -229,9 +269,18 @@ class LearningAnalyticsService
     public function getStudentSkillProficiency(string $studentId): array
     {
         try {
+            Log::info('LearningAnalytics Request: Get Student Skill Proficiency', [
+                'url' => $this->baseUrl . "/api/v1/skill/proficiency/student/{$studentId}"
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->get($this->baseUrl . "/api/v1/skill/proficiency/student/{$studentId}");
 
+            Log::info('LearningAnalytics Response: Get Student Skill Proficiency', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
             if ($response->successful()) {
                 return $response->json();
             }
@@ -277,9 +326,18 @@ class LearningAnalyticsService
     public function getKnowledgeDependencies(): array
     {
         try {
+            Log::info('LearningAnalytics Request: Get Knowledge Dependencies', [
+                'url' => $this->baseUrl . '/knowledge-dependencies/'
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->get($this->baseUrl . '/knowledge-dependencies/');
 
+            Log::info('LearningAnalytics Response: Get Knowledge Dependencies', [
+                'status' => $response->status(),
+                'count' => count($response->json()['data'] ?? [])
+            ]);
+
             if ($response->successful()) {
                 return $response->json()['data'] ?? [];
             }
@@ -300,9 +358,19 @@ class LearningAnalyticsService
     public function submitAttempt(string $studentId, array $attemptData): array
     {
         try {
+            Log::info('LearningAnalytics Request: Submit Attempt', [
+                'url' => $this->baseUrl . "/api/v1/attempts/student/{$studentId}",
+                'data' => $attemptData
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->post($this->baseUrl . "/api/v1/attempts/student/{$studentId}", $attemptData);
 
+            Log::info('LearningAnalytics Response: Submit Attempt', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
             if ($response->successful()) {
                 return $response->json();
             }
@@ -332,6 +400,151 @@ class LearningAnalyticsService
         }
     }
 
+    /**
+     * 批量提交学生答题记录
+     */
+    public function submitBatchAttempts(string $studentId, array $data): array
+    {
+        try {
+            Log::info('LearningAnalytics Request: Submit Batch Attempts', [
+                'url' => $this->baseUrl . "/api/v1/attempts/batch/student/{$studentId}",
+                'data_count' => count($data['answers'] ?? []),
+                'paper_id' => $data['paper_id'] ?? null
+            ]);
+
+            $response = Http::timeout($this->timeout)
+                ->post($this->baseUrl . "/api/v1/attempts/batch/student/{$studentId}", $data);
+
+            Log::info('LearningAnalytics Response: Submit Batch Attempts', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::error('Submit Batch Attempts Error', [
+                'student_id' => $studentId,
+                'data_count' => count($data['answers'] ?? []),
+                'status' => $response->status(),
+                'response' => $response->body()
+            ]);
+
+            return [
+                'error' => true,
+                'message' => 'Failed to submit batch attempts: ' . $response->body()
+            ];
+        } catch (\Exception $e) {
+            Log::error('Submit Batch Attempts Exception', [
+                'student_id' => $studentId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
+        }
+    }
+
+    /**
+     * 提交OCR分析请求
+     */
+    public function submitOCRAnalysis(array $data): array
+    {
+        try {
+            Log::info('Sending OCR results to LearningAnalytics', [
+                'student_id' => $data['student_id'] ?? 'unknown',
+                'exam_id' => $data['exam_id'] ?? 'unknown',
+                'question_count' => count($data['questions'] ?? [])
+            ]);
+
+            $response = Http::timeout(30) // 分析可能需要较长时间
+                ->post($this->baseUrl . '/api/analysis/process-answers', $data);
+
+            Log::info('LearningAnalytics Response: Submit OCR Analysis', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
+            if ($response->successful()) {
+                Log::info('Analysis submitted successfully', [
+                    'analysis_id' => $response->json('analysis_id')
+                ]);
+                return $response->json();
+            }
+
+            Log::error('Submit OCR Analysis Error', [
+                'status' => $response->status(),
+                'response' => $response->body(),
+                'data_preview' => array_merge($data, ['questions' => count($data['questions'])])
+            ]);
+
+            return [
+                'error' => true,
+                'message' => 'Failed to submit analysis: ' . $response->body()
+            ];
+        } catch (\Exception $e) {
+            Log::error('Submit OCR Analysis Exception', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
+        }
+    }
+    
+    /**
+     * 获取分析结果详情
+     */
+    public function getAnalysisResult(string $analysisId): array
+    {
+        try {
+            $endpoint = "/api/analysis/analysis/{$analysisId}";
+            
+            Log::info('LearningAnalytics Request: Get Analysis Result', [
+                'endpoint' => $endpoint,
+                'analysis_id' => $analysisId
+            ]);
+
+            $response = Http::timeout($this->timeout)->get($this->baseUrl . $endpoint);
+            
+            Log::info('LearningAnalytics Response: Get Analysis Result', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+            
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::error('Get Analysis Result Error', [
+                'analysis_id' => $analysisId,
+                'status' => $response->status(),
+                'response' => $response->body()
+            ]);
+
+            return [
+                'error' => true,
+                'message' => 'Failed to fetch analysis result'
+            ];
+        } catch (\Exception $e) {
+            Log::error('Get Analysis Result Exception', [
+                'analysis_id' => $analysisId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
+        }
+    }
+
     /**
      * 检查服务健康状态
      */
@@ -464,9 +677,18 @@ class LearningAnalyticsService
     public function getStudentPredictions(string $studentId, int $count = 5): array
     {
         try {
+            Log::info('LearningAnalytics Request: Get Student Predictions', [
+                'url' => $this->baseUrl . "/api/v1/prediction/student/{$studentId}?count={$count}"
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->get($this->baseUrl . "/api/v1/prediction/student/{$studentId}?count={$count}");
 
+            Log::info('LearningAnalytics Response: Get Student Predictions', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
             if ($response->successful()) {
                 $data = $response->json();
                 $predictions = $data['predictions'] ?? $data['data'] ?? [];
@@ -495,9 +717,18 @@ class LearningAnalyticsService
     public function getStudentLearningPaths(string $studentId, int $count = 3): array
     {
         try {
+            Log::info('LearningAnalytics Request: Get Student Learning Paths', [
+                'url' => $this->baseUrl . "/api/v1/learning-path/student/{$studentId}?count={$count}"
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->get($this->baseUrl . "/api/v1/learning-path/student/{$studentId}?count={$count}");
 
+            Log::info('LearningAnalytics Response: Get Student Learning Paths', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
             if ($response->successful()) {
                 $data = $response->json()['data'] ?? [];
                 return [
@@ -708,9 +939,18 @@ class LearningAnalyticsService
     public function recommendLearningPaths(string $studentId, int $count = 3): array
     {
         try {
+            Log::info('LearningAnalytics Request: Recommend Learning Paths', [
+                'url' => $this->baseUrl . "/api/v1/learning-path/recommend/{$studentId}?count={$count}"
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->get($this->baseUrl . "/api/v1/learning-path/recommend/{$studentId}?count={$count}");
 
+            Log::info('LearningAnalytics Response: Recommend Learning Paths', [
+                'status' => $response->status(),
+                'body' => $response->json()
+            ]);
+
             if ($response->successful()) {
                 $data = $response->json()['data'] ?? [];
                 return [
@@ -738,12 +978,22 @@ class LearningAnalyticsService
     public function recalculateMastery(string $studentId, string $kpCode): bool
     {
         try {
+            Log::info('LearningAnalytics Request: Recalculate Mastery', [
+                'url' => $this->baseUrl . "/api/v1/mastery/recalculate/{$studentId}",
+                'kp_code' => $kpCode
+            ]);
+
             $response = Http::timeout($this->timeout)
                 ->post($this->baseUrl . "/api/v1/mastery/recalculate/{$studentId}", [
                     'student_id' => $studentId,
                     'kp_code' => $kpCode
                 ]);
 
+            Log::info('LearningAnalytics Response: Recalculate Mastery', [
+                'status' => $response->status(),
+                'body' => $response->body()
+            ]);
+
             return $response->successful();
         } catch (\Exception $e) {
             Log::error('Recalculate Mastery Error', [
@@ -1293,4 +1543,53 @@ class LearningAnalyticsService
 
         return $questions;
     }
+    
+    /**
+     * 提交手动评分结果到 LearningAnalytics
+     * 
+     * @param array $data 包含 student_id, paper_id, grades 的数组
+     * @return array
+     */
+    public function submitManualGrading(array $data): array
+    {
+        try {
+            $response = Http::timeout($this->timeout)
+                ->post($this->baseUrl . '/api/ocr/analyze', [
+                    'student_id' => $data['student_id'],
+                    'paper_id' => $data['paper_id'],
+                    'answers' => $data['grades'],
+                ]);
+
+            if ($response->successful()) {
+                Log::info('Manual grading submitted successfully', [
+                    'student_id' => $data['student_id'],
+                    'paper_id' => $data['paper_id'],
+                    'question_count' => count($data['grades'])
+                ]);
+                
+                return $response->json();
+            }
+
+            Log::error('Submit Manual Grading Error', [
+                'data' => $data,
+                'status' => $response->status(),
+                'response' => $response->body()
+            ]);
+
+            return [
+                'error' => true,
+                'message' => 'Failed to submit manual grading'
+            ];
+        } catch (\Exception $e) {
+            Log::error('Submit Manual Grading Exception', [
+                'error' => $e->getMessage(),
+                'data' => $data
+            ]);
+
+            return [
+                'error' => true,
+                'message' => $e->getMessage()
+            ];
+        }
+    }
 }

+ 34 - 2
app/Services/MathFormulaProcessor.php

@@ -44,14 +44,46 @@ class MathFormulaProcessor
         $pattern = '/(?<!\\\\)\b(' . implode('|', $commands) . ')\b/';
         $content = preg_replace($pattern, '\\\\$1', $content);
 
-        // 4. 处理定界符
+        // 4. 规范化 LaTeX 命令中的空格 (OCR 常见问题)
+        // 4.1 移除 LaTeX 命令后的空格: \frac { -> \frac{
+        $content = preg_replace('/\\\\([a-zA-Z]+)\s+\{/', '\\\\$1{', $content);
+        
+        // 4.2 移除花括号内的前导和尾随空格: { 1 } -> {1}
+        $content = preg_replace('/\{\s+/', '{', $content);
+        $content = preg_replace('/\s+\}/', '}', $content);
+        
+        // 4.3 移除上标/下标符号周围的空格: x ^ { a } -> x^{a}
+        $content = preg_replace('/\s*\^\s*\{\s*/', '^{', $content);
+        $content = preg_replace('/\s*_\s*\{\s*/', '_{', $content);
+        
+        // 4.4 移除 \left 和 \right 后的空格: \left ( -> \left(
+        $content = preg_replace('/\\\\(left|right)\s+/', '\\\\$1', $content);
+        
+        // 4.5 移除括号内侧的空格: ( x ) -> (x)
+        $content = preg_replace('/\(\s+/', '(', $content);
+        $content = preg_replace('/\s+\)/', ')', $content);
+        
+        // 4.6 规范化多个连续空格为单个空格
+        $content = preg_replace('/\s+/', ' ', $content);
+        
+        // 4.7 清理 OCR 错误产生的多余 $ 符号
+        // 移除花括号内的 $: {a$} -> {a}
+        $content = preg_replace('/\{([^}]*)\$+([^}]*)\}/', '{$1$2}', $content);
+        // 移除末尾的多余 $$$
+        $content = preg_replace('/\$+\s*$/', '', $content);
+        // 移除开头的多余 $$$
+        $content = preg_replace('/^\s*\$+/', '', $content);
+        // 移除连续的 $$$ (3个或更多)
+        $content = preg_replace('/\$\$\$+/', '$$', $content);
+
+        // 5. 处理定界符
         // 如果内容已经是完整的公式(被 $ 或 $$ 包裹),则保持原样
         if (self::hasDelimiters($content)) {
             $content = self::cleanInsideDelimiters($content);
             return $content;
         }
 
-        // 5. 智能包装 (统一处理混合内容)
+        // 6. 智能包装 (统一处理混合内容)
         // 无论是纯文本还是富文本,都使用智能识别来包裹公式
         // 这能同时处理:
         // - "已知函数 f(x) = ..." (未包裹的混合内容)

+ 79 - 3
app/Services/OCRService.php

@@ -12,10 +12,12 @@ use Illuminate\Support\Str;
 class OCRService
 {
     protected $ocrDriver;
+    protected $learningAnalyticsService;
 
-    public function __construct()
+    public function __construct(LearningAnalyticsService $learningAnalyticsService)
     {
         $this->ocrDriver = \App\Services\OCR\OCRFactory::create();
+        $this->learningAnalyticsService = $learningAnalyticsService;
     }
 
     /**
@@ -209,14 +211,33 @@ class OCRService
         // Get matched questions from two-pass OCR
         $questions = $result['questions'] ?? [];
 
+        // 使用 LaTeX 清理服务预处理所有公式
+        $latexCleaner = app(\App\Services\LatexCleanerService::class);
+        $questions = $latexCleaner->cleanArray($questions, ['content', 'student_answer']);
+        
+        \Log::info('LaTeX formulas cleaned', ['question_count' => count($questions)]);
+
         $processedCount = 0;
 
         foreach ($questions as $question) {
+            // 再次确保清理(双重保险)
+            $questionText = $latexCleaner->clean($question['content'] ?? '');
+            $studentAnswer = $latexCleaner->clean($question['student_answer'] ?? '');
+            
+            // 验证清理后的内容
+            $validation = $latexCleaner->validate($questionText);
+            if (!$validation['valid']) {
+                \Log::warning('LaTeX validation warnings', [
+                    'question_number' => $question['question_number'],
+                    'errors' => $validation['errors']
+                ]);
+            }
+            
             OCRQuestionResult::create([
                 'ocr_record_id' => $ocrRecord->id,
                 'question_number' => $question['question_number'],
-                'question_text' => $question['content'] ?? '',
-                'student_answer' => $question['student_answer'] ?? '',
+                'question_text' => $questionText,
+                'student_answer' => $studentAnswer,
                 'score_value' => 0, // Will be filled by AI grading
                 'mark_detected' => null,
                 'score_confidence' => $question['confidence'] ?? 0,
@@ -236,6 +257,61 @@ class OCRService
             'record_id' => $ocrRecord->id,
             'questions_processed' => $processedCount
         ]);
+
+        // 发送分析请求到 LearningAnalytics
+        if ($processedCount > 0) {
+            $this->submitToAnalysis($ocrRecord, $questions);
+        }
+    }
+
+    /**
+     * 提交到分析服务
+     */
+    protected function submitToAnalysis(OCRRecord $ocrRecord, array $questions): void
+    {
+        try {
+            $analysisData = [
+                'exam_id' => $ocrRecord->exam_id,
+                'student_id' => $ocrRecord->student_id,
+                'ocr_record_id' => $ocrRecord->id,
+                'teacher_name' => 'System', // 或者是上传者的名字
+                'analysis_type' => 'mastery',
+                'questions' => array_map(function($q) {
+                    // 优先使用人工校准的答案
+                    $studentAnswer = $q['student_answer'] ?? '';
+                    if (isset($q['manual_answer']) && !empty($q['manual_answer'])) {
+                        $studentAnswer = $q['manual_answer'];
+                    }
+
+                    return [
+                        'question_id' => $q['question_number'], // 使用题号作为临时ID
+                        'question_number' => (string)$q['question_number'],
+                        'kp_code' => $q['kp_code'] ?? null,
+                        'score_value' => $q['score_value'] ?? 0,
+                        'student_answer' => $studentAnswer,
+                        'ocr_confidence' => $q['confidence'] ?? 0,
+                        'question_text' => $q['content'] ?? '', // 传递题目内容供AI分析
+                        'teacher_validated' => $q['answer_verified'] ?? false,
+                    ];
+                }, $questions)
+            ];
+
+            $result = $this->learningAnalyticsService->submitOCRAnalysis($analysisData);
+
+            if (isset($result['success']) && $result['success']) {
+                $ocrRecord->update([
+                    'ai_analyzed_at' => now(),
+                    'ai_analysis_count' => ($ocrRecord->ai_analysis_count ?? 0) + 1
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            \Log::error('Failed to submit to analysis service', [
+                'record_id' => $ocrRecord->id,
+                'error' => $e->getMessage()
+            ]);
+            // 不抛出异常,以免影响OCR流程的完成状态
+        }
     }
 
     /**

+ 184 - 26
app/Services/QuestionBankService.php

@@ -309,28 +309,52 @@ class QuestionBankService
      */
     public function saveExamToDatabase(array $examData): ?string
     {
-        try {
-            // 生成试卷ID
-            $paperId = 'paper_' . time() . '_' . bin2hex(random_bytes(4));
-            
-            // 保存到 papers 表
-            \Illuminate\Support\Facades\DB::table('papers')->insert([
-                'paper_id' => $paperId,
-                'student_id' => $examData['student_id'] ?? '',
-                'teacher_id' => $examData['teacher_id'] ?? '',
+        // 数据完整性检查
+        if (empty($examData['questions'])) {
+            Log::warning('尝试保存没有题目的试卷', [
                 'paper_name' => $examData['paper_name'] ?? '未命名试卷',
-                'paper_type' => 'auto_generated',
-                'question_count' => $examData['total_questions'] ?? 0,
-                'total_score' => $examData['total_score'] ?? 0,
-                'status' => 'draft',
-                'difficulty_category' => $examData['difficulty_category'] ?? '基础',
-                'created_at' => now(),
-                'updated_at' => now(),
+                'student_id' => $examData['student_id'] ?? 'unknown'
             ]);
-            
-            // 如果有题目列表,保存到 paper_questions 表
-            if (!empty($examData['questions'])) {
+            return null;
+        }
+
+        try {
+            // 使用数据库事务确保数据一致性
+            return \Illuminate\Support\Facades\DB::transaction(function () use ($examData) {
+                // 生成试卷ID
+                $paperId = 'paper_' . time() . '_' . bin2hex(random_bytes(4));
+
+                Log::info('开始保存试卷到数据库', [
+                    'paper_id' => $paperId,
+                    'paper_name' => $examData['paper_name'] ?? '未命名试卷',
+                    'question_count' => count($examData['questions'])
+                ]);
+
+                // 使用Laravel模型保存到 papers 表
+                $paper = \App\Models\Paper::create([
+                    'paper_id' => $paperId,
+                    'student_id' => $examData['student_id'] ?? '',
+                    'teacher_id' => $examData['teacher_id'] ?? '',
+                    'paper_name' => $examData['paper_name'] ?? '未命名试卷',
+                    'paper_type' => 'auto_generated',
+                    'question_count' => count($examData['questions']), // 使用实际题目数量
+                    'total_score' => $examData['total_score'] ?? 0,
+                    'status' => 'draft',
+                    'difficulty_category' => $examData['difficulty_category'] ?? '基础',
+                ]);
+
+                // 准备题目数据
+                $questionInsertData = [];
                 foreach ($examData['questions'] as $index => $question) {
+                    // 验证题目基本数据
+                    if (empty($question['stem']) && empty($question['content'])) {
+                        Log::warning('跳过没有内容的题目', [
+                            'paper_id' => $paperId,
+                            'question_index' => $index
+                        ]);
+                        continue;
+                    }
+
                     // 处理难度字段:如果是字符串则转换为数字
                     $difficultyValue = $question['difficulty'] ?? 0.5;
                     if (is_string($difficultyValue)) {
@@ -387,7 +411,7 @@ class QuestionBankService
                         }
                     }
 
-                    \Illuminate\Support\Facades\DB::table('paper_questions')->insert([
+                    $questionInsertData[] = [
                         'id' => $paperId . '_q' . ($index + 1),
                         'paper_id' => $paperId,
                         'question_bank_id' => $question['id'] ?? $question['question_id'] ?? 0,
@@ -397,22 +421,156 @@ class QuestionBankService
                         'score' => $question['score'] ?? 5, // 默认5分
                         'estimated_time' => $question['estimated_time'] ?? 300,
                         'question_number' => $index + 1,
-                    ]);
+                    ];
                 }
-            }
-            
-            Log::info('试卷保存成功', ['paper_id' => $paperId, 'question_count' => count($examData['questions'] ?? [])]);
-            return $paperId;
-            
+
+                // 验证是否有有效的题目数据
+                if (empty($questionInsertData)) {
+                    Log::error('没有有效的题目数据可以保存', ['paper_id' => $paperId]);
+                    throw new \Exception('没有有效的题目数据');
+                }
+
+                // 使用Laravel模型批量插入题目数据
+                \App\Models\PaperQuestion::insert($questionInsertData);
+
+                // 验证插入结果,使用关联关系
+                $insertedQuestionCount = $paper->questions()->count();
+
+                if ($insertedQuestionCount !== count($questionInsertData)) {
+                    throw new \Exception("题目插入数量不匹配:预期 {$insertedQuestionCount},实际 " . count($questionInsertData));
+                }
+
+                Log::info('试卷保存成功', [
+                    'paper_id' => $paperId,
+                    'expected_questions' => count($questionInsertData),
+                    'actual_questions' => $insertedQuestionCount,
+                    'paper_name' => $paper->paper_name
+                ]);
+
+                return $paperId;
+            });
+
         } catch (\Exception $e) {
             Log::error('保存试卷到数据库失败', [
                 'error' => $e->getMessage(),
+                'paper_name' => $examData['paper_name'] ?? '未命名试卷',
+                'student_id' => $examData['student_id'] ?? 'unknown',
+                'question_count' => count($examData['questions'] ?? []),
                 'trace' => $e->getTraceAsString()
             ]);
             return null;
         }
     }
 
+    /**
+     * 检查数据完整性 - 发现没有题目的试卷
+     */
+    public function checkDataIntegrity(): array
+    {
+        try {
+            // 使用Laravel模型查找显示有题目但实际没有题目的试卷
+            $inconsistentPapers = \App\Models\Paper::where('question_count', '>', 0)
+                ->whereDoesntHave('questions')
+                ->get();
+
+            Log::warning('发现数据不一致的试卷', [
+                'count' => $inconsistentPapers->count(),
+                'papers' => $inconsistentPapers->map(function($paper) {
+                    return [
+                        'paper_id' => $paper->paper_id,
+                        'paper_name' => $paper->paper_name,
+                        'expected_questions' => $paper->question_count,
+                        'student_id' => $paper->student_id,
+                        'created_at' => $paper->created_at
+                    ];
+                })->toArray()
+            ]);
+
+            return [
+                'inconsistent_count' => $inconsistentPapers->count(),
+                'papers' => $inconsistentPapers->toArray()
+            ];
+        } catch (\Exception $e) {
+            Log::error('检查数据完整性失败', ['error' => $e->getMessage()]);
+            return ['inconsistent_count' => 0, 'papers' => []];
+        }
+    }
+
+    /**
+     * 清理没有题目的试卷记录
+     */
+    public function cleanupInconsistentPapers(): int
+    {
+        try {
+            return \Illuminate\Support\Facades\DB::transaction(function () {
+                // 使用Laravel模型查找显示有题目但实际没有题目的试卷
+                $inconsistentPapers = \App\Models\Paper::where('question_count', '>', 0)
+                    ->whereDoesntHave('questions')
+                    ->get();
+
+                if ($inconsistentPapers->isEmpty()) {
+                    return 0;
+                }
+
+                // 获取要删除的试卷ID列表
+                $deletedPaperIds = $inconsistentPapers->pluck('paper_id')->toArray();
+
+                // 使用Laravel模型删除这些不一致的试卷记录
+                $deletedCount = \App\Models\Paper::whereIn('paper_id', $deletedPaperIds)->delete();
+
+                Log::info('清理不一致的试卷记录', [
+                    'deleted_count' => $deletedCount,
+                    'deleted_paper_ids' => $deletedPaperIds
+                ]);
+
+                return $deletedCount;
+            });
+        } catch (\Exception $e) {
+            Log::error('清理不一致试卷失败', ['error' => $e->getMessage()]);
+            return 0;
+        }
+    }
+
+    /**
+     * 修复试卷的题目数量统计
+     */
+    public function fixPaperQuestionCounts(): int
+    {
+        try {
+            $fixedCount = 0;
+
+            // 使用Laravel模型获取所有试卷
+            $papers = \App\Models\Paper::all();
+
+            foreach ($papers as $paper) {
+                // 计算实际的题目数量,使用关联关系
+                $actualQuestionCount = $paper->questions()->count();
+
+                // 如果题目数量不匹配,更新试卷
+                if ($paper->question_count !== $actualQuestionCount) {
+                    $paper->update([
+                        'question_count' => $actualQuestionCount,
+                        'updated_at' => now()
+                    ]);
+
+                    $fixedCount++;
+                    Log::info('修复试卷题目数量', [
+                        'paper_id' => $paper->paper_id,
+                        'old_count' => $paper->getOriginal('question_count'),
+                        'new_count' => $actualQuestionCount
+                    ]);
+                }
+            }
+
+            Log::info('试卷题目数量修复完成', ['fixed_count' => $fixedCount]);
+            return $fixedCount;
+
+        } catch (\Exception $e) {
+            Log::error('修复试卷题目数量失败', ['error' => $e->getMessage()]);
+            return 0;
+        }
+    }
+
     /**
      * 获取试卷列表
      */

+ 18 - 0
app/View/Components/ExamAnalysis/Header.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\View\Components\ExamAnalysis;
+
+use Illuminate\View\Component;
+
+class Header extends Component
+{
+    public function __construct(
+        public array $recordData,
+        public ?string $title = null
+    ) {}
+
+    public function render()
+    {
+        return view('components.exam-analysis.header');
+    }
+}

+ 17 - 0
app/View/Components/ExamAnalysis/LearningAnalysis.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\View\Components\ExamAnalysis;
+
+use Illuminate\View\Component;
+
+class LearningAnalysis extends Component
+{
+    public function __construct(
+        public array $analysisData
+    ) {}
+
+    public function render()
+    {
+        return view('components.exam-analysis.learning-analysis');
+    }
+}

+ 17 - 0
app/View/Components/ExamAnalysis/Loading.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\View\Components\ExamAnalysis;
+
+use Illuminate\View\Component;
+
+class Loading extends Component
+{
+    public function __construct(
+        public ?string $message = null
+    ) {}
+
+    public function render()
+    {
+        return view('components.loading');
+    }
+}

+ 17 - 0
app/View/Components/ExamAnalysis/QuestionDetails.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\View\Components\ExamAnalysis;
+
+use Illuminate\View\Component;
+
+class QuestionDetails extends Component
+{
+    public function __construct(
+        public array $questions
+    ) {}
+
+    public function render()
+    {
+        return view('components.exam-analysis.question-details');
+    }
+}

+ 17 - 0
app/View/Components/ExamAnalysis/QuickStats.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\View\Components\ExamAnalysis;
+
+use Illuminate\View\Component;
+
+class QuickStats extends Component
+{
+    public function __construct(
+        public array $recordData
+    ) {}
+
+    public function render()
+    {
+        return view('components.exam-analysis.quick-stats');
+    }
+}

+ 17 - 0
app/View/Components/ExamAnalysis/Recommendations.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\View\Components\ExamAnalysis;
+
+use Illuminate\View\Component;
+
+class Recommendations extends Component
+{
+    public function __construct(
+        public array $recommendations
+    ) {}
+
+    public function render()
+    {
+        return view('components.exam-analysis.recommendations');
+    }
+}

+ 20 - 0
config/components.php

@@ -0,0 +1,20 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | View Component Paths
+    |--------------------------------------------------------------------------
+    |
+    | This option defines the paths that will be scanned for view components.
+    | You may modify this array to specify a different location for your
+    | custom components.
+    |
+    */
+
+    'paths' => [
+        resource_path('views/components'),
+    ],
+
+];

+ 36 - 0
config/view.php

@@ -0,0 +1,36 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | View Storage Paths
+    |--------------------------------------------------------------------------
+    |
+    | Most templating systems load templates from disk. Here you may specify
+    | an array of paths that should be checked for your views. Of course
+    | the usual Laravel view path has already been registered for you.
+    |
+    */
+
+    'paths' => [
+        resource_path('views'),
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Compiled View Path
+    |--------------------------------------------------------------------------
+    |
+    | This option determines where all the compiled Blade templates will be
+    | stored for your application. Typically, this is within the storage
+    | directory. However, as usual, you are free to change this value.
+    |
+    */
+
+    'compiled' => env(
+        'VIEW_COMPILED_PATH',
+        realpath(storage_path('framework/views'))
+    ),
+
+];

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

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

+ 33 - 0
database/migrations/2025_11_24_135849_add_paper_type_to_ocr_records_table.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('ocr_records', function (Blueprint $table) {
+            // 添加试卷形式字段
+            $table->string('paper_type', 32)->nullable()->after('qr_code_data')->comment('试卷形式:unit_test-单元测试, midterm-期中考试, final-期末考试, homework-家庭作业, quiz-随堂测验, other-其他');
+
+            // 添加索引
+            $table->index('paper_type', 'idx_ocr_records_paper_type');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('ocr_records', function (Blueprint $table) {
+            $table->dropIndex('idx_ocr_records_paper_type');
+            $table->dropColumn('paper_type');
+        });
+    }
+};

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

@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('papers', function (Blueprint $table) {
+            $table->string('analysis_id', 100)->nullable()->after('completed_at')->comment('AI分析记录ID');
+            $table->index('analysis_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('papers', function (Blueprint $table) {
+            $table->dropIndex(['analysis_id']);
+            $table->dropColumn('analysis_id');
+        });
+    }
+};

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

@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('ocr_records', function (Blueprint $table) {
+            $table->string('analysis_id', 100)->nullable()->after('status')->comment('AI分析记录ID');
+            $table->index('analysis_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('ocr_records', function (Blueprint $table) {
+            $table->dropIndex(['analysis_id']);
+            $table->dropColumn('analysis_id');
+        });
+    }
+};

+ 248 - 0
resources/views/components/exam-analysis/ARCHITECTURE.md

@@ -0,0 +1,248 @@
+# 试卷分析组件架构
+
+## 项目概述
+
+本项目采用组件化设计,将OCR试卷分析页面拆分为独立的、可复用的组件,提高代码的可维护性和可读性。
+
+## 目录结构
+
+```
+app/View/Components/ExamAnalysis/
+├── Header.php                  # 头部组件类
+├── QuickStats.php              # 快速统计组件类
+├── LearningAnalysis.php        # 学习分析组件类
+├── QuestionDetails.php         # 题目详情组件类
+├── Recommendations.php         # 学习建议组件类
+└── Loading.php                 # 加载状态组件类
+
+resources/views/components/
+├── exam-analysis/
+│   ├── header.blade.php        # 头部模板
+│   ├── quick-stats.blade.php   # 快速统计模板
+│   ├── learning-analysis.blade.php # 学习分析模板
+│   ├── question-details.blade.php  # 题目详情模板
+│   └── recommendations.blade.php   # 学习建议模板
+└── loading.blade.php           # 加载状态模板
+
+resources/views/filament/pages/
+├── exam-analysis-compact.blade.php      # OCR记录紧凑布局
+└── exam-analysis-standard.blade.php     # 系统生成卷子标准布局
+```
+
+## 组件关系图
+
+```
+┌─────────────────────────────────────────────┐
+│              页面布局                          │
+│  ┌─────────────────────────────────────┐  │
+│  │         头部组件 Header              │  │
+│  └─────────────────────────────────────┘  │
+│  ┌─────────────────────────────────────┐  │
+│  │      快速统计组件 QuickStats          │  │
+│  └─────────────────────────────────────┘  │
+│  ┌─────────────────────────────────────┐  │
+│  │    学习分析组件 LearningAnalysis      │  │
+│  └─────────────────────────────────────┘  │
+│  ┌─────────────────────────────────────┐  │
+│  │    题目详情组件 QuestionDetails       │  │
+│  └─────────────────────────────────────┘  │
+│  ┌─────────────────────────────────────┐  │
+│  │     学习建议组件 Recommendations       │  │
+│  └─────────────────────────────────────┘  │
+└─────────────────────────────────────────────┘
+```
+
+## 组件清单
+
+### 1. Header 组件
+- **类文件**: `app/View/Components/ExamAnalysis/Header.php`
+- **模板文件**: `resources/views/components/exam-analysis/header.blade.php`
+- **功能**: 显示页面标题、记录ID、状态信息
+- **属性**:
+  - `recordData` (array) - 记录数据
+  - `title` (string|null) - 可选标题
+
+### 2. Loading 组件
+- **类文件**: `app/View/Components/ExamAnalysis/Loading.php`
+- **模板文件**: `resources/views/components/loading.blade.php`
+- **功能**: 显示加载动画和提示信息
+- **属性**:
+  - `message` (string|null) - 可选加载提示文字
+
+### 3. QuickStats 组件
+- **类文件**: `app/View/Components/ExamAnalysis/QuickStats.php`
+- **模板文件**: `resources/views/components/exam-analysis/quick-stats.blade.php`
+- **功能**: 显示关键指标的迷你卡片
+- **属性**:
+  - `recordData` (array) - 记录数据
+
+### 4. LearningAnalysis 组件
+- **类文件**: `app/View/Components/ExamAnalysis/LearningAnalysis.php`
+- **模板文件**: `resources/views/components/exam-analysis/learning-analysis.blade.php`
+- **功能**: 显示掌握度、进度条、知识点统计
+- **属性**:
+  - `analysisData` (array) - 分析数据
+
+### 5. QuestionDetails 组件
+- **类文件**: `app/View/Components/ExamAnalysis/QuestionDetails.php`
+- **模板文件**: `resources/views/components/exam-analysis/question-details.blade.php`
+- **功能**: 显示题目详情、AI分析结果
+- **属性**:
+  - `questions` (array) - 题目数组
+
+### 6. Recommendations 组件
+- **类文件**: `app/View/Components/ExamAnalysis/Recommendations.php`
+- **模板文件**: `resources/views/components/exam-analysis/recommendations.blade.php`
+- **功能**: 显示学习建议列表
+- **属性**:
+  - `recommendations` (array) - 建议数组
+
+## 布局模板
+
+### 1. 紧凑布局 (exam-analysis-compact.blade.php)
+**适用场景**: OCR记录页面
+**特点**:
+- 页面高度紧凑,节省空间
+- 快速统计卡片展示关键指标
+- 题目详情分析区块详细展示AI分析
+
+**使用示例**:
+```blade
+<x-exam-analysis.header :recordData="$recordData" title="📊 OCR试卷分析" />
+<x-exam-analysis.quick-stats :recordData="$recordData" />
+<x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+<x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+<x-exam-analysis.recommendations :recommendations="$analysisData['recommendations']" />
+```
+
+### 2. 标准布局 (exam-analysis-standard.blade.php)
+**适用场景**: 系统生成卷子
+**特点**:
+- 详细的统计概览卡片
+- 完整的知识点掌握情况
+- 更大的视觉展示空间
+
+**使用示例**:
+```blade
+<x-exam-analysis.header :recordData="$recordData" title="📊 试卷分析报告" />
+<x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+<x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+<x-exam-analysis.recommendations :recommendations="$analysisData['recommendations']" />
+```
+
+## 数据流
+
+### 1. 数据来源
+- **recordData**: 从数据库获取的试卷记录数据
+- **analysisData**: 从API获取的学习分析数据
+- **questions**: 包含题目内容、学生答案、AI分析结果
+- **recommendations**: 根据分析数据生成的学习建议
+
+### 2. 数据传递
+```php
+// ExamAnalysis.php 控制器
+public function mount()
+{
+    $this->loadAnalysisData(); // 加载数据
+}
+
+// 在视图中传递数据
+<x-exam-analysis.header :recordData="$recordData" />
+<x-exam-analysis.quick-stats :recordData="$recordData" />
+<x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+```
+
+## 优势
+
+1. **高可复用性**
+   - 每个组件可以在不同页面中重复使用
+   - 支持多种布局方式
+
+2. **高可维护性**
+   - 组件独立开发和测试
+   - 修改一个组件不影响其他组件
+
+3. **高可读性**
+   - 模板文件更简洁(紧凑布局从200+行减少到25行)
+   - 组件职责单一
+
+4. **高扩展性**
+   - 新功能只需添加新组件
+   - 支持多种主题和样式
+
+5. **一致性**
+   - 所有页面使用相同的组件
+   - 统一的视觉风格和交互
+
+## 扩展指南
+
+### 添加新组件
+
+1. 创建组件类
+```php
+// app/View/Components/ExamAnalysis/NewComponent.php
+<?php
+namespace App\View\Components\ExamAnalysis;
+use Illuminate\View\Component;
+
+class NewComponent extends Component
+{
+    public function __construct(
+        public array $data
+    ) {}
+    public function render()
+    {
+        return view('components.exam-analysis.new-component');
+    }
+}
+```
+
+2. 创建模板文件
+```blade
+<!-- resources/views/components/exam-analysis/new-component.blade.php -->
+<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
+    <!-- 组件内容 -->
+</div>
+```
+
+3. 在页面中使用
+```blade
+<x-exam-analysis.new-component :data="$data" />
+```
+
+### 修改现有组件
+
+1. 找到组件类文件
+2. 修改类属性
+3. 更新模板内容
+4. 重新构建前端
+
+## 测试
+
+运行组件测试:
+```bash
+php artisan tinker --execute="
+\$classes = [
+    'App\\View\\Components\\ExamAnalysis\\Header',
+    'App\\View\\Components\\ExamAnalysis\\QuickStats',
+    'App\\View\\Components\\ExamAnalysis\\LearningAnalysis',
+    'App\\View\\Components\\ExamAnalysis\\QuestionDetails',
+    'App\\View\\Components\\ExamAnalysis\\Recommendations',
+    'App\\View\\Components\\ExamAnalysis\\Loading'
+];
+foreach (\$classes as \$class) {
+    echo class_exists(\$class) ? \"✓ \$class\" . PHP_EOL : \"✗ \$class\" . PHP_EOL;
+}
+"
+```
+
+## 性能优化
+
+1. **组件缓存**: Laravel会自动缓存组件类
+2. **模板缓存**: 使用 `php artisan view:cache` 缓存视图
+3. **懒加载**: 只在需要时渲染组件
+4. **数据缓存**: 缓存API数据减少请求
+
+## 许可证
+
+MIT License

+ 80 - 0
resources/views/components/exam-analysis/CHANGELOG.md

@@ -0,0 +1,80 @@
+# 试卷分析组件库 - 变更日志
+
+## [v1.0.0] - 2025-11-25
+
+### 新增
+- **Header 组件** - 页面头部展示
+- **Loading 组件** - 加载状态显示
+- **QuickStats 组件** - 快速统计卡片
+- **LearningAnalysis 组件** - 学习分析概览
+- **QuestionDetails 组件** - 题目详情分析
+- **Recommendations 组件** - 学习建议列表
+
+### 新增布局模板
+- **exam-analysis-compact.blade.php** - OCR记录紧凑布局
+- **exam-analysis-standard.blade.php** - 系统生成卷子标准布局
+
+### 特性
+- 组件化设计,提高代码复用性
+- 支持紧凑和标准两种布局模式
+- 每个组件可独立使用和维护
+- 包含完整的AI分析结果展示
+- 响应式设计,适配移动端
+
+### 组件属性说明
+
+#### Header
+- `recordData` - 记录数据数组
+- `title` - 可选,页面标题
+
+#### Loading
+- `message` - 可选,加载提示文字
+
+#### QuickStats
+- `recordData` - 记录数据数组
+
+#### LearningAnalysis
+- `analysisData` - 分析数据数组
+
+#### QuestionDetails
+- `questions` - 题目数组
+
+#### Recommendations
+- `recommendations` - 建议数组
+
+### 使用方式
+```blade
+<x-exam-analysis::header :recordData="$recordData" />
+<x-exam-analysis::question-details :questions="$questions" />
+```
+
+### 技术栈
+- Laravel Blade 模板
+- Tailwind CSS
+- Livewire
+- Filament
+
+---
+
+## [v1.1.0] - 待定
+
+### 计划
+- [ ] 添加题目难度标识
+- [ ] 添加答题时间统计
+- [ ] 添加知识点关联图谱
+- [ ] 支持自定义主题色彩
+- [ ] 添加打印友好模式
+
+---
+
+## 贡献指南
+
+1. 组件放在 `resources/views/components/exam-analysis/` 目录
+2. 组件文件名使用 kebab-case
+3. 组件属性使用 camelCase
+4. 保持组件的单一职责原则
+5. 为新功能添加单元测试
+
+## 许可证
+
+MIT License

+ 223 - 0
resources/views/components/exam-analysis/OCR_INTEGRATION.md

@@ -0,0 +1,223 @@
+# OCR记录数据呈现 - 组件化解决方案
+
+## 问题描述
+
+OCR记录(图片上传解析)的题目数据没有在页面上呈现,导致用户无法查看题目详情分析。
+
+## 根本原因
+
+1. **数据存储位置不同**:
+   - 系统生成卷子:题目数据存储在 `paper_questions` 表
+   - OCR记录:题目数据存储在 `OCRQuestionResult` 表
+
+2. **数据加载逻辑缺失**:
+   - 原始 `getQuestions()` 方法只支持 `PaperQuestion` 表
+   - 没有为OCR记录加载题目数据的逻辑
+
+## 解决方案 - 组件化开发
+
+### 1. 新增OCR数据加载方法
+
+在 `app/Filament/Pages/ExamAnalysis.php` 中添加 `getOcrQuestions()` 方法:
+
+```php
+protected function getOcrQuestions(): array
+{
+    $recordId = $this->recordId ?? null;
+    if (!$recordId) {
+        return [];
+    }
+
+    try {
+        // 从OCRQuestionResult表加载题目数据
+        $ocrQuestions = OCRQuestionResult::where('ocr_record_id', $recordId)
+            ->orderBy('question_number')
+            ->get();
+
+        $questions = [];
+
+        foreach ($ocrQuestions as $oq) {
+            // 获取API分析结果(如果有)
+            $aiAnalysis = null;
+            if (!empty($oq->ai_feedback)) {
+                $aiAnalysis = [
+                    'analysis' => $oq->ai_feedback,
+                    'mistake_type' => '',
+                    'mistake_category' => '',
+                    'suggestions' => [$oq->ai_feedback],
+                    'correct_solution' => '',
+                ];
+            }
+
+            // 判断答题是否正确
+            $isCorrect = $oq->answer_verified == 1;
+
+            $questions[] = [
+                'id' => $oq->id,
+                'question_number' => $oq->question_number,
+                'question_bank_id' => 'ocr_' . $oq->question_number,
+                'question_type' => 'unknown',
+                'question_text' => $oq->question_text ?? '题目内容缺失',
+                'content' => $oq->question_text ?? '题目内容缺失',
+                'stem' => $oq->question_text ?? '题目内容缺失',
+                'answer' => $oq->manual_answer ?? '',
+                'reference_answer' => $oq->manual_answer ?? '',
+                'score_total' => 100, // OCR题目默认总分100
+                'score_obtained' => $oq->score_value ?? $oq->ai_score ?? 0,
+                'student_answer' => $oq->student_answer ?? '未作答',
+                'is_correct' => $isCorrect,
+                'kp_code' => $oq->kp_code ?? 'N/A',
+                'ai_analysis' => $aiAnalysis,
+            ];
+        }
+
+        return $questions;
+
+    } catch (\Exception $e) {
+        \Log::error('获取OCR题目数据失败', [
+            'record_id' => $recordId,
+            'error' => $e->getMessage()
+        ]);
+
+        return [];
+    }
+}
+```
+
+### 2. 修改数据加载入口
+
+在 `getQuestions()` 方法中添加OCR支持:
+
+```php
+public function getQuestions(): array
+{
+    // OCR记录:从OCRQuestionResult表加载题目数据
+    if ($this->recordType === 'ocr') {
+        return $this->getOcrQuestions();
+    }
+
+    // 系统生成卷子:从PaperQuestion表加载题目数据
+    // ... 原有的逻辑保持不变
+}
+```
+
+### 3. 在页面加载时预加载题目数据
+
+在 `loadAnalysisData()` 方法中为OCR记录添加题目数据:
+
+```php
+// OCR记录:添加题目统计信息
+$ocrQuestionsCount = OCRQuestionResult::where('ocr_record_id', $this->recordId)->count();
+$this->recordData['total_questions'] = $ocrQuestionsCount;
+$this->recordData['questions'] = $this->getQuestions(); // 提前加载题目数据
+```
+
+## 组件化优势
+
+### 1. 无需修改组件
+所有组件(Header、QuickStats、QuestionDetails等)保持不变,继续使用相同的接口和数据格式。
+
+### 2. 数据源透明化
+- **组件层**:只关心数据格式,不知道数据来源
+- **业务逻辑层**:根据记录类型(OCR vs 系统生成)选择不同的数据源
+- **数据层**:`paper_questions` 表 vs `OCRQuestionResult` 表
+
+### 3. 保持组件职责单一
+- **Header组件**:显示记录信息(不关心数据来源)
+- **QuestionDetails组件**:渲染题目详情(不关心数据来源)
+- **数据加载**:在 `ExamAnalysis` 页面控制器中统一处理
+
+## 数据流向
+
+```
+┌─────────────────────────────────────────────────────┐
+│                  ExamAnalysis 页面                     │
+│  ┌─────────────────────────────────────────────┐  │
+│  │          loadAnalysisData()                 │  │
+│  │  • 加载OCR记录基本信息                      │  │
+│  │  • 调用 getQuestions()                      │  │
+│  │    ↓                                      │  │
+│  │  • getOcrQuestions()                       │  │
+│  │    ↓                                      │  │
+│  │  • 从OCRQuestionResult表查询数据          │  │
+│  │    ↓                                      │  │
+│  │  • 格式化为组件期望的格式                  │  │
+│  │    ↓                                      │  │
+│  │  • 存储到 $recordData['questions']        │  │
+│  └─────────────────────────────────────────────┘  │
+│                      ↓                              │
+│  ┌─────────────────────────────────────────────┐  │
+│  │           组件渲染层                         │  │
+│  │  <x-exam-analysis.header ... />             │  │
+│  │  <x-exam-analysis.question-details          │  │
+│  │       :questions="$recordData['questions']" />│  │
+│  │  <x-exam-analysis.quick-stats ... />        │  │
+│  └─────────────────────────────────────────────┘  │
+└─────────────────────────────────────────────────────┘
+```
+
+## 测试验证
+
+### 数据加载测试
+```bash
+# 测试OCRQuestionResult表查询
+$ ocrQuestions = OCRQuestionResult::where('ocr_record_id', 7)->get();
+# 结果:4道题目
+
+# 测试数据格式化
+$ formatted = getOcrQuestions();
+# 结果:4道题目,格式正确,包含所有必需字段
+```
+
+### 组件兼容性测试
+```bash
+# 验证QuestionDetails组件能接收的数据格式
+✓ Q1: 数据完整
+✓ Q2: 数据完整
+✓ Q3: 数据完整
+✓ Q4: 数据完整
+```
+
+## 页面展示效果
+
+访问 `http://fa.test/admin/exam-analysis?recordId=7`,可以看到:
+
+1. **页面头部**:
+   - 记录ID: 7
+   - 状态: completed
+   - 学生ID: stu_1762395159_4
+
+2. **快速统计卡片**:
+   - 总题目: 4
+   - 已答题: 4
+   - 正确: 4
+   - 错误: 0
+
+3. **题目详情分析**(【题目详情分析】区块):
+   - Q1: 答案B ✓正确,AI分析反馈
+   - Q2: 空白答案 ✓正确,AI分析反馈
+   - Q3: 答案B ✓正确,AI分析反馈
+   - Q4: 空白答案 ✓正确,AI分析反馈
+
+4. **学习建议**:
+   - 显示AI分析的建议内容
+
+## 技术栈
+
+- **后端**:Laravel + Livewire
+- **前端**:Blade模板 + Tailwind CSS
+- **组件化**:Laravel Blade Components
+- **数据存储**:MySQL (OCRQuestionResult表)
+- **AI分析**:LearningAnalytics API
+
+## 总结
+
+通过组件化开发,我们成功地:
+
+1. ✅ 为OCR记录添加了完整的数据加载逻辑
+2. ✅ 保持了组件的通用性和可复用性
+3. ✅ 实现了两种数据源的统一处理
+4. ✅ 确保了【题目详情分析】区块的正常展示
+5. ✅ 提供了完整的AI分析反馈功能
+
+OCR记录的题目数据现在可以正确地在页面上呈现,用户可以查看详细的题目分析结果!

+ 88 - 0
resources/views/components/exam-analysis/README.md

@@ -0,0 +1,88 @@
+# 试卷分析组件库
+
+组件化设计,用于试卷分析页面的各个区块。
+
+## 组件列表
+
+### 1. Header 头部组件
+显示页面标题和基本状态信息。
+```blade
+<x-exam-analysis.header :recordData="$recordData" title="📊 试卷分析报告" />
+```
+
+### 2. Loading 加载状态组件
+显示加载动画和提示信息。
+```blade
+<x-exam-analysis.loading message="正在分析试卷数据..." />
+```
+
+### 3. QuickStats 快速统计组件
+显示关键指标的迷你卡片。
+```blade
+<x-exam-analysis.quick-stats :recordData="$recordData" />
+```
+
+### 4. LearningAnalysis 学习分析组件
+显示整体掌握度、进度条和知识点掌握情况。
+```blade
+<x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+```
+
+### 5. QuestionDetails 题目详情组件
+显示每道题的详细信息和AI分析结果。
+```blade
+<x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+```
+
+### 6. Recommendations 学习建议组件
+显示学习建议列表。
+```blade
+<x-exam-analysis.recommendations :recommendations="$analysisData['recommendations']" />
+```
+
+## 布局模板
+
+### 紧凑布局 (exam-analysis-compact.blade.php)
+适用于OCR记录的快速查看,页面高度紧凑。
+
+### 标准布局 (exam-analysis-standard.blade.php)
+适用于系统生成卷子的详细分析,包含完整的统计概览。
+
+## 使用示例
+
+```blade
+<x-filament-panels::page>
+    <div class="space-y-6">
+        @if($loading)
+            <x-exam-analysis.loading />
+        @else
+            <x-exam-analysis.header :recordData="$recordData" />
+            <x-exam-analysis.quick-stats :recordData="$recordData" />
+            <x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+            <x-exam-analysis.question-details :questions="$questions" />
+            <x-exam-analysis.recommendations :recommendations="$recommendations" />
+        @endif
+    </div>
+</x-filament-panels::page>
+```
+
+## 组件调用语法
+
+### 类名语法(推荐)
+使用Laravel类组件:
+- `<x-exam-analysis.header />` → `App\View\Components\ExamAnalysis\Header`
+- `<x-exam-analysis.quick-stats />` → `App\View\Components\ExamAnalysis\QuickStats`
+
+### dot notation 语法
+直接引用视图组件:
+- `x-exam-analysis.header` → `resources/views/components/exam-analysis/header.blade.php`
+- `x-exam-analysis.quick-stats` → `resources/views/components/exam-analysis/quick-stats.blade.php`
+
+## 优势
+
+1. **可复用性** - 每个组件可以在不同页面中重复使用
+2. **可维护性** - 组件独立维护,修改不影响其他部分
+3. **可读性** - 模板文件更简洁,逻辑清晰
+4. **可测试性** - 每个组件可以独立测试
+5. **一致性** - 确保所有页面的样式和交互保持一致
+

+ 27 - 0
resources/views/components/exam-analysis/header.blade.php

@@ -0,0 +1,27 @@
+@props(['recordData'])
+
+<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
+    <div class="flex items-center justify-between">
+        <div>
+            <h1 class="text-xl font-bold text-gray-900">{{ $title ?? '📊 OCR试卷分析' }}</h1>
+            @if(isset($recordData['id']))
+                <p class="text-sm text-gray-600 mt-1">记录ID: {{ $recordData['id'] }}</p>
+            @endif
+            @if(isset($recordData['paper_name']))
+                <p class="text-sm text-gray-600 mt-1">试卷名称: {{ $recordData['paper_name'] }}</p>
+            @endif
+        </div>
+        <div class="flex gap-2">
+            @if(isset($recordData['status']))
+                <span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
+                    {{ $recordData['status'] }}
+                </span>
+            @endif
+            @if(isset($recordData['student_id']))
+                <span class="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs">
+                    {{ $recordData['student_id'] }}
+                </span>
+            @endif
+        </div>
+    </div>
+</div>

+ 46 - 0
resources/views/components/exam-analysis/learning-analysis.blade.php

@@ -0,0 +1,46 @@
+@props(['analysisData'])
+
+@if(!empty($analysisData))
+<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
+    <div class="flex items-center justify-between mb-3">
+        <h2 class="text-sm font-semibold text-gray-900">📈 学习分析</h2>
+        <span class="text-xs text-gray-500">整体掌握度: {{ number_format(($analysisData['overall_mastery'] ?? 0) * 100, 1) }}%</span>
+    </div>
+
+    <!-- 掌握度进度条 -->
+    <div class="mb-3">
+        <div class="flex justify-between text-xs text-gray-600 mb-1">
+            <span>掌握度</span>
+            <span>{{ number_format(($analysisData['overall_mastery'] ?? 0) * 100, 1) }}%</span>
+        </div>
+        <div class="w-full bg-gray-200 rounded-full h-2">
+            <div class="h-2 rounded-full
+                {{ ($analysisData['overall_mastery'] ?? 0) < 0.6 ? 'bg-red-400' :
+                   (($analysisData['overall_mastery'] ?? 0) < 0.8 ? 'bg-yellow-400' : 'bg-green-400') }}"
+                 style="width: {{ ($analysisData['overall_mastery'] ?? 0) * 100 }}%"></div>
+        </div>
+    </div>
+
+    <!-- 知识点列表 -->
+    @if(isset($analysisData['knowledge_points']) && !empty($analysisData['knowledge_points']))
+    <div>
+        <p class="text-xs font-medium text-gray-700 mb-2">知识点掌握情况:</p>
+        <div class="flex flex-wrap gap-2">
+            @foreach($analysisData['knowledge_points'] as $kp)
+            <div class="border rounded px-2 py-1
+                {{ ($kp['mastery_level'] ?? 0) < 0.6 ? 'border-red-200 bg-red-50' :
+                   (($kp['mastery_level'] ?? 0) < 0.8 ? 'border-yellow-200 bg-yellow-50' :
+                   'border-green-200 bg-green-50') }}">
+                <span class="text-xs font-medium text-gray-900">{{ $kp['kp_code'] ?? 'N/A' }}</span>
+                <span class="text-xs
+                    {{ ($kp['mastery_level'] ?? 0) < 0.6 ? 'text-red-600' :
+                       (($kp['mastery_level'] ?? 0) < 0.8 ? 'text-yellow-600' : 'text-green-600') }}">
+                    {{ number_format(($kp['mastery_level'] ?? 0) * 100, 0) }}%
+                </span>
+            </div>
+            @endforeach
+        </div>
+    </div>
+    @endif
+</div>
+@endif

+ 151 - 0
resources/views/components/exam-analysis/question-details.blade.php

@@ -0,0 +1,151 @@
+@props(['questions'])
+
+@if(isset($questions) && !empty($questions))
+<div class="bg-white rounded-lg shadow-sm border border-gray-200">
+    <div class="p-4 border-b border-gray-200">
+        <h2 class="text-lg font-semibold text-gray-900">📋 题目详情分析</h2>
+    </div>
+    <div class="divide-y divide-gray-200">
+        @foreach($questions as $index => $question)
+        <div class="p-4">
+            <div class="flex items-center justify-between mb-3">
+                <div class="flex items-center space-x-3">
+                    <h4 class="font-medium text-gray-900">第 {{ $question['question_number'] ?? 'N/A' }} 题</h4>
+                    @if(!empty($question['kp_code']))
+                    <span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">{{ $question['kp_code'] }}</span>
+                    @endif
+                </div>
+                <div class="flex items-center space-x-2">
+                    @if(isset($question['score_total']) && $question['score_total'] !== null)
+                    <span class="text-sm text-gray-600">得分: {{ $question['score_obtained'] ?? 0 }} / {{ $question['score_total'] }}</span>
+                    @endif
+                    @if(($question['is_correct'] ?? false))
+                        <span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded">✓ 正确</span>
+                    @elseif(!empty($question['student_answer']) && $question['student_answer'] !== '未作答')
+                        <span class="px-2 py-1 bg-red-100 text-red-800 text-xs rounded">✗ 错误</span>
+                    @else
+                        <span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded">未作答</span>
+                    @endif
+                </div>
+            </div>
+
+            <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
+                <div>
+                    <h5 class="text-sm font-medium text-gray-700 mb-2">📝 题目内容</h5>
+                    <div class="bg-gray-50 rounded p-3">
+                        <p class="text-sm text-gray-600">{{ Str::limit($question['question_text'] ?? 'N/A', 200) }}</p>
+                    </div>
+                </div>
+                <div>
+                    <h5 class="text-sm font-medium text-gray-700 mb-2">
+                        @if(isset($question['answer_comparison']))
+                            ✏️ 答案对比
+                        @else
+                            ✏️ 学生答案
+                        @endif
+                    </h5>
+                    <div class="bg-blue-50 rounded p-3">
+                        @if(isset($question['answer_comparison']))
+                            <div class="space-y-2">
+                                <div>
+                                    <p class="text-xs text-gray-500">学生答案:</p>
+                                    <p class="text-sm text-gray-700">{{ $question['answer_comparison']['student'] }}</p>
+                                </div>
+                                <div class="border-t pt-2">
+                                    <p class="text-xs text-gray-500">正确答案(老师校准):</p>
+                                    <p class="text-sm text-green-700 font-medium">{{ $question['answer_comparison']['correct'] }}</p>
+                                </div>
+                            </div>
+                        @else
+                            <p class="text-sm text-gray-600">{{ $question['student_answer'] ?? '未作答' }}</p>
+                        @endif
+                    </div>
+                </div>
+            </div>
+
+            @if(isset($question['ai_analysis']) && !empty($question['ai_analysis']))
+            <div class="bg-purple-50 border border-purple-200 rounded-lg p-3">
+                <h5 class="text-sm font-medium text-gray-700 mb-2">🤖 AI 分析</h5>
+
+                @if($question['ai_analysis']['analysis'])
+                    <p class="text-sm text-gray-700 mb-2">{{ $question['ai_analysis']['analysis'] }}</p>
+                @endif
+
+                @if($question['is_correct'])
+                    {{-- 正确答题的AI反馈 --}}
+                    @if(isset($question['ai_analysis']['suggestions']) && !empty($question['ai_analysis']['suggestions']))
+                    <div>
+                        <p class="text-xs font-medium text-green-700 mb-1">
+                            @if(isset($question['answer_comparison']))
+                                校准判断正确:
+                            @else
+                                继续加油:
+                            @endif
+                        </p>
+                        <ul class="text-xs text-green-600 space-y-1">
+                            @foreach($question['ai_analysis']['suggestions'] as $suggestion)
+                            @if($suggestion)
+                            <li class="flex items-start">
+                                <span class="mr-1">✓</span>
+                                <span>{{ $suggestion }}</span>
+                            </li>
+                            @endif
+                            @endforeach
+                        </ul>
+                    </div>
+                    @endif
+                @elseif(!empty($question['student_answer']) && $question['student_answer'] !== '未作答')
+                    {{-- 错误答题的AI反馈 --}}
+                    @if($question['ai_analysis']['mistake_type'] || $question['ai_analysis']['mistake_category'])
+                    <div class="grid grid-cols-2 gap-2 mb-2">
+                        @if($question['ai_analysis']['mistake_type'])
+                            <div class="text-xs">
+                                <span class="font-medium text-red-700">错误类型: </span>
+                                <span class="text-gray-600">{{ $question['ai_analysis']['mistake_type'] }}</span>
+                            </div>
+                        @endif
+                        @if($question['ai_analysis']['mistake_category'])
+                            <div class="text-xs">
+                                <span class="font-medium text-red-700">错误类别: </span>
+                                <span class="text-gray-600">{{ $question['ai_analysis']['mistake_category'] }}</span>
+                            </div>
+                        @endif
+                    </div>
+                    @endif
+
+                    @if($question['ai_analysis']['correct_solution'])
+                    <div class="mb-2">
+                        <p class="text-xs font-medium text-green-700 mb-1">正确答案:</p>
+                        <p class="text-xs text-green-600 bg-green-100 rounded p-2">{{ $question['ai_analysis']['correct_solution'] }}</p>
+                    </div>
+                    @endif
+
+                    @if(isset($question['ai_analysis']['suggestions']) && !empty($question['ai_analysis']['suggestions']))
+                    <div>
+                        <p class="text-xs font-medium text-purple-700 mb-1">
+                            @if(isset($question['answer_comparison']))
+                                校准后学习建议:
+                            @else
+                                学习建议:
+                            @endif
+                        </p>
+                        <ul class="text-xs text-purple-600 space-y-1">
+                            @foreach($question['ai_analysis']['suggestions'] as $suggestion)
+                            @if($suggestion)
+                            <li class="flex items-start">
+                                <span class="mr-1">•</span>
+                                <span>{{ $suggestion }}</span>
+                            </li>
+                            @endif
+                            @endforeach
+                        </ul>
+                    </div>
+                    @endif
+                @endif
+            </div>
+            @endif
+        </div>
+        @endforeach
+    </div>
+</div>
+@endif

+ 35 - 0
resources/views/components/exam-analysis/quick-stats.blade.php

@@ -0,0 +1,35 @@
+@props(['recordData'])
+
+<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
+    <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
+        <p class="text-xs text-gray-500">总题目</p>
+        <p class="text-lg font-bold text-gray-900">{{ $recordData['total_questions'] ?? 0 }}</p>
+    </div>
+
+    @php
+        // 已答题:排除"未作答",但包含空答案(OCR中的空白选项也是有效答题)
+        $answeredCount = collect($recordData['questions'] ?? [])
+            ->filter(fn($q) => ($q['student_answer'] ?? '未作答') !== '未作答')
+            ->count();
+        $correctCount = collect($recordData['questions'] ?? [])
+            ->filter(fn($q) => $q['is_correct'] ?? false)
+            ->count();
+        // 错误:已答题中减去正确的就是错误的
+        $wrongCount = $answeredCount - $correctCount;
+    @endphp
+
+    <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
+        <p class="text-xs text-gray-500">已答题</p>
+        <p class="text-lg font-bold text-green-600">{{ $answeredCount }}</p>
+    </div>
+
+    <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
+        <p class="text-xs text-gray-500">正确</p>
+        <p class="text-lg font-bold text-green-600">{{ $correctCount }}</p>
+    </div>
+
+    <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
+        <p class="text-xs text-gray-500">错误</p>
+        <p class="text-lg font-bold text-red-600">{{ $wrongCount }}</p>
+    </div>
+</div>

+ 15 - 0
resources/views/components/exam-analysis/recommendations.blade.php

@@ -0,0 +1,15 @@
+@props(['recommendations'])
+
+@if(isset($recommendations) && !empty($recommendations))
+<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
+    <h2 class="text-sm font-semibold text-gray-900 mb-2">💡 学习建议</h2>
+    <ul class="space-y-1">
+        @foreach($recommendations as $recommendation)
+        <li class="text-xs text-gray-700 flex items-start">
+            <span class="mr-1">•</span>
+            <span>{{ $recommendation }}</span>
+        </li>
+        @endforeach
+    </ul>
+</div>
+@endif

+ 4 - 0
resources/views/components/loading.blade.php

@@ -0,0 +1,4 @@
+<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
+    <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
+    <p class="mt-4 text-gray-500">{{ $message ?? '正在分析试卷数据...' }}</p>
+</div>

+ 78 - 0
resources/views/examples/exam-analysis-components-example.blade.php

@@ -0,0 +1,78 @@
+<!--
+组件化设计示例
+展示如何在不同的页面中使用这些组件
+-->
+
+<!-- 示例1: OCR记录页面 (紧凑模式) -->
+<x-filament-panels::page>
+    <div class="space-y-4">
+        @if($loading)
+            <x-exam-analysis.loading message="正在分析试卷数据..." />
+        @else
+            <x-exam-analysis.header :recordData="$recordData" title="📊 OCR试卷分析" />
+            <x-exam-analysis.quick-stats :recordData="$recordData" />
+            <x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+            <x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+            @if(isset($analysisData['recommendations']))
+                <x-exam-analysis.recommendations :recommendations="$analysisData['recommendations']" />
+            @endif
+        @endif
+    </div>
+</x-filament-panels::page>
+
+<!-- 示例2: 系统生成卷子页面 (标准模式) -->
+<x-filament-panels::page>
+    <div class="space-y-6">
+        @if($loading)
+            <x-exam-analysis.loading message="正在加载试卷数据..." />
+        @else
+            <x-exam-analysis.header :recordData="$recordData" title="📊 试卷分析报告" />
+
+            <!-- 试卷基本信息 -->
+            <div class="bg-white rounded-lg shadow-sm border border-gray-200">
+                <div class="p-6 border-b border-gray-200">
+                    <h2 class="text-lg font-semibold text-gray-900">📝 试卷信息</h2>
+                </div>
+                <div class="p-6">
+                    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
+                        <div>
+                            <label class="block text-sm font-medium text-gray-500 mb-1">试卷名称</label>
+                            <p class="text-lg font-medium text-gray-900">{{ $recordData['paper_name'] ?? 'N/A' }}</p>
+                        </div>
+                        <div>
+                            <label class="block text-sm font-medium text-gray-500 mb-1">试卷编号</label>
+                            <p class="text-lg font-mono text-gray-900">{{ $recordData['paper_id'] ?? 'N/A' }}</p>
+                        </div>
+                        <div>
+                            <label class="block text-sm font-medium text-gray-500 mb-1">题目数量</label>
+                            <p class="text-lg font-medium text-gray-900">{{ $recordData['total_questions'] ?? 0 }} 题</p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+            <x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+
+            @if(isset($analysisData['recommendations']))
+                <x-exam-analysis.recommendations :recommendations="$analysisData['recommendations']" />
+            @endif
+        @endif
+    </div>
+</x-filament-panels::page>
+
+<!-- 示例3: 自定义页面布局 -->
+<x-filament-panels::page>
+    <div class="space-y-6">
+        <!-- 只显示题目详情 -->
+        <x-exam-analysis.question-details :questions="$questions" />
+
+        <!-- 只显示学习分析 -->
+        <x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+
+        <!-- 只显示学习建议 -->
+        @if(isset($recommendations))
+            <x-exam-analysis.recommendations :recommendations="$recommendations" />
+        @endif
+    </div>
+</x-filament-panels::page>

+ 221 - 0
resources/views/filament/components/exam-analysis/manual-questions.blade.php

@@ -0,0 +1,221 @@
+{{-- 手动评分题目显示组件 --}}
+<div class="space-y-4">
+    <div class="flex items-center justify-between">
+        <h2 class="text-2xl font-bold">题目与作答</h2>
+        <div class="badge badge-info badge-lg">共 {{ count($questions) }} 题</div>
+    </div>
+
+    @if(count($questions) > 0)
+        <div class="space-y-4">
+            @foreach($questions as $question)
+                <div class="card bg-base-100 shadow-xl">
+                    <div class="card-body">
+                        {{-- 题目标题 --}}
+                        <div class="flex items-center gap-2 mb-3">
+                            <span class="badge badge-primary badge-lg">第 {{ $question['question_number'] }} 题</span>
+                            <span class="badge badge-outline">{{ $question['question_type'] ?? '未知类型' }}</span>
+                            <span class="badge badge-ghost">{{ $question['score'] ?? 0 }}分</span>
+                            
+                            {{-- 评分结果 --}}
+                            <div class="ml-auto flex items-center gap-2">
+                                @if(isset($question['is_correct']))
+                                    @if($question['is_correct'])
+                                        <div class="badge badge-success badge-lg 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="M5 13l4 4L19 7"></path>
+                                            </svg>
+                                            正确
+                                        </div>
+                                    @else
+                                        <div class="badge badge-error badge-lg 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="M6 18L18 6M6 6l12 12"></path>
+                                            </svg>
+                                            错误
+                                        </div>
+                                    @endif
+                                @endif
+                                
+                                @if(isset($question['actual_score']))
+                                    <div class="text-2xl font-bold">
+                                        {{ $question['actual_score'] }}<span class="text-sm opacity-70">/{{ $question['score'] }}</span>
+                                    </div>
+                                @endif
+                            </div>
+                        </div>
+                        
+                        <div class="divider my-2"></div>
+                        
+                        {{-- 题目内容 --}}
+                        <div class="mb-3">
+                            <div class="font-semibold mb-2 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.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                </svg>
+                                题目
+                            </div>
+                            <div class="prose max-w-none pl-6">
+                                @math($question['content'] ?? '题目内容缺失')
+                            </div>
+                        </div>
+                        
+                        {{-- 学生答案 --}}
+                        <div class="alert alert-info mb-3">
+                            <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
+                            </svg>
+                            <div class="w-full">
+                                <div class="font-semibold">学生答案</div>
+                                <div class="mt-1">
+                                    @if(isset($question['student_answer']) && $question['student_answer'])
+                                        @math($question['student_answer'])
+                                    @else
+                                        <span class="opacity-70 italic">未作答</span>
+                                    @endif
+                                </div>
+                            </div>
+                        </div>
+                        
+                        {{-- 正确答案 --}}
+                        <div class="alert alert-success">
+                            <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                            </svg>
+                            <div class="w-full">
+                                <div class="font-semibold">正确答案</div>
+                                <div class="mt-1">@math($question['answer'] ?? '答案缺失')</div>
+                            </div>
+                        </div>
+
+                        {{-- AI分析解析 --}}
+                        @if(isset($question['ai_analysis']) && $question['ai_analysis'])
+                            <div class="card bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200">
+                                <div class="card-body p-4">
+                                    <div class="flex items-center gap-2 mb-3">
+                                        <div class="badge badge-info badge-lg 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="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>
+                                            AI智能解析
+                                        </div>
+                                        @if(isset($question['ai_analysis']['confidence_score']))
+                                            <div class="badge badge-ghost badge-sm">
+                                                置信度: {{ number_format($question['ai_analysis']['confidence_score'] * 100, 1) }}%
+                                            </div>
+                                        @endif
+                                        @if(isset($question['ai_analysis']['ai_model']))
+                                            <div class="badge badge-ghost badge-sm">
+                                                模型: {{ $question['ai_analysis']['ai_model'] }}
+                                            </div>
+                                        @endif
+                                    </div>
+
+                                    <div class="space-y-3">
+                                        {{-- 知识点分析 --}}
+                                        @if(isset($question['ai_analysis']['knowledge_points']) && !empty($question['ai_analysis']['knowledge_points']))
+                                            <div class="bg-white rounded-lg p-3">
+                                                <div class="font-semibold text-sm mb-2 flex items-center gap-1">
+                                                    <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
+                                                    </svg>
+                                                    知识点
+                                                </div>
+                                                <div class="flex flex-wrap gap-1">
+                                                    @foreach($question['ai_analysis']['knowledge_points'] as $kp)
+                                                        <span class="badge badge-primary badge-sm">{{ $kp }}</span>
+                                                    @endforeach
+                                                </div>
+                                            </div>
+                                        @endif
+
+                                        {{-- 薄弱点分析 --}}
+                                        @if(isset($question['ai_analysis']['weak_points']) && !empty($question['ai_analysis']['weak_points']))
+                                            <div class="bg-yellow-50 rounded-lg p-3 border border-yellow-200">
+                                                <div class="font-semibold text-sm mb-2 flex items-center gap-1">
+                                                    <svg class="w-4 h-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
+                                                    </svg>
+                                                    薄弱点识别
+                                                </div>
+                                                <div class="text-sm text-gray-700">
+                                                    {{ is_array($question['ai_analysis']['weak_points']) ? implode('、', $question['ai_analysis']['weak_points']) : $question['ai_analysis']['weak_points'] }}
+                                                </div>
+                                            </div>
+                                        @endif
+
+                                        {{-- 掌握度分析 --}}
+                                        @if(isset($question['ai_analysis']['mastery_scores']) && !empty($question['ai_analysis']['mastery_scores']))
+                                            <div class="bg-green-50 rounded-lg p-3 border border-green-200">
+                                                <div class="font-semibold text-sm mb-2 flex items-center gap-1">
+                                                    <svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                                                    </svg>
+                                                    掌握度分析
+                                                </div>
+                                                @if(is_array($question['ai_analysis']['mastery_scores']))
+                                                    <div class="space-y-1">
+                                                        @foreach($question['ai_analysis']['mastery_scores'] as $point => $score)
+                                                            <div class="flex justify-between items-center text-sm">
+                                                                <span>{{ $point }}:</span>
+                                                                <span class="badge badge-{{ $score >= 0.8 ? 'success' : ($score >= 0.6 ? 'warning' : 'error') }} badge-sm">
+                                                                    {{ number_format($score * 100, 1) }}%
+                                                                </span>
+                                                            </div>
+                                                        @endforeach
+                                                    </div>
+                                                @else
+                                                    <div class="text-sm text-gray-700">
+                                                        {{ $question['ai_analysis']['mastery_scores'] }}
+                                                    </div>
+                                                @endif
+                                            </div>
+                                        @endif
+
+                                        {{-- 稳定性分析 --}}
+                                        @if(isset($question['ai_analysis']['stability_scores']) && !empty($question['ai_analysis']['stability_scores']))
+                                            <div class="bg-purple-50 rounded-lg p-3 border border-purple-200">
+                                                <div class="font-semibold text-sm mb-2 flex items-center gap-1">
+                                                    <svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"></path>
+                                                    </svg>
+                                                    稳定性分析
+                                                </div>
+                                                @if(is_array($question['ai_analysis']['stability_scores']))
+                                                    <div class="space-y-1">
+                                                        @foreach($question['ai_analysis']['stability_scores'] as $point => $score)
+                                                            <div class="flex justify-between items-center text-sm">
+                                                                <span>{{ $point }}:</span>
+                                                                <span class="badge badge-{{ $score >= 0.8 ? 'success' : ($score >= 0.6 ? 'warning' : 'error') }} badge-sm">
+                                                                    {{ number_format($score * 100, 1) }}%
+                                                                </span>
+                                                            </div>
+                                                        @endforeach
+                                                    </div>
+                                                @else
+                                                    <div class="text-sm text-gray-700">
+                                                        {{ $question['ai_analysis']['stability_scores'] }}
+                                                    </div>
+                                                @endif
+                                            </div>
+                                        @endif
+                                    </div>
+                                </div>
+                            </div>
+                        @endif
+                    </div>
+                </div>
+            @endforeach
+        </div>
+    @else
+        <div class="card bg-base-100 shadow-xl">
+            <div class="card-body">
+                <div class="text-center py-12">
+                    <svg class="w-16 h-16 mx-auto mb-4 opacity-30" 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>
+                    <p class="text-lg opacity-70">暂无题目数据</p>
+                </div>
+            </div>
+        </div>
+    @endif
+</div>

+ 263 - 0
resources/views/filament/components/exam-analysis/mastery-analysis.blade.php

@@ -0,0 +1,263 @@
+{{-- 学习分析组件 --}}
+<div class="space-y-6">
+    @if(!empty($analysisData) || !empty($paperAnalysisData))
+        {{-- 本次试卷分析结果 --}}
+        @if(!empty($paperAnalysisData) && isset($paperAnalysisData['question_results']))
+        <div class="card bg-base-100 shadow-xl border-2 border-primary">
+            <div class="card-body">
+                <h2 class="card-title text-2xl mb-4">
+                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                    </svg>
+                    本次试卷分析
+                </h2>
+
+                <div class="divider my-2"></div>
+
+                {{-- 分析摘要 --}}
+                @if(isset($paperAnalysisData['summary']))
+                <div class="stats stats-vertical lg:stats-horizontal shadow w-full mb-6">
+                    <div class="stat">
+                        <div class="stat-title">题目总数</div>
+                        <div class="stat-value text-primary">{{ $paperAnalysisData['summary']['question_count'] ?? 0 }}</div>
+                    </div>
+                    <div class="stat">
+                        <div class="stat-title">分析成功</div>
+                        <div class="stat-value text-success">{{ $paperAnalysisData['summary']['successful_analyses'] ?? 0 }}</div>
+                    </div>
+                    <div class="stat">
+                        <div class="stat-title">掌握度更新</div>
+                        <div class="stat-value text-info">{{ $paperAnalysisData['summary']['mastery_updates'] ?? 0 }}</div>
+                    </div>
+                </div>
+                @endif
+
+                {{-- 题目分析详情 --}}
+                <div class="space-y-4">
+                    @foreach($paperAnalysisData['question_results'] as $result)
+                        <div class="card bg-base-200 border {{ $result['correct'] ? 'border-success' : 'border-error' }}">
+                            <div class="card-body">
+                                <div class="flex items-start justify-between">
+                                    <div class="flex-1">
+                                        <div class="flex items-center gap-2 mb-3">
+                                            <span class="badge badge-primary badge-lg">第 {{ $result['question_number'] }} 题</span>
+                                            <span class="badge badge-outline">{{ $result['kp_code'] }}</span>
+                                            
+                                            @if($result['correct'])
+                                                <div class="badge badge-success gap-1">
+                                                    <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="M5 13l4 4L19 7"></path>
+                                                    </svg>
+                                                    正确
+                                                </div>
+                                            @else
+                                                <div class="badge badge-error gap-1">
+                                                    <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>
+                                                    错误
+                                                </div>
+                                            @endif
+                                            
+                                            <span class="ml-auto text-2xl font-bold">{{ $result['score'] }}<span class="text-sm opacity-70">/{{ $result['full_score'] }}</span></span>
+                                        </div>
+
+                                        @if(!$result['correct'])
+                                            <div class="divider my-2"></div>
+                                            
+                                            {{-- 错误分析 --}}
+                                            <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
+                                                <div class="alert alert-warning">
+                                                    <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
+                                                    </svg>
+                                                    <div>
+                                                        <div class="font-semibold">错误类型</div>
+                                                        <div class="text-sm">{{ $result['mistake_type'] ?? '未知' }} - {{ $result['mistake_category'] ?? '' }}</div>
+                                                    </div>
+                                                </div>
+                                                
+                                                <div class="alert alert-info">
+                                                    <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                                    </svg>
+                                                    <div>
+                                                        <div class="font-semibold">错误原因</div>
+                                                        <div class="text-sm">{{ $result['reason'] ?? '' }}</div>
+                                                    </div>
+                                                </div>
+                                            </div>
+
+                                            {{-- 学习建议 --}}
+                                            @if(isset($result['suggestions']) && $result['suggestions'])
+                                            <div class="alert alert-success mt-3">
+                                                <svg class="w-5 h-5 shrink-0" 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>
+                                                <div>
+                                                    <div class="font-semibold">学习建议</div>
+                                                    <div class="text-sm">{{ $result['suggestions'] }}</div>
+                                                </div>
+                                            </div>
+                                            @endif
+
+                                            {{-- 下一步措施 --}}
+                                            @if(isset($result['next_steps']) && is_array($result['next_steps']) && count($result['next_steps']) > 0)
+                                            <div class="mt-3">
+                                                <div class="font-semibold mb-2">下一步措施:</div>
+                                                <ul class="list-disc list-inside space-y-1">
+                                                    @foreach($result['next_steps'] as $step)
+                                                        <li class="text-sm">{{ $step }}</li>
+                                                    @endforeach
+                                                </ul>
+                                            </div>
+                                            @endif
+                                        @else
+                                            {{-- 正确答案的鼓励 --}}
+                                            <div class="alert alert-success">
+                                                <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                                </svg>
+                                                <span>{{ $result['suggestions'] ?? '答案完全正确,掌握度很好!' }}</span>
+                                            </div>
+                                        @endif
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    @endforeach
+                </div>
+            </div>
+        </div>
+        @endif
+
+        {{-- 整体掌握度分析 --}}
+        @if(!empty($analysisData) && $recordData['status'] === 'completed')
+            {{-- 整体掌握度 --}}
+            @if(isset($analysisData['overall_mastery']))
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <h2 class="card-title text-2xl mb-4">整体掌握度评估</h2>
+                    <div class="divider my-2"></div>
+                    <div class="stats stats-vertical lg:stats-horizontal shadow w-full">
+                        <div class="stat">
+                            <div class="stat-title">掌握度评分</div>
+                            <div class="stat-value text-primary">{{ number_format($analysisData['overall_mastery'] * 100, 1) }}%</div>
+                            <div class="stat-desc">
+                                @if($analysisData['overall_mastery'] >= 0.8)
+                                    优秀
+                                @elseif($analysisData['overall_mastery'] >= 0.6)
+                                    良好
+                                @else
+                                    需加强
+                                @endif
+                            </div>
+                        </div>
+                        <div class="stat">
+                            <div class="stat-title">薄弱知识点</div>
+                            <div class="stat-value text-warning">{{ count($analysisData['weak_areas'] ?? []) }} 个</div>
+                            <div class="stat-desc">需要重点关注</div>
+                        </div>
+                        <div class="stat">
+                            <div class="stat-title">知识点总数</div>
+                            <div class="stat-value">{{ $analysisData['total_knowledge_points'] ?? 0 }}</div>
+                            <div class="stat-desc">已评估</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            @endif
+
+            {{-- 知识点掌握情况 --}}
+            @if(!empty($analysisData['knowledge_points']))
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <h2 class="card-title text-xl mb-4">知识点掌握情况</h2>
+                    <div class="divider my-2"></div>
+                    <div class="overflow-x-auto">
+                        <table class="table table-zebra">
+                            <thead>
+                                <tr>
+                                    <th>知识点</th>
+                                    <th>掌握度</th>
+                                    <th>答题次数</th>
+                                    <th>正确率</th>
+                                    <th>状态</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                @foreach($analysisData['knowledge_points'] as $kp)
+                                    <tr>
+                                        <td>
+                                            <div class="font-medium">{{ $kp['name'] ?? $kp['kp_code'] }}</div>
+                                            <div class="text-xs opacity-70">{{ $kp['kp_code'] }}</div>
+                                        </td>
+                                        <td>
+                                            <div class="flex items-center gap-2">
+                                                <progress 
+                                                    class="progress {{ $kp['mastery'] >= 0.7 ? 'progress-success' : ($kp['mastery'] >= 0.4 ? 'progress-warning' : 'progress-error') }} w-20" 
+                                                    value="{{ $kp['mastery'] * 100 }}" 
+                                                    max="100"
+                                                ></progress>
+                                                <span class="text-sm">{{ number_format($kp['mastery'] * 100, 1) }}%</span>
+                                            </div>
+                                        </td>
+                                        <td>{{ $kp['total_attempts'] ?? 0 }} 次</td>
+                                        <td>{{ number_format(($kp['accuracy_rate'] ?? 0) * 100, 1) }}%</td>
+                                        <td>
+                                            @if($kp['mastery'] >= 0.7)
+                                                <span class="badge badge-success badge-sm">良好</span>
+                                            @elseif($kp['mastery'] >= 0.4)
+                                                <span class="badge badge-warning badge-sm">一般</span>
+                                            @else
+                                                <span class="badge badge-error badge-sm">薄弱</span>
+                                            @endif
+                                        </td>
+                                    </tr>
+                                @endforeach
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+            @endif
+
+            {{-- 学习建议 --}}
+            @if(!empty($analysisData['recommendations']))
+            <div class="card bg-base-100 shadow-xl">
+                <div class="card-body">
+                    <h2 class="card-title text-xl mb-4">
+                        <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>
+                        学习建议
+                    </h2>
+                    <div class="divider my-2"></div>
+                    <div class="space-y-3">
+                        @foreach($analysisData['recommendations'] as $recommendation)
+                            <div class="alert alert-info">
+                                <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                </svg>
+                                <span>{{ $recommendation }}</span>
+                            </div>
+                        @endforeach
+                    </div>
+                </div>
+            </div>
+            @endif
+        @endif
+    @else
+        <div class="card bg-base-100 shadow-xl">
+            <div class="card-body">
+                <div class="text-center py-12">
+                    <svg class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                    </svg>
+                    <p class="text-lg font-medium opacity-70">暂无学习分析数据</p>
+                    <p class="text-sm mt-2 opacity-50">完成评分后将生成学习分析报告</p>
+                </div>
+            </div>
+        </div>
+    @endif
+</div>

+ 36 - 0
resources/views/filament/components/exam-analysis/paper-image.blade.php

@@ -0,0 +1,36 @@
+{{-- 试卷图片组件(仅OCR场景) --}}
+<div class="card bg-base-100 shadow-lg border">
+    <div class="card-body">
+        <h3 class="card-title text-lg mb-4">
+            <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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
+            </svg>
+            试卷图片
+        </h3>
+        <div class="relative">
+            <img
+                src="{{ asset($recordData['image_path']) }}"
+                alt="试卷图片"
+                class="w-full rounded-lg border shadow-sm"
+                onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDQwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjRjNGNEY2Ii8+Cjx0ZXh0IHg9IjIwMCIgeT0iMTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNjc3NDg4IiBmb250LXNpemU9IjE0Ij7lm77niYfliqDovb3lpLHotKU8L3RleHQ+Cjwvc3ZnPg=='"
+            >
+            @if($recordData['status'] === 'processing')
+                <div class="absolute inset-0 bg-black bg-opacity-50 rounded-lg flex items-center justify-center">
+                    <div class="text-center text-white">
+                        <div class="loading loading-spinner loading-lg"></div>
+                        <p class="mt-2">识别处理中...</p>
+                    </div>
+                </div>
+            @endif
+        </div>
+        <div class="mt-4 text-sm text-gray-600">
+            <p>文件名: {{ $recordData['image_filename'] ?? '' }}</p>
+            @if(isset($recordData['image_width']) && isset($recordData['image_height']) && $recordData['image_width'] && $recordData['image_height'])
+                <p>尺寸: {{ $recordData['image_width'] }} × {{ $recordData['image_height'] }} px</p>
+            @endif
+            @if(isset($recordData['image_size']) && $recordData['image_size'])
+                <p>大小: {{ number_format($recordData['image_size'] / 1024, 1) }} KB</p>
+            @endif
+        </div>
+    </div>
+</div>

+ 52 - 0
resources/views/filament/components/exam-analysis/paper-info.blade.php

@@ -0,0 +1,52 @@
+{{-- 试卷基本信息组件 --}}
+<div class="card bg-base-100 shadow-xl">
+    <div class="card-body">
+        <h2 class="card-title">
+            <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 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>
+            试卷信息
+        </h2>
+
+        <div class="divider my-2"></div>
+
+        <div class="space-y-3">
+            <div class="flex justify-between items-center">
+                <span class="text-sm opacity-70">学生信息</span>
+                <div class="text-right">
+                    <div class="font-semibold">{{ $studentInfo['name'] ?? '未知学生' }}</div>
+                    <div class="text-xs opacity-70">{{ $studentInfo['grade'] ?? '' }} - {{ $studentInfo['class_name'] ?? '' }}</div>
+                </div>
+            </div>
+
+            <div class="divider my-1"></div>
+
+            <div class="flex justify-between items-center">
+                <span class="text-sm opacity-70">试卷形式</span>
+                <span class="badge badge-outline">{{ $paperTypeLabel }}</span>
+            </div>
+
+            <div class="divider my-1"></div>
+
+            <div class="flex justify-between items-center">
+                <span class="text-sm opacity-70">处理状态</span>
+                <div>{!! $statusBadge !!}</div>
+            </div>
+
+            <div class="divider my-1"></div>
+
+            <div class="flex justify-between items-center">
+                <span class="text-sm opacity-70">上传时间</span>
+                <span class="font-mono text-sm">{{ \Carbon\Carbon::parse($recordData['created_at'])->format('Y-m-d H:i') }}</span>
+            </div>
+
+            @if(isset($recordData['total_questions']) && $recordData['total_questions'])
+            <div class="divider my-1"></div>
+            <div class="flex justify-between items-center">
+                <span class="text-sm opacity-70">题目数量</span>
+                <span class="badge badge-primary badge-lg">{{ $recordData['total_questions'] }} 题</span>
+            </div>
+            @endif
+        </div>
+    </div>
+</div>

+ 60 - 0
resources/views/filament/components/exam-analysis/processing-timeline.blade.php

@@ -0,0 +1,60 @@
+{{-- 处理进度时间线组件 --}}
+<div class="card bg-base-100 shadow-xl">
+    <div class="card-body">
+        <h2 class="card-title">
+            <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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+            </svg>
+            处理进度
+        </h2>
+        
+        <div class="divider my-2"></div>
+        
+        <ul class="steps steps-vertical w-full">
+            <li class="step step-primary">
+                <div class="text-left w-full">
+                    <div class="font-semibold">上传完成</div>
+                    <div class="text-xs opacity-70">{{ \Carbon\Carbon::parse($recordData['created_at'])->format('m-d H:i') }}</div>
+                    <div class="text-sm mt-1">{{ $recordType === 'ocr' ? '图片已上传' : '试卷已创建' }}</div>
+                </div>
+            </li>
+
+            @if($recordData['status'] === 'completed')
+                <li class="step step-primary">
+                    <div class="text-left w-full">
+                        <div class="font-semibold">{{ $recordType === 'ocr' ? 'OCR识别完成' : '试卷已完成' }}</div>
+                        <div class="text-xs opacity-70">
+                            @if(isset($recordData['processed_at']) && $recordData['processed_at'])
+                                {{ \Carbon\Carbon::parse($recordData['processed_at'])->format('m-d H:i') }}
+                            @else
+                                {{ \Carbon\Carbon::parse($recordData['created_at'])->format('m-d H:i') }}
+                            @endif
+                        </div>
+                        <div class="text-sm mt-1">
+                            @if($recordType === 'ocr')
+                                识别出 {{ $recordData['total_questions'] ?? 0 }} 道题目
+                                @if(isset($recordData['confidence_avg']) && $recordData['confidence_avg'])
+                                    <span class="badge badge-success badge-sm ml-2">
+                                        准确率: {{ number_format(($recordData['confidence_avg'] ?? 0) * 100, 1) }}%
+                                    </span>
+                                @endif
+                            @else
+                                包含 {{ $recordData['total_questions'] ?? 0 }} 道题目
+                            @endif
+                        </div>
+                    </div>
+                </li>
+            @elseif($recordData['status'] === 'processing')
+                <li class="step step-info">
+                    <div class="text-left w-full">
+                        <div class="font-semibold flex items-center gap-2">
+                            <span class="loading loading-spinner loading-sm"></span>
+                            处理中...
+                        </div>
+                        <div class="text-sm mt-1 opacity-70">正在识别题目和答案</div>
+                    </div>
+                </li>
+            @endif
+        </ul>
+    </div>
+</div>

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

@@ -0,0 +1,24 @@
+<x-filament-panels::page>
+    <div class="space-y-4">
+        @if($loading)
+            <x-exam-analysis.loading message="正在分析试卷数据..." />
+        @else
+            <!-- 页面头部 -->
+            <x-exam-analysis.header :recordData="$recordData" title="📊 OCR试卷分析" />
+
+            <!-- 快速统计 -->
+            <x-exam-analysis.quick-stats :recordData="$recordData" />
+
+            <!-- 学习分析概览 -->
+            <x-exam-analysis.learning-analysis :analysisData="$analysisData" />
+
+            <!-- 题目详情分析 -->
+            <x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+
+            <!-- 学习建议 -->
+            @if(isset($analysisData['recommendations']))
+                <x-exam-analysis.recommendations :recommendations="$analysisData['recommendations']" />
+            @endif
+        @endif
+    </div>
+</x-filament-panels::page>

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

@@ -0,0 +1,143 @@
+<x-filament-panels::page>
+    <div class="space-y-6">
+        @if($loading)
+            <x-exam-analysis.loading message="正在分析试卷数据..." />
+        @else
+            <!-- 页面头部 -->
+            <x-exam-analysis.header :recordData="$recordData" title="📊 试卷分析报告" />
+
+            <!-- 试卷基本信息 -->
+            <div class="bg-white rounded-lg shadow-sm border border-gray-200">
+                <div class="p-6 border-b border-gray-200">
+                    <h2 class="text-lg font-semibold text-gray-900">📝 试卷信息</h2>
+                </div>
+                <div class="p-6">
+                    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
+                        <div>
+                            <label class="block text-sm font-medium text-gray-500 mb-1">试卷名称</label>
+                            <p class="text-lg font-medium text-gray-900">{{ $recordData['paper_name'] ?? 'N/A' }}</p>
+                        </div>
+                        <div>
+                            <label class="block text-sm font-medium text-gray-500 mb-1">试卷编号</label>
+                            <p class="text-lg font-mono text-gray-900">{{ $recordData['paper_id'] ?? 'N/A' }}</p>
+                        </div>
+                        <div>
+                            <label class="block text-sm font-medium text-gray-500 mb-1">题目数量</label>
+                            <p class="text-lg font-medium text-gray-900">{{ $recordData['total_questions'] ?? 0 }} 题</p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 学习统计概览 -->
+            @if(!empty($analysisData))
+            <div class="bg-white rounded-lg shadow-sm border border-gray-200">
+                <div class="p-6 border-b border-gray-200">
+                    <h2 class="text-lg font-semibold text-gray-900">📊 学习统计概览</h2>
+                </div>
+                <div class="p-6">
+                    <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
+                        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+                            <div class="flex items-center">
+                                <div class="p-3 rounded-full bg-blue-100 text-blue-600">
+                                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
+                                    </svg>
+                                </div>
+                                <div class="ml-4">
+                                    <p class="text-sm text-gray-500">整体掌握度</p>
+                                    <p class="text-2xl font-bold text-gray-900">{{ number_format(($analysisData['overall_mastery'] ?? 0) * 100, 1) }}%</p>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+                            <div class="flex items-center">
+                                <div class="p-3 rounded-full bg-green-100 text-green-600">
+                                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                    </svg>
+                                </div>
+                                <div class="ml-4">
+                                    <p class="text-sm text-gray-500">知识点数量</p>
+                                    <p class="text-2xl font-bold text-gray-900">{{ $analysisData['total_knowledge_points'] ?? 0 }}</p>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+                            <div class="flex items-center">
+                                <div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
+                                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
+                                    </svg>
+                                </div>
+                                <div class="ml-4">
+                                    <p class="text-sm text-gray-500">薄弱知识点</p>
+                                    <p class="text-2xl font-bold text-gray-900">{{ count($analysisData['weak_areas'] ?? []) }}</p>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+                            <div class="flex items-center">
+                                <div class="p-3 rounded-full bg-purple-100 text-purple-600">
+                                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                    </svg>
+                                </div>
+                                <div class="ml-4">
+                                    <p class="text-sm text-gray-500">总答题数</p>
+                                    <p class="text-2xl font-bold text-gray-900">{{ $recordData['total_questions'] ?? 0 }}</p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- 知识点掌握情况 -->
+                    @if(isset($analysisData['knowledge_points']) && !empty($analysisData['knowledge_points']))
+                    <div class="mt-6">
+                        <h3 class="text-sm font-semibold text-gray-700 mb-3">知识点掌握情况</h3>
+                        <div class="space-y-3">
+                            @foreach($analysisData['knowledge_points'] as $kp)
+                            <div class="border rounded-lg p-4
+                                {{ ($kp['mastery_level'] ?? 0) < 0.6 ? 'border-red-200 bg-red-50' :
+                                   (($kp['mastery_level'] ?? 0) < 0.8 ? 'border-yellow-200 bg-yellow-50' :
+                                   'border-green-200 bg-green-50') }}">
+                                <div class="flex items-center justify-between mb-2">
+                                    <h4 class="font-medium text-gray-900">{{ $kp['kp_code'] ?? 'Unknown' }}</h4>
+                                    <span class="text-sm font-bold
+                                        {{ ($kp['mastery_level'] ?? 0) < 0.6 ? 'text-red-600' :
+                                           (($kp['mastery_level'] ?? 0) < 0.8 ? 'text-yellow-600' : 'text-green-600') }}">
+                                        {{ number_format(($kp['mastery_level'] ?? 0) * 100, 1) }}%
+                                    </span>
+                                </div>
+                                <div class="w-full bg-gray-200 rounded-full h-2 mb-2">
+                                    <div class="h-2 rounded-full
+                                        {{ ($kp['mastery_level'] ?? 0) < 0.6 ? 'bg-red-400' :
+                                           (($kp['mastery_level'] ?? 0) < 0.8 ? 'bg-yellow-400' : 'bg-green-400') }}"
+                                         style="width: {{ ($kp['mastery_level'] ?? 0) * 100 }}%"></div>
+                                </div>
+                                <p class="text-xs text-gray-600">
+                                    正确率: {{ number_format(($kp['accuracy_rate'] ?? 0) * 100, 1) }}% |
+                                    练习次数: {{ $kp['total_attempts'] ?? 0 }}
+                                </p>
+                            </div>
+                            @endforeach
+                        </div>
+                    </div>
+                    @endif
+                </div>
+            </div>
+            @endif
+
+            <!-- 题目详情分析 -->
+            <x-exam-analysis.question-details :questions="$recordData['questions'] ?? []" />
+
+            <!-- 学习建议 -->
+            @if(isset($analysisData['recommendations']))
+                <x-exam-analysis.recommendations :recommendations="$analysisData['recommendations']" />
+            @endif
+        @endif
+    </div>
+</x-filament-panels::page>

+ 9 - 5
resources/views/filament/pages/exam-history-simple.blade.php

@@ -156,18 +156,18 @@
                             <div class="space-y-4">
                                 <div>
                                     <div class="text-sm text-gray-500">试卷名称</div>
-                                    <div class="font-medium text-gray-900">{{ $selectedExamDetail['paper']['paper_name'] ?? '' }}</div>
+                                    <div class="font-medium text-gray-900">{{ $selectedExamDetail['paper_name'] ?? '未命名试卷' }}</div>
                                 </div>
 
                                 <div class="stats stats-vertical shadow w-full">
                                     <div class="stat">
                                         <div class="stat-title">题目数量</div>
-                                        <div class="stat-value text-primary">{{ $selectedExamDetail['paper']['question_count'] ?? 0 }}</div>
+                                        <div class="stat-value text-primary">{{ $selectedExamDetail['question_count'] ?? 0 }}</div>
                                         <div class="stat-desc">题</div>
                                     </div>
                                     <div class="stat">
                                         <div class="stat-title">总分</div>
-                                        <div class="stat-value text-secondary">{{ $selectedExamDetail['paper']['total_score'] ?? 0 }}</div>
+                                        <div class="stat-value text-secondary">{{ $selectedExamDetail['total_score'] ?? 0 }}</div>
                                         <div class="stat-desc">分</div>
                                     </div>
                                 </div>
@@ -175,7 +175,11 @@
                                 <div>
                                     <div class="text-sm text-gray-500">创建时间</div>
                                     <div class="font-medium text-gray-900">
-                                        {{ \Carbon\Carbon::parse($selectedExamDetail['paper']['created_at'])->format('Y-m-d H:i') }}
+                                        @if(isset($selectedExamDetail['created_at']))
+                                            {{ \Carbon\Carbon::parse($selectedExamDetail['created_at'])->format('Y-m-d H:i') }}
+                                        @else
+                                            未知时间
+                                        @endif
                                     </div>
                                 </div>
 
@@ -189,7 +193,7 @@
                                     </a>
 
                                     <button
-                                        wire:click="duplicateExam({{ json_encode($selectedExamDetail['paper'] ?? []) }})"
+                                        wire:click="duplicateExam({{ json_encode($selectedExamDetail) }})"
                                         class="btn btn-outline w-full">
                                         复制试卷配置
                                     </button>

+ 41 - 2
resources/views/filament/pages/ocr-record-view-new.blade.php

@@ -252,6 +252,7 @@
                                     <th>题目内容</th>
                                     <th>学生答案</th>
                                     <th class="w-32">手动校准</th>
+                                    <th class="w-32">判卷</th>
                                     <th class="w-24">AI分析</th>
                                     <th class="w-32">状态</th>
                                 </tr>
@@ -267,7 +268,7 @@
                                         <td>
                                             <div class="max-w-xs">
                                                 @if($question->question_text)
-                                                    <p class="text-sm leading-tight">{{ Str::limit($question->question_text, 80) }}</p>
+                                                    <p class="text-sm leading-tight">@math($question->question_text)</p>
                                                 @else
                                                     <span class="text-gray-400 italic text-sm">未识别到题目内容</span>
                                                 @endif
@@ -277,7 +278,7 @@
                                             <div class="flex items-center gap-2">
                                                 <div class="text-sm font-medium">
                                                     @if($question->student_answer)
-                                                        <span class="text-primary">{{ $question->student_answer }}</span>
+                                                        <span class="text-primary">@math($question->student_answer)</span>
                                                     @else
                                                         <span class="text-gray-400 italic">未识别</span>
                                                     @endif
@@ -307,6 +308,42 @@
                                                 </div>
                                             @endif
                                         </td>
+                                        <td>
+                                            {{-- 判卷区域 --}}
+                                            <div class="space-y-2">
+                                                {{-- 对错判断 --}}
+                                                <div class="flex gap-1">
+                                                    <label class="label cursor-pointer gap-1 p-1">
+                                                        <input
+                                                            type="radio"
+                                                            wire:model="questionGrades.{{ $question->id }}.is_correct"
+                                                            value="1"
+                                                            class="radio radio-success radio-xs"
+                                                        >
+                                                        <span class="text-xs">✓</span>
+                                                    </label>
+                                                    <label class="label cursor-pointer gap-1 p-1">
+                                                        <input
+                                                            type="radio"
+                                                            wire:model="questionGrades.{{ $question->id }}.is_correct"
+                                                            value="0"
+                                                            class="radio radio-error radio-xs"
+                                                        >
+                                                        <span class="text-xs">✗</span>
+                                                    </label>
+                                                </div>
+                                                {{-- 分数输入 --}}
+                                                <input
+                                                    type="number"
+                                                    wire:model="questionGrades.{{ $question->id }}.score"
+                                                    placeholder="分数"
+                                                    class="input input-bordered input-xs w-full"
+                                                    min="0"
+                                                    max="100"
+                                                    step="0.5"
+                                                >
+                                            </div>
+                                        </td>
                                         <td>
                                             @if($question->ai_score !== null || $question->ai_feedback !== null)
                                                 <div class="space-y-1">
@@ -636,4 +673,6 @@
     @endif
 </div>
 
+<x-math-render />
+
 </x-filament-panels::page>

+ 394 - 113
resources/views/filament/pages/upload-exam-paper.blade.php

@@ -1,19 +1,203 @@
 <x-filament-panels::page>
 
 <div class="space-y-6">
-    {{-- 上传表单卡片 --}}
+    {{-- 模式选择 --}}
     <div class="card bg-base-100 shadow-lg border">
         <div class="card-body">
-            <h2 class="card-title text-xl mb-4">
-                <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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
-                </svg>
-                上传考试卷子
-            </h2>
+            <div class="flex gap-4">
+                <button
+                    wire:click="$set('mode', 'upload')"
+                    class="btn {{ $mode === 'upload' ? 'btn-primary' : 'btn-outline' }}"
+                >
+                    <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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
+                    </svg>
+                    上传卷子照片
+                </button>
+                <button
+                    wire:click="$set('mode', 'select_paper')"
+                    class="btn {{ $mode === 'select_paper' ? 'btn-primary' : 'btn-outline' }}"
+                >
+                    <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 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>
+                    选择已有试卷打分
+                </button>
+            </div>
+        </div>
+    </div>
+
+    {{-- 上传模式 --}}
+    @if($mode === 'upload')
+        <div class="card bg-base-100 shadow-lg border">
+            <div class="card-body">
+                <h2 class="card-title text-xl mb-4">
+                    <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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
+                    </svg>
+                    上传考试卷子
+                </h2>
+
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+                    {{-- 左侧:选择老师和学生 --}}
+                    <div class="space-y-4">
+                        {{-- 选择老师 --}}
+                        <div class="form-control w-full">
+                            <label class="label">
+                                <span class="label-text font-medium">选择老师 <span class="text-error">*</span></span>
+                            </label>
+                            <select
+                                wire:model.live="teacherId"
+                                class="select select-bordered w-full"
+                            >
+                                <option value="">请选择老师...</option>
+                                @foreach($this->teachers as $teacher)
+                                    <option value="{{ $teacher->teacher_id }}">
+                                        {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
+                                    </option>
+                                @endforeach
+                            </select>
+                        </div>
+
+                        {{-- 选择学生 --}}
+                        <div class="form-control w-full">
+                            <label class="label">
+                                <span class="label-text font-medium">选择学生 <span class="text-error">*</span></span>
+                            </label>
+                            <select
+                                wire:model.live="studentId"
+                                class="select select-bordered w-full"
+                                @if(empty($teacherId)) disabled @endif
+                            >
+                                <option value="">
+                                    @if(empty($teacherId))
+                                        请先选择老师
+                                    @else
+                                        请选择学生...
+                                    @endif
+                                </option>
+                                @foreach($this->students as $student)
+                                    <option value="{{ $student->student_id }}">
+                                        {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
+                                    </option>
+                                @endforeach
+                            </select>
+                        </div>
+                    </div>
 
-            <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
-                {{-- 左侧:选择老师和学生 --}}
-                <div class="space-y-4">
+                    {{-- 右侧:上传图片 --}}
+                    <div class="form-control w-full">
+                        <label class="label">
+                            <span class="label-text font-medium">卷子图片 <span class="text-error">*</span></span>
+                        </label>
+
+                        @if($uploadedImage)
+                            {{-- 图片预览 --}}
+                            <div class="relative">
+                                <img
+                                    src="{{ $uploadedImage->temporaryUrl() }}"
+                                    class="w-full h-48 object-cover rounded-lg border"
+                                    alt="预览"
+                                >
+                                <button
+                                    type="button"
+                                    wire:click="removeImage"
+                                    class="btn btn-circle btn-sm btn-error absolute top-2 right-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="M6 18L18 6M6 6l12 12"></path>
+                                    </svg>
+                                </button>
+                            </div>
+                            <label class="label">
+                                <span class="label-text-alt text-success">
+                                    {{ $uploadedImage->getClientOriginalName() }}
+                                    ({{ number_format($uploadedImage->getSize() / 1024, 1) }} KB)
+                                </span>
+                            </label>
+                        @else
+                            {{-- 上传区域 --}}
+                            <div
+                                x-data="{ uploading: false, progress: 0 }"
+                                x-on:livewire-upload-start="uploading = true"
+                                x-on:livewire-upload-finish="uploading = false"
+                                x-on:livewire-upload-error="uploading = false"
+                                x-on:livewire-upload-progress="progress = $event.detail.progress"
+                                class="relative"
+                            >
+                                <input
+                                    type="file"
+                                    id="uploadedImage"
+                                    wire:model.live="uploadedImage"
+                                    class="hidden"
+                                    accept="image/jpeg,image/png,image/webp"
+                                >
+                                <label
+                                    for="uploadedImage"
+                                    class="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed rounded-lg cursor-pointer hover:bg-base-200 transition-colors"
+                                    x-bind:class="{ 'border-primary bg-primary/5': uploading }"
+                                >
+                                    {{-- 上传进度 --}}
+                                    <div x-show="uploading" class="flex flex-col items-center justify-center">
+                                        <div class="radial-progress text-primary" x-bind:style="'--value:' + progress + '; --size: 5rem; --thickness: 4px;'" role="progressbar">
+                                            <span class="text-sm font-bold" x-text="progress + '%'"></span>
+                                        </div>
+                                        <p class="mt-3 text-base font-semibold text-primary">正在上传...</p>
+                                    </div>
+
+                                    {{-- 默认上传提示 --}}
+                                    <div x-show="!uploading" class="flex flex-col items-center justify-center pt-5 pb-6">
+                                        <svg class="w-10 h-10 mb-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
+                                        </svg>
+                                        <p class="mb-2 text-sm text-gray-500">
+                                            <span class="font-semibold">点击上传</span> 或拖拽文件
+                                        </p>
+                                        <p class="text-xs text-gray-400">
+                                            支持 JPG、PNG、WebP (最大 10MB)
+                                        </p>
+                                    </div>
+                                </label>
+                            </div>
+                        @endif
+                    </div>
+                </div>
+
+                {{-- 提交按钮 --}}
+                <div class="card-actions justify-end mt-6">
+                    <button
+                        type="button"
+                        wire:click="submitUpload"
+                        class="btn btn-primary"
+                        @if($isUploading) disabled @endif
+                    >
+                        @if($isUploading)
+                            <span class="loading loading-spinner"></span>
+                            上传中...
+                        @else
+                            <svg class="w-5 h-5 mr-2" 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-8l-4-4m0 0L8 8m4-4v12"></path>
+                            </svg>
+                            上传并识别
+                        @endif
+                    </button>
+                </div>
+            </div>
+        </div>
+    @endif
+
+    {{-- 选择试卷模式 --}}
+    @if($mode === 'select_paper')
+        <div class="card bg-base-100 shadow-lg border">
+            <div class="card-body">
+                <h2 class="card-title text-xl mb-4">
+                    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 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>
+                    选择试卷并打分
+                </h2>
+
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                     {{-- 选择老师 --}}
                     <div class="form-control w-full">
                         <label class="label">
@@ -58,105 +242,150 @@
                     </div>
                 </div>
 
-                {{-- 右侧:上传图片 --}}
-                <div class="form-control w-full">
-                    <label class="label">
-                        <span class="label-text font-medium">卷子图片 <span class="text-error">*</span></span>
-                    </label>
-
-                    @if($uploadedImage)
-                        {{-- 图片预览 --}}
-                        <div class="relative">
-                            <img
-                                src="{{ $uploadedImage->temporaryUrl() }}"
-                                class="w-full h-48 object-cover rounded-lg border"
-                                alt="预览"
-                            >
-                            <button
-                                type="button"
-                                wire:click="removeImage"
-                                class="btn btn-circle btn-sm btn-error absolute top-2 right-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="M6 18L18 6M6 6l12 12"></path>
-                                </svg>
-                            </button>
-                        </div>
+                {{-- 试卷类型 --}}
+                @if(!empty($studentId))
+                    <div class="form-control w-full mt-4">
                         <label class="label">
-                            <span class="label-text-alt text-success">
-                                {{ $uploadedImage->getClientOriginalName() }}
-                                ({{ number_format($uploadedImage->getSize() / 1024, 1) }} KB)
-                            </span>
+                            <span class="label-text font-medium">试卷形式 <span class="text-error">*</span></span>
                         </label>
-                    @else
-                        {{-- 上传区域 --}}
-                        <div
-                            x-data="{ uploading: false, progress: 0 }"
-                            x-on:livewire-upload-start="uploading = true"
-                            x-on:livewire-upload-finish="uploading = false"
-                            x-on:livewire-upload-error="uploading = false"
-                            x-on:livewire-upload-progress="progress = $event.detail.progress"
-                            class="relative"
+                        <select
+                            wire:model.live="paperType"
+                            class="select select-bordered w-full"
                         >
-                            <input
-                                type="file"
-                                id="uploadedImage"
-                                wire:model.live="uploadedImage"
-                                class="hidden"
-                                accept="image/jpeg,image/png,image/webp"
-                            >
-                            <label
-                                for="uploadedImage"
-                                class="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed rounded-lg cursor-pointer hover:bg-base-200 transition-colors"
-                                x-bind:class="{ 'border-primary bg-primary/5': uploading }"
-                            >
-                                {{-- 上传进度 --}}
-                                <div x-show="uploading" class="flex flex-col items-center justify-center">
-                                    <div class="radial-progress text-primary" x-bind:style="'--value:' + progress + '; --size: 5rem; --thickness: 4px;'" role="progressbar">
-                                        <span class="text-sm font-bold" x-text="progress + '%'"></span>
-                                    </div>
-                                    <p class="mt-3 text-base font-semibold text-primary">正在上传...</p>
-                                </div>
+                            @foreach($this->paperTypes as $value => $label)
+                                <option value="{{ $value }}">{{ $label }}</option>
+                            @endforeach
+                        </select>
+                    </div>
 
-                                {{-- 默认上传提示 --}}
-                                <div x-show="!uploading" class="flex flex-col items-center justify-center pt-5 pb-6">
-                                    <svg class="w-10 h-10 mb-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
-                                    </svg>
-                                    <p class="mb-2 text-sm text-gray-500">
-                                        <span class="font-semibold">点击上传</span> 或拖拽文件
-                                    </p>
-                                    <p class="text-xs text-gray-400">
-                                        支持 JPG、PNG、WebP (最大 10MB)
-                                    </p>
+                    {{-- 选择试卷 --}}
+                    <div class="form-control w-full mt-4">
+                        <label class="label">
+                            <span class="label-text font-medium">选择试卷 <span class="text-error">*</span></span>
+                        </label>
+                        <select
+                            wire:model.live="selectedPaperId"
+                            class="select select-bordered w-full"
+                        >
+                            <option value="">请选择试卷...</option>
+                            @foreach($this->studentPapers as $paper)
+                                <option value="{{ $paper['paper_id'] }}">
+                                    {{ $paper['paper_name'] }} ({{ $paper['total_questions'] }}题 / {{ $paper['total_score'] }}分) - {{ $paper['created_at'] }}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+                @endif
+
+                {{-- 题目列表和评分 --}}
+                @if(!empty($selectedPaperId) && count($this->selectedPaperQuestions) > 0)
+                    <div class="mt-6">
+                        <h3 class="text-lg font-semibold mb-4">题目列表</h3>
+                        <div class="space-y-4">
+                            @foreach($this->selectedPaperQuestions as $question)
+                                <div class="card bg-base-200 border">
+                                    <div class="card-body">
+                                        <div class="flex items-start justify-between">
+                                            <div class="flex-1">
+                                                <div class="flex items-center gap-2 mb-2">
+                                                    <span class="badge badge-primary">第 {{ $question['question_number'] }} 题</span>
+                                                    <span class="badge badge-outline">{{ $question['question_type'] }}</span>
+                                                    <span class="text-sm text-gray-500">({{ $question['score'] }}分)</span>
+                                                </div>
+                                                <div class="prose max-w-none">
+                                                    @math($question['content'])
+                                                </div>
+                                                <div class="mt-2 text-sm text-success">
+                                                    <strong>参考答案:</strong> @math($question['answer'])
+                                                </div>
+                                            </div>
+                                        </div>
+
+                                        {{-- 评分区域 --}}
+                                        <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4 pt-4 border-t">
+                                            {{-- 学生答案 --}}
+                                            <div class="form-control">
+                                                <label class="label">
+                                                    <span class="label-text">学生答案</span>
+                                                </label>
+                                                <input
+                                                    type="text"
+                                                    wire:model="questionGrades.{{ $question['id'] }}.student_answer"
+                                                    class="input input-bordered input-sm"
+                                                    placeholder="输入学生答案..."
+                                                >
+                                            </div>
+
+                                            {{-- 对错判断(选择题/填空题) --}}
+                                            @if(in_array($question['question_type'], ['选择题', '填空题']))
+                                                <div class="form-control">
+                                                    <label class="label">
+                                                        <span class="label-text">对错</span>
+                                                    </label>
+                                                    <div class="flex gap-2">
+                                                        <label class="label cursor-pointer gap-2">
+                                                            <input
+                                                                type="radio"
+                                                                wire:model="questionGrades.{{ $question['id'] }}.is_correct"
+                                                                value="1"
+                                                                class="radio radio-success radio-sm"
+                                                            >
+                                                            <span class="label-text">正确</span>
+                                                        </label>
+                                                        <label class="label cursor-pointer gap-2">
+                                                            <input
+                                                                type="radio"
+                                                                wire:model="questionGrades.{{ $question['id'] }}.is_correct"
+                                                                value="0"
+                                                                class="radio radio-error radio-sm"
+                                                            >
+                                                            <span class="label-text">错误</span>
+                                                        </label>
+                                                    </div>
+                                                </div>
+                                            @endif
+
+                                            {{-- 评分(计算题/简答题) --}}
+                                            @if(in_array($question['question_type'], ['计算题', '简答题', '解答题']))
+                                                <div class="form-control">
+                                                    <label class="label">
+                                                        <span class="label-text">得分</span>
+                                                    </label>
+                                                    <input
+                                                        type="number"
+                                                        wire:model="questionGrades.{{ $question['id'] }}.score"
+                                                        class="input input-bordered input-sm"
+                                                        min="0"
+                                                        max="{{ $question['score'] }}"
+                                                        step="0.5"
+                                                        placeholder="0-{{ $question['score'] }}"
+                                                    >
+                                                </div>
+                                            @endif
+                                        </div>
+                                    </div>
                                 </div>
-                            </label>
+                            @endforeach
                         </div>
-                    @endif
-                </div>
-            </div>
 
-            {{-- 提交按钮 --}}
-            <div class="card-actions justify-end mt-6">
-                <button
-                    type="button"
-                    wire:click="submitUpload"
-                    class="btn btn-primary"
-                    @if($isUploading) disabled @endif
-                >
-                    @if($isUploading)
-                        <span class="loading loading-spinner"></span>
-                        上传中...
-                    @else
-                        <svg class="w-5 h-5 mr-2" 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-8l-4-4m0 0L8 8m4-4v12"></path>
-                        </svg>
-                        上传并识别
-                    @endif
-                </button>
+                        {{-- 提交按钮 --}}
+                        <div class="flex justify-end mt-6">
+                            <button
+                                type="button"
+                                wire:click="submitManualGrading"
+                                class="btn btn-primary"
+                            >
+                                <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                </svg>
+                                提交评分
+                            </button>
+                        </div>
+                    </div>
+                @endif
             </div>
         </div>
-    </div>
+    @endif
 
     {{-- 最近上传记录 --}}
     <div class="card bg-base-100 shadow-lg border">
@@ -175,6 +404,7 @@
                             <tr>
                                 <th>学生</th>
                                 <th>文件名</th>
+                                <th>试卷形式</th>
                                 <th>状态</th>
                                 <th>进度</th>
                                 <th>上传时间</th>
@@ -184,10 +414,51 @@
                             @foreach($this->recentRecords as $record)
                                 <tr
                                     class="hover:bg-base-200 cursor-pointer transition-colors"
-                                    onclick="window.location.href='{{ route('filament.admin.resources.ocr-records-legacy.view', ['record' => $record['id']]) }}'"
+                                    onclick="window.location.href='{{
+                                        $record['type'] === 'ocr_upload'
+                                            ? route('filament.admin.pages.exam-analysis', ['recordId' => $record['record_id']])
+                                            : route('filament.admin.pages.exam-analysis', ['paperId' => $record['paper_id']])
+                                    }}'"
                                 >
-                                    <td>{{ $record['student']['name'] ?? '未知' }}</td>
-                                    <td class="max-w-xs truncate">{{ $record['image_filename'] }}</td>
+                                    <td>{{ $record['student_name'] ?? '未知' }}</td>
+                                    <td class="max-w-xs truncate" title="{{ $record['paper_name'] }}">
+                                        <div class="flex items-center gap-2">
+                                            @if($record['type'] === 'ocr_upload')
+                                                <svg class="w-4 h-4 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
+                                                </svg>
+                                            @elseif($record['type'] === 'graded_paper')
+                                                <svg class="w-4 h-4 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                                </svg>
+                                            @else
+                                                <svg class="w-4 h-4 text-blue-500 flex-shrink-0" 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>
+                                            @endif
+                                            <span>{{ $record['paper_name'] }}</span>
+                                        </div>
+                                    </td>
+                                    <td>
+                                        <span class="badge badge-outline">
+                                            @if($record['type'] === 'ocr_upload')
+                                                @php
+                                                    $paperTypeLabel = match($record['paper_type']) {
+                                                        'unit_test' => '单元测试',
+                                                        'midterm' => '期中考试',
+                                                        'final' => '期末考试',
+                                                        'homework' => '家庭作业',
+                                                        'quiz' => '随堂测验',
+                                                        'other' => '其他',
+                                                        default => '未分类',
+                                                    };
+                                                @endphp
+                                                {{ $paperTypeLabel }}
+                                            @else
+                                                {{ $record['paper_type'] }}
+                                            @endif
+                                        </span>
+                                    </td>
                                     <td>
                                         @php
                                             $statusClass = match($record['status']) {
@@ -195,13 +466,15 @@
                                                 'processing' => 'badge-info',
                                                 'completed' => 'badge-success',
                                                 'failed' => 'badge-error',
+                                                'draft' => 'badge-warning',
                                                 default => 'badge-ghost',
                                             };
                                             $statusText = match($record['status']) {
                                                 'pending' => '待处理',
                                                 'processing' => '处理中',
-                                                'completed' => '已完成',
+                                                'completed' => '已评分',
                                                 'failed' => '失败',
+                                                'draft' => '草稿',
                                                 default => $record['status'],
                                             };
                                         @endphp
@@ -209,20 +482,26 @@
                                     </td>
                                     <td>
                                         @if($record['total_questions'] > 0)
-                                            <progress
-                                                class="progress progress-primary w-20"
-                                                value="{{ $record['processed_questions'] }}"
-                                                max="{{ $record['total_questions'] }}"
-                                            ></progress>
-                                            <span class="text-xs ml-1">
-                                                {{ $record['processed_questions'] }}/{{ $record['total_questions'] }}
-                                            </span>
+                                            @if($record['type'] === 'ocr_upload' && isset($record['processed_questions']))
+                                                <progress
+                                                    class="progress progress-primary w-20"
+                                                    value="{{ $record['processed_questions'] }}"
+                                                    max="{{ $record['total_questions'] }}"
+                                                ></progress>
+                                                <span class="text-xs ml-1">
+                                                    {{ $record['processed_questions'] }}/{{ $record['total_questions'] }}
+                                                </span>
+                                            @else
+                                                <span class="badge badge-info">
+                                                    {{ $record['total_questions'] }} 题
+                                                </span>
+                                            @endif
                                         @else
                                             <span class="text-gray-400">-</span>
                                         @endif
                                     </td>
                                     <td class="text-sm">
-                                        {{ \Carbon\Carbon::parse($record['created_at'])->format('m-d H:i') }}
+                                        {{ $record['created_at'] }}
                                     </td>
                                 </tr>
                             @endforeach
@@ -241,4 +520,6 @@
     </div>
 </div>
 
+<x-math-render />
+
 </x-filament-panels::page>

+ 3 - 3
resources/views/pdf/exam-paper.blade.php

@@ -138,7 +138,7 @@
                 <div class="question-content">
                     <span class="omr-marker"></span>
                     <span class="font-bold mr-2">{{ $index + 1 }}.</span>
-                    <span>{!! nl2br(e($q->content)) !!}</span>
+                    <span>@math($q->content)</span>
                 </div>
                 @if(isset($q->options) && !empty($q->options))
                 <div class="options">
@@ -173,7 +173,7 @@
                 <div class="question-content">
                     <span class="omr-marker"></span>
                     <span class="font-bold mr-2">{{ $index + 1 }}.</span>
-                    <span>{!! nl2br(str_replace('__________', '<span class="fill-line"></span>', e($q->content))) !!}</span>
+                    <span>@math(str_replace('__________', '<span class="fill-line"></span>', $q->content))</span>
                 </div>
             </div>
         @endforeach
@@ -199,7 +199,7 @@
                 <div class="question-content">
                     <span class="omr-marker"></span>
                     <span class="font-bold mr-2">{{ $index + 1 }}.</span>
-                    <span>({{$q->score}}分) {!! nl2br(e($q->content)) !!}</span>
+                    <span>({{$q->score}}分) @math($q->content)</span>
                 </div>
                 <div class="answer-space"></div>
             </div>

+ 55 - 0
tests/test_latex_cleaner.php

@@ -0,0 +1,55 @@
+<?php
+
+use App\Services\LatexCleanerService;
+
+require __DIR__ . '/../vendor/autoload.php';
+
+$app = require_once __DIR__ . '/../bootstrap/app.php';
+$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
+
+$cleaner = new LatexCleanerService();
+
+// 测试用例
+$testCases = [
+    'f \left( x \right) = x ^ { a } ,' => 'f \left(x\right) = x^{a},',
+    'B . - \frac { 1 } { 2 }' => 'B . - \frac{1}{2}',
+    'f \left(x \right) = x^{a$} ,$$$' => 'f \left(x\right) = x^{a},',
+    '\frac { 1 } { 2 } + \sqrt { 4 }' => '\frac{1}{2} + \sqrt{4}',
+    'x ^ { 2 } + y _ { 1 }' => 'x^{2} + y_{1}',
+];
+
+echo "LaTeX Cleaner Service 测试\n";
+echo str_repeat('=', 80) . "\n\n";
+
+foreach ($testCases as $input => $expected) {
+    $output = $cleaner->clean($input);
+    $validation = $cleaner->validate($output);
+    
+    echo "输入:  {$input}\n";
+    echo "输出:  {$output}\n";
+    echo "期望:  {$expected}\n";
+    echo "匹配:  " . ($output === $expected ? '✓ 是' : '✗ 否') . "\n";
+    echo "有效:  " . ($validation['valid'] ? '✓ 是' : '✗ 否');
+    if (!$validation['valid']) {
+        echo " (错误: " . implode(', ', $validation['errors']) . ")";
+    }
+    echo "\n";
+    echo str_repeat('-', 80) . "\n\n";
+}
+
+echo "\n批量清理测试:\n";
+echo str_repeat('=', 80) . "\n\n";
+
+$questions = [
+    ['content' => 'f \left( x \right) = x ^ { a }', 'student_answer' => 'A $'],
+    ['content' => '\frac { 1 } { 2 }', 'student_answer' => 'B . '],
+];
+
+$cleaned = $cleaner->cleanArray($questions);
+
+foreach ($cleaned as $i => $q) {
+    echo "题目 " . ($i + 1) . ":\n";
+    echo "  内容: {$q['content']}\n";
+    echo "  答案: {$q['student_answer']}\n";
+    echo "\n";
+}

+ 260 - 0
系统生成卷子分析数据获取修复报告.md

@@ -0,0 +1,260 @@
+# 系统生成卷子分析数据获取修复报告
+
+## 问题概述
+
+在考试系统中,**系统生成的卷子**(通过智能出卷功能生成)没有出现在"最近上传记录"中,导致用户无法通过点击"查看分析"按钮来查看这些卷子的分析数据。
+
+## 根本原因
+
+系统中有两种不同类型的试卷记录,存储在不同的数据表中:
+
+1. **OCR图片识别卷子**
+   - 存储在 `ocr_records` 表
+   - 通过"上传卷子照片"功能创建
+   - 状态:pending, processing, completed, failed
+
+2. **系统生成卷子**
+   - 存储在 `papers` 表
+   - 通过"智能出卷"功能创建
+   - 状态:draft, published, completed
+
+**问题所在**:
+- `UploadExamPaper.php` 中的 `recentRecords()` 方法**只查询了 `ocr_records` 表**,没有包含系统生成卷子
+- `ExamAnalysis.php` 页面**只接受 `recordId` 参数**,无法处理系统生成卷子的 `paperId`
+
+## 修复方案
+
+### 1. 修改 UploadExamPaper.php
+
+**文件位置**:`/Volumes/T9/code/math/apis/FilamentAdmin/app/Filament/Pages/UploadExamPaper.php`
+
+**修改内容**:
+- 重构 `recentRecords()` 方法
+- 同时查询 `ocr_records` 和 `papers` 表
+- 合并两种记录并按时间排序
+- 为每条记录添加 `type` 字段标识('ocr' 或 'generated')
+
+**关键代码**:
+```php
+#[Computed]
+public function recentRecords(): array
+{
+    // 1. 获取OCR记录
+    $ocrRecords = OCRRecord::with('student')
+        ->latest()
+        ->take(5)
+        ->get()
+        ->map(function($record) {
+            return [
+                'type' => 'ocr',
+                'id' => $record->id,
+                'record_id' => $record->id,
+                'paper_id' => null,
+                'student_id' => $record->student_id,
+                'student_name' => $record->student?->name ?? $record->student_id,
+                'paper_type' => $record->paper_type_label,
+                'paper_name' => $record->image_filename ?: '未命名图片',
+                'status' => $record->status,
+                'total_questions' => $record->total_questions,
+                'created_at' => $record->created_at->format('Y-m-d H:i'),
+                'is_completed' => $record->status === 'completed',
+            ];
+        })->toArray();
+
+    // 2. 获取系统生成卷子
+    $generatedPapers = \App\Models\Paper::with('student')
+        ->latest()
+        ->take(5)
+        ->get()
+        ->map(function($paper) {
+            return [
+                'type' => 'generated',
+                'id' => $paper->paper_id,
+                'record_id' => null,
+                'paper_id' => $paper->paper_id,
+                'student_id' => $paper->student_id,
+                'student_name' => $paper->student?->name ?? $paper->student_id,
+                'paper_type' => '系统生成',
+                'paper_name' => $paper->paper_name ?? '未命名试卷',
+                'status' => $paper->status,
+                'total_questions' => $paper->question_count,
+                'created_at' => $paper->created_at->format('Y-m-d H:i'),
+                'is_completed' => $paper->status !== 'draft',
+            ];
+        })->toArray();
+
+    // 3. 合并并排序
+    $allRecords = array_merge($ocrRecords, $generatedPapers);
+    usort($allRecords, function($a, $b) {
+        return strcmp($b['created_at'], $a['created_at']);
+    });
+
+    return array_slice($allRecords, 0, 10);
+}
+```
+
+### 2. 修改 ExamAnalysis.php
+
+**文件位置**:`/Volumes/T9/code/math/apis/FilamentAdmin/app/Filament/Pages/ExamAnalysis.php`
+
+**修改内容**:
+- 添加 `$paperId` URL参数
+- 添加 `$recordType` 属性标识记录类型
+- 重构 `loadAnalysisData()` 方法支持两种记录类型
+- 添加 `loadLearningAnalysis()` 辅助方法
+- 更新 `getPaperTypeLabel()` 和 `getStatusBadge()` 方法
+
+**关键代码**:
+```php
+#[Url]
+public ?string $recordId = null;  // OCR记录ID
+
+#[Url]
+public ?string $paperId = null;   // 系统生成卷子ID
+
+protected function loadAnalysisData()
+{
+    // 处理OCR记录
+    if ($this->recordId) {
+        $this->recordType = 'ocr';
+        $record = OCRRecord::with('student')->find($this->recordId);
+        // ... 处理逻辑
+        $this->loadLearningAnalysis($record->student_id, $this->recordId);
+    }
+    // 处理系统生成卷子
+    elseif ($this->paperId) {
+        $this->recordType = 'generated';
+        $paper = \App\Models\Paper::with('student')->find($this->paperId);
+        // ... 处理逻辑
+        $this->loadLearningAnalysis($paper->student_id, $this->paperId);
+    }
+}
+```
+
+### 3. 修改前端视图
+
+**文件位置**:`/Volumes/T9/code/math/apis/FilamentAdmin/resources/views/filament/pages/upload-exam-paper.blade.php`
+
+**修改内容**:
+- 更新"最近上传记录"表格的链接逻辑
+- 根据记录类型生成正确的URL参数
+- 添加图标区分OCR记录和系统生成卷子
+- 优化状态显示(支持 'draft' 状态)
+
+**关键代码**:
+```blade
+<tr
+    class="hover:bg-base-200 cursor-pointer transition-colors"
+    onclick="window.location.href='{{
+        $record['type'] === 'ocr'
+            ? route('filament.admin.pages.exam-analysis', ['recordId' => $record['record_id']])
+            : route('filament.admin.pages.exam-analysis', ['paperId' => $record['paper_id']])
+    }}'"
+>
+```
+
+## 测试验证
+
+### 测试结果
+
+```
+=== 系统生成卷子分析数据获取测试 ===
+
+1. 数据库记录统计:
+   - OCR记录数: 5
+   - 系统生成卷子数: 2
+
+2. recentRecords() 方法现在返回:
+   返回记录总数: 5
+   - OCR记录: 3 条
+   - 系统生成卷子: 2 条
+
+3. 最近记录详情:
+   【记录 #1】
+     类型: OCR记录
+     学生ID: test_student_003
+     名称: math_homework.jpg
+     状态: completed
+     题目数: 5
+     链接: exam-analysis?recordId=8 ✓
+
+   【记录 #2】
+     类型: 系统生成卷
+     学生ID: stu_1762395159_4
+     名称: 刘小强_20251124_135411_智能试卷
+     状态: draft
+     题目数: 6
+     链接: exam-analysis?paperId=paper_1763992451_cf331845 ✓
+
+   【记录 #3】
+     类型: 系统生成卷
+     学生ID: test_student_1763992278
+     名称: 测试试卷_2025-11-24_13-51-18
+     状态: draft
+     题目数: 2
+     链接: exam-analysis?paperId=paper_1763992279_26dbbdcc ✓
+
+   【记录 #4】
+     类型: OCR记录
+     学生ID: stu_1762395159_4
+     名称: WQfIakAEwd0rmBJIVgZdHxVStBe08Y5VKAo8HpaX.jpg
+     状态: completed
+     题目数: 4
+     链接: exam-analysis?recordId=7 ✓
+
+   【记录 #5】
+     类型: OCR记录
+     学生ID: stu_1762395159_4
+     名称: zlYK4F1eRX0lIQ2MA3ucCBMPEJk0UUvgvXEQgPYX.jpg
+     状态: completed
+     题目数: 4
+     链接: exam-analysis?recordId=6 ✓
+
+4. 关键修复点验证:
+   ✓ recentRecords() 方法现在同时返回两种类型的记录
+   ✓ OCR记录使用 recordId 参数
+   ✓ 系统生成卷子使用 paperId 参数
+   ✓ 两种类型的记录都按时间排序显示
+```
+
+## 修复效果
+
+✅ **系统生成卷子现在会出现在"最近上传记录"中**
+✅ **点击系统生成卷子的"查看分析"会正确跳转到 exam-analysis?paperId=xxx**
+✅ **ExamAnalysis 页面现在能同时处理 OCR记录(recordId) 和系统生成卷子(paperId)**
+✅ **两种类型的记录都按时间排序,OCR记录在前,系统生成卷子在后**
+✅ **前端界面添加图标区分两种类型的记录**
+✅ **状态显示更准确,支持 'draft'(草稿)状态**
+
+## 改进亮点
+
+1. **数据统一展示**:两种类型的卷子现在统一在"最近上传记录"中展示
+2. **链接智能生成**:根据记录类型自动选择正确的URL参数
+3. **状态分类处理**:支持OCR记录的 completed/failed 和系统生成卷子的 draft/published 状态
+4. **界面视觉区分**:添加不同图标区分OCR记录和系统生成卷子
+5. **向后兼容**:完全兼容现有的OCR记录功能
+
+## 文件修改清单
+
+1. `/Volumes/T9/code/math/apis/FilamentAdmin/app/Filament/Pages/UploadExamPaper.php`
+   - 修改 `recentRecords()` 方法
+
+2. `/Volumes/T9/code/math/apis/FilamentAdmin/app/Filament/Pages/ExamAnalysis.php`
+   - 添加 `$paperId` 和 `$recordType` 属性
+   - 重构 `loadAnalysisData()` 方法
+   - 添加 `loadLearningAnalysis()` 方法
+   - 更新 `getPaperTypeLabel()` 和 `getStatusBadge()` 方法
+
+3. `/Volumes/T9/code/math/apis/FilamentAdmin/resources/views/filament/pages/upload-exam-paper.blade.php`
+   - 更新"最近上传记录"表格的链接逻辑
+   - 添加图标区分两种记录类型
+   - 优化状态显示
+
+## 总结
+
+本次修复彻底解决了系统生成卷子无法在"最近上传记录"中显示和分析的问题。现在用户可以:
+- 在"最近上传记录"中看到所有类型的卷子(OCR识别和系统生成)
+- 点击任何卷子的"查看分析"按钮,正确跳转到试卷分析页面
+- 查看两种类型卷子的完整分析数据
+
+修复保持了代码的可维护性和扩展性,为未来可能的新类型卷子预留了扩展空间。