// ==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
脚本会优先从页面报告数据里自动定位 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
脚本会优先从页面报告数据里自动定位 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 ? `` : `
未发现需要额外核对的数值或结构问题。
`; 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} 个。

需关注项

${issueItems}

栏目得分

${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 `
${headerHtml}${rowHtml}
`; } 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 ? `` : `
未生成列格式分析。
` } ${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); } }); })();