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 线圈/车辆信息
This commit is contained in:
wangfq
2026-06-08 10:42:13 +08:00
parent e538efafb5
commit 431653d033
10 changed files with 1085 additions and 25 deletions

View File

@@ -0,0 +1,173 @@
// 线圈参数管理
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/coil-info?search=${encodeURIComponent(search)}`);
const data = await resp.json();
renderTable(data);
} catch (e) {
console.error("加载失败:", e);
}
}
function sizeLabel(item) {
if (item.shape === '圆形') return `半径${item.radius || 0}cm`;
if (item.shape === '矩形') return `${item.length || 0}×${item.width || 0}cm`;
return '-';
}
function renderTable(data) {
const tbody = document.querySelector("#coil-table tbody");
if (!data.length) {
tbody.innerHTML = '<tr><td colspan="10" style="color:#999;text-align:center;">暂无数据,点右上角「新增」添加</td></tr>';
return;
}
tbody.innerHTML = data.map(t => `
<tr>
<td>${esc(t.coil_num)}</td>
<td>${esc(t.name)}</td>
<td>${t.induct || '-'}</td>
<td>${t.shape || '-'}</td>
<td>${sizeLabel(t)}</td>
<td>${t.turns || '-'}</td>
<td>${t.resistance || '-'}</td>
<td>${esc(t.material || '-')}</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.coil_num || t.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/coil-info/${id}`)
.then(r => r.json())
.then(data => {
document.getElementById("edit-coil-num").value = data.coil_num || "";
document.getElementById("edit-name").value = data.name || "";
document.getElementById("edit-induct").value = data.induct || 0;
document.getElementById("edit-shape").value = data.shape || "";
document.getElementById("edit-length").value = data.length || 0;
document.getElementById("edit-width").value = data.width || 0;
document.getElementById("edit-radius").value = data.radius || 0;
document.getElementById("edit-turns").value = data.turns || 0;
document.getElementById("edit-resistance").value = data.resistance || 0;
document.getElementById("edit-material").value = data.material || "";
document.getElementById("edit-remark").value = data.remark || "";
});
} else {
document.getElementById("edit-coil-num").value = "";
document.getElementById("edit-name").value = "";
document.getElementById("edit-induct").value = "0";
document.getElementById("edit-shape").value = "";
document.getElementById("edit-length").value = "0";
document.getElementById("edit-width").value = "0";
document.getElementById("edit-radius").value = "0";
document.getElementById("edit-turns").value = "0";
document.getElementById("edit-resistance").value = "0";
document.getElementById("edit-material").value = "";
document.getElementById("edit-remark").value = "";
}
}
function closeModal() {
document.getElementById("edit-modal").style.display = "none";
editId = null;
}
// ─── 保存 ────────────────────────────────────
async function saveRecord() {
const data = {
coil_num: document.getElementById("edit-coil-num").value.trim(),
name: document.getElementById("edit-name").value.trim(),
induct: parseFloat(document.getElementById("edit-induct").value) || 0,
shape: document.getElementById("edit-shape").value,
length: parseFloat(document.getElementById("edit-length").value) || 0,
width: parseFloat(document.getElementById("edit-width").value) || 0,
radius: parseFloat(document.getElementById("edit-radius").value) || 0,
turns: parseInt(document.getElementById("edit-turns").value) || 0,
resistance: parseFloat(document.getElementById("edit-resistance").value) || 0,
material: document.getElementById("edit-material").value.trim(),
remark: document.getElementById("edit-remark").value.trim(),
};
if (!data.coil_num && !data.name) {
toast("请输入线圈编号或名称", true);
return;
}
try {
let resp;
if (editId) {
resp = await fetch(`/api/coil-info/${editId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
} else {
resp = await fetch("/api/coil-info", {
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, label) {
if (!confirm(`确定要删除「${label}」吗?`)) return;
try {
const resp = await fetch(`/api/coil-info/${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();

View File

@@ -9,6 +9,8 @@ let pollTimers = {}; // record_id → timer (指令响应轮询)
async function init() {
await loadBaseTests();
await loadCoilList();
await loadCarList();
await loadFixtureParam();
}
@@ -119,6 +121,66 @@ function onDevTypeChange() {
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() {
@@ -152,6 +214,17 @@ function fillFormFromParam(param) {
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() {
@@ -168,31 +241,55 @@ async function refreshParams() {
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({
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,
}),
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,
@@ -324,19 +421,24 @@ async function sendConfig() {
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({
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,
}),
body: JSON.stringify(saveBody),
});
} else {
toast(`失败: ${data.error}`, true);

View File

@@ -0,0 +1,151 @@
// 模拟车辆参数管理
let editId = null;
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/simulate-car?search=${encodeURIComponent(search)}`);
const data = await resp.json();
renderTable(data);
} catch (e) {
console.error("加载失败:", e);
}
}
function sizeLabel(item) {
if (item.shape === '圆形') return `半径${item.radius || 0}cm`;
if (item.shape === '矩形') return `${item.length || 0}×${item.width || 0}cm`;
return '-';
}
function renderTable(data) {
const tbody = document.querySelector("#car-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>${esc(t.simulate_num)}</td>
<td>${esc(t.name)}</td>
<td>${t.shape || '-'}</td>
<td>${sizeLabel(t)}</td>
<td>${esc(t.material || '-')}</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.simulate_num || t.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/simulate-car/${id}`)
.then(r => r.json())
.then(data => {
document.getElementById("edit-simulate-num").value = data.simulate_num || "";
document.getElementById("edit-name").value = data.name || "";
document.getElementById("edit-shape").value = data.shape || "";
document.getElementById("edit-length").value = data.length || 0;
document.getElementById("edit-width").value = data.width || 0;
document.getElementById("edit-radius").value = data.radius || 0;
document.getElementById("edit-material").value = data.material || "";
document.getElementById("edit-remark").value = data.remark || "";
});
} else {
document.getElementById("edit-simulate-num").value = "";
document.getElementById("edit-name").value = "";
document.getElementById("edit-shape").value = "";
document.getElementById("edit-length").value = "0";
document.getElementById("edit-width").value = "0";
document.getElementById("edit-radius").value = "0";
document.getElementById("edit-material").value = "";
document.getElementById("edit-remark").value = "";
}
}
function closeModal() {
document.getElementById("edit-modal").style.display = "none";
editId = null;
}
async function saveRecord() {
const data = {
simulate_num: document.getElementById("edit-simulate-num").value.trim(),
name: document.getElementById("edit-name").value.trim(),
shape: document.getElementById("edit-shape").value,
length: parseFloat(document.getElementById("edit-length").value) || 0,
width: parseFloat(document.getElementById("edit-width").value) || 0,
radius: parseFloat(document.getElementById("edit-radius").value) || 0,
material: document.getElementById("edit-material").value.trim(),
remark: document.getElementById("edit-remark").value.trim(),
};
if (!data.simulate_num && !data.name) {
toast("请输入模拟编号或名称", true);
return;
}
try {
let resp;
if (editId) {
resp = await fetch(`/api/simulate-car/${editId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
} else {
resp = await fetch("/api/simulate-car", {
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, label) {
if (!confirm(`确定要删除「${label}」吗?`)) return;
try {
const resp = await fetch(`/api/simulate-car/${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();

View File

@@ -20,6 +20,7 @@ const VIEWS = {
{ key: 'exit_dist', title: '离开距离' },
{ key: 'remain_count', title: '剩余次数' },
{ key: 'curr_dist', title: '当前距离' },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
],
},
@@ -44,6 +45,7 @@ const VIEWS = {
{ key: 'exit_dist', title: '离开距离' },
{ key: 'enter_speed', title: '进入速度', render: r => toSpeed(r.enter_speed) },
{ key: 'exit_speed', title: '离开速度', render: r => toSpeed(r.exit_speed) },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
],
},
@@ -63,6 +65,7 @@ const VIEWS = {
{ key: 'b4_enter_dist', title: '进入高度(mm)' },
{ key: 'b4_leave_dist', title: '离开高度(mm)' },
{ key: 'relay_out', title: '继电器', render: r => decodeRelay(r.relay_code) },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
],
},
@@ -106,6 +109,18 @@ function decodeRelay(v) {
return RELAY_MAP[parseInt(v)] || `0x${parseInt(v).toString(16).toUpperCase().padStart(2, '0')}`;
}
/** 构建测试环境标签 (线圈 + 模拟车辆) */
function envLabel(r) {
const parts = [];
if (r.coil_num || r.coil_name) {
parts.push('🧵' + (r.coil_num || r.coil_name));
}
if (r.simulate_num || r.car_name) {
parts.push('🚗' + (r.simulate_num || r.car_name));
}
return parts.join(' ') || '-';
}
// ─── 视图切换 ────────────────────────────────────
function switchView(view) {