// 测试信息页 — 三视图 (全部 / B2 / B4) // ─── 视图定义 ─────────────────────────────────── const VIEWS = { all: { label: '全部数据', data_source: '', // '' = 不过滤 cols: [ { key: 'id', title: 'ID' }, { key: 'serial', title: '设备编码' }, { key: 'dpg430_addr', title: '地址' }, { key: 'model', title: '型号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' }, { key: 'str_type', title: '类型' }, { key: 'data_source', title: '来源' }, { key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' }, { key: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' }, { key: 'idle_freq', title: '开始频率' }, { key: 'enter_dist', title: '进入距离' }, { key: 'exit_dist', title: '离开距离' }, { key: 'remain_count', title: '剩余次数' }, { key: 'curr_dist', title: '当前距离' }, { key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) }, ], }, b2: { label: '灵敏度测试', data_source: 'B2', cols: [ { key: 'id', title: 'ID' }, { key: 'serial', title: '设备编码' }, { key: 'dpg430_addr', title: '地址' }, { key: 'model', title: '型号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' }, { key: 'str_type', title: '类型' }, { key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' }, { key: 'iffinish', title: '完成', render: r => r.iffinish === '1' ? '是' : '否' }, { key: 'fault_info', title: '故障信息' }, { key: 'relay_out', title: '继电器', render: r => decodeRelay(r.relay_code) }, { key: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' }, { key: 'idle_freq', title: '开始频率' }, { key: 'enter_freq', title: '进入频率' }, { key: 'exit_freq', title: '离开频率' }, { key: 'enter_dist', title: '进入距离' }, { 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: 'create_time', title: '时间', render: r => fmtTime(r.create_time) }, ], }, b4: { label: '波动测试', data_source: 'B4', cols: [ { key: 'id', title: 'ID' }, { key: 'serial', title: '设备编码' }, { key: 'dpg430_addr', 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: '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}`; } 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 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 = '
暂无数据
'; 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(); 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); } }