Files
vd_test_fixture/edc-web/app/static/js/fixture.js
wangfq 4c337b60ae fix: 修复浏览器缓存导致工装参数 GET 请求返回旧数据
根因: /api/fixture/param/<id> GET 没有 Cache-Control 头,F5 刷新时
浏览器直接用磁盘缓存返回旧 TestMode,导致测试页显示旧模式。

修复:
- 服务端 fixture.py: GET 响应加 Cache-Control: no-store/no-cache/must-revalidate
- 客户端 test_op.js + fixture.js: 4 处 GET fetch 全部加 ?_=Date.now() 缓存破坏参数
2026-06-10 14:29:14 +08:00

470 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 工装配置页
// ─── 频率/峰峰值转换常量 ─────────────────────
// 协议: 工作频率 f(Hz) = 10 * X, X 为 DB/设备中存储和传输的原始值
// 协议: 峰峰值 V = ((X * 3.3) / 4095) * 4, X 为 DB/设备中存储和传输的原始值(正整数)
const FREQ_SCALE = 10;
const PEAK_SCALE = 4095 / (4 * 3.3); // ≈ 310.227
function rawFreqToHz(x) { return x * FREQ_SCALE; }
function hzToRawFreq(hz) { return Math.round(hz / FREQ_SCALE); }
function rawPeakToV(x) { return parseFloat(((x * 3.3) / 4095 * 4).toFixed(2)); }
function vToRawPeak(v) { return Math.round(v * PEAK_SCALE); }
let baseTests = []; // 所有车检器基准参数
let selectedBaseTest = null;
let pollTimer4C = null; // 0x4C 参数查询轮询
let pollTimers = {}; // record_id → timer (指令响应轮询)
// ─── 初始化 ─────────────────────────────────
async function init() {
await loadBaseTests();
await loadCoilList();
await loadCarList();
await loadFixtureParam();
}
// ─── Toast 提示 ─────────────────────────────
function toast(msg, isError = false) {
const el = document.getElementById("toast");
el.textContent = msg;
el.className = "msg-toast " + (isError ? "error" : "") + " show";
clearTimeout(el._timeout);
el._timeout = setTimeout(() => { el.className = "msg-toast"; }, 3000);
}
// ─── 通信日志 ───────────────────────────────
function commLog(type, hex, detail) {
const el = document.getElementById("comm-log");
if (el.querySelector('div') && el.querySelector('div').textContent === '等待操作…') {
el.innerHTML = '';
}
const time = new Date().toLocaleTimeString();
const colors = { send: '#4ec9b0', recv: '#ce9178', ok: '#6a9955', fail: '#f44747', info: '#888' };
const labels = { send: '→ 发送', recv: '← 收到', ok: '✓ 成功', fail: '✗ 失败', info: '· 信息' };
const color = colors[type] || '#888';
const label = labels[type] || '';
const html = `<div style="margin-bottom:3px;">
<span style="color:#569cd6;">${time}</span>
<span style="color:${color}; font-weight:600;"> ${label}</span>
<span style="color:#aaa;"> ${detail || ''}</span>
${hex ? `<br><span style="color:#808080; padding-left:10px;">${hex}</span>` : ''}
</div>`;
el.insertAdjacentHTML('afterbegin', html);
}
// ─── 解析 Flag 响应 ──────────────────────────
const FLAG_CMDS = { '4B': '配置参数', '4D': '出厂初始化', '4E': '设备复位' };
function parseFlagResponse(rcvPkg, cmd) {
// 格式: 7F8102{CMD}{FLAG}{XOR}{SUM} → FLAG 在位置 8-9
if (!rcvPkg || rcvPkg.length < 10) return null;
const flag = parseInt(rcvPkg.substring(8, 10), 16);
return flag;
}
// ─── 加载车检器测试基准 ──────────────────────
async function loadBaseTests() {
const search = document.getElementById("ref-search").value;
try {
const resp = await fetch(`/api/vehicle-base-test?search=${encodeURIComponent(search)}`);
baseTests = await resp.json();
renderBaseTestTable();
populateDevTypeSelect();
} catch (e) {
console.error("加载基准参数失败:", e);
}
}
function renderBaseTestTable() {
const tbody = document.getElementById("ref-table-body");
if (!baseTests.length) {
tbody.innerHTML = '<tr><td colspan="5" style="color:#999;text-align:center;">暂无数据</td></tr>';
return;
}
tbody.innerHTML = baseTests.map(t => `
<tr onclick="selectBaseTest(${t.id})" data-id="${t.id}"
class="${selectedBaseTest && selectedBaseTest.id === t.id ? 'selected' : ''}">
<td>${t.type_num}</td>
<td>${esc(t.dev_name)}</td>
<td>${t.SensMin}~${t.SensMax}</td>
<td>${rawFreqToHz(t.FreMin)}~${rawFreqToHz(t.FreMax)}</td>
<td>${rawPeakToV(t.PeakMin)}~${rawPeakToV(t.PeakMax)}</td>
</tr>
`).join("");
}
function populateDevTypeSelect() {
const sel = document.getElementById("param-dev-type");
sel.innerHTML = '<option value="0">-- 请选择车检器基准 --</option>' +
baseTests.map(t => `<option value="${t.type_num}">${t.type_num} - ${esc(t.dev_name)}</option>`).join("");
if (selectedBaseTest) {
sel.value = selectedBaseTest.type_num;
}
}
function selectBaseTest(id) {
selectedBaseTest = baseTests.find(t => t.id === id);
if (!selectedBaseTest) return;
fillFromBaseTest(selectedBaseTest);
renderBaseTestTable();
}
function fillFromBaseTest(t) {
document.getElementById("param-dev-type").value = t.type_num;
document.getElementById("param-sens-min").value = t.SensMin;
document.getElementById("param-sens-max").value = t.SensMax;
document.getElementById("param-fre-min").value = rawFreqToHz(t.FreMin);
document.getElementById("param-fre-max").value = rawFreqToHz(t.FreMax);
document.getElementById("param-peak-min").value = rawPeakToV(t.PeakMin);
document.getElementById("param-peak-max").value = rawPeakToV(t.PeakMax);
}
function onDevTypeChange() {
const typeNum = parseInt(document.getElementById("param-dev-type").value);
const matched = baseTests.find(t => t.type_num === typeNum);
if (matched) selectBaseTest(matched.id);
else { selectedBaseTest = null; renderBaseTestTable(); }
}
// ─── 线圈列表 ────────────────────────────────
let coilList = [];
async function loadCoilList() {
try {
const resp = await fetch("/api/coil-info");
coilList = await resp.json();
populateCoilSelect();
} catch (e) { console.error("加载线圈列表失败:", e); }
}
function populateCoilSelect() {
const sel = document.getElementById("coil-select");
sel.innerHTML = '<option value="">-- 选择线圈 --</option>' +
coilList.map(c => `<option value="${c.id}">${esc(c.coil_num || c.name || `#${c.id}`)}</option>`).join("");
}
function onCoilChange() {
const id = parseInt(document.getElementById("coil-select").value);
const coil = coilList.find(c => c.id === id);
const detail = document.getElementById("coil-detail");
if (coil) {
const sizeText = coil.shape === '圆形' ? `半径${coil.radius}cm` : `${coil.length}×${coil.width}cm`;
detail.innerHTML = `${coil.shape || '-'} / ${sizeText} / ${coil.turns || 0}圈 / ${coil.resistance || 0}Ω / ${coil.material || ''}`;
} else {
detail.innerHTML = '';
}
}
// ─── 模拟车辆列表 ────────────────────────────
let carList = [];
async function loadCarList() {
try {
const resp = await fetch("/api/simulate-car");
carList = await resp.json();
populateCarSelect();
} catch (e) { console.error("加载模拟车辆列表失败:", e); }
}
function populateCarSelect() {
const sel = document.getElementById("car-select");
sel.innerHTML = '<option value="">-- 选择模拟车辆 --</option>' +
carList.map(c => `<option value="${c.id}">${esc(c.simulate_num || c.name || `#${c.id}`)}</option>`).join("");
}
function onCarChange() {
const id = parseInt(document.getElementById("car-select").value);
const car = carList.find(c => c.id === id);
const detail = document.getElementById("car-detail");
if (car) {
const sizeText = car.shape === '圆形' ? `半径${car.radius}cm` : `${car.length}×${car.width}cm`;
detail.innerHTML = `${car.shape || '-'} / ${sizeText} / ${car.material || ''}`;
} else {
detail.innerHTML = '';
}
}
// ─── 从 DB 加载/刷新/保存 ────────────────────
async function loadFixtureParam() {
try {
const resp = await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`);
const param = await resp.json();
if (param && param.dnt_id) {
fillFormFromParam(param);
commLog('info', null, '已从数据库加载工装参数');
}
} catch (e) { console.error("加载工装参数失败:", e); }
}
function fillFormFromParam(param) {
document.getElementById("param-addr").value = param.Addr || 1;
document.getElementById("param-test-mode").value = param.TestMode || 0;
document.getElementById("param-reset-dis").value = (param.RestDis || 0) * 10;
document.getElementById("param-minus-dis").value = (param.MinusDis || 0) * 10;
document.getElementById("param-dev-type").value = param.DevType || 0;
document.getElementById("param-sens-min").value = param.SensMin || 0;
document.getElementById("param-sens-max").value = param.SensMax || 0;
document.getElementById("param-fre-min").value = rawFreqToHz(param.FreMin || 0);
document.getElementById("param-fre-max").value = rawFreqToHz(param.FreMax || 0);
document.getElementById("param-peak-min").value = rawPeakToV(param.PeakMin || 0);
document.getElementById("param-peak-max").value = rawPeakToV(param.PeakMax || 0);
document.getElementById("param-far-tol").value = (param.FarTol || 0) * 10;
document.getElementById("param-near-tol").value = (param.NearTol || 0) * 10;
document.getElementById("param-step-tol").value = (param.StepTol || 0) * 10;
document.getElementById("param-back-forth").value = param.BackForth || 0;
document.getElementById("param-near-stay").value = param.NearStay || 0;
document.getElementById("param-far-stay").value = param.FarStay || 0;
const matched = baseTests.find(t => t.type_num === param.DevType);
if (matched) { selectedBaseTest = matched; renderBaseTestTable(); }
// 设置线圈和模拟车辆选中
if (param.coil_id) {
document.getElementById("coil-select").value = param.coil_id;
onCoilChange();
}
if (param.simulate_car_id) {
document.getElementById("car-select").value = param.simulate_car_id;
onCarChange();
}
// 更新当前关联标签
updateCurrentLabels(param);
}
async function refreshParams() {
const param = await (await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`)).json();
if (param && param.dnt_id) {
fillFormFromParam(param);
commLog('info', null, '已刷新:从数据库加载工装参数');
toast("已刷新");
} else {
toast("数据库中没有该工装的参数,请先查询(0x4C)或保存", true);
commLog('info', null, '刷新失败:数据库中没有参数');
}
}
async function saveToDb() {
const data = getFormParams();
const coilId = parseInt(document.getElementById("coil-select").value) || null;
const carId = parseInt(document.getElementById("car-select").value) || null;
const body = {
Addr: data.addr, DevType: data.dev_type, TestMode: data.test_mode,
RestDis: data.reset_dis, MinusDis: data.minus_dis,
SensMin: data.sens_min, SensMax: data.sens_max,
FreMin: data.fre_min, FreMax: data.fre_max,
PeakMin: data.peak_min, PeakMax: data.peak_max,
FarTol: data.far_tol, NearTol: data.near_tol,
StepTol: data.step_tol, BackForth: data.back_forth,
NearStay: data.near_stay, FarStay: data.far_stay,
};
if (coilId) body.coil_id = coilId;
if (carId) body.simulate_car_id = carId;
try {
const resp = await fetch(`/api/fixture/param/${DNT_ID}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const result = await resp.json();
if (result.ok) {
commLog('info', null, '参数已保存到数据库');
toast("已保存到数据库");
updateCurrentLabels();
} else {
toast("保存失败: " + (result.error || ""), true);
}
} catch (e) { toast("保存失败: " + e.message, true); }
}
/** 更新页面上的当前线圈/车辆标签 */
function updateCurrentLabels(param) {
const coilLabel = document.getElementById("current-coil-label");
const carLabel = document.getElementById("current-car-label");
if (param) {
coilLabel.textContent = param.coil_name || param.coil_num || '未设置';
carLabel.textContent = param.car_name || param.simulate_num || '未设置';
} else {
// 从 DOM 读取当前选择
const coilId = parseInt(document.getElementById("coil-select").value);
const carId = parseInt(document.getElementById("car-select").value);
const coil = coilId ? coilList.find(c => c.id === coilId) : null;
const car = carId ? carList.find(c => c.id === carId) : null;
coilLabel.textContent = coil ? (coil.coil_num || coil.name) : '未设置';
carLabel.textContent = car ? (car.simulate_num || car.name) : '未设置';
}
}
function getFormParams() {
return {
addr: parseInt(document.getElementById("param-addr").value) || 1,
dev_type: parseInt(document.getElementById("param-dev-type").value) || 0,
test_mode: parseInt(document.getElementById("param-test-mode").value) || 0,
reset_dis: Math.round((parseInt(document.getElementById("param-reset-dis").value) || 0) / 10),
minus_dis: Math.round((parseInt(document.getElementById("param-minus-dis").value) || 0) / 10),
sens_min: parseInt(document.getElementById("param-sens-min").value) || 0,
sens_max: parseInt(document.getElementById("param-sens-max").value) || 0,
fre_min: hzToRawFreq(parseFloat(document.getElementById("param-fre-min").value) || 0),
fre_max: hzToRawFreq(parseFloat(document.getElementById("param-fre-max").value) || 0),
peak_min: vToRawPeak(parseFloat(document.getElementById("param-peak-min").value) || 0),
peak_max: vToRawPeak(parseFloat(document.getElementById("param-peak-max").value) || 0),
far_tol: Math.round((parseInt(document.getElementById("param-far-tol").value) || 0) / 10),
near_tol: Math.round((parseInt(document.getElementById("param-near-tol").value) || 0) / 10),
step_tol: Math.round((parseInt(document.getElementById("param-step-tol").value) || 0) / 10),
back_forth: parseInt(document.getElementById("param-back-forth").value) || 0,
near_stay: parseInt(document.getElementById("param-near-stay").value) || 0,
far_stay: parseInt(document.getElementById("param-far-stay").value) || 0,
};
}
// ─── 轮询 serialnet 响应(通用)─────────────
function startRespPolling(recordId, cmd) {
stopRespPolling(recordId);
let attempts = 0;
pollTimers[recordId] = setInterval(async () => {
attempts++;
try {
const resp = await fetch(`/api/fixture/serialnet/${recordId}`);
const rec = await resp.json();
if (rec.state === 2) {
// 已完成
stopRespPolling(recordId);
const rcvPkg = rec.rcv_pkg || '';
if (rcvPkg) {
commLog('recv', rcvPkg, '');
// 解析 Flag0x4B/0x4D/0x4E
if (cmd in FLAG_CMDS) {
const flag = parseFlagResponse(rcvPkg, cmd);
if (flag === 0) {
commLog('ok', null, `${FLAG_CMDS[cmd]} 成功`);
toast(`${FLAG_CMDS[cmd]} 成功`);
} else {
commLog('fail', null, `${FLAG_CMDS[cmd]} 失败 (Flag=${flag})`);
toast(`${FLAG_CMDS[cmd]} 失败`, true);
}
} else {
commLog('ok', null, `指令 0x${cmd} 已完成`);
toast(`指令 0x${cmd} 已完成`);
}
// 0x4C 返回后刷新参数
if (cmd === '4C') {
// 参数已在后端 upsert直接从 DB 加载
setTimeout(async () => {
const p = await (await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`)).json();
if (p && p.dnt_id) fillFormFromParam(p);
}, 500);
}
} else {
commLog('info', null, `指令 0x${cmd} 已完成(无返回数据)`);
}
} else if (rec.state === 3) {
// 超时
stopRespPolling(recordId);
commLog('fail', null, `指令 0x${cmd} 超时(设备无响应)`);
toast(`指令 0x${cmd} 超时`, true);
}
// state=0/1 → 继续等待
} catch (e) {
// 忽略网络错误
}
if (attempts >= 4) {
stopRespPolling(recordId);
commLog('fail', null, `指令 0x${cmd} 轮询超时4秒无响应`);
}
}, 1000);
}
function stopRespPolling(recordId) {
if (pollTimers[recordId]) {
clearInterval(pollTimers[recordId]);
delete pollTimers[recordId];
}
}
// ─── 发送工装指令(通用)────────────────────
async function sendFixtureCmd(cmd) {
try {
const resp = await fetch("/api/fixture/command", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dnt_id: DNT_ID, cmd }),
});
const data = await resp.json();
if (data.ok) {
commLog('send', data.send_pkg, `${cmdName(cmd)} (0x${cmd})`);
toast(`指令 0x${cmd} 已下发`);
startRespPolling(data.record_id, cmd);
} else {
commLog('fail', null, `指令 0x${cmd} 发送失败: ${data.error}`);
toast(`失败: ${data.error}`, true);
}
} catch (e) {
commLog('fail', null, `发送 0x${cmd} 异常: ${e.message}`);
toast("发送失败: " + e.message, true);
}
}
// ─── 发送配置指令 0x4B ───────────────────────
async function sendConfig() {
const params = getFormParams();
try {
const resp = await fetch("/api/fixture/command", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dnt_id: DNT_ID, cmd: "4B", params }),
});
const data = await resp.json();
if (data.ok) {
commLog('send', data.send_pkg, `配置参数 (0x4B)`);
toast("配置指令 0x4B 已下发");
startRespPolling(data.record_id, "4B");
// 同时保存到数据库
const coilId = parseInt(document.getElementById("coil-select").value) || null;
const carId = parseInt(document.getElementById("car-select").value) || null;
const saveBody = {
Addr: params.addr, DevType: params.dev_type, TestMode: params.test_mode,
RestDis: params.reset_dis, MinusDis: params.minus_dis,
SensMin: params.sens_min, SensMax: params.sens_max,
FreMin: params.fre_min, FreMax: params.fre_max,
PeakMin: params.peak_min, PeakMax: params.peak_max,
FarTol: params.far_tol, NearTol: params.near_tol,
StepTol: params.step_tol, BackForth: params.back_forth,
NearStay: params.near_stay, FarStay: params.far_stay,
};
if (coilId) saveBody.coil_id = coilId;
if (carId) saveBody.simulate_car_id = carId;
await fetch(`/api/fixture/param/${DNT_ID}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(saveBody),
});
} else {
toast(`失败: ${data.error}`, true);
}
} catch (e) {
toast("配置失败: " + e.message, true);
}
}
function cmdName(cmd) {
const names = { '4A': '获取版本号', '4B': '配置参数', '4C': '查询参数', '4D': '出厂初始化', '4E': '设备复位' };
return names[cmd] || cmd;
}
function esc(s) { return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
init();