feat: 工装配置页面 + 车检器基准参数管理

- 设备页增加「配置」按钮(devices.js)
- 新增工装配置页面(fixture.html+js): 参数表单、5个操作按钮、通信日志区、基准参数表
- 新增车检器基准参数管理页面(vehicle_base_test.html+js): CRUD + 搜索
- 新增 fixture 蓝图(routes/fixture.py): 0x4A~0x4E 指令发送、参数CRUD、serialnet状态查询
- models.py: 新增 get_serialnet_by_id, tb_fixture_param/tb_vechicle_base_test CRUD
- edc_server 子模块更新
This commit is contained in:
wangfq
2026-05-29 17:26:07 +08:00
parent ae816eaffd
commit dbe5d1cefb
10 changed files with 1197 additions and 1 deletions

View File

@@ -32,6 +32,8 @@ tr:hover { background: #f8f9fa; }
/* === Buttons === */
.btn-test { padding: 4px 14px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
.btn-test:hover { background: #2980b9; }
.btn-config { padding: 4px 14px; background: #9b59b6; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 4px; }
.btn-config:hover { background: #8e44ad; }
/* === Test Page Layout === */
.test-header { margin-bottom: 20px; }
@@ -103,3 +105,67 @@ tr:hover { background: #f8f9fa; }
.search-bar { justify-content: center; }
.btn-export { margin-left: 0; }
}
/* === Fixture Page === */
.fixture-layout { display: flex; gap: 24px; }
.fixture-left { flex: 1; min-width: 420px; }
.fixture-right { flex: 1; min-width: 380px; }
.fixture-card { background: #fff; padding: 16px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08); margin-bottom: 16px; }
.fixture-card h3 { margin: 0 0 12px; font-size: 15px; color: #555; }
.fixture-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.fixture-form .form-group { display: flex; flex-direction: column; }
.fixture-form .form-group label { font-size: 12px; color: #777; margin-bottom: 3px; }
.fixture-form .form-group input,
.fixture-form .form-group select { padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
.fixture-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.btn-fixture { padding: 8px 14px; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; font-size: 12px; transition: .15s; background: #ecf0f1; }
.btn-fixture:hover { background: #3498db; color: #fff; border-color: #3498db; }
.btn-fixture.danger { color: #e74c3c; border-color: #e74c3c; }
.btn-fixture.danger:hover { background: #e74c3c; color: #fff; }
.btn-fixture.primary { background: #3498db; color: #fff; border-color: #3498db; }
.btn-fixture.primary:hover { background: #2980b9; }
.version-info { margin-top: 8px; padding: 8px 12px; background: #f0f8ff; border-radius: 4px; font-size: 13px; color: #2980b9; }
.msg-toast { position: fixed; top: 20px; right: 20px; background: #27ae60; color: #fff; padding: 10px 20px; border-radius: 6px; font-size: 14px; z-index: 1000; opacity: 0; transition: opacity .3s; }
.msg-toast.show { opacity: 1; }
.msg-toast.error { background: #e74c3c; }
/* === Vehicle base test table in fixture page === */
.ref-table-wrapper { max-height: 300px; overflow-y: auto; margin-top: 8px; }
.ref-table-wrapper table { font-size: 12px; }
.ref-table-wrapper th, .ref-table-wrapper td { padding: 6px 8px; }
.ref-table-wrapper tr { cursor: pointer; }
.ref-table-wrapper tr:hover { background: #ebf5fb; }
.ref-table-wrapper tr.selected { background: #d4e6f1; }
/* === Vehicle Base Test management === */
.vbt-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.btn-add { padding: 8px 18px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
.btn-add:hover { background: #219a52; }
.btn-edit { padding: 3px 10px; background: #f39c12; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; }
.btn-edit:hover { background: #e67e22; }
.btn-del { padding: 3px 10px; background: #e74c3c; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; margin-left: 4px; }
.btn-del:hover { background: #c0392b; }
/* === Modal === */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: flex; align-items: center; justify-content: center; z-index: 999; }
.modal-box { background: #fff; border-radius: 10px; padding: 24px; width: 480px; max-width: 95vw; box-shadow: 0 8px 32px rgba(0,0,0,.15); }
.modal-box h3 { margin: 0 0 16px; font-size: 16px; }
.modal-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.modal-form .form-group { display: flex; flex-direction: column; }
.modal-form .form-group.full { grid-column: 1 / -1; }
.modal-form label { font-size: 12px; color: #777; margin-bottom: 3px; }
.modal-form input, .modal-form select, .modal-form textarea { padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.btn-save { padding: 8px 20px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
.btn-save:hover { background: #2980b9; }
.btn-cancel { padding: 8px 20px; background: #ecf0f1; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; }
/* === Communication Log === */
#comm-log { word-break: break-all; }
#comm-log::-webkit-scrollbar { width: 6px; }
#comm-log::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }

View File

@@ -22,6 +22,7 @@ function renderTable(devices) {
<td>${d.last_login || '-'}</td>
<td>
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>
</td>
</tr>
`).join("");

View File

@@ -0,0 +1,338 @@
// 工装配置页
let baseTests = []; // 所有车检器基准参数
let selectedBaseTest = null;
let pollTimer4C = null; // 0x4C 参数查询轮询
let pollTimers = {}; // record_id → timer (指令响应轮询)
// ─── 初始化 ─────────────────────────────────
async function init() {
await loadBaseTests();
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(); }
}
// ─── 从 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;
const matched = baseTests.find(t => t.type_num === param.DevType);
if (matched) { selectedBaseTest = matched; renderBaseTestTable(); }
}
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();
try {
const resp = await fetch(`/api/fixture/param/${DNT_ID}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
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,
}),
});
const result = await resp.json();
if (result.ok) {
commLog('info', null, '参数已保存到数据库');
toast("已保存到数据库");
} else {
toast("保存失败: " + (result.error || ""), true);
}
} catch (e) { toast("保存失败: " + e.message, true); }
}
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,
};
}
// ─── 轮询 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");
// 同时保存到数据库
await fetch(`/api/fixture/param/${DNT_ID}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
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,
}),
});
} 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();

View File

@@ -0,0 +1,160 @@
// 车检器测试基准参数管理
let editId = null; // null=新增, number=编辑
// ─── 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);
}
// ─── 列表加载 ────────────────────────────────
async function loadList() {
const search = document.getElementById("search-input").value;
try {
const resp = await fetch(`/api/vehicle-base-test?search=${encodeURIComponent(search)}`);
const data = await resp.json();
renderTable(data);
} catch (e) {
console.error("加载失败:", e);
}
}
function renderTable(data) {
const tbody = document.querySelector("#vbt-table tbody");
if (!data.length) {
tbody.innerHTML = '<tr><td colspan="7" style="color:#999;text-align:center;">暂无数据,点右上角「新增」添加</td></tr>';
return;
}
tbody.innerHTML = data.map(t => `
<tr>
<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>
<td>${esc(t.remark || '-')}</td>
<td>
<button class="btn-edit" onclick="openModal(${t.id})">编辑</button>
<button class="btn-del" onclick="deleteRecord(${t.id}, '${esc(t.dev_name)}')">删除</button>
</td>
</tr>
`).join("");
}
// ─── 弹窗 ────────────────────────────────────
function openModal(id = null) {
editId = id;
document.getElementById("modal-title").textContent = id ? "编辑车检器测试基准" : "新增车检器测试基准";
document.getElementById("edit-modal").style.display = "flex";
if (id) {
// 编辑:加载数据
fetch(`/api/vehicle-base-test/${id}`)
.then(r => r.json())
.then(data => {
document.getElementById("edit-type-num").value = data.type_num;
document.getElementById("edit-dev-name").value = data.dev_name;
document.getElementById("edit-sens-min").value = data.SensMin;
document.getElementById("edit-sens-max").value = data.SensMax;
document.getElementById("edit-fre-min").value = data.FreMin;
document.getElementById("edit-fre-max").value = data.FreMax;
document.getElementById("edit-peak-min").value = data.PeakMin;
document.getElementById("edit-peak-max").value = data.PeakMax;
document.getElementById("edit-remark").value = data.remark || "";
});
} else {
// 新增:清空
document.getElementById("edit-type-num").value = "";
document.getElementById("edit-dev-name").value = "";
document.getElementById("edit-sens-min").value = "0";
document.getElementById("edit-sens-max").value = "0";
document.getElementById("edit-fre-min").value = "0";
document.getElementById("edit-fre-max").value = "0";
document.getElementById("edit-peak-min").value = "0";
document.getElementById("edit-peak-max").value = "0";
document.getElementById("edit-remark").value = "";
}
}
function closeModal() {
document.getElementById("edit-modal").style.display = "none";
editId = null;
}
// ─── 保存 ────────────────────────────────────
async function saveRecord() {
const data = {
type_num: parseInt(document.getElementById("edit-type-num").value) || 0,
dev_name: document.getElementById("edit-dev-name").value.trim(),
SensMin: parseInt(document.getElementById("edit-sens-min").value) || 0,
SensMax: parseInt(document.getElementById("edit-sens-max").value) || 0,
FreMin: parseInt(document.getElementById("edit-fre-min").value) || 0,
FreMax: parseInt(document.getElementById("edit-fre-max").value) || 0,
PeakMin: parseInt(document.getElementById("edit-peak-min").value) || 0,
PeakMax: parseInt(document.getElementById("edit-peak-max").value) || 0,
remark: document.getElementById("edit-remark").value.trim(),
};
if (!data.dev_name) {
toast("请输入型号/名称", true);
return;
}
try {
let resp;
if (editId) {
resp = await fetch(`/api/vehicle-base-test/${editId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
} else {
resp = await fetch("/api/vehicle-base-test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
}
const result = await resp.json();
if (result.ok || resp.ok) {
toast(editId ? "更新成功" : "新增成功");
closeModal();
loadList();
} else {
toast("保存失败: " + (result.error || "未知错误"), true);
}
} catch (e) {
toast("保存失败: " + e.message, true);
}
}
// ─── 删除 ────────────────────────────────────
async function deleteRecord(id, name) {
if (!confirm(`确定要删除「${name}」吗?`)) return;
try {
const resp = await fetch(`/api/vehicle-base-test/${id}`, { method: "DELETE" });
const result = await resp.json();
if (result.ok) {
toast("删除成功");
loadList();
} else {
toast("删除失败: " + (result.error || "未知错误"), true);
}
} catch (e) {
toast("删除失败: " + e.message, true);
}
}
function esc(s) { return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }
loadList();