question-tem-quality-review.blade.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. @php
  2. $rules = \App\Services\QuestionQualityCheckService::RULES;
  3. $btnSm = \Filament\Support\Enums\Size::Small;
  4. @endphp
  5. <x-filament::page
  6. x-data="{}"
  7. x-on:qtr-scroll-top.window="window.scrollTo({ top: 0, behavior: 'smooth' })"
  8. >
  9. {{-- 仅监听服务端动作,避免对现有组卷视图组件产生耦合副作用 --}}
  10. <div
  11. wire:loading.delay.shortest
  12. wire:target="selectKp, updatedSelectedKpCode, importSelected, importSelectedTemIdsFast, importAllCurrentKpToQuestions, importAssemblyQueueToQuestions, addSelectionToAssemblyQueue, clearAssemblyQueue, clearTemSelection, removeFromAssemblyQueue, generateTrialGradingPdf"
  13. class="fixed inset-0 z-[130] flex items-center justify-center bg-white/70 dark:bg-gray-950/70"
  14. >
  15. <div class="qtr-page-loading-card">
  16. <div class="qtr-page-loading-spinner" role="status" aria-hidden="true"></div>
  17. <span class="qtr-page-loading-text">{{ __('加载中…') }}</span>
  18. </div>
  19. </div>
  20. {{-- Filament 主题自带 fi-* 样式;此处用内联布局避免依赖 Tailwind 工具类是否被打进 app.css --}}
  21. <style>
  22. .qtr-page-loading-card {
  23. display: flex;
  24. flex-direction: column;
  25. align-items: center;
  26. gap: 0.5rem;
  27. padding: 1rem 1.5rem;
  28. border-radius: 0.5rem;
  29. border: 1px solid rgb(226 232 240);
  30. background: #fff;
  31. box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.08);
  32. }
  33. .dark .qtr-page-loading-card {
  34. border-color: rgb(55 65 81);
  35. background: rgb(17 24 39);
  36. box-shadow: none;
  37. }
  38. .qtr-page-loading-text {
  39. font-size: 0.875rem;
  40. line-height: 1.25;
  41. color: #475569;
  42. margin: 0;
  43. }
  44. .dark .qtr-page-loading-text {
  45. color: #94a3b8;
  46. }
  47. /* 纯 CSS 小环,固定 1.5rem;勿用依赖 Tailwind 的 SVG */
  48. .qtr-page-loading-spinner {
  49. box-sizing: border-box;
  50. width: 1.5rem;
  51. height: 1.5rem;
  52. flex-shrink: 0;
  53. border: 2px solid rgba(100, 116, 139, 0.25);
  54. border-top-color: rgb(100, 116, 139);
  55. border-radius: 50%;
  56. animation: qtr-spin 0.7s linear infinite;
  57. }
  58. @@keyframes qtr-spin {
  59. to { transform: rotate(360deg); }
  60. }
  61. .qtr-shell {
  62. width: 100%;
  63. display: grid;
  64. grid-template-columns: minmax(13rem, 16rem) minmax(0, 1fr) minmax(15rem, 22rem);
  65. gap: 1rem;
  66. align-items: start;
  67. }
  68. /* 左右栏:滚动主内容时保持吸顶悬浮(中间栏正常滚动) */
  69. .qtr-sticky-side {
  70. position: sticky;
  71. top: 0.75rem;
  72. align-self: start;
  73. max-height: calc(100vh - 1.25rem);
  74. overflow-y: auto;
  75. overflow-x: hidden;
  76. z-index: 2;
  77. -webkit-overflow-scrolling: touch;
  78. }
  79. @@media (max-width: 1024px) {
  80. .qtr-shell {
  81. grid-template-columns: 1fr;
  82. }
  83. .qtr-sticky-side {
  84. position: static;
  85. max-height: none;
  86. overflow: visible;
  87. }
  88. }
  89. .qtr-kp-scroll {
  90. display: flex;
  91. flex-direction: column;
  92. gap: 0.375rem;
  93. }
  94. </style>
  95. <div class="qtr-shell">
  96. {{-- 左:知识点 --}}
  97. <div class="qtr-sticky-side">
  98. <x-filament::section
  99. heading="知识点"
  100. description="按 questions 正式题量升序;仅含 questions_tem 出现过的 KP。数字含义:表内=questions_tem 本 KP 总行数;可入库=去掉与正式库同题干重复后、与中间列表一致的可提交条数"
  101. :compact="true"
  102. >
  103. <div class="mb-3 space-y-2">
  104. @if (count($this->gradeOptions) > 0)
  105. <x-filament::input.wrapper>
  106. <x-filament::input.select wire:model.live="gradeFilter">
  107. <option value="">全部年级</option>
  108. @foreach ($this->gradeOptions as $opt)
  109. <option value="{{ $opt['value'] }}">{{ $opt['label'] }}</option>
  110. @endforeach
  111. </x-filament::input.select>
  112. </x-filament::input.wrapper>
  113. @endif
  114. @if (count($this->semesterOptions) > 0)
  115. <x-filament::input.wrapper>
  116. <x-filament::input.select wire:model.live="semesterFilter">
  117. <option value="">全部学期</option>
  118. @foreach ($this->semesterOptions as $opt)
  119. <option value="{{ $opt['value'] }}">{{ $opt['label'] }}</option>
  120. @endforeach
  121. </x-filament::input.select>
  122. </x-filament::input.wrapper>
  123. @endif
  124. <x-filament::input.wrapper
  125. inline-prefix
  126. prefix-icon="heroicon-m-magnifying-glass"
  127. >
  128. <x-filament::input
  129. type="search"
  130. wire:model.live.debounce.300ms="kpSearch"
  131. placeholder="搜索代码或名称…"
  132. autocomplete="off"
  133. />
  134. </x-filament::input.wrapper>
  135. @if (filled($this->kpSearch))
  136. <p class="text-xs text-gray-500 dark:text-gray-400">
  137. 显示 {{ count($this->filteredKpRows) }} / 共 {{ count($this->kpRows) }} 个知识点
  138. </p>
  139. @endif
  140. @if (filled($this->gradeFilter))
  141. @php
  142. $gradeLabel = collect($this->gradeOptions)->firstWhere('value', (string) $this->gradeFilter)['label'] ?? $this->gradeFilter;
  143. @endphp
  144. <p class="text-xs text-gray-500 dark:text-gray-400">
  145. 已按年级筛选:{{ $gradeLabel }}
  146. </p>
  147. @endif
  148. @if (filled($this->semesterFilter))
  149. @php
  150. $semesterLabel = collect($this->semesterOptions)->firstWhere('value', (string) $this->semesterFilter)['label'] ?? $this->semesterFilter;
  151. @endphp
  152. <p class="text-xs text-gray-500 dark:text-gray-400">
  153. 已按学期筛选:{{ $semesterLabel }}
  154. </p>
  155. @endif
  156. </div>
  157. <div class="qtr-kp-scroll">
  158. @forelse($this->filteredKpRows as $row)
  159. <div
  160. class="w-full"
  161. wire:key="kp-row-{{ $row['kp_code'] }}"
  162. x-data="{ kp: @js($row['kp_code']) }"
  163. >
  164. <x-filament::button
  165. x-on:click.prevent="$wire.selectKp(kp)"
  166. :outlined="$this->selectedKpCode !== $row['kp_code']"
  167. :color="$this->selectedKpCode === $row['kp_code'] ? 'primary' : 'gray'"
  168. :size="$btnSm"
  169. class="w-full"
  170. style="justify-content: flex-start;"
  171. >
  172. <div class="flex w-full flex-col items-stretch gap-1 text-left">
  173. @if (! empty($row['kp_name'] ?? ''))
  174. <span class="text-sm font-medium leading-snug line-clamp-2">{{ $row['kp_name'] }}</span>
  175. @endif
  176. <span class="font-mono text-xs break-all opacity-90">{{ $row['kp_code'] }}</span>
  177. <span class="text-xs opacity-80">
  178. 正式 {{ $row['questions_count'] }}
  179. · 表内 {{ $row['tem_count'] }}
  180. · 可入库 {{ $row['tem_importable_count'] ?? 0 }}
  181. </span>
  182. </div>
  183. </x-filament::button>
  184. </div>
  185. @empty
  186. <p class="text-sm text-gray-600 dark:text-gray-400">暂无数据(检查 questions_tem)</p>
  187. @endforelse
  188. </div>
  189. </x-filament::section>
  190. </div>
  191. {{-- 中:按 /api/questions/pdf 同源版式展示(交互独立) --}}
  192. <x-filament::section
  193. heading="待审题目(题目质检PDF同源版式)"
  194. description="中间区按 /api/questions/pdf(题目质检)同源布局展示;勾选/聚焦在独立控件中完成。"
  195. :compact="true"
  196. >
  197. @if (! $this->selectedKpCode)
  198. <p class="text-sm text-gray-600 dark:text-gray-400">请先在左侧选择一个知识点。</p>
  199. @else
  200. @php
  201. $qtrKpMeta = collect($this->kpRows)->firstWhere('kp_code', $this->selectedKpCode);
  202. @endphp
  203. <div class="mb-3 flex flex-wrap items-center gap-2 text-sm">
  204. <span>当前 KP</span>
  205. <code class="rounded bg-gray-100 px-2 py-0.5 text-xs dark:bg-white/10">{{ $this->selectedKpCode }}</code>
  206. <span class="text-xs text-gray-500">
  207. 本列表 <span class="font-semibold text-gray-700 dark:text-gray-200">{{ count($this->temQuestions) }}</span> 道可入库预览
  208. @if ($qtrKpMeta)
  209. <span class="text-gray-400 dark:text-gray-500">
  210. (表内 {{ (int) ($qtrKpMeta['tem_count'] ?? 0) }} 道,已与正式库题干重复而隐藏
  211. {{ max(0, (int) ($qtrKpMeta['tem_count'] ?? 0) - (int) ($qtrKpMeta['tem_importable_count'] ?? 0)) }}
  212. 道)
  213. </span>
  214. @endif
  215. </span>
  216. </div>
  217. <div class="mb-3 rounded-lg border border-gray-100 p-3 dark:border-white/10">
  218. <p class="mb-2 text-xs text-gray-500">独立勾选区(每题三项快检:题干/答案/解题思路)</p>
  219. @if (count($this->temQuestionCards) === 0)
  220. <p class="text-sm text-gray-600 dark:text-gray-400">当前知识点暂无可入库题目。</p>
  221. @else
  222. <style>
  223. .qtr-card-list { max-height: none; overflow: visible; display: grid; gap: 0.75rem; }
  224. .qtr-card { border: 1px solid rgb(229 231 235); border-radius: 0.5rem; padding: 0.5rem; }
  225. .dark .qtr-card { border-color: rgb(75 85 99); }
  226. .qtr-badge { font-size: 11px; border-radius: 999px; padding: 1px 8px; border: 1px solid transparent; }
  227. .qtr-badge-ok { color: #166534; background: #bbf7d0; border-color: #4ade80; }
  228. .qtr-badge-no { color: #991b1b; background: #fee2e2; border-color: #fca5a5; }
  229. .dark .qtr-badge-ok { color: #86efac; background: rgba(22,101,52,0.25); border-color: rgba(134,239,172,0.35); }
  230. .dark .qtr-badge-no { color: #fca5a5; background: rgba(127,29,29,0.25); border-color: rgba(252,165,165,0.35); }
  231. .qtr-full-preview-wrap { margin-top: 0.5rem; border-top: 1px dashed rgb(209 213 219); padding-top: 0.5rem; }
  232. .dark .qtr-full-preview-wrap { border-top-color: rgb(75 85 99); }
  233. </style>
  234. <div class="qtr-card-list">
  235. @foreach ($this->visibleTemQuestionCards as $card)
  236. @php
  237. $tid = (int) $card['id'];
  238. $checks = $card['checks'];
  239. $meta = $card['meta'] ?? [];
  240. $similar = $this->temSimilarityHints[$tid] ?? null;
  241. $similarScore = (float) ($similar['score'] ?? 0);
  242. $similarStyle = 'background:#f0fdf4;color:#166534;border:1px solid #86efac;';
  243. if ($similarScore >= 60.0 && $similarScore <= 80.0) {
  244. $similarStyle = 'background:#fff7ed;color:#c2410c;border:1px solid #fdba74;';
  245. } elseif ($similarScore > 80.0) {
  246. $similarStyle = 'background:#fee2e2;color:#b91c1c;border:1px solid #fca5a5;';
  247. }
  248. $auditReason = trim((string) ($meta['audit_reason'] ?? ''));
  249. $isQualified = $auditReason === '合格';
  250. $updatedDate = '';
  251. if (! empty($meta['updated_at']) && $meta['updated_at'] !== '-') {
  252. $updatedDate = substr((string) $meta['updated_at'], 0, 10);
  253. }
  254. $grouped = $card['grouped_questions'] ?? ['choice' => [], 'fill' => [], 'answer' => []];
  255. $q = $grouped['choice'][0] ?? $grouped['fill'][0] ?? $grouped['answer'][0] ?? null;
  256. @endphp
  257. <div class="qtr-card" wire:key="tem-card-{{ $tid }}">
  258. <div class="mb-1 flex items-start gap-2">
  259. <button type="button" wire:click="focusTemQuestion({{ $tid }})" class="text-left hover:underline">
  260. <span class="font-mono text-xs">tem #{{ $tid }}</span>
  261. </button>
  262. <span class="qtr-badge qtr-badge-ok">难度 {{ $meta['difficulty'] ?? '-' }}</span>
  263. @if ((int) ($this->selectedTemId ?? 0) === $tid)
  264. <span class="qtr-badge qtr-badge-ok">当前聚焦</span>
  265. @endif
  266. @if (in_array($tid, $this->pendingImportTemIds, true))
  267. <span class="qtr-badge qtr-badge-ok">待入库</span>
  268. @endif
  269. </div>
  270. <div class="flex flex-wrap gap-1.5">
  271. <span class="qtr-badge {{ $checks['stem'] ? 'qtr-badge-ok' : 'qtr-badge-no' }}">题干</span>
  272. <span class="qtr-badge {{ $checks['answer'] ? 'qtr-badge-ok' : 'qtr-badge-no' }}">答案</span>
  273. <span class="qtr-badge {{ $checks['solution'] ? 'qtr-badge-ok' : 'qtr-badge-no' }}">解题思路</span>
  274. @if ($updatedDate !== '')
  275. <span class="qtr-badge qtr-badge-ok">
  276. {{ $updatedDate }}
  277. </span>
  278. @endif
  279. </div>
  280. @if ($similar)
  281. <div class="mt-1 text-[11px] text-gray-600 dark:text-gray-300">
  282. <button
  283. type="button"
  284. class="inline-flex items-center rounded-full px-2 py-0.5 hover:opacity-90"
  285. style="background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;"
  286. x-on:click.prevent="
  287. const qid = {{ (int) ($similar['question_id'] ?? 0) }};
  288. window.dispatchEvent(new CustomEvent('qtr-scroll-to-question', { detail: { qid } }));
  289. "
  290. >
  291. 最相似 question #{{ (int) ($similar['question_id'] ?? 0) }}
  292. </button>
  293. <span class="ml-1 inline-flex items-center rounded-full px-2 py-0.5" style="{{ $similarStyle }}">
  294. 综合相似度 {{ (string) ($similar['score_text'] ?? '-') }}
  295. </span>
  296. <x-filament::button
  297. size="xs"
  298. color="danger"
  299. class="ml-1"
  300. wire:click="markAsSimilarQuestion({{ $tid }}, {{ (int) ($similar['question_id'] ?? 0) }})"
  301. >
  302. 判定相似题
  303. </x-filament::button>
  304. </div>
  305. @endif
  306. <div class="qtr-full-preview-wrap">
  307. @if ($q)
  308. @include('filament.pages.partials.question-tem-question-check-preview', [
  309. 'questions' => $card['grouped_questions'],
  310. 'student' => ['name' => '________', 'grade' => '________'],
  311. 'teacher' => ['name' => '________'],
  312. 'pdfMeta' => [
  313. 'exam_code' => 'tem_'.$tid,
  314. 'student_name' => '________',
  315. 'header_title' => '________|tem_'.$tid.'|题目质检',
  316. 'grading_pdf_title' => '题目质检_tem_'.$tid,
  317. ],
  318. ])
  319. @else
  320. <div class="text-xs text-gray-500">该题暂无可展示内容</div>
  321. @endif
  322. </div>
  323. <div class="mt-3 border-t border-gray-100 pt-2 dark:border-white/10" x-data="{ operated: false }">
  324. @php $st = $this->importStatusMap[$tid] ?? null; @endphp
  325. @php
  326. $directBtnBase = $isQualified ? '#16a34a' : '#dc2626';
  327. $directBtnHover = $isQualified ? '#15803d' : '#b91c1c';
  328. @endphp
  329. @if ($st)
  330. <div class="mb-1 text-[11px]">
  331. @if (($st['state'] ?? '') === 'queued')
  332. <span class="text-primary-600">状态:排队中</span>
  333. @elseif (($st['state'] ?? '') === 'running')
  334. <span class="text-primary-600">状态:入库中</span>
  335. @elseif (($st['state'] ?? '') === 'done')
  336. <span class="text-success-600">状态:已入库(question #{{ (int) ($st['question_id'] ?? 0) }})</span>
  337. @elseif (($st['state'] ?? '') === 'failed')
  338. <span class="text-danger-600">状态:失败({{ (string) ($st['message'] ?? '未知错误') }})</span>
  339. @endif
  340. </div>
  341. @endif
  342. <div class="flex flex-wrap items-center gap-2">
  343. <x-filament::button
  344. size="xs"
  345. color="{{ in_array($tid, $this->pendingImportTemIds, true) ? 'gray' : 'primary' }}"
  346. wire:click="addToPendingImport({{ $tid }})"
  347. :disabled="in_array($tid, $this->pendingImportTemIds, true)"
  348. >
  349. {{ in_array($tid, $this->pendingImportTemIds, true) ? '已在待入库' : '加入待入库' }}
  350. </x-filament::button>
  351. @if (in_array($tid, $this->pendingImportTemIds, true))
  352. <x-filament::button size="xs" color="danger" wire:click="removeFromPendingImport({{ $tid }})">
  353. 移除待入库
  354. </x-filament::button>
  355. @endif
  356. @php
  357. $opLocked = in_array(($st['state'] ?? ''), ['queued', 'running', 'done'], true);
  358. @endphp
  359. <button
  360. type="button"
  361. class="fi-btn fi-size-sm inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-semibold text-white shadow-sm transition"
  362. style="background:{{ $directBtnBase }};"
  363. onmouseover="this.style.background='{{ $directBtnHover }}'"
  364. onmouseout="this.style.background='{{ $directBtnBase }}'"
  365. :disabled="operated || {{ $opLocked ? 'true' : 'false' }}"
  366. :class="{ 'opacity-80 cursor-not-allowed': operated || {{ $opLocked ? 'true' : 'false' }} }"
  367. x-on:click.prevent="if (operated || {{ $opLocked ? 'true' : 'false' }}) return; operated = true; $wire.queueImportTem({{ $tid }})"
  368. >
  369. <span x-show="!(operated || {{ $opLocked ? 'true' : 'false' }})">直接入库</span>
  370. <span x-show="operated || {{ $opLocked ? 'true' : 'false' }}">已操作</span>
  371. </button>
  372. <span class="text-xs text-success-600" x-show="operated">已入库</span>
  373. @if ($auditReason !== '' && $auditReason !== '-')
  374. <span class="ml-auto qtr-badge {{ $isQualified ? 'qtr-badge-ok' : 'qtr-badge-no' }}">
  375. {{ $auditReason }}
  376. </span>
  377. @endif
  378. </div>
  379. </div>
  380. </div>
  381. @endforeach
  382. </div>
  383. @if (count($this->temQuestionCards) > count($this->visibleTemQuestionCards))
  384. <div class="mt-3">
  385. <x-filament::button size="sm" color="gray" wire:click="loadMoreCards">
  386. 加载更多(已显示 {{ count($this->visibleTemQuestionCards) }} / {{ count($this->temQuestionCards) }})
  387. </x-filament::button>
  388. </div>
  389. @endif
  390. @endif
  391. </div>
  392. @endif
  393. </x-filament::section>
  394. {{-- 右:待入库 + 人工判重 --}}
  395. <div
  396. class="qtr-sticky-side"
  397. wire:key="qtr-right-stack"
  398. wire:loading.class=""
  399. wire:target="addToPendingImport,removeFromPendingImport,queueImportTem,importPendingTem,importPendingAll,clearPendingImport,selectKp,updatedSelectedKpCode,updatedGradeFilter,updatedSemesterFilter"
  400. x-data="{}"
  401. x-on:qtr-scroll-to-question.window="
  402. const qid = Number($event.detail?.qid || 0);
  403. if (!qid) return;
  404. const target = document.getElementById('kp-question-' + qid);
  405. if (!target) return;
  406. // 优先滚动右侧容器本身(sticky side),避免只滚动主页面
  407. const targetTop = target.offsetTop;
  408. const targetHeight = target.offsetHeight || 0;
  409. const viewHeight = $el.clientHeight || 0;
  410. const nextTop = Math.max(0, targetTop - (viewHeight / 2) + (targetHeight / 2));
  411. $el.scrollTo({ top: nextTop, behavior: 'smooth' });
  412. // 兜底:再触发一次元素就位
  413. setTimeout(() => {
  414. target.scrollIntoView({ behavior: 'smooth', block: 'center' });
  415. target.classList.add('ring-2', 'ring-green-400');
  416. setTimeout(() => target.classList.remove('ring-2', 'ring-green-400'), 1200);
  417. }, 180);
  418. "
  419. >
  420. <x-filament::section
  421. heading="待入库"
  422. description="优先处理当前待入库列表,可直接入库、回到中间审核或移除。"
  423. :compact="true"
  424. >
  425. @if ($this->pendingImportRows === [])
  426. <p class="text-sm text-gray-600 dark:text-gray-400">先在中间卡片点击「加入待入库」。</p>
  427. @else
  428. <div class="mb-2">
  429. <x-filament::button size="xs" color="gray" wire:click="syncAsyncImportStatuses">
  430. 同步异步状态
  431. </x-filament::button>
  432. </div>
  433. <div class="mb-2 text-xs text-gray-500">
  434. 待入库 <span class="font-semibold text-gray-800 dark:text-gray-200">{{ count($this->pendingImportRows) }}</span> 道
  435. </div>
  436. <div class="mb-3 rounded-lg border border-gray-100 p-2 text-xs dark:border-white/10 space-y-2">
  437. @foreach ($this->pendingImportRows as $pr)
  438. <div class="rounded border border-gray-100 p-2 dark:border-white/10" wire:key="pending-{{ (int) ($pr->id ?? 0) }}">
  439. <div class="mb-1 font-mono">tem #{{ (int) ($pr->id ?? 0) }}</div>
  440. @php $st = $this->importStatusMap[(int) ($pr->id ?? 0)] ?? null; @endphp
  441. @if ($st)
  442. <div class="mb-1 text-[11px]">
  443. @if (($st['state'] ?? '') === 'queued')
  444. <span class="text-primary-600">排队中</span>
  445. @elseif (($st['state'] ?? '') === 'running')
  446. <span class="text-primary-600">入库中</span>
  447. @elseif (($st['state'] ?? '') === 'done')
  448. <span class="text-success-600">已入库 #{{ (int) ($st['question_id'] ?? 0) }}</span>
  449. @elseif (($st['state'] ?? '') === 'failed')
  450. <span class="text-danger-600">失败:{{ (string) ($st['message'] ?? '未知错误') }}</span>
  451. @endif
  452. </div>
  453. @endif
  454. <div class="flex flex-wrap gap-1">
  455. <x-filament::button size="xs" color="success" wire:click="queueImportTem({{ (int) ($pr->id ?? 0) }})">
  456. 入库
  457. </x-filament::button>
  458. <x-filament::button size="xs" color="gray" wire:click="focusTemQuestion({{ (int) ($pr->id ?? 0) }})">
  459. 审核
  460. </x-filament::button>
  461. <x-filament::button size="xs" color="danger" wire:click="removeFromPendingImport({{ (int) ($pr->id ?? 0) }})">
  462. 移除
  463. </x-filament::button>
  464. </div>
  465. </div>
  466. @endforeach
  467. </div>
  468. @endif
  469. <div class="flex flex-wrap gap-2">
  470. <x-filament::button
  471. color="success"
  472. wire:click="importPendingAll"
  473. wire:loading.attr="disabled"
  474. :disabled="$this->pendingImportRows === []"
  475. >
  476. 一键入库待入库全部
  477. </x-filament::button>
  478. <x-filament::button
  479. color="gray"
  480. wire:click="clearPendingImport"
  481. wire:loading.attr="disabled"
  482. :disabled="$this->pendingImportRows === []"
  483. >
  484. 清空待入库
  485. </x-filament::button>
  486. </div>
  487. </x-filament::section>
  488. <x-filament::section
  489. heading="同知识点正式库题目(人工判重)"
  490. description="当前筛选 + 当前知识点下,列出 questions 中已有题目(含题干/答案)用于人工核对是否重复。"
  491. :compact="true"
  492. class="mt-4"
  493. >
  494. @if (! $this->selectedKpCode)
  495. <p class="text-sm text-gray-600 dark:text-gray-400">先在左侧选择知识点。</p>
  496. @elseif ($this->currentKpQuestionStemRows === [])
  497. <p class="text-sm text-gray-600 dark:text-gray-400">正式库当前知识点暂无题目。</p>
  498. @else
  499. <div class="rounded-lg border border-gray-100 p-2 text-xs dark:border-white/10 space-y-2">
  500. @foreach ($this->currentKpQuestionStemRows as $qr)
  501. @php
  502. $qid = (int) ($qr->id ?? 0);
  503. $qStem = (string) ($qr->stem ?? '');
  504. $qAnswer = trim((string) ($qr->answer ?? ''));
  505. $qTypeRaw = mb_strtolower((string) ($qr->question_type ?? ''));
  506. $noPalette = [
  507. 'background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;',
  508. 'background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;',
  509. 'background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;',
  510. 'background:#fffbeb;color:#b45309;border:1px solid #fde68a;',
  511. 'background:#fff1f2;color:#be123c;border:1px solid #fecdd3;',
  512. ];
  513. $noStyle = $noPalette[$qid % count($noPalette)];
  514. $typeLabel = '简答';
  515. $typeStyle = 'background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;';
  516. if (str_contains($qTypeRaw, 'choice') || str_contains($qTypeRaw, '选择')) {
  517. $typeLabel = '选择';
  518. $typeStyle = 'background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;';
  519. } elseif (str_contains($qTypeRaw, 'fill') || str_contains($qTypeRaw, 'blank') || str_contains($qTypeRaw, '填空')) {
  520. $typeLabel = '填空';
  521. $typeStyle = 'background:#fff7ed;color:#c2410c;border:1px solid #fed7aa;';
  522. }
  523. $bestForSelected = null;
  524. if ((int) ($this->selectedTemId ?? 0) > 0) {
  525. $bestForSelected = $this->temSimilarityHints[(int) $this->selectedTemId] ?? null;
  526. }
  527. $isTopSimilar = $bestForSelected && (int) ($bestForSelected['question_id'] ?? 0) === $qid;
  528. $bestScore = (float) ($bestForSelected['score'] ?? 0);
  529. // 右侧用“建议阈值”:>=90 绿,80-90 橙,<80 灰
  530. $bestScoreStyle = 'background:#f3f4f6;color:#4b5563;border:1px solid #d1d5db;';
  531. if ($bestScore >= 90.0) {
  532. $bestScoreStyle = 'background:#f0fdf4;color:#166534;border:1px solid #86efac;';
  533. } elseif ($bestScore >= 80.0) {
  534. $bestScoreStyle = 'background:#fff7ed;color:#c2410c;border:1px solid #fdba74;';
  535. }
  536. @endphp
  537. <div
  538. class="rounded border p-2 dark:border-white/10"
  539. style="{{ $isTopSimilar ? 'border-color:#22c55e;background:#f0fdf4;' : 'border-color:rgb(243 244 246);' }}"
  540. wire:key="kp-q-{{ (int) ($qr->id ?? 0) }}"
  541. id="kp-question-{{ $qid }}"
  542. data-kp-question-id="{{ $qid }}"
  543. >
  544. <div class="mb-1 flex items-center gap-1.5">
  545. <span class="inline-flex items-center rounded-full px-1.5 py-0.5 font-mono text-[10px]" style="{{ $noStyle }}">
  546. question #{{ $qid }}
  547. </span>
  548. <span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px]" style="{{ $typeStyle }}">
  549. {{ $typeLabel }}
  550. </span>
  551. @if ($isTopSimilar && $bestForSelected)
  552. <span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px]" style="{{ $bestScoreStyle }}">
  553. 综合相似 {{ number_format((float) ($bestForSelected['score'] ?? 0), 1) }}%
  554. </span>
  555. @endif
  556. </div>
  557. <div style="font-size:11px;line-height:1.35;color:#4b5563;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;">
  558. {{ $qStem }}
  559. </div>
  560. @if ($qAnswer !== '')
  561. <div class="mt-1 text-[11px] text-gray-500" style="display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;">
  562. 答案:{{ $qAnswer }}
  563. </div>
  564. @endif
  565. </div>
  566. @endforeach
  567. </div>
  568. @endif
  569. </x-filament::section>
  570. </div>
  571. </div>
  572. </x-filament::page>