Files
vd_test_fixture/edc-web/app/static/js/test_data.js

533 lines
23 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)
// ─── 型号名称缓存 ─────────────────────────────────
let devTypeNameCache = {};
async function initDevTypeNames() {
try {
const resp = await fetch('/api/vehicle-base-test');
const tests = await resp.json();
devTypeNameCache = {};
tests.forEach(t => {
if (t.type_num != null && t.dev_name) {
devTypeNameCache[t.type_num] = t.dev_name;
}
});
} catch (e) { console.error('加载型号名称失败:', e); }
}
function getDevTypeName(subType) {
if (subType == null || subType === 0) return '-';
return devTypeNameCache[subType] || `Unknown(${subType})`;
}
// ─── 视图定义 ───────────────────────────────────
const VIEWS = {
all: {
label: '全部数据',
data_source: '', // '' = 不过滤
cols: [
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
{ key: 'model', title: '型号', render: r => getDevTypeName(r.sub_type) },
{ key: 'data_source', title: '来源' },
{ 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 => fmtRelay(r.relay_out) },
{ 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: 'ppvalue', title: '峰峰值(V)', render: r => r.data_source === 'B4' ? '-' : (r.ppvalue != null ? r.ppvalue.toFixed(2) : '-') },
{ 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: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
],
},
b2: {
label: '灵敏度测试',
data_source: 'B2',
cols: [
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
{ key: 'model', title: '型号', render: r => getDevTypeName(r.sub_type) },
{ 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 => fmtRelay(r.relay_out) },
{ 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: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' },
{ 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: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
],
},
b4: {
label: '波动测试',
data_source: 'B4',
cols: [
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
{ 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 => fmtRelay(r.relay_out) },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
],
},
};
// ─── 状态 ───────────────────────────────────────
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}`;
}
const RELAY_MAP = {
0: '无输出',
1: '存在信号',
2: '脉冲信号',
3: '存在信号; 脉冲信号',
};
function decodeRelay(v) {
if (v === null || v === undefined || v === '') return '-';
return RELAY_MAP[parseInt(v)] || `0x${parseInt(v).toString(16).toUpperCase().padStart(2, '0')}`;
}
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 fmtRelay(s) {
if (!s) return '-';
return s
.replace(/继电器有输出/g, '<span style="color:#22c55e;font-weight:600">✅有</span>')
.replace(/继电器无输出/g, '<span style="color:#ef4444;font-weight:600">❌无</span>');
}
// ─── 视图切换 ────────────────────────────────────
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 detectorSerial = document.getElementById("search-detector-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) || 100;
const params = new URLSearchParams({ page, per_page: perPage });
if (serial) params.set("serial", serial);
if (detectorSerial) params.set("detector_serial", detectorSerial);
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 detectorSerial = document.getElementById("search-detector-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 (detectorSerial) params.set("detector_serial", detectorSerial);
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 RELAY_MAP[value] || `未知(${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 detectorSerial = document.getElementById('search-detector-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 (detectorSerial) params.set('detector_serial', detectorSerial);
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 RELAY_MAP[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();
// 先加载型号名称再查询数据,确保型号列正确渲染
initDevTypeNames().then(() => 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);
}
}