472 lines
18 KiB
JavaScript
472 lines
18 KiB
JavaScript
const Export = (() => {
|
||
async function render() {
|
||
setTimeout(bindEvents, 0);
|
||
return `
|
||
<div class="card">
|
||
<div style="font-weight:600;margin-bottom:16px;font-size:16px">📤 数据导出</div>
|
||
|
||
<div class="field">
|
||
<label>选择要导出的数据类型</label>
|
||
<select id="export-type" class="input">
|
||
<option value="stats">良/不良统计</option>
|
||
<option value="mac">MAC与批次</option>
|
||
<option value="repairs">返修记录</option>
|
||
<option value="defects">不良明细</option>
|
||
<option value="shipments">发货记录</option>
|
||
<option value="devices">设备状态</option>
|
||
<option value="environment">环境参数</option>
|
||
<option value="personnel">人员信息</option>
|
||
<option value="qa">质检报告</option>
|
||
<option value="production">时间记录</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>导出格式</label>
|
||
<div style="display:flex;gap:12px;margin-top:8px">
|
||
<button class="btn" id="export-excel" style="flex:1">
|
||
<span>📊</span>
|
||
<span>导出为 Excel</span>
|
||
</button>
|
||
<button class="btn btn-secondary" id="export-pdf" style="flex:1">
|
||
<span>📄</span>
|
||
<span>导出为 PDF</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="export-status" style="margin-top:16px;padding:12px;border-radius:8px;display:none"></div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:12px">
|
||
<div style="font-weight:600;margin-bottom:12px">📋 数据预览</div>
|
||
<div id="export-preview" style="max-height:400px;overflow-y:auto">
|
||
<div style="color:var(--text-2);text-align:center;padding:40px">请选择数据类型查看预览</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function bindEvents() {
|
||
const typeSelect = document.getElementById('export-type');
|
||
const excelBtn = document.getElementById('export-excel');
|
||
const pdfBtn = document.getElementById('export-pdf');
|
||
|
||
// 类型改变时更新预览
|
||
if (typeSelect) {
|
||
typeSelect.addEventListener('change', updatePreview);
|
||
// 初始加载预览
|
||
updatePreview();
|
||
}
|
||
|
||
// Excel导出
|
||
if (excelBtn) {
|
||
excelBtn.addEventListener('click', async () => {
|
||
const type = typeSelect.value;
|
||
await exportData(type, 'excel');
|
||
});
|
||
}
|
||
|
||
// PDF导出
|
||
if (pdfBtn) {
|
||
pdfBtn.addEventListener('click', async () => {
|
||
const type = typeSelect.value;
|
||
await exportData(type, 'pdf');
|
||
});
|
||
}
|
||
}
|
||
|
||
async function updatePreview() {
|
||
const type = document.getElementById('export-type').value;
|
||
const preview = document.getElementById('export-preview');
|
||
|
||
try {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">加载中...</div>';
|
||
|
||
let data;
|
||
switch(type) {
|
||
case 'stats':
|
||
data = await API.listStats();
|
||
renderStatsPreview(data.list || []);
|
||
break;
|
||
case 'mac':
|
||
data = await API.listMac();
|
||
renderMacPreview(data.list || []);
|
||
break;
|
||
case 'repairs':
|
||
data = await API.listRepairs();
|
||
renderRepairsPreview(data.list || []);
|
||
break;
|
||
case 'defects':
|
||
data = await API.listDefects();
|
||
renderDefectsPreview(data.list || []);
|
||
break;
|
||
case 'shipments':
|
||
data = await API.listShipments();
|
||
renderShipmentsPreview(data.list || []);
|
||
break;
|
||
case 'devices':
|
||
data = await API.devices();
|
||
renderDevicesPreview(data.list || []);
|
||
break;
|
||
case 'environment':
|
||
data = await API.environment();
|
||
renderEnvironmentPreview(data);
|
||
break;
|
||
case 'personnel':
|
||
data = await API.personnel();
|
||
renderPersonnelPreview(data.list || []);
|
||
break;
|
||
case 'qa':
|
||
data = await API.qa();
|
||
renderQaPreview(data.list || []);
|
||
break;
|
||
case 'production':
|
||
data = await API.production();
|
||
renderProductionPreview(data.list || []);
|
||
break;
|
||
}
|
||
} catch (e) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--danger)">加载失败</div>';
|
||
}
|
||
}
|
||
|
||
function renderStatsPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">直通良品数</th>
|
||
<th style="padding:8px;text-align:left">良品数</th>
|
||
<th style="padding:8px;text-align:left">不良品数</th>
|
||
<th style="padding:8px;text-align:left">时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.slice(0, 50).map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">${item.fpy_good || 0}</td>
|
||
<td style="padding:8px">${item.good}</td>
|
||
<td style="padding:8px">${item.bad}</td>
|
||
<td style="padding:8px;font-size:12px;color:var(--text-2)">${new Date(item.ts).toLocaleString('zh-CN')}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
${list.length > 50 ? `<div style="text-align:center;padding:12px;color:var(--text-2);font-size:13px">仅显示前50条,导出时将包含全部 ${list.length} 条数据</div>` : ''}
|
||
`;
|
||
}
|
||
|
||
function renderMacPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">MAC地址</th>
|
||
<th style="padding:8px;text-align:left">批次号</th>
|
||
<th style="padding:8px;text-align:left">时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.slice(0, 50).map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px;font-family:monospace">${item.mac}</td>
|
||
<td style="padding:8px">${item.batch}</td>
|
||
<td style="padding:8px;font-size:12px;color:var(--text-2)">${new Date(item.ts).toLocaleString('zh-CN')}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
${list.length > 50 ? `<div style="text-align:center;padding:12px;color:var(--text-2);font-size:13px">仅显示前50条,导出时将包含全部 ${list.length} 条数据</div>` : ''}
|
||
`;
|
||
}
|
||
|
||
function renderRepairsPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">返修数量</th>
|
||
<th style="padding:8px;text-align:left">备注</th>
|
||
<th style="padding:8px;text-align:left">时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.slice(0, 50).map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">${item.qty}</td>
|
||
<td style="padding:8px">${item.note || '无'}</td>
|
||
<td style="padding:8px;font-size:12px;color:var(--text-2)">${new Date(item.ts).toLocaleString('zh-CN')}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
${list.length > 50 ? `<div style="text-align:center;padding:12px;color:var(--text-2);font-size:13px">仅显示前50条,导出时将包含全部 ${list.length} 条数据</div>` : ''}
|
||
`;
|
||
}
|
||
|
||
function renderDefectsPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">MAC地址</th>
|
||
<th style="padding:8px;text-align:left">批次号</th>
|
||
<th style="padding:8px;text-align:left">时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.slice(0, 50).map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px;font-family:monospace">${item.mac}</td>
|
||
<td style="padding:8px">${item.batch}</td>
|
||
<td style="padding:8px;font-size:12px;color:var(--text-2)">${new Date(item.ts).toLocaleString('zh-CN')}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
${list.length > 50 ? `<div style="text-align:center;padding:12px;color:var(--text-2);font-size:13px">仅显示前50条,导出时将包含全部 ${list.length} 条数据</div>` : ''}
|
||
`;
|
||
}
|
||
|
||
function renderShipmentsPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">日期</th>
|
||
<th style="padding:8px;text-align:left">数量</th>
|
||
<th style="padding:8px;text-align:left">收货方</th>
|
||
<th style="padding:8px;text-align:left">时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.slice(0, 50).map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">${item.date}</td>
|
||
<td style="padding:8px">${item.qty}</td>
|
||
<td style="padding:8px">${item.receiver}</td>
|
||
<td style="padding:8px;font-size:12px;color:var(--text-2)">${new Date(item.ts).toLocaleString('zh-CN')}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
${list.length > 50 ? `<div style="text-align:center;padding:12px;color:var(--text-2);font-size:13px">仅显示前50条,导出时将包含全部 ${list.length} 条数据</div>` : ''}
|
||
`;
|
||
}
|
||
|
||
function renderDevicesPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">设备名称</th>
|
||
<th style="padding:8px;text-align:left">状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">${item.name}</td>
|
||
<td style="padding:8px">${item.status}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function renderEnvironmentPreview(data) {
|
||
const preview = document.getElementById('export-preview');
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">参数</th>
|
||
<th style="padding:8px;text-align:left">值</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">温度</td>
|
||
<td style="padding:8px">${data.temp || '—'}</td>
|
||
</tr>
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">湿度</td>
|
||
<td style="padding:8px">${data.hum || '—'}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function renderPersonnelPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">姓名</th>
|
||
<th style="padding:8px;text-align:left">角色</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">${item.name}</td>
|
||
<td style="padding:8px">${item.role || '—'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function renderQaPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">标题</th>
|
||
<th style="padding:8px;text-align:left">日期</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">${item.title}</td>
|
||
<td style="padding:8px">${item.date}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function renderProductionPreview(list) {
|
||
const preview = document.getElementById('export-preview');
|
||
if (list.length === 0) {
|
||
preview.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">暂无数据</div>';
|
||
return;
|
||
}
|
||
preview.innerHTML = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:var(--surface);border-bottom:2px solid var(--border)">
|
||
<th style="padding:8px;text-align:left">批次</th>
|
||
<th style="padding:8px;text-align:left">时长</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${list.map(item => `
|
||
<tr style="border-bottom:1px solid var(--border)">
|
||
<td style="padding:8px">${item.batch}</td>
|
||
<td style="padding:8px">${item.duration}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
async function exportData(type, format) {
|
||
const statusEl = document.getElementById('export-status');
|
||
|
||
try {
|
||
statusEl.style.display = 'block';
|
||
statusEl.style.background = 'rgba(79,140,255,0.1)';
|
||
statusEl.style.color = 'var(--primary)';
|
||
statusEl.textContent = `正在导出${format.toUpperCase()}...`;
|
||
|
||
// 调用导出API并下载文件
|
||
const endpoint = format === 'excel' ? '/api/export/excel' : '/api/export/pdf';
|
||
const response = await fetch(endpoint, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({ type })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('导出失败');
|
||
}
|
||
|
||
// 获取文件名
|
||
const contentDisposition = response.headers.get('Content-Disposition');
|
||
let filename = `export_${type}_${Date.now()}.${format === 'excel' ? 'xlsx' : 'pdf'}`;
|
||
if (contentDisposition) {
|
||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||
if (filenameMatch && filenameMatch[1]) {
|
||
filename = filenameMatch[1].replace(/['"]/g, '');
|
||
}
|
||
}
|
||
|
||
// 下载文件
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.style.display = 'none';
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
statusEl.style.background = 'rgba(34,197,94,0.1)';
|
||
statusEl.style.color = 'var(--success)';
|
||
statusEl.textContent = `✓ ${format.toUpperCase()}导出成功!文件已下载`;
|
||
|
||
setTimeout(() => {
|
||
statusEl.style.display = 'none';
|
||
}, 3000);
|
||
} catch (e) {
|
||
statusEl.style.background = 'rgba(239,68,68,0.1)';
|
||
statusEl.style.color = 'var(--danger)';
|
||
statusEl.textContent = `✗ 导出失败: ${e.message}`;
|
||
}
|
||
}
|
||
|
||
Router.register('/export', render);
|
||
})(); |