[], 'skill_ids' => [], 'error_types' => [], 'time_range' => 'last_30', 'start_date' => null, 'end_date' => null, 'sort_by' => 'created_at_desc', 'correct_filter' => 'incorrect', // 默认只显示错误题目 'filter' => [], ]; public array $filterOptions = [ 'knowledge_points' => [], 'skills' => [], ]; public array $mistakes = []; public array $patterns = []; public array $summary = []; public array $recommendations = []; public array $relatedQuestions = []; public array $selectedMistakeIds = []; public bool $isLoading = false; public string $errorMessage = ''; public string $actionMessage = ''; public string $actionMessageType = 'success'; // 分页 public int $page = 1; public int $perPage = 10; public int $total = 0; public function mount(Request $request): void { // 初始化用户角色检查 $this->initializeUserRole(); // 如果是老师,自动选择当前老师 if ($this->isTeacher) { $teacherId = $this->getCurrentTeacherId(); if ($teacherId) { $this->teacherId = $teacherId; } } else { $this->teacherId = (string) ($request->input('teacher_id') ?? ''); } $this->studentId = (string) ($request->input('student_id') ?? ''); $this->filters['time_range'] = (string) ($request->input('range') ?? 'last_30'); if ($this->studentId && empty($this->teacherId)) { $student = Student::find($this->studentId); if ($student && $student->teacher_id) { $this->teacherId = (string) $student->teacher_id; } } $this->loadFilterOptions(); if ($this->studentId) { $this->loadMistakeData(); } } public function updatedStudentId(): void { if ($this->studentId) { $this->loadMistakeData(); } else { $this->resetPageState(); } } public function loadMistakeData(): void { if (empty($this->studentId)) { $this->errorMessage = '请先选择学生'; return; } $this->isLoading = true; $this->errorMessage = ''; $this->actionMessage = ''; try { $service = app(MistakeBookService::class); // 处理筛选参数 $params = [ 'student_id' => $this->studentId, 'page' => $this->page, 'per_page' => $this->perPage, ]; // 基础筛选 if (!empty($this->filters['kp_ids'])) { $params['kp_ids'] = $this->filters['kp_ids']; } if (!empty($this->filters['skill_ids'])) { $params['skill_ids'] = $this->filters['skill_ids']; } if (!empty($this->filters['error_types'])) { $params['error_types'] = $this->filters['error_types']; } if (isset($this->filters['time_range']) && $this->filters['time_range'] !== 'all') { $params['time_range'] = $this->filters['time_range']; } if (!empty($this->filters['start_date'])) { $params['start_date'] = $this->filters['start_date']; } if (!empty($this->filters['end_date'])) { $params['end_date'] = $this->filters['end_date']; } if (!empty($this->filters['sort_by'])) { $params['sort_by'] = $this->filters['sort_by']; } // 正确与否筛选 if (isset($this->filters['correct_filter']) && $this->filters['correct_filter'] !== 'all') { if ($this->filters['correct_filter'] === 'correct') { $params['correct_only'] = true; } elseif ($this->filters['correct_filter'] === 'incorrect') { $params['incorrect_only'] = true; } } // 状态筛选 if (!empty($this->filters['filter'])) { if (in_array('unreviewed', $this->filters['filter'])) { $params['unreviewed_only'] = true; } if (in_array('favorite', $this->filters['filter'])) { $params['favorite_only'] = true; } } $list = $service->listMistakes($params); $this->mistakes = $list['data'] ?? []; $this->total = $list['meta']['total'] ?? 0; $this->summary = $service->summarize($this->studentId); $this->patterns = $service->getMistakePatterns($this->studentId); // 清理无效的选中项 $validIds = collect($this->mistakes)->pluck('id')->filter()->all(); $this->selectedMistakeIds = array_values( array_intersect($this->selectedMistakeIds, $validIds) ); } catch (\Throwable $e) { $this->errorMessage = '加载错题本数据失败:' . $e->getMessage(); Log::error('Load mistake book failed', [ 'student_id' => $this->studentId, 'error' => $e->getMessage(), ]); } finally { $this->isLoading = false; } } public function gotoPage(int $page): void { $this->page = max(1, $page); $this->loadMistakeData(); } public function nextPage(): void { $maxPage = (int) ceil($this->total / $this->perPage); if ($this->page < $maxPage) { $this->page++; $this->loadMistakeData(); } } public function prevPage(): void { if ($this->page > 1) { $this->page--; $this->loadMistakeData(); } } public function refreshPatterns(): void { if (!$this->studentId) { return; } try { $service = app(MistakeBookService::class); $this->patterns = $service->getMistakePatterns($this->studentId); } catch (\Throwable $e) { Log::error('Refresh mistake patterns failed', [ 'student_id' => $this->studentId, 'error' => $e->getMessage(), ]); } } public function toggleFavorite(string $mistakeId): void { $service = app(MistakeBookService::class); $current = $this->findMistakeById($mistakeId); $willFavorite = !($current['favorite'] ?? false); if ($service->toggleFavorite($mistakeId, $willFavorite)) { $this->updateMistakeField($mistakeId, 'favorite', $willFavorite); $this->notify('已更新收藏状态'); } else { $this->notify('收藏操作失败,请稍后再试', 'danger'); } } public function markReviewed(string $mistakeId): void { $service = app(MistakeBookService::class); if ($service->markReviewed($mistakeId)) { $this->updateMistakeField($mistakeId, 'reviewed', true); $this->notify('已标记为已复习'); } else { $this->notify('标记失败,请稍后再试', 'danger'); } } public function addToRetryList(string $mistakeId): void { $service = app(MistakeBookService::class); if ($service->addToRetryList($mistakeId)) { $this->notify('已加入重练清单'); } else { $this->notify('加入清单失败,请稍后再试', 'danger'); } } public function loadRelatedQuestions(string $mistakeId): void { $mistake = $this->findMistakeById($mistakeId); if (empty($mistake)) { return; } $questionBank = app(QuestionBankService::class); $kpIds = Arr::wrap($mistake['kp_ids'] ?? []); $skills = Arr::wrap($mistake['skill_ids'] ?? $mistake['skills'] ?? []); $response = $questionBank->filterQuestions(array_filter([ 'kp_codes' => !empty($kpIds) ? implode(',', $kpIds) : null, 'skills' => !empty($skills) ? implode(',', $skills) : null, 'limit' => 5, ])); $this->relatedQuestions[$mistakeId] = $response['data'] ?? []; } public function toggleSelection(string $mistakeId): void { if (in_array($mistakeId, $this->selectedMistakeIds, true)) { $this->selectedMistakeIds = array_values(array_diff($this->selectedMistakeIds, [$mistakeId])); } else { $this->selectedMistakeIds[] = $mistakeId; } } public function generatePracticeFromSelection(): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $selected = array_filter($this->mistakes, fn ($item) => in_array($item['id'] ?? '', $this->selectedMistakeIds, true)); $kpIds = collect($selected) ->pluck('kp_ids') ->flatten() ->filter() ->unique() ->values() ->all(); $skillIds = collect($selected) ->pluck('skill_ids') ->flatten() ->filter() ->unique() ->values() ->all(); $service = app(MistakeBookService::class); $result = $service->recommendPractice($this->studentId, $kpIds, $skillIds); $this->recommendations = $result['data'] ?? ($result['questions'] ?? []); if (!empty($this->recommendations)) { $this->notify('已生成重练题单'); } else { $this->notify('未能生成题单,请稍后再试', 'warning'); } } public function batchMarkReviewed(): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $service = app(MistakeBookService::class); $result = $service->batchOperation($this->selectedMistakeIds, 'reviewed'); if ($result['success'] ?? false) { $this->selectedMistakeIds = []; $this->notify("已标记 {$result['success_count']} 道题为已复习"); $this->loadMistakeData(); // 重新加载数据 } else { $this->notify($result['error'] ?? '操作失败', 'danger'); } } /** * 批量标记为已掌握 */ public function batchMarkMastered(): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $service = app(MistakeBookService::class); $result = $service->batchOperation($this->selectedMistakeIds, 'mastered'); if ($result['success'] ?? false) { $this->selectedMistakeIds = []; $this->notify("已标记 {$result['success_count']} 道题为重点掌握"); $this->loadMistakeData(); } else { $this->notify($result['error'] ?? '操作失败', 'danger'); } } /** * 批量加入重练清单 */ public function batchAddToRetryList(): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $service = app(MistakeBookService::class); $result = $service->batchOperation($this->selectedMistakeIds, 'retry_list'); if ($result['success'] ?? false) { $this->notify("已加入 {$result['success_count']} 道题到重练清单"); $this->loadMistakeData(); } else { $this->notify($result['error'] ?? '操作失败', 'danger'); } } /** * 批量从重练清单移除 */ public function batchRemoveFromRetryList(): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $service = app(MistakeBookService::class); $result = $service->batchOperation($this->selectedMistakeIds, 'remove_retry_list'); if ($result['success'] ?? false) { $this->notify("已从重练清单移除 {$result['success_count']} 道题"); $this->loadMistakeData(); } else { $this->notify($result['error'] ?? '操作失败', 'danger'); } } /** * 批量设置错误类型 */ public function batchSetErrorType(string $errorType): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $service = app(MistakeBookService::class); $result = $service->batchOperation($this->selectedMistakeIds, 'set_error_type', [ 'error_type' => $errorType, ]); if ($result['success'] ?? false) { $this->selectedMistakeIds = []; $this->notify("已为 {$result['success_count']} 道题设置错误类型"); $this->loadMistakeData(); } else { $this->notify($result['error'] ?? '操作失败', 'danger'); } } /** * 批量设置重要程度 */ public function batchSetImportance(int $importance): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $service = app(MistakeBookService::class); $result = $service->batchOperation($this->selectedMistakeIds, 'set_importance', [ 'importance' => $importance, ]); if ($result['success'] ?? false) { $this->selectedMistakeIds = []; $this->notify("已为 {$result['success_count']} 道题设置重要程度"); $this->loadMistakeData(); } else { $this->notify($result['error'] ?? '操作失败', 'danger'); } } /** * 批量切换收藏状态 */ public function batchToggleFavorite(): void { if (empty($this->selectedMistakeIds)) { $this->notify('请先选择至少一道错题', 'warning'); return; } $service = app(MistakeBookService::class); $result = $service->batchOperation($this->selectedMistakeIds, 'favorite'); if ($result['success'] ?? false) { $this->selectedMistakeIds = []; $this->notify("已为 {$result['success_count']} 道题切换收藏状态"); $this->loadMistakeData(); } else { $this->notify($result['error'] ?? '操作失败', 'danger'); } } public function startQuickReview(): void { if (empty($this->mistakes)) { $this->notify('没有可复习的错题', 'warning'); return; } // 选取前5题进行快速复习 $reviewIds = collect($this->mistakes) ->take(5) ->pluck('id') ->filter() ->values() ->all(); // 自动选中这些题 $this->selectedMistakeIds = $reviewIds; $this->notify('已选择前5题进行快速复习'); } public function applyFilters(): void { $this->page = 1; // 重置到第一页 $this->loadMistakeData(); } public function clearFilters(): void { $this->filters = [ 'kp_ids' => [], 'skill_ids' => [], 'error_types' => [], 'time_range' => 'last_30', 'start_date' => null, 'end_date' => null, 'sort_by' => 'created_at_desc', 'correct_filter' => 'incorrect', 'filter' => [], ]; $this->page = 1; $this->loadMistakeData(); } public function resetFilters(): void { $this->filters = [ 'kp_ids' => [], 'skill_ids' => [], 'error_types' => [], 'time_range' => 'last_30', 'start_date' => null, 'end_date' => null, 'sort_by' => 'created_at_desc', 'correct_filter' => 'incorrect', 'filter' => [], ]; $this->page = 1; $this->loadMistakeData(); } public function toggleFilter(string $type, string $value): void { $current = $this->filters[$type] ?? []; if (in_array($value, $current)) { // 移除 $this->filters[$type] = array_values(array_diff($current, [$value])); } else { // 添加 $this->filters[$type][] = $value; } $this->applyFilters(); } public function clearCustomRange(): void { $this->filters['start_date'] = null; $this->filters['end_date'] = null; } #[Computed] public function teachers(): array { try { $query = Teacher::query() ->leftJoin('users as u', 'teachers.teacher_id', '=', 'u.user_id') ->select( 'teachers.teacher_id', 'teachers.name', 'teachers.subject', 'u.username', 'u.email' ); // 如果是老师,只返回自己 if ($this->isTeacher) { $teacherId = $this->getCurrentTeacherId(); if ($teacherId) { $query->where('teachers.teacher_id', $teacherId); } } $teachers = $query->orderBy('teachers.name')->get(); $teacherIds = $teachers->pluck('teacher_id')->toArray(); $missingTeacherIds = Student::query() ->distinct() ->whereNotIn('teacher_id', $teacherIds) ->pluck('teacher_id') ->toArray(); $teachersArray = $teachers->all(); if (!empty($missingTeacherIds)) { foreach ($missingTeacherIds as $missingId) { $teachersArray[] = (object) [ 'teacher_id' => $missingId, 'name' => '未知老师 (' . $missingId . ')', 'subject' => '未知', 'username' => null, 'email' => null ]; } usort($teachersArray, function($a, $b) { return strcmp($a->name, $b->name); }); } return $teachersArray; } catch (\Exception $e) { Log::error('加载老师列表失败', [ 'error' => $e->getMessage() ]); return []; } } #[Computed] public function students(): array { if (empty($this->teacherId)) { return []; } try { return Student::query() ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id') ->where('students.teacher_id', $this->teacherId) ->select( 'students.student_id', 'students.name', 'students.grade', 'students.class_name', 'u.username', 'u.email' ) ->orderBy('students.grade') ->orderBy('students.class_name') ->orderBy('students.name') ->get() ->all(); } catch (\Exception $e) { Log::error('加载学生列表失败', [ 'teacher_id' => $this->teacherId, 'error' => $e->getMessage() ]); return []; } } public function getStudents(): array { return Student::query() ->select(['student_id', 'name', 'grade', 'class_name']) ->orderBy('grade') ->orderBy('class_name') ->orderBy('name') ->get() ->toArray(); } #[On('teacherChanged')] public function onTeacherChanged(string $teacherId): void { $this->teacherId = $teacherId; $this->studentId = ''; $this->resetPageState(); } #[On('studentChanged')] public function onStudentChanged(?string $teacherId, ?string $studentId): void { $this->teacherId = (string) ($teacherId ?? ''); $this->studentId = (string) ($studentId ?? ''); if ($this->studentId) { $this->loadMistakeData(); } else { $this->resetPageState(); } } protected function loadFilterOptions(): void { try { $knowledgeService = app(KnowledgeServiceApi::class); $knowledge = $knowledgeService->listKnowledgePoints(150); $this->filterOptions['knowledge_points'] = $knowledge ->map(function ($item) { $code = $item['kp_code'] ?? $item['code'] ?? null; if (!$code) { return null; } return [ 'code' => $code, 'name' => $item['cn_name'] ?? $item['name'] ?? $code, ]; }) ->filter() ->take(200) ->values() ->toArray(); $skills = $knowledgeService->listSkills(null, 200); $this->filterOptions['skills'] = $skills ->map(function ($item) { return [ 'id' => $item['skill_id'] ?? $item['id'] ?? ($item['code'] ?? ''), 'name' => $item['name'] ?? $item['skill_name'] ?? ($item['code'] ?? ''), 'kp_code' => $item['kp_code'] ?? $item['knowledge_point_code'] ?? null, ]; }) ->filter(fn ($item) => filled($item['id'])) ->values() ->toArray(); } catch (\Throwable $e) { Log::error('Load filter options failed', [ 'error' => $e->getMessage(), ]); $this->filterOptions = ['knowledge_points' => [], 'skills' => []]; } } protected function updateMistakeField(string $mistakeId, string $field, $value): void { foreach ($this->mistakes as &$mistake) { if (($mistake['id'] ?? null) === $mistakeId) { $mistake[$field] = $value; break; } } } protected function findMistakeById(string $mistakeId): array { foreach ($this->mistakes as $mistake) { if (($mistake['id'] ?? null) === $mistakeId) { return $mistake; } } return []; } protected function resetPageState(): void { $this->mistakes = []; $this->patterns = []; $this->summary = []; $this->selectedMistakeIds = []; $this->recommendations = []; $this->relatedQuestions = []; $this->actionMessage = ''; $this->errorMessage = ''; } #[Computed(cache: true, key: 'kp-options')] public function knowledgePointOptions(): array { try { $service = app(KnowledgeGraphService::class); $kps = $service->listKnowledgePoints(1, 1000); $options = []; foreach ($kps['data'] ?? [] as $kp) { $code = $kp['kp_code'] ?? $kp['id']; $name = $kp['cn_name'] ?? $kp['name'] ?? $code; $options[$code] = $name; } return $options; } catch (\Throwable $e) { Log::error('Failed to load knowledge points: ' . $e->getMessage()); return []; } } protected function notify(string $message, string $type = 'success'): void { $this->actionMessage = $message; $this->actionMessageType = $type; } }