// ==UserScript== // @name CAU 仿真实验与预习题增强 // @namespace https://example.com/ // @version 0.7.1 // @description 直接解析学校仿真实验 XML 评分明细,选课预习题自动填写 // @match http://211.82.81.133:7100/* // @match http://211.82.81.133:7101/* // @match http://211.82.81.133:7102/* // @match http://211.82.81.133:7104/* // @grant GM_getValue // @grant GM_setValue // @grant GM.getValue // @grant GM.setValue // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function () { "use strict"; const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const CONFIG = { firstRunNotice: { storageKey: "tm-cau-lab-free-notice-seen", overlayId: "tm-cau-free-notice", styleId: "tm-cau-free-notice-style", scriptCatUrl: "https://scriptcat.org/zh-CN/script-show-page/5909", }, virtualLab: { pagePath: "/ReportStudent/SLabStatisticPage/SStuStatisticPage", styleId: "tm-vlab-style", modalId: "tm-vlab-detail-modal", modalContentId: "tm-vlab-detail-content", headerAttr: "data-vlab-header", cellAttr: "data-vlab-cell", buttonAttr: "data-vlab-action", keyAttr: "data-vlab-key", syncDebounceMs: 180, }, }; const state = { firstRunNoticeChecked: false }; const virtualLabState = { initialized: false, eventsBound: false, viewMounted: false, syncTimer: null, bodyObserver: null, detailCache: new Map(), pendingDetail: new Map(), activeDetailRequest: null, }; function maybeShowFirstRunNotice() { if (state.firstRunNoticeChecked || !isFirstRunNoticeHost()) { return; } state.firstRunNoticeChecked = true; readFirstRunNoticeSeen() .then((seen) => { if (!seen) { showFirstRunNotice(); } }) .catch((error) => { console.warn("[cau-lab] failed to read first-run notice state", error); showFirstRunNotice(); }); } function isFirstRunNoticeHost() { return /^211\.82\.81\.133:710[0124]$/.test(location.host); } function readFirstRunNoticeSeen() { const key = CONFIG.firstRunNotice.storageKey; if (typeof GM_getValue === "function") { return Promise.resolve(GM_getValue(key, false)).then(normalizeStoredBoolean); } if (typeof GM !== "undefined" && GM && typeof GM.getValue === "function") { return Promise.resolve(GM.getValue(key, false)).then(normalizeStoredBoolean); } return Promise.resolve(readLocalFirstRunNoticeValue(key)); } function writeFirstRunNoticeSeen() { const key = CONFIG.firstRunNotice.storageKey; if (typeof GM_setValue === "function") { return Promise.resolve(GM_setValue(key, true)); } if (typeof GM !== "undefined" && GM && typeof GM.setValue === "function") { return Promise.resolve(GM.setValue(key, true)); } return Promise.resolve(writeLocalFirstRunNoticeValue(key)); } function normalizeStoredBoolean(value) { return value === true || value === "true" || value === "1"; } function readLocalFirstRunNoticeValue(key) { try { return normalizeStoredBoolean(window.localStorage.getItem(key)); } catch (error) { return false; } } function writeLocalFirstRunNoticeValue(key) { try { window.localStorage.setItem(key, "1"); } catch (error) { console.warn("[cau-lab] failed to save first-run notice state", error); } } function showFirstRunNotice() { if (!document.body) { document.addEventListener("DOMContentLoaded", showFirstRunNotice, { once: true }); return; } if (document.getElementById(CONFIG.firstRunNotice.overlayId)) { return; } ensureFirstRunNoticeStyles(); const overlay = document.createElement("div"); overlay.id = CONFIG.firstRunNotice.overlayId; overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.innerHTML = `

脚本免费说明

本脚本免费发布,唯一公开发布页为 ScriptCat 脚本页。

如果你是从其他付费渠道获得本脚本,说明你被骗了,请立即停止付款并保留相关记录。

