// ==UserScript==
// @name CAU 实验报告与仿真实验增强
// @namespace https://example.com/
// @version 0.6.2
// @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:7103/*
// @match http://211.82.81.133:7104/*
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.getValue
// @grant GM.setValue
// @grant unsafeWindow
// @connect expir.outdesigner.top
// @connect 211.82.81.133
// @run-at document-start
// ==/UserScript==
(function () {
"use strict";
const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const CONFIG = {
selectors: {
modal: ".modal-content",
modalBody: ".modal-body",
modalFooter: ".modal-footer",
fileInput: "#importButton",
closeButton: '[data-dismiss="modal"], .close',
titleBlocks:
".panel-heading span:first-child, .panel-heading .col-md-7, .panel-heading",
},
shellId: "tm-xml-review-shell",
panelId: "tm-xml-review-panel",
styleId: "tm-xml-review-style",
badgeId: "tm-xml-review-badge",
markerAttr: "data-xml-review-marker",
debounceMs: 250,
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,
serverBase: "https://expir.outdesigner.top",
serverBaseKey: "tm-vlab-server-base",
summaryTtlMs: 5 * 60 * 1000,
},
thresholds: {
richTextMinLength: 8,
slopeTolerance: 0.02,
planckTolerance: 0.05,
minB2Rows: 10,
},
labels: {
SYMD: "实验目的",
SYYL: "实验原理",
SYYQ: "实验仪器",
SYBZ: "实验步骤",
OP: "数据处理",
SKT: "思考题",
SYZJ: "实验总结",
JSPY: "教师评语",
},
experimentAliases: {
photoelectric: "光电效应测定普朗克常数",
collision: "碰撞",
thermalExpansion: "热膨胀系数",
elasticModulus: "弹性模量",
newtonRings: "牛顿环",
surfaceTension: "表面张力",
polarization: "偏振",
thermocouple: "温差电偶",
standingWave: "驻波",
lengthDensity: "长度与固体密度测量",
viscosity: "黏度",
errorCompanion: "误差配套",
wheatstone: "惠斯通电桥",
oscilloscope: "示波器",
hallEffect: "霍尔效应",
},
};
const state = {
observer: null,
modalObserver: null,
scanTimer: null,
autoLoadTimer: null,
currentModal: null,
currentInput: null,
globalBound: false,
lastFileFingerprint: "",
reviewData: null,
xmlUrlMap: new Map(),
networkHooked: false,
autoLoadAttempts: 0,
autoLoadDebug: null,
resolvedXmlUrl: "",
currentXmlText: "",
currentXmlFileName: "",
currentXmlSourceUrl: "",
firstRunNoticeChecked: false,
};
const virtualLabState = {
initialized: false,
eventsBound: false,
viewMounted: false,
syncTimer: null,
bodyObserver: null,
summaryCache: new Map(),
detailCache: new Map(),
pendingSummary: new Map(),
pendingDetail: new Map(),
lastServiceMessage: "",
};
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[0-4]$/.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);
}
function injectStyles() {
if (document.getElementById(CONFIG.styleId)) {
return;
}
const style = document.createElement("style");
style.id = CONFIG.styleId;
style.textContent = `
#${CONFIG.shellId} {
margin: 0 0 16px;
font-family: "Microsoft YaHei", sans-serif;
}
#${CONFIG.shellId} .xml-review-tabs {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
#${CONFIG.shellId} .xml-review-tab {
border: 1px solid #cdded8;
background: #f6fbf8;
color: #28463d;
border-radius: 999px;
padding: 8px 14px;
cursor: pointer;
font-size: 13px;
font-weight: 700;
}
#${CONFIG.shellId} .xml-review-tab.is-active {
background: #1f6d4a;
color: #ffffff;
border-color: #1f6d4a;
}
#${CONFIG.shellId} .xml-review-tab-state {
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
font-size: 11px;
}
#${CONFIG.shellId} .xml-review-tab:not(.is-active) .xml-review-tab-state {
background: #e7f1ec;
color: #31584d;
}
#${CONFIG.panelId} {
padding: 16px;
border: 1px solid #d8e2dc;
border-radius: 12px;
background:
linear-gradient(180deg, #f8fbfa 0%, #f3f8f6 100%);
color: #243b35;
box-shadow: 0 8px 24px rgba(25, 55, 46, 0.08);
display: none;
}
#${CONFIG.panelId} h3,
#${CONFIG.panelId} h4 {
margin: 0 0 10px;
color: #19372e;
}
#${CONFIG.panelId} .xml-review-top {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
#${CONFIG.panelId} .xml-review-actions {
display: inline-flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
#${CONFIG.panelId} .xml-review-action {
border: 1px solid #bfd6cd;
background: #ffffff;
color: #22473c;
border-radius: 999px;
padding: 8px 14px;
cursor: pointer;
font-size: 12px;
font-weight: 700;
}
#${CONFIG.panelId} .xml-review-action:hover {
background: #eef7f3;
}
#${CONFIG.panelId} .xml-review-status {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 700;
padding: 8px 12px;
border-radius: 999px;
}
#${CONFIG.panelId} .status-pass {
background: #dff3e7;
color: #1e6a41;
}
#${CONFIG.panelId} .status-warn {
background: #fff0d9;
color: #9a5b00;
}
#${CONFIG.panelId} .status-error {
background: #ffe0e0;
color: #a32929;
}
#${CONFIG.panelId} .xml-review-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin: 12px 0;
}
#${CONFIG.panelId} .xml-review-card {
background: #ffffff;
border: 1px solid #e2ece8;
border-radius: 10px;
padding: 12px;
}
#${CONFIG.panelId} .xml-review-card strong {
display: block;
margin-bottom: 6px;
}
#${CONFIG.panelId} .xml-review-card p {
margin: 0;
line-height: 1.55;
}
#${CONFIG.panelId} ul {
margin: 8px 0 0;
padding-left: 18px;
}
#${CONFIG.panelId} li {
margin: 4px 0;
line-height: 1.45;
}
#${CONFIG.panelId} .xml-review-muted {
color: #5f766f;
font-size: 12px;
}
#${CONFIG.panelId} .xml-review-debug {
margin-top: 12px;
padding: 10px 12px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.72);
border: 1px dashed #c7d8d2;
font-size: 12px;
color: #4c6660;
line-height: 1.5;
}
#${CONFIG.panelId} .xml-review-debug strong {
display: inline;
margin: 0;
}
#${CONFIG.panelId} .xml-review-section + .xml-review-section {
margin-top: 16px;
}
#${CONFIG.panelId} .xml-review-section h4 {
margin-bottom: 10px;
}
#${CONFIG.panelId} .xml-review-table-wrap {
overflow-x: auto;
border: 1px solid #e0ece7;
border-radius: 10px;
background: #ffffff;
}
#${CONFIG.panelId} table.xml-review-table {
width: 100%;
border-collapse: collapse;
min-width: 520px;
}
#${CONFIG.panelId} table.xml-review-table th,
#${CONFIG.panelId} table.xml-review-table td {
border-bottom: 1px solid #edf3f0;
padding: 8px 10px;
text-align: left;
vertical-align: top;
font-size: 13px;
line-height: 1.5;
}
#${CONFIG.panelId} table.xml-review-table th {
background: #f3f8f6;
font-weight: 700;
white-space: nowrap;
}
#${CONFIG.panelId} table.xml-review-table tr:last-child td {
border-bottom: none;
}
#${CONFIG.panelId} .xml-review-inline-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
#${CONFIG.panelId} .xml-review-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
background: #ffffff;
border: 1px solid #dde9e4;
font-size: 12px;
}
#${CONFIG.panelId} .xml-review-compare {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
#${CONFIG.panelId} .xml-review-compare-item {
background: #ffffff;
border: 1px solid #e2ece8;
border-radius: 10px;
padding: 12px;
}
#${CONFIG.panelId} .xml-review-compare-item strong {
display: block;
margin-bottom: 8px;
}
#${CONFIG.panelId} .xml-review-compare-item div + div {
margin-top: 6px;
}
#${CONFIG.panelId} .xml-review-subcard {
margin-top: 12px;
padding: 12px;
border-radius: 10px;
border: 1px solid #e2ece8;
background: #ffffff;
}
#${CONFIG.panelId} .xml-review-subcard.is-standard {
border-color: #e8c26b;
background: linear-gradient(180deg, #fff9eb 0%, #fff5dc 100%);
}
#${CONFIG.panelId} .xml-review-subcard h5 {
margin: 0 0 8px;
color: #27453c;
font-size: 13px;
}
#${CONFIG.panelId} .xml-review-subcard.is-standard h5 {
color: #835200;
}
#${CONFIG.panelId} .xml-review-answer-standard {
margin-top: 8px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid #ebcb77;
background: #fff4d2;
color: #7d5300;
font-weight: 700;
}
#${CONFIG.panelId} .xml-review-preview {
margin-top: 6px;
color: #385850;
}
#${CONFIG.panelId} .xml-review-empty {
color: #6b817a;
}
#${CONFIG.panelId}.is-visible {
display: block;
}
[${CONFIG.markerAttr}]::after {
content: attr(data-xml-review-marker);
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
border-radius: 999px;
background: #fff0d9;
color: #9a5b00;
font-size: 12px;
font-weight: 700;
vertical-align: middle;
}
#${CONFIG.badgeId} {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 99999;
padding: 10px 14px;
border-radius: 999px;
background: rgba(25, 55, 46, 0.92);
color: #ffffff;
font: 12px/1.2 "Microsoft YaHei", sans-serif;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
}
#${CONFIG.badgeId}.is-active {
background: rgba(26, 106, 65, 0.92);
}
`;
document.head.appendChild(style);
}
function ensureLoadBadge() {
let badge = document.getElementById(CONFIG.badgeId);
if (!badge) {
badge = document.createElement("div");
badge.id = CONFIG.badgeId;
badge.textContent = "XML 批改脚本已加载";
(document.body || document.documentElement).appendChild(badge);
}
return badge;
}
function debounceScan() {
window.clearTimeout(state.scanTimer);
state.scanTimer = window.setTimeout(scanForModal, CONFIG.debounceMs);
}
function scanForModal() {
const modals = Array.from(document.querySelectorAll(CONFIG.selectors.modal));
const targetModal = modals.find(isTargetReportModal);
if (!targetModal) {
if (state.currentModal) {
cleanupCurrentModal();
}
return;
}
if (state.currentModal !== targetModal) {
attachToModal(targetModal);
} else {
ensurePanel(targetModal);
}
}
function isTargetReportModal(modal) {
if (!modal || !isVisible(modal)) {
return false;
}
const hasReportStructure =
!!modal.querySelector("#content") ||
!!modal.querySelector(".panel.panel-default") ||
!!modal.querySelector(".btn.btn-LightGreen");
if (hasReportStructure) {
return true;
}
const text = cleanText(modal.textContent);
return (
/完成.*报告|报告/.test(text) &&
(text.includes("实验目的") ||
text.includes("数据处理") ||
text.includes("思考题") ||
text.includes("实验总结"))
);
}
function isVisible(element) {
if (!element) {
return false;
}
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return (
style.display !== "none" &&
style.visibility !== "hidden" &&
rect.width > 0 &&
rect.height > 0
);
}
function isReportFileInput(element) {
return !!(
element &&
element.tagName === "INPUT" &&
element.type === "file" &&
(element.id === "importButton" || element.name === "importButton")
);
}
function cleanupCurrentModal() {
clearSectionMarkers();
window.clearTimeout(state.autoLoadTimer);
state.autoLoadTimer = null;
state.currentModal = null;
state.currentInput = null;
state.lastFileFingerprint = "";
state.reviewData = null;
state.autoLoadAttempts = 0;
state.autoLoadDebug = null;
state.resolvedXmlUrl = "";
state.currentXmlText = "";
state.currentXmlFileName = "";
state.currentXmlSourceUrl = "";
if (state.xmlUrlMap && typeof state.xmlUrlMap.clear === "function") {
state.xmlUrlMap.clear();
}
}
function attachToModal(modal) {
cleanupCurrentModal();
state.currentModal = modal;
const panel = ensurePanel(modal);
bindFileInput(modal);
watchModalForChanges(modal);
bindSubmitHook(modal);
scheduleAutoLoadXml(panel);
}
function watchModalForChanges(modal) {
if (state.modalObserver) {
state.modalObserver.disconnect();
}
state.modalObserver = new MutationObserver((mutations) => {
if (!document.body.contains(modal)) {
cleanupCurrentModal();
return;
}
const hasExternalMutation = mutations.some((mutation) => {
const target = mutation.target && mutation.target.nodeType === 1 ? mutation.target : null;
return !target || !target.closest || !target.closest(`#${CONFIG.shellId}`);
});
if (!hasExternalMutation) {
return;
}
ensurePanel(modal);
bindFileInput(modal);
if (!state.reviewData) {
scheduleAutoLoadXml(ensurePanel(modal));
}
});
state.modalObserver.observe(modal, {
childList: true,
subtree: true,
});
}
function ensurePanel(modal) {
const body = modal.querySelector(CONFIG.selectors.modalBody);
if (!body) {
return null;
}
let shell = modal.querySelector(`#${CONFIG.shellId}`);
if (!shell) {
shell = document.createElement("section");
shell.id = CONFIG.shellId;
shell.innerHTML = `
脚本会优先从页面报告数据里自动定位 XML;若失败,再回退到手动上传文件解析。
`;
shell.addEventListener("click", (event) => {
const actionButton = event.target.closest("[data-xml-review-action]");
if (actionButton) {
handleReviewAction(actionButton.getAttribute("data-xml-review-action"));
return;
}
const button = event.target.closest("[data-xml-review-tab]");
if (!button || !state.currentModal) {
return;
}
switchReviewTab(state.currentModal, button.getAttribute("data-xml-review-tab"));
});
const firstBlock = body.firstElementChild;
if (firstBlock) {
body.insertBefore(shell, firstBlock);
} else {
body.appendChild(shell);
}
updateAnalysisTabState(modal, "待加载", "warn");
switchReviewTab(modal, "report");
}
const panel = shell.querySelector(`#${CONFIG.panelId}`);
if (panel && !panel.dataset.initialized) {
panel.dataset.initialized = "1";
panel.innerHTML = `
脚本会优先从页面报告数据里自动定位 XML;若失败,再回退到手动上传文件解析。
`;
updateAnalysisTabState(modal, "待加载", "warn");
}
if (panel && panel.querySelector(".xml-review-debug")) {
updatePanelDebug(panel, {
stage: "初始化",
message: "已插入批改分析标签,正在等待页面报告数据。",
});
}
return panel;
}
function switchReviewTab(modal, tabName) {
if (!modal) {
return;
}
const body = modal.querySelector(CONFIG.selectors.modalBody);
const shell = modal.querySelector(`#${CONFIG.shellId}`);
const panel = modal.querySelector(`#${CONFIG.panelId}`);
if (!body || !shell || !panel) {
return;
}
shell
.querySelectorAll("[data-xml-review-tab]")
.forEach((button) =>
button.classList.toggle(
"is-active",
button.getAttribute("data-xml-review-tab") === tabName
)
);
Array.from(body.children).forEach((child) => {
if (child === shell) {
return;
}
child.style.display = tabName === "analysis" ? "none" : "";
});
panel.classList.toggle("is-visible", tabName === "analysis");
}
function updateAnalysisTabState(modal, text, tone) {
if (!modal) {
return;
}
const tab = modal.querySelector('[data-xml-review-tab="analysis"]');
const stateNode = tab && tab.querySelector(".xml-review-tab-state");
if (!tab || !stateNode) {
return;
}
stateNode.textContent = text;
stateNode.setAttribute("data-tone", tone || "warn");
}
function handleReviewAction(action) {
if (action === "download-xml") {
downloadCurrentXml();
} else if (action === "recheck") {
recheckXml();
}
}
function recheckXml() {
if (!state.currentModal) {
return;
}
clearSectionMarkers();
state.reviewData = null;
state.lastFileFingerprint = "";
state.resolvedXmlUrl = "";
state.currentXmlText = "";
state.currentXmlFileName = "";
state.currentXmlSourceUrl = "";
state.autoLoadAttempts = 0;
state.autoLoadDebug = null;
window.clearTimeout(state.autoLoadTimer);
const panel = state.currentModal.querySelector(`#${CONFIG.panelId}`);
if (panel) {
panel.dataset.initialized = "";
ensurePanel(state.currentModal);
scheduleAutoLoadXml(panel);
}
const badge = ensureLoadBadge();
badge.classList.remove("is-active");
badge.textContent = "正在重新检查";
}
function downloadCurrentXml() {
if (!state.currentXmlText) {
window.alert("当前还没有可下载的 XML 内容,请先打开已成功解析的报告。");
return;
}
const link = document.createElement("a");
const baseName = sanitizeFileName(
state.currentXmlFileName ||
`${(state.reviewData && state.reviewData.parsedReport.meta.paperName) || "report"}.xml`
);
link.download = baseName.endsWith(".xml") ? baseName : `${baseName}.xml`;
let revokeHref = "";
if (state.currentXmlSourceUrl) {
link.href = state.currentXmlSourceUrl;
} else {
const blob = new Blob([state.currentXmlText], {
type: "application/xml;charset=utf-8",
});
revokeHref = URL.createObjectURL(blob);
link.href = revokeHref;
}
document.body.appendChild(link);
link.click();
link.remove();
if (revokeHref) {
window.setTimeout(() => URL.revokeObjectURL(revokeHref), 1000);
}
const badge = ensureLoadBadge();
badge.classList.add("is-active");
badge.textContent = "XML 已下载,可反馈适配";
}
function bindFileInput(modal) {
const input = modal.querySelector(CONFIG.selectors.fileInput);
if (!input || input === state.currentInput) {
return;
}
state.currentInput = input;
input.addEventListener("change", handleFileChange, true);
}
function bindSubmitHook(modal) {
const hookNames = ["XMLSubmitReport_ZSTJ", "XMLSubmitReport_LSTJ"];
hookNames.forEach((fnName) => {
if (typeof pageWindow[fnName] === "function" && !pageWindow[fnName].__xmlReviewHooked) {
const original = pageWindow[fnName];
pageWindow[fnName] = function () {
const result = original.apply(this, arguments);
if (result && typeof result.then === "function") {
result.then(() => schedulePostSubmitRecheck(modal));
} else {
schedulePostSubmitRecheck(modal);
}
return result;
};
pageWindow[fnName].__xmlReviewHooked = true;
}
});
}
function schedulePostSubmitRecheck(modal) {
window.setTimeout(() => {
if (!state.currentModal || state.currentModal !== modal) {
return;
}
recheckXml();
}, 2000);
}
function bindGlobalListeners() {
if (state.globalBound) {
return;
}
state.globalBound = true;
document.addEventListener(
"change",
(event) => {
if (!isReportFileInput(event.target)) {
return;
}
handleFileChange(event);
},
true
);
}
async function handleFileChange(event) {
const input = event.target || event.currentTarget;
const file = input.files && input.files[0];
if (!file || !state.currentModal) {
return;
}
const fingerprint = `${file.name}:${file.size}:${file.lastModified}`;
if (fingerprint === state.lastFileFingerprint) {
return;
}
state.lastFileFingerprint = fingerprint;
const panel = ensurePanel(state.currentModal);
const badge = ensureLoadBadge();
badge.classList.add("is-active");
badge.textContent = "XML 已上传,正在解析";
setPanelStatus(panel, "warn", "正在解析 XML");
try {
const xmlText = await file.text();
processXmlText(panel, xmlText, file.name);
badge.textContent = `XML 解析完成:${state.reviewData.reviewResult.status === "pass" ? "通过" : "需检查"}`;
} catch (error) {
console.error("[xml-review] parse failed", error);
renderFailure(panel, error);
badge.textContent = "XML 解析失败";
}
}
function scheduleAutoLoadXml(panel) {
if (!panel || !state.currentModal || state.reviewData) {
return;
}
window.clearTimeout(state.autoLoadTimer);
const runAttempt = async () => {
if (!state.currentModal || !document.body.contains(state.currentModal) || state.reviewData) {
return;
}
state.autoLoadAttempts += 1;
const resolution = resolveXmlSource();
updatePanelDebug(panel, resolution);
if (!resolution.xmlUrl) {
setPanelStatus(panel, "warn", resolution.pending ? "正在定位 XML" : "未找到 XML");
const badge = ensureLoadBadge();
badge.classList.remove("is-active");
badge.textContent = resolution.pending ? "正在定位 XML" : "未找到 XML 地址";
if (resolution.pending && state.autoLoadAttempts < 30) {
state.autoLoadTimer = window.setTimeout(runAttempt, 500);
}
return;
}
await tryAutoLoadXmlForModal(panel, resolution);
};
state.autoLoadTimer = window.setTimeout(runAttempt, 0);
}
async function tryAutoLoadXmlForModal(panel, resolution) {
if (!panel) {
return;
}
const xmlUrl = resolution && resolution.xmlUrl;
if (!xmlUrl) {
return;
}
state.resolvedXmlUrl = xmlUrl;
const badge = ensureLoadBadge();
badge.classList.add("is-active");
badge.textContent = "已定位 XML,正在拉取";
setPanelStatus(panel, "warn", "正在加载 XML");
updatePanelDebug(panel, {
...(resolution || {}),
stage: "拉取 XML",
message: "已找到 XML 地址,正在读取并解析。",
});
try {
const response = await fetch(buildFreshXmlRequestUrl(xmlUrl), {
credentials: "include",
cache: "no-store",
headers: {
"cache-control": "no-cache, no-store, max-age=0",
pragma: "no-cache",
},
});
if (!response.ok) {
throw new Error(`拉取 XML 失败:${response.status}`);
}
const xmlText = await response.text();
processXmlText(panel, xmlText, xmlUrl.split("/").pop() || "remote.xml");
badge.textContent = `XML 解析完成:${state.reviewData.reviewResult.status === "pass" ? "通过" : "需检查"}`;
} catch (error) {
console.error("[xml-review] auto fetch failed", error);
setPanelStatus(panel, "warn", "未能自动加载 XML");
badge.textContent = "自动拉取 XML 失败";
updatePanelDebug(panel, {
...(resolution || {}),
stage: "拉取失败",
pending: false,
error: error && error.message ? error.message : String(error),
message: "已找到 XML 地址,但读取失败。可保留当前面板用于继续排查。",
});
}
}
function buildFreshXmlRequestUrl(xmlUrl) {
if (!xmlUrl) {
return "";
}
try {
const url = new URL(xmlUrl, location.href);
url.searchParams.set("_xmlReviewTs", `${Date.now()}`);
return url.toString();
} catch (error) {
const joiner = xmlUrl.includes("?") ? "&" : "?";
return `${xmlUrl}${joiner}_xmlReviewTs=${Date.now()}`;
}
}
function processXmlText(panel, xmlText, fileLabel) {
const parsedReport = parseXmlReport(xmlText);
const reviewResult = reviewReport(parsedReport);
state.currentXmlText = xmlText;
state.currentXmlFileName = fileLabel || `${parsedReport.meta.paperName || "report"}.xml`;
state.currentXmlSourceUrl = state.resolvedXmlUrl || "";
state.reviewData = { parsedReport, reviewResult };
renderReview(panel, parsedReport, reviewResult, {
name: state.currentXmlFileName,
sourceUrl: state.currentXmlSourceUrl,
});
applySectionMarkers(reviewResult);
}
function setPanelStatus(panel, status, text) {
if (!panel) {
return;
}
const node = panel.querySelector(".xml-review-status");
if (!node) {
return;
}
node.className = `xml-review-status status-${status}`;
node.textContent = text;
updateAnalysisTabState(state.currentModal, text, status);
}
function renderFailure(panel, error) {
clearSectionMarkers();
setPanelStatus(panel, "error", "解析失败");
switchReviewTab(state.currentModal, "analysis");
panel.innerHTML = `
错误信息
${escapeHtml(error && error.message ? error.message : String(error))}
该错误不会阻止原页面继续上传或提交报告。
`;
}
function renderReview(panel, parsedReport, reviewResult, file) {
const statusText =
reviewResult.status === "pass"
? "通过"
: reviewResult.status === "warn"
? "需检查"
: "解析失败";
const statusClass =
reviewResult.status === "pass"
? "pass"
: reviewResult.status === "warn"
? "warn"
: "error";
const metricCards = reviewResult.metrics
.map(
(metric) => `
${escapeHtml(metric.label)}
${escapeHtml(metric.value)}
${
metric.note
? `
${escapeHtml(metric.note)}
`
: ""
}
`
)
.join("");
const issueItems =
reviewResult.issues.length > 0
? `${reviewResult.issues
.map((item) => `- ${escapeHtml(item.message)}
`)
.join("")}
`
: `未发现需要额外核对的数值或结构问题。
`;
panel.innerHTML = `
批改结果
${statusText}
文件:${escapeHtml(file.name)} | 报告:${escapeHtml(
parsedReport.meta.paperName || "未知报告"
)}
${
file.sourceUrl
? ` | XML 地址:${escapeHtml(file.sourceUrl)}`
: ""
}
${
reviewResult.adaptationMessage
? `适配反馈${escapeHtml(
reviewResult.adaptationMessage
)}
`
: ""
}
${metricCards}
总体结论
${escapeHtml(reviewResult.summary)}
解析概况
识别栏目 ${Object.keys(parsedReport.questionsByType).length} 个,识别数据项 ${Object.keys(parsedReport.opItemsByName).length} 个。
栏目得分
${renderBreakdownTable(
["栏目", "得分", "内容类型", "分析说明"],
reviewResult.sectionBreakdown.map((item) => [
item.label,
item.scoreText,
item.contentType,
item.analysis || "无额外说明",
])
)}
数据项得分
${renderBreakdownTable(
["数据项", "得分", "类型", "学生作答", "标准答案", "说明"],
reviewResult.opBreakdown.map((item) => [
item.label,
item.scoreText,
item.kind,
item.studentAnswer,
item.standardAnswer,
stripRecomputePhrases(item.analysis) || "无额外说明",
])
)}
表格分析
${renderTableAnalysis(reviewResult.tableBreakdown)}
计算结果分析
${renderCalculationAnalysis(reviewResult.calculationBreakdown)}
文本可分析项
${renderBreakdownTable(
["栏目", "得分", "内容摘要", "自动分析"],
reviewResult.textBreakdown.map((item) => [
item.label,
item.scoreText,
item.preview,
item.analysis,
])
)}
`;
updateAnalysisTabState(state.currentModal, statusText, statusClass);
}
function renderBreakdownTable(headers, rows) {
const safeRows = rows && rows.length > 0 ? rows : [["暂无数据", "", "", ""]];
const headerHtml = headers.map((header) => `${escapeHtml(header)} | `).join("");
const rowHtml = safeRows
.map(
(row) => `
${row
.map((cell) => `| ${escapeHtml(cell == null ? "" : String(cell))} | `)
.join("")}
`
)
.join("");
return `
`;
}
function renderTableAnalysis(items) {
if (!items || items.length === 0) {
return `当前没有可展开的表格型数据。
`;
}
return items
.map(
(item) => `
${escapeHtml(item.label)}
得分 ${escapeHtml(item.scoreText)}
${escapeHtml(item.summary)}
${renderTableBlock("标准答案(StdResult)", item.standardHeaders, item.standardRows, item.standardColumnNotes, true)}
${renderTableBlock("学生作答(RealResult)", item.submittedHeaders, item.submittedRows, item.submittedColumnNotes, false)}
`
)
.join("");
}
function renderTableBlock(title, headers, rows, notes, emphasize) {
const hasHeaders = Array.isArray(headers) && headers.length > 0;
const hasRows = Array.isArray(rows) && rows.length > 0;
if (!hasHeaders && !hasRows) {
return `
${escapeHtml(title)}
当前未提供这部分数据。
`;
}
return `
${escapeHtml(title)}
${
notes && notes.length > 0
? `
${notes.map((note) => `- ${escapeHtml(note)}
`).join("")}
`
: `
未生成列格式分析。
`
}
${renderBreakdownTable(headers, rows)}
`;
}
function renderCalculationAnalysis(items) {
if (!items || items.length === 0) {
return `当前没有可重算的计算项。
`;
}
return `
${items
.map(
(item) => `
${escapeHtml(item.label)}
得分:${escapeHtml(item.scoreText)}
学生作答(RealResult):${escapeHtml(item.studentValue)}
标准答案(StdResult):${escapeHtml(item.standardValue)}
${escapeHtml(item.deltaNote)}
${escapeHtml(item.precisionNote)}
`
)
.join("")}
`;
}
function parseXmlReport(xmlText) {
const parser = new DOMParser();
const xml = parser.parseFromString(xmlText, "text/xml");
const parserError = xml.querySelector("parsererror");
if (parserError) {
throw new Error("XML 解析失败,请确认上传的是完整报告 XML 文件。");
}
const paper = xml.querySelector("Paper");
if (!paper) {
throw new Error("未找到 Paper 根节点,文件结构不是预期的实验报告 XML。");
}
const title = paper.querySelector("Title");
const meta = {
paperType: directText(title, "PaperType"),
paperName: directText(title, "PaperName"),
realScore: toNumber(directText(title, "RealScore")),
totalScore: toNumber(directText(title, "TotalScore")),
fullScore: toNumber(directText(title, "FullScore")),
};
const questions = {};
const opItemsByName = {};
Array.from(paper.querySelectorAll("Content > Question")).forEach((questionNode) => {
const type = questionNode.getAttribute("Type") || "";
const rawTitleText =
directText(questionNode, "CheckPoint > Script") ||
directText(questionNode, "CheckPoint > TestTarget > Script");
const sanitizedTitle = sanitizeForDisplay(rawTitleText);
const titleText =
(sanitizedTitle && sanitizedTitle !== "[编码数据]" && sanitizedTitle !== "[图片数据]"
? sanitizedTitle
: "") ||
CONFIG.labels[type] ||
type;
const paras = Array.from(questionNode.querySelectorAll("Para")).map(parsePara);
const question = {
type,
title: titleText,
score: {
total: toNumber(directText(questionNode, "Score > Total")),
real: toNumber(directText(questionNode, "Score > RealScore")),
raw: cleanText(directText(questionNode, "Score")),
},
paras,
decodedContent: extractQuestionContent(questionNode),
};
questions[type] = questions[type]
? mergeQuestionEntries(questions[type], question)
: question;
if (type === "OP") {
paras.forEach((para) => {
if (!para.name) {
return;
}
const normalized = normalizeOpPara(para);
opItemsByName[para.name] = opItemsByName[para.name]
? mergeNormalizedOpEntries(opItemsByName[para.name], normalized)
: normalized;
});
}
});
return {
meta,
questionsByType: questions,
opItemsByName,
};
}
function mergeQuestionEntries(baseQuestion, nextQuestion) {
return {
...baseQuestion,
title: baseQuestion.title || nextQuestion.title,
score: {
total: sumFiniteNumbers(baseQuestion.score.total, nextQuestion.score.total),
real: sumFiniteNumbers(baseQuestion.score.real, nextQuestion.score.real),
raw: [baseQuestion.score.raw, nextQuestion.score.raw].filter(Boolean).join(" | "),
},
paras: baseQuestion.paras.concat(nextQuestion.paras),
decodedContent: {
docs: baseQuestion.decodedContent.docs.concat(nextQuestion.decodedContent.docs),
text: [baseQuestion.decodedContent.text, nextQuestion.decodedContent.text]
.filter(Boolean)
.join("\n")
.trim(),
imageCount:
baseQuestion.decodedContent.imageCount + nextQuestion.decodedContent.imageCount,
hasContent:
baseQuestion.decodedContent.hasContent || nextQuestion.decodedContent.hasContent,
},
};
}
function parsePara(paraNode) {
const hasDirectStdResult = !!findDirectChildByTagName(paraNode, "StdResult");
const hasDirectRealResult = !!findDirectChildByTagName(paraNode, "RealResult");
const rawStdResult = extractContentValue(paraNode, "StdResult");
const rawRealResult = extractContentValue(paraNode, "RealResult");
const para = {
sn: directText(paraNode, "Sn"),
name: directText(paraNode, "Name"),
varType: directText(paraNode, "VarType"),
totalScore: toNumber(directText(paraNode, "TotalScore")),
realScore: toNumber(directText(paraNode, "RealScore")),
teacherScore: toNumber(directText(paraNode, "TeacherScore")),
stdSource: directText(paraNode, "StdSource"),
stdResultShowInfo: directText(paraNode, "StdResultShowInfo"),
script: extractContentValue(paraNode, "Script"),
stdResult: hasDirectStdResult
? rawStdResult
: hasContentValue(rawStdResult)
? rawStdResult
: extractFallbackParaContent(paraNode, [
"StandardAnswer",
"StandardResult",
"CorrectAnswer",
"ExpectedAnswer",
"ExpectedResult",
"StdValue",
"DefaultValue",
]),
realResult: hasDirectRealResult
? rawRealResult
: hasContentValue(rawRealResult)
? rawRealResult
: extractFallbackParaContent(paraNode, [
"StudentAnswer",
"StudentResult",
"StuAnswer",
"StuResult",
"UserAnswer",
"UserResult",
"InputValue",
"RealValue",
"Value",
"Result",
"Answer",
]),
};
const numDateNode = paraNode.querySelector("JudgeRule NumDate");
if (numDateNode) {
para.numDate = cleanText(numDateNode.textContent);
}
const numTypeNode = paraNode.querySelector("JudgeRule NumType");
if (numTypeNode) {
para.numType = cleanText(numTypeNode.textContent);
}
return para;
}
function extractQuestionContent(questionNode) {
const docs = Array.from(questionNode.querySelectorAll("DOC"))
.map((docNode) => parseDocValue(docNode.textContent || ""))
.filter((item) => item.raw.length > 0 || item.text.length > 0 || item.imageCount > 0);
const textParts = docs.map((doc) => doc.text).filter(Boolean);
return {
docs,
text: textParts.join("\n").trim(),
imageCount: docs.reduce((sum, doc) => sum + doc.imageCount, 0),
hasContent:
docs.some((doc) => doc.text.trim().length > 0 || doc.imageCount > 0) ||
questionNode.textContent.trim().length > 0,
};
}
function extractContentValue(parentNode, tagName) {
const targetNode =
findDirectChildByTagName(parentNode, tagName) ||
Array.from(parentNode.getElementsByTagName(tagName))[0];
if (!targetNode) {
return createEmptyContentValue();
}
const docNode = targetNode.querySelector("DOC");
if (docNode) {
return parseDocValue(docNode.textContent || "");
}
const raw = targetNode.textContent || "";
return {
kind: "text",
raw,
text: cleanText(raw),
html: "",
imageCount: 0,
};
}
function extractFallbackParaContent(paraNode, tagNames) {
for (const tagName of tagNames) {
const content = extractContentValue(paraNode, tagName);
if (hasContentValue(content)) {
return content;
}
}
return createEmptyContentValue();
}
function findDirectChildByTagName(parentNode, tagName) {
return Array.from(parentNode.children || []).find(
(node) => String(node.tagName || "").toLowerCase() === String(tagName || "").toLowerCase()
);
}
function createEmptyContentValue() {
return {
kind: "empty",
raw: "",
text: "",
html: "",
imageCount: 0,
};
}
function parseDocValue(rawValue) {
const raw = typeof rawValue === "string" ? rawValue.trim() : "";
if (!raw) {
return { kind: "empty", raw: "", text: "", html: "", imageCount: 0 };
}
let decoded = raw;
let kind = "text";
if (looksLikeBase64(raw)) {
try {
decoded = decodeBase64Unicode(raw);
} catch (error) {
decoded = raw;
}
if (looksLikeImageData(decoded)) {
return {
kind: "html",
raw: "[图片数据]",
text: "",
html: decoded,
imageCount: Math.max(1, countImgInText(decoded)),
};
}
}
if (looksLikeImageData(raw)) {
return {
kind: "binary",
raw: "[图片数据]",
text: "",
html: "",
imageCount: 1,
};
}
if (looksLikeHtml(decoded)) {
const doc = new DOMParser().parseFromString(decoded, "text/html");
const text = cleanText(doc.body ? doc.body.textContent : decoded);
const imageCount = doc.images ? doc.images.length : 0;
return {
kind: "html",
raw,
text,
html: decoded,
imageCount,
};
}
return {
kind,
raw,
text: cleanText(decoded),
html: "",
imageCount: 0,
};
}
function normalizeOpPara(para) {
const base = {
...para,
label: para.name,
type: (para.varType || "").toLowerCase(),
};
if (base.type === "tlist") {
const submittedTable = parseTList(base.realResult.text || base.realResult.raw || "");
const standardTable = parseTList(base.stdResult.text || base.stdResult.raw || "");
return {
...base,
label: mapOpLabel(base.name),
parsedType: "table",
table: submittedTable.isValid ? submittedTable : standardTable,
submittedTable,
standardTable,
};
}
if (base.type === "double") {
const submittedRawText = getContentText(base.realResult);
const standardRawText = getContentText(base.stdResult);
const submittedValues = extractNumberList(submittedRawText);
const standardValues = extractNumberList(standardRawText);
const submittedValue = submittedValues[0];
const standardValue = standardValues[0];
if (submittedValues.length > 1 || standardValues.length > 1) {
return {
...base,
label: mapOpLabel(base.name),
parsedType: "numberList",
value: submittedRawText || standardRawText,
submittedValue,
standardValue,
submittedValues,
standardValues,
};
}
return {
...base,
label: mapOpLabel(base.name),
parsedType: "number",
value: Number.isFinite(submittedValue) ? submittedValue : standardValue,
submittedValue,
standardValue,
};
}
if (base.type === "drawing") {
const submittedImageCount =
base.realResult.imageCount || countImgInText(base.realResult.html || base.realResult.raw);
const standardImageCount =
base.stdResult.imageCount ||
countImgInText(base.stdResult.html || base.stdResult.raw);
return {
...base,
label: mapOpLabel(base.name),
parsedType: "drawing",
imageCount: submittedImageCount || standardImageCount,
submittedImageCount,
standardImageCount,
};
}
const submittedText = cleanText(base.realResult.text || base.realResult.raw);
const standardText = cleanText(base.stdResult.text || base.stdResult.raw);
return {
...base,
label: mapOpLabel(base.name),
parsedType: "text",
value: submittedText || standardText,
submittedText,
standardText,
};
}
function mergeNormalizedOpEntries(baseItem, nextItem) {
const typePriority = {
table: 4,
number: 3,
text: 2,
drawing: 1,
};
const preferred =
(typePriority[nextItem.parsedType] || 0) > (typePriority[baseItem.parsedType] || 0)
? nextItem
: baseItem;
return {
...preferred,
totalScore: sumFiniteNumbers(baseItem.totalScore, nextItem.totalScore),
realScore: sumFiniteNumbers(baseItem.realScore, nextItem.realScore),
teacherScore: sumFiniteNumbers(baseItem.teacherScore, nextItem.teacherScore),
script:
baseItem.script && nextItem.script
? {
...baseItem.script,
raw: [baseItem.script.raw, nextItem.script.raw].filter(Boolean).join(" | "),
text: [baseItem.script.text, nextItem.script.text].filter(Boolean).join(" | "),
}
: baseItem.script || nextItem.script,
stdResult:
getContentText(preferred.stdResult).length > 0 ? preferred.stdResult : baseItem.stdResult || nextItem.stdResult,
realResult:
getContentText(preferred.realResult).length > 0
? preferred.realResult
: baseItem.realResult || nextItem.realResult,
table:
preferred.table && preferred.table.isValid
? preferred.table
: baseItem.table && baseItem.table.isValid
? baseItem.table
: nextItem.table,
submittedTable:
preferred.submittedTable && preferred.submittedTable.isValid
? preferred.submittedTable
: baseItem.submittedTable && baseItem.submittedTable.isValid
? baseItem.submittedTable
: nextItem.submittedTable,
standardTable:
preferred.standardTable && preferred.standardTable.isValid
? preferred.standardTable
: baseItem.standardTable && baseItem.standardTable.isValid
? baseItem.standardTable
: nextItem.standardTable,
submittedValue: Number.isFinite(preferred.submittedValue)
? preferred.submittedValue
: Number.isFinite(baseItem.submittedValue)
? baseItem.submittedValue
: nextItem.submittedValue,
standardValue: Number.isFinite(preferred.standardValue)
? preferred.standardValue
: Number.isFinite(baseItem.standardValue)
? baseItem.standardValue
: nextItem.standardValue,
value:
preferred.value != null && preferred.value !== ""
? preferred.value
: baseItem.value != null && baseItem.value !== ""
? baseItem.value
: nextItem.value,
submittedText: preferred.submittedText || baseItem.submittedText || nextItem.submittedText,
standardText: preferred.standardText || baseItem.standardText || nextItem.standardText,
imageCount: Math.max(baseItem.imageCount || 0, nextItem.imageCount || 0),
submittedImageCount: Math.max(baseItem.submittedImageCount || 0, nextItem.submittedImageCount || 0),
standardImageCount: Math.max(baseItem.standardImageCount || 0, nextItem.standardImageCount || 0),
};
}
function parseTList(raw) {
const normalized = cleanText(raw);
if (!normalized) {
return { headers: [], rows: [], isValid: false };
}
const segments = normalized
.split(";")
.map((part) => part.trim())
.filter(Boolean);
const parsedRows = segments
.map((segment) => {
const match = segment.match(/^\((.*)\)$/);
if (!match) {
return null;
}
return splitCsvLike(match[1]);
})
.filter(Boolean);
if (parsedRows.length === 0) {
return { headers: [], rows: [], isValid: false };
}
const headers = parsedRows[0];
const rows = parsedRows.slice(1);
return { headers, rows, isValid: headers.length > 0 && rows.length > 0 };
}
function reviewReport(parsedReport) {
const issues = [];
const suggestions = [];
const metrics = [];
const markers = [];
const experimentKind = detectExperimentKind(parsedReport);
const isSpecialized = !!experimentKind;
const sectionBreakdown = buildSectionBreakdown(parsedReport);
const opBreakdown = buildOpBreakdown(parsedReport);
const tableBreakdown = buildTableBreakdown(parsedReport);
const calculationBreakdown = [];
const textBreakdown = buildTextBreakdown(parsedReport);
reviewGeneralQuestions(
parsedReport,
issues,
suggestions,
markers,
sectionBreakdown,
textBreakdown
);
reviewGeneralOpItems(parsedReport, issues, suggestions, tableBreakdown, opBreakdown);
const reviewers = {
photoelectric: reviewPhotoelectric,
collision: reviewCollision,
thermalExpansion: reviewThermalExpansion,
elasticModulus: reviewElasticModulus,
newtonRings: reviewNewtonRings,
surfaceTension: reviewSurfaceTension,
polarization: reviewPolarization,
thermocouple: reviewThermocouple,
standingWave: reviewStandingWave,
lengthDensity: reviewLengthDensity,
viscosity: reviewViscosity,
errorCompanion: reviewErrorCompanion,
wheatstone: reviewWheatstone,
oscilloscope: reviewOscilloscope,
hallEffect: reviewHallEffect,
};
const reviewer = reviewers[experimentKind];
if (reviewer) {
reviewer(parsedReport, issues, suggestions, metrics, markers, {
sectionBreakdown,
opBreakdown,
tableBreakdown,
calculationBreakdown,
textBreakdown,
});
} else {
metrics.push({
label: "实验类型",
value: parsedReport.meta.paperName || "未识别",
note: "当前未命中特定实验规则,已使用通用检查。",
});
}
const status = issues.length === 0 ? "pass" : "warn";
const summary =
status === "pass"
? "XML 结构完整,未发现明显缺项或计算异常。"
: `发现 ${issues.length} 项需要检查的内容,建议提交前核对。`;
metrics.unshift({
label: "总分",
value: formatScore(parsedReport.meta.realScore, parsedReport.meta.fullScore || parsedReport.meta.totalScore),
note: parsedReport.meta.totalScore !== parsedReport.meta.fullScore
? `机器评分 ${formatScore(parsedReport.meta.realScore, parsedReport.meta.totalScore)}`
: "",
});
return {
status,
summary,
metrics,
issues,
suggestions,
markers,
isSpecialized,
adaptationMessage: isSpecialized
? ""
: "当前报告还没有命中特定实验规则。你可以直接用“下载当前 XML”把这份报告反馈给我继续适配。",
sectionBreakdown,
opBreakdown,
tableBreakdown,
calculationBreakdown,
textBreakdown,
};
}
function reviewGeneralQuestions(
parsedReport,
issues,
suggestions,
markers,
sectionBreakdown,
textBreakdown
) {
["SYMD", "SYYL", "SYYQ", "SYBZ", "SKT", "SYZJ"].forEach((type) => {
const question = parsedReport.questionsByType[type];
const label = CONFIG.labels[type] || type;
if (!question) {
issues.push({
code: `${type}_missing`,
section: type,
message: `${label}缺失,XML 中未找到对应栏目。`,
});
markers.push({ section: type, label: "缺项" });
return;
}
const meaningfulText = getMeaningfulQuestionText(question);
const textLength = meaningfulText.length;
const imageCount = question.decodedContent.imageCount;
const hasContent = textLength > 0 || imageCount > 0;
if (!hasContent) {
issues.push({
code: `${type}_empty`,
section: type,
message: `${label}没有识别到文本或图片内容,建议检查 XML 是否完整。`,
});
markers.push({ section: type, label: "内容少" });
}
const sectionItem = sectionBreakdown.find((item) => item.type === type);
if (sectionItem && !sectionItem.analysis) {
sectionItem.analysis =
textLength > 0
? `识别到 ${textLength} 字文本。`
: imageCount > 0
? `图片型内容,共 ${imageCount} 张,不做文字建议。`
: "未识别到可分析内容。";
}
const textItem = textBreakdown.find((item) => item.type === type);
if (textItem && type === "SYZJ" && textLength > 0) {
const hasConclusion = /实验结论|结论/.test(meaningfulText);
const hasAnalysis = /分析讨论|分析|讨论/.test(meaningfulText);
textItem.analysis = hasConclusion && hasAnalysis
? "总结文本中已识别到“实验结论”和“分析讨论”两部分。"
: "总结文本存在实际内容,但未明显区分“实验结论”和“分析讨论”。";
}
});
}
function reviewGeneralOpItems(parsedReport, issues, suggestions, tableBreakdown, opBreakdown) {
Object.values(parsedReport.opItemsByName).forEach((item) => {
if (isAuxiliaryOpItem(item.name)) {
return;
}
if (item.parsedType === "table") {
if (!item.table.isValid) {
issues.push({
code: `${item.name}_table_invalid`,
section: "OP",
message: `${item.label}表格结构无法识别,可能存在括号或分隔符异常。`,
});
updateBreakdownAnalysis(opBreakdown, item.name, "表格结构异常,建议核对导出的行列格式。");
} else if (
item.table.rows.some((row) => row.length !== item.table.headers.length)
) {
issues.push({
code: `${item.name}_table_shape`,
section: "OP",
message: `${item.label}表格行列不一致,建议核对导出数据是否完整。`,
});
updateBreakdownAnalysis(opBreakdown, item.name, "表格中存在行列数不一致的记录。");
}
}
if (item.parsedType === "number" && !Number.isFinite(item.value)) {
issues.push({
code: `${item.name}_nan`,
section: "OP",
message: `${item.label}不是有效数字,建议检查 XML 中该项结果。`,
});
updateBreakdownAnalysis(opBreakdown, item.name, "当前未识别到有效数字。");
}
});
}
function reviewPhotoelectric(parsedReport, issues, suggestions, metrics, markers, detailState) {
const u0 = parsedReport.opItemsByName.U0;
const b2 = parsedReport.opItemsByName.B2;
const k = parsedReport.opItemsByName.k;
const h = parsedReport.opItemsByName.h;
const drawing = parsedReport.opItemsByName.FATX_Drawing;
const theoreticalPlanck = 6.62607015;
const theoreticalSlope = -(theoreticalPlanck / 16.02);
const u0Rows = tableToObjects(u0 && u0.table);
const standardU0Rows = tableToObjects(u0 && u0.standardTable);
const fit = computeLinearFit(
u0Rows.map((row) => ({
x: toNumber(firstNumberLike(row["ν/10^14Hz"])),
y: toNumber(firstNumberLike(row["Us/V"])),
}))
);
const studentK = numberValueOf(k);
const standardK = numberStandardValueOf(k);
const studentH = numberValueOf(h);
const standardH = numberStandardValueOf(h);
const recomputedH = Number.isFinite(fit.slope)
? Math.abs(fit.slope) * 16.02
: NaN;
const slopeBaseline = Number.isFinite(standardK) ? standardK : fit.slope;
const hBaseline = Number.isFinite(standardH) ? standardH : recomputedH;
const slopeDelta = Number.isFinite(studentK) && Number.isFinite(slopeBaseline)
? relativeDelta(Math.abs(studentK), Math.abs(slopeBaseline))
: Infinity;
const hDelta = Number.isFinite(studentH) && Number.isFinite(hBaseline)
? relativeDelta(studentH, hBaseline)
: Infinity;
metrics.push({
label: "U0 表格",
value: u0 && u0.table && u0.table.isValid ? `${u0.table.rows.length} 行学生作答` : "未识别",
note:
standardU0Rows.length > 0
? `标准答案 ${standardU0Rows.length} 行;填写内容将用于截止电压与频率关系拟合。`
: "填写内容将用于截止电压与频率关系拟合。",
});
metrics.push({
label: "斜率 k",
value: Number.isFinite(studentK) ? `${studentK}` : "缺失",
note: Number.isFinite(fit.slope)
? `重算值 ${roundTo(fit.slope, 3)}${Number.isFinite(standardK) ? `,标准答案 ${rawStandardDisplay(k, standardK, 3)}` : ""},理论参考 ${roundTo(theoreticalSlope, 3)}`
: "无法重算",
});
metrics.push({
label: "普朗克常数 h",
value: Number.isFinite(studentH) ? `${studentH}` : "缺失",
note: Number.isFinite(recomputedH)
? `重算值 ${roundTo(recomputedH, 2)}${Number.isFinite(standardH) ? `,标准答案 ${rawStandardDisplay(h, standardH, 2, "×10^-34 J*s")}` : ""},理论参考 ${roundTo(theoreticalPlanck, 2)} ×10^-34 J*s`
: "无法重算",
});
metrics.push({
label: "伏安特性图",
value: drawing && drawing.imageCount > 0 ? "已识别图像" : "未识别可分析图像",
note: b2 && b2.table && b2.table.isValid ? `表2 含 ${b2.table.rows.length} 行` : "",
});
if (!u0 || !u0.table || !u0.table.isValid) {
issues.push({
code: "U0_missing",
section: "OP",
message: "未识别到截止电压表 U0,无法检查频率-截止电压拟合结果。",
});
markers.push({ section: "OP", label: "U0缺失" });
} else {
if (u0.table.rows.length < 5) {
issues.push({
code: "U0_rows_short",
section: "OP",
message: "截止电压表数据点偏少,建议确认 5 个波长点是否完整。",
});
}
if (!Number.isFinite(fit.slope)) {
issues.push({
code: "U0_fit_failed",
section: "OP",
message: "截止电压表无法完成线性拟合,可能存在非数字或空值。",
});
}
}
if (!b2 || !b2.table || !b2.table.isValid) {
issues.push({
code: "B2_missing",
section: "OP",
message: "未识别到伏安特性表 B2,建议确认数据处理部分是否完整。",
});
} else if (b2.table.rows.length < CONFIG.thresholds.minB2Rows) {
issues.push({
code: "B2_too_short",
section: "OP",
message: "伏安特性表数据点偏少,建议确认是否完整导出。",
});
}
if (!Number.isFinite(studentK)) {
issues.push({
code: "k_missing",
section: "OP",
message: "XML 中未识别到学生作答的斜率 k。",
});
} else if (Number.isFinite(fit.slope)) {
if (slopeDelta > CONFIG.thresholds.slopeTolerance) {
issues.push({
code: "k_mismatch",
section: "OP",
message: Number.isFinite(standardK)
? `斜率 k 与标准答案偏差较大:学生作答=${studentK},标准答案=${rawStandardDisplay(k, standardK, 3)}。`
: `斜率 k 与重算值偏差较大:学生作答=${studentK},重算=${roundTo(fit.slope, 3)}。`,
});
markers.push({ section: "OP", label: "k偏差" });
}
}
if (!Number.isFinite(studentH)) {
issues.push({
code: "h_missing",
section: "OP",
message: "XML 中未识别到学生作答的普朗克常数 h。",
});
} else if (Number.isFinite(recomputedH)) {
if (hDelta > CONFIG.thresholds.planckTolerance) {
issues.push({
code: "h_mismatch",
section: "OP",
message: Number.isFinite(standardH)
? `普朗克常数 h 与标准答案偏差较大:学生作答=${studentH},标准答案=${rawStandardDisplay(h, standardH, 2, "×10^-34 J*s")}。`
: `普朗克常数 h 与重算值偏差较大:学生作答=${studentH},重算=${roundTo(recomputedH, 2)}。`,
});
markers.push({ section: "OP", label: "h偏差" });
}
}
detailState.calculationBreakdown.push(
{
label: "斜率 k",
scoreText: formatScoreText(k && k.realScore, k && k.totalScore),
studentValue: rawStudentDisplay(k, studentK, 3),
standardValue: rawStandardDisplay(k, standardK, 3),
computedValue: Number.isFinite(fit.slope) ? `${roundTo(fit.slope, 3)}` : "无法按 U0 表重算",
referenceValue: `${roundTo(theoreticalSlope, 3)}(由理论常数 h/e 换算)`,
deltaNote: Number.isFinite(slopeDelta)
? `学生作答与${Number.isFinite(standardK) ? "标准答案" : "重算值"}偏差 ${(slopeDelta * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(k, 3),
},
{
label: "普朗克常数 h",
scoreText: formatScoreText(h && h.realScore, h && h.totalScore),
studentValue: rawStudentDisplay(h, studentH, 2),
standardValue: rawStandardDisplay(h, standardH, 2, "×10^-34 J*s"),
computedValue: Number.isFinite(recomputedH)
? `${roundTo(recomputedH, 2)} ×10^-34 J*s`
: "无法按 k 重算",
referenceValue: `${roundTo(theoreticalPlanck, 2)} ×10^-34 J*s`,
deltaNote: Number.isFinite(hDelta)
? `学生作答与${Number.isFinite(standardH) ? "标准答案" : "重算值"}偏差 ${(hDelta * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(h, 3),
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"U0",
u0 && u0.table && u0.table.isValid
? `共 ${u0.table.rows.length} 行,列格式:${buildColumnNotes(u0.table).join(";")}`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B2",
b2 && b2.table && b2.table.isValid
? `共 ${b2.table.rows.length} 行,列格式:${buildColumnNotes(b2.table).join(";")}`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"k",
Number.isFinite(studentK) && Number.isFinite(fit.slope)
? `学生作答 ${studentK},按 U0 表重算 ${roundTo(fit.slope, 3)}${Number.isFinite(standardK) ? `,标准答案 ${rawStandardDisplay(k, standardK, 3)}` : ""},建议保留 ${formatItemPrecisionRequirement(k) || "3 位有效数字"}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"h",
Number.isFinite(studentH) && Number.isFinite(recomputedH)
? `学生作答 ${studentH},按 k 重算 ${roundTo(recomputedH, 2)}${Number.isFinite(standardH) ? `,标准答案 ${rawStandardDisplay(h, standardH, 2, "×10^-34 J*s")}` : ""},建议保留 ${formatItemPrecisionRequirement(h) || "3 位有效数字"}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"FATX_Drawing",
drawing && drawing.imageCount > 0
? "图像型数据已上传,不做数值建议。"
: "未识别到可分析图像内容。"
);
const summaryItem = detailState.textBreakdown.find((item) => item.type === "SYZJ");
if (summaryItem && summaryItem.analyzable) {
const hasConclusion = /实验结论|结论/.test(summaryItem.fullText);
const hasAnalysis = /分析讨论|分析|讨论/.test(summaryItem.fullText);
summaryItem.analysis = hasConclusion && hasAnalysis
? "总结文本已区分实验结论与分析讨论。"
: "总结文本有实际内容,但没有明显区分实验结论与分析讨论。";
}
}
function reviewCollision(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "B1", "弹性碰撞表");
renameBreakdownLabel(detailState.opBreakdown, "B2", "非完全弹性碰撞表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "弹性碰撞表");
renameBreakdownLabel(detailState.tableBreakdown, "B2", "非完全弹性碰撞表");
const m11 = resolveOpItem(parsedReport, "M11", "M1");
const m21 = resolveOpItem(parsedReport, "M21", "M2");
const x1 = resolveOpItem(parsedReport, "X1", "X");
const b1 = resolveOpItem(parsedReport, "B1");
const m12 = resolveOpItem(parsedReport, "M12", "M1");
const m22 = resolveOpItem(parsedReport, "M22", "M2");
const x2 = resolveOpItem(parsedReport, "X2", "X");
const b2 = resolveOpItem(parsedReport, "B2");
const elasticRows = computeCollisionRows(
b1 && b1.table,
"elastic",
numberValueOf(m11),
numberValueOf(m21),
numberValueOf(x1, 1)
);
const elasticStandardRows = computeCollisionRows(
b1 && b1.standardTable,
"elastic",
numberStandardValueOf(m11, numberValueOf(m11)),
numberStandardValueOf(m21, numberValueOf(m21)),
numberStandardValueOf(x1, numberValueOf(x1, 1))
);
const inelasticRows = computeCollisionRows(
b2 && b2.table,
"inelastic",
numberValueOf(m12),
numberValueOf(m22),
numberValueOf(x2, 1)
);
const inelasticStandardRows = computeCollisionRows(
b2 && b2.standardTable,
"inelastic",
numberStandardValueOf(m12, numberValueOf(m12)),
numberStandardValueOf(m22, numberValueOf(m22)),
numberStandardValueOf(x2, numberValueOf(x2, 1))
);
const elasticEAvg = average(elasticRows.map((row) => row.computedE));
const elasticRAvg = average(elasticRows.map((row) => row.computedR));
const elasticErrorAvg = average(elasticRows.map((row) => row.computedErrorPct));
const elasticFilledEAvg = average(elasticRows.map((row) => row.filledE));
const elasticFilledRAvg = average(elasticRows.map((row) => row.filledR));
const elasticFilledErrAvg = average(elasticRows.map((row) => row.filledErrorPct));
const elasticStandardEAvg = average(elasticStandardRows.map((row) => row.filledE));
const elasticStandardRAvg = average(elasticStandardRows.map((row) => row.filledR));
const elasticStandardErrAvg = average(elasticStandardRows.map((row) => row.filledErrorPct));
const inelasticEAvg = average(inelasticRows.map((row) => row.computedE));
const inelasticRAvg = average(inelasticRows.map((row) => row.computedR));
const inelasticErrorAvg = average(inelasticRows.map((row) => row.computedErrorPct));
const inelasticFilledEAvg = average(inelasticRows.map((row) => row.filledE));
const inelasticFilledRAvg = average(inelasticRows.map((row) => row.filledR));
const inelasticFilledErrAvg = average(inelasticRows.map((row) => row.filledErrorPct));
const inelasticStandardEAvg = average(inelasticStandardRows.map((row) => row.filledE));
const inelasticStandardRAvg = average(inelasticStandardRows.map((row) => row.filledR));
const inelasticStandardErrAvg = average(
inelasticStandardRows.map((row) => row.filledErrorPct)
);
metrics.push({
label: "弹性碰撞 B1",
value: b1 && b1.table && b1.table.isValid ? `${elasticRows.length} 次测量` : "未识别",
note:
Number.isFinite(elasticEAvg) && Number.isFinite(elasticErrorAvg)
? `e 均值 ${roundTo(elasticEAvg, 3)},E% 均值 ${roundTo(elasticErrorAvg, 2)}`
: "无法重算",
});
metrics.push({
label: "非完全弹性 B2",
value: b2 && b2.table && b2.table.isValid ? `${inelasticRows.length} 次测量` : "未识别",
note:
Number.isFinite(inelasticEAvg) && Number.isFinite(inelasticErrorAvg)
? `e 均值 ${roundTo(inelasticEAvg, 3)},E% 均值 ${roundTo(
inelasticErrorAvg,
2
)}`
: "无法重算",
});
metrics.push({
label: "参数组 1",
value:
Number.isFinite(numberValueOf(m11)) && Number.isFinite(numberValueOf(m21))
? `m1=${rawStudentDisplay(m11, numberValueOf(m11), 2, "g")},m2=${rawStudentDisplay(m21, numberValueOf(m21), 2, "g")}`
: "未识别",
note: Number.isFinite(numberValueOf(x1)) ? `挡光片宽度 X1=${numberValueOf(x1)} cm` : "",
});
metrics.push({
label: "参数组 2",
value:
Number.isFinite(numberValueOf(m12)) && Number.isFinite(numberValueOf(m22))
? `m1=${rawStudentDisplay(m12, numberValueOf(m12), 2, "g")},m2=${rawStudentDisplay(m22, numberValueOf(m22), 2, "g")}`
: "未识别",
note: Number.isFinite(numberValueOf(x2)) ? `挡光片宽度 X2=${numberValueOf(x2)} cm` : "",
});
if (!b1 || !b1.table || !b1.table.isValid) {
issues.push({
code: "collision_b1_missing",
section: "OP",
message: "未识别到弹性碰撞表 B1,无法检查碰前后速度与动量关系。",
});
markers.push({ section: "OP", label: "B1缺失" });
} else if (elasticRows.length < 3) {
issues.push({
code: "collision_b1_rows_short",
section: "OP",
message: "弹性碰撞表 B1 的测量次数少于 3 次,建议补足完整测量。",
});
} else {
if (Number.isFinite(elasticEAvg) && Math.abs(elasticEAvg - 1) > 0.05) {
issues.push({
code: "collision_b1_e_off",
section: "OP",
message: `弹性碰撞表 B1 的 e 均值为 ${roundTo(
elasticEAvg,
3
)},与理论上接近 1 的情况偏差较大。`,
});
}
if (Number.isFinite(elasticErrorAvg) && elasticErrorAvg > 2) {
issues.push({
code: "collision_b1_error_large",
section: "OP",
message: `弹性碰撞表 B1 的动量误差均值约 ${roundTo(
elasticErrorAvg,
2
)}%,建议复核时间测量与速度计算。`,
});
}
}
if (!b2 || !b2.table || !b2.table.isValid) {
issues.push({
code: "collision_b2_missing",
section: "OP",
message: "未识别到非完全弹性碰撞表 B2,无法检查恢复系数和动量守恒关系。",
});
markers.push({ section: "OP", label: "B2缺失" });
} else if (inelasticRows.length < 3) {
issues.push({
code: "collision_b2_rows_short",
section: "OP",
message: "非完全弹性碰撞表 B2 的测量次数少于 3 次,建议补足完整测量。",
});
} else {
if (Number.isFinite(inelasticEAvg) && (inelasticEAvg <= 0 || inelasticEAvg >= 1)) {
issues.push({
code: "collision_b2_e_range",
section: "OP",
message: `非完全弹性碰撞表 B2 的 e 均值为 ${roundTo(
inelasticEAvg,
3
)},通常应满足 0 < e < 1。`,
});
}
if (Number.isFinite(inelasticRAvg) && Math.abs(inelasticRAvg - 1) > 0.05) {
issues.push({
code: "collision_b2_r_off",
section: "OP",
message: `非完全弹性碰撞表 B2 的动量比 R 均值为 ${roundTo(
inelasticRAvg,
3
)},与 1 的偏差较大。`,
});
}
}
detailState.calculationBreakdown.push(
{
label: "弹性碰撞 B1",
scoreText: formatScoreText(b1 && b1.realScore, b1 && b1.totalScore),
studentValue:
Number.isFinite(elasticFilledEAvg) && Number.isFinite(elasticFilledRAvg)
? `表中 e 均值 ${roundTo(elasticFilledEAvg, 3)},R 均值 ${roundTo(
elasticFilledRAvg,
3
)}`
: "表中缺少可比对的 e / R 列",
standardValue:
Number.isFinite(elasticStandardEAvg) && Number.isFinite(elasticStandardRAvg)
? `标准答案 e 均值 ${roundTo(elasticStandardEAvg, 3)},R 均值 ${roundTo(
elasticStandardRAvg,
3
)}`
: "XML 未提供标准答案",
computedValue:
Number.isFinite(elasticEAvg) && Number.isFinite(elasticRAvg)
? `按 Δt 重算 e 均值 ${roundTo(elasticEAvg, 3)},R 均值 ${roundTo(
elasticRAvg,
3
)}`
: "无法按原始数据重算",
referenceValue: "弹性碰撞中通常 e≈1,动量比 R≈1",
deltaNote:
Number.isFinite(elasticFilledErrAvg) && Number.isFinite(elasticStandardErrAvg)
? `学生作答 E% 均值 ${roundTo(elasticFilledErrAvg, 2)}%,标准答案 ${roundTo(
elasticStandardErrAvg,
2
)}%`
: Number.isFinite(elasticFilledErrAvg) && Number.isFinite(elasticErrorAvg)
? `学生作答 E% 均值 ${roundTo(elasticFilledErrAvg, 2)}%,按原始数据重算 ${roundTo(
elasticErrorAvg,
2
)}%`
: "当前无法稳定比较 E% 列",
precisionNote: "建议 Δt 保留 2 位小数,速度与 e / R 列保留 2 至 3 位小数。",
},
{
label: "非完全弹性碰撞 B2",
scoreText: formatScoreText(b2 && b2.realScore, b2 && b2.totalScore),
studentValue:
Number.isFinite(inelasticFilledEAvg) && Number.isFinite(inelasticFilledRAvg)
? `表中 e 均值 ${roundTo(inelasticFilledEAvg, 3)},R 均值 ${roundTo(
inelasticFilledRAvg,
3
)}`
: "表中缺少可比对的 e / R 列",
standardValue:
Number.isFinite(inelasticStandardEAvg) && Number.isFinite(inelasticStandardRAvg)
? `标准答案 e 均值 ${roundTo(inelasticStandardEAvg, 3)},R 均值 ${roundTo(
inelasticStandardRAvg,
3
)}`
: "XML 未提供标准答案",
computedValue:
Number.isFinite(inelasticEAvg) && Number.isFinite(inelasticRAvg)
? `按 Δt 重算 e 均值 ${roundTo(inelasticEAvg, 3)},R 均值 ${roundTo(
inelasticRAvg,
3
)}`
: "无法按原始数据重算",
referenceValue: "非完全弹性碰撞中通常 0 < e < 1,动量比 R 仍应接近 1",
deltaNote:
Number.isFinite(inelasticFilledErrAvg) && Number.isFinite(inelasticStandardErrAvg)
? `学生作答 E% 均值 ${roundTo(
inelasticFilledErrAvg,
2
)}%,标准答案 ${roundTo(inelasticStandardErrAvg, 2)}%`
: Number.isFinite(inelasticFilledErrAvg) && Number.isFinite(inelasticErrorAvg)
? `学生作答 E% 均值 ${roundTo(
inelasticFilledErrAvg,
2
)}%,按原始数据重算 ${roundTo(inelasticErrorAvg, 2)}%`
: "当前无法稳定比较 E% 列",
precisionNote: "建议 Δt 保留 2 位小数,速度与 e / R 列保留 2 至 3 位小数。",
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${elasticRows.length} 次测量,重算 e 均值 ${roundTo(
elasticEAvg,
3
)},动量误差均值 ${roundTo(elasticErrorAvg, 2)}%。${
Number.isFinite(elasticStandardEAvg)
? ` 标准答案 e 均值 ${roundTo(elasticStandardEAvg, 3)}。`
: ""
}`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B2",
b2 && b2.table && b2.table.isValid
? `共 ${inelasticRows.length} 次测量,重算 e 均值 ${roundTo(
inelasticEAvg,
3
)},动量误差均值 ${roundTo(inelasticErrorAvg, 2)}%。${
Number.isFinite(inelasticStandardEAvg)
? ` 标准答案 e 均值 ${roundTo(inelasticStandardEAvg, 3)}。`
: ""
}`
: "未形成可分析表格。"
);
}
function reviewThermalExpansion(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "B1", "温升数据表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "温升数据表");
const t0 = parsedReport.opItemsByName.t0;
const stepT = parsedReport.opItemsByName.T;
const scaleCount = parsedReport.opItemsByName.ST;
const h = parsedReport.opItemsByName.H;
const h1 = parsedReport.opItemsByName.H1;
const l0 = parsedReport.opItemsByName.L0;
const delL = parsedReport.opItemsByName.delL;
const b1 = parsedReport.opItemsByName.B1;
const drawing = parsedReport.opItemsByName.tu1;
const k = parsedReport.opItemsByName.K;
const a = parsedReport.opItemsByName.a;
const b1Rows = tableToObjects(b1 && b1.table);
const fit = computeLinearFit(
b1Rows.map((row) => ({
x: toNumber(firstNumberLike(pickRowValue(row, [/△ti/, /Δti/, /ti-t0/] ))),
y: toNumber(firstNumberLike(pickRowValue(row, [/环数/]))),
}))
);
const xmlK = numberValueOf(k);
const standardK = numberStandardValueOf(k);
const xmlA = numberValueOf(a);
const standardA = numberStandardValueOf(a);
const paramT = numberValueOf(stepT);
const paramST = numberValueOf(scaleCount);
const paramH = numberValueOf(h);
const paramH1 = numberValueOf(h1);
const paramL0 = numberValueOf(l0);
const recomputedA =
Number.isFinite(fit.slope) &&
Number.isFinite(paramT) &&
Number.isFinite(paramST) &&
Number.isFinite(paramH) &&
Number.isFinite(paramH1) &&
Number.isFinite(paramL0) &&
paramST !== 0 &&
paramH1 !== 0 &&
paramL0 !== 0
? (fit.slope * paramT * paramH * 100000) / (paramST * paramH1 * paramL0)
: NaN;
const summaryText =
(parsedReport.questionsByType.SYZJ && parsedReport.questionsByType.SYZJ.decodedContent.text) ||
"";
const materialReference = /铝/.test(summaryText) ? 2.3 : NaN;
const kBaseline = Number.isFinite(standardK) ? standardK : fit.slope;
const aBaseline = Number.isFinite(standardA) ? standardA : recomputedA;
const kDelta =
Number.isFinite(xmlK) && Number.isFinite(kBaseline)
? relativeDelta(xmlK, kBaseline)
: Infinity;
const aDelta =
Number.isFinite(xmlA) && Number.isFinite(aBaseline)
? relativeDelta(xmlA, aBaseline)
: Infinity;
metrics.push({
label: "温升表 B1",
value: b1 && b1.table && b1.table.isValid ? `${b1.table.rows.length} 行数据` : "未识别",
note: Number.isFinite(fit.slope) ? `按环数-温升拟合得到 K≈${roundTo(fit.slope, 2)}` : "",
});
metrics.push({
label: "斜率 K",
value: Number.isFinite(xmlK) ? `${xmlK}` : "缺失",
note: Number.isFinite(fit.slope)
? `重算值 ${roundTo(fit.slope, 2)}${Number.isFinite(standardK) ? `,标准答案 ${rawStandardDisplay(k, standardK, 2)}` : ""}`
: "无法重算",
});
metrics.push({
label: "线膨胀系数 a",
value: Number.isFinite(xmlA) ? `${xmlA}` : "缺失",
note: Number.isFinite(recomputedA)
? `重算值 ${roundTo(recomputedA, 2)}${Number.isFinite(standardA) ? `,标准答案 ${rawStandardDisplay(a, standardA, 2, "×10^-5/℃")}` : ""}${Number.isFinite(materialReference) ? `,若按铝棒参考约 ${materialReference}` : ""}`
: "无法重算",
});
metrics.push({
label: "作图",
value: drawing && drawing.imageCount > 0 ? "已识别图像" : "未识别可分析图像",
note: Number.isFinite(numberValueOf(delL)) ? `填写伸长量 ΔL=${numberValueOf(delL)}` : "",
});
if (!b1 || !b1.table || !b1.table.isValid) {
issues.push({
code: "thermal_b1_missing",
section: "OP",
message: "未识别到温升数据表 B1,无法检查斜率 K 与线膨胀系数 a。",
});
markers.push({ section: "OP", label: "B1缺失" });
} else if (b1.table.rows.length < 5) {
issues.push({
code: "thermal_b1_rows_short",
section: "OP",
message: "温升数据表 B1 的数据点偏少,建议确认是否完整记录多组温升数据。",
});
}
if (!Number.isFinite(xmlK)) {
issues.push({
code: "thermal_k_missing",
section: "OP",
message: "未识别到斜率 K,无法继续核对线膨胀系数计算。",
});
} else if (Number.isFinite(fit.slope) && kDelta > 0.05) {
issues.push({
code: "thermal_k_mismatch",
section: "OP",
message: Number.isFinite(standardK)
? `斜率 K 与标准答案偏差较大:学生作答=${xmlK},标准答案=${rawStandardDisplay(k, standardK, 2)}。`
: `斜率 K 与按温升表重算值偏差较大:学生作答=${xmlK},重算=${roundTo(
fit.slope,
2
)}。`,
});
markers.push({ section: "OP", label: "K偏差" });
}
if (!Number.isFinite(xmlA)) {
issues.push({
code: "thermal_a_missing",
section: "OP",
message: "未识别到线膨胀系数 a。",
});
} else if (Number.isFinite(recomputedA) && aDelta > 0.1) {
issues.push({
code: "thermal_a_mismatch",
section: "OP",
message: Number.isFinite(standardA)
? `线膨胀系数 a 与标准答案偏差较大:学生作答=${xmlA},标准答案=${rawStandardDisplay(a, standardA, 2, "×10^-5/℃")}。`
: `线膨胀系数 a 与按 K 及实验参数重算值偏差较大:学生作答=${xmlA},重算=${roundTo(
recomputedA,
2
)}。`,
});
markers.push({ section: "OP", label: "a偏差" });
}
detailState.calculationBreakdown.push(
{
label: "斜率 K",
scoreText: formatScoreText(k && k.realScore, k && k.totalScore),
studentValue: rawStudentDisplay(k, xmlK, 2),
standardValue: rawStandardDisplay(k, standardK, 2),
computedValue: Number.isFinite(fit.slope)
? `${roundTo(fit.slope, 2)}(由环数-温升拟合)`
: "无法按 B1 表重算",
referenceValue: "K 应与温升图线斜率一致",
deltaNote: Number.isFinite(kDelta)
? `学生作答与${Number.isFinite(standardK) ? "标准答案" : "重算值"}偏差 ${(kDelta * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(k, 2),
},
{
label: "线膨胀系数 a",
scoreText: formatScoreText(a && a.realScore, a && a.totalScore),
studentValue: rawStudentDisplay(a, xmlA, 2, "×10^-5/℃"),
standardValue: rawStandardDisplay(a, standardA, 2, "×10^-5/℃"),
computedValue: Number.isFinite(recomputedA)
? `${roundTo(recomputedA, 2)} ×10^-5/℃`
: "无法按 K 与实验参数重算",
referenceValue: Number.isFinite(materialReference)
? `若按铝棒常用参考约 ${materialReference} ×10^-5/℃`
: "当前未识别出稳定的材料理论值",
deltaNote: Number.isFinite(aDelta)
? `学生作答与${Number.isFinite(standardA) ? "标准答案" : "重算值"}偏差 ${(aDelta * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(a, 2),
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${b1.table.rows.length} 行,按环数-温升拟合得到 K≈${roundTo(
fit.slope,
2
)}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"K",
Number.isFinite(xmlK) && Number.isFinite(fit.slope)
? `学生作答 ${xmlK},按 B1 表重算 ${roundTo(fit.slope, 2)}${Number.isFinite(standardK) ? `,标准答案 ${rawStandardDisplay(k, standardK, 2)}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"a",
Number.isFinite(xmlA) && Number.isFinite(recomputedA)
? `学生作答 ${xmlA},按 K 与参数重算 ${roundTo(recomputedA, 2)} ×10^-5/℃${Number.isFinite(standardA) ? `,标准答案 ${rawStandardDisplay(a, standardA, 2, "×10^-5/℃")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"delL",
Number.isFinite(numberValueOf(delL))
? `填写 ${numberValueOf(delL)},当前暂未建立稳定重算公式,不做偏差判断。`
: "未识别到伸长量数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"tu1",
drawing && drawing.imageCount > 0
? "图像型数据已上传,不做数值建议。"
: "未识别到可分析图像内容。"
);
}
function reviewSurfaceTension(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "B1", "砝码-电压标定表");
renameBreakdownLabel(detailState.opBreakdown, "B2", "液膜电压测量表");
renameBreakdownLabel(detailState.opBreakdown, "B3", "液膜电压平均值表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "砝码-电压标定表");
renameBreakdownLabel(detailState.tableBreakdown, "B2", "液膜电压测量表");
renameBreakdownLabel(detailState.tableBreakdown, "B3", "液膜电压平均值表");
const b1 = parsedReport.opItemsByName.B1;
const b2 = parsedReport.opItemsByName.B2;
const b3 = parsedReport.opItemsByName.B3;
const c = parsedReport.opItemsByName.XL_C;
const instrumentK = parsedReport.opItemsByName.LMD_K;
const sigma = parsedReport.opItemsByName.ZLXS;
const drawing = parsedReport.opItemsByName.BKD_SJB_Drawing;
const effectivePerimeter = 0.1073;
const calibrationFit = computeLinearFit(
tableToObjects(b1 && b1.table).map((row) => ({
x: toNumber(firstNumberLike(pickRowValue(row, [/质量/, /m\/g/]))),
y: toNumber(firstNumberLike(pickRowValue(row, [/电压/, /U\/mV/]))),
}))
);
const b2Analysis = computeSurfaceTensionB2Analysis(b2 && b2.table);
const b2StandardAnalysis = computeSurfaceTensionB2Analysis(b2 && b2.standardTable);
const b3Analysis = computeSurfaceTensionB3Deviation(b3 && b3.table, b2Analysis);
const recomputedSigma =
Number.isFinite(calibrationFit.slope) && Number.isFinite(b2Analysis.diffAverage)
? (b2Analysis.diffAverage / calibrationFit.slope) * 0.0098 / (2 * effectivePerimeter)
: NaN;
const studentC = numberValueOf(c);
const standardC = numberStandardValueOf(c);
const studentK = numberValueOf(instrumentK);
const standardK = numberStandardValueOf(instrumentK);
const studentSigma = numberValueOf(sigma);
const standardSigma = numberStandardValueOf(sigma);
const cBaseline = Number.isFinite(standardC) ? standardC : calibrationFit.slope;
const sigmaBaseline = Number.isFinite(standardSigma) ? standardSigma : recomputedSigma;
const cDelta =
Number.isFinite(studentC) && Number.isFinite(cBaseline)
? relativeDelta(studentC, cBaseline)
: Infinity;
const sigmaDelta =
Number.isFinite(studentSigma) && Number.isFinite(sigmaBaseline)
? relativeDelta(studentSigma, sigmaBaseline)
: Infinity;
metrics.push({
label: "标定表 B1",
value: b1 && b1.table && b1.table.isValid ? `${b1.table.rows.length} 行学生作答` : "未识别",
note: Number.isFinite(calibrationFit.slope)
? `按 U-m 拟合可重算 C≈${roundTo(calibrationFit.slope, 2)} mV/g`
: "当前无法按表格重算灵敏度。",
});
metrics.push({
label: "液膜电压差",
value: Number.isFinite(b2Analysis.diffAverage)
? `${roundTo(b2Analysis.diffAverage, 2)} mV`
: "未识别",
note: Number.isFinite(b2StandardAnalysis.diffAverage)
? `标准答案平均 U1-U2 = ${roundTo(b2StandardAnalysis.diffAverage, 2)} mV`
: "用于表面张力系数计算。",
});
metrics.push({
label: "表面张力系数 σ",
value: rawStudentDisplay(sigma, studentSigma, 4, "N/m"),
note: Number.isFinite(recomputedSigma)
? `按 B1/B2 重算约 ${roundTo(recomputedSigma, 4)} N/m`
: "当前无法稳定重算。",
});
if (b2Analysis.maxDeltaDeviation > 0.3) {
issues.push({
code: "surface_tension_delta_mismatch",
section: "OP",
message: `B2 表中的 U1-U2 与按 U1、U2 重算值最大偏差 ${roundTo(
b2Analysis.maxDeltaDeviation,
2
)} mV,建议逐行核对。`,
});
}
if (b3Analysis.maxDeviation > 0.3) {
issues.push({
code: "surface_tension_b3_mismatch",
section: "OP",
message: `B3 平均值表与 B2 表重算结果最大偏差 ${roundTo(
b3Analysis.maxDeviation,
2
)} mV,建议核对平均值填写。`,
});
}
if (Number.isFinite(cDelta) && cDelta > 0.02) {
issues.push({
code: "surface_tension_c_mismatch",
section: "OP",
message: Number.isFinite(standardC)
? `灵敏度 C 与标准答案偏差较大:学生作答=${studentC},标准答案=${rawStandardDisplay(c, standardC, 2, "mV/g")}。`
: `灵敏度 C 与按 B1 表重算值偏差较大:学生作答=${studentC},重算=${roundTo(
calibrationFit.slope,
2
)}。`,
});
markers.push({ section: "OP", label: "C偏差" });
}
if (Number.isFinite(sigmaDelta) && sigmaDelta > 0.03) {
issues.push({
code: "surface_tension_sigma_mismatch",
section: "OP",
message: Number.isFinite(standardSigma)
? `表面张力系数 σ 与标准答案偏差较大:学生作答=${studentSigma},标准答案=${roundTo(
standardSigma,
4
)}。`
: `表面张力系数 σ 与重算值偏差较大:学生作答=${studentSigma},重算=${roundTo(
recomputedSigma,
4
)}。`,
});
markers.push({ section: "OP", label: "σ偏差" });
}
detailState.calculationBreakdown.push(
{
label: "灵敏度 C",
scoreText: formatScoreText(c && c.realScore, c && c.totalScore),
studentValue: rawStudentDisplay(c, studentC, 2, "mV/g"),
standardValue: rawStandardDisplay(c, standardC, 2, "mV/g"),
computedValue: Number.isFinite(calibrationFit.slope)
? `${roundTo(calibrationFit.slope, 2)} mV/g(由 B1 线性拟合)`
: "无法按 B1 表重算",
referenceValue: "砝码-电压标定应近似线性,斜率即灵敏度 C",
deltaNote: Number.isFinite(cDelta)
? `学生作答与${Number.isFinite(standardC) ? "标准答案" : "重算值"}偏差 ${(cDelta * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(c, 3),
},
{
label: "仪器常数 K",
scoreText: formatScoreText(instrumentK && instrumentK.realScore, instrumentK && instrumentK.totalScore),
studentValue: rawStudentDisplay(instrumentK, studentK, 2),
standardValue: rawStandardDisplay(instrumentK, standardK, 2),
computedValue: "当前未建立稳定重算公式",
referenceValue: "此项请优先以 XML 中标准答案为准核对。",
deltaNote:
Number.isFinite(studentK) && Number.isFinite(standardK)
? `学生作答与标准答案偏差 ${(relativeDelta(studentK, standardK) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(instrumentK, 3),
},
{
label: "表面张力系数 σ",
scoreText: formatScoreText(sigma && sigma.realScore, sigma && sigma.totalScore),
studentValue: rawStudentDisplay(sigma, studentSigma, 4, "N/m"),
standardValue: rawStandardDisplay(sigma, standardSigma, 4, "N/m"),
computedValue: Number.isFinite(recomputedSigma)
? `${roundTo(recomputedSigma, 4)} N/m(按 ΔU/C 与环周长近似)`
: "无法按当前表格重算",
referenceValue: `按圆环法近似可用 σ≈(ΔU/C)g/(2l),当前取有效环周长 l≈${effectivePerimeter.toFixed(4)} m`,
deltaNote: Number.isFinite(sigmaDelta)
? `学生作答与${Number.isFinite(standardSigma) ? "标准答案" : "重算值"}偏差 ${(sigmaDelta * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(sigma, 4),
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${b1.table.rows.length} 行,按 m-U 拟合得到 C≈${roundTo(calibrationFit.slope, 2)} mV/g。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B2",
b2 && b2.table && b2.table.isValid
? `共 ${b2.table.rows.length} 次测量,U1-U2 平均值 ${roundTo(
b2Analysis.diffAverage,
2
)} mV,逐行最大偏差 ${roundTo(b2Analysis.maxDeltaDeviation, 2)} mV。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B3",
b3 && b3.table && b3.table.isValid
? `平均值表与 B2 重算结果最大偏差 ${roundTo(b3Analysis.maxDeviation, 2)} mV。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"XL_C",
Number.isFinite(studentC) && Number.isFinite(calibrationFit.slope)
? `学生作答 ${rawStudentDisplay(c, studentC, 2, "mV/g")},按 B1 表重算 ${roundTo(calibrationFit.slope, 2)} mV/g${
Number.isFinite(standardC) ? `,标准答案 ${rawStandardDisplay(c, standardC, 2, "mV/g")}` : ""
}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"LMD_K",
Number.isFinite(studentK)
? `学生作答 ${rawStudentDisplay(instrumentK, studentK, 2)}${Number.isFinite(standardK) ? `,标准答案 ${rawStandardDisplay(instrumentK, standardK, 2)}` : ""}。当前未建立稳定重算公式。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"ZLXS",
Number.isFinite(studentSigma)
? `学生作答 ${rawStudentDisplay(sigma, studentSigma, 4, "N/m")},按 B1/B2 近似重算 ${roundTo(
recomputedSigma,
4
)} N/m${Number.isFinite(standardSigma) ? `,标准答案 ${rawStandardDisplay(sigma, standardSigma, 4, "N/m")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"BKD_SJB_Drawing",
drawing && (drawing.imageCount > 0 || drawing.standardImageCount > 0)
? "图像型数据已提供,不做额外数值建议。"
: "未识别到可分析图像内容。"
);
}
function reviewPolarization(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "List_PZD", "偏振度测量表");
renameBreakdownLabel(detailState.opBreakdown, "List_PZDPJ", "偏振度平均值表");
renameBreakdownLabel(detailState.opBreakdown, "List_MLSDL", "马吕斯定律数据表");
renameBreakdownLabel(detailState.tableBreakdown, "List_PZD", "偏振度测量表");
renameBreakdownLabel(detailState.tableBreakdown, "List_PZDPJ", "偏振度平均值表");
renameBreakdownLabel(detailState.tableBreakdown, "List_MLSDL", "马吕斯定律数据表");
const pzd = parsedReport.opItemsByName.List_PZD;
const avgTable = parsedReport.opItemsByName.List_PZDPJ;
const v = parsedReport.opItemsByName.V;
const imax = parsedReport.opItemsByName.Imax;
const jd0 = parsedReport.opItemsByName.JD0;
const malus = parsedReport.opItemsByName.List_MLSDL;
const xlk = parsedReport.opItemsByName.XLK;
const drawing = parsedReport.opItemsByName.Imax_IUMap;
const pzdAnalysis = computePolarizationDegreeAnalysis(pzd && pzd.table);
const pzdStandardAnalysis = computePolarizationDegreeAnalysis(pzd && pzd.standardTable);
const malusAnalysis = computeMalusAnalysis(malus && malus.table);
const malusStandardAnalysis = computeMalusAnalysis(malus && malus.standardTable);
const studentV = numberValueOf(v);
const standardV = numberStandardValueOf(v);
const studentImax = numberValueOf(imax);
const standardImax = numberStandardValueOf(imax);
const studentJd0 = numberValueOf(jd0);
const standardJd0 = numberStandardValueOf(jd0);
const studentXlk = numberValueOf(xlk);
const standardXlk = numberStandardValueOf(xlk);
const vBaseline = Number.isFinite(standardV) ? standardV : pzdAnalysis.degree;
const xlkBaseline = Number.isFinite(standardXlk) ? standardXlk : malusAnalysis.fit.slope;
metrics.push({
label: "偏振度 V",
value: rawStudentDisplay(v, studentV, 3),
note: Number.isFinite(pzdAnalysis.degree)
? `按 Imax/Imin 重算 ${roundTo(pzdAnalysis.degree, 3)}`
: "当前无法由偏振度表重算。",
});
metrics.push({
label: "马吕斯拟合",
value:
malus && malus.table && malus.table.isValid ? `${malus.table.rows.length} 行学生作答` : "未识别",
note: Number.isFinite(malusAnalysis.fit.slope)
? `按 I-cos²θ 拟合得到斜率 ${roundTo(malusAnalysis.fit.slope, 3)}`
: "当前无法形成稳定线性拟合。",
});
if (Number.isFinite(studentV) && Number.isFinite(vBaseline) && relativeDelta(studentV, vBaseline) > 0.02) {
issues.push({
code: "polarization_v_mismatch",
section: "OP",
message: Number.isFinite(standardV)
? `偏振度 V 与标准答案偏差较大:学生作答=${studentV},标准答案=${rawStandardDisplay(v, standardV, 3)}。`
: `偏振度 V 与重算值偏差较大:学生作答=${studentV},重算=${roundTo(pzdAnalysis.degree, 3)}。`,
});
markers.push({ section: "OP", label: "V偏差" });
}
if (
Number.isFinite(studentXlk) &&
Number.isFinite(xlkBaseline) &&
relativeDelta(studentXlk, xlkBaseline) > 0.03
) {
issues.push({
code: "polarization_xlk_mismatch",
section: "OP",
message: Number.isFinite(standardXlk)
? `马吕斯线性系数 k 与标准答案偏差较大:学生作答=${studentXlk},标准答案=${rawStandardDisplay(xlk, standardXlk, 3)}。`
: `马吕斯线性系数 k 与拟合值偏差较大:学生作答=${studentXlk},拟合=${roundTo(malusAnalysis.fit.slope, 3)}。`,
});
markers.push({ section: "OP", label: "马吕斯拟合偏差" });
}
detailState.calculationBreakdown.push(
{
label: "偏振度 V",
scoreText: formatScoreText(v && v.realScore, v && v.totalScore),
studentValue: rawStudentDisplay(v, studentV, 3),
standardValue: rawStandardDisplay(v, standardV, 3),
computedValue: Number.isFinite(pzdAnalysis.degree)
? `${roundTo(pzdAnalysis.degree, 3)}(由 Imax/Imin 平均值计算)`
: "无法按偏振度表重算",
referenceValue: "V = (Imax - Imin) / (Imax + Imin)",
deltaNote:
Number.isFinite(studentV) && Number.isFinite(vBaseline)
? `学生作答与${Number.isFinite(standardV) ? "标准答案" : "重算值"}偏差 ${(relativeDelta(studentV, vBaseline) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(v, 3),
},
{
label: "马吕斯拟合 Imax",
scoreText: formatScoreText(imax && imax.realScore, imax && imax.totalScore),
studentValue: rawStudentDisplay(imax, studentImax, 3, "mW"),
standardValue: rawStandardDisplay(imax, standardImax, 3, "mW"),
computedValue: Number.isFinite(malusAnalysis.fit.slope + malusAnalysis.fit.intercept)
? `${roundTo(malusAnalysis.fit.slope + malusAnalysis.fit.intercept, 3)} mW(按 cos²θ=1 外推)`
: "无法按马吕斯表重算",
referenceValue: "I 与 cos²θ 近似线性时,cos²θ=1 对应最大透过强度",
deltaNote:
Number.isFinite(studentImax) && Number.isFinite(standardImax)
? `学生作答与标准答案偏差 ${(relativeDelta(studentImax, standardImax) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(imax, 4),
},
{
label: "马吕斯线性系数 k",
scoreText: formatScoreText(xlk && xlk.realScore, xlk && xlk.totalScore),
studentValue: rawStudentDisplay(xlk, studentXlk, 3),
standardValue: rawStandardDisplay(xlk, standardXlk, 3),
computedValue: Number.isFinite(malusAnalysis.fit.slope)
? `${roundTo(malusAnalysis.fit.slope, 3)}(由 I-cos²θ 线性拟合)`
: "无法按马吕斯表重算",
referenceValue: "斜率应接近偏振光最大透过强度",
deltaNote:
Number.isFinite(studentXlk) && Number.isFinite(xlkBaseline)
? `学生作答与${Number.isFinite(standardXlk) ? "标准答案" : "拟合值"}偏差 ${(relativeDelta(studentXlk, xlkBaseline) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(xlk, 3),
},
{
label: "偏振角 θ0",
scoreText: formatScoreText(jd0 && jd0.realScore, jd0 && jd0.totalScore),
studentValue: rawStudentDisplay(jd0, studentJd0, 1, "°"),
standardValue: rawStandardDisplay(jd0, standardJd0, 1, "°"),
computedValue: "当前未建立稳定重算公式",
referenceValue: "此项需要结合具体实验布置与图像信息判断",
deltaNote:
Number.isFinite(studentJd0) && Number.isFinite(standardJd0)
? `学生作答与标准答案偏差 ${(relativeDelta(studentJd0, standardJd0) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(jd0, 3),
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"List_PZD",
pzd && pzd.table && pzd.table.isValid
? `共 ${pzd.table.rows.length} 次测量,平均 Imax=${roundTo(pzdAnalysis.avgImax, 3)} mW,平均 Imin=${roundTo(pzdAnalysis.avgImin, 3)} mW,重算 V=${roundTo(pzdAnalysis.degree, 3)}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"List_PZDPJ",
avgTable && avgTable.table && avgTable.table.isValid
? `平均值表已提供${Number.isFinite(pzdStandardAnalysis.avgImax) ? `,标准答案平均 Imax=${roundTo(pzdStandardAnalysis.avgImax, 3)} mW` : ""}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"List_MLSDL",
malus && malus.table && malus.table.isValid
? `共 ${malus.table.rows.length} 行,按 I-cos²θ 拟合得到 k≈${roundTo(malusAnalysis.fit.slope, 3)}${Number.isFinite(malusStandardAnalysis.fit.slope) ? `,标准答案拟合约 ${roundTo(malusStandardAnalysis.fit.slope, 3)}` : ""}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"V",
Number.isFinite(studentV)
? `学生作答 ${rawStudentDisplay(v, studentV, 3)},按偏振度表重算 ${roundTo(pzdAnalysis.degree, 3)}${Number.isFinite(standardV) ? `,标准答案 ${rawStandardDisplay(v, standardV, 3)}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"Imax",
Number.isFinite(studentImax)
? `学生作答 ${rawStudentDisplay(imax, studentImax, 3, "mW")}${Number.isFinite(standardImax) ? `,标准答案 ${rawStandardDisplay(imax, standardImax, 3, "mW")}` : ""}。按马吕斯拟合外推约 ${roundTo(malusAnalysis.fit.slope + malusAnalysis.fit.intercept, 3)} mW。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"JD0",
Number.isFinite(studentJd0)
? `学生作答 ${rawStudentDisplay(jd0, studentJd0, 1, "°")}${Number.isFinite(standardJd0) ? `,标准答案 ${rawStandardDisplay(jd0, standardJd0, 1, "°")}` : ""}。当前未建立稳定重算公式。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"XLK",
Number.isFinite(studentXlk)
? `学生作答 ${rawStudentDisplay(xlk, studentXlk, 3)},按马吕斯表拟合 ${roundTo(malusAnalysis.fit.slope, 3)}${Number.isFinite(standardXlk) ? `,标准答案 ${rawStandardDisplay(xlk, standardXlk, 3)}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"Imax_IUMap",
drawing && (drawing.imageCount > 0 || drawing.standardImageCount > 0)
? "图像型数据已提供,不做额外数值建议。"
: "未识别到可分析图像内容。"
);
}
function reviewThermocouple(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "B1", "温差电偶测量表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "温差电偶测量表");
const t0 = parsedReport.opItemsByName.SW_t0;
const b1 = parsedReport.opItemsByName.B1;
const drawing = parsedReport.opItemsByName.BKD_SJB_Drawing;
const k = parsedReport.opItemsByName.WCXS_k;
const b1Analysis = computeThermocoupleAnalysis(b1 && b1.table, numberValueOf(t0));
const b1StandardAnalysis = computeThermocoupleAnalysis(
b1 && b1.standardTable,
numberStandardValueOf(t0)
);
const studentT0 = numberValueOf(t0);
const standardT0 = numberStandardValueOf(t0);
const studentK = numberValueOf(k);
const standardK = numberStandardValueOf(k);
const kBaseline = Number.isFinite(standardK) ? standardK : b1Analysis.microVoltSlope;
metrics.push({
label: "冷端温度 t0",
value: rawStudentDisplay(t0, studentT0, 1, "℃"),
note: Number.isFinite(standardT0) ? `标准答案 ${rawStandardDisplay(t0, standardT0, 1, "℃")}` : "",
});
metrics.push({
label: "温差系数 k",
value: rawStudentDisplay(k, studentK, 1, "μV/℃"),
note: Number.isFinite(b1Analysis.microVoltSlope)
? `按 B1 拟合重算 ${roundTo(b1Analysis.microVoltSlope, 1)} μV/℃`
: "当前无法由 B1 表重算。",
});
if (b1Analysis.maxDeltaDeviation > 0.3) {
issues.push({
code: "thermocouple_delta_mismatch",
section: "OP",
message: `B1 表中的 t-t0 与按 t、t0 重算值最大偏差 ${roundTo(
b1Analysis.maxDeltaDeviation,
2
)} ℃,建议逐行核对。`,
});
}
if (
Number.isFinite(studentK) &&
Number.isFinite(kBaseline) &&
relativeDelta(studentK, kBaseline) > 0.03
) {
issues.push({
code: "thermocouple_k_mismatch",
section: "OP",
message: Number.isFinite(standardK)
? `温差系数 k 与标准答案偏差较大:学生作答=${studentK},标准答案=${rawStandardDisplay(k, standardK, 1, "μV/℃")}。`
: `温差系数 k 与 B1 表拟合值偏差较大:学生作答=${studentK},拟合=${roundTo(
b1Analysis.microVoltSlope,
1
)}。`,
});
markers.push({ section: "OP", label: "k偏差" });
}
detailState.calculationBreakdown.push(
{
label: "冷端温度 t0",
scoreText: formatScoreText(t0 && t0.realScore, t0 && t0.totalScore),
studentValue: rawStudentDisplay(t0, studentT0, 1, "℃"),
standardValue: rawStandardDisplay(t0, standardT0, 1, "℃"),
computedValue: rawStudentDisplay(t0, studentT0, 1, "℃"),
referenceValue: "B1 表中的 t-t0 列应由 t 减去冷端温度 t0 得到",
deltaNote:
Number.isFinite(studentT0) && Number.isFinite(standardT0)
? `学生作答与标准答案偏差 ${(relativeDelta(studentT0, standardT0) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(t0, 3),
},
{
label: "温差系数 k",
scoreText: formatScoreText(k && k.realScore, k && k.totalScore),
studentValue: rawStudentDisplay(k, studentK, 1, "μV/℃"),
standardValue: rawStandardDisplay(k, standardK, 1, "μV/℃"),
computedValue: Number.isFinite(b1Analysis.microVoltSlope)
? `${roundTo(b1Analysis.microVoltSlope, 1)} μV/℃(由 E-Δt 拟合)`
: "无法按 B1 表重算",
referenceValue: "E 与 Δt 应近似线性,斜率即温差系数",
deltaNote:
Number.isFinite(studentK) && Number.isFinite(kBaseline)
? `学生作答与${Number.isFinite(standardK) ? "标准答案" : "拟合值"}偏差 ${(relativeDelta(studentK, kBaseline) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(k, 3),
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${b1.table.rows.length} 行,Δt 最大重算偏差 ${roundTo(b1Analysis.maxDeltaDeviation, 2)} ℃,E-Δt 拟合得到 k≈${roundTo(b1Analysis.microVoltSlope, 1)} μV/℃${Number.isFinite(b1StandardAnalysis.microVoltSlope) ? `,标准答案拟合约 ${roundTo(b1StandardAnalysis.microVoltSlope, 1)} μV/℃` : ""}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"SW_t0",
Number.isFinite(studentT0)
? `学生作答 ${rawStudentDisplay(t0, studentT0, 1, "℃")}${Number.isFinite(standardT0) ? `,标准答案 ${rawStandardDisplay(t0, standardT0, 1, "℃")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"WCXS_k",
Number.isFinite(studentK)
? `学生作答 ${rawStudentDisplay(k, studentK, 1, "μV/℃")},按 B1 表重算 ${roundTo(b1Analysis.microVoltSlope, 1)} μV/℃${Number.isFinite(standardK) ? `,标准答案 ${rawStandardDisplay(k, standardK, 1, "μV/℃")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"BKD_SJB_Drawing",
drawing && (drawing.imageCount > 0 || drawing.standardImageCount > 0)
? "图像型数据已提供,不做额外数值建议。"
: "未识别到可分析图像内容。"
);
}
function reviewStandingWave(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "BG", "驻波测量表");
renameBreakdownLabel(detailState.tableBreakdown, "BG", "驻波测量表");
const mass = parsedReport.opItemsByName.FM_m;
const force = parsedReport.opItemsByName.ZL_F;
const table = parsedReport.opItemsByName.BG;
const avg = parsedReport.opItemsByName.XXMD_average;
const uncertainty = parsedReport.opItemsByName.XXMD_Uncertainty;
const expression = parsedReport.opItemsByName.XXMD_Expression;
const error = parsedReport.opItemsByName.XD_WC;
const drawing = parsedReport.opItemsByName.BKD_SJB_Drawing;
const studentMass = numberValueOf(mass);
const standardMass = numberStandardValueOf(mass);
const studentForce = numberValueOf(force);
const standardForce = numberStandardValueOf(force);
const computedForce = Number.isFinite(studentMass) ? studentMass * 9.8 : NaN;
const tableAnalysis = computeStandingWaveAnalysis(table && table.table, computedForce);
const standardTableAnalysis = computeStandingWaveAnalysis(
table && table.standardTable,
Number.isFinite(standardMass) ? standardMass * 9.8 : standardForce
);
const studentAverage = numberValueOf(avg);
const standardAverage = numberStandardValueOf(avg);
const studentUncertainty = numberValueOf(uncertainty);
const standardUncertainty = numberStandardValueOf(uncertainty);
const studentExpression = getContentText(expression && expression.realResult);
const standardExpression = getContentText(expression && expression.stdResult);
const studentError = numberValueOf(error);
const standardError = numberStandardValueOf(error);
const computedExpression =
Number.isFinite(tableAnalysis.average) && Number.isFinite(tableAnalysis.maxDeviation)
? `${formatFixed(tableAnalysis.average, 2)}±${formatFixed(tableAnalysis.maxDeviation, 2)}`
: "";
const forceBaseline = Number.isFinite(standardForce) ? standardForce : computedForce;
const averageBaseline = Number.isFinite(standardAverage)
? standardAverage
: tableAnalysis.average;
metrics.push({
label: "张力 F",
value: rawStudentDisplay(force, studentForce, 5, "N"),
note: Number.isFinite(computedForce) ? `按砝码质量重算 ${roundTo(computedForce, 5)} N` : "",
});
metrics.push({
label: "线密度平均值",
value: rawStudentDisplay(avg, studentAverage, 3, "×10^-3 kg/m"),
note: Number.isFinite(tableAnalysis.average)
? `按 BG 表重算 ${roundTo(tableAnalysis.average, 3)} ×10^-3 kg/m`
: "当前无法由 BG 表重算。",
});
if (tableAnalysis.maxSigmaDeviation > 0.01) {
issues.push({
code: "standing_wave_sigma_mismatch",
section: "OP",
message: `驻波表中的 σ 列与按 F、n、L、f 重算值最大偏差 ${roundTo(
tableAnalysis.maxSigmaDeviation,
3
)} ×10^-3 kg/m,建议逐行核对。`,
});
}
if (
Number.isFinite(studentForce) &&
Number.isFinite(forceBaseline) &&
relativeDelta(studentForce, forceBaseline) > 0.01
) {
issues.push({
code: "standing_wave_force_mismatch",
section: "OP",
message: Number.isFinite(standardForce)
? `张力 F 与标准答案偏差较大:学生作答=${studentForce},标准答案=${rawStandardDisplay(force, standardForce, 5, "N")}。`
: `张力 F 与 m·g 重算值偏差较大:学生作答=${studentForce},重算=${roundTo(computedForce, 5)}。`,
});
markers.push({ section: "OP", label: "F偏差" });
}
if (
Number.isFinite(studentAverage) &&
Number.isFinite(averageBaseline) &&
relativeDelta(studentAverage, averageBaseline) > 0.02
) {
issues.push({
code: "standing_wave_average_mismatch",
section: "OP",
message: Number.isFinite(standardAverage)
? `线密度平均值与标准答案偏差较大:学生作答=${studentAverage},标准答案=${rawStandardDisplay(avg, standardAverage, 3, "×10^-3 kg/m")}。`
: `线密度平均值与 BG 表重算值偏差较大:学生作答=${studentAverage},重算=${roundTo(tableAnalysis.average, 3)}。`,
});
markers.push({ section: "OP", label: "线密度偏差" });
}
detailState.calculationBreakdown.push(
buildNumericCalculationRow({
label: "张力 F",
item: force,
studentValue: studentForce,
standardValue: standardForce,
computedValue: computedForce,
computedValueText: Number.isFinite(computedForce)
? `${roundTo(computedForce, 5)} N(按 m·g,g=9.8 m/s²)`
: "无法按砝码质量重算",
referenceValue: "张力 F = m·g",
baselineValue: forceBaseline,
baselineLabel: Number.isFinite(standardForce) ? "标准答案" : "重算值",
precisionFallback: 5,
studentSuffix: "N",
}),
buildNumericCalculationRow({
label: "线密度平均值",
item: avg,
studentValue: studentAverage,
standardValue: standardAverage,
computedValue: tableAnalysis.average,
computedValueText: Number.isFinite(tableAnalysis.average)
? `${roundTo(tableAnalysis.average, 3)} ×10^-3 kg/m(由 BG 表逐行重算后取平均)`
: "无法按 BG 表重算",
referenceValue: "本实验表中 n 表示节点数,重算时按有效阶次 (n-1) 使用:σ = F·(n-1)² / (4L²f²)",
baselineValue: averageBaseline,
baselineLabel: Number.isFinite(standardAverage) ? "标准答案" : "重算值",
precisionFallback: 3,
studentSuffix: "×10^-3 kg/m",
}),
buildNumericCalculationRow({
label: "线密度不确定度",
item: uncertainty,
studentValue: studentUncertainty,
standardValue: standardUncertainty,
computedValue: tableAnalysis.maxDeviation,
computedValueText: Number.isFinite(tableAnalysis.maxDeviation)
? `${roundTo(tableAnalysis.maxDeviation, 3)} ×10^-3 kg/m(按最大偏差近似)`
: "无法按 BG 表估计",
referenceValue: "当前以各行 σ 相对平均值的最大偏差作为近似不确定度",
baselineValue: tableAnalysis.maxDeviation,
baselineLabel: "近似重算值",
precisionFallback: 2,
studentSuffix: "×10^-3 kg/m",
}),
{
label: "线密度结果表达式",
scoreText: formatScoreText(expression && expression.realScore, expression && expression.totalScore),
studentValue: studentExpression || "未填写",
standardValue: standardExpression || "XML 未提供标准答案",
computedValue: computedExpression || "无法按当前数据重算",
referenceValue: "结果通常写成 平均值±不确定度",
deltaNote:
studentExpression && computedExpression
? normalizeResultExpression(studentExpression) === normalizeResultExpression(computedExpression)
? "学生作答与重算结果表达式一致。"
: `学生作答与重算结果表达式不一致:${studentExpression} vs ${computedExpression}`
: "当前无法计算偏差",
precisionNote: "建议主值与不确定度保留到同一小数位。",
},
buildNumericCalculationRow({
label: "相对误差",
item: error,
studentValue: studentError,
standardValue: standardError,
computedValue: NaN,
computedValueText: "当前未建立稳定理论参考值",
referenceValue: "优先以 XML 中标准答案为准核对。",
baselineValue: standardError,
baselineLabel: "标准答案",
precisionFallback: 2,
studentSuffix: "%",
})
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"BG",
table && table.table && table.table.isValid
? `共 ${table.table.rows.length} 行,按 F、(n-1)、L、f 重算 σ 最大偏差 ${roundTo(tableAnalysis.maxSigmaDeviation, 3)} ×10^-3 kg/m${Number.isFinite(standardTableAnalysis.average) ? `,标准答案平均值 ${standardTableAnalysis.average} ×10^-3 kg/m` : ""}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"FM_m",
Number.isFinite(studentMass)
? `砝码质量 ${rawStudentDisplay(mass, studentMass, 5, "kg")}${Number.isFinite(standardMass) ? `,标准答案 ${rawStandardDisplay(mass, standardMass, 5, "kg")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"ZL_F",
Number.isFinite(studentForce)
? `学生作答 ${rawStudentDisplay(force, studentForce, 5, "N")},按 m·g 重算 ${roundTo(computedForce, 5)} N${Number.isFinite(standardForce) ? `,标准答案 ${rawStandardDisplay(force, standardForce, 5, "N")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"XXMD_average",
Number.isFinite(studentAverage)
? `学生作答 ${rawStudentDisplay(avg, studentAverage, 3, "×10^-3 kg/m")},按 BG 表以 (n-1) 为有效阶次重算 ${roundTo(tableAnalysis.average, 3)} ×10^-3 kg/m${Number.isFinite(standardAverage) ? `,标准答案 ${rawStandardDisplay(avg, standardAverage, 3, "×10^-3 kg/m")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"XXMD_Uncertainty",
Number.isFinite(studentUncertainty)
? `学生作答 ${rawStudentDisplay(uncertainty, studentUncertainty, 2, "×10^-3 kg/m")},按 BG 表近似估计 ${roundTo(tableAnalysis.maxDeviation, 3)} ×10^-3 kg/m${Number.isFinite(standardUncertainty) ? `,标准答案 ${rawStandardDisplay(uncertainty, standardUncertainty, 2, "×10^-3 kg/m")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"XXMD_Expression",
studentExpression
? `学生作答 ${studentExpression}${standardExpression ? `,标准答案 ${standardExpression}` : ""}${computedExpression ? `,按当前数据重算 ${computedExpression}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"XD_WC",
Number.isFinite(studentError)
? `学生作答 ${rawStudentDisplay(error, studentError, 1, "%")}${Number.isFinite(standardError) ? `,标准答案 ${rawStandardDisplay(error, standardError, 1, "%")}` : ""}。当前未建立稳定理论参考值。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"BKD_SJB_Drawing",
drawing && (drawing.imageCount > 0 || drawing.standardImageCount > 0)
? "图像型数据已提供,不做额外数值建议。"
: "未识别到可分析图像内容。"
);
}
function reviewLengthDensity(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "B1", "空心圆柱尺寸表");
renameBreakdownLabel(detailState.opBreakdown, "B2", "螺旋测微器读数表");
renameBreakdownLabel(detailState.opBreakdown, "B3", "游标卡尺读数表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "空心圆柱尺寸表");
renameBreakdownLabel(detailState.tableBreakdown, "B2", "螺旋测微器读数表");
renameBreakdownLabel(detailState.tableBreakdown, "B3", "游标卡尺读数表");
const mass = parsedReport.opItemsByName.KXZZL_m;
const b1 = parsedReport.opItemsByName.B1;
const volume = parsedReport.opItemsByName.KXYZT_V;
const volumeExpr = parsedReport.opItemsByName.YZTTJ_Expression;
const density = parsedReport.opItemsByName.MD_p;
const densityErr = parsedReport.opItemsByName.LDDS;
const b2 = parsedReport.opItemsByName.B2;
const microAvg = parsedReport.opItemsByName.ZJ_Average;
const microExpr = parsedReport.opItemsByName.ZJ_Expression;
const b3 = parsedReport.opItemsByName.B3;
const caliperAvg = parsedReport.opItemsByName.ZJ_PJZ;
const caliperExpr = parsedReport.opItemsByName.JSZJ_xpression;
const b1Analysis = computeLengthDensityB1Analysis(b1 && b1.table);
const b1StandardAnalysis = computeLengthDensityB1Analysis(b1 && b1.standardTable);
const studentMass = numberValueOf(mass);
const standardMass = numberStandardValueOf(mass);
const studentVolume = numberValueOf(volume);
const standardVolume = numberStandardValueOf(volume);
const studentDensity = numberValueOf(density);
const standardDensity = numberStandardValueOf(density);
const studentMicroAvg = numberValueOf(microAvg);
const standardMicroAvg = numberStandardValueOf(microAvg);
const studentCaliperAvg = numberValueOf(caliperAvg);
const standardCaliperAvg = numberStandardValueOf(caliperAvg);
const computedDensity =
Number.isFinite(studentMass) && Number.isFinite(b1Analysis.volumeCm3)
? studentMass / b1Analysis.volumeCm3
: NaN;
const densityBaseline = Number.isFinite(standardDensity) ? standardDensity : computedDensity;
const microTableAnalysis = computeSimpleAverageTableAnalysis(b2 && b2.table, [/d\/mm/]);
const microTableStandardAnalysis = computeSimpleAverageTableAnalysis(
b2 && b2.standardTable,
[/d\/mm/]
);
const caliperTableAnalysis = computeCaliperDifferenceAnalysis(b3 && b3.table);
const caliperTableStandardAnalysis = computeCaliperDifferenceAnalysis(b3 && b3.standardTable);
metrics.push({
label: "空心圆柱体积 V",
value: rawStudentDisplay(volume, studentVolume, 3, "cm^3"),
note: Number.isFinite(b1Analysis.volumeCm3)
? `按 B1 表重算 ${roundTo(b1Analysis.volumeCm3, 3)} cm^3`
: "当前无法由 B1 表重算。",
});
metrics.push({
label: "密度 ρ",
value: rawStudentDisplay(density, studentDensity, 3, "g/cm^3"),
note: Number.isFinite(computedDensity)
? `按 m/V 重算 ${roundTo(computedDensity, 3)} g/cm^3`
: "当前无法稳定重算。",
});
if (b1Analysis.maxDimensionDeviation > 0.01) {
issues.push({
code: "length_density_b1_dimension_mismatch",
section: "OP",
message: `B1 表平均值与逐项数据重算结果最大偏差 ${roundTo(
b1Analysis.maxDimensionDeviation,
3
)} mm,建议核对平均值列。`,
});
}
if (
Number.isFinite(studentVolume) &&
Number.isFinite(b1Analysis.volumeCm3) &&
relativeDelta(studentVolume, Number.isFinite(standardVolume) ? standardVolume : b1Analysis.volumeCm3) > 0.01
) {
issues.push({
code: "length_density_volume_mismatch",
section: "OP",
message: Number.isFinite(standardVolume)
? `空心圆柱体积 V 与标准答案偏差较大:学生作答=${studentVolume},标准答案=${rawStandardDisplay(volume, standardVolume, 3, "cm^3")}。`
: `空心圆柱体积 V 与重算值偏差较大:学生作答=${studentVolume},重算=${roundTo(
b1Analysis.volumeCm3,
3
)}。`,
});
markers.push({ section: "OP", label: "体积偏差" });
}
if (
Number.isFinite(studentDensity) &&
Number.isFinite(densityBaseline) &&
relativeDelta(studentDensity, densityBaseline) > 0.01
) {
issues.push({
code: "length_density_density_mismatch",
section: "OP",
message: Number.isFinite(standardDensity)
? `密度 ρ 与标准答案偏差较大:学生作答=${studentDensity},标准答案=${rawStandardDisplay(density, standardDensity, 3, "g/cm^3")}。`
: `密度 ρ 与 m/V 重算值偏差较大:学生作答=${studentDensity},重算=${roundTo(
computedDensity,
3
)}。`,
});
markers.push({ section: "OP", label: "密度偏差" });
}
detailState.calculationBreakdown.push(
buildNumericCalculationRow({
label: "空心圆柱体积 V",
item: volume,
studentValue: studentVolume,
standardValue: standardVolume,
computedValue: b1Analysis.volumeCm3,
computedValueText: Number.isFinite(b1Analysis.volumeCm3)
? `${roundTo(b1Analysis.volumeCm3, 3)} cm^3(由 h、D、d 重算)`
: "无法按 B1 表重算",
referenceValue: "V = πh(D²-d²)/4,B1 原始量按 mm 读数,结果换算为 cm³",
baselineValue: Number.isFinite(standardVolume) ? standardVolume : b1Analysis.volumeCm3,
baselineLabel: Number.isFinite(standardVolume) ? "标准答案" : "重算值",
precisionFallback: 4,
studentSuffix: "cm^3",
}),
buildNumericCalculationRow({
label: "密度 ρ",
item: density,
studentValue: studentDensity,
standardValue: standardDensity,
computedValue: computedDensity,
computedValueText: Number.isFinite(computedDensity)
? `${roundTo(computedDensity, 3)} g/cm^3(由 m/V 重算)`
: "无法按当前数据重算",
referenceValue: "ρ = m / V",
baselineValue: densityBaseline,
baselineLabel: Number.isFinite(standardDensity) ? "标准答案" : "重算值",
precisionFallback: 4,
studentSuffix: "g/cm^3",
}),
buildNumericCalculationRow({
label: "螺旋测微器平均值",
item: microAvg,
studentValue: studentMicroAvg,
standardValue: standardMicroAvg,
computedValue: microTableAnalysis.average,
computedValueText: Number.isFinite(microTableAnalysis.average)
? `${roundTo(microTableAnalysis.average, 3)} mm(由 B2 表平均)`
: "无法按 B2 表重算",
referenceValue: "螺旋测微器结果取 6 次读数平均值",
baselineValue: Number.isFinite(standardMicroAvg)
? standardMicroAvg
: microTableAnalysis.average,
baselineLabel: Number.isFinite(standardMicroAvg) ? "标准答案" : "重算值",
precisionFallback: 4,
studentSuffix: "mm",
}),
buildNumericCalculationRow({
label: "游标卡尺平均值",
item: caliperAvg,
studentValue: studentCaliperAvg,
standardValue: standardCaliperAvg,
computedValue: caliperTableAnalysis.average,
computedValueText: Number.isFinite(caliperTableAnalysis.average)
? `${roundTo(caliperTableAnalysis.average, 3)} mm(由 B3 表平均)`
: "无法按 B3 表重算",
referenceValue: "d = |d2-d1|,再对 6 次结果取平均",
baselineValue: Number.isFinite(standardCaliperAvg)
? standardCaliperAvg
: caliperTableAnalysis.average,
baselineLabel: Number.isFinite(standardCaliperAvg) ? "标准答案" : "重算值",
precisionFallback: 4,
studentSuffix: "mm",
})
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${b1.table.rows.length} 行,按平均值列重算 V=${roundTo(b1Analysis.volumeCm3, 3)} cm^3,平均值列最大偏差 ${roundTo(b1Analysis.maxDimensionDeviation, 3)} mm${Number.isFinite(b1StandardAnalysis.volumeCm3) ? `,标准答案表重算 ${roundTo(b1StandardAnalysis.volumeCm3, 3)} cm^3` : ""}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B2",
b2 && b2.table && b2.table.isValid
? `共 ${b2.table.rows.length} 次测量,平均值 ${roundTo(microTableAnalysis.average, 3)} mm${Number.isFinite(microTableStandardAnalysis.average) ? `,标准答案表平均 ${roundTo(microTableStandardAnalysis.average, 3)} mm` : ""}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B3",
b3 && b3.table && b3.table.isValid
? `共 ${b3.table.rows.length} 次测量,按 |d2-d1| 重算平均值 ${roundTo(caliperTableAnalysis.average, 3)} mm${Number.isFinite(caliperTableStandardAnalysis.average) ? `,标准答案表平均 ${roundTo(caliperTableStandardAnalysis.average, 3)} mm` : ""}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"KXYZT_V",
Number.isFinite(studentVolume)
? `学生作答 ${rawStudentDisplay(volume, studentVolume, 3, "cm^3")},按 B1 表重算 ${roundTo(b1Analysis.volumeCm3, 3)} cm^3${Number.isFinite(standardVolume) ? `,标准答案 ${rawStandardDisplay(volume, standardVolume, 3, "cm^3")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"MD_p",
Number.isFinite(studentDensity)
? `学生作答 ${rawStudentDisplay(density, studentDensity, 3, "g/cm^3")},按 m/V 重算 ${roundTo(computedDensity, 3)} g/cm^3${Number.isFinite(standardDensity) ? `,标准答案 ${rawStandardDisplay(density, standardDensity, 3, "g/cm^3")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"ZJ_Average",
Number.isFinite(studentMicroAvg)
? `学生作答 ${rawStudentDisplay(microAvg, studentMicroAvg, 3, "mm")},按 B2 表重算 ${roundTo(microTableAnalysis.average, 3)} mm${Number.isFinite(standardMicroAvg) ? `,标准答案 ${rawStandardDisplay(microAvg, standardMicroAvg, 3, "mm")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"ZJ_PJZ",
Number.isFinite(studentCaliperAvg)
? `学生作答 ${rawStudentDisplay(caliperAvg, studentCaliperAvg, 3, "mm")},按 B3 表重算 ${roundTo(caliperTableAnalysis.average, 3)} mm${Number.isFinite(standardCaliperAvg) ? `,标准答案 ${rawStandardDisplay(caliperAvg, standardCaliperAvg, 3, "mm")}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"YZTTJ_Expression",
getContentText(volumeExpr && volumeExpr.realResult) || getContentText(volumeExpr && volumeExpr.stdResult)
? `结果表达式:学生作答 ${getContentText(volumeExpr && volumeExpr.realResult) || "未填写"}${getContentText(volumeExpr && volumeExpr.stdResult) ? `,标准答案 ${getContentText(volumeExpr.stdResult)}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"JSZJ_xpression",
getContentText(caliperExpr && caliperExpr.realResult) || getContentText(caliperExpr && caliperExpr.stdResult)
? `结果表达式:学生作答 ${getContentText(caliperExpr && caliperExpr.realResult) || "未填写"}${getContentText(caliperExpr && caliperExpr.stdResult) ? `,标准答案 ${getContentText(caliperExpr.stdResult)}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"ZJ_Expression",
getContentText(microExpr && microExpr.realResult) || getContentText(microExpr && microExpr.stdResult)
? `结果表达式:学生作答 ${getContentText(microExpr && microExpr.realResult) || "未填写"}${getContentText(microExpr && microExpr.stdResult) ? `,标准答案 ${getContentText(microExpr.stdResult)}` : ""}。`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"LDDS",
Number.isFinite(numberValueOf(densityErr))
? `密度相对误差 ${rawStudentDisplay(densityErr, numberValueOf(densityErr), 4)}${Number.isFinite(numberStandardValueOf(densityErr)) ? `,标准答案 ${rawStandardDisplay(densityErr, numberStandardValueOf(densityErr), 4)}` : ""}。`
: "缺少可分析数值。"
);
}
function reviewViscosity(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "ZTF_List1", "转筒法原始计时表");
renameBreakdownLabel(detailState.opBreakdown, "ZTF_List2", "转筒法平均值与不确定度表");
renameBreakdownLabel(detailState.opBreakdown, "ZTF_List21", "转筒法黏度不确定度表");
renameBreakdownLabel(detailState.opBreakdown, "LQF_List3", "落球法原始计时表");
renameBreakdownLabel(detailState.opBreakdown, "LQF_List4", "落球法平均值与不确定度表");
renameBreakdownLabel(detailState.opBreakdown, "LQF_List5", "落球法黏度不确定度表");
renameBreakdownLabel(detailState.tableBreakdown, "ZTF_List1", "转筒法原始计时表");
renameBreakdownLabel(detailState.tableBreakdown, "ZTF_List2", "转筒法平均值与不确定度表");
renameBreakdownLabel(detailState.tableBreakdown, "ZTF_List21", "转筒法黏度不确定度表");
renameBreakdownLabel(detailState.tableBreakdown, "LQF_List3", "落球法原始计时表");
renameBreakdownLabel(detailState.tableBreakdown, "LQF_List4", "落球法平均值与不确定度表");
renameBreakdownLabel(detailState.tableBreakdown, "LQF_List5", "落球法黏度不确定度表");
const ztfList1 = parsedReport.opItemsByName.ZTF_List1;
const ztfList2 = parsedReport.opItemsByName.ZTF_List2;
const ztf40 = parsedReport.opItemsByName.ZTF_NZ40;
const ztf60 = parsedReport.opItemsByName.ZTF_NZ60;
const ztf80 = parsedReport.opItemsByName.ZTF_NZ80;
const lqfList3 = parsedReport.opItemsByName.LQF_List3;
const lqfList4 = parsedReport.opItemsByName.LQF_List4;
const lqf20 = parsedReport.opItemsByName.LQF_NZ20;
const lqf15 = parsedReport.opItemsByName.LQF_NZ15;
const lqfL = parsedReport.opItemsByName.LQF_L;
const ztfAnalysis = computeViscosityRotatingCylinderAnalysis(ztfList1 && ztfList1.table, {
0.04: numberStandardValueOf(ztf40),
0.06: numberStandardValueOf(ztf60),
0.08: numberStandardValueOf(ztf80),
});
const ztfList2Analysis = computeViscosityAverageTableAnalysis(ztfList2 && ztfList2.table);
const lqfAnalysis = computeViscosityFallingBallAnalysis(
lqfList3 && lqfList3.table,
{
2.0: numberStandardValueOf(lqf20),
1.5: numberStandardValueOf(lqf15),
},
numberValueOf(lqfL)
);
const lqfList4Analysis = computeViscosityAverageTableAnalysis(lqfList4 && lqfList4.table);
const ztf40Student = numberValueOf(ztf40);
const ztf60Student = numberValueOf(ztf60);
const ztf80Student = numberValueOf(ztf80);
const ztf40Standard = numberStandardValueOf(ztf40);
const ztf60Standard = numberStandardValueOf(ztf60);
const ztf80Standard = numberStandardValueOf(ztf80);
const lqf20Student = numberValueOf(lqf20);
const lqf15Student = numberValueOf(lqf15);
const lqf20Standard = numberStandardValueOf(lqf20);
const lqf15Standard = numberStandardValueOf(lqf15);
metrics.push({
label: "转筒法黏度",
value: Number.isFinite(ztfAnalysis.averageEta)
? `${roundTo(ztfAnalysis.averageEta, 3)} Pa*s`
: "未识别",
note: Number.isFinite(ztfAnalysis.coefficient)
? `按标准答案反推系数后重算,平均约 ${roundTo(ztfAnalysis.averageEta, 3)} Pa*s`
: "当前无法稳定重算。",
});
metrics.push({
label: "落球法黏度",
value: Number.isFinite(lqfAnalysis.averageEta)
? `${roundTo(lqfAnalysis.averageEta, 3)} Pa*s`
: "未识别",
note: Number.isFinite(lqfAnalysis.coefficient)
? `按标准答案反推系数后重算,平均约 ${roundTo(lqfAnalysis.averageEta, 3)} Pa*s`
: "当前无法稳定重算。",
});
appendViscosityCalculation(
detailState.calculationBreakdown,
ztf40,
"转筒法 0.0400kg 黏度",
ztf40Student,
ztf40Standard,
ztfAnalysis.byMass["0.0400"],
"按同组标准答案反推转筒法比例系数后,由 m·t_average 重算"
);
appendViscosityCalculation(
detailState.calculationBreakdown,
ztf60,
"转筒法 0.0600kg 黏度",
ztf60Student,
ztf60Standard,
ztfAnalysis.byMass["0.0600"],
"按同组标准答案反推转筒法比例系数后,由 m·t_average 重算"
);
appendViscosityCalculation(
detailState.calculationBreakdown,
ztf80,
"转筒法 0.0800kg 黏度",
ztf80Student,
ztf80Standard,
ztfAnalysis.byMass["0.0800"],
"按同组标准答案反推转筒法比例系数后,由 m·t_average 重算"
);
appendViscosityCalculation(
detailState.calculationBreakdown,
lqf20,
"落球法 2.000mm 黏度",
lqf20Student,
lqf20Standard,
lqfAnalysis.byDiameter["2.000"],
"按同组标准答案反推落球法比例系数后,由 d²·t_average 重算"
);
appendViscosityCalculation(
detailState.calculationBreakdown,
lqf15,
"落球法 1.500mm 黏度",
lqf15Student,
lqf15Standard,
lqfAnalysis.byDiameter["1.500"],
"按同组标准答案反推落球法比例系数后,由 d²·t_average 重算"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"ZTF_List1",
ztfList1 && ztfList1.table && ztfList1.table.isValid
? `共 ${ztfList1.table.rows.length} 次测量,按标准答案反推系数重算三组黏度平均约 ${roundTo(ztfAnalysis.averageEta, 3)} Pa*s。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"ZTF_List2",
ztfList2 && ztfList2.table && ztfList2.table.isValid
? `平均值表已提供,按表中 t_average 可直接重算三组黏度;当前与原始表核对最大偏差 ${roundTo(ztfList2Analysis.maxDeviation, 3)} s。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"LQF_List3",
lqfList3 && lqfList3.table && lqfList3.table.isValid
? `共 ${lqfList3.table.rows.length} 行,按标准答案反推系数重算两组黏度平均约 ${roundTo(lqfAnalysis.averageEta, 3)} Pa*s。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"LQF_List4",
lqfList4 && lqfList4.table && lqfList4.table.isValid
? `平均值表已提供,按表中 t_average 可直接重算两组黏度;当前与原始表核对最大偏差 ${roundTo(lqfList4Analysis.maxDeviation, 3)} s。`
: "未形成可分析表格。"
);
maybePushValueIssue(issues, markers, "OP", "转筒法 0.0400kg 黏度", ztf40Student, ztf40Standard, ztfAnalysis.byMass["0.0400"], "转筒40偏差");
maybePushValueIssue(issues, markers, "OP", "转筒法 0.0600kg 黏度", ztf60Student, ztf60Standard, ztfAnalysis.byMass["0.0600"], "转筒60偏差");
maybePushValueIssue(issues, markers, "OP", "转筒法 0.0800kg 黏度", ztf80Student, ztf80Standard, ztfAnalysis.byMass["0.0800"], "转筒80偏差");
maybePushValueIssue(issues, markers, "OP", "落球法 2.000mm 黏度", lqf20Student, lqf20Standard, lqfAnalysis.byDiameter["2.000"], "落球2.0偏差");
maybePushValueIssue(issues, markers, "OP", "落球法 1.500mm 黏度", lqf15Student, lqf15Standard, lqfAnalysis.byDiameter["1.500"], "落球1.5偏差");
}
function computeLengthDensityB1Analysis(table) {
const rows = tableToObjects(table);
const getRow = (matcher) => rows.find((row) => matcher.test(cleanText(row["次数"] || row[""] || "")));
const hRow = getRow(/高h/);
const dRow = getRow(/内径d/);
const dOuterRow = getRow(/外径D/);
const extract = (row) => {
if (!row) {
return { measurements: [], average: NaN };
}
const measurements = Object.entries(row)
.filter(([header]) => /^[1-6]/.test(header))
.map(([, value]) => toNumber(firstNumberLike(value)))
.filter((value) => Number.isFinite(value));
const averageValue = toNumber(firstNumberLike(row["平均值/mm"] || row["7平均值"]));
const computedAverage = average(measurements);
return {
measurements,
average: averageValue,
computedAverage,
};
};
const h = extract(hRow);
const d = extract(dRow);
const D = extract(dOuterRow);
const volumeCm3 =
Number.isFinite(h.computedAverage) &&
Number.isFinite(d.computedAverage) &&
Number.isFinite(D.computedAverage)
? (Math.PI * h.computedAverage * (D.computedAverage * D.computedAverage - d.computedAverage * d.computedAverage)) /
4 /
1000
: NaN;
return {
rows,
h,
d,
D,
volumeCm3,
maxDimensionDeviation: maxFinite([
Number.isFinite(h.average) && Number.isFinite(h.computedAverage) ? Math.abs(h.average - h.computedAverage) : NaN,
Number.isFinite(d.average) && Number.isFinite(d.computedAverage) ? Math.abs(d.average - d.computedAverage) : NaN,
Number.isFinite(D.average) && Number.isFinite(D.computedAverage) ? Math.abs(D.average - D.computedAverage) : NaN,
]),
};
}
function computeSimpleAverageTableAnalysis(table, preferredHeaders) {
const rows = tableToObjects(table);
const values = rows
.map((row) => {
const header = Object.keys(row).find((key) =>
(preferredHeaders || []).some((matcher) =>
typeof matcher === "string" ? key.includes(matcher) : matcher.test(key)
)
);
return toNumber(firstNumberLike(header ? row[header] : ""));
})
.filter((value) => Number.isFinite(value));
return {
rows,
average: average(values),
maxDeviation: maxFinite(
values.map((value) =>
Number.isFinite(average(values)) ? Math.abs(value - average(values)) : NaN
)
),
};
}
function computeCaliperDifferenceAnalysis(table) {
const rows = tableToObjects(table).map((row) => {
const left = toNumber(firstNumberLike(pickRowValue(row, [/左读数/, /d1/])));
const right = toNumber(firstNumberLike(pickRowValue(row, [/右读数/, /d2/])));
const filled = toNumber(firstNumberLike(pickRowValue(row, [/直径d=|d2- d1|/, /直径d/])));
const computed = Number.isFinite(left) && Number.isFinite(right) ? Math.abs(right - left) : NaN;
return { left, right, filled, computed };
});
const computedValues = rows.map((row) => row.computed).filter((value) => Number.isFinite(value));
return {
rows,
average: average(computedValues),
maxDeviation: maxFinite(
rows.map((row) =>
Number.isFinite(row.filled) && Number.isFinite(row.computed)
? Math.abs(row.filled - row.computed)
: NaN
)
),
};
}
function computeViscosityRotatingCylinderAnalysis(table, standardMap) {
const rows = table && table.isValid ? table.rows : [];
const headers = table && table.headers ? table.headers.slice(1) : [];
const avgByMass = {};
headers.forEach((header, headerIndex) => {
const values = rows
.map((row) => toNumber(firstNumberLike(row[headerIndex + 1])))
.filter((value) => Number.isFinite(value));
const key = cleanText(header).replace('/kg', '').replace(':', '');
avgByMass[key] = average(values);
});
const coefficients = Object.entries(avgByMass)
.map(([massText, avgTime]) => {
const mass = toNumber(firstNumberLike(massText));
const standardValue = standardMap[mass.toFixed(2)];
return Number.isFinite(mass) && Number.isFinite(avgTime) && Number.isFinite(standardValue)
? standardValue / (mass * avgTime)
: NaN;
})
.filter((value) => Number.isFinite(value));
const coefficient = average(coefficients);
const byMass = {};
Object.entries(avgByMass).forEach(([massText, avgTime]) => {
const mass = toNumber(firstNumberLike(massText));
byMass[mass.toFixed(4)] =
Number.isFinite(coefficient) && Number.isFinite(mass) && Number.isFinite(avgTime)
? coefficient * mass * avgTime
: NaN;
});
return {
avgByMass,
coefficient,
byMass,
averageEta: average(Object.values(byMass).filter((value) => Number.isFinite(value))),
};
}
function computeViscosityAverageTableAnalysis(table) {
const rows = tableToObjects(table);
const deviations = rows.map((row) => {
const tAverage = toNumber(firstNumberLike(pickRowValue(row, [/t_average/])));
const deltaT = toNumber(firstNumberLike(pickRowValue(row, [/∆t/, /△t/])));
return Number.isFinite(tAverage) && Number.isFinite(deltaT) ? Math.abs(deltaT) : NaN;
});
return {
rows,
maxDeviation: maxFinite(deviations),
};
}
function computeViscosityFallingBallAnalysis(table, standardMap, distanceValue) {
const rows = tableToObjects(table);
const columns = table && table.headers ? table.headers.slice(1) : [];
const avgByDiameter = {};
columns.forEach((header, index) => {
const values = rows
.map((row) => toNumber(firstNumberLike(row[header])))
.filter((value) => Number.isFinite(value));
const key = cleanText(header).replace('mm', '');
avgByDiameter[key] = average(values);
});
const coefficients = Object.entries(avgByDiameter)
.map(([diameterText, avgTime]) => {
const diameter = toNumber(firstNumberLike(diameterText));
const standardValue = standardMap[diameter.toFixed(1)];
return Number.isFinite(diameter) && Number.isFinite(avgTime) && Number.isFinite(standardValue)
? standardValue / (diameter * diameter * avgTime)
: NaN;
})
.filter((value) => Number.isFinite(value));
const coefficient = average(coefficients);
const byDiameter = {};
Object.entries(avgByDiameter).forEach(([diameterText, avgTime]) => {
const diameter = toNumber(firstNumberLike(diameterText));
byDiameter[diameter.toFixed(3)] =
Number.isFinite(coefficient) && Number.isFinite(diameter) && Number.isFinite(avgTime)
? coefficient * diameter * diameter * avgTime
: NaN;
});
return {
avgByDiameter,
coefficient,
byDiameter,
averageEta: average(Object.values(byDiameter).filter((value) => Number.isFinite(value))),
distanceValue,
};
}
function appendViscosityCalculation(list, item, label, studentValue, standardValue, computedValue, referenceValue) {
list.push({
label,
scoreText: formatScoreText(item && item.realScore, item && item.totalScore),
studentValue: rawStudentDisplay(item, studentValue, 3, "Pa*s"),
standardValue: rawStandardDisplay(item, standardValue, 3, "Pa*s"),
computedValue: Number.isFinite(computedValue) ? `${roundTo(computedValue, 3)} Pa*s` : "无法按当前数据重算",
referenceValue,
deltaNote:
Number.isFinite(studentValue) && Number.isFinite(Number.isFinite(standardValue) ? standardValue : computedValue)
? `学生作答与${Number.isFinite(standardValue) ? "标准答案" : "重算值"}偏差 ${(relativeDelta(studentValue, Number.isFinite(standardValue) ? standardValue : computedValue) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(item, 3),
});
}
function maybePushValueIssue(issues, markers, section, label, studentValue, standardValue, computedValue, markerLabel) {
const baseline = Number.isFinite(standardValue) ? standardValue : computedValue;
if (
Number.isFinite(studentValue) &&
Number.isFinite(baseline) &&
relativeDelta(studentValue, baseline) > 0.03
) {
issues.push({
code: `${markerLabel}_mismatch`,
section,
message: Number.isFinite(standardValue)
? `${label}与标准答案偏差较大:学生作答=${studentValue},标准答案=${rawStandardDisplay(null, standardValue, 3)}。`
: `${label}与重算值偏差较大:学生作答=${studentValue},重算=${roundTo(computedValue, 3)}。`,
});
markers.push({ section, label: markerLabel });
}
}
function reviewErrorCompanion(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "DataList1", "尺寸测量原始表");
renameBreakdownLabel(detailState.opBreakdown, "DataList2", "统计量与不确定度表");
renameBreakdownLabel(detailState.opBreakdown, "DataList3", "纸张尺寸表");
renameBreakdownLabel(detailState.tableBreakdown, "DataList1", "尺寸测量原始表");
renameBreakdownLabel(detailState.tableBreakdown, "DataList2", "统计量与不确定度表");
renameBreakdownLabel(detailState.tableBreakdown, "DataList3", "纸张尺寸表");
const dataList1 = parsedReport.opItemsByName.DataList1;
const dataList2 = parsedReport.opItemsByName.DataList2;
const jsbV = parsedReport.opItemsByName.JSB_V;
const vRelative = parsedReport.opItemsByName.V_RelativeUncertain;
const vUncertain = parsedReport.opItemsByName.V_Uncertain;
const vExpression = parsedReport.opItemsByName.V_Expression;
const dataList3 = parsedReport.opItemsByName.DataList3;
const paperV = parsedReport.opItemsByName.Paper_V;
const paperRelative = parsedReport.opItemsByName.Paper_RelativeUncertain;
const paperUncertain = parsedReport.opItemsByName.Paper_Uncertain;
const paperExpression = parsedReport.opItemsByName.Paper_Expression;
const dimsAnalysis = computeErrorCompanionDimensions(dataList1 && dataList1.table);
const dimsStandardAnalysis = computeErrorCompanionDimensions(dataList1 && dataList1.standardTable);
const statsAnalysis = computeErrorCompanionStats(dataList2 && dataList2.table);
const statsStandardAnalysis = computeErrorCompanionStats(dataList2 && dataList2.standardTable);
const paperAnalysis = computeErrorCompanionPaper(dataList3 && dataList3.table);
const paperStandardAnalysis = computeErrorCompanionPaper(dataList3 && dataList3.standardTable);
const studentV = numberValueOf(jsbV);
const standardV = numberStandardValueOf(jsbV);
const studentVRelative = numberValueOf(vRelative);
const standardVRelative = numberStandardValueOf(vRelative);
const studentVUncertain = numberValueOf(vUncertain);
const standardVUncertain = numberStandardValueOf(vUncertain);
const studentPaperV = numberValueOf(paperV);
const standardPaperV = numberStandardValueOf(paperV);
const studentPaperRelative = numberValueOf(paperRelative);
const standardPaperRelative = numberStandardValueOf(paperRelative);
const studentPaperUncertain = numberValueOf(paperUncertain);
const standardPaperUncertain = numberStandardValueOf(paperUncertain);
metrics.push({
label: "几何体体积 V",
value: rawStudentDisplay(jsbV, studentV, 3, "cm^3"),
note: Number.isFinite(dimsAnalysis.volume)
? `按 DataList1 平均值重算 ${roundTo(dimsAnalysis.volume, 3)} cm^3`
: "当前无法由尺寸表重算。",
});
metrics.push({
label: "纸张体积",
value: rawStudentDisplay(paperV, studentPaperV, 3, "cm^3"),
note: Number.isFinite(paperAnalysis.volume)
? `按 DataList3 数值重算 ${roundTo(paperAnalysis.volume, 3)} cm^3`
: "当前无法由纸张尺寸表重算。",
});
if (dimsAnalysis.maxAverageDeviation > 0.01) {
issues.push({
code: "error_companion_average_mismatch",
section: "OP",
message: `DataList2 平均值列与 DataList1 原始数据重算结果存在偏差,最大约 ${roundTo(
dimsAnalysis.maxAverageDeviation,
3
)} mm。`,
});
}
pushErrorCompanionCalculation(
detailState.calculationBreakdown,
jsbV,
"几何体体积 V",
studentV,
standardV,
dimsAnalysis.volume,
"按 L×B×H 近似重算体积,并由 mm^3 换算为 cm^3"
);
pushErrorCompanionCalculation(
detailState.calculationBreakdown,
vRelative,
"体积相对不确定度",
studentVRelative,
standardVRelative,
statsAnalysis.relativeUncertainty,
"按各边相对不确定度平方和开方近似合成"
);
pushErrorCompanionCalculation(
detailState.calculationBreakdown,
vUncertain,
"体积不确定度",
studentVUncertain,
standardVUncertain,
Number.isFinite(dimsAnalysis.volume) && Number.isFinite(statsAnalysis.relativeUncertainty)
? dimsAnalysis.volume * statsAnalysis.relativeUncertainty
: NaN,
"ΔV ≈ V·u_r(V)"
);
pushErrorCompanionCalculation(
detailState.calculationBreakdown,
paperV,
"纸张体积",
studentPaperV,
standardPaperV,
paperAnalysis.volume,
"按 长×宽×厚 重算纸张体积,并由 mm^3 换算为 cm^3"
);
pushErrorCompanionCalculation(
detailState.calculationBreakdown,
paperRelative,
"纸张相对不确定度",
studentPaperRelative,
standardPaperRelative,
paperAnalysis.relativeUncertainty,
"按长宽厚相对不确定度平方和开方近似合成"
);
pushErrorCompanionCalculation(
detailState.calculationBreakdown,
paperUncertain,
"纸张不确定度",
studentPaperUncertain,
standardPaperUncertain,
Number.isFinite(paperAnalysis.volume) && Number.isFinite(paperAnalysis.relativeUncertainty)
? paperAnalysis.volume * paperAnalysis.relativeUncertainty
: NaN,
"ΔV ≈ V·u_r(V)"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"DataList1",
dataList1 && dataList1.table && dataList1.table.isValid
? `共 ${dataList1.table.rows.length} 行,按原始数据重算几何体体积 ${roundTo(
dimsAnalysis.volume,
3
)} cm^3${Number.isFinite(dimsStandardAnalysis.volume) ? `,标准答案表重算 ${roundTo(dimsStandardAnalysis.volume, 3)} cm^3` : ""}。`
: "当前表格为空或未形成可分析数据。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"DataList2",
dataList2 && dataList2.table && dataList2.table.isValid
? `统计量表已提供${Number.isFinite(statsAnalysis.relativeUncertainty) ? `,按表中 Δ/L 等近似重算相对不确定度 ${roundTo(statsAnalysis.relativeUncertainty, 4)}` : ""}${Number.isFinite(statsStandardAnalysis.relativeUncertainty) ? `,标准答案表约 ${roundTo(statsStandardAnalysis.relativeUncertainty, 4)}` : ""}。`
: "当前表格为空或未形成可分析数据。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"DataList3",
dataList3 && dataList3.table && dataList3.table.isValid
? `纸张尺寸表已提供,按数值重算纸张体积 ${roundTo(paperAnalysis.volume, 3)} cm^3${Number.isFinite(paperStandardAnalysis.volume) ? `,标准答案表重算 ${roundTo(paperStandardAnalysis.volume, 3)} cm^3` : ""}。`
: "当前表格为空或未形成可分析数据。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"JSB_V",
Number.isFinite(studentV)
? `学生作答 ${rawStudentDisplay(jsbV, studentV, 3, "cm^3")},按 DataList1 重算 ${roundTo(dimsAnalysis.volume, 3)} cm^3${Number.isFinite(standardV) ? `,标准答案 ${rawStandardDisplay(jsbV, standardV, 3, "cm^3")}` : ""}。`
: "当前未识别到有效数字。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"Paper_V",
Number.isFinite(studentPaperV)
? `学生作答 ${rawStudentDisplay(paperV, studentPaperV, 3, "cm^3")},按 DataList3 重算 ${roundTo(paperAnalysis.volume, 3)} cm^3${Number.isFinite(standardPaperV) ? `,标准答案 ${rawStandardDisplay(paperV, standardPaperV, 3, "cm^3")}` : ""}。`
: "当前未识别到有效数字。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"V_Expression",
getContentText(vExpression && vExpression.realResult) || getContentText(vExpression && vExpression.stdResult)
? `结果表达式:学生作答 ${getContentText(vExpression && vExpression.realResult) || "未填写"}${getContentText(vExpression && vExpression.stdResult) ? `,标准答案 ${getContentText(vExpression.stdResult)}` : ""}。`
: "当前未识别到有效表达式。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"Paper_Expression",
getContentText(paperExpression && paperExpression.realResult) || getContentText(paperExpression && paperExpression.stdResult)
? `结果表达式:学生作答 ${getContentText(paperExpression && paperExpression.realResult) || "未填写"}${getContentText(paperExpression && paperExpression.stdResult) ? `,标准答案 ${getContentText(paperExpression.stdResult)}` : ""}。`
: "当前未识别到有效表达式。"
);
}
function computeErrorCompanionDimensions(table) {
const rows = tableToObjects(table);
const grouped = {
L: rows.map((row) => toNumber(firstNumberLike(pickRowValue(row, [/L\(mm\)/])))).filter((value) => Number.isFinite(value)),
B: rows.map((row) => toNumber(firstNumberLike(pickRowValue(row, [/B\(mm\)/])))).filter((value) => Number.isFinite(value)),
H: rows.map((row) => toNumber(firstNumberLike(pickRowValue(row, [/H\(mm\)/])))).filter((value) => Number.isFinite(value)),
D: rows.map((row) => toNumber(firstNumberLike(pickRowValue(row, [/D\(mm\)/])))).filter((value) => Number.isFinite(value)),
};
const averages = Object.fromEntries(
Object.entries(grouped).map(([key, values]) => [key, average(values)])
);
const stds = Object.fromEntries(
Object.entries(grouped).map(([key, values]) => [key, sampleStdDev(values)])
);
const deltas = Object.fromEntries(
Object.entries(stds).map(([key, value]) => [key, Number.isFinite(value) ? value * 3 : NaN])
);
const volume =
Number.isFinite(averages.L) && Number.isFinite(averages.B) && Number.isFinite(averages.H)
? (averages.L * averages.B * averages.H) / 1000
: NaN;
return {
rows,
averages,
stds,
deltas,
volume,
maxAverageDeviation: NaN,
};
}
function computeErrorCompanionStats(table) {
const rows = tableToObjects(table);
const mapping = {};
rows.forEach((row) => {
const label = cleanText(row[""] || pickRowValue(row, [/L|B|H|D/]));
if (!label) {
return;
}
mapping[label] = {
average: toNumber(firstNumberLike(pickRowValue(row, [/平均值/]))),
s: toNumber(firstNumberLike(pickRowValue(row, [/s\(mm\)/, /^s/]))),
delta: toNumber(firstNumberLike(pickRowValue(row, [/Δ\(mm\)/, /△\(mm\)/, /^Δ/, /^△/]))),
};
});
const parts = ["L", "B", "H"]
.map((key) => safeDivide(mapping[key] && mapping[key].delta, mapping[key] && mapping[key].average))
.filter((value) => Number.isFinite(value));
return {
rows,
mapping,
relativeUncertainty:
parts.length > 0 ? Math.sqrt(parts.reduce((sum, value) => sum + value * value, 0)) : NaN,
};
}
function computeErrorCompanionPaper(table) {
const rows = tableToObjects(table);
const row = rows[0] || {};
const length = toNumber(firstNumberLike(pickRowValue(row, [/长\(mm\)/])));
const width = toNumber(firstNumberLike(pickRowValue(row, [/宽\(mm\)/])));
const thickness = toNumber(firstNumberLike(pickRowValue(row, [/厚\(mm\)/])));
const volume =
Number.isFinite(length) && Number.isFinite(width) && Number.isFinite(thickness)
? (length * width * thickness) / 1000
: NaN;
const defaultDelta = { length: 0.1, width: 0.1, thickness: 0.005 };
const relativeParts = [
safeDivide(defaultDelta.length, length),
safeDivide(defaultDelta.width, width),
safeDivide(defaultDelta.thickness, thickness),
].filter((value) => Number.isFinite(value));
return {
rows,
length,
width,
thickness,
volume,
relativeUncertainty:
relativeParts.length > 0
? Math.sqrt(relativeParts.reduce((sum, value) => sum + value * value, 0))
: NaN,
};
}
function pushErrorCompanionCalculation(list, item, label, studentValue, standardValue, computedValue, referenceValue) {
list.push({
label,
scoreText: formatScoreText(item && item.realScore, item && item.totalScore),
studentValue: rawStudentDisplay(item, studentValue, 4),
standardValue: rawStandardDisplay(item, standardValue, 4),
computedValue: Number.isFinite(computedValue) ? `${roundTo(computedValue, 4)}` : "无法按当前数据重算",
referenceValue,
deltaNote:
Number.isFinite(studentValue) && Number.isFinite(Number.isFinite(standardValue) ? standardValue : computedValue)
? `学生作答与${Number.isFinite(standardValue) ? "标准答案" : "重算值"}偏差 ${(relativeDelta(studentValue, Number.isFinite(standardValue) ? standardValue : computedValue) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(item, 3),
});
}
function computeSurfaceTensionB2Analysis(table) {
const rows = tableToObjects(table).map((row) => {
const u1 = toNumber(firstNumberLike(pickRowValue(row, [/U1/])));
const u2 = toNumber(firstNumberLike(pickRowValue(row, [/U2/])));
const delta = toNumber(firstNumberLike(pickRowValue(row, [/U1-U2/, /U1-U2/])));
const computedDelta = Number.isFinite(u1) && Number.isFinite(u2) ? u1 - u2 : NaN;
return { u1, u2, delta, computedDelta };
});
return {
rows,
avgU1: average(rows.map((row) => row.u1).filter((value) => Number.isFinite(value))),
avgU2: average(rows.map((row) => row.u2).filter((value) => Number.isFinite(value))),
diffAverage: average(rows.map((row) => row.delta).filter((value) => Number.isFinite(value))),
maxDeltaDeviation: maxFinite(
rows.map((row) =>
Number.isFinite(row.delta) && Number.isFinite(row.computedDelta)
? Math.abs(row.delta - row.computedDelta)
: NaN
)
),
};
}
function computeSurfaceTensionB3Deviation(table, b2Analysis) {
const rows = tableToObjects(table);
const mapping = {};
rows.forEach((row) => {
const label = cleanText(row[""] || row["名称"] || pickRowValue(row, [/U1-U2/, /^U1$/, /^U2$/]));
const value = toNumber(firstNumberLike(pickRowValue(row, [/平均值/])));
if (label) {
mapping[label] = value;
}
});
const deviations = [
Number.isFinite(mapping.U1) && Number.isFinite(b2Analysis.avgU1)
? Math.abs(mapping.U1 - b2Analysis.avgU1)
: NaN,
Number.isFinite(mapping.U2) && Number.isFinite(b2Analysis.avgU2)
? Math.abs(mapping.U2 - b2Analysis.avgU2)
: NaN,
Number.isFinite(mapping["U1-U2"]) && Number.isFinite(b2Analysis.diffAverage)
? Math.abs(mapping["U1-U2"] - b2Analysis.diffAverage)
: NaN,
];
return {
rows,
mapping,
maxDeviation: maxFinite(deviations),
};
}
function computePolarizationDegreeAnalysis(table) {
const rows = tableToObjects(table).map((row) => ({
imax: toNumber(firstNumberLike(pickRowValue(row, [/Imax/]))),
imin: toNumber(firstNumberLike(pickRowValue(row, [/Imin/]))),
}));
const avgImax = average(rows.map((row) => row.imax).filter((value) => Number.isFinite(value)));
const avgImin = average(rows.map((row) => row.imin).filter((value) => Number.isFinite(value)));
return {
rows,
avgImax,
avgImin,
degree:
Number.isFinite(avgImax) && Number.isFinite(avgImin) && avgImax + avgImin !== 0
? (avgImax - avgImin) / (avgImax + avgImin)
: NaN,
maxDeviation: maxFinite(
rows.flatMap((row) => [
Number.isFinite(row.imax) && Number.isFinite(avgImax) ? Math.abs(row.imax - avgImax) : NaN,
Number.isFinite(row.imin) && Number.isFinite(avgImin) ? Math.abs(row.imin - avgImin) : NaN,
])
),
};
}
function computeMalusAnalysis(table) {
const rows = tableToObjects(table).map((row) => ({
cos2: toNumber(firstNumberLike(pickRowValue(row, [/cos/, /\^2/]))),
intensity: toNumber(firstNumberLike(row["I"] || pickRowValue(row, [/^I$/]))),
}));
return {
rows,
fit: computeLinearFit(
rows.map((row) => ({
x: row.cos2,
y: row.intensity,
}))
),
};
}
function computeThermocoupleAnalysis(table, t0Value) {
const rows = tableToObjects(table).map((row) => {
const t = toNumber(firstNumberLike(pickRowValue(row, [/t\/℃/, /^t$/])));
const e = toNumber(firstNumberLike(pickRowValue(row, [/E温差/, /^E$/])));
const delta = toNumber(firstNumberLike(pickRowValue(row, [/t-t0/])));
const computedDelta = Number.isFinite(t) && Number.isFinite(t0Value) ? t - t0Value : NaN;
return { t, e, delta, computedDelta };
});
const fit = computeLinearFit(
rows.map((row) => ({
x: row.delta,
y: row.e,
}))
);
return {
rows,
fit,
microVoltSlope: Number.isFinite(fit.slope) ? fit.slope * 1000 : NaN,
maxDeltaDeviation: maxFinite(
rows.map((row) =>
Number.isFinite(row.delta) && Number.isFinite(row.computedDelta)
? Math.abs(row.delta - row.computedDelta)
: NaN
)
),
};
}
function computeStandingWaveAnalysis(table, forceValue) {
const rows = tableToObjects(table).map((row) => {
const n = toNumber(firstNumberLike(pickRowValue(row, [/^n$/])));
const f = toNumber(firstNumberLike(pickRowValue(row, [/f\/Hz/])));
const l = toNumber(firstNumberLike(pickRowValue(row, [/L\/cm/])));
const sigma = toNumber(firstNumberLike(pickRowValue(row, [/σ/])));
const effectiveOrder = Number.isFinite(n) ? n - 1 : NaN;
const computedSigma =
Number.isFinite(forceValue) &&
Number.isFinite(effectiveOrder) &&
effectiveOrder > 0 &&
Number.isFinite(f) &&
Number.isFinite(l) &&
l !== 0
? (forceValue * effectiveOrder * effectiveOrder) /
(4 * Math.pow(l / 100, 2) * f * f) *
1000
: NaN;
return { n, effectiveOrder, f, l, sigma, computedSigma };
});
const averageSigma = average(
rows.map((row) => row.computedSigma).filter((value) => Number.isFinite(value))
);
return {
rows,
average: averageSigma,
maxDeviation: maxFinite(
rows.map((row) =>
Number.isFinite(row.computedSigma) && Number.isFinite(averageSigma)
? Math.abs(row.computedSigma - averageSigma)
: NaN
)
),
maxSigmaDeviation: maxFinite(
rows.map((row) =>
Number.isFinite(row.sigma) && Number.isFinite(row.computedSigma)
? Math.abs(row.sigma - row.computedSigma)
: NaN
)
),
};
}
function reviewElasticModulus(parsedReport, issues, suggestions, metrics, markers, detailState) {
const readingItem = resolveOpItem(parsedReport, "BQDD_n", "BQDDn");
const uncertaintyItem = resolveOpItem(parsedReport, "YSML_BQDD", "YSMLBQDD");
const modulusItem = resolveOpItem(parsedReport, "YSML_E", "YSMLE");
renameBreakdownLabel(
detailState.opBreakdown,
readingItem && readingItem.name,
"标尺读数不确定度 Δn"
);
renameBreakdownLabel(
detailState.opBreakdown,
uncertaintyItem && uncertaintyItem.name,
"杨氏模量不确定度"
);
renameBreakdownLabel(
detailState.opBreakdown,
modulusItem && modulusItem.name,
"杨氏模量 E"
);
renameBreakdownLabel(detailState.opBreakdown, "D_Expression", "钢丝直径 d 表达式");
renameBreakdownLabel(detailState.opBreakdown, "L_Expression", "钢丝长度 L 表达式");
renameBreakdownLabel(detailState.opBreakdown, "b_Expression", "光杠杆臂长 b 表达式");
renameBreakdownLabel(detailState.opBreakdown, "E_Expression", "杨氏模量结果表达式");
renameBreakdownLabel(detailState.opBreakdown, "ZCF_n", "逐差法 Δn 平均值");
renameBreakdownLabel(detailState.opBreakdown, "B2", "读数差表");
const studentReadings = numberListValueOf(readingItem);
const standardReadings = numberListStandardValueOf(readingItem);
const studentUncertainty = firstFiniteValue(numberListValueOf(uncertaintyItem));
const standardUncertainty = firstFiniteValue(numberListStandardValueOf(uncertaintyItem));
const studentModulus = firstFiniteValue(numberListValueOf(modulusItem));
const standardModulus = firstFiniteValue(numberListStandardValueOf(modulusItem));
const modulusStdRawText = modulusItem
? getContentText(modulusItem.stdResult).replace(/\s*[;;]\s*/g, ";")
: "";
metrics.push({
label: "标尺读数 n",
value:
studentReadings.length > 0
? `${studentReadings.length} 个值`
: standardReadings.length > 0
? `标准答案 ${standardReadings.length} 个值`
: "未识别",
note:
studentReadings.length > 0
? `学生作答:${formatNumberList(studentReadings)}${
standardReadings.length > 0
? `;标准答案:${formatNumberList(standardReadings)}`
: ""
}`
: "当前 XML 中未识别到可用读数序列。",
});
metrics.push({
label: "杨氏模量不确定度",
value: Number.isFinite(studentUncertainty) ? `${studentUncertainty}` : "未识别",
note: Number.isFinite(standardUncertainty)
? `标准答案 ${rawStandardDisplay(uncertaintyItem, standardUncertainty, 1)}`
: "当前 XML 未提供可比较的标准值。",
});
metrics.push({
label: "杨氏模量 E",
value: Number.isFinite(studentModulus) ? `${studentModulus}` : "未识别",
note: modulusStdRawText
? `标准答案 ${modulusStdRawText}${formatItemPrecisionRequirement(modulusItem) ? `,要求 ${formatItemPrecisionRequirement(modulusItem)}` : ""}`
: "当前 XML 未提供可比较的标准值。",
});
if (studentReadings.length === 0 && standardReadings.length === 0) {
issues.push({
code: "elastic_modulus_readings_missing",
section: "OP",
message: "未识别到标尺读数 n,当前 XML 里的多值字段可能仍未导出到可分析节点。",
});
markers.push({ section: "OP", label: "n未识别" });
}
if (!Number.isFinite(studentUncertainty) && !Number.isFinite(standardUncertainty)) {
issues.push({
code: "elastic_modulus_uncertainty_missing",
section: "OP",
message: "未识别到杨氏模量不确定度字段 YSML_BQDD / YSMLBQDD。",
});
}
if (!Number.isFinite(studentModulus) && !Number.isFinite(standardModulus)) {
issues.push({
code: "elastic_modulus_value_missing",
section: "OP",
message: "未识别到杨氏模量 E 字段 YSML_E。",
});
}
if (readingItem) {
updateBreakdownAnalysis(
detailState.opBreakdown,
readingItem.name,
studentReadings.length > 0
? `学生作答识别为 ${studentReadings.length} 个数值:${formatNumberList(studentReadings)}${
standardReadings.length > 0
? `;标准答案为 ${formatNumberList(standardReadings)}。`
: "。"
}`
: standardReadings.length > 0
? `学生作答未识别,但标准答案中存在 ${standardReadings.length} 个数值:${formatNumberList(
standardReadings
)}。`
: "当前未识别到多值读数。"
);
}
if (uncertaintyItem) {
updateBreakdownAnalysis(
detailState.opBreakdown,
uncertaintyItem.name,
Number.isFinite(studentUncertainty)
? `学生作答 ${studentUncertainty}${
Number.isFinite(standardUncertainty)
? `,标准答案 ${rawStandardDisplay(uncertaintyItem, standardUncertainty, 1)}。`
: "。"
}`
: Number.isFinite(standardUncertainty)
? `学生作答未识别,标准答案 ${rawStandardDisplay(uncertaintyItem, standardUncertainty, 1)}。`
: "当前未识别到可分析数值。"
);
}
if (modulusItem) {
const precisionRequirement = formatItemPrecisionRequirement(modulusItem);
const sigFigsHint = precisionRequirement ? ` 系统要求 ${precisionRequirement}。` : "";
updateBreakdownAnalysis(
detailState.opBreakdown,
modulusItem.name,
Number.isFinite(studentModulus)
? `学生作答 ${getContentText(modulusItem.realResult) || studentModulus}${
modulusStdRawText ? `,标准答案 ${modulusStdRawText}。` : "。"
}${sigFigsHint}`
: modulusStdRawText
? `学生作答未识别,标准答案 ${modulusStdRawText}。${sigFigsHint}`
: "当前未识别到可分析数值。"
);
}
detailState.calculationBreakdown.push(
{
label: "杨氏模量 E",
scoreText: formatScoreText(modulusItem && modulusItem.realScore, modulusItem && modulusItem.totalScore),
studentValue: rawStudentDisplay(modulusItem, studentModulus, 2, ""),
standardValue: modulusStdRawText || "XML 未提供标准答案",
deltaNote: Number.isFinite(studentModulus) && Number.isFinite(standardModulus)
? `学生作答与标准答案偏差 ${(relativeDelta(studentModulus, standardModulus) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(modulusItem, 3),
},
{
label: "杨氏模量不确定度",
scoreText: formatScoreText(uncertaintyItem && uncertaintyItem.realScore, uncertaintyItem && uncertaintyItem.totalScore),
studentValue: rawStudentDisplay(uncertaintyItem, studentUncertainty, 1, ""),
standardValue: rawStandardDisplay(uncertaintyItem, standardUncertainty, 1),
deltaNote: Number.isFinite(studentUncertainty) && Number.isFinite(standardUncertainty)
? `学生作答与标准答案偏差 ${(relativeDelta(studentUncertainty, standardUncertainty) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(uncertaintyItem, 1),
}
);
}
function reviewNewtonRings(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "B1", "牛顿环直径测量表");
renameBreakdownLabel(detailState.opBreakdown, "B2", "D^2 差值表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "牛顿环直径测量表");
renameBreakdownLabel(detailState.tableBreakdown, "B2", "D^2 差值表");
const b1 = parsedReport.opItemsByName.B1;
const b2 = parsedReport.opItemsByName.B2;
const avgA = parsedReport.opItemsByName.PJZ_D;
const stdS = parsedReport.opItemsByName.BZC_S;
const deltaA = parsedReport.opItemsByName.DAIER_D;
const radius = parsedReport.opItemsByName.PJZ_R;
const relativeRadius = parsedReport.opItemsByName.DAIER_R_R;
const deltaRadius = parsedReport.opItemsByName.DAIER_R;
const expression = parsedReport.opItemsByName.R_Expression;
const b1Analysis = computeNewtonRingB1Analysis(b1 && b1.table);
const b1StandardAnalysis = computeNewtonRingB1Analysis(b1 && b1.standardTable);
const b2Analysis = computeNewtonRingB2Analysis(b1 && b1.table, b2 && b2.table, b1Analysis);
const b2StandardAnalysis = computeNewtonRingB2Analysis(
b1 && b1.standardTable,
b2 && b2.standardTable,
b1StandardAnalysis
);
const computedMeanA = average(
b2Analysis.rows.map((row) => row.filledA).filter((value) => Number.isFinite(value))
);
const computedStdS = sampleStdDev(
b2Analysis.rows.map((row) => row.filledA).filter((value) => Number.isFinite(value))
);
const computedDeltaA = Number.isFinite(computedStdS) ? computedStdS * 3 : NaN;
const computedRadius = computeNewtonRingRadiusMeters(computedMeanA, 15);
const computedRelativeRadius =
Number.isFinite(computedDeltaA) && Number.isFinite(computedMeanA) && computedMeanA !== 0
? (computedDeltaA / computedMeanA) * 2
: NaN;
const computedDeltaRadius =
Number.isFinite(computedRadius) && Number.isFinite(computedRelativeRadius)
? computedRadius * computedRelativeRadius
: NaN;
const computedExpression =
Number.isFinite(computedRadius) && Number.isFinite(computedDeltaRadius)
? `${formatFixed(computedRadius, 2)}±${formatFixed(computedDeltaRadius, 2)}`
: "";
const studentMeanA = numberValueOf(avgA);
const standardMeanA = numberStandardValueOf(avgA);
const studentStdS = numberValueOf(stdS);
const standardStdS = numberStandardValueOf(stdS);
const studentDeltaA = numberValueOf(deltaA);
const standardDeltaA = numberStandardValueOf(deltaA);
const studentRadius = numberValueOf(radius);
const standardRadius = numberStandardValueOf(radius);
const studentRelativeRadius = numberValueOf(relativeRadius);
const standardRelativeRadius = numberStandardValueOf(relativeRadius);
const studentDeltaRadius = numberValueOf(deltaRadius);
const standardDeltaRadius = numberStandardValueOf(deltaRadius);
const studentExpression = getContentText(expression && expression.realResult);
const standardExpression = getContentText(expression && expression.stdResult);
metrics.push({
label: "直径表 B1",
value: b1 && b1.table && b1.table.isValid ? `${b1.table.rows.length} 行数据` : "未识别",
note:
Number.isFinite(b1Analysis.maxDDeviation) && Number.isFinite(b1Analysis.maxD2Deviation)
? `按 X左 / X右 重算时,D 最大偏差 ${formatFixed(b1Analysis.maxDDeviation, 3)} mm,D^2 最大偏差 ${formatFixed(
b1Analysis.maxD2Deviation,
2
)} mm^2`
: "当前未形成稳定的直径重算结果。",
});
metrics.push({
label: "差值表 B2",
value: b2 && b2.table && b2.table.isValid ? `${b2.table.rows.length} 组差值` : "未识别",
note: Number.isFinite(computedMeanA)
? `按 B1 表重算 a 平均值 ${formatFixed(computedMeanA, 2)} mm^2${
Number.isFinite(standardMeanA) ? `,标准答案 ${rawStandardDisplay(avgA, standardMeanA, 2, "mm^2")}` : ""
}`
: "当前未形成稳定的 a 重算结果。",
});
metrics.push({
label: "曲率半径 R",
value: rawStudentDisplay(radius, studentRadius, 3, "m"),
note: Number.isFinite(computedRadius)
? `按 a/(4nλ) 重算 ${formatFixed(computedRadius, 3)} m${
Number.isFinite(standardRadius) ? `,标准答案 ${rawStandardDisplay(radius, standardRadius, 3, "m")}` : ""
}`
: "无法按 a 重算。",
});
metrics.push({
label: "最终表达式",
value: studentExpression || "缺失",
note: standardExpression
? `标准答案 ${standardExpression}${computedExpression ? `;按当前数据可得 ${computedExpression}` : ""}`
: computedExpression
? `按当前数据可得 ${computedExpression}`
: "当前未形成稳定的结果表达式。",
});
if (!b1 || !b1.table || !b1.table.isValid) {
issues.push({
code: "newton_b1_missing",
section: "OP",
message: "未识别到牛顿环直径测量表 B1,无法检查 D 与 D^2 的计算。",
});
markers.push({ section: "OP", label: "B1缺失" });
} else {
if (Number.isFinite(b1Analysis.maxDDeviation) && b1Analysis.maxDDeviation > 0.02) {
issues.push({
code: "newton_b1_d_mismatch",
section: "OP",
message: `B1 表中 D=|X左-X右| 与按 X左 / X右 重算值最大偏差约 ${formatFixed(
b1Analysis.maxDDeviation,
3
)} mm,建议逐行核对。`,
});
}
if (Number.isFinite(b1Analysis.maxD2Deviation) && b1Analysis.maxD2Deviation > 0.2) {
issues.push({
code: "newton_b1_d2_mismatch",
section: "OP",
message: `B1 表中 D^2 与按 D 重算值最大偏差约 ${formatFixed(
b1Analysis.maxD2Deviation,
2
)} mm^2,建议检查平方运算和保留位数。`,
});
}
}
if (!b2 || !b2.table || !b2.table.isValid) {
issues.push({
code: "newton_b2_missing",
section: "OP",
message: "未识别到 D^2 差值表 B2,无法继续检查 a 和曲率半径 R。",
});
markers.push({ section: "OP", label: "B2缺失" });
} else if (Number.isFinite(b2Analysis.maxDeviation) && b2Analysis.maxDeviation > 0.05) {
issues.push({
code: "newton_b2_mismatch",
section: "OP",
message: `B2 表中的 a 与按 B1 表 D^2 差值重算结果最大偏差约 ${formatFixed(
b2Analysis.maxDeviation,
2
)} mm^2。`,
});
markers.push({ section: "OP", label: "a偏差" });
}
if (
Number.isFinite(studentMeanA) &&
Number.isFinite(computedMeanA) &&
relativeDelta(studentMeanA, computedMeanA) > 0.01
) {
issues.push({
code: "newton_mean_a_mismatch",
section: "OP",
message: `a 平均值与按 B2 表重算值偏差较大:学生作答=${studentMeanA},重算=${formatFixed(
computedMeanA,
2
)}。`,
});
}
if (
Number.isFinite(studentRadius) &&
Number.isFinite(computedRadius) &&
relativeDelta(studentRadius, computedRadius) > 0.01
) {
issues.push({
code: "newton_radius_mismatch",
section: "OP",
message: Number.isFinite(standardRadius)
? `曲率半径 R 与标准答案偏差较大:学生作答=${formatFixed(
studentRadius,
3
)} m,标准答案=${rawStandardDisplay(radius, standardRadius, 3, "m")}。`
: `曲率半径 R 与重算值偏差较大:学生作答=${formatFixed(
studentRadius,
3
)} m,重算=${formatFixed(computedRadius, 3)} m。`,
});
markers.push({ section: "OP", label: "R偏差" });
}
if (
studentExpression &&
computedExpression &&
normalizeResultExpression(studentExpression) !== normalizeResultExpression(computedExpression)
) {
issues.push({
code: "newton_expression_mismatch",
section: "OP",
message: `最终结果表达式与按当前数据重算值不一致:学生作答=${studentExpression},重算=${computedExpression}。`,
});
}
appendNewtonRingCalculationBreakdown(detailState.calculationBreakdown, {
avgA,
stdS,
deltaA,
radius,
relativeRadius,
deltaRadius,
expression,
studentMeanA,
standardMeanA,
computedMeanA,
studentStdS,
standardStdS,
computedStdS,
studentDeltaA,
standardDeltaA,
computedDeltaA,
studentRadius,
standardRadius,
computedRadius,
studentRelativeRadius,
standardRelativeRadius,
computedRelativeRadius,
studentDeltaRadius,
standardDeltaRadius,
computedDeltaRadius,
studentExpression,
standardExpression,
computedExpression,
});
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${b1.table.rows.length} 行。按 X左 / X右 重算得到 D 最大偏差 ${formatFixed(
b1Analysis.maxDDeviation,
3
)} mm,D^2 最大偏差 ${formatFixed(b1Analysis.maxD2Deviation, 2)} mm^2。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B2",
b2 && b2.table && b2.table.isValid
? `共 ${b2.table.rows.length} 组差值。按 B1 表重算 a 最大偏差 ${formatFixed(
b2Analysis.maxDeviation,
2
)} mm^2${Number.isFinite(computedMeanA) ? `,a 平均值 ${formatFixed(computedMeanA, 2)} mm^2。` : "。"}`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"PJZ_D",
Number.isFinite(studentMeanA) && Number.isFinite(computedMeanA)
? `学生作答 ${rawStudentDisplay(avgA, studentMeanA, 2, "mm^2")},按 B2 表重算 ${formatFixed(
computedMeanA,
2
)} mm^2${Number.isFinite(standardMeanA) ? `,标准答案 ${rawStandardDisplay(avgA, standardMeanA, 2, "mm^2")}。` : "。"}`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"PJZ_R",
Number.isFinite(studentRadius) && Number.isFinite(computedRadius)
? `学生作答 ${rawStudentDisplay(radius, studentRadius, 3, "m")},按 a/(4nλ) 重算 ${formatFixed(
computedRadius,
3
)} m${Number.isFinite(standardRadius) ? `,标准答案 ${rawStandardDisplay(radius, standardRadius, 3, "m")}。` : "。"}`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"DAIER_R_R",
Number.isFinite(studentRelativeRadius) && Number.isFinite(computedRelativeRadius)
? `学生作答 ${rawStudentDisplay(relativeRadius, studentRelativeRadius, 3)},按 2Δa/a 近似 ${formatFixed(
computedRelativeRadius,
3
)}${Number.isFinite(standardRelativeRadius) ? `,标准答案 ${rawStandardDisplay(relativeRadius, standardRelativeRadius, 3)}。` : "。"}`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"DAIER_R",
Number.isFinite(studentDeltaRadius) && Number.isFinite(computedDeltaRadius)
? `学生作答 ${rawStudentDisplay(deltaRadius, studentDeltaRadius, 2, "m")},按 R×相对不确定度近似 ${formatFixed(
computedDeltaRadius,
2
)} m${Number.isFinite(standardDeltaRadius) ? `,标准答案 ${rawStandardDisplay(deltaRadius, standardDeltaRadius, 2, "m")}。` : "。"}`
: "缺少可分析数值。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"R_Expression",
studentExpression
? `学生作答 ${studentExpression}${standardExpression ? `,标准答案 ${standardExpression}` : ""}${
computedExpression ? `,按当前数据可得 ${computedExpression}。` : "。"
}`
: "未识别到结果表达式。"
);
}
function computeCollisionRows(table, mode, mass1, mass2, distanceCm) {
const rows = tableToObjects(table);
const distance = Number.isFinite(distanceCm) ? distanceCm : 1;
return rows
.map((row) => {
const dt10 = toNumber(firstNumberLike(pickRowValue(row, [/△t10/, /Δt10/])));
const dt2 = toNumber(firstNumberLike(pickRowValue(row, [/△t2/, /Δt2/])));
const dt1 =
mode === "inelastic"
? toNumber(
firstNumberLike(
pickRowValue(row, [/△t1(?!0)/, /Δt1(?!0)/, /^△t1\(ms\)/, /^Δt1\(ms\)/])
)
)
: NaN;
const v10 = dt10 > 0 ? (distance * 100) / dt10 : NaN;
const v2 = dt2 > 0 ? (distance * 100) / dt2 : NaN;
const v1 = dt1 > 0 ? (distance * 100) / dt1 : NaN;
const beforeMomentum =
Number.isFinite(mass1) && Number.isFinite(v10) ? (mass1 * v10) / 100 : NaN;
const afterMomentum =
mode === "elastic"
? Number.isFinite(mass2) && Number.isFinite(v2)
? (mass2 * v2) / 100
: NaN
: Number.isFinite(mass1) &&
Number.isFinite(mass2) &&
Number.isFinite(v1) &&
Number.isFinite(v2)
? (mass1 * v1 + mass2 * v2) / 100
: NaN;
return {
computedE:
mode === "elastic"
? safeDivide(v2, v10)
: safeDivide(v2 - v1, v10),
computedR: safeDivide(afterMomentum, beforeMomentum),
computedErrorPct:
Number.isFinite(beforeMomentum) && Number.isFinite(afterMomentum)
? ((beforeMomentum - afterMomentum) / beforeMomentum) * 100
: NaN,
filledE: toNumber(firstNumberLike(pickRowValue(row, [/^e$/i]))),
filledR: toNumber(firstNumberLike(pickRowValue(row, [/^R$/]))),
filledErrorPct: toNumber(firstNumberLike(pickRowValue(row, [/E%/]))),
};
})
.filter(
(row) =>
Number.isFinite(row.computedE) ||
Number.isFinite(row.computedR) ||
Number.isFinite(row.computedErrorPct)
);
}
function computeNewtonRingB1Analysis(table) {
const rows = tableToObjects(table);
const analyzedRows = rows
.map((row) => {
const m = toNumber(firstNumberLike(pickRowValue(row, [/m\(级\)/, /^m/])));
const xLeft = toNumber(firstNumberLike(pickRowValue(row, [/X左/])));
const xRight = toNumber(firstNumberLike(pickRowValue(row, [/X右/])));
const filledD = toNumber(firstNumberLike(pickRowValue(row, [/D=\|X左-X右\|/])));
const filledD2 = toNumber(firstNumberLike(pickRowValue(row, [/D\^2/])));
const computedD =
Number.isFinite(xLeft) && Number.isFinite(xRight) ? Math.abs(xLeft - xRight) : NaN;
const computedD2 = Number.isFinite(computedD) ? computedD * computedD : NaN;
return {
m,
filledD,
filledD2,
computedD,
computedD2,
dDelta:
Number.isFinite(filledD) && Number.isFinite(computedD)
? Math.abs(filledD - computedD)
: NaN,
d2Delta:
Number.isFinite(filledD2) && Number.isFinite(computedD2)
? Math.abs(filledD2 - computedD2)
: NaN,
};
})
.filter(
(row) =>
Number.isFinite(row.m) ||
Number.isFinite(row.filledD) ||
Number.isFinite(row.filledD2) ||
Number.isFinite(row.computedD)
);
return {
rows: analyzedRows,
maxDDeviation: maxFinite(analyzedRows.map((row) => row.dDelta)),
maxD2Deviation: maxFinite(analyzedRows.map((row) => row.d2Delta)),
};
}
function computeNewtonRingB2Analysis(b1Table, b2Table, precomputedB1Analysis) {
const b1Rows = (precomputedB1Analysis || computeNewtonRingB1Analysis(b1Table)).rows;
const b1Map = new Map();
b1Rows.forEach((row) => {
if (Number.isFinite(row.m)) {
b1Map.set(String(Math.round(row.m)), row);
}
});
const rows = tableToObjects(b2Table)
.map((row) => {
const serial = pickRowValue(row, [/序号/]);
const filledA = toNumber(firstNumberLike(pickRowValue(row, [/a\/mm\^2/, /^a/])));
const pair = parseNewtonPair(serial);
const upper = pair ? b1Map.get(String(pair.upper)) : null;
const lower = pair ? b1Map.get(String(pair.lower)) : null;
const computedA =
upper && lower && Number.isFinite(upper.filledD2) && Number.isFinite(lower.filledD2)
? upper.filledD2 - lower.filledD2
: NaN;
return {
serial,
pair,
filledA,
computedA,
delta:
Number.isFinite(filledA) && Number.isFinite(computedA)
? Math.abs(filledA - computedA)
: NaN,
};
})
.filter(
(row) => row.serial || Number.isFinite(row.filledA) || Number.isFinite(row.computedA)
);
return {
rows,
maxDeviation: maxFinite(rows.map((row) => row.delta)),
};
}
function parseNewtonPair(label) {
const match = String(label || "").match(/m\+n\s*=\s*(\d+).*m\s*=\s*(\d+)/);
if (!match) {
return null;
}
return {
upper: Number(match[1]),
lower: Number(match[2]),
};
}
function computeNewtonRingRadiusMeters(aValue, orderGap) {
if (!Number.isFinite(aValue) || !Number.isFinite(orderGap) || orderGap === 0) {
return NaN;
}
const lambdaMm = 589.3e-6;
return aValue / (4 * orderGap * lambdaMm) / 1000;
}
function sampleStdDev(values) {
const finite = values.filter((value) => Number.isFinite(value));
if (finite.length < 2) {
return NaN;
}
const mean = average(finite);
const variance =
finite.reduce((sum, value) => sum + Math.pow(value - mean, 2), 0) /
(finite.length - 1);
return Math.sqrt(variance);
}
function maxFinite(values) {
const finite = values.filter((value) => Number.isFinite(value));
return finite.length > 0 ? Math.max(...finite) : NaN;
}
function formatFixed(value, digits) {
return Number.isFinite(value) ? Number(value).toFixed(digits) : "NaN";
}
function normalizeResultExpression(value) {
return cleanText(value)
.replace(/[==]/g, "")
.replace(/[±\u00B1]/g, "±")
.replace(/\s+/g, "");
}
function appendNewtonRingCalculationBreakdown(list, context) {
const {
avgA,
stdS,
deltaA,
radius,
relativeRadius,
deltaRadius,
expression,
studentMeanA,
standardMeanA,
computedMeanA,
studentStdS,
standardStdS,
computedStdS,
studentDeltaA,
standardDeltaA,
computedDeltaA,
studentRadius,
standardRadius,
computedRadius,
studentRelativeRadius,
standardRelativeRadius,
computedRelativeRadius,
studentDeltaRadius,
standardDeltaRadius,
computedDeltaRadius,
studentExpression,
standardExpression,
computedExpression,
} = context;
list.push(
buildNumericCalculationRow({
label: "a 平均值",
item: avgA,
studentValue: studentMeanA,
standardValue: standardMeanA,
computedValue: computedMeanA,
computedValueText: Number.isFinite(computedMeanA)
? `${formatFixed(computedMeanA, 2)} mm^2(由 B2 表平均)`
: "无法按 B2 表重算",
referenceValue: "a = D^2(m+n) - D^2(m),本实验 n=15",
baselineValue: computedMeanA,
baselineLabel: "重算值",
precisionFallback: 4,
studentSuffix: "mm^2",
}),
buildNumericCalculationRow({
label: "a 标准差 s",
item: stdS,
studentValue: studentStdS,
standardValue: standardStdS,
computedValue: computedStdS,
computedValueText: Number.isFinite(computedStdS)
? `${formatFixed(computedStdS, 3)}(按 B2 三组差值样本标准差)`
: "无法按 B2 表重算",
referenceValue: "样本标准差反映三组 D^2 差值的离散程度",
baselineValue: computedStdS,
baselineLabel: "重算值",
precisionFallback: 1,
}),
buildNumericCalculationRow({
label: "Δa",
item: deltaA,
studentValue: studentDeltaA,
standardValue: standardDeltaA,
computedValue: computedDeltaA,
computedValueText: Number.isFinite(computedDeltaA)
? `${formatFixed(computedDeltaA, 2)}(按 3s 近似)`
: "当前未建立稳定重算公式",
referenceValue: "此项通常由重复测量离散程度折算得到",
baselineValue: computedDeltaA,
baselineLabel: "近似重算值",
precisionFallback: 1,
}),
buildNumericCalculationRow({
label: "曲率半径 R",
item: radius,
studentValue: studentRadius,
standardValue: standardRadius,
computedValue: computedRadius,
computedValueText: Number.isFinite(computedRadius)
? `${formatFixed(computedRadius, 3)} m(按 a/(4nλ),λ=589.3 nm,n=15)`
: "无法按 a 重算",
referenceValue: "牛顿环公式 D_m^2 = 4mRλ",
baselineValue: computedRadius,
baselineLabel: "重算值",
precisionFallback: 4,
studentSuffix: "m",
}),
{
label: "R 相对不确定度 / ΔR",
scoreText: formatScoreText(deltaRadius && deltaRadius.realScore, deltaRadius && deltaRadius.totalScore),
studentValue:
Number.isFinite(studentRelativeRadius) || Number.isFinite(studentDeltaRadius)
? `${Number.isFinite(studentRelativeRadius) ? `相对不确定度 ${rawStudentDisplay(relativeRadius, studentRelativeRadius, 3)}` : "相对不确定度缺失"};${
Number.isFinite(studentDeltaRadius) ? `ΔR ${rawStudentDisplay(deltaRadius, studentDeltaRadius, 2, "m")}` : "ΔR 缺失"
}`
: "未填写",
standardValue:
Number.isFinite(standardRelativeRadius) || Number.isFinite(standardDeltaRadius)
? `${Number.isFinite(standardRelativeRadius) ? `标准答案 ${rawStandardDisplay(relativeRadius, standardRelativeRadius, 3)}` : "XML 未提供相对不确定度"};${
Number.isFinite(standardDeltaRadius) ? `标准答案 ΔR ${rawStandardDisplay(deltaRadius, standardDeltaRadius, 2, "m")}` : "XML 未提供 ΔR"
}`
: "XML 未提供标准答案",
computedValue:
Number.isFinite(computedRelativeRadius) || Number.isFinite(computedDeltaRadius)
? `${Number.isFinite(computedRelativeRadius) ? `按 2Δa/a 近似 ${formatFixed(computedRelativeRadius, 3)}` : "无法估计相对不确定度"};${
Number.isFinite(computedDeltaRadius) ? `ΔR≈${formatFixed(computedDeltaRadius, 2)} m` : "无法估计 ΔR"
}`
: "当前未建立稳定重算公式",
referenceValue: "R 的相对不确定度通常与 a 的相对不确定度同量级",
deltaNote:
Number.isFinite(studentRelativeRadius) && Number.isFinite(computedRelativeRadius)
? `学生作答与近似重算值偏差 ${(relativeDelta(studentRelativeRadius, computedRelativeRadius) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(relativeRadius, 2),
},
{
label: "最终结果表达式",
scoreText: formatScoreText(expression && expression.realScore, expression && expression.totalScore),
studentValue: studentExpression || "未填写",
standardValue: standardExpression || "XML 未提供标准答案",
computedValue: computedExpression || "无法按当前数据重算",
referenceValue: "结果通常写成 R = (数值 ± 不确定度) m",
deltaNote:
studentExpression && computedExpression
? normalizeResultExpression(studentExpression) === normalizeResultExpression(computedExpression)
? "学生作答与重算结果表达式一致。"
: `学生作答与重算结果表达式不一致:${studentExpression} vs ${computedExpression}`
: "当前无法计算偏差",
precisionNote: "建议主值与不确定度保留到同一小数位,再写最终结果表达式。",
}
);
}
function stripRecomputePhrases(text) {
if (!text) return text;
return text
.replace(/[,,]\s*按[^,。,]*重算[^,。,]*/g, "")
.replace(/[,,]\s*无法按[^,。,]*重算[^,。,]*/g, "")
.replace(/[。,,]?\s*当前无法按[^。]*/g, "")
.replace(/[。,,]?\s*当前未建立稳定理论参考值[。]?/g, "")
.replace(/^[,,。]\s*/, "")
.replace(/[,,]\s*$/, "。")
.trim() || text;
}
function buildSectionBreakdown(parsedReport) {
const order = ["SYMD", "SYYL", "SYYQ", "SYBZ", "OP", "SKT", "SYZJ", "JSPY"];
return order
.map((type) => {
const question = parsedReport.questionsByType[type];
if (!question) {
return {
type,
label: CONFIG.labels[type] || type,
scoreText: "-",
contentType: "缺失",
analysis: "XML 中未找到该栏目。",
};
}
const textLength = question.decodedContent.text.trim().length;
const meaningfulText = getMeaningfulQuestionText(question);
const imageCount = question.decodedContent.imageCount;
let contentType = "空";
if (type === "OP") {
contentType = `数据项 ${question.paras.length} 个`;
} else if (meaningfulText.length > 0 && imageCount > 0) {
contentType = `图文混合`;
} else if (meaningfulText.length > 0) {
contentType = "文本";
} else if (imageCount > 0) {
contentType = "图片型";
}
return {
type,
label: question.title || CONFIG.labels[type] || type,
scoreText: formatScoreText(question.score.real, question.score.total),
contentType,
analysis:
type === "OP"
? `包含 ${Object.values(parsedReport.opItemsByName).filter((item) => !isAuxiliaryOpItem(item.name)).length} 个主要数据项。`
: meaningfulText.length > 0
? `识别到 ${meaningfulText.length} 字文本。`
: imageCount > 0
? `图片型内容,共 ${imageCount} 张,不做文字建议。`
: "未识别到可分析内容。",
};
})
.filter(Boolean);
}
function buildOpBreakdown(parsedReport) {
return Object.values(parsedReport.opItemsByName)
.filter((item) => !isAuxiliaryOpItem(item.name))
.map((item) => ({
name: item.name,
label: item.label,
scoreText: formatScoreText(item.realScore, item.totalScore),
kind: mapParsedType(item.parsedType),
studentAnswer: summarizeOpAnswer(item, "student"),
standardAnswer: summarizeOpAnswer(item, "standard"),
analysis:
item.parsedType === "table" && item.table && item.table.isValid
? `${item.table.rows.length} 行 ${item.table.headers.length} 列。${
item.standardTable && item.standardTable.isValid
? ` 标准答案同样提供 ${item.standardTable.rows.length} 行。`
: ""
}`
: item.parsedType === "number"
? `学生作答 ${getContentText(item.realResult) || "未填写"}${
getContentText(item.stdResult)
? `,标准答案 ${getContentText(item.stdResult)}`
: ""
}`
: item.parsedType === "numberList"
? `学生作答 ${summarizeOpAnswer(item, "student")}${
item.standardValues && item.standardValues.length > 0
? `,标准答案 ${summarizeOpAnswer(item, "standard")}`
: ""
}`
: item.parsedType === "drawing"
? item.imageCount > 0 || item.standardImageCount > 0
? "图像型数据,不做数值建议。"
: "未识别到可分析图像内容。"
: item.value
? truncateText(item.value, 60)
: "无可分析文本。",
}));
}
function buildTableBreakdown(parsedReport) {
return Object.values(parsedReport.opItemsByName)
.filter((item) => !isAuxiliaryOpItem(item.name) && item.parsedType === "table")
.map((item) => ({
name: item.name,
label: item.label,
scoreText: formatScoreText(item.realScore, item.totalScore),
summary: buildTableSummary(item),
submittedHeaders:
item.submittedTable && item.submittedTable.headers.length > 0
? item.submittedTable.headers
: getContentText(item.realResult)
? ["原始内容"]
: [],
submittedRows:
item.submittedTable && item.submittedTable.isValid
? item.submittedTable.rows
: getContentText(item.realResult)
? [[getContentText(item.realResult)]]
: [],
submittedColumnNotes:
item.submittedTable && item.submittedTable.isValid ? buildColumnNotes(item.submittedTable) : [],
standardHeaders:
item.standardTable && item.standardTable.headers.length > 0
? item.standardTable.headers
: getContentText(item.stdResult)
? ["原始内容"]
: [],
standardRows:
item.standardTable && item.standardTable.isValid
? item.standardTable.rows
: getContentText(item.stdResult)
? [[getContentText(item.stdResult)]]
: [],
standardColumnNotes:
item.standardTable && item.standardTable.isValid ? buildColumnNotes(item.standardTable) : [],
}));
}
function summarizeOpAnswer(item, mode) {
if (!item) {
return "未提供";
}
if (item.parsedType === "table") {
const table = mode === "student" ? item.submittedTable : item.standardTable;
const content = mode === "student" ? item.realResult : item.stdResult;
if (table && table.isValid) {
return `${table.rows.length} 行 ${table.headers.length} 列`;
}
return truncateText(getContentText(content) || "未提供", 60);
}
if (item.parsedType === "number") {
const value = mode === "student" ? item.submittedValue : item.standardValue;
const rawText = getContentText(mode === "student" ? item.realResult : item.stdResult);
if (rawText && !looksLikeGarbledText(rawText)) {
return truncateText(rawText, 60);
}
return Number.isFinite(value) ? `${value}` : "未填写";
}
if (item.parsedType === "numberList") {
const rawContent = mode === "student" ? item.realResult : item.stdResult;
const rawText = getContentText(rawContent);
if (rawText && !looksLikeGarbledText(rawText)) {
return truncateText(rawText.replace(/\s*[;;]\s*/g, ";"), 60);
}
const values = mode === "student" ? item.submittedValues : item.standardValues;
return Array.isArray(values) && values.length > 0
? formatNumberList(values)
: "未填写";
}
if (item.parsedType === "drawing") {
const count = mode === "student" ? item.submittedImageCount : item.standardImageCount;
return count > 0 ? `图像 ${count} 张` : "未提供";
}
const text = mode === "student" ? item.submittedText : item.standardText;
return truncateText(text || "未提供", 60);
}
function buildTableSummary(item) {
const parts = [];
if (item.submittedTable && item.submittedTable.isValid) {
parts.push(`学生作答 ${item.submittedTable.rows.length} 行 ${item.submittedTable.headers.length} 列`);
}
if (item.standardTable && item.standardTable.isValid) {
parts.push(`标准答案 ${item.standardTable.rows.length} 行 ${item.standardTable.headers.length} 列`);
}
return parts.join(";") || "表格结构异常";
}
function buildTextBreakdown(parsedReport) {
return ["SYMD", "SYYL", "SYYQ", "SYBZ", "SKT", "SYZJ"]
.map((type) => {
const question = parsedReport.questionsByType[type];
if (!question) {
return {
type,
label: CONFIG.labels[type] || type,
scoreText: "-",
preview: "XML 中未找到该栏目。",
analysis: "无法分析。",
analyzable: false,
fullText: "",
};
}
const text = getMeaningfulQuestionText(question);
const imageCount = question.decodedContent.imageCount;
return {
type,
label: question.title || CONFIG.labels[type] || type,
scoreText: formatScoreText(question.score.real, question.score.total),
preview:
text.length > 0
? truncateText(text, 120)
: imageCount > 0
? `图片型内容,共 ${imageCount} 张`
: "无可分析文本",
analysis:
text.length > 0
? `文本 ${text.length} 字,可结合原文自行复核。`
: imageCount > 0
? "图片型内容,不做文字建议。"
: "无可分析文本。",
analyzable: text.length > 0,
fullText: text,
};
})
.filter(Boolean);
}
function buildColumnNotes(table) {
if (!table || !table.isValid) {
return [];
}
return table.headers.map((header, index) => {
const values = table.rows.map((row) => cleanText(row[index]));
const numericValues = values.filter(isNumberLike);
if (numericValues.length !== values.length || values.length === 0) {
return `${header}:包含非纯数字值。`;
}
const decimals = numericValues.map(countDecimalPlaces);
const uniqueDecimals = Array.from(new Set(decimals)).sort((a, b) => a - b);
if (uniqueDecimals.length === 1) {
return `${header}:统一保留 ${uniqueDecimals[0]} 位小数。`;
}
return `${header}:小数位混合,出现 ${uniqueDecimals.join(" / ")} 位。`;
});
}
function updateBreakdownAnalysis(items, key, analysis) {
const item = items.find((entry) => entry.name === key);
if (item) {
item.analysis = analysis;
}
}
function renameBreakdownLabel(items, key, label) {
const item = items.find((entry) => entry.name === key);
if (item) {
item.label = label;
}
}
function mapParsedType(parsedType) {
const mapping = {
table: "表格",
number: "数值",
numberList: "多值数值",
drawing: "图像",
text: "文本",
};
return mapping[parsedType] || parsedType || "未知";
}
function formatScoreText(real, total) {
return Number.isFinite(real) || Number.isFinite(total) ? formatScore(real, total) : "-";
}
function sumFiniteNumbers(...values) {
const finiteValues = values.filter((value) => Number.isFinite(value));
if (finiteValues.length === 0) {
return NaN;
}
return finiteValues.reduce((sum, value) => sum + value, 0);
}
function numberValueOf(item, fallback = NaN) {
if (!item) return fallback;
if (Number.isFinite(item.value)) return item.value;
if (Number.isFinite(item.submittedValue)) return item.submittedValue;
return fallback;
}
function rawStudentDisplay(item, numericFallback, decimals, suffix) {
const raw = item && cleanText(item.realResult.text || item.realResult.raw || "");
const sanitized = raw && !looksLikeGarbledText(raw) ? raw : "";
const display = sanitized || (Number.isFinite(numericFallback) ? `${numericFallback}` : "");
if (!display) return "未填写";
return suffix ? `${display} ${suffix}` : display;
}
function rawStandardDisplay(item, numericFallback, decimals, suffix) {
const raw = item && item.stdResult ? cleanText(item.stdResult.text || item.stdResult.raw || "") : "";
const sanitized = raw && !looksLikeGarbledText(raw) ? raw : "";
const display = sanitized || (Number.isFinite(numericFallback) ? `${numericFallback}` : "");
if (!display) return "XML 未提供标准答案";
return suffix ? `${display} ${suffix}` : display;
}
function buildNumericCalculationRow(options) {
const {
label,
item,
studentValue,
standardValue,
computedValue,
computedValueText,
computedMissingText = "无法按当前数据重算",
referenceValue,
baselineValue,
baselineLabel,
precisionFallback,
precisionNote,
studentSuffix = "",
standardSuffix = studentSuffix,
} = options;
const fallbackBaseline = Number.isFinite(standardValue) ? standardValue : computedValue;
const comparisonValue = Number.isFinite(baselineValue) ? baselineValue : fallbackBaseline;
const comparisonLabel =
baselineLabel || (Number.isFinite(standardValue) ? "标准答案" : "重算值");
return {
label,
scoreText: formatScoreText(item && item.realScore, item && item.totalScore),
studentValue: rawStudentDisplay(item, studentValue, null, studentSuffix),
standardValue: rawStandardDisplay(item, standardValue, null, standardSuffix),
computedValue:
typeof computedValueText === "string"
? computedValueText
: Number.isFinite(computedValue)
? `${computedValue}`
: computedMissingText,
referenceValue,
deltaNote:
Number.isFinite(studentValue) && Number.isFinite(comparisonValue)
? `学生作答与${comparisonLabel}偏差 ${(relativeDelta(studentValue, comparisonValue) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: precisionNote || buildItemPrecisionNote(item, precisionFallback),
};
}
function numberStandardValueOf(item, fallback = NaN) {
return item && Number.isFinite(item.standardValue) ? item.standardValue : fallback;
}
function numberListValueOf(item) {
if (item && Array.isArray(item.submittedValues) && item.submittedValues.length > 0) {
return item.submittedValues.filter((value) => Number.isFinite(value));
}
const value = numberValueOf(item);
return Number.isFinite(value) ? [value] : [];
}
function numberListStandardValueOf(item) {
if (item && Array.isArray(item.standardValues) && item.standardValues.length > 0) {
return item.standardValues.filter((value) => Number.isFinite(value));
}
const value = numberStandardValueOf(item);
return Number.isFinite(value) ? [value] : [];
}
function safeDivide(numerator, denominator) {
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0) {
return NaN;
}
return numerator / denominator;
}
function average(values) {
const finiteValues = values.filter((value) => Number.isFinite(value));
if (finiteValues.length === 0) {
return NaN;
}
return finiteValues.reduce((sum, value) => sum + value, 0) / finiteValues.length;
}
function pickRowValue(row, matchers) {
const entries = Object.entries(row || {});
const match = entries.find(([header]) =>
matchers.some((matcher) =>
typeof matcher === "string" ? header.includes(matcher) : matcher.test(header)
)
);
return match ? match[1] : "";
}
function getContentText(content) {
if (!content) {
return "";
}
const text = cleanText(content.text || content.raw || "");
return sanitizeForDisplay(text) || "";
}
function parseExpectedSigFigs(value) {
if (typeof value === "number" && Number.isFinite(value)) return [value];
if (typeof value === "string") {
return value.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n));
}
if (Array.isArray(value)) return value.filter((n) => Number.isFinite(n));
return [];
}
function formatExpectedSigFigs(value) {
const list = parseExpectedSigFigs(value);
return list.length > 1 ? list.join(" 或 ") : String(list[0] || "?");
}
function precisionUnitForType(precisionType) {
return precisionType === "小数点" ? "小数" : "有效数字";
}
function formatItemPrecisionRequirement(item) {
if (!item || !item.numDate) {
return "";
}
return `${formatExpectedSigFigs(item.numDate)} 位${precisionUnitForType(item.numType)}`;
}
function buildPrecisionNote(rawValue, expectedSigFigs, precisionType) {
const rawText = cleanText(rawValue);
const displayExpected = formatExpectedSigFigs(expectedSigFigs);
const isDecimalPlaces = precisionType === "小数点";
const unitText = precisionUnitForType(precisionType);
if (!rawText || !isNumberLike(rawText)) {
return `建议保留 ${displayExpected} 位${unitText},当前未识别到有效填写值。`;
}
const actual = isDecimalPlaces ? countDecimalPlaces(rawText) : countSignificantFigures(rawText);
const expectedList = parseExpectedSigFigs(expectedSigFigs);
const matches = expectedList.includes(actual);
const result = matches
? `当前${unitText}位数符合。`
: actual > Math.max(...expectedList)
? `当前${unitText}位偏多。`
: `当前${unitText}位偏少。`;
return `建议保留 ${displayExpected} 位${unitText},当前填写 ${actual} 位。${result}`;
}
function buildItemPrecisionNote(item, fallbackSigFigs) {
const rawText = item && (item.realResult.text || item.realResult.raw);
const expected = (item && item.numDate) || fallbackSigFigs;
return buildPrecisionNote(rawText, expected, item && item.numType);
}
function getMeaningfulQuestionText(question) {
if (!question || !question.decodedContent) {
return "";
}
const text = cleanText(question.decodedContent.text);
const title = cleanText(question.title);
if (!text) {
return "";
}
if (title && text === title) {
return "";
}
if (question.decodedContent.imageCount > 0 && text.length <= Math.max(title.length, 6)) {
return "";
}
return text;
}
function applySectionMarkers(reviewResult) {
clearSectionMarkers();
if (!state.currentModal) {
return;
}
reviewResult.markers.forEach((marker) => {
const label = CONFIG.labels[marker.section] || marker.section;
const candidates = Array.from(
state.currentModal.querySelectorAll(CONFIG.selectors.titleBlocks)
);
const node = candidates.find((candidate) =>
cleanText(candidate.textContent).includes(label)
);
if (node) {
node.setAttribute(CONFIG.markerAttr, marker.label);
}
});
}
function clearSectionMarkers() {
document
.querySelectorAll(`[${CONFIG.markerAttr}]`)
.forEach((node) => node.removeAttribute(CONFIG.markerAttr));
}
const EXPERIMENT_DETECTORS = [
{ kind: "photoelectric", aliasKey: "photoelectric" },
{ kind: "collision", aliasKey: "collision" },
{ kind: "thermalExpansion", aliasKey: "thermalExpansion" },
{
kind: "elasticModulus",
aliasKey: "elasticModulus",
pattern: /拉伸法/,
extra: (parsedReport) =>
!!resolveOpItem(parsedReport, "BQDD_n", "BQDDn", "YSML_BQDD", "YSMLBQDD", "YSML_E"),
},
{ kind: "newtonRings", aliasKey: "newtonRings", pattern: /曲率半径/ },
{ kind: "surfaceTension", pattern: /表面张力/ },
{ kind: "polarization", aliasKey: "polarization" },
{ kind: "thermocouple", pattern: /温差电偶/ },
{ kind: "standingWave", aliasKey: "standingWave" },
{ kind: "lengthDensity", aliasKey: "lengthDensity" },
{ kind: "viscosity", pattern: /黏度|粘度/ },
{ kind: "errorCompanion", aliasKey: "errorCompanion", pattern: /误差配套/ },
{ kind: "wheatstone", pattern: /惠斯通|惠斯登|电桥.*电阻温度/ },
{ kind: "oscilloscope", pattern: /示波器/ },
{ kind: "hallEffect", pattern: /霍尔效应|霍尔元件/ },
];
function detectExperimentKind(parsedReport) {
const name = parsedReport.meta.paperName || "";
for (const detector of EXPERIMENT_DETECTORS) {
const alias = detector.aliasKey ? CONFIG.experimentAliases[detector.aliasKey] : "";
if (alias && name.includes(alias)) {
return detector.kind;
}
if (detector.pattern && detector.pattern.test(name)) {
return detector.kind;
}
if (detector.extra && detector.extra(parsedReport)) {
return detector.kind;
}
}
return "";
}
function reviewWheatstone(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "SJ_BG", "温度-电阻数据表");
renameBreakdownLabel(detailState.tableBreakdown, "SJ_BG", "温度-电阻数据表");
renameBreakdownLabel(detailState.opBreakdown, "R2_R1", "电阻比 R₂/R₁");
renameBreakdownLabel(detailState.opBreakdown, "R0", "R₀ (0℃电阻)");
renameBreakdownLabel(detailState.opBreakdown, "BZRt", "ΔRx/Δt (电阻温度斜率)");
renameBreakdownLabel(detailState.opBreakdown, "a", "温度系数 α");
renameBreakdownLabel(detailState.opBreakdown, "BFC", "百分差");
const r2r1 = resolveOpItem(parsedReport, "R2_R1");
const sjbg = resolveOpItem(parsedReport, "SJ_BG");
const drawing = resolveOpItem(parsedReport, "BKD_SJB_Drawing");
const r0 = resolveOpItem(parsedReport, "R0");
const bzrt = resolveOpItem(parsedReport, "BZRt");
const a = resolveOpItem(parsedReport, "a");
const bfc = resolveOpItem(parsedReport, "BFC");
const studentR2R1 = numberValueOf(r2r1);
const standardR2R1 = numberStandardValueOf(r2r1);
const studentR0 = numberValueOf(r0);
const standardR0 = numberStandardValueOf(r0);
const studentBzrt = numberValueOf(bzrt);
const standardBzrt = numberStandardValueOf(bzrt);
const studentA = numberValueOf(a);
const standardA = numberStandardValueOf(a);
const studentBfc = numberValueOf(bfc);
const standardBfc = numberStandardValueOf(bfc);
const sjbgRows = tableToObjects(sjbg && sjbg.table);
const fit = computeLinearFit(
sjbgRows.map((row) => ({
x: toNumber(firstNumberLike(pickRowValue(row, ["温度", "t"]))),
y: toNumber(firstNumberLike(pickRowValue(row, ["Rx", "电阻"]))),
}))
);
metrics.push({
label: "电阻比 R₂/R₁",
value: Number.isFinite(studentR2R1) ? `${studentR2R1}` : "未填写",
note: Number.isFinite(standardR2R1) ? `标准答案 ${standardR2R1}` : "",
});
metrics.push({
label: "数据表",
value: sjbg && sjbg.table && sjbg.table.isValid ? `${sjbg.table.rows.length} 行` : "未识别",
note: "温度-电阻测量数据。",
});
metrics.push({
label: "R₀",
value: Number.isFinite(studentR0) ? `${studentR0} Ω` : "未填写",
note: Number.isFinite(standardR0) ? `标准答案 ${rawStandardDisplay(r0, standardR0, 2, "Ω")}` : "",
});
metrics.push({
label: "ΔRx/Δt",
value: Number.isFinite(studentBzrt) ? `${studentBzrt} Ω/℃` : "未填写",
note: Number.isFinite(fit.slope)
? `按数据表拟合 ${roundTo(fit.slope, 3)} Ω/℃${Number.isFinite(standardBzrt) ? `,标准答案 ${rawStandardDisplay(bzrt, standardBzrt, 3, "Ω/℃")}` : ""}`
: "",
});
metrics.push({
label: "温度系数 α",
value: Number.isFinite(studentA) ? `${studentA} /℃` : "未填写",
note: Number.isFinite(standardA) ? `标准答案 ${standardA} /℃` : "",
});
metrics.push({
label: "百分差",
value: Number.isFinite(studentBfc) ? `${studentBfc}%` : "未填写",
note: Number.isFinite(standardBfc) ? `标准答案 ${standardBfc}%` : "",
});
if (!sjbg || !sjbg.table || !sjbg.table.isValid) {
issues.push({
code: "SJ_BG_missing",
section: "OP",
message: "未识别到温度-电阻数据表,无法检验拟合结果。",
});
markers.push({ section: "OP", label: "数据表缺失" });
}
maybePushValueIssue(issues, markers, "OP", "R₀", studentR0, standardR0, fit.intercept, "R0偏差");
maybePushValueIssue(issues, markers, "OP", "ΔRx/Δt", studentBzrt, standardBzrt, fit.slope, "斜率偏差");
if (Number.isFinite(studentA) && Number.isFinite(standardA) && relativeDelta(studentA, standardA) > 0.05) {
issues.push({
code: "a_mismatch",
section: "OP",
message: `温度系数 α 与标准答案偏差较大:学生作答=${studentA},标准答案=${standardA}。`,
});
markers.push({ section: "OP", label: "α偏差" });
}
if (Number.isFinite(studentBfc) && Number.isFinite(standardBfc) && Math.abs(studentBfc - standardBfc) > 3) {
issues.push({
code: "BFC_mismatch",
section: "OP",
message: `百分差与标准答案偏差较大:学生作答=${studentBfc}%,标准答案=${standardBfc}%。`,
});
}
detailState.calculationBreakdown.push(
{
label: "R₀ (截距)",
scoreText: formatScoreText(r0 && r0.realScore, r0 && r0.totalScore),
studentValue: rawStudentDisplay(r0, studentR0, 2, "Ω"),
standardValue: rawStandardDisplay(r0, standardR0, 2, "Ω"),
deltaNote: Number.isFinite(studentR0) && Number.isFinite(standardR0)
? `学生作答与标准答案偏差 ${(relativeDelta(studentR0, standardR0) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(r0, "4,5"),
},
{
label: "ΔRx/Δt (斜率)",
scoreText: formatScoreText(bzrt && bzrt.realScore, bzrt && bzrt.totalScore),
studentValue: rawStudentDisplay(bzrt, studentBzrt, 3, "Ω/℃"),
standardValue: rawStandardDisplay(bzrt, standardBzrt, 3, "Ω/℃"),
deltaNote: Number.isFinite(studentBzrt) && Number.isFinite(standardBzrt)
? `学生作答与标准答案偏差 ${(relativeDelta(studentBzrt, standardBzrt) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(bzrt, 3),
},
{
label: "温度系数 α",
scoreText: formatScoreText(a && a.realScore, a && a.totalScore),
studentValue: rawStudentDisplay(a, studentA, 5, "/℃"),
standardValue: rawStandardDisplay(a, standardA, 5, "/℃"),
deltaNote: Number.isFinite(studentA) && Number.isFinite(standardA)
? `学生作答与标准答案偏差 ${(relativeDelta(studentA, standardA) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(a, 3),
},
{
label: "百分差",
scoreText: formatScoreText(bfc && bfc.realScore, bfc && bfc.totalScore),
studentValue: rawStudentDisplay(bfc, studentBfc, 1, "%"),
standardValue: rawStandardDisplay(bfc, standardBfc, 1, "%"),
deltaNote: Number.isFinite(studentBfc) && Number.isFinite(standardBfc)
? `绝对偏差 ${Math.abs(studentBfc - standardBfc).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(bfc, "1,2"),
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"SJ_BG",
sjbg && sjbg.table && sjbg.table.isValid
? `共 ${sjbg.table.rows.length} 行,拟合斜率 ${Number.isFinite(fit.slope) ? roundTo(fit.slope, 3) : "无法拟合"},截距 ${Number.isFinite(fit.intercept) ? roundTo(fit.intercept, 2) : "无法拟合"}。`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"BKD_SJB_Drawing",
drawing && (drawing.imageCount > 0 || drawing.standardImageCount > 0)
? "Rx-t 曲线图,图像型数据,不做数值建议。"
: "未识别到可分析图像内容。"
);
}
function reviewOscilloscope(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "B1", "波形参数测量表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "波形参数测量表");
const b1 = resolveOpItem(parsedReport, "B1");
const drawing = resolveOpItem(parsedReport, "BKD_SJB_Drawing");
const b1Rows = tableToObjects(b1 && b1.table);
const b1StandardRows = tableToObjects(b1 && b1.standardTable);
metrics.push({
label: "波形参数表",
value: b1 && b1.table && b1.table.isValid
? `${b1.table.rows.length} 行学生作答`
: "未识别",
note: b1StandardRows.length > 0
? `标准答案 ${b1StandardRows.length} 行`
: "",
});
metrics.push({
label: "利萨如图形",
value: drawing && drawing.imageCount > 0 ? "已识别图像" : "未识别图像",
note: "图像型内容,不做数值建议。",
});
if (!b1 || !b1.table || !b1.table.isValid) {
issues.push({
code: "B1_missing",
section: "OP",
message: "未识别到波形参数表 B1,建议确认数据处理部分是否完整。",
});
markers.push({ section: "OP", label: "B1缺失" });
} else {
if (b1.table.rows.length < 3) {
issues.push({
code: "B1_rows_short",
section: "OP",
message: "波形参数表数据行偏少,应至少包含方波、锯齿波、正弦波三行数据。",
});
}
if (b1StandardRows.length > 0 && b1Rows.length > 0) {
const mismatchColumns = [];
b1Rows.forEach((row, index) => {
const stdRow = b1StandardRows[index];
if (!stdRow) return;
Object.keys(stdRow).forEach((key) => {
const sv = toNumber(firstNumberLike(stdRow[key]));
const rv = toNumber(firstNumberLike(row[key]));
if (Number.isFinite(sv) && Number.isFinite(rv) && sv !== 0 && relativeDelta(rv, sv) > 0.15) {
mismatchColumns.push(`第${index + 1}行 ${key}`);
}
});
});
if (mismatchColumns.length > 0) {
issues.push({
code: "B1_value_mismatch",
section: "OP",
message: `波形参数表部分数据与标准答案偏差较大:${mismatchColumns.slice(0, 5).join("、")}${mismatchColumns.length > 5 ? " 等" : ""}。`,
});
}
}
}
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${b1.table.rows.length} 行 ${b1.table.headers.length} 列。${
b1StandardRows.length > 0
? `标准答案 ${b1StandardRows.length} 行。`
: ""
}`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"BKD_SJB_Drawing",
drawing && (drawing.imageCount > 0 || drawing.standardImageCount > 0)
? "利萨如图形,图像型数据,不做数值建议。"
: "未识别到可分析图像内容。"
);
}
function reviewHallEffect(parsedReport, issues, suggestions, metrics, markers, detailState) {
renameBreakdownLabel(detailState.opBreakdown, "N", "螺线管匝数 N");
renameBreakdownLabel(detailState.opBreakdown, "L", "螺线管长度 L (mm)");
renameBreakdownLabel(detailState.opBreakdown, "D", "螺线管外径 D (mm)");
renameBreakdownLabel(detailState.opBreakdown, "B1", "VH-B 测量表");
renameBreakdownLabel(detailState.tableBreakdown, "B1", "VH-B 测量表");
renameBreakdownLabel(detailState.opBreakdown, "tu1", "VH-B 曲线图");
renameBreakdownLabel(detailState.opBreakdown, "k", "斜率 k (mV/T)");
renameBreakdownLabel(detailState.opBreakdown, "Kn", "霍尔元件灵敏度 Kₕ");
renameBreakdownLabel(detailState.opBreakdown, "B2", "B-x 测量表");
renameBreakdownLabel(detailState.tableBreakdown, "B2", "B-x 测量表");
renameBreakdownLabel(detailState.opBreakdown, "tu2", "B-x 曲线图");
renameBreakdownLabel(detailState.opBreakdown, "Bz", "B 中心理论值");
renameBreakdownLabel(detailState.opBreakdown, "B0", "B₀ 端面理论值");
const itemN = resolveOpItem(parsedReport, "N");
const itemL = resolveOpItem(parsedReport, "L");
const itemD = resolveOpItem(parsedReport, "D");
const b1 = resolveOpItem(parsedReport, "B1");
const tu1 = resolveOpItem(parsedReport, "tu1");
const itemK = resolveOpItem(parsedReport, "k");
const itemKn = resolveOpItem(parsedReport, "Kn");
const b2 = resolveOpItem(parsedReport, "B2");
const tu2 = resolveOpItem(parsedReport, "tu2");
const itemBz = resolveOpItem(parsedReport, "Bz");
const itemB0 = resolveOpItem(parsedReport, "B0");
const studentK = numberValueOf(itemK);
const standardK = numberStandardValueOf(itemK);
const studentKn = numberValueOf(itemKn);
const standardKn = numberStandardValueOf(itemKn);
const studentBz = numberValueOf(itemBz);
const standardBz = numberStandardValueOf(itemBz);
const studentB0 = numberValueOf(itemB0);
const standardB0 = numberStandardValueOf(itemB0);
const b1Rows = tableToObjects(b1 && b1.table);
const b1StdRows = tableToObjects(b1 && b1.standardTable);
const b2Rows = tableToObjects(b2 && b2.table);
const b2StdRows = tableToObjects(b2 && b2.standardTable);
const fit = computeLinearFit(
b1Rows.map((row) => ({
x: toNumber(firstNumberLike(pickRowValue(row, [/B/, /T/]))),
y: toNumber(firstNumberLike(pickRowValue(row, [/\|VH\|/, /VH/]))),
}))
);
metrics.push({
label: "螺线管参数",
value: `N=${numberValueOf(itemN, "?")}, L=${numberValueOf(itemL, "?")}mm, D=${numberValueOf(itemD, "?")}mm`,
note: "仪器参数,由系统赋值。",
});
metrics.push({
label: "VH-B 数据表",
value: b1 && b1.table && b1.table.isValid ? `${b1.table.rows.length} 行` : "未识别",
note: b1StdRows.length > 0 ? `标准答案 ${b1StdRows.length} 行` : "",
});
metrics.push({
label: "斜率 k",
value: Number.isFinite(studentK) ? `${studentK} mV/T` : "未填写",
note: Number.isFinite(fit.slope)
? `按 B1 表拟合 ${roundTo(fit.slope, 2)} mV/T${Number.isFinite(standardK) ? `,标准答案 ${standardK}` : ""}`
: Number.isFinite(standardK) ? `标准答案 ${standardK}` : "",
});
metrics.push({
label: "灵敏度 Kₕ",
value: Number.isFinite(studentKn) ? `${studentKn} mV/(mA·T)` : "未填写",
note: Number.isFinite(standardKn) ? `标准答案 ${standardKn}` : "",
});
metrics.push({
label: "B-x 数据表",
value: b2 && b2.table && b2.table.isValid ? `${b2.table.rows.length} 行` : "未识别",
note: b2StdRows.length > 0 ? `标准答案 ${b2StdRows.length} 行` : "",
});
metrics.push({
label: "B 中心",
value: Number.isFinite(studentBz) ? `${studentBz} T` : "未填写",
note: Number.isFinite(standardBz) ? `标准答案 ${standardBz} T` : "",
});
metrics.push({
label: "B₀ 端面",
value: Number.isFinite(studentB0) ? `${studentB0} T` : "未填写",
note: Number.isFinite(standardB0) ? `标准答案 ${standardB0} T` : "",
});
if (!b1 || !b1.table || !b1.table.isValid) {
issues.push({ code: "B1_missing", section: "OP", message: "未识别到 VH-B 测量表 B1。" });
markers.push({ section: "OP", label: "B1缺失" });
}
if (!b2 || !b2.table || !b2.table.isValid) {
issues.push({ code: "B2_missing", section: "OP", message: "未识别到 B-x 测量表 B2。" });
markers.push({ section: "OP", label: "B2缺失" });
}
maybePushValueIssue(issues, markers, "OP", "斜率 k", studentK, standardK, fit.slope, "k偏差");
if (Number.isFinite(studentKn) && Number.isFinite(standardKn) && relativeDelta(studentKn, standardKn) > 0.05) {
issues.push({
code: "Kn_mismatch",
section: "OP",
message: `霍尔元件灵敏度 Kₕ 与标准答案偏差较大:学生作答=${studentKn},标准答案=${standardKn}。`,
});
markers.push({ section: "OP", label: "Kₕ偏差" });
}
if (Number.isFinite(studentBz) && Number.isFinite(standardBz) && relativeDelta(studentBz, standardBz) > 0.05) {
issues.push({
code: "Bz_mismatch",
section: "OP",
message: `B 中心理论值与标准答案偏差较大:学生作答=${studentBz},标准答案=${standardBz}。`,
});
markers.push({ section: "OP", label: "Bz偏差" });
}
if (Number.isFinite(studentB0) && Number.isFinite(standardB0) && relativeDelta(studentB0, standardB0) > 0.05) {
issues.push({
code: "B0_mismatch",
section: "OP",
message: `B₀ 端面理论值与标准答案偏差较大:学生作答=${studentB0},标准答案=${standardB0}。`,
});
markers.push({ section: "OP", label: "B₀偏差" });
}
if (b1StdRows.length > 0 && b1Rows.length > 0) {
const mismatchCols = [];
b1Rows.forEach((row, idx) => {
const stdRow = b1StdRows[idx];
if (!stdRow) return;
Object.keys(stdRow).forEach((key) => {
const sv = toNumber(firstNumberLike(stdRow[key]));
const rv = toNumber(firstNumberLike(row[key]));
if (Number.isFinite(sv) && Number.isFinite(rv) && sv !== 0 && relativeDelta(rv, sv) > 0.1) {
mismatchCols.push(`第${idx + 1}行 ${key}`);
}
});
});
if (mismatchCols.length > 0) {
issues.push({
code: "B1_value_mismatch",
section: "OP",
message: `VH-B 表部分数据与标准答案偏差较大:${mismatchCols.slice(0, 5).join("、")}${mismatchCols.length > 5 ? " 等" : ""}。`,
});
}
}
if (b2StdRows.length > 0 && b2Rows.length > 0) {
const mismatchCols = [];
b2Rows.forEach((row, idx) => {
const stdRow = b2StdRows[idx];
if (!stdRow) return;
Object.keys(stdRow).forEach((key) => {
const sv = toNumber(firstNumberLike(stdRow[key]));
const rv = toNumber(firstNumberLike(row[key]));
if (Number.isFinite(sv) && Number.isFinite(rv) && sv !== 0 && relativeDelta(rv, sv) > 0.1) {
mismatchCols.push(`第${idx + 1}行 ${key}`);
}
});
});
if (mismatchCols.length > 0) {
issues.push({
code: "B2_value_mismatch",
section: "OP",
message: `B-x 表部分数据与标准答案偏差较大:${mismatchCols.slice(0, 5).join("、")}${mismatchCols.length > 5 ? " 等" : ""}。`,
});
}
}
detailState.calculationBreakdown.push(
{
label: "斜率 k",
scoreText: formatScoreText(itemK && itemK.realScore, itemK && itemK.totalScore),
studentValue: rawStudentDisplay(itemK, studentK, 2, "mV/T"),
standardValue: rawStandardDisplay(itemK, standardK, 2, "mV/T"),
deltaNote: Number.isFinite(studentK) && Number.isFinite(standardK)
? `学生作答与标准答案偏差 ${(relativeDelta(studentK, standardK) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(itemK, 3),
},
{
label: "霍尔元件灵敏度 Kₕ",
scoreText: formatScoreText(itemKn && itemKn.realScore, itemKn && itemKn.totalScore),
studentValue: rawStudentDisplay(itemKn, studentKn, 2, "mV/(mA·T)"),
standardValue: rawStandardDisplay(itemKn, standardKn, 2, "mV/(mA·T)"),
deltaNote: Number.isFinite(studentKn) && Number.isFinite(standardKn)
? `学生作答与标准答案偏差 ${(relativeDelta(studentKn, standardKn) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(itemKn, 3),
},
{
label: "B 中心理论值",
scoreText: formatScoreText(itemBz && itemBz.realScore, itemBz && itemBz.totalScore),
studentValue: rawStudentDisplay(itemBz, studentBz, 4, "T"),
standardValue: rawStandardDisplay(itemBz, standardBz, 4, "T"),
deltaNote: Number.isFinite(studentBz) && Number.isFinite(standardBz)
? `学生作答与标准答案偏差 ${(relativeDelta(studentBz, standardBz) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(itemBz, 3),
},
{
label: "B₀ 端面理论值",
scoreText: formatScoreText(itemB0 && itemB0.realScore, itemB0 && itemB0.totalScore),
studentValue: rawStudentDisplay(itemB0, studentB0, 4, "T"),
standardValue: rawStandardDisplay(itemB0, standardB0, 4, "T"),
deltaNote: Number.isFinite(studentB0) && Number.isFinite(standardB0)
? `学生作答与标准答案偏差 ${(relativeDelta(studentB0, standardB0) * 100).toFixed(2)}%`
: "当前无法计算偏差",
precisionNote: buildItemPrecisionNote(itemB0, 3),
}
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B1",
b1 && b1.table && b1.table.isValid
? `共 ${b1.table.rows.length} 行。${Number.isFinite(fit.slope) ? `拟合斜率 ${roundTo(fit.slope, 2)} mV/T。` : ""}`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"tu1",
tu1 && (tu1.imageCount > 0 || tu1.standardImageCount > 0)
? "VH-B 曲线图,图像型数据,不做数值建议。"
: "未识别到可分析图像内容。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"B2",
b2 && b2.table && b2.table.isValid
? `共 ${b2.table.rows.length} 行。${b2StdRows.length > 0 ? `标准答案 ${b2StdRows.length} 行。` : ""}`
: "未形成可分析表格。"
);
updateBreakdownAnalysis(
detailState.opBreakdown,
"tu2",
tu2 && (tu2.imageCount > 0 || tu2.standardImageCount > 0)
? "B-x 曲线图,图像型数据,不做数值建议。"
: "未识别到可分析图像内容。"
);
}
function computeLinearFit(points) {
const valid = points.filter(
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
);
if (valid.length < 2) {
return { slope: NaN, intercept: NaN };
}
const n = valid.length;
const sumX = valid.reduce((sum, item) => sum + item.x, 0);
const sumY = valid.reduce((sum, item) => sum + item.y, 0);
const sumXY = valid.reduce((sum, item) => sum + item.x * item.y, 0);
const sumXX = valid.reduce((sum, item) => sum + item.x * item.x, 0);
const denominator = n * sumXX - sumX * sumX;
if (denominator === 0) {
return { slope: NaN, intercept: NaN };
}
const slope = (n * sumXY - sumX * sumY) / denominator;
const intercept = (sumY - slope * sumX) / n;
return { slope, intercept };
}
function tableToObjects(table) {
if (!table || !table.isValid) {
return [];
}
return table.rows.map((row) => {
const obj = {};
table.headers.forEach((header, index) => {
obj[header] = row[index];
});
return obj;
});
}
function directText(root, selector) {
if (!root) {
return "";
}
const node = root.querySelector(selector);
return node ? cleanText(node.textContent) : "";
}
function mapOpLabel(name) {
const mapping = {
B1: "B1 表格",
B2: "B2 表格",
B3: "B3 表格",
BG: "BG 表格",
M1: "m1",
M2: "m2",
M11: "m1(弹性)",
M21: "m2(弹性)",
X: "挡光片宽度 X",
X1: "挡光片宽度 X1",
M12: "m1(碰后分离)",
M22: "m2(碰后分离)",
X2: "挡光片宽度 X2",
t0: "初始温度 t0",
T: "温度分度 T",
ST: "分划常数 ST",
H: "光杠杆臂 H",
H1: "光杠杆臂 H1",
L0: "金属棒原长 L0",
delL: "伸长量 ΔL",
tu1: "作图",
K: "斜率 K",
a: "线膨胀系数 a",
PJZ_D: "a 平均值",
BZC_S: "a 标准差 s",
DAIER_D: "Δa",
PJZ_R: "曲率半径 R",
DAIER_R_R: "R 相对不确定度",
DAIER_R: "ΔR",
R_Expression: "最终结果表达式",
XL_C: "灵敏度 C",
LMD_K: "仪器常数 K",
ZLXS: "表面张力系数 σ",
List_PZD: "偏振度测量表",
List_PZDPJ: "偏振度平均值表",
V: "偏振度 V",
Imax: "马吕斯拟合 Imax",
JD0: "偏振角 θ0",
List_MLSDL: "马吕斯定律数据表",
Imax_IUMap: "马吕斯拟合图",
XLK: "马吕斯线性系数 k",
SW_t0: "冷端温度 t0",
WCXS_k: "温差系数 k",
FM_m: "砝码质量 m",
ZL_F: "张力 F",
XXMD_average: "线密度平均值",
XXMD_Uncertainty: "线密度不确定度",
XXMD_Expression: "线密度结果表达式",
XD_WC: "相对误差",
KXZZL_m: "样品质量 m",
KXYZT_V: "空心圆柱体积 V",
YZTTJ_XD_UncertaintyA: "体积相对不确定度",
YZTTJ_UncertaintyA: "体积不确定度",
YZTTJ_Expression: "体积结果表达式",
MD_p: "密度 ρ",
LDDS: "密度相对误差",
ZJ_Average: "螺旋测微器平均值",
HC_Uncertainty: "螺旋测微器合成不确定度",
ZJ_Uncertainty_XD: "螺旋测微器相对不确定度",
ZJ_Expression: "螺旋测微器结果表达式",
ZJ_PJZ: "游标卡尺平均值",
ZJ_HC_BQDD: "游标卡尺合成不确定度",
ZJ_XD_BQDD: "游标卡尺相对不确定度",
JSZJ_xpression: "游标卡尺结果表达式",
BQDD_n: "标尺读数 n",
BQDDn: "标尺读数 n",
YSML_BQDD: "杨氏模量不确定度",
YSMLBQDD: "杨氏模量不确定度",
YSML_E: "杨氏模量 E",
YSMLE: "杨氏模量 E",
D_Expression: "钢丝直径 d 表达式",
L_Expression: "钢丝长度 L 表达式",
b_Expression: "光杠杆臂长 b 表达式",
E_Expression: "杨氏模量结果表达式",
ZCF_n: "逐差法 Δn 平均值",
ZTF_H: "转筒法 H",
ZTF_HZ: "转筒法 H 不确定度",
ZTF_BZ: "转筒法 B 值",
ZTF_List1: "转筒法原始计时表",
ZTF_List2: "转筒法平均值与不确定度表",
ZTF_List1_40: "0.0400kg 对应系数",
ZTF_List1_80: "0.0800kg 对应系数",
ZTF_NZ40: "转筒法 0.0400kg 黏度",
ZTF_NZ60: "转筒法 0.0600kg 黏度",
ZTF_NZ80: "转筒法 0.0800kg 黏度",
ZTF_List21: "转筒法黏度不确定度表",
ZTF_BDS40: "转筒法 0.0400kg 结果表达式",
ZTF_BDS60: "转筒法 0.0600kg 结果表达式",
ZTF_BDS80: "转筒法 0.0800kg 结果表达式",
LQF_ZJ: "落球法量筒直径",
LQF_GD: "落球法量筒高度",
LQF_L: "落球法下落距离",
LQF_LZ: "落球法 L 不确定度",
LQF_dZ: "落球法 d 不确定度",
LQF_BZ: "落球法 B 值",
LQF_List3: "落球法原始计时表",
LQF_List3_20: "落球法 2.000mm 中间量",
LQF_List4: "落球法平均值与不确定度表",
LQF_NZ20: "落球法 2.000mm 黏度",
LQF_NZ15: "落球法 1.500mm 黏度",
LQF_List5: "落球法黏度不确定度表",
LQF_BDS20: "落球法 2.000mm 结果表达式",
LQF_BDS15: "落球法 1.500mm 结果表达式",
DataList1: "尺寸测量原始表",
DataList2: "统计量与不确定度表",
JSB_V: "几何体体积 V",
V_RelativeUncertain: "体积相对不确定度",
V_Uncertain: "体积不确定度",
V_Expression: "体积结果表达式",
DataList3: "纸张尺寸表",
Paper_V: "纸张体积",
Paper_RelativeUncertain: "纸张相对不确定度",
Paper_Uncertain: "纸张不确定度",
Paper_Expression: "纸张结果表达式",
U0: "截止电压表",
k: "斜率 k",
h: "普朗克常数 h",
FATX_Drawing: "曲线图",
N: "螺线管匝数 N",
L: "螺线管长度 L",
D: "螺线管外径 D",
Kn: "霍尔元件灵敏度 Kₕ",
Bz: "B 中心理论值",
B0: "B₀ 端面理论值",
tu2: "B-x 曲线图",
};
return mapping[name] || name;
}
function isAuxiliaryOpItem(name) {
return (
name === "INDEX" ||
/^U0\d+$/.test(name) ||
/^B\d{2,}$/.test(name) ||
/^List_.*\d/.test(name) ||
/^(List_L|List_B|List_H|List_D|List_Average|List_S|List_Derta)$/.test(name) ||
/^\d/.test(name)
);
}
function hookNetwork() {
if (state.networkHooked) {
return;
}
state.networkHooked = true;
const originalFetch = pageWindow.fetch;
if (typeof originalFetch === "function") {
pageWindow.fetch = async function (...args) {
const response = await originalFetch.apply(this, args);
tryCaptureResponse(response);
return response;
};
}
const OriginalXHR = pageWindow.XMLHttpRequest;
if (typeof OriginalXHR === "function") {
const open = OriginalXHR.prototype.open;
const send = OriginalXHR.prototype.send;
OriginalXHR.prototype.open = function (method, url, ...rest) {
this.__xmlReviewUrl = url;
return open.call(this, method, url, ...rest);
};
OriginalXHR.prototype.send = function (...args) {
this.addEventListener("load", () => {
try {
const contentType = this.getResponseHeader("content-type") || "";
if (!/json|text/i.test(contentType)) {
return;
}
collectXmlPathsFromText(this.responseText || "");
} catch (error) {
console.debug("[xml-review] xhr capture skipped", error);
}
});
return send.apply(this, args);
};
}
}
function tryCaptureResponse(response) {
try {
const contentType = response.headers.get("content-type") || "";
if (!/json|text/i.test(contentType)) {
return;
}
response
.clone()
.text()
.then((text) => collectXmlPathsFromText(text))
.catch(() => {});
} catch (error) {
console.debug("[xml-review] fetch capture skipped", error);
}
}
function collectXmlPathsFromText(text) {
if (!text || text.length < 10) {
return;
}
const matches =
text.match(/\/Upload\/StudentReport\/[^"'\\\s]+?\.xml/gi) ||
text.match(/Upload\\\\StudentReport\\\\[^"'\\\s]+?\.xml/gi) ||
text.match(/Upload\\StudentReport\\[^"'\\\s]+?\.xml/gi) ||
[];
matches.forEach((path) => registerXmlUrl(path));
}
function registerXmlUrl(relativeOrAbsolute) {
const fullUrl = toAbsoluteUrl(relativeOrAbsolute);
if (!fullUrl) {
return;
}
state.xmlUrlMap.set(safeDecode(fullUrl), fullUrl);
}
function findXmlUrlForTitle(title, viewList) {
const directUrl = findXmlUrlFromViewList(title, viewList);
if (directUrl) {
return directUrl;
}
const normalizedTitle = reportTitleKey(title);
for (const [decodedUrl, fullUrl] of state.xmlUrlMap.entries()) {
if (reportTitleKey(decodedUrl).includes(normalizedTitle)) {
return fullUrl;
}
}
return null;
}
function findXmlUrlFromViewList(title, viewList) {
const list = viewList || getViewList();
if (!Array.isArray(list) || list.length === 0) {
return null;
}
const normalizedTitle = reportTitleKey(title);
const item = list.find((entry) => {
const paperName = reportTitleKey(entry.PaperName || entry.ReportName || "");
return paperName && normalizedTitle && titlesMatch(paperName, normalizedTitle);
});
return item && item.PaperContentXml ? toAbsoluteUrl(item.PaperContentXml) : null;
}
function getViewList() {
const list =
pageWindow.view && typeof pageWindow.view.CompleteReportList === "function"
? pageWindow.view.CompleteReportList()
: [];
if (Array.isArray(list)) {
list.forEach((entry) => {
if (entry && entry.PaperContentXml) {
registerXmlUrl(entry.PaperContentXml);
}
});
}
return list;
}
function getCurrentReportTitle() {
if (!state.currentModal) {
return "";
}
const titleNode =
state.currentModal.querySelector("h2") ||
state.currentModal.querySelector(".modal-title") ||
state.currentModal.querySelector("h4");
return normalizeReportTitle(titleNode ? titleNode.textContent : "");
}
function toAbsoluteUrl(value) {
if (!value) {
return "";
}
const normalized = String(value).replace(/\\/g, "/");
if (/^https?:\/\//i.test(normalized)) {
return normalized;
}
if (normalized.startsWith("/")) {
return `${location.origin}${normalized}`;
}
return `${location.origin}/${normalized.replace(/^\.?\//, "")}`;
}
function safeDecode(value) {
try {
return decodeURIComponent(value);
} catch (error) {
return value;
}
}
function normalizeReportTitle(value) {
return cleanText(value)
.replace(/^完成/, "")
.replace(/报告报告$/, "报告")
.replace(/\s+/g, "");
}
function reportTitleKey(value) {
return normalizeReportTitle(value).replace(/报告$/, "");
}
function titlesMatch(a, b) {
if (!a || !b) {
return false;
}
return a === b || a.includes(b) || b.includes(a);
}
function resolveXmlSource() {
const title = getCurrentReportTitle();
const list = getViewList();
const xmlUrl = title ? findXmlUrlForTitle(title, list) : "";
const directMatch = Array.isArray(list)
? list.find((entry) =>
titlesMatch(
reportTitleKey(entry && (entry.PaperName || entry.ReportName || "")),
reportTitleKey(title)
)
)
: null;
const info = {
stage: xmlUrl ? "已匹配 XML" : "定位 XML",
title,
listCount: Array.isArray(list) ? list.length : 0,
xmlUrl,
source: directMatch && directMatch.PaperContentXml ? "CompleteReportList.PaperContentXml" : xmlUrl ? "network-cache" : "",
pending: !xmlUrl && (!title || !Array.isArray(list) || list.length === 0),
attempt: state.autoLoadAttempts,
message: xmlUrl
? "已从页面报告列表中定位到 XML 地址。"
: !title
? "尚未识别出当前弹窗报告标题。"
: !Array.isArray(list) || list.length === 0
? "报告列表尚未准备好,继续等待页面数据。"
: "标题已识别,但暂未在报告列表里匹配到 XML 地址。",
};
return info;
}
function updatePanelDebug(panel, info) {
if (!panel) {
return;
}
const debugNode = panel.querySelector(".xml-review-debug");
if (!debugNode) {
return;
}
state.autoLoadDebug = info || null;
const lines = [];
if (info && info.stage) {
lines.push(`阶段:${escapeHtml(info.stage)}
`);
}
if (info && Number.isFinite(info.attempt)) {
lines.push(`尝试次数:${escapeHtml(info.attempt)}
`);
}
if (info && info.title) {
lines.push(`当前标题:${escapeHtml(info.title)}
`);
}
if (info && Number.isFinite(info.listCount)) {
lines.push(`报告列表条数:${escapeHtml(info.listCount)}
`);
}
if (info && info.source) {
lines.push(`来源:${escapeHtml(info.source)}
`);
}
if (info && info.xmlUrl) {
lines.push(`XML 地址:${escapeHtml(info.xmlUrl)}
`);
}
if (info && info.error) {
lines.push(`错误:${escapeHtml(info.error)}
`);
}
if (info && info.message) {
lines.push(`说明:${escapeHtml(info.message)}
`);
}
debugNode.innerHTML = lines.join("") || "等待调试信息...
";
}
// ═══════════════════════════════════════════════════════════════
// ███ 课程选修预习题自动填写模块 ███
// ═══════════════════════════════════════════════════════════════
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.modalContentId} * {
color: #223630 !important;
}
#${CONFIG.virtualLab.modalContentId} span[style*="color:red"] {
color: #c43b2f !important;
}
#${CONFIG.virtualLab.modalContentId} .panel {
border-radius: 12px !important;
overflow: hidden;
border: 1px solid #dce9e3 !important;
}
#${CONFIG.virtualLab.modalContentId} .panel-heading {
background: #eef6f2 !important;
}
#${CONFIG.virtualLab.modalContentId} img {
max-width: 100%;
height: auto;
}
`;
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 detailRecord = virtualLabState.detailCache.get(key);
const isLoading = virtualLabState.pendingDetail.has(key);
const scoreValue = formatVirtualLabScore(item.LabScore);
const recordName = item.LabDateUrl ? extractVirtualLabRecordName(item.LabDateUrl) : "未提供";
const hasRecord = !!cleanText(item.LabDateUrl);
const detailMeta = detailRecord && detailRecord.meta ? detailRecord.meta : null;
const statusTone = hasRecord
? isLoading
? "is-loading"
: detailRecord
? "is-ready"
: "is-loading"
: "is-error";
const statusText = hasRecord
? isLoading
? "加载中"
: detailRecord
? "已缓存"
: "可查看"
: "缺少记录";
const detailScoreText =
detailMeta && (detailMeta.actualScore || detailMeta.totalScore)
? `${detailMeta.actualScore || "-"}/${detailMeta.totalScore || "-"}`
: "点击按钮后从服务读取";
cell.setAttribute(CONFIG.virtualLab.keyAttr, key);
cell.innerHTML = `
${escapeHtml(scoreValue)}
`;
}
function formatVirtualLabScore(value) {
const score = toNumber(value);
return Number.isFinite(score) ? `${score} 分` : "未出分";
}
function extractVirtualLabRecordName(labDateUrl) {
const normalized = String(labDateUrl || "").replace(/\\/g, "/");
const parts = normalized.split("/").filter(Boolean);
return parts.length > 0 ? parts[parts.length - 1] : "未提供";
}
function clearVirtualLabDetailCache(item) {
const key = buildVirtualLabKey(item);
virtualLabState.detailCache.delete(key);
}
function refreshVirtualLabSummary(item, forceRefresh) {
const key = buildVirtualLabKey(item);
const cached = virtualLabState.summaryCache.get(key);
if (
!forceRefresh &&
cached &&
cached.status === "ready" &&
Date.now() - cached.fetchedAt < CONFIG.virtualLab.summaryTtlMs
) {
scheduleVirtualLabSync();
return Promise.resolve(cached);
}
if (virtualLabState.pendingSummary.has(key)) {
return virtualLabState.pendingSummary.get(key);
}
const loadingRecord = {
status: "loading",
serverItem: null,
fetchedAt: Date.now(),
message: "正在从评分服务同步实验结果...",
};
virtualLabState.summaryCache.set(key, loadingRecord);
scheduleVirtualLabSync();
const promise = requestVirtualLabServer("/api/query", {
stu_id: item.USERID,
stu_name: item.USERNAME,
lab_name: item.LABNAME,
})
.then((result) => {
const list = extractVirtualLabQueryList(result);
const matched = findBestVirtualLabMatch(list, item);
if (!matched) {
throw new Error("评分服务已返回结果,但没有匹配到当前实验记录。");
}
const readyRecord = {
status: "ready",
serverItem: matched,
fetchedAt: Date.now(),
message: "已从评分服务同步当前实验信息。",
};
virtualLabState.summaryCache.set(key, readyRecord);
return readyRecord;
})
.catch((error) => {
const failedRecord = {
status: "error",
serverItem: null,
fetchedAt: Date.now(),
message:
error && error.message
? error.message
: "评分服务连接失败,请确认 localhost 服务已启动。",
};
virtualLabState.summaryCache.set(key, failedRecord);
return failedRecord;
})
.finally(() => {
virtualLabState.pendingSummary.delete(key);
scheduleVirtualLabSync();
});
virtualLabState.pendingSummary.set(key, promise);
return promise;
}
function openVirtualLabDetail(item, forceRefresh) {
const modal = ensureVirtualLabDetailModal();
const content = modal.querySelector(`#${CONFIG.virtualLab.modalContentId}`);
if (!content) {
return;
}
modal.classList.add("is-open");
content.innerHTML =
'正在从评分服务获取完整评分,请稍候...
';
const key = buildVirtualLabKey(item);
loadVirtualLabDetail(item, forceRefresh)
.then((detailRecord) => {
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) => {
content.innerHTML = `${escapeHtml(
error && error.message
? error.message
: "获取完整评分失败,请确认服务可访问。"
)}
`;
});
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 = requestVirtualLabServer("/api/detail", {
stu_id: context.stu_id,
stu_name: context.stu_name,
lab_id: context.lab_id,
lab_name: context.lab_name,
lab_date_url: context.lab_date_url,
})
.then((result) => {
const detailHtml =
result &&
result.success &&
result.data &&
result.data.IsSuccess &&
typeof result.data.Data === "string"
? result.data.Data
: "";
if (!detailHtml) {
throw new Error(
(result && result.message) || "评分服务没有返回完整评分 HTML。"
);
}
const normalized = normalizeVirtualLabDetailHtml(detailHtml);
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() {
const modal = document.getElementById(CONFIG.virtualLab.modalId);
if (modal) {
modal.classList.remove("is-open");
}
}
function normalizeVirtualLabDetailHtml(rawHtml) {
const html = String(rawHtml || "");
const doc = new DOMParser().parseFromString(html, "text/html");
doc.querySelectorAll("script").forEach((node) => node.remove());
doc.querySelectorAll(".collapse").forEach((node) => {
node.classList.remove("collapse");
node.classList.remove("in");
});
doc.querySelectorAll("[data-toggle]").forEach((node) =>
node.removeAttribute("data-toggle")
);
doc.querySelectorAll("[data-target]").forEach((node) =>
node.removeAttribute("data-target")
);
doc.querySelectorAll(".btnUp, .btnDown").forEach((node) => node.remove());
doc.querySelectorAll("image").forEach((node) => {
const img = doc.createElement("img");
Array.from(node.attributes).forEach((attribute) =>
img.setAttribute(attribute.name, attribute.value)
);
node.parentNode.replaceChild(img, node);
});
const headingText = cleanText(
(doc.querySelector("h2") && doc.querySelector("h2").textContent) || ""
);
const scoreText = cleanText(
Array.from(doc.querySelectorAll(".panel-heading"))
.map((node) => node.textContent || "")
.find((text) => /总分/.test(text) && /本题得分/.test(text)) || ""
);
const scoreMatch = scoreText.match(/总分[::]?\s*([0-9.]+).*本题得分[::]?\s*([0-9.]+)/);
return {
html: doc.body ? doc.body.innerHTML : html,
meta: {
title: headingText,
totalScore: scoreMatch ? scoreMatch[1] : "",
actualScore: scoreMatch ? scoreMatch[2] : "",
},
};
}
function requestVirtualLabServer(path, data) {
const url = `${getVirtualLabServerBase().replace(/\/$/, "")}${path}`;
const body = new URLSearchParams();
Object.entries(data || {}).forEach(([key, value]) =>
body.append(key, value == null ? "" : String(value))
);
const gmRequest =
typeof GM_xmlhttpRequest === "function"
? GM_xmlhttpRequest
: typeof GM !== "undefined" &&
GM &&
typeof GM.xmlHttpRequest === "function"
? GM.xmlHttpRequest.bind(GM)
: null;
if (gmRequest) {
return new Promise((resolve, reject) => {
gmRequest({
method: "POST",
url,
data: body.toString(),
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
},
timeout: 30000,
onload: (response) => {
if (response.status < 200 || response.status >= 300) {
reject(
new Error(`评分服务返回 ${response.status},请确认服务已正常启动。`)
);
return;
}
try {
resolve(JSON.parse(response.responseText));
} catch (error) {
reject(new Error("评分服务返回的不是有效 JSON。"));
}
},
onerror: () => {
reject(new Error(`无法连接评分服务:${url}`));
},
ontimeout: () => {
reject(new Error("请求评分服务超时,请稍后重试。"));
},
});
});
}
return fetch(url, {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: body.toString(),
}).then(async (response) => {
if (!response.ok) {
throw new Error(`评分服务返回 ${response.status}。`);
}
try {
return await response.json();
} catch (error) {
throw new Error("评分服务返回的不是有效 JSON。");
}
}).catch((error) => {
if (error && error.message) {
throw error;
}
throw new Error(`无法连接评分服务:${url}`);
});
}
function getVirtualLabServerBase() {
return (
pageWindow.__virtualLabServerBase ||
window.localStorage.getItem(CONFIG.virtualLab.serverBaseKey) ||
CONFIG.virtualLab.serverBase
);
}
function extractVirtualLabQueryList(result) {
if (!result || result.success !== true || !result.data) {
throw new Error((result && result.message) || "评分服务未返回实验列表数据。");
}
if (!Array.isArray(result.data.DataList)) {
throw new Error("评分服务返回的数据结构不符合预期。");
}
return result.data.DataList;
}
function findBestVirtualLabMatch(list, item) {
if (!Array.isArray(list) || list.length === 0) {
return null;
}
return (
list.find(
(entry) =>
String(entry.LABID || "") === String(item.LABID || "") &&
normalizeLabName(entry.LABNAME) === normalizeLabName(item.LABNAME)
) ||
list.find(
(entry) =>
normalizeLabName(entry.LABNAME) === normalizeLabName(item.LABNAME)
) ||
list[0]
);
}
function normalizeLabName(value) {
return cleanText(value).replace(/\s+/g, "");
}
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;
}
function splitCsvLike(text) {
return text.split(",").map((part) => cleanText(part));
}
function truncateText(value, maxLength) {
const text = cleanText(value);
if (text.length <= maxLength) {
return text;
}
return `${text.slice(0, Math.max(0, maxLength - 1))}…`;
}
function sanitizeFileName(value) {
return String(value || "report.xml")
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_")
.trim();
}
// 按题目文本匹配答案(题目每次随机排列,不能按顺序匹配)
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 normalizeOpKey(value) {
return cleanText(value).replace(/[\s_]+/g, "").toLowerCase();
}
function resolveOpItem(parsedReport, ...names) {
const opItemsByName = (parsedReport && parsedReport.opItemsByName) || {};
for (const name of names) {
if (name && opItemsByName[name]) {
return opItemsByName[name];
}
}
const normalizedNames = names.map(normalizeOpKey).filter(Boolean);
if (normalizedNames.length === 0) {
return null;
}
return (
Object.values(opItemsByName).find((item) =>
normalizedNames.includes(normalizeOpKey(item && item.name))
) || null
);
}
function looksLikeBase64(value) {
if (!value || value.length < 16 || value.length % 4 !== 0) {
return false;
}
return /^[A-Za-z0-9+/=\s]+$/.test(value);
}
function looksLikeHtml(value) {
return /<[^>]+>/.test(value);
}
function looksLikeImageData(value) {
if (!value || value.length < 20) {
return false;
}
return /data:image\/|^\xff\xd8\xff|^\x89PNG|
]+src\s*=\s*["']data:image\//i.test(value);
}
function looksLikeGarbledText(text) {
if (!text || text.length < 40) return false;
const chineseCount = (text.match(/[\u4e00-\u9fff]/g) || []).length;
const numericCount = (text.match(/[\d.,+\-×±=()()%°Ω℃\/^ ]/g) || []).length;
return (chineseCount + numericCount) / text.length < 0.3;
}
function sanitizeForDisplay(text) {
if (!text || text.length < 40) return text;
if (looksLikeBase64(text)) {
try {
const decoded = decodeBase64Unicode(text);
if (looksLikeImageData(decoded)) return "[图片数据]";
if (looksLikeHtml(decoded)) {
const doc = new DOMParser().parseFromString(decoded, "text/html");
const extracted = cleanText(doc.body ? doc.body.textContent : "");
if (extracted.length > 0) return extracted;
return "[编码数据]";
}
} catch (e) {}
}
if (looksLikeGarbledText(text)) return "[编码数据]";
return text;
}
function decodeBase64Unicode(value) {
const binary = atob(value.replace(/\s+/g, ""));
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}
function countImgInText(text) {
const matches = String(text || "").match(/
parseFlexibleNumber(token))
.filter((num) => Number.isFinite(num));
}
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 formatNumberList(values) {
return values
.filter((value) => Number.isFinite(value))
.map((value) => `${value}`)
.join(";");
}
function firstFiniteValue(values) {
return Array.isArray(values) ? values.find((value) => Number.isFinite(value)) : NaN;
}
function hasContentValue(content) {
return !!(
content &&
(getContentText(content) ||
cleanText(content.html || "") ||
Number(content.imageCount || 0) > 0)
);
}
function roundTo(value, digits) {
if (!Number.isFinite(value)) {
return "NaN";
}
return Number(value.toFixed(digits));
}
function relativeDelta(a, b) {
if (!Number.isFinite(a) || !Number.isFinite(b)) {
return Infinity;
}
const base = Math.max(Math.abs(a), Math.abs(b), 1e-9);
return Math.abs(a - b) / base;
}
function formatScore(real, total) {
if (Number.isFinite(real) && Number.isFinite(total)) {
return `${real} / ${total}`;
}
if (Number.isFinite(real)) {
return `${real}`;
}
return "未识别";
}
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;
}
if (location.host !== "211.82.81.133:7103") {
return;
}
if (location.pathname.indexOf("ScoreInquiry") !== -1) {
return;
}
injectStyles();
ensureLoadBadge();
bindGlobalListeners();
hookNetwork();
pageWindow.__xmlReviewLoaded = true;
pageWindow.__cauLabEnhancerLoaded = true;
window.__xmlReviewLoaded = true;
window.__cauLabEnhancerLoaded = true;
console.log("[xml-review] script loaded");
if (!state.observer && document.body) {
state.observer = new MutationObserver(debounceScan);
state.observer.observe(document.body, {
childList: true,
subtree: true,
});
}
debounceScan();
}
const debugApi = {
injectStyles,
parseXmlReport,
reviewReport,
renderReview,
renderFailure,
switchReviewTab,
};
pageWindow.__xmlReviewDebug = debugApi;
window.__xmlReviewDebug = debugApi;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init, { once: true });
} else {
init();
}
window.addEventListener("load", () => {
try {
init();
} catch (error) {
console.error("[xml-review] load init failed", error);
}
});
})();