ERP/frontend/js/components/export.js

472 lines
18 KiB
JavaScript
Raw Normal View History

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);
})();