`; const closeButton = overlay.querySelector(".tm-cau-free-notice-button"); closeButton.addEventListener("click", () => { overlay.remove(); writeFirstRunNoticeSeen().catch((error) => { console.warn("[cau-lab] failed to write first-run notice state", error); }); }); document.body.appendChild(overlay); closeButton.focus(); } function ensureFirstRunNoticeStyles() { if (document.getElementById(CONFIG.firstRunNotice.styleId)) { return; } const style = document.createElement("style"); style.id = CONFIG.firstRunNotice.styleId; style.textContent = ` #${CONFIG.firstRunNotice.overlayId} { position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; padding: 20px; background: rgba(15, 23, 42, 0.48); box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; } #${CONFIG.firstRunNotice.overlayId} .tm-cau-free-notice-dialog { width: min(460px, 100%); box-sizing: border-box; border-radius: 8px; background: #fff; color: #1f2937; padding: 22px 24px 20px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.26); line-height: 1.65; } #${CONFIG.firstRunNotice.overlayId} h2 { margin: 0 0 14px; color: #111827; font-size: 20px; font-weight: 700; line-height: 1.35; } #${CONFIG.firstRunNotice.overlayId} p { margin: 0 0 12px; font-size: 15px; } #${CONFIG.firstRunNotice.overlayId} a { color: #0f766e; font-weight: 600; text-decoration: none; } #${CONFIG.firstRunNotice.overlayId} a:hover { text-decoration: underline; } #${CONFIG.firstRunNotice.overlayId} .tm-cau-free-notice-button { display: block; width: 100%; margin-top: 18px; border: 0; border-radius: 6px; background: #0f766e; color: #fff; cursor: pointer; padding: 10px 14px; font-size: 15px; font-weight: 600; line-height: 1.4; } #${CONFIG.firstRunNotice.overlayId} .tm-cau-free-notice-button:hover { background: #0d665f; } #${CONFIG.firstRunNotice.overlayId} .tm-cau-free-notice-button:focus { outline: 3px solid rgba(13, 148, 136, 0.28); outline-offset: 2px; } `; (document.head || document.documentElement).appendChild(style); } // LabDate XML already contains the platform's grading results; do not re-grade it. function resolveVirtualLabXmlUrl(path) { const url = new URL(String(path || "").trim().replace(/\\/g, "/"), `${location.origin}/`); if (url.origin !== location.origin || !/^\/Upload\/LabDate\/.+\.xml$/i.test(url.pathname) || url.search || url.hash) { throw new Error("实验记录地址不是本站的 Upload/LabDate XML。"); } return url.href; } async function fetchVirtualLabXml(path) { const controller = new AbortController(); const timer = window.setTimeout(() => controller.abort(), 30000); try { const response = await fetch(resolveVirtualLabXmlUrl(path), { credentials: "same-origin", cache: "no-store", signal: controller.signal, }); if (!response.ok) throw new Error(`学校 XML 请求失败(HTTP ${response.status})。`); return await response.text(); } catch (error) { if (error.name === "AbortError") throw new Error("读取学校 XML 超时,请稍后重试。"); throw error; } finally { window.clearTimeout(timer); } } function renderVirtualLabXml(xmlText) { const xml = new DOMParser().parseFromString(xmlText, "application/xml"); if (xml.querySelector("parsererror") || xml.documentElement.nodeName !== "ExamPaper") { throw new Error("学校未返回有效的 ExamPaper XML,请检查登录状态或记录地址。"); } const child = (node, name) => Array.from(node.children).find((entry) => entry.nodeName === name); const text = (node, name) => child(node, name)?.textContent.trim() || ""; const scriptText = (node) => { const script = child(node, "Script"); if (!script) return ""; const paragraphs = Array.from(script.querySelectorAll("Paragraph")); return paragraphs.length ? paragraphs.map((p) => Array.from(p.querySelectorAll("Inline")).map((run) => run.getAttribute("Text") || run.textContent).join("")).join("\n") : script.textContent.trim(); }; const valueHtml = (value, isTable) => { // Preserve numeric strings (0.20, 1.00) and empty cells verbatim. if (isTable && /^\([\s\S]*\)$/.test(value)) { const rows = value.slice(1, -1).split(/\)\s*[;;]\s*\(/); return `
${rows.map((row, i) => `${row.split(",").map((cell) => `<${i ? "td" : "th"}>${escapeHtml(cell)}`).join("")}`).join("")}
`; } return `${escapeHtml(value || "未提供")}`; }; const answerHtml = (value, isTable, info) => { if (!isTable && /^(true|false)$/i.test(value)) { return valueHtml(info && info !== "原值" ? info : (/^true$/i.test(value) ? "是" : "否"), false); } return valueHtml(value, isTable) + (info && info !== "原值" ? `${escapeHtml(info)}` : ""); }; const renderPara = (para) => { const isTable = text(para, "VarType") === "TList"; const real = text(para, "RealResult"); const standard = text(para, "StdResult"); const studentInfo = text(para, "StudentResultShowInfo"); const standardInfo = text(para, "StdResultShowInfo"); const score = text(para, "RealScore"); const total = text(para, "TotalScore"); const teacher = text(para, "TeacherScore"); const lost = score !== "" && total !== "" && Number(score) < Number(total); return `

${escapeHtml(scriptText(para) || text(para, "Name") || "评分项")}

