// 测试信息页 — 三视图 (全部 / 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' ? '-' : `` }, { 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 => `` }, { 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, '&').replace(//g, '>').replace(/"/g, '"'); } /** 构建测试环境标签 (线圈 + 模拟车辆) */ 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, '✅有') .replace(/继电器无输出/g, '❌无'); } // ─── 视图切换 ──────────────────────────────────── 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 = '
暂无数据
'; 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); } }