新增SPC前端验证页面
This commit is contained in:
998
public/spc_test.html
Normal file
998
public/spc_test.html
Normal 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>
|
||||
Reference in New Issue
Block a user