Jelajahi Sumber

优化公式识别等 UI 效果

yemeishu 1 bulan lalu
induk
melakukan
444032da6e

+ 1 - 3
app/Filament/Pages/QuestionManagement.php

@@ -5,7 +5,6 @@ namespace App\Filament\Pages;
 use App\Services\QuestionServiceApi;
 use App\Services\KnowledgeGraphService;
 use App\Services\QuestionBankService;
-use App\Livewire\Traits\WithMathRender;
 use BackedEnum;
 use Filament\Notifications\Notification;
 use Filament\Pages\Page;
@@ -14,14 +13,13 @@ use Livewire\Attributes\Computed;
 
 class QuestionManagement extends Page
 {
-    use WithMathRender;
 
     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 string $view = 'filament.pages.question-management';
+    protected string $view = 'filament.pages.question-management-simple';
 
     public ?string $search = null;
     public ?string $selectedKpCode = null;

+ 1134 - 0
app/Filament/Pages/SimulatedGrading.php

@@ -0,0 +1,1134 @@
+<?php
+
+namespace App\Filament\Pages;
+
+use App\Services\LearningAnalyticsService;
+use BackedEnum;
+use Filament\Pages\Page;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+use UnitEnum;
+use Livewire\Attributes\Layout;
+use Livewire\Attributes\Title;
+
+class SimulatedGrading extends Page
+{
+    use \Filament\Pages\Concerns\InteractsWithFormActions;
+
+    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-check';
+
+    protected static string|UnitEnum|null $navigationGroup = '学习分析';
+
+    protected static ?string $navigationLabel = '专题测试';
+
+    protected static ?int $navigationSort = 2;
+
+    protected ?string $heading = '专题测试';
+
+    protected string $view = 'filament.pages.simulated-grading';
+
+    public string $studentId = '';
+    public string $teacherId = '';
+    public array $teachers = [];
+    public array $students = [];
+
+    // 模拟判卷相关
+    public array $exerciseQuestions = [];
+    public array $exerciseAnswers = [];
+    public ?string $selectedKnowledgePoint = null;
+    public array $availableKnowledgePoints = [];
+    public array $availableSkills = [];
+    public array $selectedSkills = [];
+    public string $currentBatchId = '';
+    public int $questionsPerSet = 5;
+    protected array $knowledgePointCodeIndex = [];
+    protected array $knowledgePointIdIndex = [];
+    public bool $isLoading = false;
+
+    // 测试相关
+    public int $testClickCount = 0;
+
+    // 答题历史相关
+    public array $exerciseHistory = [];
+    public int $historyCurrentPage = 1;
+    public int $historyPerPage = 10;
+    public int $historyTotal = 0;
+    public int $historyTotalPages = 0;
+
+    public function mount(Request $request): void
+    {
+        // 加载老师列表
+        $this->loadTeachers();
+
+        // 从请求中获取老师ID或使用默认值
+        $this->teacherId = $request->input('teacher_id', $this->getDefaultTeacherId());
+
+        // 根据老师ID加载学生列表
+        $this->loadStudentsByTeacher();
+
+        // 从请求中获取学生ID或使用默认值
+        $this->studentId = $request->input('student_id', $this->getDefaultStudentId());
+
+        // 初始化知识点和技能数据
+        $this->loadKnowledgePointsAndSkills();
+
+        // 加载默认学生的答题历史
+        if (!empty($this->studentId)) {
+            $this->loadExerciseHistory();
+        }
+    }
+
+    /**
+     * 获取默认老师ID(列表中的第一个老师)
+     */
+    private function getDefaultTeacherId(): string
+    {
+        return !empty($this->teachers) ? $this->teachers[0]->teacher_id : '';
+    }
+
+    /**
+     * 获取默认学生ID(列表中的第一个学生)
+     */
+    private function getDefaultStudentId(): string
+    {
+        return !empty($this->students) ? $this->students[0]->student_id : '';
+    }
+
+    /**
+     * 从MySQL加载老师列表
+     */
+    public function loadTeachers(): void
+    {
+        try {
+            // 首先获取teachers表中的老师
+            $this->teachers = DB::connection('remote_mysql')
+                ->table('teachers as t')
+                ->leftJoin('users as u', 't.teacher_id', '=', 'u.user_id')
+                ->select(
+                    't.teacher_id',
+                    't.name',
+                    't.subject',
+                    'u.username',
+                    'u.email'
+                )
+                ->orderBy('t.name')
+                ->get()
+                ->toArray();
+
+            // 如果有学生但没有对应的老师记录,添加一个"未知老师"条目
+            $teacherIds = array_column($this->teachers, 'teacher_id');
+            $missingTeacherIds = DB::connection('remote_mysql')
+                ->table('students as s')
+                ->distinct()
+                ->whereNotIn('s.teacher_id', $teacherIds)
+                ->pluck('teacher_id')
+                ->toArray();
+
+            if (!empty($missingTeacherIds)) {
+                foreach ($missingTeacherIds as $missingId) {
+                    $this->teachers[] = (object) [
+                        'teacher_id' => $missingId,
+                        'name' => '未知老师 (' . $missingId . ')',
+                        'subject' => '未知',
+                        'username' => null,
+                        'email' => null
+                    ];
+                }
+
+                // 重新排序
+                usort($this->teachers, function($a, $b) {
+                    return strcmp($a->name, $b->name);
+                });
+            }
+        } catch (\Exception $e) {
+            Log::error('加载老师列表失败', [
+                'error' => $e->getMessage()
+            ]);
+            $this->teachers = [];
+        }
+    }
+
+    /**
+     * 根据老师ID加载学生列表
+     */
+    public function loadStudentsByTeacher(): void
+    {
+        try {
+            if (empty($this->teacherId)) {
+                $this->students = [];
+                return;
+            }
+
+            $this->students = DB::connection('remote_mysql')
+                ->table('students as s')
+                ->leftJoin('users as u', 's.student_id', '=', 'u.user_id')
+                ->where('s.teacher_id', $this->teacherId)
+                ->select(
+                    's.student_id',
+                    's.name',
+                    's.grade',
+                    's.class_name',
+                    'u.username',
+                    'u.email'
+                )
+                ->orderBy('s.name')
+                ->get()
+                ->toArray();
+        } catch (\Exception $e) {
+            Log::error('加载学生列表失败', [
+                'teacher_id' => $this->teacherId,
+                'error' => $e->getMessage()
+            ]);
+            $this->students = [];
+        }
+    }
+
+    /**
+     * 老师改变时重新加载学生列表
+     */
+    public function updatedTeacherId(): void
+    {
+        $this->loadStudentsByTeacher();
+        // 清空之前选中的学生ID
+        $this->studentId = '';
+        // 自动加载第一个学生的数据
+        $this->studentId = $this->getDefaultStudentId();
+
+        // 加载该学生的答题历史
+        if (!empty($this->studentId)) {
+            $this->loadExerciseHistory();
+        }
+    }
+
+    /**
+     * 学生ID更新后的处理
+     */
+    public function updatedStudentId(): void
+    {
+        // 清空之前的答题数据
+        $this->exerciseQuestions = [];
+        $this->exerciseAnswers = [];
+        $this->currentBatchId = '';
+
+        // 重新加载该学生的答题历史
+        if (!empty($this->studentId)) {
+            $this->loadExerciseHistory();
+        }
+    }
+
+    /**
+     * 加载学生答题历史
+     */
+    public function loadExerciseHistory(): void
+    {
+        if (empty($this->studentId)) {
+            $this->exerciseHistory = [];
+            $this->historyTotal = 0;
+            $this->historyTotalPages = 0;
+            return;
+        }
+
+        try {
+            // 使用Eloquent ORM查询
+            $query = \App\Models\StudentExercise::where('student_id', $this->studentId)
+                ->orderBy('created_at', 'desc');
+
+            // 获取总数
+            $this->historyTotal = $query->count();
+
+            // 计算总页数
+            $this->historyTotalPages = ceil($this->historyTotal / $this->historyPerPage);
+
+            // 获取分页数据并处理数学公式
+            $exercises = $query->skip(($this->historyCurrentPage - 1) * $this->historyPerPage)
+                ->take($this->historyPerPage)
+                ->get();
+
+            // 使用模型的方法处理数学公式
+            $this->exerciseHistory = $exercises->map(function ($exercise) {
+                return $exercise->toProcessedArray();
+            })->toArray();
+
+            // 渲染历史记录中的数学符号
+            $this->dispatch('math:render');
+
+            Log::info('成功加载答题历史', [
+                'student_id' => $this->studentId,
+                'total' => $this->historyTotal,
+                'current_page' => $this->historyCurrentPage,
+                'per_page' => $this->historyPerPage
+            ]);
+
+        } catch (\Exception $e) {
+            Log::error('加载答题历史失败', [
+                'student_id' => $this->studentId,
+                'error' => $e->getMessage()
+            ]);
+            $this->exerciseHistory = [];
+            $this->historyTotal = 0;
+            $this->historyTotalPages = 0;
+        }
+    }
+
+    /**
+     * 跳转到指定历史页面
+     */
+    public function gotoHistoryPage(int $page): void
+    {
+        if ($page >= 1 && $page <= $this->historyTotalPages) {
+            $this->historyCurrentPage = $page;
+            $this->loadExerciseHistory();
+        }
+    }
+
+    /**
+     * 上一页
+     */
+    public function previousHistoryPage(): void
+    {
+        if ($this->historyCurrentPage > 1) {
+            $this->historyCurrentPage--;
+            $this->loadExerciseHistory();
+        }
+    }
+
+    /**
+     * 下一页
+     */
+    public function nextHistoryPage(): void
+    {
+        if ($this->historyCurrentPage < $this->historyTotalPages) {
+            $this->historyCurrentPage++;
+            $this->loadExerciseHistory();
+        }
+    }
+
+    /**
+     * 每页显示数量变化
+     */
+    public function updatedHistoryPerPage(): void
+    {
+        $this->historyCurrentPage = 1;
+        $this->loadExerciseHistory();
+    }
+
+    /**
+     * 获取历史页面列表(用于分页导航)
+     */
+    public function getHistoryPages(): array
+    {
+        $pages = [];
+        $totalPages = $this->historyTotalPages;
+        $currentPage = $this->historyCurrentPage;
+        $start = max(1, $currentPage - 2);
+        $end = min($totalPages, $currentPage + 2);
+
+        for ($i = $start; $i <= $end; $i++) {
+            $pages[] = $i;
+        }
+
+        return $pages;
+    }
+
+    /**
+     * 加载知识点和技能数据
+     */
+    public function loadKnowledgePointsAndSkills(): void
+    {
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+
+            // 从知识图谱API获取知识点数据
+            $kpResponse = Http::timeout(10)
+                ->get($knowledgeApiBase . '/knowledge-points/', [
+                    'page' => 1,
+                    'per_page' => 100
+                ]);
+
+            if ($kpResponse->successful()) {
+                $kpData = $kpResponse->json();
+                $this->availableKnowledgePoints = $kpData['data'] ?? $kpData ?? [];
+
+                // 格式化知识点数据,确保包含必要的字段
+                $this->availableKnowledgePoints = array_map(function($kp) {
+                    return [
+                        'id' => (string)($kp['id'] ?? $kp['kp_id'] ?? uniqid()),
+                        'code' => $kp['kp_code'] ?? $kp['kp_id'] ?? $kp['code'] ?? 'KP_UNKNOWN',
+                        'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kp['name'] ?? $kp['kp_code'] ?? '未知知识点',
+                        'subject' => $kp['category'] ?? '数学'
+                    ];
+                }, $this->availableKnowledgePoints);
+            } else {
+                throw new \Exception('知识图谱API调用失败: ' . $kpResponse->status());
+            }
+
+            // 从知识图谱API获取技能数据
+            $skillResponse = Http::timeout(10)
+                ->get($knowledgeApiBase . '/skills/', [
+                    'page' => 1,
+                    'per_page' => 50
+                ]);
+
+            if ($skillResponse->successful()) {
+                $skillData = $skillResponse->json();
+                $this->availableSkills = $skillData['data'] ?? $skillData ?? [];
+
+                // 格式化技能数据
+                $this->availableSkills = array_map(function($skill) {
+                    return [
+                        'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
+                        'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
+                        'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
+                        'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
+                    ];
+                }, $this->availableSkills);
+            } else {
+                throw new \Exception('技能API调用失败: ' . $skillResponse->status());
+            }
+
+            Log::info('成功从知识图谱API加载数据', [
+                'knowledge_points_count' => count($this->availableKnowledgePoints),
+                'skills_count' => count($this->availableSkills)
+            ]);
+
+        } catch (\Exception $e) {
+            Log::error('从知识图谱API加载知识点和技能数据失败,使用备用数据', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            // 使用模拟数据作为备用
+            $this->availableKnowledgePoints = [
+                ['id' => 'factor_1', 'code' => 'factor_1', 'name' => '因式分解基础', 'subject' => '数学'],
+                ['id' => 'factor_2', 'code' => 'factor_2', 'name' => '提取公因式', 'subject' => '数学'],
+                ['id' => 'factor_3', 'code' => 'factor_3', 'name' => '平方差公式', 'subject' => '数学'],
+                ['id' => 'factor_4', 'code' => 'factor_4', 'name' => '完全平方公式', 'subject' => '数学'],
+                ['id' => 'factor_5', 'code' => 'factor_5', 'name' => '分组分解法', 'subject' => '数学'],
+                ['id' => 'factor_6', 'code' => 'factor_6', 'name' => '立方和差公式', 'subject' => '数学'],
+                ['id' => 'factor_7', 'code' => 'factor_7', 'name' => '十字相乘法', 'subject' => '数学'],
+                ['id' => 'factor_8', 'code' => 'factor_8', 'name' => '综合因式分解', 'subject' => '数学'],
+            ];
+
+            $this->availableSkills = [
+                ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'],
+                ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'],
+                ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'],
+                ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'],
+                ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'],
+                ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'],
+            ];
+        }
+
+        $this->buildKnowledgePointIndexes();
+
+        Log::info('知识点和技能数据加载完成');
+    }
+
+    /**
+     * 为知识点构建索引映射
+     */
+    private function buildKnowledgePointIndexes(): void
+    {
+        $this->knowledgePointCodeIndex = [];
+        $this->knowledgePointIdIndex = [];
+
+        foreach ($this->availableKnowledgePoints as $kp) {
+            // 使用格式化后的字段:code 对应 kp_code
+            if (!empty($kp['code'])) {
+                $this->knowledgePointCodeIndex[(string) $kp['code']] = $kp;
+            }
+            // 使用格式化后的字段:id
+            if (!empty($kp['id'])) {
+                $this->knowledgePointIdIndex[(string) $kp['id']] = $kp;
+            }
+        }
+    }
+
+    /**
+     * 知识点选择变化时更新技能列表
+     */
+    public function updatedSelectedKnowledgePoint(): void
+    {
+        // 清空已选择的技能
+        $this->selectedSkills = [];
+
+        // 如果没有选择知识点,加载所有技能
+        if (empty($this->selectedKnowledgePoint)) {
+            $this->loadAllSkills();
+            return;
+        }
+
+        // 根据选择的知识点获取相关技能
+        $this->loadSkillsForKnowledgePoint($this->selectedKnowledgePoint);
+    }
+
+    /**
+     * 根据知识点加载相关技能
+     */
+    private function loadSkillsForKnowledgePoint(string $knowledgePointId): void
+    {
+        $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+
+        // 根据knowledgePointId查找对应的kp_code
+        $kpCode = null;
+        foreach ($this->availableKnowledgePoints as $kp) {
+            if ($kp['id'] === $knowledgePointId) {
+                $kpCode = $kp['code']; // 使用kp_code作为API参数
+                break;
+            }
+        }
+
+        if (!$kpCode) {
+            Log::warning('未找到知识点对应的kp_code', ['knowledge_point_id' => $knowledgePointId]);
+            $this->availableSkills = [];
+            return;
+        }
+
+        Log::info('准备调用知识点详情API', [
+            'kp_code' => $kpCode,
+            'knowledge_point_id' => $knowledgePointId
+        ]);
+
+        // 直接从知识点详情API获取技能列表
+        $kpDetailResponse = Http::timeout(10)
+            ->get($knowledgeApiBase . '/knowledge-points/' . $kpCode);
+
+        $kpData = $kpDetailResponse->json();
+
+        // 转换技能数据格式,匹配模板期望的字段名
+        $skills = $kpData['skills'] ?? [];
+        $this->availableSkills = array_map(function($skill) {
+            return [
+                'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? ''),
+                'code' => $skill['skill_code'] ?? '',
+                'name' => $skill['skill_name'] ?? '',
+                'category' => $skill['skill_type'] ?? ''
+            ];
+        }, $skills);
+
+        Log::info('设置技能列表', [
+            'count' => count($this->availableSkills),
+            'skills' => $this->availableSkills
+        ]);
+    }
+
+    /**
+     * 加载所有技能
+     */
+    private function loadAllSkills(): void
+    {
+        // 先保存当前技能列表作为兜底
+        $fallbackSkills = $this->availableSkills;
+
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+
+            $skillResponse = Http::timeout(10)
+                ->get($knowledgeApiBase . '/skills/', [
+                    'page' => 1,
+                    'per_page' => 50
+                ]);
+
+            if ($skillResponse->successful()) {
+                $skillData = $skillResponse->json();
+                $skills = $skillData['data'] ?? $skillData ?? [];
+
+                // 只有当API返回有效数据时才更新技能列表
+                if (!empty($skills) && is_array($skills)) {
+                    $this->availableSkills = array_map(function($skill) {
+                        return [
+                            'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
+                            'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
+                            'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
+                            'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
+                        ];
+                    }, $skills);
+
+                    Log::info('成功加载所有技能', [
+                        'skills_count' => count($this->availableSkills)
+                    ]);
+                    return;
+                }
+            }
+
+            // 如果API调用失败或返回空数据,使用默认技能列表
+            Log::warning('加载所有技能失败或为空,使用默认技能列表');
+            $this->useDefaultSkills();
+
+        } catch (\Exception $e) {
+            Log::error('加载所有技能失败,使用默认技能列表', ['error' => $e->getMessage()]);
+            $this->useDefaultSkills();
+        }
+    }
+
+    /**
+     * 使用默认技能列表
+     */
+    private function useDefaultSkills(): void
+    {
+        $this->availableSkills = [
+            ['id' => 'calculation', 'code' => 'calculation', 'name' => '计算能力', 'category' => '基础技能'],
+            ['id' => 'reasoning', 'code' => 'reasoning', 'name' => '逻辑推理', 'category' => '思维技能'],
+            ['id' => 'pattern_recognition', 'code' => 'pattern_recognition', 'name' => '模式识别', 'category' => '认知技能'],
+            ['id' => 'algebraic_manipulation', 'code' => 'algebraic_manipulation', 'name' => '代数运算', 'category' => '专业技能'],
+            ['id' => 'problem_solving', 'code' => 'problem_solving', 'name' => '解题能力', 'category' => '专业技能'],
+            ['id' => 'analysis', 'code' => 'analysis', 'name' => '分析能力', 'category' => '思维技能'],
+        ];
+    }
+
+    /**
+     * 通过知识点 code 获取详情
+     */
+    private function findKnowledgePointByCode(?string $kpCode): ?array
+    {
+        if (empty($kpCode)) {
+            return null;
+        }
+
+        if (isset($this->knowledgePointCodeIndex[$kpCode])) {
+            return $this->knowledgePointCodeIndex[$kpCode];
+        }
+
+        $fetched = $this->fetchKnowledgePointFromApi($kpCode);
+        if ($fetched) {
+            $this->availableKnowledgePoints[] = $fetched;
+            $this->buildKnowledgePointIndexes();
+        }
+
+        return $fetched;
+    }
+
+    /**
+     * 调用知识图谱 API 获取知识点
+     */
+    private function fetchKnowledgePointFromApi(string $kpCode): ?array
+    {
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+            $response = Http::timeout(10)->get($knowledgeApiBase . '/knowledge-points/' . $kpCode);
+
+            if ($response->successful()) {
+                $kp = $response->json();
+                return [
+                    'id' => (string) ($kp['id'] ?? $kp['kp_id'] ?? $kpCode),
+                    'code' => $kp['kp_code'] ?? $kpCode,
+                    'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kpCode,
+                    'subject' => $kp['category'] ?? '数学'
+                ];
+            }
+        } catch (\Exception $e) {
+            Log::warning('获取知识点详情失败', [
+                'kp_code' => $kpCode,
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return null;
+    }
+
+    /**
+     * 获取知识点 ID(根据 code)
+     */
+    private function findKnowledgePointIdByCode(?string $kpCode): ?string
+    {
+        $kp = $this->findKnowledgePointByCode($kpCode);
+        if (!$kp || empty($kp['id'])) {
+            return null;
+        }
+
+        return (string) $kp['id'];
+    }
+
+    /**
+     * 生成模拟题目(备用方案)
+     */
+    private function generateMockQuestion(): array
+    {
+        // 如果用户选择了知识点,使用选中的知识点的kp_code
+        if ($this->selectedKnowledgePoint) {
+            $selectedKpCode = null;
+            foreach ($this->availableKnowledgePoints as $kp) {
+                if ($kp['id'] === $this->selectedKnowledgePoint) {
+                    $selectedKpCode = $kp['code'];
+                    break;
+                }
+            }
+
+            // 如果找到了选中的知识点的code,使用它
+            if ($selectedKpCode) {
+                $mockQuestions = [
+                    ['content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2],
+                    ['content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3],
+                    ['content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2],
+                    ['content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2],
+                    ['content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3],
+                ];
+                $question = $mockQuestions[array_rand($mockQuestions)];
+                $question['type'] = '因式分解';
+                $question['kp_code'] = $selectedKpCode;
+                $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint;
+                return $question;
+            }
+        }
+
+        // 如果没有选择知识点,使用默认的
+        $types = [
+            ['type' => '因式分解', 'kp_code' => 'KP7001', 'content' => '因式分解:x² - 9', 'answer' => '(x+3)(x-3)', 'difficulty' => 2],
+            ['type' => '因式分解', 'kp_code' => 'KP8001', 'content' => '因式分解:2x² + 5x + 2', 'answer' => '(2x+1)(x+2)', 'difficulty' => 3],
+            ['type' => '因式分解', 'kp_code' => 'KP8002', 'content' => '因式分解:x² + 6x + 9', 'answer' => '(x+3)²', 'difficulty' => 2],
+            ['type' => '因式分解', 'kp_code' => 'KP8003', 'content' => '因式分解:x² - 4x + 4', 'answer' => '(x-2)²', 'difficulty' => 2],
+            ['type' => '因式分解', 'kp_code' => 'KP8004', 'content' => '因式分解:3x² - 12', 'answer' => '3(x+2)(x-2)', 'difficulty' => 3],
+        ];
+
+        $question = $types[array_rand($types)];
+        $kpMeta = $this->getDefaultKnowledgePointMeta();
+
+        $question['kp_code'] = $question['kp_code'] ?? 'KP_UNKNOWN';
+        $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']) ?? $kpMeta['id'];
+
+        return $question;
+    }
+
+    /**
+     * 获取一个默认的知识点(用于兜底数据)
+     */
+    private function getDefaultKnowledgePointMeta(): array
+    {
+        if (!empty($this->availableKnowledgePoints)) {
+            $kp = $this->availableKnowledgePoints[array_rand($this->availableKnowledgePoints)];
+            return [
+                'id' => isset($kp['id']) ? (string) $kp['id'] : null,
+                'code' => $kp['code'] ?? null
+            ];
+        }
+
+        return [
+            'id' => null,
+            'code' => 'KP_UNKNOWN'
+        ];
+    }
+
+    /**
+     * 从题库API获取多道题目
+     */
+    private function fetchMultipleQuestionsFromBank(int $count): array
+    {
+        try {
+            $questionBankApiBase = config('services.question_bank_api.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015'));
+
+            // 调用题库API一次性获取所有题目
+            // 如果用户选择了知识点,传入知识点参数
+            $params = [
+                'limit' => $count,
+                'type' => 'factorization'
+            ];
+
+            // 如果用户选择了知识点,传入kp_code
+            if ($this->selectedKnowledgePoint) {
+                foreach ($this->availableKnowledgePoints as $kp) {
+                    if ($kp['id'] === $this->selectedKnowledgePoint) {
+                        $params['kp_code'] = $kp['code'];
+                        break;
+                    }
+                }
+            }
+
+            $response = Http::timeout(10)
+                ->get($questionBankApiBase . '/questions', $params);
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $questions = $data['data'] ?? [];
+
+                $processedQuestions = [];
+                foreach ($questions as $question) {
+                    $kpCode = $question['kp_code'] ?? null;
+                    $knowledgePointId = $question['knowledge_point_id'] ?? null;
+
+                    // 如果用户选择了知识点,优先使用选中的知识点
+                    if ($this->selectedKnowledgePoint) {
+                        // 查找选中的知识点的kp_code
+                        $selectedKpCode = null;
+                        foreach ($this->availableKnowledgePoints as $kp) {
+                            if ($kp['id'] === $this->selectedKnowledgePoint) {
+                                $selectedKpCode = $kp['code'];
+                                break;
+                            }
+                        }
+                        if ($selectedKpCode) {
+                            $kpCode = $selectedKpCode;
+                            $knowledgePointId = (string) $this->selectedKnowledgePoint;
+                        }
+                    } else {
+                        // 如果没有选择知识点,使用API返回的或查找
+                        if ($kpCode && !$knowledgePointId) {
+                            $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
+                        }
+
+                        if (!$kpCode && $knowledgePointId) {
+                            $kpCode = $this->findKnowledgePointCodeById((string) $knowledgePointId);
+                        }
+
+                        if (!$kpCode) {
+                            $kpCode = $this->getDefaultKnowledgePointMeta()['code'];
+                        }
+
+                        if (!$knowledgePointId && $kpCode) {
+                            $knowledgePointId = $this->findKnowledgePointIdByCode($kpCode);
+                        }
+                    }
+
+                    $processedQuestions[] = [
+                        'id' => $question['question_code'] ?? 'Q_' . time() . '_' . uniqid(),
+                        'content' => $question['stem'] ?? '',
+                        'answer' => $question['solution'] ?? '',
+                        'type' => '因式分解',
+                        'difficulty' => $question['difficulty'] ?? rand(1, 5),
+                        'kp_code' => $kpCode ?? 'KP_UNKNOWN',
+                        'knowledge_point_id' => $knowledgePointId,
+                        'skill' => $question['skill'] ?? 'unknown'
+                    ];
+
+                    // 达到指定数量就停止处理
+                    if (count($processedQuestions) >= $count) {
+                        break;
+                    }
+                }
+
+                // 如果API返回的题目不足,补充模拟题目
+                while (count($processedQuestions) < $count) {
+                    $mockQuestion = $this->generateMockQuestion();
+                    // 重新生成唯一ID
+                    $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
+                    $processedQuestions[] = $mockQuestion;
+                }
+
+                return $processedQuestions;
+            }
+
+            // 如果API失败,返回模拟题目
+            $mockQuestions = [];
+            for ($i = 0; $i < $count; $i++) {
+                $mockQuestion = $this->generateMockQuestion();
+                $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
+                $mockQuestions[] = $mockQuestion;
+            }
+            return $mockQuestions;
+
+        } catch (\Exception $e) {
+            Log::warning('题库API调用失败,使用模拟题目', ['error' => $e->getMessage()]);
+            $mockQuestions = [];
+            for ($i = 0; $i < $count; $i++) {
+                $mockQuestion = $this->generateMockQuestion();
+                $mockQuestion['id'] = 'Q_' . time() . '_' . uniqid();
+                $mockQuestions[] = $mockQuestion;
+            }
+            return $mockQuestions;
+        }
+    }
+
+    /**
+     * 根据 ID 获取知识点 code
+     */
+    private function findKnowledgePointCodeById(?string $kpId): ?string
+    {
+        if (isset($this->knowledgePointIdIndex[$kpId])) {
+            return $this->knowledgePointIdIndex[$kpId]['code'] ?? null;
+        }
+        return null;
+    }
+
+    /**
+     * 生成批量题目
+     */
+    public function generateBatchQuestions(): void
+    {
+        if (empty($this->studentId)) {
+            $this->dispatch('notify', message: '请先选择学生', type: 'warning');
+            return;
+        }
+
+        try {
+            $this->isLoading = true;
+
+            // 生成批次ID
+            $this->currentBatchId = 'BATCH_' . $this->studentId . '_' . time();
+
+            // 一次性获取所有需要的题目,避免重复调用API
+            $allQuestions = $this->fetchMultipleQuestionsFromBank($this->questionsPerSet);
+
+            // 处理题目
+            $questions = [];
+            foreach ($allQuestions as $question) {
+                if (empty($question['knowledge_point_id']) && !empty($question['kp_code'])) {
+                    $question['knowledge_point_id'] = $this->findKnowledgePointIdByCode($question['kp_code']);
+                }
+                if (empty($question['knowledge_point_id']) && $this->selectedKnowledgePoint) {
+                    $question['knowledge_point_id'] = (string) $this->selectedKnowledgePoint;
+                }
+
+                // 添加选择的知识点和技能信息
+                $question['batch_id'] = $this->currentBatchId;
+                $question['selected_knowledge_point'] = $this->selectedKnowledgePoint;
+                $question['selected_skills'] = $this->selectedSkills;
+                $questions[] = $question;
+            }
+
+            if (!empty($questions)) {
+                $this->exerciseQuestions = $questions;
+
+                // 初始化答题数组
+                $this->exerciseAnswers = [];
+                foreach ($questions as $index => $question) {
+                    $this->exerciseAnswers[$index] = [
+                        'user_answer' => '',
+                        'is_correct' => null,
+                    ];
+                }
+
+  
+                $this->dispatch('notify', message: "成功生成 {$this->questionsPerSet} 道题目", type: 'success');
+            } else {
+                $this->dispatch('notify', message: '生成题目失败,请重试', type: 'danger');
+            }
+
+        } catch (\Exception $e) {
+            Log::error('生成批量题目失败', [
+                'student_id' => $this->studentId,
+                'questions_count' => $this->questionsPerSet,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('notify', message: '生成题目失败:' . $e->getMessage(), type: 'danger');
+        } finally {
+            $this->isLoading = false;
+        }
+    }
+
+    /**
+     * 计算技能评分
+     */
+    private function calculateSkillScores(array $selectedSkills, bool $isCorrect): string
+    {
+        if (empty($selectedSkills)) {
+            return json_encode([]);
+        }
+
+        $scores = [];
+        $baseScore = $isCorrect ? 0.8 : 0.2; // 正确答对给0.8分,错误给0.2分
+        $randomFactor = rand(-10, 10) / 100; // 添加随机因素
+
+        foreach ($selectedSkills as $skillId) {
+            $score = max(0, min(1, $baseScore + $randomFactor));
+            $scores[$skillId] = round($score, 3);
+        }
+
+        return json_encode($scores);
+    }
+
+    /**
+     * 批量答题时更新掌握度
+     */
+    private function updateMasteryFromBatchAnswer(array $question, bool $isCorrect): void
+    {
+        try {
+            $learningAnalytics = new LearningAnalyticsService();
+            $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($question['knowledge_point_id'] ?? null);
+
+            $attemptData = [
+                'kp_code' => $kpCode ?? 'KP_UNKNOWN',
+                'is_correct' => $isCorrect,
+                'time_spent_seconds' => rand(60, 180),
+                'difficulty_level' => (string)($question['difficulty'] ?? '3'),
+                'question_id' => 'Q_' . $this->currentBatchId . '_' . rand(1000, 9999),
+                'student_answer' => '',
+                'correct_answer' => $question['answer'] ?? '',
+            ];
+
+            if (!empty($question['knowledge_point_id'])) {
+                $attemptData['knowledge_point_id'] = $question['knowledge_point_id'];
+            }
+
+            // 添加技能点数据(使用技能ID,ID是唯一的)
+            if (!empty($question['selected_skills'])) {
+                $attemptData['skill_codes'] = $question['selected_skills'];
+            } elseif (!empty($this->selectedSkills)) {
+                $attemptData['skill_codes'] = $this->selectedSkills;
+            } else {
+                $attemptData['skill_codes'] = [];
+            }
+
+            $result = $learningAnalytics->submitAttempt($this->studentId, $attemptData);
+
+            if (isset($result['error'])) {
+                Log::error('LearningAnalytics API 调用失败', [
+                    'student_id' => $this->studentId,
+                    'batch_id' => $this->currentBatchId,
+                    'error' => $result['message'] ?? 'Unknown error',
+                    'attempt_data' => $attemptData
+                ]);
+            } else {
+                Log::info('批量答题记录已成功提交', [
+                    'student_id' => $this->studentId,
+                    'batch_id' => $this->currentBatchId,
+                    'attempt_id' => $result['attempt_id'] ?? null,
+                    'knowledge_point_id' => $result['knowledge_point_id'] ?? null,
+                    'skill_codes' => $result['skill_codes'] ?? []
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            Log::error('更新批量答题掌握度失败', [
+                'student_id' => $this->studentId,
+                'batch_id' => $this->currentBatchId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 批量提交答案
+     */
+    public function submitBatchAnswers(): void
+    {
+        if (empty($this->studentId) || empty($this->exerciseQuestions) || empty($this->currentBatchId)) {
+            $this->dispatch('notify', message: '没有可提交的题目', type: 'warning');
+            return;
+        }
+
+        try {
+            $this->isLoading = true;
+
+            $successCount = 0;
+            $failureCount = 0;
+
+            foreach ($this->exerciseQuestions as $index => $question) {
+                $answer = $this->exerciseAnswers[$index] ?? null;
+
+                if (!$answer || $answer['is_correct'] === null) {
+                    continue; // 跳过未答题的题目
+                }
+
+                try {
+                    // 确保knowledge_point_id始终是数字ID
+                    $knowledgePointId = $question['knowledge_point_id'] ?? null;
+
+                    // 如果knowledge_point_id是code,转换为ID
+                    if ($knowledgePointId && !is_numeric($knowledgePointId)) {
+                        $knowledgePointId = $this->findKnowledgePointIdByCode($knowledgePointId);
+                    }
+
+                    // 如果还是没有,使用选中的知识点
+                    if (!$knowledgePointId && $this->selectedKnowledgePoint) {
+                        $selectedValue = $this->selectedKnowledgePoint;
+                        // 如果选中的是code,转换为ID;如果是ID,直接使用
+                        if (!is_numeric($selectedValue)) {
+                            $knowledgePointId = $this->findKnowledgePointIdByCode($selectedValue);
+                        } else {
+                            $knowledgePointId = (string)$selectedValue;
+                        }
+                    }
+
+                    // 如果还是没有,从题目kp_code查找
+                    if (!$knowledgePointId && !empty($question['kp_code'])) {
+                        $knowledgePointId = $this->findKnowledgePointIdByCode($question['kp_code']);
+                    }
+
+                    $kpCode = $question['kp_code'] ?? $this->findKnowledgePointCodeById($knowledgePointId) ?? 'KP_UNKNOWN';
+
+                    // 准备数据库存储数据
+                    $exerciseData = [
+                        'student_id' => $this->studentId,
+                        'question_id' => $question['id'] ?? 'Q_' . $this->currentBatchId . '_' . $index,
+                        // 确保knowledge_point_id是整数或null,不能是字符串
+                        'knowledge_point_id' => is_numeric($knowledgePointId) ? (int)$knowledgePointId : null,
+                        'question_content' => $question['content'] ?? '',
+                        'student_answer' => $answer['user_answer'] ?? '',
+                        'correct_answer' => $question['answer'] ?? '',
+                        'is_correct' => $answer['is_correct'],
+                        'submission_status' => 'submitted',
+                        'batch_id' => $this->currentBatchId,
+                        'kp_code' => $kpCode,
+                        'selected_skills' => json_encode($this->selectedSkills),
+                        'skill_scores' => $this->calculateSkillScores($this->selectedSkills, $answer['is_correct']),
+                        'time_spent_seconds' => rand(60, 180),
+                        'difficulty_level' => is_numeric($question['difficulty'] ?? 3) ? (float)$question['difficulty'] : 3,
+                        'created_at' => now(),
+                        'updated_at' => now(),
+                    ];
+
+                    // 保存到 Laravel 数据库
+                    \App\Models\StudentExercise::create($exerciseData);
+
+                    // 提交给 LearningAnalytics 系统
+                    $this->updateMasteryFromBatchAnswer($question, $answer['is_correct']);
+
+                    $successCount++;
+
+                } catch (\Exception $e) {
+                    Log::error('批量答题中的单题提交失败', [
+                        'student_id' => $this->studentId,
+                        'question_index' => $index,
+                        'error' => $e->getMessage()
+                    ]);
+                    $failureCount++;
+                }
+            }
+
+            // 清空批量数据
+            $this->exerciseQuestions = [];
+            $this->exerciseAnswers = [];
+            $this->currentBatchId = '';
+
+            // 提交结果
+            $totalQuestions = $successCount + $failureCount;
+            $this->dispatch('notify',
+                message: "批量提交完成!成功: {$successCount} 题,失败: {$failureCount} 题",
+                type: $failureCount === 0 ? 'success' : 'warning'
+            );
+
+            // 批量更新技能熟练度
+            try {
+                $learningAnalytics = new LearningAnalyticsService();
+                $skillResult = $learningAnalytics->batchUpdateSkillProficiency($this->studentId);
+                if ($skillResult) {
+                    Log::info('技能熟练度批量更新成功', ['student_id' => $this->studentId]);
+                }
+            } catch (\Exception $e) {
+                Log::warning('技能熟练度批量更新失败(不影响答题提交)', [
+                    'student_id' => $this->studentId,
+                    'error' => $e->getMessage()
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            Log::error('批量提交答案失败', [
+                'student_id' => $this->studentId,
+                'batch_id' => $this->currentBatchId,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('notify', message: '批量提交失败:' . $e->getMessage(), type: 'danger');
+        } finally {
+            $this->isLoading = false;
+        }
+    }
+
+    // 测试方法
+    public function testClick(): void
+    {
+        $this->testClickCount++;
+
+        Log::info('测试按钮被点击,当前计数: ' . $this->testClickCount);
+
+        $this->dispatch('notify', message: "测试按钮点击成功!计数: {$this->testClickCount}", type: 'success');
+    }
+
+    public function incrementCounter(): void
+    {
+        $this->testClickCount++;
+        Log::info('计数器增加: ' . $this->testClickCount);
+    }
+
+    }

+ 0 - 981
app/Filament/Pages/StudentDashboard.php

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

+ 44 - 0
app/Models/StudentExercise.php

@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use App\Services\MathFormulaProcessor;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 
@@ -38,4 +39,47 @@ class StudentExercise extends Model
         'created_at' => 'datetime',
         'updated_at' => 'datetime',
     ];
+
+    /**
+     * 获取处理后的题目内容(包含数学公式处理)
+     */
+    public function getProcessedQuestionContentAttribute(): string
+    {
+        return MathFormulaProcessor::processFormulas($this->question_content ?? '');
+    }
+
+    /**
+     * 获取处理后的学生答案(包含数学公式处理)
+     */
+    public function getProcessedStudentAnswerAttribute(): string
+    {
+        return MathFormulaProcessor::processFormulas($this->student_answer ?? '');
+    }
+
+    /**
+     * 获取处理后的正确答案(包含数学公式处理)
+     */
+    public function getProcessedCorrectAnswerAttribute(): string
+    {
+        return MathFormulaProcessor::processFormulas($this->correct_answer ?? '');
+    }
+
+    /**
+     * 获取处理后的数据数组(用于API返回)
+     */
+    public function toProcessedArray(): array
+    {
+        $data = $this->toArray();
+
+        // 处理数学公式字段
+        $mathFields = ['question_content', 'student_answer', 'correct_answer'];
+
+        foreach ($mathFields as $field) {
+            if (isset($data[$field]) && is_string($data[$field])) {
+                $data[$field] = MathFormulaProcessor::processFormulas($data[$field]);
+            }
+        }
+
+        return $data;
+    }
 }

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

@@ -56,6 +56,7 @@ class AdminPanelProvider extends PanelProvider
                 view('filament.layout.vite-styles')->render() . view('filament.layout.vite-scripts')->render()
             )
             ->renderHook('global::head.start', fn (): string => view('filament.layout.vite-styles')->render())
+            // ->renderHook('panels::body.end', fn (): string => view('filament.layout.math-renderer')->render())
             ->middleware([
                 EncryptCookies::class,
                 AddQueuedCookiesToResponse::class,

+ 28 - 0
app/Providers/MathServiceProvider.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Providers;
+
+use Illuminate\Support\Facades\Blade;
+use Illuminate\Support\ServiceProvider;
+
+class MathServiceProvider extends ServiceProvider
+{
+    public function boot()
+    {
+        // 注册 blade directive
+        Blade::directive('math', function ($expression) {
+            return "<?php
+                \$content = {$expression};
+                // 强制通过处理器进行标准化
+                \$processedContent = \\App\\Services\\MathFormulaProcessor::processFormulas(\$content);
+                // 直接输出内容,让 auto-render 扩展去解析
+                echo '<span class=\"math-render\">' . \$processedContent . '</span>';
+            ?>";
+        });
+    }
+
+    public function register()
+    {
+        //
+    }
+}

+ 259 - 0
app/Services/MathFormulaProcessor.php

@@ -0,0 +1,259 @@
+<?php
+
+namespace App\Services;
+
+class MathFormulaProcessor
+{
+    /**
+     * 处理数学公式,确保有正确的 LaTeX 标记
+     * 
+     * 策略:
+     * 1. 清理 HTML 标签和实体
+     * 2. 规范化反斜杠(处理多重转义)
+     * 3. 修复丢失反斜杠的常见 LaTeX 命令
+     * 4. 确保公式被正确的定界符包裹
+     */
+    public static function processFormulas(string $content): string
+    {
+        if (empty($content)) {
+            return $content;
+        }
+
+        // 1. 基础清理
+        // 递归解码 HTML 实体
+        $decoded = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
+        while ($decoded !== $content) {
+            $content = $decoded;
+            $decoded = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
+        }
+        
+        $content = trim($content);
+
+        // 2. 规范化反斜杠
+        $content = preg_replace('/\\\\+([a-zA-Z])/', '\\\\$1', $content);
+
+        // 3. 修复常见 LaTeX 命令
+        $commands = [
+            'sqrt', 'frac', 'times', 'div', 'pm', 'cdot', 
+            'sin', 'cos', 'tan', 'log', 'ln', 'lim',
+            'alpha', 'beta', 'gamma', 'theta', 'pi', 'sigma', 'omega', 'Delta',
+            'leq', 'geq', 'neq', 'approx', 'infty',
+            'sum', 'prod', 'int', 'partial', 'nabla'
+        ];
+        
+        $pattern = '/(?<!\\\\)\b(' . implode('|', $commands) . ')\b/';
+        $content = preg_replace($pattern, '\\\\$1', $content);
+
+        // 4. 处理定界符
+        // 如果内容已经是完整的公式(被 $ 或 $$ 包裹),则保持原样
+        if (self::hasDelimiters($content)) {
+            $content = self::cleanInsideDelimiters($content);
+            return $content;
+        }
+
+        // 5. 智能包装 (统一处理混合内容)
+        // 无论是纯文本还是富文本,都使用智能识别来包裹公式
+        // 这能同时处理:
+        // - "已知函数 f(x) = ..." (未包裹的混合内容)
+        // - "验证:$2x...$" (部分包裹的混合内容)
+        // - "4x^2 - 25y^2" (未包裹的纯公式)
+        
+        // 先清理已有的定界符内部
+        $content = self::cleanInsideDelimiters($content);
+        // 然后智能包裹剩余的数学部分
+        $content = self::smartWrapMixedContent($content);
+
+        return $content;
+    }
+
+    /**
+     * 清理定界符内部的 HTML 标签
+     */
+    private static function cleanInsideDelimiters(string $content): string
+    {
+        // 定义定界符模式
+        $patterns = [
+            '/\$\$([\s\S]*?)\$\$/', // $$...$$
+            '/\$([\s\S]*?)\$/',     // $...$
+            '/\\\\\(([\s\S]*?)\\\\\)/', // \(...\)
+            '/\\\\\[([\s\S]*?)\\\\\]/'  // \[...\]
+        ];
+
+        foreach ($patterns as $pattern) {
+            $content = preg_replace_callback($pattern, function ($matches) {
+                // $matches[0] 是完整匹配 (如 $...$)
+                // $matches[1] 是内部内容
+                
+                // 清理内部的 HTML 标签
+                $cleanContent = strip_tags($matches[1]);
+                // 解码实体
+                $cleanContent = html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8');
+                $cleanContent = trim($cleanContent);
+                
+                // 重建定界符 (保持原样)
+                // 注意:我们需要根据原来的定界符类型来重建
+                // 这里简单起见,我们直接用匹配到的完整字符串的定界符部分
+                // 但 preg_replace_callback 不容易直接获取定界符,所以我们硬编码
+                
+                if (str_starts_with($matches[0], '$$')) return '$$' . $cleanContent . '$$';
+                if (str_starts_with($matches[0], '$')) return '$' . $cleanContent . '$';
+                if (str_starts_with($matches[0], '\[')) return '\[' . $cleanContent . '\]';
+                if (str_starts_with($matches[0], '\(')) return '\(' . $cleanContent . '\)';
+                
+                // 默认回退 (不应该发生)
+                return $matches[0];
+            }, $content);
+        }
+
+        return $content;
+    }
+
+    /**
+     * 智能识别并包裹富文本中的数学公式
+     */
+    private static function smartWrapMixedContent(string $content): string
+    {
+        // 正则策略:匹配 HTML 标签 OR 数学公式候选
+        // 捕获组 1: HTML 标签 (忽略)
+        // 捕获组 2: 已有的定界符 (忽略)
+        // 捕获组 3: 数学公式 (处理)
+        
+        $tagPattern = '<[^>]+>';
+        
+        // 匹配已有的定界符
+        $existingDelimiterPattern = '(?:\$\$[\s\S]*?\$\$|\$[\s\S]*?\$|\\\\\([\s\S]*?\\\\\)|\\\\\[[\s\S]*?\\\\\])';
+        
+        // 数学公式特征:
+        // 1. 函数定义: f(x) = ...
+        // 2. 等式/不等式: ... = ..., ... > ..., ... < ...
+        // 3. 包含 LaTeX 命令: \sqrt, \frac 等
+        // 4. 包含上标/下标: x^2, a_n
+        
+        // 匹配函数定义或等式 (例如 f(x) = 2x^2 + 1)
+        // 必须包含 = 或 > 或 <,且周围有类数学字符
+        $equationPattern = '(?<![\w\\\\])(?:[a-zA-Z]\([a-zA-Z0-9,]+\)|[a-zA-Z0-9\^_\{\}]+)\s*[=<>]\s*[\w\s\+\-\*\/\^\.\(\)\{\}\\\\]+(?=\s|$|<|[.,;])';
+        
+        // 匹配显式 LaTeX 命令 (例如 \sqrt{...})
+        $latexPattern = '\\\\[a-zA-Z]+(?:\{[^\}]*\})?';
+        
+        // 匹配简单的代数项 (例如 x^2, a_n) - 需谨慎,避免匹配普通单词
+        $algebraPattern = '(?<![\w\\\\])[a-zA-Z0-9]+\^[\w\{]+';
+
+        // 匹配多项式/复杂表达式 (例如 4x^2 - 25y^2, 2x \times 2 + ...)
+        // 特征:包含变量、数字、运算符 (+, -, *, /)、LaTeX命令、上标/下标
+        // 必须包含至少一个运算符,且长度适中
+        $polynomialPattern = '(?<![\w\\\\])(?:[a-zA-Z0-9\.]+(?:[\^_\{\}][a-zA-Z0-9\.\{\}]+)?|\\\\[a-zA-Z]+(?:\{[^\}]*\})?)(?:\s*[\+\-\*\/]\s*(?:[a-zA-Z0-9\.]+(?:[\^_\{\}][a-zA-Z0-9\.\{\}]+)?|\\\\[a-zA-Z]+(?:\{[^\}]*\})?))+';
+
+        $pattern = "/($tagPattern)|($existingDelimiterPattern)|($equationPattern|$polynomialPattern|$latexPattern|$algebraPattern)/u";
+
+        return preg_replace_callback($pattern, function ($matches) {
+            // 如果是 HTML 标签 (组1),原样返回
+            if (!empty($matches[1])) {
+                return $matches[1];
+            }
+            
+            // 如果是已有的定界符 (组2),原样返回
+            if (!empty($matches[2])) {
+                return $matches[2];
+            }
+            
+            // 如果是数学公式 (组3)
+            if (!empty($matches[3])) {
+                $math = $matches[3];
+                // 再次检查是否已经被包裹 (虽然外层逻辑应该处理了,但为了安全)
+                if (str_contains($math, '$')) {
+                    return $math;
+                }
+                // 排除纯数字或普通单词误判
+                if (preg_match('/^[a-zA-Z0-9\s]+$/', $math)) {
+                    return $math;
+                }
+                return '$' . $math . '$';
+            }
+            
+            return $matches[0];
+        }, $content);
+    }
+
+
+    /**
+     * 检查是否已有定界符
+     */
+    private static function hasDelimiters(string $content): bool
+    {
+        $content = trim($content);
+        // 检查 $$...$$
+        if (str_starts_with($content, '$$') && str_ends_with($content, '$$')) {
+            return true;
+        }
+        // 检查 $...$
+        if (str_starts_with($content, '$') && str_ends_with($content, '$')) {
+            return true;
+        }
+        // 检查 \[...\]
+        if (str_starts_with($content, '\\[') && str_ends_with($content, '\\]')) {
+            return true;
+        }
+        // 检查 \(...\)
+        if (str_starts_with($content, '\\(') && str_ends_with($content, '\\)')) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 检测数学特征
+     */
+    private static function containsMathFeatures(string $content): bool
+    {
+        // 1. 检查是否有 LaTeX 命令
+        if (strpos($content, '\\') !== false) {
+            return true;
+        }
+
+        // 2. 检查数学符号
+        $symbols = ['+', '-', '*', '/', '=', '<', '>', '^', '_', '{', '}'];
+        foreach ($symbols as $symbol) {
+            if (strpos($content, $symbol) !== false) {
+                // 排除普通文本中的符号(如连字符),这里做一个简单的宽容判断
+                // 如果有数字紧随其后,或者是特定组合
+                return true;
+            }
+        }
+
+        // 3. 检查数字和字母的组合 (如 2x, x^2)
+        if (preg_match('/[a-zA-Z]\d|\d[a-zA-Z]/', $content)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 批量处理
+     */
+    public static function processArray(array $data, array $fieldsToProcess): array
+    {
+        foreach ($data as $key => &$value) {
+            if (in_array($key, $fieldsToProcess) && is_string($value)) {
+                $value = self::processFormulas($value);
+            } elseif (is_array($value)) {
+                $value = self::processArray($value, $fieldsToProcess);
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * 处理题目数据
+     */
+    public static function processQuestionData(array $question): array
+    {
+        $fieldsToProcess = [
+            'stem', 'content', 'question_text', 'answer', 
+            'correct_answer', 'student_answer', 'explanation', 
+            'solution', 'question_content'
+        ];
+        return self::processArray($question, $fieldsToProcess);
+    }
+}

+ 26 - 6
app/Services/QuestionServiceApi.php

@@ -47,13 +47,11 @@ class QuestionServiceApi
 
                 $response = $this->request('GET', '/questions', $query);
 
-                // 仅做基础清理
+                // 处理数学公式
                 $data = $response['data'] ?? [];
                 foreach ($data as &$question) {
-                    if (isset($question['stem'])) {
-                        // 只移除HTML标签,不做其他处理
-                        $question['stem'] = strip_tags($question['stem']);
-                    }
+                    // 使用数学公式处理器处理题目数据
+                    $question = MathFormulaProcessor::processQuestionData($question);
                 }
 
                 return [
@@ -97,10 +95,19 @@ class QuestionServiceApi
      */
     public function getQuestionsByKpCode(string $kpCode, int $limit = 100): array
     {
-        return $this->request('GET', '/questions', [
+        $response = $this->request('GET', '/questions', [
             'kp_code' => $kpCode,
             'limit' => $limit,
         ]);
+
+        // 处理数学公式
+        if ($response && isset($response['data']) && is_array($response['data'])) {
+            foreach ($response['data'] as &$question) {
+                $question = MathFormulaProcessor::processQuestionData($question);
+            }
+        }
+
+        return $response;
     }
 
     /**
@@ -120,6 +127,13 @@ class QuestionServiceApi
                         'limit' => $limit,
                     ]);
 
+                    // 处理数学公式
+                    if ($response && is_array($response)) {
+                        $response = MathFormulaProcessor::processArray($response, [
+                            'stem', 'content', 'question_text', 'answer', 'explanation'
+                        ]);
+                    }
+
                     return $response ?? [];
                 } catch (\Exception $e) {
                     \Log::error('Question search failed: ' . $e->getMessage());
@@ -142,6 +156,12 @@ class QuestionServiceApi
             function () use ($id): ?array {
                 try {
                     $response = $this->request('GET', "/questions/{$id}");
+
+                    // 处理数学公式
+                    if ($response && is_array($response)) {
+                        $response = MathFormulaProcessor::processQuestionData($response);
+                    }
+
                     return $response ?: null;
                 } catch (\Exception $e) {
                     \Log::error("Failed to get question {$id}: " . $e->getMessage());

+ 1 - 0
config/app.php

@@ -161,6 +161,7 @@ return [
 
         // Application Service Providers...
         App\Providers\AppServiceProvider::class,
+        App\Providers\MathServiceProvider::class,
         // App\Providers\AuthServiceProvider::class,
         // App\Providers\EventServiceProvider::class,
         // App\Providers\RouteServiceProvider::class,

File diff ditekan karena terlalu besar
+ 0 - 0
public/css/katex/katex.min.css


File diff ditekan karena terlalu besar
+ 0 - 0
public/js/auto-render.min.js


File diff ditekan karena terlalu besar
+ 0 - 0
public/js/katex.min.js


+ 1 - 1
public/js/math-render.js

@@ -11,7 +11,7 @@
         attempts: 0,
         maxAttempts: 50,
         delay: 100,
-        selector: '.math-render',
+        selector: '.math-text',
         autoInit: true
     };
 

+ 87 - 1
resources/css/app.css

@@ -37,6 +37,7 @@
 }
 
 @layer components {
+
     /* Filament 登录页面定制 */
     .fi-logo {
         display: none !important;
@@ -203,9 +204,94 @@
     .divider-modern {
         @apply my-6 border-0 border-t border-slate-200 dark:border-slate-700;
     }
+
+    /* =========================================
+       全局输入框美化 (Global Input Polish)
+       ========================================= */
+
+    /* 
+       策略:
+       1. 对于登录页 (.fi-simple-layout),我们禁用 Filament 的 wrapper 样式,直接对 input 应用 DaisyUI 组件样式。
+       2. 对于后台管理页,我们保留 Filament 的 wrapper 结构(为了兼容前缀/后缀图标),但用 CSS 模拟 DaisyUI 的外观。
+    */
+
+    /* --- 登录页面 (Simple Layout) --- */
+
+    /* 移除 Filament wrapper 的默认样式 */
+    .fi-simple-layout .fi-input-wrp {
+        @apply ring-0 shadow-none bg-transparent !important;
+    }
+
+    /* 直接对 input 应用 DaisyUI 样式 */
+    .fi-simple-layout input:not([type='checkbox']):not([type='radio']),
+    .fi-simple-layout select {
+        @apply input input-bordered input-primary w-full bg-white text-gray-900 !important;
+        height: 2.75rem !important;
+        /* 修复高度 */
+    }
+
+    .fi-simple-layout input:focus,
+    .fi-simple-layout select:focus {
+        @apply outline-none border-primary-500 ring-2 ring-primary-500/20 !important;
+    }
+
+    /* 登录按钮美化 */
+    .fi-simple-layout .fi-btn {
+        @apply btn btn-primary w-full text-white font-bold shadow-lg hover:shadow-xl transition-all duration-300 !important;
+        border: none !important;
+    }
+
+    /* 复选框美化 */
+    .fi-simple-layout input[type='checkbox'] {
+        @apply checkbox checkbox-primary rounded border-gray-300 !important;
+    }
+
+    /* 链接美化 */
+    .fi-simple-layout a {
+        @apply link link-primary hover:link-hover transition-colors !important;
+    }
+
+    /* --- 后台管理页面 (Admin Panel) --- */
+
+    /* 模拟 DaisyUI input-bordered 的外观 */
+    .fi-input-wrp,
+    .fi-select-input {
+        @apply bg-white border border-base-300 shadow-sm rounded-lg transition-all duration-200 !important;
+        /* 覆盖 Filament 的默认 ring */
+        box-shadow: none !important;
+        ring: 0 !important;
+    }
+
+    /* 聚焦状态 */
+    .fi-input-wrp:focus-within,
+    .fi-select-input:focus-within {
+        @apply border-primary-500 ring-2 ring-primary-500/20 !important;
+    }
+
+    /* 内部 input 保持透明 */
+    .fi-input-wrp input,
+    .fi-select-input select {
+        @apply bg-transparent border-none shadow-none ring-0 focus:ring-0 !important;
+    }
+
+    /* 暗色模式适配 */
+    :is(.dark) .fi-simple-layout input:not([type='checkbox']):not([type='radio']),
+    :is(.dark) .fi-simple-layout select {
+        @apply bg-base-200 text-base-content border-base-content/20 !important;
+    }
+
+    :is(.dark) .fi-input-wrp,
+    :is(.dark) .fi-select-input {
+        @apply bg-base-200 border-base-content/20 !important;
+    }
+
+    :is(.dark) .fi-simple-layout .fi-btn {
+        @apply btn-primary text-white !important;
+    }
 }
 
 @layer utilities {
+
     /* 文字渐变 */
     .text-gradient {
         background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@@ -232,4 +318,4 @@
     .mix-blend-screen {
         mix-blend-mode: screen;
     }
-}
+}

+ 100 - 80
resources/views/components/math-render.blade.php

@@ -1,115 +1,135 @@
 @props(['content' => '', 'class' => '', 'inline' => false])
 
-<div class="math-render {{ $class }}" data-math-content="{!! $content !!}">
+<span class="math-render {{ $class }}">
     {!! $content !!}
-</div>
+</span>
 
 @push('scripts')
+<!-- 引入 KaTeX 核心库 -->
+<script src="/js/katex.min.js?v={{ time() }}"></script>
+<!-- 引入 auto-render 扩展 (本地) -->
+<script src="/js/auto-render.min.js?v={{ time() }}"></script>
+
 <script>
 (function() {
     'use strict';
 
-    function renderMathElement(element) {
-        if (typeof window.katex === 'undefined') {
-            // 等待 KaTeX 加载
-            if (window.mathRenderAttempts < 50) {
-                window.mathRenderAttempts++;
-                setTimeout(() => renderMathElement(element), 100);
-            }
-            return;
-        }
+    console.log('Math Render Script Loaded (Local Auto-Render)');
+    
+    // 配置项
+    const renderOptions = {
+        delimiters: [
+            {left: '$$', right: '$$', display: true},
+            {left: '$', right: '$', display: false},
+            {left: '\\(', right: '\\)', display: false},
+            {left: '\\[', right: '\\]', display: true}
+        ],
+        throwOnError: false,
+        ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'option']
+    };
 
-        // 避免重复渲染
-        if (element.dataset.rendered === 'true') {
+    function renderAllMath() {
+        if (typeof renderMathInElement === 'undefined') {
+            console.warn('auto-render extension not loaded yet');
             return;
         }
+        
+        if (typeof window.katex === 'undefined') {
+             console.warn('katex core not loaded yet');
+             return;
+        }
 
-        const content = element.dataset.mathContent || element.textContent;
-        if (!content) return;
-
+        console.log('Rendering math using auto-render...');
+        
+        // 1. 优先渲染 .math-render 元素 (通常是经过后端处理的)
+        const elements = document.querySelectorAll('.math-render');
+        elements.forEach(elem => {
+            try {
+                renderMathInElement(elem, renderOptions);
+                elem.dataset.rendered = 'true';
+            } catch (e) {
+                console.error('Auto-render failed for element:', elem, e);
+            }
+        });
+        
+        // 2. 尝试渲染整个 body,以捕获未被包裹的公式
+        // 使用较低的优先级或特定的容器如果可能
         try {
-            let html = content;
-
-            // 渲染 $$...$$ 块级公式
-            html = html.replace(/\$\$([\s\S]*?)\$\$/g, (match, formula) => {
-                try {
-                    return window.katex.renderToString(formula.trim(), {
-                        throwOnError: false,
-                        displayMode: true
-                    });
-                } catch (e) {
-                    console.warn('KaTeX render error:', e);
-                    return match;
-                }
-            });
-
-            // 渲染 $...$ 行内公式
-            html = html.replace(/\$(.*?)\$/g, (match, formula) => {
-                try {
-                    return window.katex.renderToString(formula, {
-                        throwOnError: false,
-                        displayMode: false
-                    });
-                } catch (e) {
-                    console.warn('KaTeX render error:', e);
-                    return match;
-                }
-            });
-
-            // 渲染 \(...\) 行内公式
-            html = html.replace(/\\\((.*?)\\\)/g, (match, formula) => {
-                try {
-                    return window.katex.renderToString(formula, {
-                        throwOnError: false,
-                        displayMode: false
-                    });
-                } catch (e) {
-                    console.warn('KaTeX render error:', e);
-                    return match;
-                }
-            });
-
-            element.innerHTML = html;
-            element.dataset.rendered = 'true';
+            renderMathInElement(document.body, renderOptions);
         } catch (e) {
-            console.error('Math render error:', e);
+            console.warn('Global auto-render warning:', e);
         }
     }
 
-    function renderAllMath() {
-        document.querySelectorAll('.math-render:not([data-rendered="true"])').forEach(renderMathElement);
-    }
-
     // 初始化
     document.addEventListener('DOMContentLoaded', () => {
-        if (typeof window.katex === 'undefined') {
-            const script = document.createElement('script');
-            script.src = '/js/katex.min.js';
-            script.onload = () => {
-                window.mathRenderAttempts = 0;
-                renderAllMath();
-            };
-            document.head.appendChild(script);
+        // 稍微延迟以确保脚本执行顺序
+        setTimeout(checkAndRender, 100);
+    });
+
+    function checkAndRender() {
+        if (typeof window.katex !== 'undefined' && typeof renderMathInElement !== 'undefined') {
+            console.log('KaTeX and auto-render loaded');
+            initMathRenderer();
         } else {
-            renderAllMath();
+            console.log('Waiting for KaTeX/auto-render...');
+            setTimeout(checkAndRender, 100);
         }
-    });
+    }
+
+    function initMathRenderer() {
+        renderAllMath();
+        setupObservers();
+    }
+
+    function setupObservers() {
+        const observer = new MutationObserver((mutations) => {
+            let shouldRender = false;
+            mutations.forEach((mutation) => {
+                if (mutation.addedNodes.length > 0) {
+                    shouldRender = true;
+                }
+            });
+            
+            if (shouldRender) {
+                if (window.mathRenderTimeout) clearTimeout(window.mathRenderTimeout);
+                window.mathRenderTimeout = setTimeout(() => {
+                    console.log('DOM mutation detected');
+                    renderAllMath();
+                }, 100);
+            }
+        });
+
+        observer.observe(document.body, {
+            childList: true,
+            subtree: true
+        });
+    }
 
     // Livewire 兼容性
     document.addEventListener('livewire:initialized', () => {
-        renderAllMath();
+        setTimeout(renderAllMath, 50);
+        
+        if (typeof Livewire !== 'undefined' && Livewire.hook) {
+            Livewire.hook('morph.updated', ({ el, component }) => {
+                renderAllMath();
+            });
+            Livewire.hook('commit', ({ component, commit, respond, succeed, fail }) => {
+                succeed(({ snapshot, effect }) => {
+                    setTimeout(renderAllMath, 50);
+                });
+            });
+        }
     });
 
-    document.addEventListener('livewire:updated', () => {
-        setTimeout(renderAllMath, 100);
+    document.addEventListener('livewire:navigated', () => {
+        setTimeout(renderAllMath, 50);
     });
 
-    // Alpine.js 兼容性
     document.addEventListener('alpine:init', () => {
-        renderAllMath();
+        setTimeout(renderAllMath, 50);
     });
 
-    // 自定义事件监听
     document.addEventListener('math:render', () => {
         renderAllMath();
     });

+ 9 - 0
resources/views/components/math-text.blade.php

@@ -0,0 +1,9 @@
+@props(['content' => '', 'class' => '', 'inline' => false, 'limit' => null])
+
+@php
+    $displayContent = $limit ? \Illuminate\Support\Str::limit($content, $limit) : $content;
+@endphp
+
+<span class="math-text {{ $class }}" {{ $inline ? '' : 'style="display: block;"' }}>
+    {!! $displayContent !!}
+</span>

+ 54 - 0
resources/views/filament/layout/math-renderer.blade.php

@@ -0,0 +1,54 @@
+@once
+    @push('styles')
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
+    @endpush
+
+    @push('scripts')
+    <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
+    <script>
+    document.addEventListener('DOMContentLoaded', function() {
+        console.log('数学渲染器加载');
+
+        function renderMath() {
+            // 查找所有包含 $$ 的元素
+            const elements = document.querySelectorAll('*');
+
+            elements.forEach(element => {
+                if (element.children.length === 0) { // 只处理叶子节点
+                    const text = element.textContent;
+                    if (text && text.includes('$$')) {
+                        try {
+                            const newHtml = text.replace(/\$\$([^$]+)\$\$/g, (match, formula) => {
+                                return katex.renderToString(formula.trim(), {
+                                    throwOnError: false,
+                                    displayMode: true
+                                });
+                            });
+
+                            if (newHtml !== text) {
+                                element.innerHTML = newHtml;
+                            }
+                        } catch (e) {
+                            console.warn('数学公式渲染失败:', e);
+                        }
+                    }
+                }
+            });
+        }
+
+        // 页面加载后渲染
+        setTimeout(renderMath, 500);
+
+        // Livewire 更新后重新渲染
+        if (window.Livewire) {
+            window.Livewire.on('updated', () => {
+                setTimeout(renderMath, 100);
+            });
+        }
+
+        // 添加手动渲染函数
+        window.renderMath = renderMath;
+    });
+    </script>
+    @endpush
+@endonce

+ 362 - 0
resources/views/filament/pages/question-management-simple.blade.php

@@ -0,0 +1,362 @@
+<x-filament-panels::page>
+
+<div class="space-y-6">
+    @php
+        $questionsData = $this->questions;
+        $metaData = $this->meta;
+        $statisticsData = $this->statistics;
+    @endphp
+
+    <!-- 后台生成状态栏 - 仅在生成中显示 -->
+    @if($isGenerating && $currentTaskId)
+        <div class="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-r-lg animate-pulse">
+            <div class="flex items-center">
+                <div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600 mr-3"></div>
+                <div class="flex-1">
+                    <p class="text-sm text-blue-800">
+                        <strong>正在后台生成题目...</strong>
+                    </p>
+                    <p class="text-xs text-blue-600 mt-1">
+                        任务 ID: {{ $currentTaskId }} | AI生成完成后将自动刷新页面
+                    </p>
+                </div>
+                <button type="button" wire:click="$set('isGenerating', false)" class="text-blue-400 hover:text-blue-600">
+                    <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="M6 18L18 6M6 6l12 12"></path>
+                    </svg>
+                </button>
+            </div>
+        </div>
+    @endif
+
+    <div class="flex justify-end">
+        <button
+            type="button"
+            wire:click="$dispatch('ai-generate')"
+            class="filament-button filament-button-size-sm filament-button-color-success filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+        >
+            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
+            </svg>
+            生成题目
+        </button>
+    </div>
+
+    <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+        <div class="bg-white p-4 rounded-lg border">
+            <div class="text-sm text-gray-500">题目总数</div>
+            <div class="text-2xl font-bold text-primary-600">{{ $statisticsData['total'] ?? 0 }}</div>
+        </div>
+        <div class="bg-white p-4 rounded-lg border">
+            <div class="text-sm text-gray-500">基础难度 (≤0.4)</div>
+            <div class="text-2xl font-bold text-green-600">
+                @php
+                    $basicCount = 0;
+                    foreach ($statisticsData['by_difficulty'] ?? [] as $key => $value) {
+                        if ((float)$key <= 0.4) {
+                            $basicCount += $value;
+                        }
+                    }
+                    echo $basicCount;
+                @endphp
+            </div>
+        </div>
+        <div class="bg-white p-4 rounded-lg border">
+            <div class="text-sm text-gray-500">中等难度 (0.4-0.7)</div>
+            <div class="text-2xl font-bold text-yellow-600">
+                @php
+                    $mediumCount = 0;
+                    foreach ($statisticsData['by_difficulty'] ?? [] as $key => $value) {
+                        if ((float)$key > 0.4 && (float)$key <= 0.7) {
+                            $mediumCount += $value;
+                        }
+                    }
+                    echo $mediumCount;
+                @endphp
+            </div>
+        </div>
+        <div class="bg-white p-4 rounded-lg border">
+            <div class="text-sm text-gray-500">拔高难度 (>0.7)</div>
+            <div class="text-2xl font-bold text-red-600">
+                @php
+                    $advancedCount = 0;
+                    foreach ($statisticsData['by_difficulty'] ?? [] as $key => $value) {
+                        if ((float)$key > 0.7) {
+                            $advancedCount += $value;
+                        }
+                    }
+                    echo $advancedCount;
+                @endphp
+            </div>
+        </div>
+    </div>
+
+    <div class="bg-white p-4 rounded-lg border">
+        <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-2">搜索题目</label>
+                <input type="text" wire:model.live.debounce.300ms="search" placeholder="输入关键词" class="w-full border rounded p-2">
+            </div>
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-2">知识点筛选</label>
+                <input type="text" wire:model.live="selectedKpCode" placeholder="KP1001" class="w-full border rounded p-2">
+            </div>
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-2">难度筛选</label>
+                <input type="text" wire:model.live="selectedDifficulty" placeholder="0.3/0.6/0.85" class="w-full border rounded p-2">
+            </div>
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-2">每页显示</label>
+                <input type="number" wire:model.live="perPage" min="10" max="100" step="5" class="w-full border rounded p-2">
+            </div>
+        </div>
+    </div>
+
+    <div class="bg-white rounded-lg border overflow-hidden">
+        <table class="min-w-full divide-y divide-gray-200">
+            <thead class="bg-gray-50">
+                <tr>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">题目编号</th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">知识点</th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">题干</th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">难度</th>
+                    <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
+                </tr>
+            </thead>
+            <tbody class="bg-white divide-y divide-gray-200">
+                @forelse($questionsData as $question)
+                    <tr class="hover:bg-gray-50">
+                        <td class="px-6 py-4 whitespace-nowrap">{{ $question['question_code'] ?? 'N/A' }}</td>
+                        <td class="px-6 py-4 whitespace-nowrap">{{ $question['kp_code'] ?? 'N/A' }}</td>
+                        <td class="px-6 py-4" style="word-wrap: break-word; white-space: normal; line-height: 1.8; max-width: 400px;">
+                            <span class="text-sm">
+                                @math($question['stem'] ?? 'N/A')
+                            </span>
+                        </td>
+                        <td class="px-6 py-4">
+                            @php
+                                $difficulty = $question['difficulty'] ?? null;
+                                $label = match (true) {
+                                    !$difficulty => 'N/A',
+                                    (float)$difficulty <= 0.4 => '基础',
+                                    (float)$difficulty <= 0.7 => '中等',
+                                    default => '拔高',
+                                };
+                            @endphp
+                            {{ $label }}
+                            @if(app()->environment('local'))
+                                <span class="text-xs text-gray-400">({{ $difficulty }})</span>
+                            @endif
+                        </td>
+                        <td class="px-6 py-4 whitespace-nowrap">
+                            <button wire:click="deleteQuestion('{{ $question['question_code'] }}')" class="text-red-600 hover:underline">删除</button>
+                        </td>
+                    </tr>
+                @empty
+                    <tr><td colspan="5" class="px-6 py-12 text-center">暂无数据</td></tr>
+                @endforelse
+            </tbody>
+        </table>
+
+        @if(!empty($metaData) && ($metaData['total'] ?? 0) > 0)
+            <div class="px-4 py-3 border-t border-gray-200 flex items-center justify-between">
+                <div class="text-sm text-gray-700">共 {{ $metaData['total'] ?? 0 }} 条记录</div>
+                <div class="flex items-center gap-2">
+                    <button wire:click="previousPage" @disabled($currentPage <= 1) class="px-3 py-1 border rounded">上一页</button>
+                    @foreach($this->getPages() as $page)
+                        <button wire:click="gotoPage({{ $page }})" class="px-3 py-1 border rounded {{ $page === $currentPage ? 'bg-blue-50 text-blue-700' : '' }}">{{ $page }}</button>
+                    @endforeach
+                    <button wire:click="nextPage" @disabled($currentPage >= ($metaData['total_pages'] ?? 1)) class="px-3 py-1 border rounded">下一页</button>
+                </div>
+            </div>
+        @endif
+    </div>
+
+    @if($showGenerateModal)
+        <div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
+            <div class="bg-white rounded-lg p-6 w-96 max-w-[28rem] shadow-xl">
+                <h3 class="text-lg font-semibold mb-4">生成题目</h3>
+                <div class="space-y-4">
+                    <div>
+                        <label class="block text-sm font-medium mb-2">知识点 <span class="text-red-500">*</span></label>
+                        <select wire:model.live="generateKpCode" class="w-full border rounded p-2">
+                            <option value="">选择知识点</option>
+                            @foreach($this->knowledgePointOptions as $code => $name)
+                                <option value="{{ $code }}">{{ $code }} - {{ $name }}</option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    @if(!empty($this->skillsOptions))
+                        <div>
+                            <div class="flex items-center justify-between mb-2">
+                                <label class="block text-sm font-medium">选择技能 <span class="text-red-500">*</span></label>
+                                <button type="button" class="text-sm text-blue-600 hover:underline" wire:click="toggleAllSkills">
+                                    {{ count($selectedSkills) === count($this->skillsOptions) ? '取消全选' : '全选' }}
+                                </button>
+                            </div>
+                            <div class="max-h-48 overflow-y-auto border rounded p-3 space-y-1">
+                                @foreach($this->skillsOptions as $skill)
+                                    <label class="flex items-center space-x-2">
+                                        <input type="checkbox" value="{{ $skill['code'] }}" wire:model="selectedSkills" class="rounded border-gray-300">
+                                        <span class="text-sm">
+                                            <span class="font-medium">{{ $skill['code'] }}</span>
+                                            <span class="text-gray-600 ml-2">{{ $skill['name'] }}</span>
+                                            <span class="text-xs text-gray-400 ml-2">(权重: {{ $skill['weight'] ?? 1 }})</span>
+                                        </span>
+                                    </label>
+                                @endforeach
+                            </div>
+                        </div>
+                    @else
+                        <div class="text-sm text-gray-500 italic">
+                            请先选择知识点以加载技能列表
+                        </div>
+                    @endif
+
+                    <div>
+                        <label class="block text-sm font-medium mb-2">题目数量</label>
+                        <input type="number" wire:model="questionCount" min="1" max="500" class="w-full border rounded p-2">
+                    </div>
+                </div>
+                <div class="flex justify-end gap-3 mt-6">
+                    <button type="button" wire:click="closeGenerateModal" class="px-4 py-2 border rounded" @disabled($isGenerating)>取消</button>
+                    <button
+                        type="button"
+                        wire:click="executeGenerate"
+                        wire:loading.attr="disabled"
+                        wire:loading.class="bg-yellow-500 cursor-not-allowed opacity-90"
+                        wire:loading.class.remove="bg-blue-600 hover:bg-blue-700"
+                        wire:target="executeGenerate"
+                        class="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded font-medium transition-all duration-200 flex items-center gap-2 text-white"
+                    >
+                        @if($isGenerating)
+                            <svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                            </svg>
+                            <span class="text-white font-semibold">生成中...</span>
+                        @else
+                            <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
+                            </svg>
+                            <span class="text-white font-semibold">开始生成</span>
+                        @endif
+                    </button>
+                </div>
+            </div>
+        </div>
+    @endif
+
+    <script>
+    document.addEventListener('livewire:init', () => {
+        Livewire.on('ai-generate', () => {
+            @this.call('openGenerateModal');
+        });
+
+        Livewire.on('refresh-page', () => {
+            // 页面刷新事件
+            // 触发数学公式重新渲染
+            document.dispatchEvent(new Event('math:render'));
+        });
+
+        // 监听页面刷新事件
+        Livewire.on('refresh-page', () => {
+            console.log('[QuestionGen] 收到刷新页面事件');
+            // 1秒后刷新页面,确保状态更新完成
+            setTimeout(() => {
+                console.log('[QuestionGen] 执行页面刷新');
+                window.location.reload();
+            }, 1000);
+        });
+
+        // ✅ 捕获回调参数,直接检查状态 - 避免盲目轮询
+        Livewire.on('start-async-task-monitoring', () => {
+            console.log('[QuestionGen] 开始监控任务状态');
+            const taskId = @this.currentTaskId;
+
+            if (!taskId) {
+                console.error('[QuestionGen] 未找到任务ID');
+                return;
+            }
+
+            window.currentTaskId = taskId;
+            let checkCount = 0;
+            const maxChecks = 5; // 最多检查5次
+
+            function checkCallbackStatus() {
+                checkCount++;
+                console.log(`[QuestionGen] 检查回调 #${checkCount}/${maxChecks}`);
+
+                // 直接调用 API 检查回调数据 - GET 请求无需 CSRF
+                fetch(`/api/questions/callback/${taskId}`, {
+                    method: 'GET',
+                    headers: {
+                        'X-Requested-With': 'XMLHttpRequest',
+                        'Accept': 'application/json',
+                    }
+                })
+                    .then(response => response.json())
+                    .then(data => {
+                        console.log('[QuestionGen] 回调数据:', data);
+
+                        // ✅ 如果有状态字段,说明回调已收到
+                        if (data.status) {
+                            if (data.status === 'completed') {
+                                console.log('[QuestionGen] ✅ 任务完成');
+                                @this.set('isGenerating', false);
+                                @this.set('currentTaskId', null);
+
+                                // 显示成功通知
+                                setTimeout(() => {
+                                    window.location.reload();
+                                }, 1000);
+                            } else if (data.status === 'failed') {
+                                console.log('[QuestionGen] ❌ 任务失败');
+                                @this.set('isGenerating', false);
+                                @this.set('currentTaskId', null);
+                            }
+                        } else if (checkCount < maxChecks) {
+                            // 没收到回调,继续检查
+                            setTimeout(checkCallbackStatus, 3000);
+                        } else {
+                            // 达到最大检查次数,停止
+                            console.log('[QuestionGen] 检查超时,停止监控');
+                            @this.set('isGenerating', false);
+                            @this.set('currentTaskId', null);
+                        }
+                    })
+                    .catch(error => {
+                        console.error('[QuestionGen] 检查回调失败:', error);
+                        if (checkCount < maxChecks) {
+                            setTimeout(checkCallbackStatus, 3000);
+                        }
+                    });
+            }
+
+            // 立即检查一次
+            checkCallbackStatus();
+
+            // 15秒后强制停止
+            setTimeout(() => {
+                if (checkCount < maxChecks) {
+                    console.log('[QuestionGen] 强制停止监控');
+                    @this.set('isGenerating', false);
+                    @this.set('currentTaskId', null);
+                }
+            }, 15000);
+        });
+
+        // 监听强制关闭状态栏事件
+        Livewire.on('force-close-status-bar', () => {
+            console.log('[QuestionGen] 强制关闭状态栏');
+            @this.set('isGenerating', false);
+            @this.set('currentTaskId', null);
+        });
+    });
+    </script>
+</div>
+
+<x-math-render />
+
+</x-filament-panels::page>

+ 491 - 0
resources/views/filament/pages/simulated-grading.blade.php

@@ -0,0 +1,491 @@
+<x-filament-panels::page>
+
+<div class="min-h-screen bg-gray-50 p-8">
+    {{-- 页面标题区域 --}}
+    {{-- 页面标题区域 --}}
+    <div class="mb-8">
+        <div class="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
+            <div class="flex items-center justify-between mb-6">
+                <div>
+                    <h1 class="text-3xl font-bold text-gray-900 flex items-center">
+                        <svg class="w-8 h-8 mr-3 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 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>
+                        专题测试
+                    </h1>
+                    <p class="mt-2 text-sm text-gray-600 ml-11">
+                        选择知识点和技能,生成题目进行练习,自动记录答题结果并更新学生掌握度
+                    </p>
+                </div>
+            </div>
+
+            {{-- 选择器区域 --}}
+            <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
+                <div class="flex items-end space-x-6">
+                    <div class="flex-1">
+                        <label for="teacher-select" class="block text-sm font-medium text-gray-700 mb-2">
+                            <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
+                            </svg>
+                            选择老师
+                        </label>
+                        <select
+                            id="teacher-select"
+                            wire:model.live="teacherId"
+                            class="select select-bordered select-primary w-full bg-white"
+                        >
+                            <option value="">请选择老师</option>
+                            @foreach ($teachers as $teacher)
+                                <option value="{{ $teacher->teacher_id }}">
+                                    {{ $teacher->name }} ({{ $teacher->subject }})
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    <div class="flex-1">
+                        <label for="student-select" class="block text-sm font-medium text-gray-700 mb-2">
+                            <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
+                            </svg>
+                            选择学生
+                        </label>
+                        <select
+                            id="student-select"
+                            wire:model.live="studentId"
+                            class="select select-bordered select-primary w-full bg-white"
+                            @disabled(empty($teacherId))
+                        >
+                            <option value="">请选择学生</option>
+                            @foreach ($students as $student)
+                                <option value="{{ $student->student_id }}">
+                                    {{ $student->name }} - {{ $student->grade }}{{ $student->class_name }}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    {{-- 加载状态 --}}
+    @if ($isLoading)
+        <div class="mb-8">
+            <div class="bg-white rounded-xl shadow-sm p-12 border border-gray-200">
+                <div class="flex flex-col items-center justify-center">
+                    <svg class="animate-spin h-12 w-12 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                    </svg>
+                    <p class="mt-4 text-sm text-gray-600">正在生成题目,请稍候...</p>
+                </div>
+            </div>
+        </div>
+    @else
+        {{-- 练习题目模块 --}}
+        <div class="mb-8">
+            <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                <div class="px-6 py-5 border-b border-gray-100">
+                    <div class="flex items-center justify-between mb-4">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                            </svg>
+                            专题测试练习
+                        </h3>
+
+                </div>
+                <div class="p-6">
+                        <div class="space-y-6">
+                            {{-- 知识点和技能选择区域 --}}
+                            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+                                {{-- 知识点选择 --}}
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                        <svg class="w-4 h-4 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
+                                        </svg>
+                                        选择知识点
+                                    </h4>
+                                    <select
+                                        wire:model.live="selectedKnowledgePoint"
+                                        class="select select-bordered select-primary w-full bg-white"
+                                    >
+                                        <option value="">随机知识点</option>
+                                        @foreach ($availableKnowledgePoints as $kp)
+                                            <option value="{{ $kp['id'] ?? $kp['code'] ?? $kp }}">{{ $kp['name'] ?? $kp['code'] ?? $kp }}</option>
+                                        @endforeach
+                                    </select>
+                                </div>
+
+                                {{-- 技能选择 --}}
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                        <svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                                        </svg>
+                                        选择技能(可多选)
+                                    </h4>
+                                    <div class="space-y-2 max-h-32 overflow-y-auto">
+                                        @foreach ($availableSkills as $skill)
+                                            <label class="flex items-center">
+                                                <input
+                                                    type="checkbox"
+                                                    wire:model.live="selectedSkills"
+                                                    value="{{ $skill['id'] ?? $skill['code'] ?? $skill }}"
+                                                    class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
+                                                />
+                                                <span class="ml-2 text-sm text-gray-700">{{ $skill['name'] ?? $skill['code'] ?? $skill }}</span>
+                                            </label>
+                                        @endforeach
+                                    </div>
+                                </div>
+                            </div>
+
+                            {{-- 题目数量和生成按钮 --}}
+                            <div class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
+                                <div class="flex items-center space-x-2">
+                                    <label class="text-sm font-medium text-gray-700">题目数量:</label>
+                                    <input
+                                        type="number"
+                                        wire:model.live="questionsPerSet"
+                                        min="1"
+                                        max="10"
+                                        class="w-16 rounded-lg border-gray-300 text-sm text-center bg-gray-50"
+                                    />
+                                </div>
+                                <button
+                                    wire:click="generateBatchQuestions"
+                                    wire:loading.attr="disabled"
+                                    class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
+                                >
+                                    <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                                    </svg>
+                                    生成题目组
+                                </button>
+                                <div class="text-sm text-gray-500">
+                                    当前批次ID: {{ $currentBatchId ?: '未生成' }}
+                                </div>
+                            </div>
+
+                            {{-- 题目列表 --}}
+                            @if (!empty($exerciseQuestions))
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-4">题目列表 (请标记对错)</h4>
+                                    <div class="space-y-4 max-h-96 overflow-y-auto">
+                                        @foreach ($exerciseQuestions as $index => $question)
+                                            <div class="border border-gray-200 rounded-lg p-4">
+                                                <div class="flex items-start justify-between mb-3">
+                                                    <div class="flex-1">
+                                                        <div class="flex items-center mb-2">
+                                                            <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-indigo-100 text-indigo-800 text-xs font-medium mr-2">
+                                                                {{ $index + 1 }}
+                                                            </span>
+                                                            <span class="text-sm text-gray-500">{{ $question['type'] ?? '数学题' }}</span>
+                                                            <span class="mx-2 text-gray-300">|</span>
+                                                            <span class="text-sm text-gray-500">难度: {{ $question['difficulty'] ?? 3 }}/5</span>
+                                                        </div>
+                                                        <h5 class="text-base font-medium text-gray-900">
+                                                            @math($question['content'] ?? '')
+                                                        </h5>
+                                                    </div>
+                                                </div>
+
+                                                <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+                                                    <div>
+                                                        <label class="block text-sm font-medium text-gray-700 mb-1">学生答案(可选)</label>
+                                                        <input
+                                                            type="text"
+                                                            wire:model.live="exerciseAnswers.{{ $index }}.user_answer"
+                                                            placeholder="输入答案..."
+                                                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50"
+                                                        />
+                                                    </div>
+                                                    <div>
+                                                        <label class="block text-sm font-medium text-gray-700 mb-1">答题结果</label>
+                                                        <div class="flex space-x-4">
+                                                            <label class="flex items-center">
+                                                                <input
+                                                                    type="radio"
+                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
+                                                                    value="1"
+                                                                    class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300"
+                                                                />
+                                                                <span class="ml-2 text-sm text-green-700">正确</span>
+                                                            </label>
+                                                            <label class="flex items-center">
+                                                                <input
+                                                                    type="radio"
+                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
+                                                                    value="0"
+                                                                    class="h-4 w-4 text-red-600 focus:ring-red-500 border-gray-300"
+                                                                />
+                                                                <span class="ml-2 text-sm text-red-700">错误</span>
+                                                            </label>
+                                                        </div>
+                                                    </div>
+                                                </div>
+
+                                                <div class="mt-3 p-2 bg-green-50 border border-green-200 rounded">
+                                                    <span class="text-xs text-green-800">正确答案: </span>
+                                                    <span class="text-xs">
+                                                        @math($question['answer'] ?? 'N/A')
+                                                    </span>
+                                                </div>
+                                            </div>
+                                        @endforeach
+                                    </div>
+
+                                    {{-- 批量提交按钮 --}}
+                                    <div class="mt-6 pt-4 border-t border-gray-200">
+                                        <button
+                                            wire:click="submitBatchAnswers"
+                                            wire:loading.attr="disabled"
+                                            class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
+                                        >
+                                            <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                                            </svg>
+                                            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                            </svg>
+                                            批量提交答案 ({{ count($exerciseQuestions) }} 题)
+                                        </button>
+                                    </div>
+                                </div>
+                            @else
+                                {{-- 批量模式空状态 --}}
+                                <div class="text-center py-12">
+                                    <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
+                                    </svg>
+                                    <h3 class="mt-4 text-lg font-medium text-gray-900">批量练习模式</h3>
+                                    <p class="mt-2 text-sm text-gray-500 max-w-md mx-auto">
+                                        选择知识点和技能,设置题目数量,点击"生成题目组"开始批量答题练习
+                                    </p>
+                                </div>
+                            @endif
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        {{-- 答题历史模块 --}}
+        @if (!empty($studentId))
+            <div class="mb-8">
+                <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                    <div class="px-6 py-5 border-b border-gray-100">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-purple-600" 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>
+                            答题历史
+                        </h3>
+                    </div>
+                    <div class="p-6">
+                        {{-- 每页显示数量选择 --}}
+                        <div class="flex items-center justify-between mb-6">
+                            <div class="flex items-center space-x-2">
+                                <label class="text-sm font-medium text-gray-700">每页显示:</label>
+                                <select
+                                    wire:model.live="historyPerPage"
+                                    class="select select-bordered select-sm select-primary bg-white"
+                                >
+                                    <option value="5">5 条</option>
+                                    <option value="10">10 条</option>
+                                    <option value="20">20 条</option>
+                                    <option value="50">50 条</option>
+                                </select>
+                            </div>
+                            <div class="text-sm text-gray-500">
+                                共 {{ $historyTotal }} 条记录
+                            </div>
+                        </div>
+
+                        {{-- 历史记录列表 --}}
+                        @if (!empty($exerciseHistory))
+                            <div class="space-y-4">
+                                @foreach ($exerciseHistory as $history)
+                                    <div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors">
+                                        <div class="flex items-start justify-between mb-3">
+                                            <div class="flex-1">
+                                                <div class="flex items-center mb-2">
+                                                    <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-purple-100 text-purple-800 text-xs font-medium mr-2">
+                                                        {{ $loop->iteration + ($historyCurrentPage - 1) * $historyPerPage }}
+                                                    </span>
+                                                    <span class="text-sm text-gray-500">批次: {{ $history['batch_id'] ?? 'N/A' }}</span>
+                                                    <span class="mx-2 text-gray-300">|</span>
+                                                    <span class="text-sm text-gray-500">
+                                                        {{ $history['kp_code'] ?? 'N/A' }}
+                                                    </span>
+                                                </div>
+                                                <h5 class="text-base font-medium text-gray-900 mb-2">
+                                                    @math(Str::limit($history['question_content'] ?? 'N/A', 100))
+                                                </h5>
+                                                <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-3">
+                                                    <div>
+                                                        <span class="text-xs text-gray-500">学生答案:</span>
+                                                        <div class="text-sm text-gray-900">
+                                                            {!! $history['student_answer'] ?? '未填写' !!}
+                                                        </div>
+                                                    </div>
+                                                    <div>
+                                                        <span class="text-xs text-gray-500">正确答案:</span>
+                                                        <div class="text-sm text-gray-900">
+                                                            {!! $history['correct_answer'] ?? 'N/A' !!}
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="ml-4 text-right">
+                                                @if ($history['is_correct'])
+                                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
+                                                        <svg class="w-3 h-3 mr-1" 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>
+                                                        正确
+                                                    </span>
+                                                @else
+                                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
+                                                        <svg class="w-3 h-3 mr-1" 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>
+                                                        错误
+                                                    </span>
+                                                @endif
+                                                <div class="mt-2 text-xs text-gray-500">
+                                                    {{ date('Y-m-d H:i', strtotime($history['created_at'])) }}
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                @endforeach
+                            </div>
+
+                            {{-- 分页导航 --}}
+                            @if ($historyTotalPages > 1)
+                                <div class="mt-6 pt-4 border-t border-gray-200 flex items-center justify-between">
+                                    <div class="text-sm text-gray-700">
+                                        显示第 {{ ($historyCurrentPage - 1) * $historyPerPage + 1 }} -
+                                        {{ min($historyCurrentPage * $historyPerPage, $historyTotal) }} 条,
+                                        共 {{ $historyTotal }} 条记录
+                                    </div>
+                                    <div class="flex items-center gap-2">
+                                        <button
+                                            wire:click="previousHistoryPage"
+                                            @disabled($historyCurrentPage <= 1)
+                                            class="px-3 py-1 border rounded-lg text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+                                        >
+                                            上一页
+                                        </button>
+
+                                        @foreach ($this->getHistoryPages() as $page)
+                                            <button
+                                                wire:click="gotoHistoryPage({{ $page }})"
+                                                class="px-3 py-1 border rounded-lg text-sm {{ $page === $historyCurrentPage ? 'bg-indigo-600 text-white' : 'hover:bg-gray-50' }}"
+                                            >
+                                                {{ $page }}
+                                            </button>
+                                        @endforeach
+
+                                        <button
+                                            wire:click="nextHistoryPage"
+                                            @disabled($historyCurrentPage >= $historyTotalPages)
+                                            class="px-3 py-1 border rounded-lg text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+                                        >
+                                            下一页
+                                        </button>
+                                    </div>
+                                </div>
+                            @endif
+                        @else
+                            <div class="text-center py-12">
+                                <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                </svg>
+                                <h3 class="mt-4 text-lg font-medium text-gray-900">暂无答题历史</h3>
+                                <p class="mt-2 text-sm text-gray-500 max-w-md mx-auto">
+                                    选择学生后,答题历史将显示在这里
+                                </p>
+                            </div>
+                        @endif
+                    </div>
+                </div>
+            </div>
+        @endif
+    @endif
+</div>
+
+{{-- 最简单的 Livewire 调试脚本 --}}
+<script>
+    document.addEventListener('DOMContentLoaded', function() {
+        console.log('页面加载完成,开始调试 Livewire');
+
+        setTimeout(() => {
+            if (typeof window.Livewire !== 'undefined') {
+                console.log('✅ Livewire 找到了');
+
+                // 查找所有 wire:click 元素
+                const wireClickElements = document.querySelectorAll('[wire\\:click]');
+                console.log(`找到 ${wireClickElements.length} 个 wire:click 元素:`);
+
+                wireClickElements.forEach((el, i) => {
+                    console.log(`  ${i+1}. ${el.textContent.trim()} -> ${el.getAttribute('wire:click')}`);
+                });
+
+                // 查找测试按钮
+                const testButtons = document.querySelectorAll('[wire\\:click*="test"], [wire\\:click*="increment"]');
+                console.log(`找到 ${testButtons.length} 个测试按钮:`);
+
+                testButtons.forEach((btn, i) => {
+                    console.log(`测试按钮 ${i+1}: ${btn.textContent.trim()}`);
+
+                    // 检查是否被禁用
+                    const disabled = btn.hasAttribute('disabled') || btn.disabled;
+                    console.log(`  状态: ${disabled ? '禁用' : '正常'}`);
+                });
+
+                // 检查 Livewire 组件
+                if (window.Livewire.components) {
+                    const componentKeys = Object.keys(window.Livewire.components.componentsById || {});
+                    console.log(`Livewire 组件数量: ${componentKeys.length}`);
+
+                    if (componentKeys.includes('simulated-grading')) {
+                        const component = window.Livewire.components.componentsById['simulated-grading'];
+                        console.log('SimulatedGrading 组件数据:', {
+                            studentId: component.data.studentId,
+                            exerciseHistory: component.data.exerciseHistory,
+                            historyCurrentPage: component.data.historyCurrentPage,
+                            historyPerPage: component.data.historyPerPage,
+                            historyTotal: component.data.historyTotal,
+                            historyTotalPages: component.data.historyTotalPages,
+                            hasHistory: component.data.exerciseHistory && component.data.exerciseHistory.length > 0
+                        });
+                    }
+                }
+
+            } else {
+                console.error('❌ Livewire 未找到');
+                console.log('检查全局变量:', {
+                    'window.Livewire': typeof window.Livewire,
+                    'window.livewire': typeof window.livewire,
+                    'window.Alpine': typeof window.Alpine
+                });
+            }
+        }, 2000);
+    });
+
+    // 监听通知事件
+    document.addEventListener('notify', (event) => {
+        console.log('🔔 收到通知:', event.detail);
+    });
+</script>
+
+<x-math-render />
+
+</x-filament-panels::page>

+ 432 - 0
resources/views/filament/pages/simulated-grading.blade.php.backup

@@ -0,0 +1,432 @@
+<x-filament-panels::page>
+
+<div class="min-h-screen bg-gray-50 p-8">
+    {{-- 页面标题区域 --}}
+    <div class="mb-8">
+        <div class="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
+            <div class="flex items-center justify-between mb-6">
+                <div>
+                    <h1 class="text-3xl font-bold text-gray-900 flex items-center">
+                        <svg class="w-8 h-8 mr-3 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 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>
+                        模拟判卷
+                    </h1>
+                    <p class="mt-2 text-sm text-gray-600 ml-11">
+                        选择知识点和技能,生成题目进行练习,自动记录答题结果并更新学生掌握度
+                    </p>
+                </div>
+            </div>
+
+            {{-- 选择器区域 --}}
+            <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
+                <div class="flex items-end space-x-6">
+                    <div class="flex-1">
+                        <label for="teacher-select" class="block text-sm font-medium text-gray-700 mb-2">
+                            <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
+                            </svg>
+                            选择老师
+                        </label>
+                        <select
+                            id="teacher-select"
+                            wire:model.live="teacherId"
+                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm py-2.5 bg-gray-50"
+                        >
+                            <option value="">请选择老师</option>
+                            @foreach ($teachers as $teacher)
+                                <option value="{{ $teacher->teacher_id }}">
+                                    {{ $teacher->name }} ({{ $teacher->subject }})
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    <div class="flex-1">
+                        <label for="student-select" class="block text-sm font-medium text-gray-700 mb-2">
+                            <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
+                            </svg>
+                            选择学生
+                        </label>
+                        <select
+                            id="student-select"
+                            wire:model.live="studentId"
+                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm py-2.5 bg-gray-50"
+                            @disabled(empty($teacherId))
+                        >
+                            <option value="">请选择学生</option>
+                            @foreach ($students as $student)
+                                <option value="{{ $student->student_id }}">
+                                    {{ $student->name }} - {{ $student->grade }}{{ $student->class_name }}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    {{-- 加载状态 --}}
+    @if ($isLoading)
+        <div class="mb-8">
+            <div class="bg-white rounded-xl shadow-sm p-12 border border-gray-200">
+                <div class="flex flex-col items-center justify-center">
+                    <svg class="animate-spin h-12 w-12 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                    </svg>
+                    <p class="mt-4 text-sm text-gray-600">正在生成题目,请稍候...</p>
+                </div>
+            </div>
+        </div>
+    @else
+        {{-- 练习题目模块 --}}
+        <div class="mb-8">
+            <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                <div class="px-6 py-5 border-b border-gray-100">
+                    <div class="flex items-center justify-between mb-4">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                            </svg>
+                            模拟判卷练习
+                        </h3>
+
+                </div>
+                <div class="p-6">
+                        <div class="space-y-6">
+                            {{-- 知识点和技能选择区域 --}}
+                            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+                                {{-- 知识点选择 --}}
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                        <svg class="w-4 h-4 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
+                                        </svg>
+                                        选择知识点
+                                    </h4>
+                                    <select
+                                        wire:model.live="selectedKnowledgePoint"
+                                        class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50"
+                                    >
+                                        <option value="">随机知识点</option>
+                                        @foreach ($availableKnowledgePoints as $kp)
+                                            <option value="{{ $kp['id'] ?? $kp['code'] ?? $kp }}">{{ $kp['name'] ?? $kp['code'] ?? $kp }}</option>
+                                        @endforeach
+                                    </select>
+                                </div>
+
+                                {{-- 技能选择 --}}
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
+                                        <svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                                        </svg>
+                                        选择技能(可多选)
+                                    </h4>
+                                    <div class="space-y-2 max-h-32 overflow-y-auto">
+                                        @foreach ($availableSkills as $skill)
+                                            <label class="flex items-center">
+                                                <input
+                                                    type="checkbox"
+                                                    wire:model.live="selectedSkills"
+                                                    value="{{ $skill['id'] ?? $skill['code'] ?? $skill }}"
+                                                    class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
+                                                />
+                                                <span class="ml-2 text-sm text-gray-700">{{ $skill['name'] ?? $skill['code'] ?? $skill }}</span>
+                                            </label>
+                                        @endforeach
+                                    </div>
+                                </div>
+                            </div>
+
+                            {{-- 题目数量和生成按钮 --}}
+                            <div class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
+                                <div class="flex items-center space-x-2">
+                                    <label class="text-sm font-medium text-gray-700">题目数量:</label>
+                                    <input
+                                        type="number"
+                                        wire:model.live="questionsPerSet"
+                                        min="1"
+                                        max="10"
+                                        class="w-16 rounded-lg border-gray-300 text-sm text-center bg-gray-50"
+                                    />
+                                </div>
+                                <button
+                                    wire:click="generateBatchQuestions"
+                                    wire:loading.attr="disabled"
+                                    class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
+                                >
+                                    <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                                    </svg>
+                                    生成题目组
+                                </button>
+                                <div class="text-sm text-gray-500">
+                                    当前批次ID: {{ $currentBatchId ?: '未生成' }}
+                                </div>
+                            </div>
+
+                            {{-- 题目列表 --}}
+                            @if (!empty($exerciseQuestions))
+                                <div class="border border-gray-200 rounded-lg p-4">
+                                    <h4 class="text-sm font-medium text-gray-900 mb-4">题目列表 (请标记对错)</h4>
+                                    <div class="space-y-4 max-h-96 overflow-y-auto">
+                                        @foreach ($exerciseQuestions as $index => $question)
+                                            <div class="border border-gray-200 rounded-lg p-4">
+                                                <div class="flex items-start justify-between mb-3">
+                                                    <div class="flex-1">
+                                                        <div class="flex items-center mb-2">
+                                                            <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-indigo-100 text-indigo-800 text-xs font-medium mr-2">
+                                                                {{ $index + 1 }}
+                                                            </span>
+                                                            <span class="text-sm text-gray-500">{{ $question['type'] ?? '数学题' }}</span>
+                                                            <span class="mx-2 text-gray-300">|</span>
+                                                            <span class="text-sm text-gray-500">难度: {{ $question['difficulty'] ?? 3 }}/5</span>
+                                                        </div>
+                                                        <h5 class="text-base font-medium text-gray-900">
+                                                            {{ $question['content'] ?? '' }}
+                                                        </h5>
+                                                    </div>
+                                                </div>
+
+                                                <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+                                                    <div>
+                                                        <label class="block text-sm font-medium text-gray-700 mb-1">学生答案(可选)</label>
+                                                        <input
+                                                            type="text"
+                                                            wire:model.live="exerciseAnswers.{{ $index }}.user_answer"
+                                                            placeholder="输入答案..."
+                                                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50"
+                                                        />
+                                                    </div>
+                                                    <div>
+                                                        <label class="block text-sm font-medium text-gray-700 mb-1">答题结果</label>
+                                                        <div class="flex space-x-4">
+                                                            <label class="flex items-center">
+                                                                <input
+                                                                    type="radio"
+                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
+                                                                    value="1"
+                                                                    class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300"
+                                                                />
+                                                                <span class="ml-2 text-sm text-green-700">正确</span>
+                                                            </label>
+                                                            <label class="flex items-center">
+                                                                <input
+                                                                    type="radio"
+                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
+                                                                    value="0"
+                                                                    class="h-4 w-4 text-red-600 focus:ring-red-500 border-gray-300"
+                                                                />
+                                                                <span class="ml-2 text-sm text-red-700">错误</span>
+                                                            </label>
+                                                        </div>
+                                                    </div>
+                                                </div>
+
+                                                <div class="mt-3 p-2 bg-green-50 border border-green-200 rounded">
+                                                    <span class="text-xs text-green-800">正确答案: </span>
+                                                    <span class="text-xs">
+                                                        {{ $question['answer'] ?? 'N/A' }}
+                                                    </span>
+                                                </div>
+                                            </div>
+                                        @endforeach
+                                    </div>
+
+                                    {{-- 批量提交按钮 --}}
+                                    <div class="mt-6 pt-4 border-t border-gray-200">
+                                        <button
+                                            wire:click="submitBatchAnswers"
+                                            wire:loading.attr="disabled"
+                                            class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
+                                        >
+                                            <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                                            </svg>
+                                            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                            </svg>
+                                            批量提交答案 ({{ count($exerciseQuestions) }} 题)
+                                        </button>
+                                    </div>
+                                </div>
+                            @else
+                                {{-- 批量模式空状态 --}}
+                                <div class="text-center py-12">
+                                    <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
+                                    </svg>
+                                    <h3 class="mt-4 text-lg font-medium text-gray-900">批量练习模式</h3>
+                                    <p class="mt-2 text-sm text-gray-500 max-w-md mx-auto">
+                                        选择知识点和技能,设置题目数量,点击"生成题目组"开始批量答题练习
+                                    </p>
+                                </div>
+                            @endif
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        {{-- 答题历史模块 --}}
+        @if (!empty($studentId))
+            <div class="mb-8">
+                <div class="bg-white shadow-sm rounded-xl border border-gray-200">
+                    <div class="px-6 py-5 border-b border-gray-100">
+                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
+                            <svg class="w-5 h-5 mr-2 text-purple-600" 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>
+                            答题历史
+                        </h3>
+                    </div>
+                    <div class="p-6">
+                        {{-- 每页显示数量选择 --}}
+                        <div class="flex items-center justify-between mb-6">
+                            <div class="flex items-center space-x-2">
+                                <label class="text-sm font-medium text-gray-700">每页显示:</label>
+                                <select
+                                    wire:model.live="historyPerPage"
+                                    class="rounded-lg border-gray-300 text-sm bg-gray-50"
+                                >
+                                    <option value="5">5 条</option>
+                                    <option value="10">10 条</option>
+                                    <option value="20">20 条</option>
+                                    <option value="50">50 条</option>
+                                </select>
+                            </div>
+                            <div class="text-sm text-gray-500">
+                                共 {{ $historyTotal }} 条记录
+                            </div>
+                        </div>
+
+                        {{-- 历史记录列表 --}}
+                        @if (!empty($exerciseHistory))
+                            <div class="space-y-4">
+                                @foreach ($exerciseHistory as $history)
+                                    <div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors">
+                                        <div class="flex items-start justify-between mb-3">
+                                            <div class="flex-1">
+                                                <div class="flex items-center mb-2">
+                                                    <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-purple-100 text-purple-800 text-xs font-medium mr-2">
+                                                        {{ $loop->iteration + ($historyCurrentPage - 1) * $historyPerPage }}
+                                                    </span>
+                                                    <span class="text-sm text-gray-500">批次: {{ $history['batch_id'] ?? 'N/A' }}</span>
+                                                    <span class="mx-2 text-gray-300">|</span>
+                                                    <span class="text-sm text-gray-500">
+                                                        {{ $history['kp_code'] ?? 'N/A' }}
+                                                    </span>
+                                                </div>
+                                                <h5 class="text-base font-medium text-gray-900 mb-2">
+                                                    {{ Str::limit($history['question_content'] ?? 'N/A', 100) }}
+                                                </h5>
+                                                <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-3">
+                                                    <div>
+                                                        <span class="text-xs text-gray-500">学生答案:</span>
+                                                        <div class="text-sm text-gray-900">
+                                                            {{ $history['student_answer'] ?? '未填写' }}
+                                                        </div>
+                                                    </div>
+                                                    <div>
+                                                        <span class="text-xs text-gray-500">正确答案:</span>
+                                                        <div class="text-sm text-gray-900">
+                                                            {{ $history['correct_answer'] ?? 'N/A' }}
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="ml-4 text-right">
+                                                @if ($history['is_correct'])
+                                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
+                                                        <svg class="w-3 h-3 mr-1" 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>
+                                                        正确
+                                                    </span>
+                                                @else
+                                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
+                                                        <svg class="w-3 h-3 mr-1" 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>
+                                                        错误
+                                                    </span>
+                                                @endif
+                                                <div class="mt-2 text-xs text-gray-500">
+                                                    {{ date('Y-m-d H:i', strtotime($history['created_at'])) }}
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                @endforeach
+                            </div>
+
+                            {{-- 分页导航 --}}
+                            @if ($historyTotalPages > 1)
+                                <div class="mt-6 pt-4 border-t border-gray-200 flex items-center justify-between">
+                                    <div class="text-sm text-gray-700">
+                                        显示第 {{ ($historyCurrentPage - 1) * $historyPerPage + 1 }} -
+                                        {{ min($historyCurrentPage * $historyPerPage, $historyTotal) }} 条,
+                                        共 {{ $historyTotal }} 条记录
+                                    </div>
+                                    <div class="flex items-center gap-2">
+                                        <button
+                                            wire:click="previousHistoryPage"
+                                            @disabled($historyCurrentPage <= 1)
+                                            class="px-3 py-1 border rounded-lg text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+                                        >
+                                            上一页
+                                        </button>
+
+                                        @foreach ($this->getHistoryPages() as $page)
+                                            <button
+                                                wire:click="gotoHistoryPage({{ $page }})"
+                                                class="px-3 py-1 border rounded-lg text-sm {{ $page === $historyCurrentPage ? 'bg-indigo-600 text-white' : 'hover:bg-gray-50' }}"
+                                            >
+                                                {{ $page }}
+                                            </button>
+                                        @endforeach
+
+                                        <button
+                                            wire:click="nextHistoryPage"
+                                            @disabled($historyCurrentPage >= $historyTotalPages)
+                                            class="px-3 py-1 border rounded-lg text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+                                        >
+                                            下一页
+                                        </button>
+                                    </div>
+                                </div>
+                            @endif
+                        @else
+                            <div class="text-center py-12">
+                                <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                </svg>
+                                <h3 class="mt-4 text-lg font-medium text-gray-900">暂无答题历史</h3>
+                                <p class="mt-2 text-sm text-gray-500 max-w-md mx-auto">
+                                    选择学生后,答题历史将显示在这里
+                                </p>
+                            </div>
+                        @endif
+                    </div>
+                </div>
+            </div>
+        @endif
+    @endif
+</div>
+
+{{-- 通知脚本 --}}
+<script>
+    document.addEventListener('notify', (event) => {
+        const message = event.detail.message;
+        const type = event.detail.type || 'info';
+        alert(message);
+    });
+</script>
+
+</x-filament-panels::page>

+ 0 - 186
resources/views/filament/pages/student-dashboard.blade.php

@@ -702,192 +702,6 @@
             </div>
         @endif --}}
 
-        {{-- 练习题目模块 --}}
-        <div class="mb-8">
-            <div class="bg-white shadow-sm rounded-xl border border-gray-200">
-                <div class="px-6 py-5 border-b border-gray-100">
-                    <div class="flex items-center justify-between mb-4">
-                        <h3 class="text-lg font-semibold text-gray-900 flex items-center">
-                            <svg class="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
-                            </svg>
-                            模拟判卷
-                        </h3>
-
-                </div>
-                <div class="p-6">
-                        <div class="space-y-6">
-                            {{-- 知识点和技能选择区域 --}}
-                            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
-                                {{-- 知识点选择 --}}
-                                <div class="border border-gray-200 rounded-lg p-4">
-                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
-                                        <svg class="w-4 h-4 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
-                                        </svg>
-                                        选择知识点
-                                    </h4>
-                                    <select
-                                        wire:model.live="selectedKnowledgePoint"
-                                        class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50"
-                                    >
-                                        <option value="">随机知识点</option>
-                                        @foreach ($availableKnowledgePoints as $kp)
-                                            <option value="{{ $kp['id'] ?? $kp['code'] ?? $kp }}">{{ $kp['name'] ?? $kp['code'] ?? $kp }}</option>
-                                        @endforeach
-                                    </select>
-                                </div>
-
-                                {{-- 技能选择 --}}
-                                <div class="border border-gray-200 rounded-lg p-4">
-                                    <h4 class="text-sm font-medium text-gray-900 mb-3 flex items-center">
-                                        <svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
-                                        </svg>
-                                        选择技能(可多选)
-                                    </h4>
-                                    <div class="space-y-2 max-h-32 overflow-y-auto">
-                                        @foreach ($availableSkills as $skill)
-                                            <label class="flex items-center">
-                                                <input
-                                                    type="checkbox"
-                                                    wire:model.live="selectedSkills"
-                                                    value="{{ $skill['id'] ?? $skill['code'] ?? $skill }}"
-                                                    class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
-                                                />
-                                                <span class="ml-2 text-sm text-gray-700">{{ $skill['name'] ?? $skill['code'] ?? $skill }}</span>
-                                            </label>
-                                        @endforeach
-                                    </div>
-                                </div>
-                            </div>
-
-                            {{-- 题目数量和生成按钮 --}}
-                            <div class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
-                                <div class="flex items-center space-x-2">
-                                    <label class="text-sm font-medium text-gray-700">题目数量:</label>
-                                    <input
-                                        type="number"
-                                        wire:model.live="questionsPerSet"
-                                        min="1"
-                                        max="10"
-                                        class="w-16 rounded-lg border-gray-300 text-sm text-center bg-gray-50"
-                                    />
-                                </div>
-                                <button
-                                    wire:click="generateBatchQuestions"
-                                    wire:loading.attr="disabled"
-                                    class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
-                                >
-                                    <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-                                    </svg>
-                                    生成题目组
-                                </button>
-                                <div class="text-sm text-gray-500">
-                                    当前批次ID: {{ $currentBatchId ?: '未生成' }}
-                                </div>
-                            </div>
-
-                            {{-- 题目列表 --}}
-                            @if (!empty($exerciseQuestions))
-                                <div class="border border-gray-200 rounded-lg p-4">
-                                    <h4 class="text-sm font-medium text-gray-900 mb-4">题目列表 (请标记对错)</h4>
-                                    <div class="space-y-4 max-h-96 overflow-y-auto">
-                                        @foreach ($exerciseQuestions as $index => $question)
-                                            <div class="border border-gray-200 rounded-lg p-4">
-                                                <div class="flex items-start justify-between mb-3">
-                                                    <div class="flex-1">
-                                                        <div class="flex items-center mb-2">
-                                                            <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-indigo-100 text-indigo-800 text-xs font-medium mr-2">
-                                                                {{ $index + 1 }}
-                                                            </span>
-                                                            <span class="text-sm text-gray-500">{{ $question['type'] ?? '数学题' }}</span>
-                                                            <span class="mx-2 text-gray-300">|</span>
-                                                            <span class="text-sm text-gray-500">难度: {{ $question['difficulty'] ?? 3 }}/5</span>
-                                                        </div>
-                                                        <h5 class="text-base font-medium text-gray-900">{{ $question['content'] }}</h5>
-                                                    </div>
-                                                </div>
-
-                                                <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
-                                                    <div>
-                                                        <label class="block text-sm font-medium text-gray-700 mb-1">学生答案(可选)</label>
-                                                        <input
-                                                            type="text"
-                                                            wire:model.live="exerciseAnswers.{{ $index }}.user_answer"
-                                                            placeholder="输入答案..."
-                                                            class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50"
-                                                        />
-                                                    </div>
-                                                    <div>
-                                                        <label class="block text-sm font-medium text-gray-700 mb-1">答题结果</label>
-                                                        <div class="flex space-x-4">
-                                                            <label class="flex items-center">
-                                                                <input
-                                                                    type="radio"
-                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
-                                                                    value="1"
-                                                                    class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300"
-                                                                />
-                                                                <span class="ml-2 text-sm text-green-700">正确</span>
-                                                            </label>
-                                                            <label class="flex items-center">
-                                                                <input
-                                                                    type="radio"
-                                                                    wire:model.live="exerciseAnswers.{{ $index }}.is_correct"
-                                                                    value="0"
-                                                                    class="h-4 w-4 text-red-600 focus:ring-red-500 border-gray-300"
-                                                                />
-                                                                <span class="ml-2 text-sm text-red-700">错误</span>
-                                                            </label>
-                                                        </div>
-                                                    </div>
-                                                </div>
-
-                                                <div class="mt-3 p-2 bg-green-50 border border-green-200 rounded">
-                                                    <span class="text-xs text-green-800">正确答案: {{ $question['answer'] ?? 'N/A' }}</span>
-                                                </div>
-                                            </div>
-                                        @endforeach
-                                    </div>
-
-                                    {{-- 批量提交按钮 --}}
-                                    <div class="mt-6 pt-4 border-t border-gray-200">
-                                        <button
-                                            wire:click="submitBatchAnswers"
-                                            wire:loading.attr="disabled"
-                                            class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
-                                        >
-                                            <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-                                            </svg>
-                                            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
-                                            </svg>
-                                            批量提交答案 ({{ count($exerciseQuestions) }} 题)
-                                        </button>
-                                    </div>
-                                </div>
-                            @else
-                                {{-- 批量模式空状态 --}}
-                                <div class="text-center py-12">
-                                    <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
-                                    </svg>
-                                    <h3 class="mt-4 text-lg font-medium text-gray-900">批量练习模式</h3>
-                                    <p class="mt-2 text-sm text-gray-500 max-w-md mx-auto">
-                                        选择知识点和技能,设置题目数量,点击"生成题目组"开始批量答题练习
-                                    </p>
-                                </div>
-                            @endif
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
     </div>
 </div>
 @endif

+ 0 - 25
resources/views/test-math.blade.php

@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>数学公式渲染测试</title>
-    <link rel="stylesheet" href="/css/katex/katex.min.css">
-</head>
-<body>
-    <h1>数学公式渲染测试</h1>
-
-    <h2>测试1: 简单公式</h2>
-    <x-math-render :content="'$f(x) = 2x^2 - 3x + 1$'" class="text-lg" />
-
-    <h2>测试2: 分数公式</h2>
-    <x-math-render :content="'$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$'" class="text-lg" />
-
-    <h2>测试3: 积分公式</h2>
-    <x-math-render :content="'$$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$'" class="text-lg" />
-
-    <h2>测试4: 求和公式</h2>
-    <x-math-render :content="'$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$'" class="text-lg" />
-
-    <script src="/js/katex.min.js"></script>
-    <script src="/js/math-render.js"></script>
-</body>
-</html>

+ 1 - 0
resources/views/test-math3.blade.php

@@ -0,0 +1 @@
+测试: @math('化简代数式:$$3(x+2) - 2(x-1)$$')

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini