Files
vd_test_fixture/edc-web/app/static/js/test_data.js
wangfq a26d8807cb fix: 前端继电器显示同步为新格式 + 更新子模块
与后端 decode_relay_info 保持一致:bit 0/1 始终显示 有/无,不再用缩写 MAP
2026-06-10 16:25:38 +08:00

498 lines
21 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.
// 测试信息页 — 三视图 (全部 / B2 / B4)
// ─── 视图定义 ───────────────────────────────────
const VIEWS = {
all: {
label: '全部数据',
data_source: '', // '' = 不过滤
cols: [
{ key: 'id', title: 'ID' },
{ key: 'serial', title: '设备编码' },
{ key: 'model', title: '型号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' },
{ key: 'data_source', title: '来源' },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
{ key: 'iffinish', title: '完成', render: r => r.data_source === 'B4' ? '-' : (r.iffinish === '1' ? '是' : '否') },
{ key: 'fault_info', title: '故障信息', render: r => r.data_source === 'B4' ? '-' : `<span style="display:inline-block;max-width:12em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.fault_info || '')}">${escHtml(r.fault_info || '-')}</span>` },
{ key: 'relay_out', title: '继电器', render: r => decodeRelay(r.relay_code) },
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.data_source === 'B4' ? '-' : (r.ppvalue != null ? r.ppvalue.toFixed(2) : '-') },
{ key: 'idle_freq', title: '开始频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.idle_freq || '-') },
{ key: 'enter_freq', title: '进入频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.enter_freq || '-') },
{ key: 'exit_freq', title: '离开频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.exit_freq || '-') },
{ key: 'enter_dist', title: '触发距离(mm)', render: r => {
const v = r.data_source === 'B4' ? r.b4_enter_dist : r.enter_dist;
return v != null ? v + ' ' : '-';
}},
{ key: 'exit_dist', title: '释放距离(mm)', render: r => {
const v = r.data_source === 'B4' ? r.b4_leave_dist : r.exit_dist;
return v != null ? v + ' ' : '-';
}},
{ key: 'enter_speed', title: '进入速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.enter_speed) },
{ key: 'exit_speed', title: '离开速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.exit_speed) },
{ key: 'remain_count', title: '剩余次数', render: r => r.data_source === 'B2' ? '-' : (r.remain_count ?? '-') },
{ key: 'work_freq', title: '工作频率(Hz)', render: r => r.data_source === 'B2' ? '-' : (r.work_freq ?? '-') },
{ key: 'curr_dist', title: '当前距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.curr_dist != null ? r.curr_dist + ' ' : '-') },
{ key: 'speed', title: '速度(dm/s)', render: r => r.data_source === 'B2' ? '-' : (r.speed ?? '-') },
{ key: 'near_dist', title: '最近距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.near_dist != null ? r.near_dist + ' ' : '-') },
{ key: 'far_dist', title: '最远距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.far_dist != null ? r.far_dist + ' ' : '-') },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
],
},
b2: {
label: '灵敏度测试',
data_source: 'B2',
cols: [
{ key: 'id', title: 'ID' },
{ key: 'serial', title: '设备编码' },
{ key: 'model', title: '型号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
{ key: 'iffinish', title: '完成', render: r => r.iffinish === '1' ? '是' : '否' },
{ key: 'fault_info', title: '故障信息', render: r => `<span style="display:inline-block;max-width:12em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.fault_info || '')}">${escHtml(r.fault_info || '-')}</span>` },
{ key: 'relay_out', title: '继电器', render: r => decodeRelay(r.relay_code) },
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' },
{ key: 'idle_freq', title: '开始频率(Hz)' },
{ key: 'enter_freq', title: '进入频率(Hz)' },
{ key: 'exit_freq', title: '离开频率(Hz)' },
{ key: 'enter_dist', title: '触发距离(mm)' },
{ key: 'exit_dist', title: '释放距离(mm)' },
{ 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) },
],
},
b4: {
label: '波动测试',
data_source: 'B4',
cols: [
{ key: 'id', title: 'ID' },
{ key: 'serial', title: '设备编码' },
{ key: 'remain_count', title: '剩余次数' },
{ key: 'work_freq', title: '工作频率(Hz)' },
{ key: 'curr_dist', title: '当前距离(mm)' },
{ key: 'speed', title: '速度(dm/s)' },
{ key: 'near_dist', title: '最近距离(mm)' },
{ key: 'far_dist', title: '最远距离(mm)' },
{ 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) },
],
},
};
// ─── 状态 ───────────────────────────────────────
let currentView = 'all';
let currentPage = 1;
let totalPages = 1;
function toSpeed(v) {
if (v === null || v === undefined || v === '') return '-';
return (parseFloat(v) / 10).toFixed(1);
}
function fmtTime(v) {
if (!v) return '-';
// Flask jsonify 给 MySQL DATETIME 加 "GMT" 后缀但实际值是服务器本地时间UTC+8
// 去掉 "GMT" 让 JS 按本地时间解析,避免时区偏移 8 小时
const cleaned = String(v).replace(/ GMT$/, '');
const d = new Date(cleaned);
if (isNaN(d.getTime())) return String(v).substring(0, 19);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const d2 = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
return `${y}-${m}-${d2} ${h}:${min}:${s}`;
}
function decodeRelay(v) {
if (v === null || v === undefined || v === '') return '-';
return decodeRelayCode(parseInt(v));
}
/** 将 relay_code 原始值解码为可读字符串:
* bit 0 → 存在继电器bit 1 → 脉冲继电器 */
function decodeRelayCode(code) {
const exist = (code & 0x01) ? "存在继电器有输出" : "存在继电器无输出";
const pulse = (code & 0x02) ? "脉冲继电器有输出" : "脉冲继电器无输出";
return `${exist}${pulse}`;
}
function escHtml(s) {
return (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/** 构建测试环境标签 (线圈 + 模拟车辆) */
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) {
currentView = view;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + view).classList.add('active');
// 重置分页
currentPage = 1;
searchData(1);
}
// ─── 查询 ────────────────────────────────────────
/** 合并日期和时间输入框,返回 "YYYY-MM-DD" 或 "YYYY-MM-DD HH:MM:SS" 或 "" */
function getDatetime(dateId, timeId) {
const d = document.getElementById(dateId).value;
const t = document.getElementById(timeId).value;
if (!d) return "";
if (!t) return d;
return d + " " + t;
}
async function searchData(page = 1) {
currentPage = page;
const serial = document.getElementById("search-serial").value;
const dateFrom = getDatetime("search-date-from", "search-time-from");
const dateTo = getDatetime("search-date-to", "search-time-to");
const v = VIEWS[currentView];
const perPage = parseInt(document.getElementById("per-page").value) || 20;
const params = new URLSearchParams({ page, per_page: perPage });
if (serial) params.set("serial", serial);
if (dateFrom) params.set("date_from", dateFrom);
if (dateTo) params.set("date_to", dateTo);
// 按 data_source 过滤(全部不过滤)
if (v.data_source) {
params.set("data_source", v.data_source);
}
try {
const resp = await fetch(`/api/test-data?${params}`);
const data = await resp.json();
renderTable(data.records);
totalPages = data.pages;
renderPagination();
} catch (e) {
console.error("查询失败:", e);
}
}
// ─── 渲染表头 ────────────────────────────────────
function renderHead() {
const thead = document.querySelector("#test-data-table thead");
const v = VIEWS[currentView];
thead.innerHTML = '<tr>' +
v.cols.map(c => `<th>${c.title}</th>`).join('') +
'</tr>';
}
// ─── 渲染数据行 ──────────────────────────────────
function renderTable(records) {
renderHead();
const tbody = document.querySelector("#test-data-table tbody");
const v = VIEWS[currentView];
const nCols = v.cols.length;
if (!records.length) {
tbody.innerHTML = `<tr><td colspan="${nCols}" style="text-align:center;color:#999;">暂无数据</td></tr>`;
return;
}
tbody.innerHTML = records.map(r =>
'<tr>' + v.cols.map(c => {
if (c.render) return `<td>${c.render(r)}</td>`;
const val = r[c.key];
return `<td>${val !== null && val !== undefined && val !== '' ? val : '-'}</td>`;
}).join('') + '</tr>'
).join("");
}
// ─── 分页 ────────────────────────────────────────
function renderPagination() {
const div = document.getElementById("pagination");
let html = "";
html += `<button onclick="searchData(${currentPage - 1})" ${currentPage <= 1 ? 'disabled' : ''}>上一页</button>`;
for (let i = 1; i <= totalPages; i++) {
if (i === currentPage) {
html += `<button class="active">${i}</button>`;
} else {
html += `<button onclick="searchData(${i})">${i}</button>`;
}
}
html += `<button onclick="searchData(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>下一页</button>`;
div.innerHTML = html;
}
// ─── 导出 ────────────────────────────────────────
function exportCSV() {
const serial = document.getElementById("search-serial").value;
const dateFrom = getDatetime("search-date-from", "search-time-from");
const dateTo = getDatetime("search-date-to", "search-time-to");
const v = VIEWS[currentView];
const params = new URLSearchParams();
if (serial) params.set("serial", serial);
if (dateFrom) params.set("date_from", dateFrom);
if (dateTo) params.set("date_to", dateTo);
if (v.data_source) params.set("data_source", v.data_source);
window.location.href = `/api/test-data/export?${params}`;
}
// ─── 图表 ────────────────────────────────────────
let chartMode = false;
let chartInstance = null;
// 图表系列定义
const CHART_SERIES = {
b2: [
{ key: 'ppvalue', name: '峰峰值', unit: 'V', yAxisIndex: 0 },
{ key: 'idle_freq', name: '开始频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'enter_freq', name: '进入频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'exit_freq', name: '离开频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'enter_dist', name: '进入距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'exit_dist', name: '离开距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'enter_speed', name: '进入速度', unit: 'dm/s',yAxisIndex: 2 },
{ key: 'exit_speed', name: '离开速度', unit: 'dm/s',yAxisIndex: 2 },
],
b4: [
{ key: 'work_freq', name: '工作频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'curr_dist', name: '当前距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'speed', name: '速度', unit: 'dm/s',yAxisIndex: 2 },
{ key: 'near_dist', name: '最近距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'far_dist', name: '最远距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'b4_enter_dist', name: '进入高度', unit: 'mm', yAxisIndex: 1 },
{ key: 'b4_leave_dist', name: '离开高度', unit: 'mm', yAxisIndex: 1 },
],
};
// 继电器系列(添加到任意视图的图表末尾)
function buildRelaySeries(records) {
return {
name: '继电器输出',
type: 'line',
step: 'end',
yAxisIndex: 3,
symbol: 'triangle',
symbolSize: 8,
lineStyle: { type: 'dotted', width: 2, color: '#e74c3c' },
itemStyle: { color: '#e74c3c' },
data: records.map(r => r.relay_code ?? null),
// tooltip 中显示解码后的文本
tooltip: {
valueFormatter: function (value) {
return decodeRelayCode(value);
}
},
};
}
function toggleChart() {
const container = document.getElementById('chart-container');
const btn = document.getElementById('btn-chart');
const table = document.getElementById('test-data-table');
const pagination = document.getElementById('pagination');
chartMode = !chartMode;
if (chartMode) {
container.style.display = 'block';
table.style.display = 'none';
pagination.style.display = 'none';
btn.textContent = '📋 表格';
btn.classList.add('active');
// 只对 B2/B4 视图显示图表
if (currentView === 'all') switchView('b2');
loadChart();
} else {
container.style.display = 'none';
table.style.display = '';
pagination.style.display = '';
btn.textContent = '📈 图表';
btn.classList.remove('active');
if (chartInstance) { chartInstance.dispose(); chartInstance = null; }
}
}
async function loadChart() {
const container = document.getElementById('chart-container');
if (!container || container.style.display === 'none') return;
const serial = document.getElementById('search-serial').value;
const dateFrom = getDatetime('search-date-from', 'search-time-from');
const dateTo = getDatetime('search-date-to', 'search-time-to');
const v = VIEWS[currentView];
// 全部视图不适用,用 B2 或 B4
const ds = v.data_source || (currentView === 'all' ? 'B2' : v.data_source);
const params = new URLSearchParams();
if (serial) params.set('serial', serial);
if (dateFrom) params.set('date_from', dateFrom);
if (dateTo) params.set('date_to', dateTo);
if (ds) params.set('data_source', ds);
let resp, data;
try {
resp = await fetch(`/api/test-data/chart?${params}`);
data = await resp.json();
} catch (e) {
console.error('加载图表数据失败:', e);
return;
}
const records = data.records || [];
if (!records.length) {
container.innerHTML = '<p style="text-align:center;color:#999;padding:100px;">暂无数据</p>';
return;
}
// 选系列定义
const seriesDef = CHART_SERIES[ds === 'B4' ? 'b4' : 'b2'] || CHART_SERIES.b2;
// 时间轴
const times = records.map(r => r.create_time);
// 构建 series
const series = seriesDef.map(def => ({
name: `${def.name}(${def.unit})`,
type: 'line',
yAxisIndex: def.yAxisIndex,
symbol: 'circle',
symbolSize: 4,
data: records.map(r => r[def.key] ?? null),
connectNulls: false,
}));
// 添加继电器状态系列
series.push(buildRelaySeries(records));
// 渲染 ECharts
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(container);
const option = {
title: {
text: ds === 'B4' ? '波动测试 (0xB4) 数据趋势' : '灵敏度测试 (0xB2) 数据趋势',
left: 'center',
textStyle: { fontSize: 14 },
},
tooltip: {
trigger: 'axis',
},
legend: {
type: 'scroll',
bottom: 0,
},
toolbox: {
right: 10,
top: 10,
feature: {
saveAsImage: {
title: '保存图片',
pixelRatio: 2,
},
},
},
grid: { left: 60, right: 200, top: 60, bottom: 80 },
xAxis: {
type: 'category',
data: times,
axisLabel: {
formatter: v => fmtTime(v).substring(5, 16), // MM-dd HH:mm
rotate: 30,
},
},
yAxis: [
{ type: 'value', name: '频率/电压', nameTextStyle: { fontSize: 11 } },
{ type: 'value', name: '距离(mm)', nameTextStyle: { fontSize: 11 } },
{ type: 'value', name: '速度(dm/s)',nameTextStyle: { fontSize: 11 },
offset: 80 },
{ type: 'value', name: '继电器输出', nameTextStyle: { fontSize: 11 },
min: -0.5, max: 3.5, interval: 1,
offset: 160,
axisLabel: {
formatter: function (v) {
return decodeRelayCode(v);
},
fontSize: 10,
}},
],
dataZoom: [
{ type: 'slider', start: 0, end: 100, height: 20, bottom: 30 },
{ type: 'inside' },
],
series: series,
};
chartInstance.setOption(option);
// 窗口 resize 时自适应
window.addEventListener('resize', () => {
if (chartInstance) chartInstance.resize();
}, { once: false });
}
// ─── 初始加载 ────────────────────────────────────
renderHead();
searchData(1);
// ─── 删除admin─────────────────────────────────
function confirmDelete() {
const serial = document.getElementById('search-serial').value;
const dateFrom = getDatetime('search-date-from', 'search-time-from');
const dateTo = getDatetime('search-date-to', 'search-time-to');
const v = VIEWS[currentView];
const ds = v.data_source || '';
let desc = '';
if (serial) desc += `设备: ${serial}\n`;
if (dateFrom || dateTo) desc += `日期: ${dateFrom || '不限'} ~ ${dateTo || '不限'}\n`;
if (ds) desc += `数据来源: ${ds}\n`;
if (!desc) desc = '⚠ 未设置任何筛选条件,不会删除任何数据';
const msg = `确认删除以下条件的测试数据?\n\n${desc}\n此操作不可撤销!`;
if (!confirm(msg)) return;
doDelete(serial, dateFrom, dateTo, ds);
}
async function doDelete(serial, dateFrom, dateTo, dataSource) {
try {
const resp = await fetch('/api/test-data/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serial, date_from: dateFrom,
date_to: dateTo, data_source: dataSource,
}),
});
const data = await resp.json();
if (data.ok) {
alert(`已删除 ${data.deleted} 条记录`);
searchData(1);
} else {
alert('删除失败: ' + (data.error || '未知错误'));
}
} catch (e) {
alert('删除请求失败: ' + e.message);
}
}