| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- @php
- $rules = \App\Services\QuestionQualityCheckService::RULES;
- $btnSm = \Filament\Support\Enums\Size::Small;
- @endphp
- <x-filament::page>
- {{-- 勾选题目用 Alpine 即时高亮 + $wire.toggleTemQuestion,勿把 toggleTemQuestion 放进 target,避免出现全屏大图标一闪 --}}
- <div
- wire:loading.delay.shortest
- wire:target="selectKp, updatedSelectedKpCode, importSelected, importSelectedTemIdsFast, importAllCurrentKpToQuestions, importAssemblyQueueToQuestions, addSelectionToAssemblyQueue, clearAssemblyQueue, clearTemSelection, removeFromAssemblyQueue, generateTrialGradingPdf"
- class="fixed inset-0 z-[130] flex items-center justify-center bg-white/70 dark:bg-gray-950/70"
- >
- <div class="flex flex-col items-center gap-2 rounded-lg border border-slate-200 bg-white px-6 py-4 shadow dark:border-gray-700 dark:bg-gray-900">
- <svg class="h-8 w-8 animate-spin text-slate-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
- <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-sm text-slate-600 dark:text-slate-300">{{ __('加载中…') }}</span>
- </div>
- </div>
- {{-- Filament 主题自带 fi-* 样式;此处用内联布局避免依赖 Tailwind 工具类是否被打进 app.css --}}
- <style>
- .qtr-shell {
- width: 100%;
- display: grid;
- grid-template-columns: minmax(13rem, 16rem) minmax(0, 1fr) minmax(15rem, 22rem);
- gap: 1rem;
- align-items: start;
- }
- /* 左右栏:滚动主内容时保持吸顶悬浮(中间栏正常滚动) */
- .qtr-sticky-side {
- position: sticky;
- top: 0.75rem;
- align-self: start;
- max-height: calc(100vh - 1.25rem);
- overflow-y: auto;
- overflow-x: hidden;
- z-index: 2;
- -webkit-overflow-scrolling: touch;
- }
- @@media (max-width: 1024px) {
- .qtr-shell {
- grid-template-columns: 1fr;
- }
- .qtr-sticky-side {
- position: static;
- max-height: none;
- overflow: visible;
- }
- }
- .qtr-kp-scroll {
- display: flex;
- flex-direction: column;
- gap: 0.375rem;
- }
- </style>
- <div class="qtr-shell">
- {{-- 左:知识点 --}}
- <div class="qtr-sticky-side">
- <x-filament::section
- heading="知识点"
- description="按 questions 表该 KP 题量升序;仅含 questions_tem 中出现过的 KP。待审数为 questions_tem 全量,中间列表会隐藏已与正式库同题干的重复题"
- :compact="true"
- >
- <div class="mb-3 space-y-2">
- <x-filament::input.wrapper
- inline-prefix
- prefix-icon="heroicon-m-magnifying-glass"
- >
- <x-filament::input
- type="search"
- wire:model.live.debounce.300ms="kpSearch"
- placeholder="搜索代码或名称…"
- autocomplete="off"
- />
- </x-filament::input.wrapper>
- @if (filled($this->kpSearch))
- <p class="text-xs text-gray-500 dark:text-gray-400">
- 显示 {{ count($this->filteredKpRows) }} / 共 {{ count($this->kpRows) }} 个知识点
- </p>
- @endif
- </div>
- <div class="qtr-kp-scroll">
- @forelse($this->filteredKpRows as $row)
- <div
- class="w-full"
- wire:key="kp-row-{{ $row['kp_code'] }}"
- x-data="{ kp: @js($row['kp_code']) }"
- >
- <x-filament::button
- x-on:click.prevent="$wire.selectKp(kp)"
- :outlined="$this->selectedKpCode !== $row['kp_code']"
- :color="$this->selectedKpCode === $row['kp_code'] ? 'primary' : 'gray'"
- :size="$btnSm"
- class="w-full"
- style="justify-content: flex-start;"
- >
- <div class="flex w-full flex-col items-stretch gap-1 text-left">
- @if (! empty($row['kp_name'] ?? ''))
- <span class="text-sm font-medium leading-snug line-clamp-2">{{ $row['kp_name'] }}</span>
- @endif
- <span class="font-mono text-xs break-all opacity-90">{{ $row['kp_code'] }}</span>
- <span class="text-xs opacity-80">
- 正式 {{ $row['questions_count'] }} · 待审 {{ $row['tem_count'] }}
- </span>
- </div>
- </x-filament::button>
- </div>
- @empty
- <p class="text-sm text-gray-600 dark:text-gray-400">暂无数据(检查 questions_tem)</p>
- @endforelse
- </div>
- </x-filament::section>
- </div>
- {{-- 中:与判卷页同源 components.exam.paper-body(一行一题,含选项布局/答案/解题思路) --}}
- <x-filament::section
- heading="待审题目(判卷页版式)"
- description="与 pdf.exam-grading 同源;已自动隐藏「正式库同 KP 且题干一致」的题目。点击题目为勾选(不跑质检);右侧可一键批量入库,精细质检与单题入库在「高级」中展开"
- :compact="true"
- >
- @if (! $this->selectedKpCode)
- <p class="text-sm text-gray-600 dark:text-gray-400">请先在左侧选择一个知识点。</p>
- @else
- <div class="mb-3 flex flex-wrap items-center gap-2 text-sm">
- <span>当前 KP</span>
- <code class="rounded bg-gray-100 px-2 py-0.5 text-xs dark:bg-white/10">{{ $this->selectedKpCode }}</code>
- <span class="text-xs text-gray-500">共 {{ count($this->temQuestions) }} 道</span>
- </div>
- {{-- KP 切换时整栏重绘;仅换选中题时 wire:ignore 阻止中间 DOM morph,右侧照常更新 --}}
- <div wire:key="qtr-paper-kp-{{ $this->selectedKpCode }}">
- <div wire:ignore>
- @include('filament.pages.partials.question-tem-paper-body', [
- 'questions' => $this->groupedPaperBodyQuestions,
- 'selectedTemId' => $this->selectedTemId,
- 'selectedTemIdsForMulti' => $this->selectedTemIds,
- ])
- </div>
- </div>
- @endif
- </x-filament::section>
- {{-- 右:快速批量入库 → 可选高级质检/单题入库 → 待组卷 / 批量 --}}
- <div
- class="qtr-sticky-side"
- wire:key="qtr-right-stack"
- wire:loading.class="opacity-70"
- wire:target="importSelectedTemIdsFast, importSelected, importAllCurrentKpToQuestions, importAssemblyQueueToQuestions, selectKp, updatedSelectedKpCode, addSelectionToAssemblyQueue, clearAssemblyQueue, clearTemSelection, removeFromAssemblyQueue, generateTrialGradingPdf"
- >
- <x-filament::section
- heading="快速入库"
- description="勾选中间题目后此处列出 tem 编号;不跑页面质检,与批量入库相同服务端规则(重复题跳过)"
- :compact="true"
- >
- @if ($this->selectedTemIds === [])
- <p class="text-sm text-gray-600 dark:text-gray-400">在中间列表点击题目加入勾选。</p>
- @else
- <div class="mb-2 text-xs text-gray-500">
- 已选 <span class="font-semibold text-gray-800 dark:text-gray-200">{{ count($this->selectedTemIds) }}</span> 道 ·
- 最后聚焦 tem #<span class="font-mono">{{ $this->selectedTemId ?? '—' }}</span>
- </div>
- <div class="mb-3 max-h-36 overflow-y-auto rounded-lg border border-gray-100 p-2 font-mono text-xs dark:border-white/10">
- {{ implode(', ', array_map('intval', $this->selectedTemIds)) }}
- </div>
- @endif
- <div class="flex flex-wrap gap-2">
- <x-filament::button
- color="success"
- wire:click="importSelectedTemIdsFast"
- wire:loading.attr="disabled"
- :disabled="$this->selectedTemIds === []"
- >
- 一键入库已选题目
- </x-filament::button>
- <x-filament::button
- color="gray"
- wire:click="clearTemSelection"
- wire:loading.attr="disabled"
- :disabled="$this->selectedTemIds === []"
- >
- 清空勾选
- </x-filament::button>
- </div>
- </x-filament::section>
- <x-filament::section heading="入库后调难度" :compact="true" class="mt-4">
- <p class="text-xs text-gray-600 dark:text-gray-400">
- 自本会话起成功写入 <span class="font-mono">questions</span> 的题目会进入列表,可集中修改难度系数。
- </p>
- <a
- href="{{ \App\Filament\Pages\QuestionImportedDifficultyTune::getUrl() }}"
- class="mt-2 inline-flex text-sm font-medium text-primary-600 underline"
- >
- 打开「已入库题目 · 难度调整」
- </a>
- </x-filament::section>
- <x-filament::section
- heading="高级:质检与单题入库"
- description="需要逐题看清规则、自定义难度再入库时展开;展开后会为当前聚焦的题目跑质检"
- :compact="true"
- class="mt-4"
- >
- @if (! $this->qcPanelExpanded)
- <x-filament::button color="gray" wire:click="$set('qcPanelExpanded', true)">
- 展开质检与单题入库
- </x-filament::button>
- @else
- <div class="mb-3">
- <x-filament::button color="gray" size="sm" wire:click="$set('qcPanelExpanded', false)">
- 收起
- </x-filament::button>
- </div>
- {{-- 质检 --}}
- <div class="rounded-lg border border-gray-100 p-3 dark:border-white/10">
- <p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-200">质检结果</p>
- @if (! $this->selectedTemId)
- <p class="text-sm text-gray-600 dark:text-gray-400">请先在中间点击一道题作为当前聚焦。</p>
- @else
- @php $qc = $this->qcResult; @endphp
- <div class="mb-2 text-xs text-gray-500">
- questions_tem.id = <span class="font-mono">{{ $this->selectedTemId }}</span>
- </div>
- @if ($this->duplicateHint)
- <div class="mb-3 rounded-lg bg-amber-50 p-3 text-xs text-amber-900 dark:bg-amber-950/40 dark:text-amber-100">
- {{ $this->duplicateHint }}
- </div>
- @endif
- @if ($qc)
- <div
- class="mb-3 rounded-lg border p-3 {{ $qc['passed'] ? 'border-success-600/40 bg-success-50 dark:bg-success-950/30' : 'border-danger-600/40 bg-danger-50 dark:bg-danger-950/30' }}"
- >
- <div class="text-sm font-medium">
- {{ $qc['passed'] ? '质检通过' : '质检未通过' }}
- </div>
- @if (! empty($qc['errors']))
- <ul class="mt-2 list-inside list-disc text-xs">
- @foreach ($qc['errors'] as $code)
- <li>{{ $rules[$code]['name'] ?? $code }}</li>
- @endforeach
- </ul>
- @endif
- </div>
- <div class="max-h-48 overflow-y-auto rounded-lg border border-gray-100 p-2 text-xs dark:border-white/10">
- @foreach ($qc['results'] as $r)
- @if (($r['auto_result'] ?? '') !== 'skip')
- <div class="flex justify-between gap-2 border-b border-gray-100 py-1 last:border-0 dark:border-white/10">
- <span>{{ $r['rule_name'] ?? $r['rule_code'] }}</span>
- <span class="{{ ($r['passed'] ?? false) ? 'text-success-600' : 'text-danger-600' }}">
- {{ ($r['passed'] ?? false) ? 'OK' : '×' }}
- </span>
- </div>
- @endif
- @endforeach
- </div>
- @endif
- @endif
- </div>
- {{-- 入库难度 --}}
- <div class="mt-3 rounded-lg border border-gray-100 p-3 dark:border-white/10">
- <p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-200">单题入库难度</p>
- @if (! $this->selectedTemId)
- <p class="text-xs text-gray-500">先选择题目标</p>
- @else
- <x-filament::input.wrapper>
- <x-filament::input
- type="number"
- min="0"
- max="0.9"
- step="0.01"
- placeholder="例如 0.35"
- wire:model.live.debounce.400ms="importDifficultyInput"
- />
- </x-filament::input.wrapper>
- <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
- 将写入 <span class="font-mono">questions.difficulty</span>;批量/快速入库使用各题 tem 原始值(规整到 0~0.90)。
- </p>
- @endif
- </div>
- {{-- 单题入库 --}}
- <div class="mt-3 rounded-lg border border-gray-100 p-3 dark:border-white/10">
- <p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-200">单题入库</p>
- @if (! $this->selectedTemId)
- <p class="text-xs text-gray-500">先选择题目标</p>
- @else
- @php
- $qc = $this->qcResult;
- $canImport = $qc && ($qc['passed'] ?? false) && ! $this->duplicateHint;
- @endphp
- <x-filament::button
- color="success"
- wire:click="importSelected"
- wire:loading.attr="disabled"
- :disabled="! $canImport"
- >
- 入库到 questions
- </x-filament::button>
- @if (! $canImport && $qc)
- <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
- @if ($this->duplicateHint)
- 与正式库重复时不可入库。
- @elseif (! ($qc['passed'] ?? false))
- 质检未通过时不可入库,请先修正题目数据。
- @endif
- </p>
- @else
- <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
- audit_status=0(若表含该字段);同 KP + 同题干重复会拒绝。
- </p>
- @endif
- @endif
- </div>
- @endif
- </x-filament::section>
- {{-- 待组卷与判卷 PDF --}}
- <x-filament::section
- heading="待组卷验 PDF"
- description="将题目加入队列后生成临时试卷;判卷页与 PDF 同源"
- :compact="true"
- class="mt-4"
- >
- <div class="mb-3 space-y-2 text-xs text-gray-600 dark:text-gray-400">
- @if ($this->selectedTemIds !== [])
- <p>当前勾选:<span class="font-mono">{{ count($this->selectedTemIds) }} 道 tem</span></p>
- @endif
- <p class="text-gray-500">队列共 {{ count($this->assemblyQueueRows) }} 道</p>
- </div>
- @if (count($this->assemblyQueueRows) > 0)
- <ul class="mb-3 max-h-32 list-inside list-decimal overflow-y-auto rounded border border-gray-100 p-2 text-xs dark:border-white/10">
- @foreach ($this->assemblyQueueRows as $qr)
- <li class="flex items-center justify-between gap-2 py-0.5">
- <span class="truncate">tem #{{ (int) ($qr->id ?? 0) }}</span>
- <button
- type="button"
- class="shrink-0 text-danger-600 underline"
- wire:click="removeFromAssemblyQueue({{ (int) ($qr->id ?? 0) }})"
- >
- 移除
- </button>
- </li>
- @endforeach
- </ul>
- @endif
- <div class="flex flex-col gap-2">
- <x-filament::button
- color="gray"
- wire:click="addSelectionToAssemblyQueue"
- :disabled="$this->selectedTemIds === []"
- >
- 将当前勾选加入待组卷
- </x-filament::button>
- <x-filament::button
- color="warning"
- wire:click="clearAssemblyQueue"
- :disabled="count($this->assemblyQueueRows) === 0"
- >
- 清空队列
- </x-filament::button>
- <x-filament::button
- color="primary"
- wire:click="generateTrialGradingPdf"
- :disabled="count($this->assemblyQueueRows) === 0"
- >
- 生成完整卷 PDF 并打开判卷页
- </x-filament::button>
- @if ($this->trialGradingUrl)
- <a
- href="{{ $this->trialGradingUrl }}"
- target="_blank"
- rel="noopener noreferrer"
- class="text-sm text-primary-600 underline"
- >
- 判卷页预览(新标签)
- </a>
- @endif
- @if ($this->trialGradingPdfUrl)
- <a
- href="{{ $this->trialGradingPdfUrl }}"
- target="_blank"
- rel="noopener noreferrer"
- class="text-sm text-primary-600 underline"
- download
- >
- 下载完整卷 PDF
- </a>
- @endif
- </div>
- </x-filament::section>
- {{-- 5. 批量:questions_tem → questions(与单题入库同一套规则,不经 JSON) --}}
- <x-filament::section
- heading="批量入库"
- description="直接从 questions_tem 写入 questions;规则与单题「入库」一致(同 KP + 同题干已存在则跳过)"
- :compact="true"
- class="mt-4"
- >
- <div class="flex flex-col gap-2">
- <x-filament::button
- color="gray"
- wire:click="importAllCurrentKpToQuestions"
- wire:loading.attr="disabled"
- wire:target="importAllCurrentKpToQuestions"
- wire:confirm="确定将当前知识点下列表中的全部题目写入 questions?(正式库已存在的同题干会跳过)"
- :disabled="! $this->selectedKpCode || count($this->temQuestions) === 0"
- >
- 一键入库当前知识点全部
- </x-filament::button>
- <x-filament::button
- color="gray"
- wire:click="importAssemblyQueueToQuestions"
- wire:loading.attr="disabled"
- wire:target="importAssemblyQueueToQuestions"
- wire:confirm="确定将上方待组卷队列中的全部题目写入 questions?(已存在的同题干会跳过)"
- :disabled="count($this->assemblyQueueRows) === 0"
- >
- 一键入库待组卷队列全部
- </x-filament::button>
- </div>
- <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
- 与单题入库相同的数据写入逻辑;批量时不强制质检通过,仅做重复与必填判断。
- </p>
- </x-filament::section>
- </div>
- </div>
- </x-filament::page>
|