// 测试信息页 — 三视图 (全部 / 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: '继电器' }, { 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: '继电器' }, { 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 '-'; const d = new Date(v); 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 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); } // ─── 查询 ──────────────────────────────────────── async function searchData(page = 1) { currentPage = page; const serial = document.getElementById("search-serial").value; const dateFrom = document.getElementById("search-date-from").value; const dateTo = document.getElementById("search-date-to").value; 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 = '' + v.cols.map(c => `${c.title}`).join('') + ''; } // ─── 渲染数据行 ────────────────────────────────── 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 = `暂无数据`; return; } tbody.innerHTML = records.map(r => '' + v.cols.map(c => { if (c.render) return `${c.render(r)}`; const val = r[c.key]; return `${val !== null && val !== undefined && val !== '' ? val : '-'}`; }).join('') + '' ).join(""); } // ─── 分页 ──────────────────────────────────────── function renderPagination() { const div = document.getElementById("pagination"); let html = ""; html += ``; for (let i = 1; i <= totalPages; i++) { if (i === currentPage) { html += ``; } else { html += ``; } } html += ``; div.innerHTML = html; } // ─── 导出 ──────────────────────────────────────── function exportCSV() { const serial = document.getElementById("search-serial").value; const dateFrom = document.getElementById("search-date-from").value; const dateTo = document.getElementById("search-date-to").value; 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 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 = document.getElementById('search-date-from').value; const dateTo = document.getElementById('search-date-to').value; 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 = '

暂无数据

'; 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, })); // 渲染 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: 140, 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 }, ], 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 = document.getElementById('search-date-from').value; const dateTo = document.getElementById('search-date-to').value; 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); } }