/* 独立题库助手面板:页面正文与面板样式互不影响。 */ (function (global) { 'use strict'; const ROLES = ['主操', '班长', '副操']; const PAGE_SIZE = 20; const TYPE_NAMES = { single: '单选', multiple: '多选', judgment: '判断', unknown: '未知' }; function plain(value) { return String(value == null ? '' : value); } function normalized(value) { return plain(value).replace(/\s+/g, ' ').trim(); } function el(doc, tag, className, value) { const node = doc.createElement(tag); if (className) node.className = className; if (value != null) node.textContent = value; return node; } function create(callbacks) { callbacks = callbacks || {}; const doc = global.document; const state = { autoAnswer: false, allowCopy: true, role: '', course: '', courses: [], bankTotal: 0, roleCounts: {}, storageMode: 'userscript', pageStatus: '等待识别练习页面', canAnswer: false, canExport: false, metrics: {}, exportEnabled: false, expanded: false, loading: false }; let destroyed = false; let selectionText = ''; let currentOffset = 0; let resultTotal = 0; const host = doc.createElement('div'); host.id = 'yian-question-assistant'; host.setAttribute('data-yian-assistant', ''); host.style.cssText = 'all:initial!important;position:fixed!important;right:16px!important;bottom:16px!important;z-index:2147483646!important;display:block!important;max-width:calc(100vw - 24px)!important;font:13px/1.5 system-ui,"Microsoft YaHei",sans-serif!important;color:#17253d!important;'; const root = host.attachShadow({ mode: 'open' }); const style = doc.createElement('style'); style.textContent = ` :host{font-family:system-ui,-apple-system,"Microsoft YaHei",sans-serif;color:#17253d;font-size:13px;line-height:1.5;color-scheme:light;--accent:#225aca;--muted:#65728a;--line:#e4e9f1;--surface:#f5f7fb} *,*::before,*::after{box-sizing:border-box}button,input,select{font:inherit}button,input,select,label{-webkit-user-select:none;user-select:none}button{cursor:pointer}button:disabled,input:disabled,select:disabled{cursor:not-allowed;opacity:.5}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #b8d1ff;outline-offset:2px}button{border:1px solid var(--line);background:white;color:#273953;border-radius:7px;padding:6px 10px}button:hover:enabled{background:#f2f6ff;border-color:#b6caf1}input[type=text],select{width:100%;min-width:0;border:1px solid #cfd8e7;border-radius:7px;background:#fff;color:#17253d;padding:8px}select{padding:6px 8px}input[type=checkbox],input[type=radio]{accent-color:var(--accent)} .panel{width:354px;max-width:calc(100vw - 24px);background:white;border:1px solid #dce3ee;border-radius:13px;box-shadow:0 8px 36px #14264528;overflow:hidden}.header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:#172a49;color:white}.brand{font-size:15px;font-weight:700;letter-spacing:.2px}.toggle{border:0;background:#ffffff17;color:white;padding:3px 9px;border-radius:5px;min-width:30px}.toggle:hover:enabled{background:#ffffff2c;color:white;border-color:transparent}.body{padding:13px;max-height:calc(100vh - 102px);max-height:calc(100dvh - 102px);overflow:auto;overscroll-behavior:contain}.body[hidden]{display:none}.panel.collapsed{width:auto;min-width:140px}.collapsed .header{padding:10px 12px;gap:16px}.switches{display:grid;gap:9px;padding-bottom:12px;border-bottom:1px solid var(--line)}.switch-row{display:flex;align-items:center;justify-content:space-between;position:relative;gap:12px;cursor:pointer;font-weight:600}.switch{appearance:none;-webkit-appearance:none;width:35px;height:21px;margin:0;border:0!important;border-radius:20px;background:#b7c0ce;position:relative;transition:background .15s}.switch::after{content:"";position:absolute;width:15px;height:15px;top:3px;left:3px;border-radius:50%;background:white;box-shadow:0 1px 3px #0002;transition:transform .15s}.switch:checked{background:var(--accent)}.switch:checked::after{transform:translateX(14px)}.hint{font-size:12px;color:var(--muted);margin:6px 0 0;overflow-wrap:anywhere}.section{padding-top:12px}.section-title{font-size:13px;font-weight:700;margin:0 0 7px}.import-row{display:flex;align-items:center;gap:10px}.import-row button{white-space:nowrap}.bank-info{font-size:12px;color:var(--muted);overflow-wrap:anywhere}.storage{color:#97702e;margin:4px 0 0;font-size:11px}.roles{display:flex;gap:6px;flex-wrap:wrap}.role{display:flex;align-items:center;gap:4px;padding:6px 8px;border:1px solid var(--line);border-radius:7px;cursor:pointer;background:white;flex:1;justify-content:center;white-space:nowrap}.role.active{background:#edf3ff;border-color:#abc5fa;color:#194dab}.role input{margin:0}.role-count{font-size:10px;color:var(--muted)}.course-row{display:grid;grid-template-columns:36px 1fr;gap:7px;align-items:center;margin-top:8px}.search-row{display:flex;gap:6px;margin-top:9px}.search-row input{flex:1}.primary{background:var(--accent);border-color:var(--accent);color:white;white-space:nowrap}.primary:hover:enabled{background:#184baf;color:white;border-color:#184baf}.selection-search{margin-top:7px;font-size:12px;width:100%}.notice{padding:8px 9px;border-radius:7px;margin:10px 0 0;font-size:12px;overflow-wrap:anywhere;background:#edf3ff;color:#254e8b}.notice.warning{background:#fff5df;color:#8a5d11}.notice.error{background:#fff0ed;color:#a23a2c}.notice.success{background:#eaf7f0;color:#1e754c}.notice[hidden]{display:none}.results-title{font-size:12px;color:var(--muted);margin:11px 0 7px}.empty{background:var(--surface);border-radius:8px;padding:13px 10px;color:var(--muted);font-size:12px}.result{border:1px solid var(--line);border-radius:9px;padding:10px;margin-bottom:9px;-webkit-user-select:text;user-select:text}.question{font-weight:600;line-height:1.6;overflow-wrap:anywhere;margin:0 0 7px;white-space:pre-wrap}.type{display:inline-block;font-size:10px;font-weight:500;color:#556984;background:#eff3f8;padding:1px 5px;border-radius:4px;margin-right:5px;vertical-align:1px}.answer{font-weight:600;color:#1b714a;background:#edf8f1;border-radius:5px;padding:6px 7px;overflow-wrap:anywhere}.options{padding:0;margin:7px 0;list-style:none;font-size:12px;display:grid;gap:4px}.option{padding:3px 6px;border-radius:4px;overflow-wrap:anywhere;white-space:pre-wrap}.option.correct{color:#196442;background:#edf8f1;font-weight:600}.sources{font-size:11px;color:var(--muted);white-space:pre-wrap;overflow-wrap:anywhere;margin-top:5px}.conflict{color:#9d6012;background:#fff5df;border-radius:5px;font-size:11px;padding:6px;margin:6px 0}.copy-result{margin-top:8px;font-size:11px}.pagination{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;color:var(--muted);margin:5px 0 0}.pagination button{font-size:11px;padding:4px 8px}.page-section{border-top:1px solid var(--line);margin-top:13px;padding-top:11px}.page-status{font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--muted)}.metrics{display:flex;flex-wrap:wrap;gap:4px 10px;font-size:11px;color:var(--muted);margin-top:6px}.unprocessed{font-size:11px;color:#8b621c;margin-top:7px}.unprocessed summary{cursor:pointer}.unprocessed-list{max-height:155px;overflow:auto;padding:7px;border-radius:6px;background:#fff7e8;margin-top:4px;display:grid;gap:5px;overflow-wrap:anywhere}.exports{display:flex;gap:6px;margin-top:9px}.exports button{flex:1;font-size:12px}.auto-export{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted);margin-top:8px}.auto-export input{margin:0}.busy{color:#83692c}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:420px){.body{padding:11px}.panel{width:350px}.role{padding:6px 5px}} `; root.appendChild(style); const visibilityStyle = doc.createElement('style'); visibilityStyle.textContent = '[hidden]{display:none!important}'; root.appendChild(visibilityStyle); const panel = el(doc, 'section', 'panel collapsed'); panel.setAttribute('aria-label', '谊安学助手 2.0'); const header = el(doc, 'div', 'header'); const brand = el(doc, 'span', 'brand', '题库助手'); const expand = el(doc, 'button', 'toggle', '展开'); expand.type = 'button'; expand.setAttribute('aria-label', '展开题库助手'); expand.setAttribute('aria-expanded', 'false'); header.append(brand, expand); const body = el(doc, 'div', 'body'); body.hidden = true; panel.append(header, body); root.appendChild(panel); function invoke(name) { if (destroyed || typeof callbacks[name] !== 'function') return; const args = Array.prototype.slice.call(arguments, 1); try { const result = callbacks[name].apply(null, args); if (result && typeof result.catch === 'function') result.catch(function (error) { setNotice(error && error.message || '操作失败,请重试。', 'error'); }); return result; } catch (error) { setNotice(error && error.message || '操作失败,请重试。', 'error'); } } function setNotice(message, level) { notice.textContent = plain(message); notice.className = 'notice ' + (['warning', 'error', 'success'].includes(level) ? level : 'info'); notice.hidden = !message; } function togglePanel() { state.expanded = !state.expanded; refresh(); } expand.addEventListener('click', togglePanel); brand.style.cursor = 'pointer'; brand.addEventListener('click', togglePanel); const switches = el(doc, 'div', 'switches'); function makeSwitch(labelText, callback, container) { const label = el(doc, 'label', 'switch-row'); const text = el(doc, 'span', '', labelText); const input = el(doc, 'input', 'switch'); input.type = 'checkbox'; input.setAttribute('role', 'switch'); input.setAttribute('aria-label', labelText); label.append(text, input); (container || switches).appendChild(label); input.addEventListener('change', callback); return { input: input, label: label }; } const autoSwitch = makeSwitch('自动选择答案', function () { state.autoAnswer = autoSwitch.input.checked; invoke('onAutoChange', state.autoAnswer); refresh(); }); const copySwitch = makeSwitch('允许复制文字', function () { state.allowCopy = copySwitch.input.checked; invoke('onCopyChange', state.allowCopy); refresh(); }); const answerHint = el(doc, 'p', 'hint'); switches.appendChild(answerHint); body.appendChild(switches); const bankSection = el(doc, 'div', 'section'); const importRow = el(doc, 'div', 'import-row'); const importButton = el(doc, 'button', '', '导入题库'); importButton.type = 'button'; const fileInput = el(doc, 'input'); fileInput.type = 'file'; fileInput.accept = '.zip,.csv,.json,application/zip,text/csv,application/json'; fileInput.multiple = true; fileInput.hidden = true; const bankInfo = el(doc, 'span', 'bank-info'); importRow.append(importButton, bankInfo, fileInput); const storageInfo = el(doc, 'p', 'storage'); bankSection.append(importRow, storageInfo); body.appendChild(bankSection); importButton.addEventListener('click', function () { fileInput.click(); }); fileInput.addEventListener('change', function () { const files = Array.from(fileInput.files || []); if (files.length) invoke('onImport', files); fileInput.value = ''; }); const searchSection = el(doc, 'div', 'section'); searchSection.appendChild(el(doc, 'h2', 'section-title', '搜索题库')); const roles = el(doc, 'div', 'roles'); roles.setAttribute('role', 'radiogroup'); roles.setAttribute('aria-label', '选择岗位'); const roleInputs = {}; const roleLabels = {}; const roleCounters = {}; ROLES.forEach(function (role) { const label = el(doc, 'label', 'role'); const input = el(doc, 'input'); input.type = 'radio'; input.name = 'yian-search-role'; input.value = role; input.setAttribute('aria-label', role); const counter = el(doc, 'span', 'role-count'); label.append(input, el(doc, 'span', '', role), counter); roles.appendChild(label); roleInputs[role] = input; roleLabels[role] = label; roleCounters[role] = counter; input.addEventListener('change', function () { if (!input.checked) return; const query = searchInput.value; state.role = role; state.course = ''; currentOffset = 0; refresh(); invoke('onRoleChange', role); if (normalized(query)) invoke('onSearch', query, { offset: 0 }); }); }); searchSection.appendChild(roles); const courseRow = el(doc, 'label', 'course-row'); const courseSelect = el(doc, 'select'); courseSelect.setAttribute('aria-label', '选择课程'); courseRow.append(el(doc, 'span', '', '课程'), courseSelect); courseSelect.addEventListener('change', function () { const query = searchInput.value; state.course = courseSelect.value; currentOffset = 0; invoke('onCourseChange', state.course); if (normalized(query)) invoke('onSearch', query, { offset: 0 }); }); searchSection.appendChild(courseRow); const searchRow = el(doc, 'div', 'search-row'); const searchInput = el(doc, 'input'); searchInput.type = 'text'; searchInput.placeholder = '输入题干、关键词或选项文字…'; searchInput.setAttribute('aria-label', '搜索题目'); const searchButton = el(doc, 'button', 'primary', '搜索'); searchButton.type = 'button'; searchRow.append(searchInput, searchButton); searchSection.appendChild(searchRow); const selectionButton = el(doc, 'button', 'selection-search', '用选中文字搜索'); selectionButton.type = 'button'; searchSection.appendChild(selectionButton); const notice = el(doc, 'div', 'notice'); notice.hidden = true; notice.setAttribute('role', 'status'); notice.setAttribute('aria-live', 'polite'); searchSection.appendChild(notice); const resultsTitle = el(doc, 'p', 'results-title'); const results = el(doc, 'div', 'results'); const pagination = el(doc, 'div', 'pagination'); const previous = el(doc, 'button', '', '上一页'); const pageLabel = el(doc, 'span'); const next = el(doc, 'button', '', '下一页'); previous.type = next.type = 'button'; pagination.append(previous, pageLabel, next); pagination.hidden = true; searchSection.append(resultsTitle, results, pagination); body.appendChild(searchSection); function search(offset) { if (!state.role) { setNotice('请先选择岗位:主操、班长或副操。', 'warning'); return; } if (!normalized(searchInput.value)) { setNotice('请输入题干、关键词或选项文字。', 'warning'); searchInput.focus(); return; } currentOffset = Math.max(0, Number(offset) || 0); invoke('onSearch', searchInput.value, { offset: currentOffset }); } searchButton.addEventListener('click', function () { search(0); }); searchInput.addEventListener('keydown', function (event) { if (event.key === 'Enter' && !event.isComposing) { event.preventDefault(); search(0); } }); previous.addEventListener('click', function () { search(Math.max(0, currentOffset - PAGE_SIZE)); }); next.addEventListener('click', function () { search(currentOffset + PAGE_SIZE); }); function rememberSelection() { const selected = global.getSelection && global.getSelection(); if (!selected || !selected.anchorNode || !selected.focusNode) return; if (host.contains(selected.anchorNode) || host.contains(selected.focusNode) || root.contains(selected.anchorNode) || root.contains(selected.focusNode)) return; const text = selected.toString().trim(); if (text) selectionText = text; } doc.addEventListener('selectionchange', rememberSelection); selectionButton.addEventListener('pointerdown', rememberSelection); selectionButton.addEventListener('click', function () { rememberSelection(); if (!state.role) { setNotice('请先选择岗位:主操、班长或副操。', 'warning'); return; } if (!selectionText) { setNotice('请先在网页正文选中题目文字。', 'warning'); return; } searchInput.value = selectionText; currentOffset = 0; if (typeof callbacks.onSelectionSearch === 'function') invoke('onSelectionSearch', selectionText); else search(0); }); const pageSection = el(doc, 'div', 'page-section'); pageSection.appendChild(el(doc, 'h2', 'section-title', '当前练习')); const pageStatus = el(doc, 'div', 'page-status'); const metrics = el(doc, 'div', 'metrics'); const unprocessed = el(doc, 'details', 'unprocessed'); const unprocessedSummary = el(doc, 'summary', '', '查看未处理题目'); const unprocessedList = el(doc, 'div', 'unprocessed-list'); unprocessed.append(unprocessedSummary, unprocessedList); unprocessed.hidden = true; pageSection.append(pageStatus, metrics, unprocessed); body.appendChild(pageSection); const exportSection = el(doc, 'section', 'page-section export-section'); exportSection.setAttribute('aria-label', '题库导出设置'); const exportSwitch = makeSwitch('导出题库', function () { state.exportEnabled = exportSwitch.input.checked; refresh(); invoke('onExportEnabledChange', state.exportEnabled); }, exportSection); const exportContent = el(doc, 'div', 'export-content'); exportContent.id = 'yian-export-content'; exportContent.hidden = true; exportSwitch.input.setAttribute('aria-controls', exportContent.id); exportContent.appendChild(el(doc, 'p', 'hint', '开启后,题目和答案完整时自动导出 CSV;也可点击下方按钮重新导出。关闭后停止后续导出。')); const exportRow = el(doc, 'div', 'exports'); const exportButtons = []; ['csv', 'json', 'txt'].forEach(function (format) { const button = el(doc, 'button', '', '导出 ' + format.toUpperCase()); button.type = 'button'; button.addEventListener('click', function () { if (state.exportEnabled && state.canExport) invoke('onExport', format); }); exportRow.appendChild(button); exportButtons.push(button); }); exportContent.appendChild(exportRow); exportSection.appendChild(exportContent); body.appendChild(exportSection); function autoUnavailableReason() { if (!state.role) return '请选择岗位后再开启自动选择。'; if (!(Number(state.roleCounts && state.roleCounts[state.role]) > 0)) return '请导入包含' + state.role + '题目的题库。'; if (!state.canAnswer) return state.pageStatus || '当前页面未识别到支持的练习题目。'; return ''; } autoSwitch.label.addEventListener('pointerdown', function () { const reason = autoUnavailableReason(); if (!state.autoAnswer && reason) setNotice(reason, 'warning'); }); let courseSignature = ''; function refresh() { if (destroyed) return; panel.classList.toggle('collapsed', !state.expanded); body.hidden = !state.expanded; brand.textContent = state.expanded ? '谊安学助手 2.0' : '题库助手'; expand.textContent = state.expanded ? '收起' : '展开'; expand.setAttribute('aria-label', state.expanded ? '收起题库助手' : '展开题库助手'); expand.setAttribute('aria-expanded', String(Boolean(state.expanded))); const unavailable = autoUnavailableReason(); autoSwitch.input.checked = Boolean(state.autoAnswer); autoSwitch.input.disabled = !state.autoAnswer && (Boolean(unavailable) || Boolean(state.loading)); autoSwitch.label.title = unavailable; copySwitch.input.checked = Boolean(state.allowCopy); answerHint.textContent = unavailable || (state.autoAnswer ? '正在自动选择;关闭即可停止后续操作。' : '开启后选择已匹配的答案,交卷由你操作。'); bankInfo.textContent = state.loading ? '正在读取题库…' : '已加载 ' + Number(state.bankTotal || 0).toLocaleString('zh-CN') + ' 条' + (state.role ? ' · ' + state.role + ' ' + Number(state.roleCounts && state.roleCounts[state.role] || 0).toLocaleString('zh-CN') + ' 条' : ''); bankInfo.classList.toggle('busy', Boolean(state.loading)); importButton.disabled = Boolean(state.loading); storageInfo.textContent = state.storageMode === 'origin' ? '此模式仅在当前网站保存题库和设置。' : ''; storageInfo.hidden = !storageInfo.textContent; ROLES.forEach(function (role) { roleInputs[role].checked = state.role === role; roleLabels[role].classList.toggle('active', state.role === role); const count = Number(state.roleCounts && state.roleCounts[role] || 0); roleCounters[role].textContent = count ? '(' + count.toLocaleString('zh-CN') + ')' : ''; }); const availableCourses = (state.courses || []).map(function (course) { return typeof course === 'string' ? { value: course, label: course } : { value: plain(course.value == null ? (course.course || course.name) : course.value), label: plain(course.label || course.name || course.course || course.value) }; }); const signature = JSON.stringify(availableCourses); if (signature !== courseSignature) { courseSignature = signature; courseSelect.replaceChildren(); const all = el(doc, 'option', '', '全部课程'); all.value = ''; courseSelect.appendChild(all); availableCourses.forEach(function (course) { const option = el(doc, 'option', '', course.label); option.value = course.value; courseSelect.appendChild(option); }); } courseSelect.value = state.course || ''; courseSelect.disabled = !state.role || Boolean(state.loading); searchButton.disabled = Boolean(state.loading); selectionButton.disabled = Boolean(state.loading); pageStatus.textContent = state.pageStatus || '等待识别练习页面'; metrics.replaceChildren(); const metricLabels = { selected: '已选择', pending: '待确认', missing: '未匹配', conflict: '冲突', locked: '已锁定', unsupported: '需人工核对', failed: '失败', skipped: '已跳过' }; Object.keys(metricLabels).forEach(function (key) { if (state.metrics && state.metrics[key] != null) metrics.appendChild(el(doc, 'span', '', metricLabels[key] + ' ' + Number(state.metrics[key] || 0))); }); const notProcessed = (state.metrics && state.metrics.details || []).filter(function (item) { return item.status !== 'selected'; }); unprocessed.hidden = !notProcessed.length; unprocessedSummary.textContent = '查看未处理题目(' + notProcessed.length + ')'; unprocessedList.replaceChildren(); notProcessed.forEach(function (item) { unprocessedList.appendChild(el(doc, 'div', '', '第 ' + item.number + ' 题 · ' + (metricLabels[item.status] || '已跳过') + ':' + (item.reason || '请人工核对。'))); }); exportSwitch.input.checked = Boolean(state.exportEnabled); exportContent.hidden = !state.exportEnabled; exportButtons.forEach(function (button) { button.disabled = !state.exportEnabled || !state.canExport; }); previous.disabled = currentOffset === 0 || Boolean(state.loading); next.disabled = currentOffset + PAGE_SIZE >= resultTotal || Boolean(state.loading); } function optionsOf(item) { if (Array.isArray(item.options)) return item.options.map(function (option, index) { return typeof option === 'string' ? { label: String.fromCharCode(65 + index), text: option } : { label: plain(option.label || option.key || String.fromCharCode(65 + index)), text: plain(option.text == null ? option.value : option.text), correct: option.correct }; }); return Object.keys(item.options || {}).map(function (label) { return { label: label, text: plain(item.options[label]) }; }); } function sourcesOf(item) { const entries = Array.isArray(item.sources) && item.sources.length ? item.sources : [{ role: item.role || state.role, course: item.course || item.title || item.bankTitle || '', file: item.filename || '' }]; return Array.from(new Set(entries.map(function (source) { if (typeof source === 'string') return source; return [source.role || item.role, source.course || source.title || source.bankTitle || source.name, source.filename || source.file].filter(Boolean).join(' / '); }).filter(Boolean))); } function answerInfo(item, options) { const answer = plain(item.answer == null ? item.correctAnswer : item.answer); const letters = plain(item.answerLabels || item.correctLabels || answer).replace(/[\s,,、;;.。]/g, '').toUpperCase(); const texts = Array.isArray(item.answerTexts) ? item.answerTexts.map(normalized) : []; const correctOptions = options.filter(function (option) { return option.correct || /^[A-Z]+$/.test(letters) && letters.includes(option.label.toUpperCase()) || texts.includes(normalized(option.text)); }); const description = item.answerText || correctOptions.map(function (option) { return option.label + ' — ' + (option.text || '[图片选项]'); }).join(';') || texts.join(';'); return { answer: answer, description: plain(description), correctOptions: correctOptions }; } async function copyText(value) { const legacyClipboard = typeof GM_setClipboard === 'function' ? GM_setClipboard : global.GM_setClipboard; const modernGM = typeof GM !== 'undefined' ? GM : global.GM; if (typeof legacyClipboard === 'function') { await legacyClipboard(value, 'text'); return; } if (modernGM && typeof modernGM.setClipboard === 'function') { await modernGM.setClipboard(value, 'text'); return; } if (global.navigator.clipboard && typeof global.navigator.clipboard.writeText === 'function') { try { await global.navigator.clipboard.writeText(value); return; } catch (_) { /* 再尝试浏览器复制命令。 */ } } const textarea = el(doc, 'textarea'); textarea.value = value; textarea.style.cssText = 'position:fixed;left:0;top:0;opacity:0;width:1px;height:1px;'; root.appendChild(textarea); textarea.focus(); textarea.select(); let copied = false; try { copied = doc.execCommand('copy'); } finally { textarea.remove(); } if (!copied) throw new Error('浏览器未允许复制,请选中结果文字手动复制。'); } function renderSearch(result) { if (destroyed) return; result = result || {}; const items = (result.items || []).slice(0, PAGE_SIZE); resultTotal = Number(result.total == null ? items.length : result.total) || 0; currentOffset = Math.max(0, Number(result.offset == null ? currentOffset : result.offset) || 0); resultsTitle.textContent = state.role ? '找到 ' + resultTotal.toLocaleString('zh-CN') + ' 道题 · 当前岗位:' + state.role : '先选择岗位,再搜索题目'; results.replaceChildren(); if (!items.length) results.appendChild(el(doc, 'div', 'empty', state.role ? '当前岗位未找到题目,可调整关键词、课程或切换岗位。' : '选择主操、班长或副操,导入题库后即可查找答案。')); items.forEach(function (item) { const card = el(doc, 'article', 'result'); const title = el(doc, 'p', 'question'); const type = TYPE_NAMES[item.type] || item.type || '题目'; title.append(el(doc, 'span', 'type', type), doc.createTextNode(plain(item.question == null ? item.stem : item.question))); card.appendChild(title); const options = optionsOf(item); const info = answerInfo(item, options); card.appendChild(el(doc, 'div', 'answer', '正确答案:' + (info.answer || '见下方选项') + (info.description ? ' · ' + info.description : ''))); const list = el(doc, 'ul', 'options'); options.forEach(function (option) { const correct = info.correctOptions.includes(option); const row = el(doc, 'li', 'option' + (correct ? ' correct' : ''), option.label + '. ' + (option.text || '[图片选项]') + (correct ? ' ✓' : '')); list.appendChild(row); }); card.appendChild(list); if (item.conflict || item.conflicts && item.conflicts.length || item.ambiguous) card.appendChild(el(doc, 'div', 'conflict', '此题存在不同答案,请核对课程来源;自动选择会跳过不明确的匹配。')); const sources = sourcesOf(item); card.appendChild(el(doc, 'div', 'sources', '来源:' + (sources.join('\n') || '未知'))); const copyButton = el(doc, 'button', 'copy-result', '复制题目与答案'); copyButton.type = 'button'; copyButton.addEventListener('click', async function () { const content = ['[' + type + '] ' + plain(item.question == null ? item.stem : item.question)].concat(options.map(function (option) { return option.label + '. ' + (option.text || '[图片选项]'); }), ['正确答案:' + info.answer, info.description ? '对应选项:' + info.description : '', '来源:' + sources.join(';')]).filter(Boolean).join('\n'); try { await copyText(content); setNotice('题目与答案已复制。', 'success'); } catch (error) { setNotice(error.message, 'error'); } }); card.appendChild(copyButton); results.appendChild(card); }); pagination.hidden = resultTotal <= PAGE_SIZE; pageLabel.textContent = resultTotal ? (currentOffset + 1) + '–' + Math.min(currentOffset + items.length, resultTotal) + ' / ' + resultTotal : '0 / 0'; refresh(); } function setState(partial) { if (destroyed) return; Object.assign(state, partial || {}); refresh(); } function destroy() { if (destroyed) return; destroyed = true; doc.removeEventListener('selectionchange', rememberSelection); host.remove(); } (doc.body || doc.documentElement).appendChild(host); renderSearch({ items: [], total: 0 }); refresh(); return { setState: setState, renderSearch: renderSearch, setNotice: setNotice, destroy: destroy, host: host, root: root }; } global.YianAssistantPanel = { create: create }; })(typeof window !== 'undefined' ? window : globalThis);