question-management-simple.blade.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <x-filament-panels::page>
  2. <!-- 数学公式渲染组件 -->
  3. <x-math-render />
  4. <div class="space-y-6">
  5. @php
  6. $questionsData = $this->questions;
  7. $metaData = $this->meta;
  8. $statisticsData = $this->statistics;
  9. @endphp
  10. <div class="flex justify-end">
  11. <a
  12. href="{{ url('/admin/question-generation') }}"
  13. class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
  14. >
  15. <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  16. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
  17. </svg>
  18. 生成题目
  19. </a>
  20. </div>
  21. @php
  22. // 显示统计数据的标签
  23. $statsLabel = $this->selectedKpCode ? "知识点 {$this->selectedKpCode}" : "全部题目";
  24. $displayStats = $statisticsData;
  25. @endphp
  26. <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
  27. <div class="bg-white p-4 rounded-lg border">
  28. <div class="text-sm text-gray-500">题目总数</div>
  29. <div class="text-2xl font-bold text-primary-600">{{ $displayStats['total'] ?? 0 }}</div>
  30. </div>
  31. <div class="bg-white p-4 rounded-lg border">
  32. <div class="text-sm text-gray-500">简单题 (≤0.4)</div>
  33. <div class="text-2xl font-bold text-green-600">
  34. @php
  35. $basicCount = 0;
  36. foreach ($displayStats['by_difficulty'] ?? [] as $key => $value) {
  37. if ((float)$key <= 0.4) {
  38. $basicCount += $value;
  39. }
  40. }
  41. echo $basicCount;
  42. @endphp
  43. </div>
  44. </div>
  45. <div class="bg-white p-4 rounded-lg border">
  46. <div class="text-sm text-gray-500">中等题 (0.4-0.7)</div>
  47. <div class="text-2xl font-bold text-yellow-600">
  48. @php
  49. $mediumCount = 0;
  50. foreach ($displayStats['by_difficulty'] ?? [] as $key => $value) {
  51. if ((float)$key > 0.4 && (float)$key <= 0.7) {
  52. $mediumCount += $value;
  53. }
  54. }
  55. echo $mediumCount;
  56. @endphp
  57. </div>
  58. </div>
  59. <div class="bg-white p-4 rounded-lg border">
  60. <div class="text-sm text-gray-500">拔高题 (>0.7)</div>
  61. <div class="text-2xl font-bold text-red-600">
  62. @php
  63. $advancedCount = 0;
  64. foreach ($displayStats['by_difficulty'] ?? [] as $key => $value) {
  65. if ((float)$key > 0.7) {
  66. $advancedCount += $value;
  67. }
  68. }
  69. echo $advancedCount;
  70. @endphp
  71. </div>
  72. </div>
  73. </div>
  74. <!-- 题型汇总统计 -->
  75. <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  76. <div class="bg-white p-4 rounded-lg border">
  77. <div class="text-sm text-gray-500">选择题</div>
  78. <div class="text-2xl font-bold text-blue-600">
  79. @php
  80. $choiceCount = 0;
  81. foreach ($displayStats['by_type'] ?? [] as $type => $count) {
  82. if ($type === '选择题') {
  83. $choiceCount += $count;
  84. }
  85. }
  86. echo $choiceCount;
  87. @endphp
  88. </div>
  89. </div>
  90. <div class="bg-white p-4 rounded-lg border">
  91. <div class="text-sm text-gray-500">填空题</div>
  92. <div class="text-2xl font-bold text-purple-600">
  93. @php
  94. $fillCount = 0;
  95. foreach ($displayStats['by_type'] ?? [] as $type => $count) {
  96. if ($type === '填空题') {
  97. $fillCount += $count;
  98. }
  99. }
  100. echo $fillCount;
  101. @endphp
  102. </div>
  103. </div>
  104. <div class="bg-white p-4 rounded-lg border">
  105. <div class="text-sm text-gray-500">简单题</div>
  106. <div class="text-2xl font-bold text-orange-600">
  107. @php
  108. $simpleCount = 0;
  109. foreach ($displayStats['by_type'] ?? [] as $type => $count) {
  110. if (in_array($type, ['解答题', '其他'])) {
  111. $simpleCount += $count;
  112. }
  113. }
  114. echo $simpleCount;
  115. @endphp
  116. </div>
  117. </div>
  118. </div>
  119. <div class="bg-white p-4 rounded-lg border">
  120. <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
  121. <div>
  122. <label class="block text-sm font-medium text-gray-700 mb-2">搜索题目</label>
  123. <input type="text" wire:model.live.debounce.300ms="search" placeholder="输入关键词" class="w-full border rounded p-2">
  124. </div>
  125. <div>
  126. <label class="block text-sm font-medium text-gray-700 mb-2">知识点筛选</label>
  127. <select wire:model.live="selectedKpCode" class="w-full border rounded p-2">
  128. <option value="">全部</option>
  129. @foreach($this->knowledgePointOptions as $value => $label)
  130. <option value="{{ $value }}">{{ $label }}</option>
  131. @endforeach
  132. </select>
  133. </div>
  134. <div>
  135. <label class="block text-sm font-medium text-gray-700 mb-2">难度筛选</label>
  136. <select wire:model.live="selectedDifficulty" class="w-full border rounded p-2">
  137. <option value="">全部难度</option>
  138. <option value="0.3">简单 (0.3)</option>
  139. <option value="0.6">中等 (0.6)</option>
  140. <option value="0.85">拔高 (0.85)</option>
  141. </select>
  142. </div>
  143. <div>
  144. <label class="block text-sm font-medium text-gray-700 mb-2">题型筛选</label>
  145. <select wire:model.live="selectedType" class="w-full border rounded p-2">
  146. <option value="">全部类型</option>
  147. @foreach($this->questionTypeOptions as $value => $label)
  148. <option value="{{ $value }}">{{ $label }}</option>
  149. @endforeach
  150. </select>
  151. </div>
  152. <div>
  153. <label class="block text-sm font-medium text-gray-700 mb-2">每页显示</label>
  154. <input type="number" wire:model.live="perPage" min="10" max="100" step="5" class="w-full border rounded p-2">
  155. </div>
  156. </div>
  157. </div>
  158. <div class="bg-white rounded-lg border overflow-hidden">
  159. <table class="min-w-full divide-y divide-gray-200">
  160. <thead class="bg-gray-50">
  161. <tr>
  162. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">题目编号</th>
  163. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">知识点</th>
  164. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">难度</th>
  165. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">技能点</th>
  166. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">题干</th>
  167. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
  168. </tr>
  169. </thead>
  170. <tbody class="bg-white divide-y divide-gray-200">
  171. @forelse($questionsData as $question)
  172. <tr class="hover:bg-gray-50">
  173. <td class="px-6 py-4 whitespace-nowrap">
  174. <a href="{{ url('/admin/question-detail') }}?question_id={{ $question['id'] }}"
  175. class="text-blue-600 hover:underline">{{ $question['question_code'] ?? 'N/A' }}</a>
  176. </td>
  177. <td class="px-6 py-4 whitespace-nowrap">
  178. <div class="text-sm font-medium text-gray-900">{{ $question['kp_name'] ?? $question['kp_code'] ?? 'N/A' }}</div>
  179. @if(!empty($question['kp_code']))
  180. <div class="text-xs text-gray-500">{{ $question['kp_code'] }}</div>
  181. @endif
  182. </td>
  183. <td class="px-6 py-4 whitespace-nowrap">
  184. @php
  185. $difficulty = $question['difficulty'] ?? null;
  186. $label = match (true) {
  187. !$difficulty => 'N/A',
  188. (float)$difficulty <= 0.4 => '基础',
  189. (float)$difficulty <= 0.7 => '中等',
  190. default => '拔高',
  191. };
  192. @endphp
  193. <span class="px-2 py-1 text-xs rounded-full
  194. @if((float)$difficulty <= 0.4) bg-green-100 text-green-800
  195. @elseif((float)$difficulty <= 0.7) bg-yellow-100 text-yellow-800
  196. @else bg-red-100 text-red-800 @endif">
  197. {{ $label }}
  198. </span>
  199. </td>
  200. <td class="px-6 py-4">
  201. @php
  202. $skills = is_array($question['skills'] ?? null) ? $question['skills'] : json_decode($question['skills'] ?? '[]', true);
  203. $skillNames = [];
  204. if (!empty($skills)) {
  205. foreach ($skills as $skill) {
  206. $skill = trim($skill);
  207. // 处理格式如 {直线斜率,直线平行条件} 的情况
  208. if (str_starts_with($skill, '{') && str_ends_with($skill, '}')) {
  209. $innerContent = substr($skill, 1, -1);
  210. $skillParts = explode(',', $innerContent);
  211. foreach ($skillParts as $part) {
  212. $part = trim($part);
  213. if (!empty($part)) {
  214. // 尝试从映射中获取名称
  215. $skillName = $this->skillNameMapping[$part] ?? $part;
  216. if (!in_array($skillName, $skillNames)) {
  217. $skillNames[] = $skillName;
  218. }
  219. }
  220. }
  221. } else {
  222. // 处理单个技能点
  223. $skillCode = preg_replace('/[{}]/', '', $skill);
  224. $skillName = $this->skillNameMapping[$skillCode] ?? $skillCode;
  225. if (!in_array($skillName, $skillNames)) {
  226. $skillNames[] = $skillName;
  227. }
  228. }
  229. }
  230. foreach (array_slice($skillNames, 0, 2) as $skillName) {
  231. echo '<span class="inline-block px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded mr-1 mb-1">' . htmlspecialchars($skillName) . '</span>';
  232. }
  233. if (count($skillNames) > 2) {
  234. echo '<span class="text-xs text-gray-500">...</span>';
  235. }
  236. } else {
  237. echo '-';
  238. }
  239. @endphp
  240. </td>
  241. <td class="px-6 py-4" style="word-wrap: break-word; white-space: normal; line-height: 1.8; max-width: 400px;">
  242. <span class="text-sm text-gray-700 line-clamp-3">
  243. @math($question['stem'] ?? 'N/A')
  244. </span>
  245. @if(strlen($question['stem'] ?? '') > 150)
  246. <button class="text-xs text-blue-500 mt-1"
  247. onclick="this.parentElement.querySelector('span').classList.toggle('line-clamp-3')">展开</button>
  248. @endif
  249. </td>
  250. <td class="px-6 py-4 whitespace-nowrap text-sm">
  251. <a href="{{ url('/admin/question-detail') }}?question_id={{ $question['id'] }}"
  252. class="text-indigo-600 hover:text-indigo-900 font-medium">查看详情</a>
  253. </td>
  254. </tr>
  255. @empty
  256. <tr><td colspan="6" class="px-6 py-12 text-center">暂无数据</td></tr>
  257. @endforelse
  258. </tbody>
  259. </table>
  260. @if(!empty($metaData) && ($metaData['total'] ?? 0) > 0)
  261. <div class="px-4 py-3 border-t border-gray-200 flex items-center justify-between">
  262. <div class="text-sm text-gray-700">共 {{ $metaData['total'] ?? 0 }} 条记录</div>
  263. <div class="flex items-center gap-2">
  264. <button wire:click="previousPage" @disabled($currentPage <= 1) class="px-3 py-1 border rounded">上一页</button>
  265. @foreach($this->getPages() as $page)
  266. <button wire:click="gotoPage({{ $page }})" class="px-3 py-1 border rounded {{ $page === $currentPage ? 'bg-blue-50 text-blue-700' : '' }}">{{ $page }}</button>
  267. @endforeach
  268. <button wire:click="nextPage" @disabled($currentPage >= ($metaData['total_pages'] ?? 1)) class="px-3 py-1 border rounded">下一页</button>
  269. </div>
  270. </div>
  271. @endif
  272. </div>
  273. {{-- 详情侧边栏 --}}
  274. @if($showDetailModal)
  275. <div class="fixed inset-0 z-40 flex">
  276. <div class="flex-1 bg-black/40" wire:click="$set('showDetailModal', false)"></div>
  277. <div class="w-full max-w-3xl bg-white shadow-xl overflow-y-auto">
  278. <div class="p-6 border-b flex justify-between items-center">
  279. <div>
  280. <h3 class="text-lg font-semibold">{{ $editing['question_code'] ?? '题目详情' }}</h3>
  281. <p class="text-sm text-gray-500">
  282. {{ $editing['kp_name'] ?? ($editing['kp_code'] ?? '') }}
  283. @if(!empty($editing['kp_code'])) ({{ $editing['kp_code'] }}) @endif
  284. </p>
  285. </div>
  286. <button class="text-gray-500 hover:text-gray-700" wire:click="$set('showDetailModal', false)">&times;</button>
  287. </div>
  288. <div class="p-6 space-y-4">
  289. <div>
  290. <label class="text-sm font-medium text-gray-700">知识点代码</label>
  291. <input type="text" wire:model="editing.kp_code" class="w-full border rounded px-3 py-2 text-sm">
  292. </div>
  293. <div>
  294. <label class="text-sm font-medium text-gray-700">题干</label>
  295. <textarea wire:model="editing.stem" rows="4" class="w-full border rounded px-3 py-2 text-sm"></textarea>
  296. </div>
  297. <div>
  298. <label class="text-sm font-medium text-gray-700">答案</label>
  299. <textarea wire:model="editing.answer" rows="3" class="w-full border rounded px-3 py-2 text-sm"></textarea>
  300. </div>
  301. <div>
  302. <label class="text-sm font-medium text-gray-700">解析</label>
  303. <textarea wire:model="editing.solution" rows="4" class="w-full border rounded px-3 py-2 text-sm"></textarea>
  304. </div>
  305. <div class="grid grid-cols-2 gap-4 text-sm">
  306. <div>
  307. <label class="text-sm font-medium text-gray-700">难度 (0-1)</label>
  308. <input type="number" step="0.01" min="0" max="1" wire:model="editing.difficulty" class="w-full border rounded px-3 py-2 text-sm">
  309. </div>
  310. <div>
  311. <label class="text-sm font-medium text-gray-700">题型</label>
  312. <input type="text" wire:model="editing.question_type" class="w-full border rounded px-3 py-2 text-sm" placeholder="choice/fill/answer">
  313. </div>
  314. <div>
  315. <label class="text-sm font-medium text-gray-700">来源</label>
  316. <input type="text" wire:model="editing.source" class="w-full border rounded px-3 py-2 text-sm">
  317. </div>
  318. <div>
  319. <label class="text-sm font-medium text-gray-700">标签</label>
  320. <input type="text" wire:model="editing.tags" class="w-full border rounded px-3 py-2 text-sm">
  321. </div>
  322. <div class="col-span-2">
  323. <label class="text-sm font-medium text-gray-700">技能(逗号分隔)</label>
  324. <input type="text" wire:model="editing.skills_text" class="w-full border rounded px-3 py-2 text-sm">
  325. </div>
  326. <div><span class="font-medium">创建时间:</span>{{ $editing['created_at'] ?? '-' }}</div>
  327. <div><span class="font-medium">更新时间:</span>{{ $editing['updated_at'] ?? '-' }}</div>
  328. </div>
  329. </div>
  330. <div class="p-6 border-t flex justify-end gap-3">
  331. <button class="px-4 py-2 border rounded text-sm" wire:click="$set('showDetailModal', false)">取消</button>
  332. <button class="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700" wire:click="saveQuestion">保存</button>
  333. </div>
  334. </div>
  335. </div>
  336. @endif
  337. </x-filament-panels::page>
  338. <script>
  339. document.addEventListener('livewire:init', () => {
  340. // 定期检查通知
  341. let checkCount = 0;
  342. const maxChecks = 30; // 最多检查30次(15秒)
  343. function checkForNotifications() {
  344. checkCount++;
  345. // 使用 fetch 检查是否有新的通知
  346. fetch('/admin/question-management/check-notifications', {
  347. method: 'GET',
  348. headers: {
  349. 'X-Requested-With': 'XMLHttpRequest',
  350. 'Accept': 'application/json',
  351. }
  352. })
  353. .then(response => response.json())
  354. .then(data => {
  355. if (data.hasNotification && data.notification) {
  356. // 刷新页面以显示通知
  357. window.location.reload();
  358. } else if (checkCount < maxChecks) {
  359. // 继续检查
  360. setTimeout(checkForNotifications, 500);
  361. }
  362. })
  363. .catch(error => {
  364. console.error('检查通知失败:', error);
  365. if (checkCount < maxChecks) {
  366. setTimeout(checkForNotifications, 500);
  367. }
  368. });
  369. }
  370. // 页面加载后开始检查
  371. setTimeout(checkForNotifications, 1000);
  372. });
  373. </script>