${escapeHtml(score || "—")} / ${escapeHtml(total || "—")}
填写值${answerHtml(real, isTable, studentInfo)}
参考值${answerHtml(standard, isTable, standardInfo)}
${teacher !== "" && teacher !== score ? `教师评分:${escapeHtml(teacher)}` : ""}
`; }; const questions = Array.from(xml.querySelectorAll("ExamPaper > Content > Question")); if (!questions.length) throw new Error("实验 XML 中没有评分题目。"); const scoreValue = (q, field) => { const score = child(q, "Score"); return score ? text(score, field) : ""; }; const sum = (field) => { const values = questions.map((q) => scoreValue(q, field)); return values.every((v) => v !== "" && Number.isFinite(Number(v))) ? String(values.reduce((total, v) => total + Number(v), 0)) : ""; }; const meta = { title: xml.querySelector("ExamPaper > Title > Name")?.textContent.trim() || "实验评分", actualScore: sum("RealScore"), totalScore: sum("Total"), }; const allParas = Array.from(xml.querySelectorAll("CheckPoint Para")).filter(p => !p.parentElement.closest("Para")); const lostCount = allParas.filter(p => text(p, "RealScore") !== "" && text(p, "TotalScore") !== "" && Number(text(p, "RealScore")) < Number(text(p, "TotalScore"))).length; const html = `
实验成绩
${escapeHtml(meta.actualScore || "—")}/ ${escapeHtml(meta.totalScore || "—")}
${allParas.length}评分项
${lostCount}未得满分
学校记录成绩
${questions.map((q) => { const paras = Array.from(q.querySelectorAll("CheckPoint Para")).filter(p => !p.parentElement.closest("Para")); const groups = new Map(); paras.forEach(p => { const key = p.closest("TestTarget"); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(p); }); return `
${questions.length > 1 ? `

${escapeHtml(text(q, "Name") || "实验内容")}

` : ""} ${scriptText(q) ? `
实验参数与说明

${escapeHtml(scriptText(q))}

` : ""} ${Array.from(groups, ([target, items], index) => `
${String(index + 1).padStart(2, "0")}

${escapeHtml(target ? scriptText(target) || "评分明细" : "评分明细")}

${items.length} 项
${items.map(renderPara).join("")}
`).join("") || '

此题未提供逐项评分。

'}
`; }).join("")}`; return { html, meta }; } function isVirtualLabStatisticPage() { return ( location.host === "211.82.81.133:7104" && location.pathname.indexOf(CONFIG.virtualLab.pagePath) !== -1 ); } function injectVirtualLabStyles() { if (document.getElementById(CONFIG.virtualLab.styleId)) { return; } const style = document.createElement("style"); style.id = CONFIG.virtualLab.styleId; style.textContent = ` td[${CONFIG.virtualLab.headerAttr}] { min-width: 240px; font-weight: 700; white-space: nowrap; } td[${CONFIG.virtualLab.cellAttr}] { min-width: 240px; vertical-align: middle; } .tm-vlab-inline { border: 1px solid #d9e6e0; border-radius: 10px; background: linear-gradient(180deg, #fcfefd 0%, #f4f9f6 100%); padding: 10px 12px; box-shadow: 0 4px 12px rgba(20, 62, 46, 0.06); } .tm-vlab-inline-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; } .tm-vlab-inline-title { font-size: 13px; font-weight: 700; color: #1f4739; } .tm-vlab-inline-score { font-size: 18px; font-weight: 800; color: #174535; white-space: nowrap; } .tm-vlab-pill { display: inline-flex; align-items: center; gap: 6px; border-radius: 999px; padding: 3px 9px; font-size: 11px; font-weight: 700; } .tm-vlab-pill.is-ready { background: #def3e7; color: #166a3f; } .tm-vlab-pill.is-loading { background: #fff4db; color: #9a5b00; } .tm-vlab-pill.is-error { background: #ffe3e3; color: #b42318; } .tm-vlab-inline-line { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; line-height: 1.45; } .tm-vlab-button { border: 1px solid #bfd6cd; background: #ffffff; color: #22473c; border-radius: 999px; padding: 6px 12px; cursor: pointer; font-size: 12px; font-weight: 700; } .tm-vlab-button:hover { background: #edf7f3; } .tm-vlab-button[disabled] { opacity: 0.55; cursor: not-allowed; } .tm-vlab-note { margin-top: 6px; color: #617972; font-size: 12px; line-height: 1.5; } .tm-vlab-note.is-error { color: #b42318; } #${CONFIG.virtualLab.modalId} { position: fixed; inset: 0; z-index: 100000; display: none; align-items: center; justify-content: center; } #${CONFIG.virtualLab.modalId}.is-open { display: flex; } #${CONFIG.virtualLab.modalId} .tm-vlab-overlay { position: absolute; inset: 0; background: rgba(16, 30, 25, 0.58); } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog { position: relative; width: min(1180px, calc(100vw - 48px)); max-height: calc(100vh - 48px); display: flex; flex-direction: column; border-radius: 18px; overflow: hidden; background: #ffffff; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28); } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-bottom: 1px solid #e6efeb; background: linear-gradient(180deg, #f9fcfb 0%, #f3f8f5 100%); } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-title { font-size: 18px; font-weight: 700; color: #1a3d31; } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-close { border: none; background: transparent; color: #33574b; cursor: pointer; font-size: 26px; line-height: 1; } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-body { overflow: auto; padding: 20px; background: #f7faf8; } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-loading { padding: 28px; text-align: center; color: #4c6660; } #${CONFIG.virtualLab.modalId} { font-family: "Segoe UI", "Microsoft YaHei", sans-serif; font-size: 14px; line-height: 1.6; color: #253449; } #${CONFIG.virtualLab.modalId} * { box-sizing: border-box; } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog { width: min(1120px, calc(100vw - 40px)); border-radius: 16px; } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-head { padding: 18px 26px; background: #fff; border-bottom: 1px solid #e7ebf0; } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-title { font-size: 18px; color: #172b3d; } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-body { padding: 24px; background: #f4f6f9; } #${CONFIG.virtualLab.modalContentId} h3, #${CONFIG.virtualLab.modalContentId} h4, #${CONFIG.virtualLab.modalContentId} p { margin: 0; line-height: 1.6; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-summary { display: flex; align-items: center; gap: 40px; padding: 20px 28px; background: #fff; border: 1px solid #e3e8ee; border-radius: 12px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-eyebrow { color: #627085; font-size: 12px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-total { display: flex; align-items: baseline; gap: 10px; font-variant-numeric: tabular-nums; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-total strong { font-size: 44px; line-height: 1.2; font-weight: 700; color: #163e51; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-total span { font-size: 18px; color: #8692a2; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-summary-stat { display: grid; gap: 2px; border-left: 1px solid #e7ebf0; padding-left: 30px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-summary-stat strong { font-size: 23px; font-weight: 600; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-summary-stat span, #${CONFIG.virtualLab.modalContentId} .tm-vlab-source { color: #738095; font-size: 12px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-source { margin-left: auto; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-parameters { margin: 14px 0 22px; color: #65758a; font-size: 13px; } #${CONFIG.virtualLab.modalContentId} summary { cursor: pointer; padding: 6px 0; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-parameters p { padding: 10px 14px; background: #eaf0f5; border-radius: 8px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-group { margin-top: 24px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-group-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-group-head h3 { font-size: 15px; font-weight: 600; flex: 1; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-group-head > span:last-child { color: #8692a2; font-size: 12px; white-space: nowrap; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-group-index { color: #2c6873; background: #e0ecee; padding: 2px 7px; border-radius: 5px; font-size: 12px; font-weight: 700; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-criterion { min-width: 0; padding: 16px 18px; background: white; border: 1px solid #e3e8ee; border-radius: 10px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-criterion.is-table { grid-column: 1 / -1; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-criterion-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 14px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-criterion h4 { font-size: 13px; font-weight: 600; color: #28394a; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-score-chip { flex-shrink: 0; font-size: 12px; padding: 2px 8px; border-radius: 5px; font-variant-numeric: tabular-nums; } #${CONFIG.virtualLab.modalContentId} .is-lost { background: #fff0ec; color: #b44c31; } #${CONFIG.virtualLab.modalContentId} .is-earned { background: #e9f4ee; color: #297254; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-compare { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-compare > div { min-width: 0; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-compare > div + div { border-left: 1px solid #edf0f4; padding-left: 16px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-label { display: block; color: #8490a1; font-size: 11px; margin-bottom: 4px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-value { white-space: pre-wrap; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; } #${CONFIG.virtualLab.modalContentId} small { display: block; color: #7c8899; font-size: 12px; margin-top: 4px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-table-wrap { overflow-x: auto; margin-top: 8px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-values { border-collapse: collapse; width: 100%; font-size: 12px; font-variant-numeric: tabular-nums; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-values th, #${CONFIG.virtualLab.modalContentId} .tm-vlab-values td { padding: 8px 10px; border: 1px solid #e6ebf0; text-align: right; white-space: nowrap; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-values th { background: #f4f7fa; color: #627085; font-weight: 500; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-values tr:nth-child(odd) td { background: #fafbfd; } @media (max-width: 700px) { #${CONFIG.virtualLab.modalId} .tm-vlab-dialog { width: calc(100vw - 16px); max-height: calc(100dvh - 16px); } #${CONFIG.virtualLab.modalId} .tm-vlab-dialog-body { padding: 14px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-summary { gap: 16px; padding: 16px; flex-wrap: wrap; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-summary-stat { padding-left: 14px; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-source { display: none; } #${CONFIG.virtualLab.modalContentId} .tm-vlab-grid, #${CONFIG.virtualLab.modalContentId} .is-table .tm-vlab-compare { grid-template-columns: 1fr; } #${CONFIG.virtualLab.modalContentId} .is-table .tm-vlab-compare > div + div { border-left: 0; padding-left: 0; } } `; document.head.appendChild(style); } function initVirtualLabEnhancer() { if (virtualLabState.initialized) { scheduleVirtualLabSync(); return; } virtualLabState.initialized = true; injectVirtualLabStyles(); ensureVirtualLabDetailModal(); bindVirtualLabEvents(); waitForVirtualLabView(0); if (document.body && !virtualLabState.bodyObserver) { virtualLabState.bodyObserver = new MutationObserver(scheduleVirtualLabSync); virtualLabState.bodyObserver.observe(document.body, { childList: true, subtree: true, }); } } function bindVirtualLabEvents() { if (virtualLabState.eventsBound) { return; } virtualLabState.eventsBound = true; document.addEventListener( "click", (event) => { const actionNode = event.target.closest(`[${CONFIG.virtualLab.buttonAttr}]`); if (!actionNode) { return; } const action = actionNode.getAttribute(CONFIG.virtualLab.buttonAttr); if (!action) { return; } event.preventDefault(); event.stopPropagation(); if (action === "close-detail") { closeVirtualLabDetailModal(); return; } const key = actionNode.getAttribute(CONFIG.virtualLab.keyAttr) || ""; if (!key) { return; } const item = findVirtualLabItemByKey(key); if (!item) { return; } if (action === "refresh-detail") { clearVirtualLabDetailCache(item); openVirtualLabDetail(item, true); } else if (action === "detail") { openVirtualLabDetail(item); } }, true ); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeVirtualLabDetailModal(); } }); } function waitForVirtualLabView(attempt) { if (!isVirtualLabStatisticPage()) { return; } if (pageWindow.view && typeof pageWindow.view.LabStatisticList === "function") { mountVirtualLabView(pageWindow.view); return; } if (attempt >= 80) { return; } window.setTimeout(() => waitForVirtualLabView(attempt + 1), 250); } function mountVirtualLabView(viewModel) { if (!viewModel) { return; } if (!virtualLabState.viewMounted) { virtualLabState.viewMounted = true; if ( viewModel.LabStatisticList && typeof viewModel.LabStatisticList.subscribe === "function" ) { viewModel.LabStatisticList.subscribe(() => scheduleVirtualLabSync()); } ["GetStuStatisticList", "GetLabStatisticList"].forEach((methodName) => { if ( typeof viewModel[methodName] === "function" && !viewModel[methodName].__tmWrapped ) { const original = viewModel[methodName]; const wrapped = function () { const result = original.apply(this, arguments); scheduleVirtualLabSync(); return result; }; wrapped.__tmWrapped = true; viewModel[methodName] = wrapped; } }); } scheduleVirtualLabSync(); } function scheduleVirtualLabSync() { if (!isVirtualLabStatisticPage()) { return; } window.clearTimeout(virtualLabState.syncTimer); virtualLabState.syncTimer = window.setTimeout( syncVirtualLabTable, CONFIG.virtualLab.syncDebounceMs ); } function syncVirtualLabTable() { if (!isVirtualLabStatisticPage()) { return; } if (!pageWindow.view || typeof pageWindow.view.LabStatisticList !== "function") { waitForVirtualLabView(0); return; } const list = pageWindow.view.LabStatisticList() || []; const tbody = document.querySelector('tbody[data-bind*="LabStatisticList"]'); if (!tbody) { return; } const table = tbody.closest("table"); if (!table) { return; } ensureVirtualLabHeader(table); const rows = Array.from(tbody.querySelectorAll(":scope > tr")); rows.forEach((row, index) => { const item = list[index]; if (!item) { return; } ensureVirtualLabCell(row, item); }); } function ensureVirtualLabHeader(table) { const headerRow = table.querySelector("thead tr"); if (!headerRow || headerRow.querySelector(`td[${CONFIG.virtualLab.headerAttr}]`)) { return; } const cell = document.createElement("td"); cell.setAttribute(CONFIG.virtualLab.headerAttr, "1"); cell.textContent = "评分详情"; headerRow.appendChild(cell); } function ensureVirtualLabCell(row, item) { let cell = row.querySelector(`td[${CONFIG.virtualLab.cellAttr}]`); if (!cell) { cell = document.createElement("td"); cell.setAttribute(CONFIG.virtualLab.cellAttr, "1"); row.appendChild(cell); } renderVirtualLabCell(cell, item); } function renderVirtualLabCell(cell, item) { const key = buildVirtualLabKey(item); const scoreValue = formatVirtualLabScore(item.LabScore); const hasRecord = !!cleanText(item.LabDateUrl); const signature = JSON.stringify([key, scoreValue, hasRecord]); if (cell.dataset.vlabSignature === signature) return; cell.dataset.vlabSignature = signature; cell.setAttribute(CONFIG.virtualLab.keyAttr, key); cell.innerHTML = `
${escapeHtml(scoreValue)}
`; } function formatVirtualLabScore(value) { const score = toNumber(value); return Number.isFinite(score) ? `${score} 分` : "未出分"; } function clearVirtualLabDetailCache(item) { const key = buildVirtualLabKey(item); virtualLabState.detailCache.delete(key); } function openVirtualLabDetail(item, forceRefresh) { const modal = ensureVirtualLabDetailModal(); const content = modal.querySelector(`#${CONFIG.virtualLab.modalContentId}`); if (!content) { return; } modal.classList.add("is-open"); const requestToken = {}; virtualLabState.activeDetailRequest = requestToken; content.innerHTML = '
正在读取学校 XML 并解析评分,请稍候...
'; const key = buildVirtualLabKey(item); loadVirtualLabDetail(item, forceRefresh) .then((detailRecord) => { if (virtualLabState.activeDetailRequest !== requestToken) return; content.innerHTML = detailRecord.html; const titleNode = modal.querySelector(".tm-vlab-dialog-title"); if (titleNode) { const startTime = cleanText(item.STARTTIMEStr); titleNode.textContent = detailRecord.meta.title || `${resolveVirtualLabItemContext(item).lab_name || "实验"}${ startTime ? ` ${startTime}` : "" } 完整评分`; } scheduleVirtualLabSync(); }) .catch((error) => { if (virtualLabState.activeDetailRequest !== requestToken) return; content.innerHTML = `
${escapeHtml( error && error.message ? error.message : "读取实验 XML 失败,请稍后重试。" )}
`; }); modal.setAttribute(CONFIG.virtualLab.keyAttr, key); } function loadVirtualLabDetail(item, forceRefresh) { const key = buildVirtualLabKey(item); if (!forceRefresh && virtualLabState.detailCache.has(key)) { return Promise.resolve(virtualLabState.detailCache.get(key)); } if (virtualLabState.pendingDetail.has(key)) { return virtualLabState.pendingDetail.get(key); } const context = resolveVirtualLabItemContext(item); if (!context.lab_date_url) { return Promise.reject(new Error("当前实验没有 LabDateUrl,无法获取完整评分。")); } const promise = fetchVirtualLabXml(context.lab_date_url) .then((xmlText) => { const normalized = renderVirtualLabXml(xmlText); const detailRecord = { html: normalized.html, meta: normalized.meta, fetchedAt: Date.now(), }; virtualLabState.detailCache.set(key, detailRecord); return detailRecord; }) .finally(() => { virtualLabState.pendingDetail.delete(key); }); virtualLabState.pendingDetail.set(key, promise); return promise; } function ensureVirtualLabDetailModal() { let modal = document.getElementById(CONFIG.virtualLab.modalId); if (modal) { return modal; } modal = document.createElement("section"); modal.id = CONFIG.virtualLab.modalId; modal.innerHTML = `
完整评分
`; document.body.appendChild(modal); return modal; } function closeVirtualLabDetailModal() { virtualLabState.activeDetailRequest = null; const modal = document.getElementById(CONFIG.virtualLab.modalId); if (modal) { modal.classList.remove("is-open"); } } function readKoValue(value) { return typeof value === "function" ? value() : value; } function resolveVirtualLabItemContext(item) { const viewModel = pageWindow.view || {}; return { stu_id: cleanText( (item && (item.USERID || item.UserID)) || readKoValue(viewModel.USERID) || "" ), stu_name: cleanText( (item && (item.USERNAME || item.UserName)) || readKoValue(viewModel.USERNAME) || "" ), lab_id: cleanText((item && item.LABID) || readKoValue(viewModel.LABID) || ""), lab_name: cleanText( (item && item.LABNAME) || readKoValue(viewModel.LABNAME) || item.LABNAMETitle || "" ), lab_date_url: cleanText((item && item.LabDateUrl) || ""), start_time: cleanText((item && item.STARTTIMEStr) || ""), end_time: cleanText((item && item.ENDTIMEStr) || ""), }; } function buildVirtualLabKey(item) { const context = resolveVirtualLabItemContext(item); return [ context.stu_id, context.lab_id, context.lab_date_url || (item && item.LabDateID) || context.start_time, context.end_time, context.lab_name, ] .map((part) => cleanText(part)) .join("|"); } function findVirtualLabItemByKey(key) { if (!pageWindow.view || typeof pageWindow.view.LabStatisticList !== "function") { return null; } const list = pageWindow.view.LabStatisticList() || []; return list.find((item) => buildVirtualLabKey(item) === key) || null; } // 按题目文本匹配答案(题目每次随机排列,不能按顺序匹配) const QUIZ_ANSWERS = { "误差配套": [ { q: "红外线在材料中传播时随着传播深度其强度", a: "A" }, { q: "红外发光二极管的发射方向对发射强度有无影响", a: "B" }, { q: "红外发光二极管的伏安特性与一般二极管的伏安特性相比", a: "B" }, { q: "红外发光二极管的输出特性与驱动电流近似成", a: "A" }, { q: "红外发光二极管的方向半值角越小", a: "A" }, { q: "测量物体的体积时如何选用测量工具", a: "C" }, { q: "测量一次某一物理量的不确定度评定的表达式", a: "C" }, { q: "不确定度均分原理是指", a: "A" }, { q: "本实验测一张纸的厚度时提供的测量仪器精度不够", a: "A" }, { q: "使用米尺、游标卡尺和千分尺进行测量", a: "A" }, ], "用霍尔元件测定磁场": [ { q: "实验中采用什么方法来消除负效应所带来的影响", a: "A" }, { q: "实验中如何获得霍尔元件灵敏度", a: "A" }, { q: "霍尔效应的本质是", a: "A" }, { q: "实验中测量的霍尔电势差与霍尔元件电流成", a: "A" }, { q: "实验中测量的霍尔电势差与励磁电流成", a: "A" }, ], "示波器实验": [ { q: "本实验中信号发生器能输出几种波形信号", a: "D" }, { q: "示波器实验中信号发生器发出的正弦波有几个参数可调", a: "C" }, { q: "示波器的基本用途", a: "C" }, { q: "实验中为了测量各被测信号", a: "A" }, { q: "实验中测量数据可以估读到", a: "A" }, ], "长度与固体密度测量": [ { q: "以下测量结果表达正确的是", a: "B" }, { q: "以下游标卡尺和螺旋测微器读数正确的是", a: "B" }, { q: "在操作读数显微镜时如何避免", a: "D" }, { q: "用读数显微镜测细丝的直径时", a: "A" }, { q: "螺旋测微器测量长度时", a: "A" }, ], "热膨胀系数的测量": [ { q: "本实验是采用什么方法测量微小长度的变化", a: "B" }, { q: "本实验处理数据是采用", a: "A" }, { q: "冒出.*湮灭.*一个条纹", a: "A" }, { q: "调节两束反射光重合获得观察屏干涉条纹时", a: "A" }, { q: "本实验观测的干涉条纹是哪种", a: "A" }, ], "用牛顿环测曲率半径": [ { q: "实验中在调节显微镜物镜筒时", a: "A" }, { q: "实验中观察到的是两束反射光的干涉", a: "B" }, { q: "实验中测量的是牛顿环的", a: "A" }, { q: "实验中测牛顿环明条纹和暗条纹所得透镜曲率半径", a: "B" }, { q: "实验中观测到的牛顿环干涉条纹的特点", a: "A" }, ], "超声波及其应用": [ { q: "超声波传播速度与介质", a: "B" }, { q: "实验中采用的超声探头是", a: "A" }, { q: "利用超声波探伤.*诊断.*主要是利用超声波的什么特点", a: "A" }, { q: "实验中超声探头要经常蘸水", a: "A" }, { q: "超声探伤是利用超声波遇到缺陷而发生什么现象", a: "A" }, ], "用箱式电位差计校准温差电偶": [ { q: "实验中使用电位差计测量.*关于校准", a: "B" }, { q: "本实验中电位差计的测量精度取决于", a: "B" }, { q: "温差电偶的应用主要是", a: "B" }, { q: "电位差计的核心工作原理是", a: "A" }, ], "光电效应测定普朗克常数": [ { q: "同一种光入射不同金属阴极时.*逸出功越大", a: "B" }, { q: "遏止电压的大小随入射光频率如何变化", a: "A" }, { q: "爱因斯坦用什么理论解释了光电效应", a: "B" }, { q: "本实验中通过变换滤波片来获得几种不同波长的光", a: "C" }, { q: "实验的数据处理中用什么方法求斜率", a: "B" }, ], "碰撞实验": [ { q: "完全弹性碰撞的特点是", a: "A" }, { q: "完全非弹性碰撞的特点是", a: "B" }, { q: "实验中通过光电门计时器可求出滑块的", a: "A" }, { q: "实验中表格2验证的是", a: "A" }, { q: "完全弹性碰撞.*完全非弹性碰撞.*恢复系数分别为", a: "A" }, ], "用补偿法测电池的电动势": [ { q: "3个电动势E.*Es.*Ex在电路中应该如何连接", a: "A" }, { q: "E.*Es.*Ex的大小关系应该是", a: "B" }, { q: "补偿法测电池电动势的核心优点是", a: "A" }, { q: "实验中的工作电源是", a: "A" }, { q: "本实验的电路中有几个回路", a: "C" }, ], "用惠斯通电桥研究金属电阻温度系数": [ { q: "一般情况下.*金属的电阻随温度升高而", a: "A" }, { q: "要测量50欧左右的电阻.*R2/R1的比例选择", a: "B" }, { q: "本实验中测量的金属电阻值有几位有效数字", a: "C" }, { q: "数据处理时求直线斜率可否用量角器", a: "B" }, { q: "本实验测量电阻值时采用的惠斯登电桥方法是", a: "A" }, ], "用转筒法和落球法测液体的粘度": [ { q: "达到小球在无限深广的液体内运动的条件", a: "A" }, { q: "落球法实验中为满足公式中的条件.*何时开始计时", a: "A" }, { q: "实验中液体的粘滞系数随着温度升高会", a: "B" }, { q: "用40克.*60克.*80克砝码测量的液体粘滞系数可否求平均", a: "B" }, { q: "将细线绕到小轮上时.*需要尽可能做到动作缓慢", a: "A" }, { q: "实验中用了几种方法来测量液体粘滞系数", a: "B" }, ], "基本电学量的测量": [ { q: "测电压时万用电表应", a: "B" }, { q: "测电流时.*万用电表应", a: "A" }, { q: "当预先不知道被测值大小时.*应将万用表", a: "A" }, { q: "本实验中验证欧姆定律.*是通过改变", a: "A" }, { q: "万用电表的内阻在测.*时最小", a: "A" }, ], "杨氏模量": [ { q: "望远镜.*激光器的作用是", a: "A" }, { q: "在处理数据的过程中.*F的正确取值是", a: "B" }, { q: "正确测量光杠杆杆长的方法是", a: "B" }, { q: "本实验中.*换用更细的钢丝对实验结果的影响", a: "C" }, { q: "金属的弹性模量与金属材料.*大小和形状的关系", a: "A" }, ], "液体表面张力系数的测定": [ { q: "液体表面张力系数随温度增加如何变化", a: "B" }, { q: "数字电压表的示数电压与力敏传感器所加外力F成", a: "A" }, { q: "水中加入相同温度的肥皂液后.*表面张力系数会", a: "A" }, { q: "吊环上有杂质对实验结果的影响", a: "A" }, ], "驻波实验": [ { q: "驻波是由两列.*的简谐波叠加合成", a: "B" }, { q: "弦线上形成稳定驻波的条件是", a: "B" }, { q: "本实验中如何获得第二列可合成驻波的简谐波", a: "A" }, { q: "理论上增大砝码重量是否影响所测金属丝弦线密度", a: "B" }, { q: "实验中.*同样实验条件下.*下列说法正确的是", a: "B" }, ], "刚体转动惯量的测量": [ { q: "质量.*直径相同的铝环和铝盘.*转动惯量相比较", a: "A" }, { q: "在测量铝盘的转动惯量时.*保证.*应在什么位置", a: "B" }, { q: "电脑式毫秒计单片机中角位移.*默认值", a: "A" }, { q: "若将毫秒计设置成.*0219.*转体共转动多少圈", a: "A" }, { q: "以下不是实验中刚体转动体系受到的外力矩为", a: "C" }, ], "光的偏振": [ { q: "光波是.*波", a: "A" }, { q: "实际中最常见的光的偏振态大体可以分为几种", a: "C" }, { q: "在光路中放入偏振片P1作为起偏器", a: "B" }, { q: "两个偏振片紧靠在一起.*将它们放在一盏灯的前面", a: "B" }, { q: "实验后处理验证马吕斯定律的实验数据是用", a: "A" }, ], }; // 实验名称模糊匹配表:页面上出现的名称 → QUIZ_ANSWERS 中的 key const QUIZ_NAME_ALIASES = { "长度": "长度与固体密度测量", "固体密度": "长度与固体密度测量", "热膨胀": "热膨胀系数的测量", "牛顿环": "用牛顿环测曲率半径", "超声波": "超声波及其应用", "电位差计": "用箱式电位差计校准温差电偶", "温差电偶": "用箱式电位差计校准温差电偶", "光电效应": "光电效应测定普朗克常数", "普朗克": "光电效应测定普朗克常数", "碰撞": "碰撞实验", "补偿法": "用补偿法测电池的电动势", "电动势": "用补偿法测电池的电动势", "惠斯通": "用惠斯通电桥研究金属电阻温度系数", "电桥": "用惠斯通电桥研究金属电阻温度系数", "粘度": "用转筒法和落球法测液体的粘度", "转筒": "用转筒法和落球法测液体的粘度", "落球": "用转筒法和落球法测液体的粘度", "单缝衍射": "单缝衍射实验", "衍射": "单缝衍射实验", "基本电学": "基本电学量的测量", "电学量": "基本电学量的测量", "杨氏": "杨氏模量", "弹性模量": "杨氏模量", "表面张力": "液体表面张力系数的测定", "驻波": "驻波实验", "转动惯量": "刚体转动惯量的测量", "刚体": "刚体转动惯量的测量", "偏振": "光的偏振", "霍尔": "用霍尔元件测定磁场", "示波器": "示波器实验", "误差": "误差配套", }; function isCourseSelectionPage() { return ( location.host === "211.82.81.133:7101" && location.pathname.indexOf("PEECM0001") !== -1 ); } function resolveExperimentName(text) { if (!text) return null; const cleaned = text.replace(/\s+/g, ""); // 精确匹配 if (QUIZ_ANSWERS[cleaned]) return cleaned; // 模糊匹配 for (const [keyword, fullName] of Object.entries(QUIZ_NAME_ALIASES)) { if (cleaned.includes(keyword)) return fullName; } return null; } function findExperimentNameFromPage() { // 从可选实验列表的表格中查找当前行的实验名称 // 找到最后一个被点击的选课按钮所在行的第一个 td const tables = document.querySelectorAll("table[id*='gvpee110201']"); for (const table of tables) { const rows = table.querySelectorAll("tr"); for (const row of rows) { const firstTd = row.querySelector("td"); if (firstTd) { const name = firstTd.textContent.trim(); if (resolveExperimentName(name)) return name; } } } return null; } // 从题目 DOM 中提取纯文本(去 HTML 标签和选项文本,只保留题干) function extractQuestionText(radio, iframeDoc) { // 沿 DOM 向上找到包含整道题的容器 div(style 含 text-align:left) var el = radio.parentElement; while (el && !(el.tagName === "DIV" && el.getAttribute("style") && el.getAttribute("style").indexOf("text-align") !== -1)) { el = el.parentElement; } if (!el) return ""; // 第一个子 div.row 就是题干行 var rows = el.querySelectorAll(":scope > div"); if (!rows.length) return ""; return rows[0].textContent.trim(); } // 规范化题目文本用于匹配:去掉空白、标点、题号等 function normalizeQuizText(text) { return text .replace(/^\d+\.\s*/, "") // 去题号 .replace(/\s+/g, "") // 去空白 .replace(/[()()??::,,。.、;;!!""''「」\u00A0]/g, ""); // 去标点 } // 在答案库中查找匹配的题目,支持正则模式串 function findAnswerByText(questionText, answerList) { var norm = normalizeQuizText(questionText); for (var i = 0; i < answerList.length; i++) { var rawPattern = answerList[i].q; var isRegex = rawPattern.indexOf(".*") !== -1; if (isRegex) { // 保护 .* 后再做 normalize,最后还原 var ph = "\uFFFD"; var safe = rawPattern.replace(/\.\*/g, ph); safe = normalizeQuizText(safe); safe = safe.replace(new RegExp(ph, "g"), ".*"); try { if (new RegExp(safe).test(norm)) return answerList[i].a; } catch (e) { /* 正则语法错误则跳过 */ } } else { var pattern = normalizeQuizText(rawPattern); if (norm.indexOf(pattern) !== -1) return answerList[i].a; } } return null; } function autoFillQuiz(iframeDoc, experimentName) { var resolvedName = resolveExperimentName(experimentName); if (!resolvedName || !QUIZ_ANSWERS[resolvedName]) { console.log("[quiz-filler] 未找到实验 " + experimentName + " 的答案"); return false; } var answers = QUIZ_ANSWERS[resolvedName]; // 也准备一个全库平铺数组用作兜底 var allAnswers = []; for (var key in QUIZ_ANSWERS) { allAnswers = allAnswers.concat(QUIZ_ANSWERS[key]); } // 收集所有题目的 radio button 组 var radioGroups = new Map(); var radios = iframeDoc.querySelectorAll("input[type='radio']"); for (var ri = 0; ri < radios.length; ri++) { var rname = radios[ri].getAttribute("name"); if (!rname) continue; if (!radioGroups.has(rname)) radioGroups.set(rname, []); radioGroups.get(rname).push(radios[ri]); } var filled = 0; var total = radioGroups.size; radioGroups.forEach(function (groupRadios, groupName) { // 提取该题的题干文本 var qText = extractQuestionText(groupRadios[0], iframeDoc); if (!qText) { console.log("[quiz-filler] 无法提取题目文本: " + groupName); return; } // 先在本实验答案库中查找 var target = findAnswerByText(qText, answers); // 找不到则在全部答案库中查找 if (!target) { target = findAnswerByText(qText, allAnswers); if (target) console.log("[quiz-filler] 从全局匹配到: " + qText.substring(0, 30)); } if (!target) { console.log("[quiz-filler] 未匹配到答案: " + qText.substring(0, 40)); return; } for (var gi = 0; gi < groupRadios.length; gi++) { if (groupRadios[gi].value === target) { groupRadios[gi].checked = true; filled++; break; } } }); console.log( "[quiz-filler] 自动填写完成: " + resolvedName + " (" + filled + "/" + total + ")" ); return filled > 0; } function addAutoFillButton(dialogBox) { if (document.querySelector("#quiz-auto-fill-btn")) return; var btn = document.createElement("button"); btn.id = "quiz-auto-fill-btn"; btn.textContent = "自动填写"; btn.style.cssText = "padding:4px 16px;background:#4CAF50;color:#fff;border:none;" + "border-radius:4px;cursor:pointer;font-size:14px;margin-right:10px;" + "vertical-align:middle;"; btn.addEventListener("mouseover", function () { btn.style.background = "#45a049"; }); btn.addEventListener("mouseout", function () { btn.style.background = "#4CAF50"; }); btn.addEventListener("click", function () { var iframe = document.getElementById("ShowObj"); if (!iframe) { btn.textContent = "未找到题目框架"; btn.style.background = "#f44336"; return; } var iframeDoc; try { iframeDoc = iframe.contentDocument || iframe.contentWindow.document; } catch (e) { btn.textContent = "无法访问框架"; btn.style.background = "#f44336"; return; } var experimentName = findExperimentNameFromIframe(iframeDoc); if (!experimentName) { experimentName = findExperimentNameFromPage(); } if (!experimentName) { btn.textContent = "未识别实验"; btn.style.background = "#ff9800"; return; } var resolved = resolveExperimentName(experimentName); if (!resolved) { btn.textContent = "无答案: " + experimentName; btn.style.background = "#ff9800"; return; } var success = autoFillQuiz(iframeDoc, experimentName); if (success) { btn.textContent = "已填写 ✓ " + resolved; btn.style.background = "#2196F3"; } else { btn.textContent = "填写失败"; btn.style.background = "#f44336"; } }); // 插入到 EasyUI dialog 底部按钮栏(确定/取消旁边) var windowEl = dialogBox.closest(".window") || dialogBox.parentElement; var buttonBar = windowEl ? windowEl.querySelector(".dialog-button") : null; if (buttonBar) { buttonBar.insertBefore(btn, buttonBar.firstChild); } else { // 兜底:放在 dialog 的 panel-header 区域 var panel = dialogBox.querySelector(".panel-header") || dialogBox.querySelector(".panel") || dialogBox; panel.style.position = "relative"; btn.style.cssText += "position:absolute;top:8px;right:120px;z-index:99999;"; panel.appendChild(btn); } } function findExperimentNameFromIframe(iframeDoc) { // 从题目内容中匹配实验名称 const text = iframeDoc.body ? iframeDoc.body.textContent : ""; // 尝试从题目关键词识别实验 const indicators = { "螺旋测微器": "长度与固体密度测量", "游标卡尺": "长度与固体密度测量", "读数显微镜": "长度与固体密度测量", "迈克尔逊干涉": "热膨胀系数的测量", "热膨胀": "热膨胀系数的测量", "牛顿环": "用牛顿环测曲率半径", "超声波": "超声波及其应用", "超声探头": "超声波及其应用", "电位差计": "用箱式电位差计校准温差电偶", "温差电偶": "用箱式电位差计校准温差电偶", "光电效应": "光电效应测定普朗克常数", "逸出功": "光电效应测定普朗克常数", "遏止电压": "光电效应测定普朗克常数", "弹性碰撞": "碰撞实验", "光电门": "碰撞实验", "滑块": "碰撞实验", "补偿法": "用补偿法测电池的电动势", "惠斯通": "用惠斯通电桥研究金属电阻温度系数", "惠斯登": "用惠斯通电桥研究金属电阻温度系数", "电桥": "用惠斯通电桥研究金属电阻温度系数", "粘滞系数": "用转筒法和落球法测液体的粘度", "落球": "用转筒法和落球法测液体的粘度", "转筒": "用转筒法和落球法测液体的粘度", "单缝衍射": "单缝衍射实验", "衍射条纹": "单缝衍射实验", "万用电表": "基本电学量的测量", "万用表": "基本电学量的测量", "欧姆定律": "基本电学量的测量", "光杠杆": "杨氏模量", "杨氏模量": "杨氏模量", "弹性模量": "杨氏模量", "表面张力": "液体表面张力系数的测定", "力敏传感器": "液体表面张力系数的测定", "吊环": "液体表面张力系数的测定", "驻波": "驻波实验", "弦线": "驻波实验", "波节": "驻波实验", "转动惯量": "刚体转动惯量的测量", "毫秒计": "刚体转动惯量的测量", "铝环": "刚体转动惯量的测量", "偏振片": "光的偏振", "马吕斯": "光的偏振", "线偏振": "光的偏振", "霍尔元件": "用霍尔元件测定磁场", "霍尔效应": "用霍尔元件测定磁场", "洛伦兹力": "用霍尔元件测定磁场", "示波器": "示波器实验", "信号发生器": "示波器实验", "红外发光二极管": "误差配套", "不确定度均分": "误差配套", }; for (const [keyword, name] of Object.entries(indicators)) { if (text.includes(keyword)) return name; } return null; } function initCourseQuizFiller() { console.log("[quiz-filler] 课程选修预习题自动填写模块已加载"); // 自动确认"您确定要选修该实验吗?"对话框 var _origConfirm = pageWindow.confirm; pageWindow.confirm = function (msg) { if (msg && msg.indexOf("确定要选修") !== -1) { console.log("[quiz-filler] 自动确认选课对话框"); return true; } return _origConfirm.call(pageWindow, msg); }; // 使用 MutationObserver 监测 ShowObjBox dialog 出现 const observer = new MutationObserver(function () { const dialogBox = document.getElementById("ShowObjBox"); if (!dialogBox) return; // 检查 dialog 是否可见(EasyUI dialog 会设置 display) if ( dialogBox.style.display === "none" || dialogBox.offsetParent === null ) { return; } // dialog 可见,添加自动填写按钮 const iframe = document.getElementById("ShowObj"); if (!iframe) return; // 等 iframe 加载完成后再添加按钮 const tryAddButton = function () { addAutoFillButton( dialogBox.closest(".window") || dialogBox.parentElement || dialogBox ); }; if (iframe.src && iframe.src.includes("Paper.aspx")) { iframe.addEventListener("load", tryAddButton, { once: true }); // 兜底:如果已经加载完了 try { const doc = iframe.contentDocument || iframe.contentWindow.document; if (doc && doc.readyState === "complete") { tryAddButton(); } } catch (e) { // 跨域,忽略 } } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener("DOMContentLoaded", function () { observer.observe(document.body, { childList: true, subtree: true }); }); } } // ═══════════════════════════════════════════════════════════════ // ███ 课程选修预习题自动填写模块 结束 ███ // ═══════════════════════════════════════════════════════════════ function cleanText(value) { return String(value ?? "").replace(/\s+/g, " ").trim(); } function toNumber(value) { const num = parseFlexibleNumber(firstNumberLike(value)); return Number.isFinite(num) ? num : NaN; } function firstNumberLike(value) { return extractNumericTokens(value)[0] || ""; } function extractNumericTokens(value) { return ( String(value ?? "").match( /[-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+|[×x*]\s*10\^?[-+]?\d+)?/g ) || [] ); } function parseFlexibleNumber(value) { const text = cleanText(value) .replace(/−/g, "-") .replace(/\s+/g, ""); if (!text) { return NaN; } const sciMatch = text.match( /^([-+]?\d+(?:\.\d+)?)(?:([eE][-+]?\d+)|([×x*]10\^?[-+]?\d+))?$/ ); if (!sciMatch) { return parseFloat(text); } const base = parseFloat(sciMatch[1]); if (!Number.isFinite(base)) { return NaN; } if (sciMatch[2]) { return Number(base * 10 ** parseInt(sciMatch[2].slice(1), 10)); } if (sciMatch[3]) { const exponent = parseInt(sciMatch[3].replace(/[×x*]10\^?/, ""), 10); return Number.isFinite(exponent) ? Number(base * 10 ** exponent) : NaN; } return base; } function escapeHtml(text) { return String(text || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function init() { maybeShowFirstRunNotice(); if (isVirtualLabStatisticPage()) { initVirtualLabEnhancer(); pageWindow.__cauLabEnhancerLoaded = true; window.__cauLabEnhancerLoaded = true; console.log("[lab-enhancer] virtual lab module loaded"); return; } if (isCourseSelectionPage()) { initCourseQuizFiller(); return; } } const debugApi = { renderVirtualLabXml, resolveVirtualLabXmlUrl, loadVirtualLabDetail, }; pageWindow.__cauLabDebug = debugApi; window.__cauLabDebug = debugApi; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init, { once: true }); } else { init(); } window.addEventListener("load", () => { try { init(); } catch (error) { console.error("[lab-enhancer] load init failed", error); } }); })();