|
|
@@ -1,572 +1,553 @@
|
|
|
-<div class="min-h-screen bg-[#f5f7fb] p-6">
|
|
|
- <div class="max-w-7xl mx-auto space-y-6">
|
|
|
- <div class="rounded-2xl bg-gradient-to-r from-sky-50 via-white to-indigo-50 border border-slate-100 shadow-sm p-6">
|
|
|
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
- <div class="lg:col-span-1">
|
|
|
- <p class="text-xs uppercase tracking-[0.2em] text-slate-400">MistakeBook</p>
|
|
|
- <h1 class="text-3xl font-bold text-slate-900 mt-1">错题本 · 诊断与重练</h1>
|
|
|
- <p class="text-sm text-slate-500 mt-1">完全复用上传卷子/智能出卷的师生联动:先选老师,再选学生,再刷新数据。</p>
|
|
|
+<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="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>
|
|
|
+ 错题本
|
|
|
+ </h1>
|
|
|
+ <p class="mt-2 text-sm text-gray-600 ml-11">查看学生错题记录与AI分析,生成针对性练习</p>
|
|
|
</div>
|
|
|
- <div class="lg:col-span-2">
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
- {{-- 选择老师(老师登录时隐藏) --}}
|
|
|
- @if(!$this->isTeacher)
|
|
|
- <div class="form-control w-full">
|
|
|
- <label class="label">
|
|
|
- <span class="label-text font-medium">选择老师 <span class="text-error">*</span></span>
|
|
|
- </label>
|
|
|
- <select
|
|
|
- wire:model.live="teacherId"
|
|
|
- class="select select-bordered w-full select-lg"
|
|
|
- >
|
|
|
- <option value="">请选择老师...</option>
|
|
|
- @foreach($this->teachers as $teacher)
|
|
|
- <option value="{{ $teacher->teacher_id }}">
|
|
|
- {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
|
|
|
- </option>
|
|
|
- @endforeach
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- @endif
|
|
|
- <div class="form-control w-full">
|
|
|
- <label class="label">
|
|
|
- <span class="label-text font-medium">选择学生 <span class="text-error">*</span></span>
|
|
|
- </label>
|
|
|
- <select
|
|
|
- wire:model.live="studentId"
|
|
|
- class="select select-bordered w-full select-lg"
|
|
|
- @if(empty($teacherId)) disabled @endif
|
|
|
- >
|
|
|
- <option value="">
|
|
|
- @if(empty($teacherId))
|
|
|
- 请先选择老师
|
|
|
- @else
|
|
|
- 请选择学生...
|
|
|
- @endif
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {{-- 选择器区域 --}}
|
|
|
+ <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
|
+ <div class="flex flex-col gap-3 lg:flex-row lg:items-center">
|
|
|
+ @if(!$this->isTeacher)
|
|
|
+ <div class="form-control w-full lg:flex-1">
|
|
|
+ <label class="label"><span class="label-text font-medium">选择老师</span></label>
|
|
|
+ <select wire:model.live="teacherId" class="select select-bordered w-full h-11">
|
|
|
+ <option value="">请选择老师...</option>
|
|
|
+ @foreach($this->teachers as $teacher)
|
|
|
+ <option value="{{ $teacher->teacher_id }}">
|
|
|
+ {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
|
|
|
</option>
|
|
|
- @foreach($this->students as $student)
|
|
|
- <option value="{{ $student->student_id }}">
|
|
|
- {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
|
|
|
- </option>
|
|
|
- @endforeach
|
|
|
- </select>
|
|
|
- </div>
|
|
|
+ @endforeach
|
|
|
+ </select>
|
|
|
</div>
|
|
|
- <div class="mt-3 flex items-center justify-between">
|
|
|
- <div class="text-xs text-slate-500">按照上传卷子/智能出卷同款逻辑联动师生</div>
|
|
|
- <button
|
|
|
- wire:click="loadMistakeData"
|
|
|
- class="btn btn-primary btn-md"
|
|
|
- >
|
|
|
- <span wire:loading.remove>刷新</span>
|
|
|
- <span wire:loading class="loading loading-spinner loading-xs"></span>
|
|
|
+ @endif
|
|
|
+
|
|
|
+ <div class="form-control w-full lg:flex-1">
|
|
|
+ <label class="label"><span class="label-text font-medium">选择学生</span></label>
|
|
|
+ <select wire:model.live="studentId" class="select select-bordered w-full h-11" @if(empty($teacherId)) disabled @endif>
|
|
|
+ <option value="">{{ empty($teacherId) ? '请先选择老师' : '请选择学生...' }}</option>
|
|
|
+ @foreach($this->students as $student)
|
|
|
+ <option value="{{ $student->student_id }}">
|
|
|
+ {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
|
|
|
+ </option>
|
|
|
+ @endforeach
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="w-full lg:w-auto flex items-center lg:pt-6">
|
|
|
+ <button wire:click="loadMistakeData" wire:loading.attr="disabled"
|
|
|
+ class="inline-flex items-center justify-center w-full h-11 px-6 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200">
|
|
|
+ <svg wire:loading class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
- @if ($actionMessage)
|
|
|
- <div class="alert {{ $actionMessageType === 'danger' ? 'alert-error' : ($actionMessageType === 'warning' ? 'alert-warning' : 'alert-success') }} shadow-sm">
|
|
|
- <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
|
|
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
- </svg>
|
|
|
- <span>{{ $actionMessage }}</span>
|
|
|
+ {{-- 错误提示 --}}
|
|
|
+ @if ($errorMessage)
|
|
|
+ <div class="mb-8">
|
|
|
+ <div class="bg-red-50 border border-red-200 rounded-xl p-4">
|
|
|
+ <div class="flex items-start">
|
|
|
+ <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ <div class="ml-3">
|
|
|
+ <h3 class="text-sm font-medium text-red-800">加载错误</h3>
|
|
|
+ <p class="mt-1 text-sm text-red-700">{{ $errorMessage }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- @endif
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
|
|
|
- @if ($errorMessage)
|
|
|
- <div class="alert alert-error shadow-sm">
|
|
|
- <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
|
|
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
- </svg>
|
|
|
- <div>
|
|
|
- <h3 class="font-bold">加载失败</h3>
|
|
|
- <div class="text-xs">{{ $errorMessage }}</div>
|
|
|
- </div>
|
|
|
+ @if ($actionMessage)
|
|
|
+ <div class="mb-8">
|
|
|
+ <div class="bg-green-50 border border-green-200 rounded-xl p-4">
|
|
|
+ <p class="text-sm text-green-700">{{ $actionMessage }}</p>
|
|
|
</div>
|
|
|
- @endif
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
|
|
|
- <div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
|
|
|
- <div class="w-10 h-10 rounded-full bg-sky-100 text-sky-600 flex items-center justify-center font-semibold">总</div>
|
|
|
- <div>
|
|
|
- <p class="text-xs text-slate-500">总错题</p>
|
|
|
- <p class="text-3xl font-bold text-slate-900">{{ $summary['total'] ?? 0 }}</p>
|
|
|
+ {{-- 统计卡片 --}}
|
|
|
+ <div class="mb-8">
|
|
|
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
|
+ <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
|
|
+ <div class="p-6">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
|
|
|
+ <span class="text-white font-bold">总</span>
|
|
|
+ </div>
|
|
|
+ <div class="ml-4">
|
|
|
+ <p class="text-sm font-medium text-gray-600">总错题</p>
|
|
|
+ <p class="text-2xl font-bold text-gray-900 mt-1">{{ $summary['total'] ?? 0 }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
|
|
|
- <div class="w-10 h-10 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center font-semibold">7d</div>
|
|
|
- <div>
|
|
|
- <p class="text-xs text-slate-500">本周错题</p>
|
|
|
- <p class="text-3xl font-bold text-slate-900">{{ $summary['this_week'] ?? 0 }}</p>
|
|
|
+ <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
|
|
+ <div class="p-6">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
|
|
|
+ <span class="text-white font-bold">周</span>
|
|
|
+ </div>
|
|
|
+ <div class="ml-4">
|
|
|
+ <p class="text-sm font-medium text-gray-600">本周新增</p>
|
|
|
+ <p class="text-2xl font-bold text-gray-900 mt-1">{{ $summary['this_week'] ?? 0 }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
|
|
|
- <div class="w-10 h-10 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center font-semibold">待</div>
|
|
|
- <div>
|
|
|
- <p class="text-xs text-slate-500">待复习</p>
|
|
|
- <p class="text-3xl font-bold text-amber-600">{{ $summary['pending_review'] ?? 0 }}</p>
|
|
|
+ <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
|
|
+ <div class="p-6">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div class="w-12 h-12 bg-gradient-to-br from-yellow-500 to-orange-500 rounded-xl flex items-center justify-center shadow-lg">
|
|
|
+ <span class="text-white font-bold">待</span>
|
|
|
+ </div>
|
|
|
+ <div class="ml-4">
|
|
|
+ <p class="text-sm font-medium text-gray-600">待复习</p>
|
|
|
+ <p class="text-2xl font-bold text-orange-600 mt-1">{{ $summary['pending_review'] ?? 0 }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
|
|
|
- <div class="w-10 h-10 rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center font-semibold">AI</div>
|
|
|
- <div class="flex-1">
|
|
|
- <p class="text-xs text-slate-500">掌握率</p>
|
|
|
- @php $masteryRate = $summary['mastery_rate'] ?? null; @endphp
|
|
|
- <p class="text-3xl font-bold text-emerald-700">
|
|
|
- {{ $masteryRate !== null ? number_format($masteryRate * 100, 1) . '%' : '--' }}
|
|
|
- </p>
|
|
|
- <div class="mt-1 h-2 bg-slate-200 rounded-full overflow-hidden">
|
|
|
- <div class="h-2 bg-emerald-500" style="width: {{ $masteryRate ? $masteryRate * 100 : 0 }}%"></div>
|
|
|
+ <div class="bg-white overflow-hidden shadow-sm rounded-xl border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
|
|
+ <div class="p-6">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg">
|
|
|
+ <span class="text-white font-bold">%</span>
|
|
|
+ </div>
|
|
|
+ <div class="ml-4">
|
|
|
+ <p class="text-sm font-medium text-gray-600">掌握率</p>
|
|
|
+ @php $rate = $summary['mastery_rate'] ?? null; @endphp
|
|
|
+ <p class="text-2xl font-bold text-green-600 mt-1">{{ $rate !== null ? number_format($rate * 100, 1) . '%' : '--' }}</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
|
- <div class="lg:col-span-4 space-y-4">
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-4">
|
|
|
- <div class="flex items-center justify-between">
|
|
|
- <h3 class="font-semibold text-slate-900">多维筛选</h3>
|
|
|
- <button class="btn btn-primary btn-sm" wire:click="applyFilters">应用</button>
|
|
|
+ {{-- 加载状态 --}}
|
|
|
+ @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" fill="none" viewBox="0 0 24 24">
|
|
|
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
+ </svg>
|
|
|
+ <p class="mt-4 text-sm text-gray-600">正在加载数据,请稍候...</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @else
|
|
|
+ {{-- 主内容区域 --}}
|
|
|
+ <div class="grid grid-cols-1 gap-6 xl:grid-cols-4">
|
|
|
+ {{-- 左侧筛选 --}}
|
|
|
+ <div class="xl:col-span-1">
|
|
|
+ <div class="bg-white shadow-sm rounded-xl border border-gray-200 sticky top-4">
|
|
|
+ <div class="px-6 py-5 border-b border-gray-100">
|
|
|
+ <h3 class="text-lg font-semibold text-gray-900">筛选条件</h3>
|
|
|
</div>
|
|
|
- <div class="space-y-3">
|
|
|
- <div class="form-control">
|
|
|
- <label class="label pb-1">
|
|
|
- <span class="label-text font-medium text-slate-800">知识点</span>
|
|
|
- <span class="text-xs text-slate-400">多选</span>
|
|
|
- </label>
|
|
|
- <select
|
|
|
- multiple
|
|
|
- size="6"
|
|
|
- wire:model="filters.kp_ids"
|
|
|
- class="select select-bordered w-full bg-white"
|
|
|
- >
|
|
|
- @foreach($filterOptions['knowledge_points'] as $kp)
|
|
|
- <option value="{{ $kp['code'] }}">{{ $kp['name'] }} ({{ $kp['code'] }})</option>
|
|
|
- @endforeach
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="form-control">
|
|
|
- <label class="label pb-1">
|
|
|
- <span class="label-text font-medium text-slate-800">技能</span>
|
|
|
- <span class="text-xs text-slate-400">联动</span>
|
|
|
- </label>
|
|
|
- @php
|
|
|
- $selectedKps = $filters['kp_ids'] ?? [];
|
|
|
- $skillOptions = collect($filterOptions['skills'] ?? [])
|
|
|
- ->when(!empty($selectedKps), function($c) use ($selectedKps) {
|
|
|
- return $c->filter(fn($item) => empty($item['kp_code']) || in_array($item['kp_code'], $selectedKps));
|
|
|
- })
|
|
|
- ->values()
|
|
|
- ->all();
|
|
|
- @endphp
|
|
|
- <select
|
|
|
- multiple
|
|
|
- size="6"
|
|
|
- wire:model="filters.skill_ids"
|
|
|
- class="select select-bordered w-full bg-white"
|
|
|
- >
|
|
|
- @foreach($skillOptions as $skill)
|
|
|
- <option value="{{ $skill['id'] }}">
|
|
|
- {{ $skill['name'] ?? $skill['id'] }}
|
|
|
- @if(!empty($skill['kp_code']))
|
|
|
- · {{ $skill['kp_code'] }}
|
|
|
- @endif
|
|
|
- </option>
|
|
|
- @endforeach
|
|
|
- </select>
|
|
|
- </div>
|
|
|
+ <div class="p-6 space-y-6">
|
|
|
+ {{-- 时间范围 --}}
|
|
|
<div>
|
|
|
- <p class="label-text font-medium mb-2">错误类型</p>
|
|
|
- <div class="grid grid-cols-2 gap-2">
|
|
|
- @foreach(['计算错误', '概念错误', '方法错误', '审题错误', '表达错误'] as $type)
|
|
|
- <label class="flex items-center gap-2 rounded-lg px-2 py-1 bg-slate-50 border border-slate-200">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- class="checkbox checkbox-sm checkbox-primary"
|
|
|
- value="{{ $type }}"
|
|
|
- wire:model="filters.error_types"
|
|
|
- >
|
|
|
- <span class="text-sm text-slate-700">{{ $type }}</span>
|
|
|
- </label>
|
|
|
- @endforeach
|
|
|
+ <label class="text-sm font-medium text-gray-700 mb-3 block">时间范围</label>
|
|
|
+ <div class="flex gap-2">
|
|
|
+ <button wire:click="$set('filters.time_range', 'last_7')"
|
|
|
+ class="flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-colors {{ $filters['time_range'] === 'last_7' ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
|
|
+ 7天
|
|
|
+ </button>
|
|
|
+ <button wire:click="$set('filters.time_range', 'last_30')"
|
|
|
+ class="flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-colors {{ $filters['time_range'] === 'last_30' ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
|
|
+ 30天
|
|
|
+ </button>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ {{-- 错误类型 --}}
|
|
|
<div>
|
|
|
- <p class="label-text font-medium mb-2">时间范围</p>
|
|
|
- <div class="grid grid-cols-3 gap-2">
|
|
|
- <button class="btn btn-sm {{ $filters['time_range'] === 'last_7' ? 'btn-primary' : 'btn-outline' }}" wire:click="$set('filters.time_range', 'last_7')">7天</button>
|
|
|
- <button class="btn btn-sm {{ $filters['time_range'] === 'last_30' ? 'btn-primary' : 'btn-outline' }}" wire:click="$set('filters.time_range', 'last_30')">30天</button>
|
|
|
- <button class="btn btn-sm {{ $filters['time_range'] === 'custom' ? 'btn-primary' : 'btn-outline' }}" wire:click="$set('filters.time_range', 'custom')">自定义</button>
|
|
|
+ <label class="text-sm font-medium text-gray-700 mb-3 block">错误类型</label>
|
|
|
+ <div class="space-y-2">
|
|
|
+ @foreach(['计算错误', '概念错误', '方法错误', '审题错误'] as $type)
|
|
|
+ <label class="flex items-center gap-3 cursor-pointer p-2 rounded-lg hover:bg-gray-50">
|
|
|
+ <input type="checkbox" value="{{ $type }}" wire:model="filters.error_types"
|
|
|
+ class="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
|
|
+ <span class="text-sm text-gray-700">{{ $type }}</span>
|
|
|
+ </label>
|
|
|
+ @endforeach
|
|
|
</div>
|
|
|
- @if($filters['time_range'] === 'custom')
|
|
|
- <div class="mt-3 space-y-2">
|
|
|
- <input type="date" class="input input-bordered w-full" wire:model="filters.start_date">
|
|
|
- <input type="date" class="input input-bordered w-full" wire:model="filters.end_date">
|
|
|
- <button class="btn btn-ghost btn-xs" wire:click="clearCustomRange">清空</button>
|
|
|
- </div>
|
|
|
- @endif
|
|
|
</div>
|
|
|
- <button
|
|
|
- wire:click="applyFilters"
|
|
|
- class="btn btn-primary btn-md w-full"
|
|
|
- >
|
|
|
- <span wire:loading.remove>应用筛选</span>
|
|
|
- <span wire:loading class="loading loading-spinner"></span>
|
|
|
+
|
|
|
+ <button wire:click="applyFilters"
|
|
|
+ class="w-full px-4 py-2.5 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition-colors">
|
|
|
+ 应用筛选
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-3">
|
|
|
- <div class="flex items-center justify-between">
|
|
|
- <h3 class="font-semibold text-slate-900">推荐补救路径</h3>
|
|
|
- <button class="btn btn-ghost btn-sm text-indigo-600" wire:click="refreshPatterns">刷新</button>
|
|
|
- </div>
|
|
|
- @if(!empty($patterns['recommend_path']))
|
|
|
- <ul class="timeline timeline-vertical">
|
|
|
- @foreach($patterns['recommend_path'] as $step)
|
|
|
- <li>
|
|
|
- <div class="timeline-middle">
|
|
|
- <div class="w-2.5 h-2.5 rounded-full bg-indigo-500"></div>
|
|
|
- </div>
|
|
|
- <div class="timeline-end timeline-box bg-white border border-indigo-100 text-sm text-slate-700">
|
|
|
- {{ $step['title'] ?? ($step['kp'] ?? '学习步骤') }}
|
|
|
- @if(!empty($step['description']))
|
|
|
- <p class="text-xs text-slate-500 mt-1">{{ $step['description'] }}</p>
|
|
|
- @endif
|
|
|
- </div>
|
|
|
- <hr class="bg-indigo-200"/>
|
|
|
- </li>
|
|
|
- @endforeach
|
|
|
- </ul>
|
|
|
- @else
|
|
|
- <p class="text-sm text-slate-500">暂无推荐路径,稍后重试</p>
|
|
|
- @endif
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="lg:col-span-8 space-y-5">
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5">
|
|
|
- <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
|
- <div>
|
|
|
- <h3 class="text-lg font-semibold text-slate-900">错题列表</h3>
|
|
|
- <p class="text-sm text-slate-500">题干、作答、AI 解析与操作</p>
|
|
|
- </div>
|
|
|
- <div class="flex flex-wrap gap-2 items-center">
|
|
|
- <button class="btn btn-md btn-secondary" wire:click="generatePracticeFromSelection">
|
|
|
- 📚 基于错题生成练习
|
|
|
- </button>
|
|
|
- <div class="badge badge-outline">
|
|
|
- 已选 {{ count($selectedMistakeIds) }} / {{ count($mistakes) }}
|
|
|
- </div>
|
|
|
+ {{-- 右侧错题列表 --}}
|
|
|
+ <div class="xl:col-span-3 space-y-6">
|
|
|
+ {{-- 错题列表 --}}
|
|
|
+ <div class="bg-white shadow-sm rounded-xl border border-gray-200">
|
|
|
+ <div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
|
|
|
+ <h3 class="text-lg font-semibold text-gray-900 flex items-center">
|
|
|
+ <svg class="w-5 h-5 mr-2 text-red-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>
|
|
|
+ 错题列表 ({{ $total }})
|
|
|
+ </h3>
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
+ @if(!empty($selectedMistakeIds))
|
|
|
+ <button wire:click="generatePracticeFromSelection"
|
|
|
+ class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition-colors">
|
|
|
+ 生成练习 ({{ count($selectedMistakeIds) }})
|
|
|
+ </button>
|
|
|
+ @endif
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- @if ($isLoading)
|
|
|
- <div class="flex items-center justify-center py-10 text-slate-500">
|
|
|
- <span class="loading loading-spinner loading-lg mr-3"></span>
|
|
|
- 正在加载错题...
|
|
|
- </div>
|
|
|
- @elseif(empty($mistakes))
|
|
|
- <div class="text-center py-12 text-slate-500">
|
|
|
- <svg class="mx-auto h-12 w-12 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6l4 2m6-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
- </svg>
|
|
|
- <p class="mt-3">暂无错题,先选择老师和学生</p>
|
|
|
- </div>
|
|
|
- @else
|
|
|
- <div class="space-y-4 mt-4">
|
|
|
- @foreach($mistakes as $mistake)
|
|
|
- <div class="rounded-xl border border-slate-200 bg-white shadow-sm p-4 space-y-4" wire:key="mistake-{{ $mistake['id'] ?? $loop->index }}">
|
|
|
- <div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
|
- <div class="flex items-center gap-3">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- class="checkbox checkbox-primary"
|
|
|
+ <div class="p-6">
|
|
|
+ @if(empty($mistakes))
|
|
|
+ <div class="text-center py-12">
|
|
|
+ <svg class="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
|
+ </svg>
|
|
|
+ <p class="text-sm font-medium text-gray-900 mb-1">暂无错题数据</p>
|
|
|
+ <p class="text-xs text-gray-500">请先选择学生后刷新数据</p>
|
|
|
+ </div>
|
|
|
+ @else
|
|
|
+ <div class="space-y-4">
|
|
|
+ @foreach($mistakes as $mistake)
|
|
|
+ <div class="border border-gray-200 rounded-xl p-5 hover:border-gray-300 transition-colors" wire:key="m-{{ $mistake['id'] ?? $loop->index }}">
|
|
|
+ {{-- 头部 --}}
|
|
|
+ <div class="flex items-start gap-4">
|
|
|
+ <input type="checkbox"
|
|
|
wire:click="toggleSelection('{{ $mistake['id'] ?? '' }}')"
|
|
|
@checked(in_array($mistake['id'] ?? '', $selectedMistakeIds, true))
|
|
|
- >
|
|
|
- <div>
|
|
|
- <p class="text-xs text-slate-400">{{ $mistake['id'] ?? 'ID' }}</p>
|
|
|
- <p class="text-sm text-slate-500">
|
|
|
- {{ $mistake['created_at'] ?? '' }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="flex flex-wrap gap-2">
|
|
|
- @foreach(($mistake['kp_ids'] ?? []) as $kp)
|
|
|
- <span class="badge badge-ghost">KP {{ $kp }}</span>
|
|
|
- @endforeach
|
|
|
- @foreach(($mistake['skill_ids'] ?? []) as $skill)
|
|
|
- <span class="badge badge-outline badge-info">{{ $skill }}</span>
|
|
|
- @endforeach
|
|
|
- @if(!empty($mistake['error_type']))
|
|
|
- <span class="badge badge-warning badge-outline">{{ $mistake['error_type'] }}</span>
|
|
|
- @endif
|
|
|
- @if(isset($mistake['correct']))
|
|
|
- <span class="badge {{ $mistake['correct'] ? 'badge-success' : 'badge-error' }}">
|
|
|
- {{ $mistake['correct'] ? '已掌握' : '错误' }}
|
|
|
- </span>
|
|
|
- @endif
|
|
|
- @if(!empty($mistake['reviewed']))
|
|
|
- <span class="badge badge-success badge-outline">已复习</span>
|
|
|
- @endif
|
|
|
- @if(!empty($mistake['favorite']))
|
|
|
- <span class="badge badge-primary badge-outline">已收藏</span>
|
|
|
- @endif
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ class="mt-1 w-5 h-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
|
|
|
|
|
- <div class="rounded-lg bg-slate-50 p-4 border border-slate-100">
|
|
|
- <p class="text-sm font-semibold text-slate-800 mb-2">题干</p>
|
|
|
- <div class="prose max-w-none question-content text-slate-800">
|
|
|
- <x-math-render :content="$mistake['question']['stem'] ?? ($mistake['question']['content'] ?? '暂无题干')" class="text-base" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div class="flex-1 min-w-0">
|
|
|
+ {{-- 标签行 --}}
|
|
|
+ <div class="flex flex-wrap items-center gap-2 mb-4">
|
|
|
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
|
+ #{{ $mistake['id'] ?? '' }}
|
|
|
+ </span>
|
|
|
+ @if(!empty($mistake['question']['question_number']))
|
|
|
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
|
+ 第{{ $mistake['question']['question_number'] }}题
|
|
|
+ </span>
|
|
|
+ @endif
|
|
|
+ <span class="text-xs text-gray-500">
|
|
|
+ @php
|
|
|
+ $createdAt = $mistake['created_at'] ?? null;
|
|
|
+ if ($createdAt) {
|
|
|
+ try {
|
|
|
+ $date = \Carbon\Carbon::parse($createdAt);
|
|
|
+ echo $date->format('Y-m-d H:i');
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ echo $createdAt;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ @endphp
|
|
|
+ </span>
|
|
|
+ @if(!empty($mistake['error_type']))
|
|
|
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
|
+ {{ $mistake['error_type'] }}
|
|
|
+ </span>
|
|
|
+ @endif
|
|
|
+ @if(!empty($mistake['mistake_category']))
|
|
|
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
|
|
+ {{ $mistake['mistake_category'] }}
|
|
|
+ </span>
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
|
- <div class="bg-white border border-slate-200 rounded-lg p-3">
|
|
|
- <p class="text-xs text-slate-500 mb-1">学生作答</p>
|
|
|
- <p class="text-sm text-slate-800 break-words">{{ $mistake['student_answer'] ?? '无' }}</p>
|
|
|
- </div>
|
|
|
- <div class="bg-white border border-slate-200 rounded-lg p-3">
|
|
|
- <p class="text-xs text-slate-500 mb-1">正确答案</p>
|
|
|
- <p class="text-sm text-emerald-700 break-words">
|
|
|
- {{ $mistake['question']['answer'] ?? ($mistake['correct_answer'] ?? '未知') }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <div class="bg-white border border-slate-200 rounded-lg p-3">
|
|
|
- <p class="text-xs text-slate-500 mb-1">错误类型</p>
|
|
|
- <p class="text-sm text-amber-700">
|
|
|
- {{ $mistake['error_type'] ?? '未分类' }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
|
- <div class="bg-gradient-to-br from-amber-50 to-white border border-amber-100 rounded-lg p-4 space-y-2">
|
|
|
- <p class="text-sm font-semibold text-amber-800">错误原因分析</p>
|
|
|
- <p class="text-sm text-amber-700">{{ $mistake['ai_analysis']['reason'] ?? $mistake['ai_analysis'] ?? '暂无分析' }}</p>
|
|
|
- <p class="text-sm font-semibold text-amber-800">对应技能</p>
|
|
|
- <p class="text-sm text-amber-700">{{ $mistake['ai_analysis']['skill'] ?? ($mistake['skill_desc'] ?? '未识别') }}</p>
|
|
|
- </div>
|
|
|
- <div class="bg-gradient-to-br from-emerald-50 to-white border border-emerald-100 rounded-lg p-4 space-y-2">
|
|
|
- <p class="text-sm font-semibold text-emerald-800">正确解法</p>
|
|
|
- <p class="text-sm text-emerald-700">{{ $mistake['ai_analysis']['solution'] ?? '可向AI请求解析' }}</p>
|
|
|
- <p class="text-sm font-semibold text-emerald-800">避免类似错误</p>
|
|
|
- <p class="text-sm text-emerald-700">{{ $mistake['ai_analysis']['tip'] ?? ($mistake['ai_analysis']['suggestion'] ?? '加强审题与演算步骤复查') }}</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ {{-- 题干 --}}
|
|
|
+ @if(!empty($mistake['question']['stem']))
|
|
|
+ <div class="bg-gray-50 rounded-lg p-4 mb-4 border border-gray-100">
|
|
|
+ <p class="text-xs font-medium text-gray-500 mb-2">题干</p>
|
|
|
+ <div class="prose prose-sm max-w-none text-gray-900">
|
|
|
+ <x-math-render :content="$mistake['question']['stem']" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
|
|
|
- <div class="divider my-2"></div>
|
|
|
- <div class="flex flex-wrap gap-2">
|
|
|
- <button class="btn btn-sm btn-ghost" wire:click="toggleFavorite('{{ $mistake['id'] ?? '' }}')">
|
|
|
- {{ !empty($mistake['favorite']) ? '取消收藏' : '收藏' }}
|
|
|
- </button>
|
|
|
- <button class="btn btn-sm btn-ghost" wire:click="markReviewed('{{ $mistake['id'] ?? '' }}')">
|
|
|
- 标记已复习
|
|
|
- </button>
|
|
|
- <button class="btn btn-sm btn-ghost" wire:click="addToRetryList('{{ $mistake['id'] ?? '' }}')">
|
|
|
- 加入重练清单
|
|
|
- </button>
|
|
|
- <button class="btn btn-sm btn-outline" wire:click="loadRelatedQuestions('{{ $mistake['id'] ?? '' }}')">
|
|
|
- 查看关联题
|
|
|
- </button>
|
|
|
- </div>
|
|
|
+ {{-- 判卷结果 + 知识点 --}}
|
|
|
+ <div class="flex flex-wrap items-center gap-3 mb-4">
|
|
|
+ @if(!empty($mistake['mark_detected']))
|
|
|
+ <span class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium {{ $mistake['mark_detected'] === '✓' || $mistake['mark_detected'] === '√' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
|
|
+ 老师判卷: {{ $mistake['mark_detected'] }}
|
|
|
+ </span>
|
|
|
+ @endif
|
|
|
+ @if(isset($mistake['score']) && isset($mistake['full_score']))
|
|
|
+ @php
|
|
|
+ $score = floatval($mistake['score'] ?? 0);
|
|
|
+ $fullScore = floatval($mistake['full_score'] ?? 0);
|
|
|
+ $isFullScore = $fullScore > 0 && $score >= $fullScore;
|
|
|
+ @endphp
|
|
|
+ <span class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium {{ $isFullScore ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800' }}">
|
|
|
+ 得分: {{ $mistake['score'] }}/{{ $mistake['full_score'] }}
|
|
|
+ @if($isFullScore)
|
|
|
+ ✓
|
|
|
+ @endif
|
|
|
+ </span>
|
|
|
+ @endif
|
|
|
+ @if(!empty($mistake['question']['kp_code']))
|
|
|
+ <a href="{{ url('/admin/knowledge-point-detail') }}?kp_code={{ urlencode($mistake['question']['kp_code']) }}"
|
|
|
+ class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-100 text-indigo-800 hover:bg-indigo-200 transition-colors">
|
|
|
+ 知识点: {{ $mistake['question']['kp_code'] }}
|
|
|
+ </a>
|
|
|
+ @endif
|
|
|
+ @if(!empty($mistake['skill_ids']))
|
|
|
+ @foreach($mistake['skill_ids'] as $skillId)
|
|
|
+ <span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-teal-100 text-teal-800">
|
|
|
+ {{ $skillId }}
|
|
|
+ </span>
|
|
|
+ @endforeach
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
|
|
|
- @if(!empty($relatedQuestions[$mistake['id'] ?? ''] ?? []))
|
|
|
- <div class="bg-slate-50 border border-slate-200 rounded-lg p-3 space-y-2">
|
|
|
- <p class="text-sm font-semibold text-slate-800">关联题目</p>
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
|
- @foreach($relatedQuestions[$mistake['id']] as $related)
|
|
|
- <div class="p-3 bg-white border border-slate-200 rounded-lg">
|
|
|
- <p class="text-xs text-slate-400 mb-1">ID: {{ $related['id'] ?? '' }}</p>
|
|
|
- <p class="text-sm text-slate-800 overflow-hidden max-h-14">{{ $related['stem'] ?? $related['content'] ?? '相关题目' }}</p>
|
|
|
- <p class="text-xs text-slate-500 mt-2">
|
|
|
- 难度: {{ $related['difficulty'] ?? '中等' }}
|
|
|
- @if(!empty($related['kp_codes']))
|
|
|
- · KP: {{ is_array($related['kp_codes']) ? implode(',', $related['kp_codes']) : $related['kp_codes'] }}
|
|
|
+ {{-- 作答对比与解题步骤 --}}
|
|
|
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-4">
|
|
|
+ @if(!empty($mistake['student_answer']) && trim($mistake['student_answer']) !== '' && trim($mistake['student_answer']) !== '未作答')
|
|
|
+ <div class="bg-red-50 rounded-lg p-4 border border-red-100">
|
|
|
+ <p class="text-xs font-medium text-red-600 mb-2">
|
|
|
+ 学生作答
|
|
|
+ @if(!empty($mistake['answer_confidence']))
|
|
|
+ <span class="text-gray-400">(置信度: {{ number_format($mistake['answer_confidence'] * 100, 1) }}%)</span>
|
|
|
+ @endif
|
|
|
+ </p>
|
|
|
+ <p class="text-sm text-gray-900">{{ $mistake['student_answer'] }}</p>
|
|
|
+ </div>
|
|
|
+ @elseif(!empty($mistake['mark_detected']) && trim($mistake['student_answer']) === '未作答')
|
|
|
+ {{-- 老师已评分但学生未作答的题目,不显示学生作答区域 --}}
|
|
|
+ @elseif(!empty($mistake['student_answer']) && trim($mistake['student_answer']) !== '')
|
|
|
+ <div class="bg-red-50 rounded-lg p-4 border border-red-100">
|
|
|
+ <p class="text-xs font-medium text-red-600 mb-2">
|
|
|
+ 学生作答
|
|
|
+ @if(!empty($mistake['answer_confidence']))
|
|
|
+ <span class="text-gray-400">(置信度: {{ number_format($mistake['answer_confidence'] * 100, 1) }}%)</span>
|
|
|
@endif
|
|
|
</p>
|
|
|
+ <p class="text-sm text-gray-900">{{ $mistake['student_answer'] }}</p>
|
|
|
</div>
|
|
|
- @endforeach
|
|
|
+ @endif
|
|
|
+
|
|
|
+ @if(!empty($mistake['question']['solution']))
|
|
|
+ <div class="bg-blue-50 rounded-lg p-4 border border-blue-100">
|
|
|
+ <p class="text-xs font-medium text-blue-600 mb-2">解题步骤</p>
|
|
|
+ <div class="prose prose-sm max-w-none text-gray-900">
|
|
|
+ {{ $mistake['question']['solution'] }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+
|
|
|
+ @if(!empty($mistake['question']['answer']))
|
|
|
+ <div class="bg-green-50 rounded-lg p-4 border border-green-100">
|
|
|
+ <p class="text-xs font-medium text-green-600 mb-2">正确答案</p>
|
|
|
+ <p class="text-sm text-gray-900">{{ $mistake['question']['answer'] }}</p>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {{-- AI分析 --}}
|
|
|
+ @if(!empty($mistake['ai_analysis']['reason']) || !empty($mistake['ai_analysis']['solution']) || !empty($mistake['ai_analysis']['suggestions']))
|
|
|
+ <details class="bg-amber-50 rounded-lg border border-amber-100 mb-4">
|
|
|
+ <summary class="px-4 py-3 text-sm font-medium text-amber-800 cursor-pointer hover:bg-amber-100 rounded-lg transition-colors">
|
|
|
+ 查看AI分析
|
|
|
+ @if(!empty($mistake['ai_analysis']['model_used']))
|
|
|
+ <span class="text-xs text-gray-500 ml-2">({{ $mistake['ai_analysis']['model_used'] }})</span>
|
|
|
+ @endif
|
|
|
+ </summary>
|
|
|
+ <div class="px-4 pb-4 text-sm text-gray-700 space-y-2">
|
|
|
+ @if(!empty($mistake['ai_analysis']['reason']))
|
|
|
+ <p><span class="font-medium text-gray-900">错因:</span>{{ $mistake['ai_analysis']['reason'] }}</p>
|
|
|
+ @endif
|
|
|
+ @if(!empty($mistake['ai_analysis']['solution']))
|
|
|
+ <p><span class="font-medium text-gray-900">解法:</span>{{ $mistake['ai_analysis']['solution'] }}</p>
|
|
|
+ @endif
|
|
|
+ @if(!empty($mistake['ai_analysis']['suggestions']))
|
|
|
+ <p><span class="font-medium text-gray-900">建议:</span>{{ $mistake['ai_analysis']['suggestions'] }}</p>
|
|
|
+ @endif
|
|
|
+ @if(!empty($mistake['ai_analysis']['next_steps']))
|
|
|
+ <div>
|
|
|
+ <span class="font-medium text-gray-900">下一步:</span>
|
|
|
+ <ul class="list-disc list-inside mt-1">
|
|
|
+ @foreach($mistake['ai_analysis']['next_steps'] as $step)
|
|
|
+ <li>{{ is_array($step) ? json_encode($step) : $step }}</li>
|
|
|
+ @endforeach
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
+ </details>
|
|
|
+ @endif
|
|
|
+
|
|
|
+ {{-- 操作按钮 --}}
|
|
|
+ <div class="flex flex-wrap gap-2 pt-4 border-t border-gray-100">
|
|
|
+ <button wire:click="toggleFavorite('{{ $mistake['id'] ?? '' }}')"
|
|
|
+ class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg {{ !empty($mistake['favorite']) ? 'bg-yellow-100 text-yellow-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }} transition-colors">
|
|
|
+ {{ !empty($mistake['favorite']) ? '★ 已收藏' : '☆ 收藏' }}
|
|
|
+ </button>
|
|
|
+ <button wire:click="markReviewed('{{ $mistake['id'] ?? '' }}')"
|
|
|
+ class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg {{ !empty($mistake['reviewed']) ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }} transition-colors">
|
|
|
+ {{ !empty($mistake['reviewed']) ? '✓ 已复习' : '标记复习' }}
|
|
|
+ </button>
|
|
|
+ <button wire:click="loadRelatedQuestions('{{ $mistake['id'] ?? '' }}')"
|
|
|
+ class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">
|
|
|
+ 关联题
|
|
|
+ </button>
|
|
|
+ @if(!empty($mistake['answer_area_crop_path']))
|
|
|
+ <a href="{{ $mistake['answer_area_crop_path'] }}" target="_blank"
|
|
|
+ class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">
|
|
|
+ 查看作答图片
|
|
|
+ </a>
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {{-- 关联题 --}}
|
|
|
+ @if(!empty($relatedQuestions[$mistake['id'] ?? ''] ?? []))
|
|
|
+ <div class="mt-4 bg-gray-50 rounded-lg p-4 border border-gray-100">
|
|
|
+ <p class="text-xs font-medium text-gray-500 mb-3">关联题目</p>
|
|
|
+ <div class="space-y-2">
|
|
|
+ @foreach($relatedQuestions[$mistake['id']] as $related)
|
|
|
+ <div class="text-sm p-3 bg-white rounded-lg border border-gray-200">
|
|
|
+ <p class="text-gray-900 line-clamp-2">{{ $related['stem'] ?? '题目' }}</p>
|
|
|
+ </div>
|
|
|
+ @endforeach
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
</div>
|
|
|
</div>
|
|
|
- @endif
|
|
|
+ </div>
|
|
|
+ @endforeach
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {{-- 分页 --}}
|
|
|
+ @php
|
|
|
+ $maxPage = (int) ceil($total / $perPage);
|
|
|
+ @endphp
|
|
|
+ @if($maxPage > 1)
|
|
|
+ <div class="mt-6 flex items-center justify-between border-t border-gray-100 pt-4">
|
|
|
+ <div class="text-sm text-gray-500">
|
|
|
+ 共 {{ $total }} 条,第 {{ $page }}/{{ $maxPage }} 页
|
|
|
</div>
|
|
|
- @endforeach
|
|
|
- </div>
|
|
|
- @endif
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <button wire:click="prevPage" @disabled($page <= 1)
|
|
|
+ class="px-3 py-1.5 text-sm font-medium rounded-lg {{ $page <= 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }} transition-colors">
|
|
|
+ 上一页
|
|
|
+ </button>
|
|
|
+ @for($i = max(1, $page - 2); $i <= min($maxPage, $page + 2); $i++)
|
|
|
+ <button wire:click="gotoPage({{ $i }})"
|
|
|
+ class="px-3 py-1.5 text-sm font-medium rounded-lg {{ $i === $page ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }} transition-colors">
|
|
|
+ {{ $i }}
|
|
|
+ </button>
|
|
|
+ @endfor
|
|
|
+ <button wire:click="nextPage" @disabled($page >= $maxPage)
|
|
|
+ class="px-3 py-1.5 text-sm font-medium rounded-lg {{ $page >= $maxPage ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }} transition-colors">
|
|
|
+ 下一页
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
+ {{-- 推荐练习题 --}}
|
|
|
@if(!empty($recommendations))
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-4">
|
|
|
- <div class="flex items-center justify-between">
|
|
|
- <div>
|
|
|
- <h3 class="text-lg font-semibold text-slate-900">重练题单</h3>
|
|
|
- <p class="text-sm text-slate-500">基于错题推荐的新题,支持导出</p>
|
|
|
- </div>
|
|
|
- <a href="{{ url('/admin/question-management') }}" class="btn btn-outline btn-sm" target="_blank">打开题库</a>
|
|
|
+ <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-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
|
+ </svg>
|
|
|
+ 推荐练习题 ({{ count($recommendations) }})
|
|
|
+ </h3>
|
|
|
</div>
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
|
- @foreach($recommendations as $rec)
|
|
|
- <div class="p-4 border border-slate-200 rounded-lg bg-slate-50">
|
|
|
- <p class="text-xs text-slate-400 mb-1">题目 ID: {{ $rec['id'] ?? '' }}</p>
|
|
|
- <p class="text-sm text-slate-800 overflow-hidden max-h-16">{{ $rec['stem'] ?? $rec['content'] ?? '推荐题目' }}</p>
|
|
|
- <div class="flex flex-wrap gap-2 mt-2">
|
|
|
+ <div class="p-6">
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
+ @foreach($recommendations as $rec)
|
|
|
+ <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
|
|
|
+ <p class="text-sm text-gray-900 line-clamp-2">{{ $rec['stem'] ?? '推荐题目' }}</p>
|
|
|
@if(!empty($rec['kp_codes']))
|
|
|
- <span class="badge badge-ghost">KP {{ is_array($rec['kp_codes']) ? implode(',', $rec['kp_codes']) : $rec['kp_codes'] }}</span>
|
|
|
- @endif
|
|
|
- @if(!empty($rec['skills']))
|
|
|
- <span class="badge badge-outline badge-info">{{ is_array($rec['skills']) ? implode(',', $rec['skills']) : $rec['skills'] }}</span>
|
|
|
+ <span class="inline-flex items-center mt-2 px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-800">
|
|
|
+ {{ is_array($rec['kp_codes']) ? implode(',', $rec['kp_codes']) : $rec['kp_codes'] }}
|
|
|
+ </span>
|
|
|
@endif
|
|
|
</div>
|
|
|
- </div>
|
|
|
- @endforeach
|
|
|
+ @endforeach
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@endif
|
|
|
|
|
|
- <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-6">
|
|
|
- <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
|
- <div>
|
|
|
- <h3 class="text-lg font-semibold text-slate-900">智能分析</h3>
|
|
|
- <p class="text-sm text-slate-500">错误类型雷达图 · 弱点技能排名 · 薄弱知识点</p>
|
|
|
+ {{-- 错误分析 --}}
|
|
|
+ @if(!empty($patterns['error_types']) || !empty($patterns['top_kps']))
|
|
|
+ <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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
|
|
+ </svg>
|
|
|
+ 错误分析
|
|
|
+ </h3>
|
|
|
</div>
|
|
|
- <button class="btn btn-ghost btn-sm" wire:click="refreshPatterns">
|
|
|
- 重新拉取
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
- @if(!empty($patterns['error_types']))
|
|
|
- <div class="bg-slate-50 rounded-xl border border-slate-200 p-4">
|
|
|
- <div class="flex items-center justify-between mb-3">
|
|
|
- <h4 class="font-semibold text-slate-800">错误类型雷达图</h4>
|
|
|
- <span class="badge badge-ghost">AI</span>
|
|
|
- </div>
|
|
|
- <canvas id="mistakeRadarChart" class="w-full h-64"></canvas>
|
|
|
- </div>
|
|
|
- @endif
|
|
|
- @if(!empty($patterns['top_skills']))
|
|
|
- <div class="bg-slate-50 rounded-xl border border-slate-200 p-4">
|
|
|
- <div class="flex items-center justify-between mb-3">
|
|
|
- <h4 class="font-semibold text-slate-800">弱点技能排名</h4>
|
|
|
- <span class="badge badge-warning badge-outline">Top</span>
|
|
|
- </div>
|
|
|
- <canvas id="skillBarChart" class="w-full h-64"></canvas>
|
|
|
- </div>
|
|
|
- @endif
|
|
|
- </div>
|
|
|
-
|
|
|
- @if(!empty($patterns['top_kps']))
|
|
|
- <div class="bg-slate-50 rounded-xl border border-slate-200 p-4">
|
|
|
- <h4 class="font-semibold text-slate-800 mb-3">薄弱知识点热力</h4>
|
|
|
- <div class="space-y-2">
|
|
|
- @foreach(($patterns['top_kps'] ?? []) as $kp)
|
|
|
+ <div class="p-6">
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
+ @if(!empty($patterns['error_types']))
|
|
|
<div>
|
|
|
- <div class="flex items-center justify-between text-sm text-slate-700">
|
|
|
- <span>{{ $kp['name'] ?? ($kp['kp'] ?? $kp['kp_code'] ?? '知识点') }}</span>
|
|
|
- <span class="text-xs text-slate-500">错误 {{ $kp['count'] ?? $kp['mistake_count'] ?? 0 }}</span>
|
|
|
+ <p class="text-sm font-medium text-gray-900 mb-4">错误类型分布</p>
|
|
|
+ <div class="space-y-3">
|
|
|
+ @foreach($patterns['error_types'] as $et)
|
|
|
+ <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
|
+ <span class="text-sm text-gray-700">{{ $et['type'] ?? '未知' }}</span>
|
|
|
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-800">
|
|
|
+ {{ $et['count'] ?? 0 }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ @endforeach
|
|
|
</div>
|
|
|
- @php
|
|
|
- $score = ($kp['score'] ?? $kp['accuracy'] ?? 0);
|
|
|
- if ($score > 1) $score = $score / 100;
|
|
|
- $width = max(10, min(100, (1 - (float) $score) * 100));
|
|
|
- @endphp
|
|
|
- <progress class="progress progress-error w-full" value="{{ $width }}" max="100"></progress>
|
|
|
</div>
|
|
|
- @endforeach
|
|
|
+ @endif
|
|
|
+ @if(!empty($patterns['top_kps']))
|
|
|
+ <div>
|
|
|
+ <p class="text-sm font-medium text-gray-900 mb-4">薄弱知识点</p>
|
|
|
+ <div class="space-y-3">
|
|
|
+ @foreach($patterns['top_kps'] as $kp)
|
|
|
+ <div class="flex items-center justify-between p-3 bg-red-50 rounded-lg">
|
|
|
+ <span class="text-sm text-gray-700">{{ $kp['name'] ?? $kp['kp_code'] ?? '知识点' }}</span>
|
|
|
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
|
+ {{ $kp['mistake_count'] ?? 0 }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ @endforeach
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
</div>
|
|
|
</div>
|
|
|
- @endif
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ @endif
|
|
|
</div>
|
|
|
-
|
|
|
-@push('scripts')
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
|
- <script>
|
|
|
- document.addEventListener('livewire:navigated', initMistakeCharts);
|
|
|
- document.addEventListener('livewire:load', initMistakeCharts);
|
|
|
-
|
|
|
- function initMistakeCharts() {
|
|
|
- const radarCanvas = document.getElementById('mistakeRadarChart');
|
|
|
- const barCanvas = document.getElementById('skillBarChart');
|
|
|
-
|
|
|
- if ((!radarCanvas && !barCanvas)) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const errorTypes = @json($patterns['error_types'] ?? []);
|
|
|
- const skills = @json($patterns['top_skills'] ?? []);
|
|
|
-
|
|
|
- const radarLabels = errorTypes.map(e => e.name || e.type || '错误');
|
|
|
- const radarData = errorTypes.map(e => e.count || e.value || 0);
|
|
|
-
|
|
|
- const skillLabels = skills.map(s => s.name || s.skill || '技能');
|
|
|
- const skillData = skills.map(s => s.score || s.count || 0);
|
|
|
-
|
|
|
- if (window.mistakeRadarChart) {
|
|
|
- window.mistakeRadarChart.destroy();
|
|
|
- }
|
|
|
- if (window.skillBarChart) {
|
|
|
- window.skillBarChart.destroy();
|
|
|
- }
|
|
|
-
|
|
|
- if (radarCanvas && radarLabels.length) {
|
|
|
- window.mistakeRadarChart = new Chart(radarCanvas.getContext('2d'), {
|
|
|
- type: 'radar',
|
|
|
- data: {
|
|
|
- labels: radarLabels,
|
|
|
- datasets: [{
|
|
|
- label: '错误频次',
|
|
|
- data: radarData,
|
|
|
- backgroundColor: 'rgba(248, 180, 0, 0.2)',
|
|
|
- borderColor: '#f59e0b',
|
|
|
- borderWidth: 2,
|
|
|
- pointBackgroundColor: '#f97316'
|
|
|
- }]
|
|
|
- },
|
|
|
- options: {
|
|
|
- plugins: { legend: { display: false } },
|
|
|
- scales: {
|
|
|
- r: {
|
|
|
- beginAtZero: true,
|
|
|
- ticks: { stepSize: 1 }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (barCanvas && skillLabels.length) {
|
|
|
- window.skillBarChart = new Chart(barCanvas.getContext('2d'), {
|
|
|
- type: 'bar',
|
|
|
- data: {
|
|
|
- labels: skillLabels,
|
|
|
- datasets: [{
|
|
|
- label: '弱点指数',
|
|
|
- data: skillData,
|
|
|
- backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
|
|
- borderColor: '#3b82f6',
|
|
|
- borderWidth: 1,
|
|
|
- borderRadius: 6
|
|
|
- }]
|
|
|
- },
|
|
|
- options: {
|
|
|
- plugins: { legend: { display: false }, tooltip: { mode: 'index' } },
|
|
|
- scales: {
|
|
|
- x: { ticks: { color: '#475569' } },
|
|
|
- y: { beginAtZero: true }
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- </script>
|
|
|
-@endpush
|