新增SPC前端验证页面

This commit is contained in:
zhangshun
2026-06-16 09:53:10 +08:00
parent bcd94b3307
commit 6f45353b21

998
public/spc_test.html Normal file
View File

@@ -0,0 +1,998 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SPC WASM 验证台</title>
<style>
:root {
--bg: #f6f7f4;
--surface: #ffffff;
--surface-soft: #eef3f1;
--line: #cfd8d3;
--text: #1b2522;
--muted: #65736e;
--primary: #126b5c;
--primary-strong: #0b4f44;
--accent: #b45f06;
--danger: #b42318;
--warning: #8a5a00;
--ok: #157347;
--code-bg: #111917;
--code-text: #edf7f3;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family:
"Segoe UI",
"Microsoft YaHei",
Arial,
sans-serif;
}
button,
input,
textarea {
font: inherit;
}
button {
min-height: 44px;
border: 1px solid transparent;
border-radius: 8px;
padding: 10px 14px;
font-weight: 650;
cursor: pointer;
transition:
background-color 160ms ease,
border-color 160ms ease,
color 160ms ease;
}
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 3px solid rgba(18, 107, 92, 0.28);
outline-offset: 2px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.primary {
background: var(--primary);
color: #ffffff;
}
.primary:hover:not(:disabled) {
background: var(--primary-strong);
}
.secondary {
background: #ffffff;
color: var(--text);
border-color: var(--line);
}
.secondary:hover:not(:disabled) {
border-color: var(--primary);
color: var(--primary-strong);
}
.shell {
width: min(1440px, 100%);
margin: 0 auto;
padding: 24px;
}
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: 28px;
line-height: 1.2;
}
.subtitle {
margin-top: 6px;
color: var(--muted);
font-size: 14px;
}
.status {
min-width: 156px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px 10px;
background: var(--surface);
color: var(--muted);
font-size: 13px;
text-align: center;
}
.layout {
display: grid;
grid-template-columns: minmax(360px, 0.9fr) minmax(520px, 1.1fr);
gap: 16px;
align-items: start;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
background: var(--surface-soft);
}
.panel-header h2 {
font-size: 17px;
}
.panel-body {
padding: 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.field {
display: grid;
gap: 6px;
}
label {
color: var(--muted);
font-size: 13px;
font-weight: 600;
}
input,
textarea {
width: 100%;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--text);
background: #ffffff;
}
input {
min-height: 44px;
padding: 10px 11px;
}
textarea {
min-height: 300px;
resize: vertical;
padding: 12px;
font:
13px/1.5 Consolas,
"Courier New",
monospace;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.message {
margin-top: 12px;
min-height: 20px;
color: var(--muted);
font-size: 13px;
}
.message.is-error {
color: var(--danger);
}
.message.is-ok {
color: var(--ok);
}
.summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-bottom: 16px;
}
.metric {
border: 1px solid var(--line);
border-radius: 8px;
padding: 11px 12px;
background: #ffffff;
}
.metric-label {
color: var(--muted);
font-size: 12px;
font-weight: 650;
}
.metric-value {
margin-top: 6px;
font-size: 20px;
font-variant-numeric: tabular-nums;
font-weight: 750;
}
.metric-value.ok {
color: var(--ok);
}
.metric-value.warn {
color: var(--warning);
}
.table-wrap {
overflow: auto;
border: 1px solid var(--line);
border-radius: 8px;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
background: #ffffff;
}
th,
td {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
text-align: left;
font-size: 13px;
vertical-align: middle;
}
th {
background: #f2f5f3;
color: var(--muted);
font-weight: 750;
}
td {
font-variant-numeric: tabular-nums;
}
tr:last-child td {
border-bottom: 0;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 26px;
border-radius: 999px;
padding: 3px 9px;
border: 1px solid var(--line);
background: #ffffff;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.badge.ok {
border-color: rgba(21, 115, 71, 0.28);
background: rgba(21, 115, 71, 0.08);
color: var(--ok);
}
.badge.warn {
border-color: rgba(180, 35, 24, 0.28);
background: rgba(180, 35, 24, 0.08);
color: var(--danger);
}
.tabs {
display: flex;
gap: 8px;
margin: 16px 0 10px;
}
.tab {
min-height: 36px;
padding: 7px 12px;
background: #ffffff;
border-color: var(--line);
color: var(--muted);
}
.tab.active {
background: var(--primary);
border-color: var(--primary);
color: #ffffff;
}
pre {
min-height: 260px;
max-height: 480px;
margin: 0;
overflow: auto;
border-radius: 8px;
padding: 14px;
background: var(--code-bg);
color: var(--code-text);
font:
13px/1.5 Consolas,
"Courier New",
monospace;
}
.hidden {
display: none;
}
@media (max-width: 980px) {
.shell {
padding: 16px;
}
.topbar {
align-items: stretch;
flex-direction: column;
}
.status {
width: 100%;
}
.layout {
grid-template-columns: 1fr;
}
.grid,
.summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 560px) {
.grid,
.summary {
grid-template-columns: 1fr;
}
h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<main class="shell">
<header class="topbar">
<div>
<h1>SPC WASM 验证台</h1>
<p class="subtitle">Cmd_Spc · XR / XS / Cpk</p>
</div>
<div class="status" id="wasmStatus">WASM 未加载</div>
</header>
<section class="layout">
<div class="panel">
<div class="panel-header">
<h2>输入</h2>
<span class="badge" id="inputCount">0 个数据</span>
</div>
<div class="panel-body">
<div class="grid">
<div class="field">
<label for="nInput">n</label>
<input id="nInput" type="number" min="2" max="25" step="1" />
</div>
<div class="field">
<label for="kInput">k</label>
<input id="kInput" type="number" min="1" step="1" />
</div>
<div class="field">
<label for="uslInput">USL</label>
<input id="uslInput" type="number" step="0.001" />
</div>
<div class="field">
<label for="lslInput">LSL</label>
<input id="lslInput" type="number" step="0.001" />
</div>
</div>
<div class="field" style="margin-top: 14px">
<label for="dataInput">x</label>
<textarea id="dataInput" spellcheck="false"></textarea>
</div>
<div class="actions">
<button class="secondary" id="sampleButton" type="button">
填入样例
</button>
<button class="secondary" id="formatButton" type="button">
格式化数据
</button>
<button class="primary" id="runButton" type="button">
调用并验证
</button>
</div>
<div class="message" id="inputMessage" aria-live="polite"></div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<h2>结果</h2>
<span class="badge" id="verifyStatus">未执行</span>
</div>
<div class="panel-body">
<div class="summary">
<div class="metric">
<div class="metric-label">匹配项</div>
<div class="metric-value" id="matchedCount">0</div>
</div>
<div class="metric">
<div class="metric-label">最大差值</div>
<div class="metric-value" id="maxDiff">-</div>
</div>
<div class="metric">
<div class="metric-label">WASM Cpk</div>
<div class="metric-value" id="wasmCpk">-</div>
</div>
<div class="metric">
<div class="metric-label">前端 Cpk</div>
<div class="metric-value" id="localCpk">-</div>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>字段</th>
<th>WASM</th>
<th>前端复算</th>
<th>差值</th>
<th>状态</th>
</tr>
</thead>
<tbody id="compareBody">
<tr>
<td colspan="5">等待执行</td>
</tr>
</tbody>
</table>
</div>
<div class="tabs" role="tablist" aria-label="结果视图">
<button class="tab active" id="responseTab" type="button">
WASM 响应
</button>
<button class="tab" id="localTab" type="button">前端复算</button>
</div>
<pre id="responseOutput">{}</pre>
<pre class="hidden" id="localOutput">{}</pre>
</div>
</div>
</section>
</main>
<script src="./wasm/smart_math.js"></script>
<script>
const sampleData = [
1.55, 1.58, 1.61, 1.6, 1.6, 1.58, 1.63, 1.63, 1.62, 1.63,
1.62, 1.63, 1.62, 1.59, 1.58, 1.58, 1.6, 1.61, 1.62, 1.63,
1.58, 1.64, 1.63, 1.62, 1.62, 1.62, 1.62, 1.63, 1.61, 1.57,
1.64, 1.62, 1.61, 1.6, 1.58, 1.57, 1.59, 1.61, 1.62, 1.63,
1.58, 1.61, 1.6, 1.62, 1.63, 1.6, 1.61, 1.64, 1.64, 1.63,
1.58, 1.6, 1.62, 1.63, 1.65, 1.62, 1.58, 1.59, 1.57, 1.58,
1.57, 1.57, 1.58, 1.59, 1.64, 1.61, 1.64, 1.62, 1.6, 1.59,
1.65, 1.62, 1.62, 1.6, 1.58, 1.57, 1.59, 1.57, 1.59, 1.62,
1.56, 1.57, 1.57, 1.61, 1.62, 1.56, 1.58, 1.59, 1.6, 1.62,
1.58, 1.6, 1.6, 1.62, 1.63, 1.58, 1.59, 1.6, 1.63, 1.62,
1.58, 1.59, 1.62, 1.63, 1.64, 1.58, 1.59, 1.62, 1.63, 1.61,
1.58, 1.59, 1.6, 1.61, 1.63, 1.57, 1.59, 1.61, 1.61, 1.62,
1.58, 1.58, 1.6, 1.61, 1.63, 1.62, 1.58, 1.58, 1.58, 1.57,
1.63, 1.59, 1.57, 1.58, 1.57, 1.58, 1.62, 1.61, 1.63, 1.61,
1.58, 1.57, 1.59, 1.6, 1.62, 1.62, 1.6, 1.6, 1.57, 1.57,
];
const coefficients = {
2: { A2: 1.88, A3: 2.659, B3: 0, B4: 3.267, D3: 0, D4: 3.267, L_D1: 1.128 },
3: { A2: 1.023, A3: 1.954, B3: 0, B4: 2.568, D3: 0, D4: 2.574, L_D1: 1.693 },
4: { A2: 0.729, A3: 1.628, B3: 0, B4: 2.266, D3: 0, D4: 2.282, L_D1: 2.059 },
5: { A2: 0.577, A3: 1.427, B3: 0, B4: 2.089, D3: 0, D4: 2.114, L_D1: 2.326 },
6: { A2: 0.483, A3: 1.287, B3: 0.03, B4: 1.97, D3: 0, D4: 2.004, L_D1: 2.534 },
7: { A2: 0.419, A3: 1.182, B3: 0.118, B4: 1.882, D3: 0.076, D4: 1.924, L_D1: 2.704 },
8: { A2: 0.373, A3: 1.099, B3: 0.185, B4: 1.815, D3: 0.136, D4: 1.864, L_D1: 2.847 },
9: { A2: 0.337, A3: 1.032, B3: 0.239, B4: 1.761, D3: 0.184, D4: 1.816, L_D1: 2.97 },
10: { A2: 0.308, A3: 0.975, B3: 0.284, B4: 1.716, D3: 0.223, D4: 1.777, L_D1: 3.078 },
11: { A2: 0.285, A3: 0.927, B3: 0.321, B4: 1.679, D3: 0.256, D4: 1.744, L_D1: 3.173 },
12: { A2: 0.266, A3: 0.886, B3: 0.354, B4: 1.646, D3: 0.283, D4: 1.717, L_D1: 3.258 },
13: { A2: 0.249, A3: 0.85, B3: 0.382, B4: 1.618, D3: 0.307, D4: 1.693, L_D1: 3.336 },
14: { A2: 0.235, A3: 0.817, B3: 0.406, B4: 1.594, D3: 0.328, D4: 1.672, L_D1: 3.407 },
15: { A2: 0.223, A3: 0.789, B3: 0.428, B4: 1.572, D3: 0.347, D4: 1.653, L_D1: 3.472 },
16: { A2: 0.212, A3: 0.763, B3: 0.448, B4: 1.552, D3: 0.363, D4: 1.637, L_D1: 3.532 },
17: { A2: 0.203, A3: 0.739, B3: 0.466, B4: 1.534, D3: 0.378, D4: 1.622, L_D1: 3.588 },
18: { A2: 0.194, A3: 0.718, B3: 0.482, B4: 1.518, D3: 0.391, D4: 1.608, L_D1: 3.64 },
19: { A2: 0.187, A3: 0.698, B3: 0.497, B4: 1.503, D3: 0.403, D4: 1.597, L_D1: 3.689 },
20: { A2: 0.18, A3: 0.68, B3: 0.51, B4: 1.49, D3: 0.415, D4: 1.585, L_D1: 3.735 },
21: { A2: 0.173, A3: 0.663, B3: 0.523, B4: 1.477, D3: 0.425, D4: 1.575, L_D1: 3.778 },
22: { A2: 0.167, A3: 0.647, B3: 0.534, B4: 1.466, D3: 0.434, D4: 1.566, L_D1: 3.819 },
23: { A2: 0.162, A3: 0.633, B3: 0.545, B4: 1.455, D3: 0.443, D4: 1.557, L_D1: 3.858 },
24: { A2: 0.157, A3: 0.619, B3: 0.555, B4: 1.445, D3: 0.451, D4: 1.548, L_D1: 3.895 },
25: { A2: 0.153, A3: 0.606, B3: 0.565, B4: 1.435, D3: 0.459, D4: 1.541, L_D1: 3.931 },
};
const compareFields = [
"XR.CL_X",
"XR.UCL_X",
"XR.LCL_X",
"XR.CL_R",
"XR.UCL_R",
"XR.LCL_R",
"XS.CL_X",
"XS.UCL_X",
"XS.LCL_X",
"XS.CL_S",
"XS.UCL_S",
"XS.LCL_S",
"Cpk.Singma",
"Cpk.SingmaS",
"Cpk.Cp",
"Cpk.CPU",
"Cpk.CPL",
"Cpk.Cpk",
"Cpk.Pp",
"Cpk.PPU",
"Cpk.PPL",
"Cpk.Ppk",
"Cpk.Ca",
"Cpk.CR",
"Cpk.PR",
];
const nInput = document.getElementById("nInput");
const kInput = document.getElementById("kInput");
const uslInput = document.getElementById("uslInput");
const lslInput = document.getElementById("lslInput");
const dataInput = document.getElementById("dataInput");
const inputCount = document.getElementById("inputCount");
const inputMessage = document.getElementById("inputMessage");
const wasmStatus = document.getElementById("wasmStatus");
const verifyStatus = document.getElementById("verifyStatus");
const compareBody = document.getElementById("compareBody");
const responseOutput = document.getElementById("responseOutput");
const localOutput = document.getElementById("localOutput");
const matchedCount = document.getElementById("matchedCount");
const maxDiff = document.getElementById("maxDiff");
const wasmCpk = document.getElementById("wasmCpk");
const localCpk = document.getElementById("localCpk");
const responseTab = document.getElementById("responseTab");
const localTab = document.getElementById("localTab");
let wasmModule = null;
// 设置提示信息,统一处理错误和成功状态。
function setMessage(text, type = "") {
inputMessage.textContent = text;
inputMessage.className = `message ${type}`.trim();
}
// 按项目返回口径保留三位小数。
function round3(value) {
return Math.round(value * 1000) / 1000;
}
// 计算数组平均值。
function mean(values) {
if (!values.length) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
// 计算数组极差。
function range(values) {
if (values.length < 2) return 0;
return Math.max(...values) - Math.min(...values);
}
// 计算样本标准差,分母为 n-1。
function sampleStd(values) {
if (values.length < 2) return 0;
const avg = mean(values);
const variance =
values.reduce((sum, value) => sum + (value - avg) ** 2, 0) /
(values.length - 1);
return Math.sqrt(variance);
}
// 计算总体标准差,分母为 n。
function populationStd(values) {
if (values.length < 2) return 0;
const avg = mean(values);
const variance =
values.reduce((sum, value) => sum + (value - avg) ** 2, 0) /
values.length;
return Math.sqrt(variance);
}
// 从文本框解析测量数据。
function parseDataInput() {
const values = dataInput.value
.split(/[\s,;]+/)
.map((item) => item.trim())
.filter(Boolean)
.map(Number);
if (values.some((value) => Number.isNaN(value))) {
throw new Error("x 中包含无法解析的数值");
}
return values;
}
// 将数据格式化为便于核对的分行文本。
function formatValues(values, groupSize = 5) {
const rows = [];
for (let i = 0; i < values.length; i += groupSize) {
rows.push(values.slice(i, i + groupSize).join(", "));
}
return rows.join("\n");
}
// 读取页面上的 SPC 输入参数。
function readInput() {
const n = Number(nInput.value);
const k = Number(kInput.value);
const usl = Number(uslInput.value);
const lsl = Number(lslInput.value);
const x = parseDataInput();
if (!Number.isInteger(n) || n < 2 || n > 25) {
throw new Error("n 必须是 2 到 25 之间的整数");
}
if (!Number.isInteger(k) || k < 1) {
throw new Error("k 必须是正整数");
}
if (!Number.isFinite(usl) || !Number.isFinite(lsl)) {
throw new Error("USL 和 LSL 必须是有效数字");
}
if (!x.length) {
throw new Error("x 不能为空");
}
return { n, k, usl, lsl, x };
}
// 按 SPC 接口格式组装请求体。
function buildRequest(input) {
return {
msg: "spc verify",
req_code: `SPC_VERIFY_${Date.now()}`,
req_from: "spc_test_page",
req_cmd: "Cmd_Spc",
req_param: input,
};
}
// 将输入切成当前代码口径下使用的子组。
function buildGroups(values, groupSize) {
const count = Math.floor(values.length / groupSize);
return Array.from({ length: count }, (_, index) =>
values.slice(index * groupSize, (index + 1) * groupSize),
);
}
// 使用前端同口径公式复算 SPC 结果。
function calculateLocalSpc(input) {
const coeff = coefficients[input.n];
if (!coeff) {
throw new Error("当前页面未配置该 n 对应的 SPC 系数");
}
const groups = buildGroups(input.x, input.n);
if (!groups.length) {
throw new Error("有效子组数为 0");
}
let usl = input.usl;
let lsl = input.lsl;
const overallMean = mean(input.x);
if (Math.abs(usl - lsl) < 0.000001) {
const sigma = populationStd(input.x);
usl = overallMean + 3 * sigma;
lsl = overallMean - 3 * sigma;
}
const xBars = groups.map(mean);
const ranges = groups.map(range);
const stds = groups.map(sampleStd);
const clX = mean(xBars);
const rBar = mean(ranges);
const sBar = mean(stds);
const sigma = rBar / coeff.L_D1;
const tolerance = usl - lsl;
const center = (usl + lsl) / 2;
const result = {
XR: {
n: input.n,
k: groups.length,
CL_X: clX,
UCL_X: clX + coeff.A2 * rBar,
LCL_X: clX - coeff.A2 * rBar,
CL_R: rBar,
UCL_R: coeff.D4 * rBar,
LCL_R: coeff.D3 * rBar,
CL_Xk: xBars,
CL_Rk: ranges,
},
XS: {
n: input.n,
k: groups.length,
CL_X: clX,
UCL_X: clX + coeff.A3 * rBar,
LCL_X: clX - coeff.A3 * rBar,
CL_S: sBar,
UCL_S: coeff.B4 * sBar,
LCL_S: coeff.B3 * sBar,
CL_Xk: xBars,
CL_Sk: stds,
},
Cpk: {
n: input.n,
k: groups.length,
SL: tolerance > 0 ? center : 0,
USL: usl,
LSL: lsl,
Singma: sigma,
SingmaS: sBar,
Ca: tolerance > 0 ? Math.abs(overallMean - center) / (tolerance / 2) : 0,
Cp: tolerance > 0 ? tolerance / (6 * sigma) : 0,
CPU: tolerance > 0 ? (usl - overallMean) / (3 * sigma) : 0,
CPL: tolerance > 0 ? (overallMean - lsl) / (3 * sigma) : 0,
Cpk: 0,
Pp: tolerance > 0 ? tolerance / (6 * sBar) : 0,
PPU: tolerance > 0 ? (usl - overallMean) / (3 * sBar) : 0,
PPL: tolerance > 0 ? (overallMean - lsl) / (3 * sBar) : 0,
Ppk: 0,
CR: tolerance > 0 ? (6 * sigma) / tolerance : 0,
PR: tolerance > 0 ? (6 * sBar) / tolerance : 0,
},
};
result.Cpk.Cpk = Math.min(result.Cpk.CPU, result.Cpk.CPL);
result.Cpk.Ppk = Math.min(result.Cpk.PPU, result.Cpk.PPL);
return roundSpcResult(result);
}
// 将前端复算结果按 WASM 返回格式整体保留三位小数。
function roundSpcResult(result) {
const clone = JSON.parse(JSON.stringify(result));
const walk = (value) => {
if (Array.isArray(value)) {
return value.map(walk);
}
if (value && typeof value === "object") {
Object.keys(value).forEach((key) => {
value[key] = walk(value[key]);
});
return value;
}
if (typeof value === "number") {
return round3(value);
}
return value;
};
return walk(clone);
}
// 根据点路径读取嵌套对象值。
function getByPath(source, path) {
return path.split(".").reduce((current, key) => {
if (current === undefined || current === null) return undefined;
return current[key];
}, source);
}
// 更新输入数据数量显示。
function updateInputCount() {
try {
const values = parseDataInput();
const n = Number(nInput.value) || 0;
const used = n > 0 ? Math.floor(values.length / n) * n : 0;
inputCount.textContent = `${values.length} 个数据 / 使用 ${used}`;
} catch {
inputCount.textContent = "数据异常";
}
}
// 初始化表单为内置样例。
function loadSample() {
nInput.value = "5";
kInput.value = "30";
uslInput.value = "1.7";
lslInput.value = "1.5";
dataInput.value = formatValues(sampleData, 5);
updateInputCount();
setMessage("样例已填入", "is-ok");
}
// 加载并初始化 WASM 模块。
async function ensureWasm() {
if (wasmModule) return wasmModule;
wasmStatus.textContent = "WASM 加载中";
wasmModule = await createSmartMathModule();
const ptr = wasmModule._init_func();
try {
wasmModule.UTF8ToString(ptr);
} finally {
wasmModule._smart_free_string(ptr);
}
wasmStatus.textContent = "WASM 已初始化";
return wasmModule;
}
// 在 WASM 内存中写入字符串。
function allocString(module, value) {
const size = module.lengthBytesUTF8(value) + 1;
const ptr = module._malloc(size);
module.stringToUTF8(value, ptr, size);
return ptr;
}
// 调用业务接口并解析 JSON 响应。
async function callSpc(input) {
const module = await ensureWasm();
const payload = JSON.stringify(buildRequest(input));
let requestPtr = 0;
let responsePtr = 0;
try {
requestPtr = allocString(module, payload);
responsePtr = module._func(requestPtr);
return JSON.parse(module.UTF8ToString(responsePtr));
} finally {
if (responsePtr) module._smart_free_string(responsePtr);
if (requestPtr) module._free(requestPtr);
}
}
// 渲染 WASM 响应和前端复算 JSON。
function renderJsonOutputs(response, localResult) {
responseOutput.textContent = JSON.stringify(response, null, 2);
localOutput.textContent = JSON.stringify(localResult, null, 2);
}
// 渲染核心字段对比表。
function renderComparison(wasmResult, localResult) {
const tolerance = 0.000001;
let okCount = 0;
let largestDiff = 0;
compareBody.innerHTML = "";
compareFields.forEach((field) => {
const wasmValue = Number(getByPath(wasmResult, field));
const localValue = Number(getByPath(localResult, field));
const diff = Math.abs(wasmValue - localValue);
const matched = Number.isFinite(diff) && diff <= tolerance;
if (matched) okCount += 1;
if (Number.isFinite(diff)) largestDiff = Math.max(largestDiff, diff);
const row = document.createElement("tr");
row.innerHTML = `
<td>${field}</td>
<td>${Number.isFinite(wasmValue) ? wasmValue : "-"}</td>
<td>${Number.isFinite(localValue) ? localValue : "-"}</td>
<td>${Number.isFinite(diff) ? diff.toFixed(6) : "-"}</td>
<td><span class="badge ${matched ? "ok" : "warn"}">${matched ? "一致" : "不一致"}</span></td>
`;
compareBody.appendChild(row);
});
matchedCount.textContent = `${okCount}/${compareFields.length}`;
maxDiff.textContent = largestDiff.toFixed(6);
wasmCpk.textContent = getByPath(wasmResult, "Cpk.Cpk") ?? "-";
localCpk.textContent = getByPath(localResult, "Cpk.Cpk") ?? "-";
const allMatched = okCount === compareFields.length;
verifyStatus.textContent = allMatched ? "全部一致" : "存在差异";
verifyStatus.className = `badge ${allMatched ? "ok" : "warn"}`;
matchedCount.className = `metric-value ${allMatched ? "ok" : "warn"}`;
}
// 切换响应和复算详情面板。
function setActiveTab(name) {
const isResponse = name === "response";
responseTab.classList.toggle("active", isResponse);
localTab.classList.toggle("active", !isResponse);
responseOutput.classList.toggle("hidden", !isResponse);
localOutput.classList.toggle("hidden", isResponse);
}
// 执行一次 WASM 调用和前端复算验证。
async function runVerification() {
try {
setMessage("");
verifyStatus.textContent = "执行中";
verifyStatus.className = "badge";
const input = readInput();
const localResult = calculateLocalSpc(input);
const response = await callSpc(input);
if (response.res_data && response.res_data.error) {
throw new Error(response.res_data.error);
}
renderJsonOutputs(response, localResult);
renderComparison(response.res_data, localResult);
setMessage("验证完成", "is-ok");
} catch (error) {
verifyStatus.textContent = "失败";
verifyStatus.className = "badge warn";
setMessage(error.message || String(error), "is-error");
}
}
document.getElementById("sampleButton").addEventListener("click", loadSample);
document.getElementById("formatButton").addEventListener("click", () => {
try {
const values = parseDataInput();
dataInput.value = formatValues(values, Number(nInput.value) || 5);
updateInputCount();
setMessage("数据已格式化", "is-ok");
} catch (error) {
setMessage(error.message || String(error), "is-error");
}
});
document.getElementById("runButton").addEventListener("click", runVerification);
responseTab.addEventListener("click", () => setActiveTab("response"));
localTab.addEventListener("click", () => setActiveTab("local"));
[nInput, kInput, dataInput].forEach((element) => {
element.addEventListener("input", updateInputCount);
});
loadSample();
</script>
</body>
</html>