// ==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)}${i ? "td" : "th"}>`).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);
}
});
})();