feat: add draggable panel and restrict assistant to site directory
This commit is contained in:
+33
-7
@@ -4,15 +4,17 @@
|
||||
const core = window.QuestionBankCore;
|
||||
const store = window.YianBankStore;
|
||||
const matcher = window.YianAnswerMatcher;
|
||||
const config = { autoAnswer: false, allowCopy: true, exportEnabled: false, role: '', course: '' };
|
||||
const config = { autoAnswer: false, allowCopy: true, exportEnabled: false, role: '', course: '', panelPosition: null };
|
||||
let snapshot = { records: [], counts: {}, total: 0, revision: '', storage: 'origin' };
|
||||
let loading = true, disposed = false, page = null, previous = null;
|
||||
let loading = true, disposed = false, scopedOut = false, page = null, previous = null;
|
||||
let searchQuery = '', runKey = '', debounceTimer;
|
||||
const exported = new Set();
|
||||
const settleMs = 1200;
|
||||
const copy = window.YianCopyController.create();
|
||||
const emptyMetrics = () => ({ selected: 0, pending: 0, missing: 0, conflict: 0, locked: 0, failed: 0, unsupported: 0, total: 0 });
|
||||
let metrics = emptyMetrics();
|
||||
let positionRevision = 0;
|
||||
const inScope = () => YianSiteScope.isAllowedURL(location.href);
|
||||
|
||||
function visible(node) {
|
||||
if (!node || node.closest('[hidden],.hidden')) return false;
|
||||
@@ -59,7 +61,7 @@
|
||||
const route = routeInfo();
|
||||
const bank = route.root ? core.extract(document, route.sourceUrl) : { title: '', questions: [] };
|
||||
const signature = route.root ? structureSignature(bank) : '';
|
||||
return { ready: !!page && page.ready && !loading && page.key === route.key
|
||||
return { ready: inScope() && !!page && page.ready && !loading && page.key === route.key
|
||||
&& page.root === route.root && page.signature === signature,
|
||||
practice: route.practice, exam: route.exam, mode: route.mode, local: route.local, role: config.role, course: bank.title,
|
||||
sourceUrl: route.sourceUrl, routeKey: route.key, root: route.root,
|
||||
@@ -80,7 +82,7 @@
|
||||
}
|
||||
function refreshPanel(extra) {
|
||||
panel.setState(Object.assign({ autoAnswer: config.autoAnswer, allowCopy: config.allowCopy,
|
||||
exportEnabled: config.exportEnabled, role: config.role, course: config.course, courses: courses(),
|
||||
exportEnabled: config.exportEnabled, position: config.panelPosition, role: config.role, course: config.course, courses: courses(),
|
||||
bankTotal: snapshot.total, roleCounts: snapshot.counts, storageMode: snapshot.storage, loading, metrics }, extra || {}));
|
||||
}
|
||||
function performSearch(query = searchQuery, options = {}) {
|
||||
@@ -111,7 +113,7 @@
|
||||
refreshPanel(); tick(); return config.autoAnswer;
|
||||
}
|
||||
function setCopy(enabled) {
|
||||
config.allowCopy = !!enabled; copy.setEnabled(config.allowCopy); refreshPanel(); saveSetting('allowCopy', config.allowCopy);
|
||||
config.allowCopy = !!enabled; copy.setEnabled(config.allowCopy && inScope()); refreshPanel(); saveSetting('allowCopy', config.allowCopy);
|
||||
}
|
||||
async function importFiles(files) {
|
||||
runner.stop('正在更新题库'); loading = true; runKey = ''; refreshPanel(); panel.setNotice('正在解析题库,请稍候…');
|
||||
@@ -125,6 +127,7 @@
|
||||
} finally { loading = false; refreshPanel(); performSearch(searchQuery, { quiet: true }); tick(); }
|
||||
}
|
||||
function exportCurrent(format = 'csv') {
|
||||
if (!inScope()) throw new Error('当前页面不在谊安学脚本生效范围内');
|
||||
if (!config.exportEnabled) throw new Error('请先开启“导出题库”开关');
|
||||
const route = routeInfo();
|
||||
if (!route.practice) throw new Error('请先进入支持的练习页面');
|
||||
@@ -144,6 +147,7 @@
|
||||
}
|
||||
const panel = window.YianAssistantPanel.create({
|
||||
onAutoChange: setAutoAnswer, onCopyChange: setCopy,
|
||||
onPositionChange: position => { positionRevision++; config.panelPosition = position; saveSetting('panelPosition', position); },
|
||||
onRoleChange: role => { setRole(role).catch(error => panel.setNotice(error.message, 'error')); },
|
||||
onCourseChange: course => { config.course = course; performSearch(); },
|
||||
onImport: files => { importFiles(files).catch(() => {}); }, onSearch: performSearch,
|
||||
@@ -154,6 +158,21 @@
|
||||
|
||||
function tick() {
|
||||
if (disposed) return;
|
||||
if (!inScope()) {
|
||||
if (!scopedOut) {
|
||||
scopedOut = true;
|
||||
config.autoAnswer = false; config.exportEnabled = false; runKey = '';
|
||||
runner.stop('页面已离开脚本生效范围'); copy.setEnabled(false);
|
||||
panel.host.style.setProperty('display', 'none', 'important');
|
||||
previous = page; page = null; metrics = emptyMetrics();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (scopedOut) {
|
||||
scopedOut = false;
|
||||
panel.host.style.setProperty('display', 'block', 'important');
|
||||
copy.setEnabled(config.allowCopy);
|
||||
}
|
||||
const route = routeInfo();
|
||||
if (!route.root) {
|
||||
if (runner.running) runner.stop('当前页面未识别到练习题目');
|
||||
@@ -225,12 +244,19 @@
|
||||
runner.destroy(); copy.destroy(); panel.destroy();
|
||||
}
|
||||
const ready = (async () => {
|
||||
const positionRevisionAtRead = positionRevision;
|
||||
try {
|
||||
const values = await Promise.all([store.load(), store.getSetting('role', ''), store.getSetting('allowCopy', true)]);
|
||||
const values = await Promise.all([store.load(), store.getSetting('role', ''), store.getSetting('allowCopy', true), store.getSetting('panelPosition', null)]);
|
||||
snapshot = values[0]; config.role = store.roles.includes(values[1]) ? values[1] : '';
|
||||
config.allowCopy = values[2] !== false;
|
||||
const position = values[3];
|
||||
// A drag during asynchronous storage loading wins over the older saved position.
|
||||
if (positionRevision === positionRevisionAtRead) {
|
||||
config.panelPosition = position && typeof position.left === 'number' && Number.isFinite(position.left)
|
||||
&& typeof position.top === 'number' && Number.isFinite(position.top) ? { left: position.left, top: position.top } : null;
|
||||
}
|
||||
} catch (error) { panel.setNotice('题库读取失败:' + error.message + '。可重新导入题库。', 'error'); }
|
||||
loading = false; copy.setEnabled(config.allowCopy); refreshPanel(); tick(); return snapshot;
|
||||
loading = false; copy.setEnabled(config.allowCopy && inScope()); refreshPanel(); tick(); return snapshot;
|
||||
})();
|
||||
const api = Object.freeze({ ready, importFiles, setRole, setAutoAnswer, setCopy, setExportEnabled, search: performSearch, getContext,
|
||||
getSnapshot: () => snapshot, getState: () => ({ ...config, metrics, pageReady: !!page && page.ready }),
|
||||
|
||||
Reference in New Issue
Block a user