Files
vd_test_fixture/edc-web/app/static/js/fixture.js
wangfq 431653d033 feat(edc-web): 线圈参数/模拟车辆参数管理 + 工装关联 + 测试环境显示
新增功能:
- 线圈参数管理页 (/coil-info): 增删改查,日志记录
- 模拟车辆管理页 (/simulate-car): 增删改查,日志记录
- 工装配置页新增线圈/模拟车辆选择区,保存时关联到 tb_fixture_param
- 测试信息查询页新增「测试环境」列,显示当前线圈和模拟车辆信息
- edc_server 写入测试数据时自动从 fixture 获取线圈/车辆关联

数据库:
- 新增 tb_coil_info、tb_simulate_car 表
- tb_fixture_param 增加 coil_id/simulate_car_id 字段
- tb_state_tst 增加 coil_id/simulate_car_id 字段

后端:
- models.py 新增线圈/模拟车辆 CRUD
- get_fixture_param 改为 LEFT JOIN 返回线圈/车辆详情
- upsert_fixture_param 支持 coil_id/simulate_car_id
- 测试数据查询 LEFT JOIN 线圈/车辆信息
2026-06-08 10:42:13 +08:00

459 lines
19 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.
// 工装配置页
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>${t.FreMin}~${t.FreMax}</td>
<td>${t.PeakMin}~${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 = t.FreMin;
document.getElementById("param-fre-max").value = t.FreMax;
document.getElementById("param-peak-min").value = t.PeakMin;
document.getElementById("param-peak-max").value = 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}`);
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;
document.getElementById("param-minus-dis").value = param.MinusDis || 0;
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 = param.FreMin || 0;
document.getElementById("param-fre-max").value = param.FreMax || 0;
document.getElementById("param-peak-min").value = param.PeakMin || 0;
document.getElementById("param-peak-max").value = param.PeakMax || 0;
document.getElementById("param-far-tol").value = param.FarTol || 0;
document.getElementById("param-near-tol").value = param.NearTol || 0;
document.getElementById("param-step-tol").value = param.StepTol || 0;
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}`)).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: parseInt(document.getElementById("param-reset-dis").value) || 0,
minus_dis: parseInt(document.getElementById("param-minus-dis").value) || 0,
sens_min: parseInt(document.getElementById("param-sens-min").value) || 0,
sens_max: parseInt(document.getElementById("param-sens-max").value) || 0,
fre_min: parseInt(document.getElementById("param-fre-min").value) || 0,
fre_max: parseInt(document.getElementById("param-fre-max").value) || 0,
peak_min: parseInt(document.getElementById("param-peak-min").value) || 0,
peak_max: parseInt(document.getElementById("param-peak-max").value) || 0,
far_tol: parseInt(document.getElementById("param-far-tol").value) || 0,
near_tol: parseInt(document.getElementById("param-near-tol").value) || 0,
step_tol: parseInt(document.getElementById("param-step-tol").value) || 0,
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}`)).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();