Files
yianxue-assistant/assistant-panel.js
T

433 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 独立题库助手面板:页面正文与面板样式互不影响。 */
(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);