markdown-import-workbench.blade.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <x-filament::page>
  2. <div class="space-y-4">
  3. @if(!$this->importRecord())
  4. <x-filament::section>
  5. @include('filament.partials.empty-state', [
  6. 'title' => '未选择导入记录',
  7. 'description' => '请先从 Markdown 导入列表进入工作台。',
  8. 'action' => new \Illuminate\Support\HtmlString('<a class="btn btn-primary btn-sm" href="' . route('filament.admin.resources.markdown-imports.index') . '">返回导入列表</a>'),
  9. ])
  10. </x-filament::section>
  11. @elseif(!$this->filenameValid)
  12. <x-filament::section>
  13. <div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
  14. 文件名解析失败:{{ $this->filenameWarning }}
  15. </div>
  16. <div class="mt-3 text-sm text-slate-600">
  17. 请在导入列表中修改文件名并重新导入后再进入工作台。
  18. </div>
  19. </x-filament::section>
  20. @else
  21. <div class="flex flex-wrap items-center gap-3">
  22. <x-filament::input.wrapper class="w-64">
  23. <x-filament::input wire:model.debounce.400ms="search" placeholder="搜索卷子标题/编码" />
  24. </x-filament::input.wrapper>
  25. <x-filament::input.wrapper class="w-44">
  26. <x-filament::input.select wire:model="groupBy">
  27. <option value="bundle">按套卷分组</option>
  28. <option value="paper">按卷子分组</option>
  29. <option value="grade">按年级分组</option>
  30. </x-filament::input.select>
  31. </x-filament::input.wrapper>
  32. <x-filament::button color="gray" wire:click="autoInfer">自动推断</x-filament::button>
  33. <x-filament::button color="gray" wire:click="autoInferSelected">批量推断</x-filament::button>
  34. <x-filament::button color="gray" wire:click="autoBundleKey">生成套卷标识</x-filament::button>
  35. <x-filament::button color="gray" wire:click="autoBundleKeySelected">批量生成套卷标识</x-filament::button>
  36. <x-filament::button color="primary" wire:click="savePaper">保存当前</x-filament::button>
  37. <x-filament::button color="gray" wire:click="$set('dense', ! $wire.dense)">密度切换</x-filament::button>
  38. </div>
  39. <div class="grid grid-cols-12 gap-6">
  40. <div class="col-span-8 space-y-4">
  41. <x-filament::section heading="导入信息">
  42. <div class="grid grid-cols-3 gap-4 text-sm text-slate-600">
  43. <div class="rounded-lg border border-slate-200 p-3">
  44. <div class="text-xs text-slate-500">导入文件</div>
  45. <div class="font-medium text-slate-800">{{ $this->importRecord()?->file_name ?? '未选择导入记录' }}</div>
  46. </div>
  47. <div class="rounded-lg border border-slate-200 p-3">
  48. <div class="text-xs text-slate-500">解析状态</div>
  49. <div class="font-medium text-slate-800">{{ $this->importRecord()?->status_label ?? '—' }}</div>
  50. </div>
  51. <div class="rounded-lg border border-slate-200 p-3">
  52. <div class="text-xs text-slate-500">候选题数</div>
  53. <div class="font-medium text-slate-800">{{ $this->importRecord()?->parsed_count ?? 0 }}</div>
  54. </div>
  55. </div>
  56. @if(!empty($this->filenameParsed))
  57. <div class="mt-3 flex flex-wrap gap-2 text-xs text-slate-500">
  58. <span class="ui-tag">系列:{{ $this->filenameParsed['series'] ?? '-' }}</span>
  59. <span class="ui-tag">年级:{{ $this->filenameParsed['grade'] ?? '-' }}</span>
  60. <span class="ui-tag">学期:{{ $this->filenameParsed['term'] ?? '-' }}</span>
  61. <span class="ui-tag">学科:{{ $this->filenameParsed['subject'] ?? '-' }}</span>
  62. <span class="ui-tag">名称:{{ $this->filenameParsed['name'] ?? '-' }}</span>
  63. </div>
  64. @endif
  65. </x-filament::section>
  66. <x-filament::section heading="卷子列表(选择后批量覆盖)">
  67. <div class="mb-2 flex flex-wrap gap-2">
  68. <x-filament::button color="gray" wire:click="selectAllVisible">全选当前列表</x-filament::button>
  69. <x-filament::button color="gray" wire:click="clearSelection">清空选择</x-filament::button>
  70. </div>
  71. <div class="max-h-72 overflow-y-auto divide-y divide-gray-100">
  72. @foreach($this->groupedPapers() as $group => $items)
  73. <div class="px-3 py-2 text-xs font-semibold text-slate-500 bg-slate-50">{{ $group }}</div>
  74. @foreach($items as $paper)
  75. @php
  76. $meta = $paper['meta'] ?? [];
  77. $expected = $meta['expected_count'] ?? null;
  78. $candidateCount = $paper['candidates_count'] ?? 0;
  79. @endphp
  80. <label class="flex items-start gap-3 {{ $dense ? 'py-1' : 'py-2' }}">
  81. <input type="checkbox" wire:model="selectedIds" value="{{ $paper['id'] }}" class="mt-1 rounded border-gray-300">
  82. <button type="button" wire:click="selectPaper({{ $paper['id'] }})" class="text-left flex-1">
  83. <div class="text-sm font-medium text-gray-900">{{ $paper['title'] ?? $paper['full_title'] ?? '未命名' }}</div>
  84. <div class="text-xs text-gray-500 flex flex-wrap gap-2 items-center">
  85. <span>年级 {{ $paper['grade'] ?? '-' }} · 学期 {{ $paper['term'] ?? '-' }}</span>
  86. <span>候选题数 {{ $candidateCount }}</span>
  87. @if($expected)
  88. <span class="{{ ((int) $expected) === (int) $candidateCount ? 'text-emerald-700 bg-emerald-50' : 'text-amber-700 bg-amber-50' }} px-2 py-0.5 rounded">
  89. 预期 {{ $expected }}
  90. </span>
  91. @endif
  92. @if(empty($paper['textbook_id']))
  93. <span class="px-2 py-0.5 rounded bg-rose-50 text-rose-700">未关联教材</span>
  94. @endif
  95. @if(empty($meta['catalog_node_id'] ?? null))
  96. <span class="px-2 py-0.5 rounded bg-rose-50 text-rose-700">未关联目录</span>
  97. @endif
  98. </div>
  99. </button>
  100. </label>
  101. @endforeach
  102. @endforeach
  103. @if($this->papers()->isEmpty())
  104. <div class="py-6 text-center text-sm text-gray-500">暂无卷子数据</div>
  105. @endif
  106. </div>
  107. </x-filament::section>
  108. <x-filament::section heading="卷子原始 Markdown">
  109. <div class="prose prose-sm max-w-none bg-gray-50 p-4 rounded-lg min-h-[240px]">
  110. @if($this->selectedPaper())
  111. {!! \App\Services\MathFormulaProcessor::processFormulas($this->selectedPaper()?->raw_markdown ?? '') !!}
  112. @else
  113. <div class="text-sm text-gray-400">暂无选中卷子</div>
  114. @endif
  115. </div>
  116. </x-filament::section>
  117. </div>
  118. <div class="col-span-4 space-y-4">
  119. <x-filament::section heading="卷子归属信息">
  120. @php
  121. $textbookSuggestions = $this->textbookSuggestions();
  122. $catalogSuggestions = $this->catalogSuggestions();
  123. $coverageSummary = $this->catalogCoverageSummary();
  124. $missingCatalogNodes = $this->missingCatalogNodes();
  125. @endphp
  126. <div class="space-y-3">
  127. <x-filament::input.wrapper>
  128. <x-filament::input wire:model="form.title" placeholder="卷子标题" />
  129. </x-filament::input.wrapper>
  130. <x-filament::input.wrapper>
  131. <x-filament::input wire:model="form.bundle_key" placeholder="套卷标识(如:九年级上册·同步卷)" />
  132. </x-filament::input.wrapper>
  133. @if(!empty($textbookSuggestions))
  134. <div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs">
  135. <div class="font-semibold text-slate-600 mb-2">教材推荐</div>
  136. <div class="flex flex-col gap-2">
  137. @foreach($textbookSuggestions as $suggest)
  138. <button type="button" wire:click="applyTextbookSuggestion({{ $suggest['id'] }})" class="text-left rounded-md border border-slate-200 bg-white px-3 py-2 hover:border-primary-400">
  139. <div class="text-slate-800 font-medium">{{ $suggest['title'] }}</div>
  140. <div class="text-slate-500 mt-1">系列:{{ $suggest['series'] }} · 年级 {{ $suggest['grade'] ?? '-' }} · 学期 {{ $suggest['semester'] ?? '-' }}</div>
  141. </button>
  142. @endforeach
  143. </div>
  144. </div>
  145. @endif
  146. <x-filament::input.wrapper>
  147. <x-filament::input.select wire:model="form.grade">
  148. <option value="">年级</option>
  149. @foreach($this->gradeOptions() as $value => $label)
  150. <option value="{{ $value }}">{{ $label }}</option>
  151. @endforeach
  152. </x-filament::input.select>
  153. </x-filament::input.wrapper>
  154. <x-filament::input.wrapper>
  155. <x-filament::input.select wire:model="form.term">
  156. <option value="">学期</option>
  157. @foreach($this->termOptions() as $value => $label)
  158. <option value="{{ $value }}">{{ $label }}</option>
  159. @endforeach
  160. </x-filament::input.select>
  161. </x-filament::input.wrapper>
  162. <x-filament::input.wrapper>
  163. <x-filament::input wire:model="form.chapter" placeholder="章节" />
  164. </x-filament::input.wrapper>
  165. <x-filament::input.wrapper>
  166. <x-filament::input.select wire:model="form.source_type">
  167. <option value="">卷子类型</option>
  168. @foreach($this->sourceTypeOptions() as $value => $label)
  169. <option value="{{ $value }}">{{ $label }}</option>
  170. @endforeach
  171. </x-filament::input.select>
  172. </x-filament::input.wrapper>
  173. <x-filament::input.wrapper>
  174. <x-filament::input wire:model="form.source_year" placeholder="来源年份" />
  175. </x-filament::input.wrapper>
  176. <x-filament::input.wrapper>
  177. <x-filament::input.select wire:model="form.textbook_id">
  178. <option value="">匹配教材</option>
  179. @foreach($this->textbookOptions() as $id => $title)
  180. <option value="{{ $id }}">{{ $title }}</option>
  181. @endforeach
  182. </x-filament::input.select>
  183. </x-filament::input.wrapper>
  184. <x-filament::input.wrapper>
  185. <x-filament::input wire:model="form.textbook_series" placeholder="教材系列" />
  186. </x-filament::input.wrapper>
  187. <x-filament::input.wrapper>
  188. <x-filament::input.select wire:model="form.catalog_node_id">
  189. <option value="">关联目录</option>
  190. @foreach($this->catalogOptions() as $id => $label)
  191. <option value="{{ $id }}">{{ $label }}</option>
  192. @endforeach
  193. </x-filament::input.select>
  194. </x-filament::input.wrapper>
  195. @if(!empty($catalogSuggestions))
  196. <div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs">
  197. <div class="font-semibold text-slate-600 mb-2">目录推荐</div>
  198. <div class="flex flex-col gap-2">
  199. @foreach($catalogSuggestions as $suggest)
  200. <button type="button" wire:click="applyCatalogSuggestion({{ $suggest['id'] }})" class="text-left rounded-md border border-slate-200 bg-white px-3 py-2 hover:border-primary-400">
  201. <div class="text-slate-800 font-medium">{{ $suggest['title'] }}</div>
  202. </button>
  203. @endforeach
  204. </div>
  205. </div>
  206. @endif
  207. <x-filament::input.wrapper>
  208. <x-filament::input wire:model="form.expected_count" placeholder="预期题量(如 24)" />
  209. </x-filament::input.wrapper>
  210. <x-filament::input.wrapper>
  211. <x-filament::input wire:model="form.source_name" placeholder="来源名称" />
  212. </x-filament::input.wrapper>
  213. <x-filament::input.wrapper>
  214. <x-filament::input wire:model="form.source_page" placeholder="页码范围" />
  215. </x-filament::input.wrapper>
  216. <x-filament::input.wrapper>
  217. <x-filament::input wire:model="form.tags" placeholder="标签(逗号分隔)" />
  218. </x-filament::input.wrapper>
  219. </div>
  220. </x-filament::section>
  221. <x-filament::section heading="批量覆盖">
  222. <div class="text-xs text-gray-500 mb-2">对勾选卷子批量应用非空字段</div>
  223. <div class="space-y-2">
  224. <x-filament::input.wrapper>
  225. <x-filament::input wire:model="batch.bundle_key" placeholder="套卷标识" />
  226. </x-filament::input.wrapper>
  227. <x-filament::input.wrapper>
  228. <x-filament::input.select wire:model="batch.grade">
  229. <option value="">年级</option>
  230. @foreach($this->gradeOptions() as $value => $label)
  231. <option value="{{ $value }}">{{ $label }}</option>
  232. @endforeach
  233. </x-filament::input.select>
  234. </x-filament::input.wrapper>
  235. <x-filament::input.wrapper>
  236. <x-filament::input.select wire:model="batch.term">
  237. <option value="">学期</option>
  238. @foreach($this->termOptions() as $value => $label)
  239. <option value="{{ $value }}">{{ $label }}</option>
  240. @endforeach
  241. </x-filament::input.select>
  242. </x-filament::input.wrapper>
  243. <x-filament::input.wrapper>
  244. <x-filament::input.select wire:model="batch.source_type">
  245. <option value="">卷子类型</option>
  246. @foreach($this->sourceTypeOptions() as $value => $label)
  247. <option value="{{ $value }}">{{ $label }}</option>
  248. @endforeach
  249. </x-filament::input.select>
  250. </x-filament::input.wrapper>
  251. <x-filament::input.wrapper>
  252. <x-filament::input.select wire:model="batch.textbook_id">
  253. <option value="">匹配教材</option>
  254. @foreach($this->textbookOptions() as $id => $title)
  255. <option value="{{ $id }}">{{ $title }}</option>
  256. @endforeach
  257. </x-filament::input.select>
  258. </x-filament::input.wrapper>
  259. <x-filament::input.wrapper>
  260. <x-filament::input.select wire:model="batch.catalog_node_id">
  261. <option value="">关联目录</option>
  262. @foreach($this->catalogOptions() as $id => $label)
  263. <option value="{{ $id }}">{{ $label }}</option>
  264. @endforeach
  265. </x-filament::input.select>
  266. </x-filament::input.wrapper>
  267. <x-filament::input.wrapper>
  268. <x-filament::input wire:model="batch.expected_count" placeholder="预期题量" />
  269. </x-filament::input.wrapper>
  270. <x-filament::input.wrapper>
  271. <x-filament::input wire:model="batch.tags" placeholder="标签(逗号分隔)" />
  272. </x-filament::input.wrapper>
  273. <x-filament::button color="gray" wire:click="seedBatchFromCurrent">以当前卷为默认</x-filament::button>
  274. <x-filament::button color="warning" x-on:click.prevent="if(confirm('确认对勾选卷子批量覆盖?')) { $wire.applyBatch() }">
  275. 批量覆盖
  276. </x-filament::button>
  277. </div>
  278. </x-filament::section>
  279. @if(!empty($coverageSummary))
  280. <x-filament::section heading="目录覆盖提示">
  281. <div class="text-xs text-slate-500 mb-2">
  282. 目录总数 {{ $coverageSummary['total'] }} · 已关联 {{ $coverageSummary['linked'] }} · 缺卷子目录 {{ $coverageSummary['missing'] }}
  283. </div>
  284. @if(!empty($missingCatalogNodes))
  285. <div class="space-y-2">
  286. @foreach($missingCatalogNodes as $node)
  287. <button type="button" wire:click="applyCatalogSuggestion({{ $node['id'] }})" class="text-left w-full rounded-lg border border-slate-200 bg-white px-3 py-2 hover:border-primary-400">
  288. <div class="text-sm text-slate-800">{{ $node['title'] }}</div>
  289. <div class="text-xs text-slate-500">点击绑定到当前卷子</div>
  290. </button>
  291. @endforeach
  292. </div>
  293. @else
  294. <div class="text-xs text-slate-500">暂无缺口目录</div>
  295. @endif
  296. </x-filament::section>
  297. @endif
  298. </div>
  299. </div>
  300. @endif
  301. </div>
  302. </x-filament::page>