ERP/frontend/js/components/upload.js

572 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Upload = (() => {
// 使用localStorage保存上传记录
const STORAGE_KEY = 'mac_upload_history';
// 事件监听器清理
const eventListeners = [];
const addListener = (element, event, handler) => {
if(element){
element.addEventListener(event, handler);
eventListeners.push({element, event, handler});
}
};
const cleanupListeners = () => {
eventListeners.forEach(({element, event, handler}) => {
element.removeEventListener(event, handler);
});
eventListeners.length = 0;
};
Router.onBeforeEach((path) => {
if(!path.startsWith('/upload')){
cleanupListeners();
}
});
function getHistory() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
} catch {
return [];
}
}
function saveToHistory(records) {
try {
const history = getHistory();
const newRecords = records.map(r => ({
...r,
timestamp: new Date().toISOString()
}));
const updated = [...newRecords, ...history].slice(0, 100); // 保留最近100条
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
} catch (e) {
console.error('保存历史记录失败:', e);
}
}
function clearHistory() {
localStorage.removeItem(STORAGE_KEY);
}
function section(title, inner) {
return `<div class="card"><div style="font-weight:600;margin-bottom:8px">${title}</div>${inner}</div>`;
}
function filePicker(id,label,accept){
return `<div class="field"><label>${label}</label><input id="${id}" type="file" class="input" accept="${accept}" /></div>`;
}
function numberInput(id,label){return `<div class="field"><label>${label}</label><input id="${id}" type="number" class="input" /></div>`}
function textarea(id,label,placeholder=''){return `<div class="field"><label>${label}</label><textarea id="${id}" class="input" rows="4" placeholder="${placeholder}"></textarea></div>`}
async function renderMac(){
return section('MAC与批次MAC与批次对应关系表',`
<div class="field">
<label>上传类型</label>
<select id="mac-type" class="input">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
</select>
</div>
${filePicker('mac-file','批量导入(Excel)','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel')}
<div class="actions"><button class="btn" id="mac-upload">上传</button></div>
<div id="upload-log" style="margin-top:12px;display:none">
<div style="font-weight:600;margin-bottom:8px">上传日志</div>
<pre style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;font-size:12px;color:var(--text);white-space:pre-wrap"></pre>
</div>
<div style="margin-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-weight:600">最新记录</span>
<div style="display:flex;gap:8px">
<button class="btn btn-secondary" id="mac-show-history" style="font-size:12px;padding:4px 8px">查看历史</button>
<button class="btn btn-secondary" id="mac-clear-display" style="font-size:12px;padding:4px 8px">清空显示</button>
</div>
</div>
<ul id="mac-list" class="list" style="max-height:300px;overflow-y:auto"></ul>
</div>
`);
}
async function renderStats(){
return section('良/不良统计',`
<div style="background:rgba(79,140,255,0.08);border:1px solid rgba(79,140,255,0.2);border-radius:8px;padding:12px;margin-bottom:12px;font-size:13px;line-height:1.6">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📊 数据说明</div>
<div style="color:var(--text)">
<div><strong>直通良品数:</strong>一次检测就通过的产品数量</div>
<div><strong>良品数:</strong>最终通过检测的产品总数(包含直通良品 + 返修后通过的产品)</div>
<div><strong>不良品数:</strong>最终未通过检测的产品数量(报废或待返修)</div>
<div style="margin-top:6px;padding-top:6px;border-top:1px solid rgba(79,140,255,0.2);color:var(--text-2)">
💡 <strong>计算公式:</strong><br>
直通良品率 = 直通良品数 / (良品数 + 不良品数) × 100%<br>
总良品率 = 良品数 / (良品数 + 不良品数) × 100%
</div>
</div>
</div>
<div class="field">
<label>平台类型</label>
<select id="stats-platform" class="input">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
</select>
</div>
${numberInput('fpy-good-count','直通良品数量(一次检测通过)')}
${numberInput('good-count','良品数量(最终通过检测的总数)')}
${numberInput('bad-count','不良品数量(最终未通过)')}
<div class="field">
<label>不良明细(可选)</label>
<textarea id="bad-details" class="input" rows="4" placeholder="每行一个不良记录格式MAC地址,批次号&#10;例如90:A9:F7:DD:EE:FF,D20250000000001"></textarea>
</div>
<div class="actions"><button class="btn" id="stats-upload">上传</button></div>
<div style="margin-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-weight:600">最新记录</span>
<div style="display:flex;gap:8px">
<button class="btn btn-secondary" id="stats-show-history" style="font-size:12px;padding:4px 8px">查看历史</button>
<button class="btn btn-secondary" id="stats-clear-display" style="font-size:12px;padding:4px 8px">清空显示</button>
</div>
</div>
<ul id="stats-list" class="list" style="max-height:300px;overflow-y:auto"></ul>
</div>
`);
}
async function renderRepairs(){
return section('返修记录',`
${numberInput('repair-qty','返修完成数量')}
${textarea('repair-note','备注(可选)','例如:批次号、问题描述等')}
<div class="actions"><button class="btn" id="repairs-upload">上传</button></div>
<div style="margin-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-weight:600">最新记录</span>
<button class="btn btn-secondary" id="repairs-show-all" style="font-size:12px;padding:4px 8px">查看全部</button>
</div>
<ul id="repairs-list" class="list" style="max-height:300px;overflow-y:auto"></ul>
</div>
`);
}
async function renderDefects(){
return section('不良明细',`
${filePicker('defects-file','批量导入不良MAC与批次(Excel/CSV)','text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel')}
${textarea('defects-manual','手动输入','AA:BB:...,BATCH-xyz; ...')}
<div class="actions"><button class="btn" id="defects-upload">上传</button></div>
<div style="margin-top:12px"><div style="font-weight:600;margin-bottom:8px">最新记录</div><ul id="defects-list" class="list"></ul></div>
`);
}
Router.register('/upload/mac', async () => {
const html = await renderMac();
setTimeout(bindMacEvents,0);
setTimeout(async ()=>{
// 从服务器获取最新10条记录
const listEl=document.getElementById('mac-list');
try {
const data = await API.listMac();
if(listEl && data.list && data.list.length > 0){
listEl.innerHTML = data.list.slice(0, 10).map(r=>`<li><span>${r.mac}</span><span class="badge">${r.batch}</span></li>`).join('');
} else if(listEl) {
listEl.innerHTML = '<li>暂无数据</li>';
}
} catch(e) {
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
},0);
return html;
});
Router.register('/upload/stats', async () => {
const html = await renderStats();
setTimeout(bindStatsEvents,0);
setTimeout(async ()=>{
// 从服务器获取最新10条记录
const listEl=document.getElementById('stats-list');
try {
const data = await API.listStats();
if(listEl && data.list && data.list.length > 0){
listEl.innerHTML = data.list.slice(0, 10).map(r=>{
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[r.platform] || '';
const platformText = platformName ? `${platformName} - ` : '';
let html = `<li><span>${platformText}直通良:${r.fpy_good||0} 良:${r.good} 不良:${r.bad}</span>`;
html += '</li>';
return html;
}).join('');
} else if(listEl) {
listEl.innerHTML = '<li>暂无数据</li>';
}
} catch(e) {
console.error('加载历史记录失败:', e);
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
},0);
return html;
});
Router.register('/upload/repairs', async () => {
const html = await renderRepairs();
setTimeout(bindRepairsEvents,0);
setTimeout(async ()=>{
const listEl=document.getElementById('repairs-list');
const data=await API.listRepairs().catch(()=>({list:[]}));
listEl.innerHTML=(data.list||[]).slice(0,10).map(r=>{
const ts = new Date(r.ts).toLocaleString('zh-CN');
return `<li><span>数量: ${r.qty}</span><span class="badge">${r.note||'无备注'}</span><span style="font-size:11px;color:var(--text-2)">${ts}</span></li>`;
}).join('')||'<li>暂无数据</li>';
},0);
return html;
});
Router.register('/upload/defects', async () => {
const html = await renderDefects();
setTimeout(bindDefectsEvents,0);
setTimeout(async ()=>{
const listEl=document.getElementById('defects-list');
const data=await API.listDefects().catch(()=>({list:[]}));
listEl.innerHTML=(data.list||[]).slice(0,10).map(r=>`<li><span>${r.mac}</span><span class="badge">${r.batch}</span></li>`).join('')||'<li>暂无数据</li>';
},0);
return html;
});
function readText(file){return new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=reject;r.readAsText(file)})}
function parseManual(text){
return text.split(/\n+/).map(l=>l.trim()).filter(Boolean).map(l=>{
const [mac,batch]=l.split(',');
return { mac, batch };
});
}
async function bindMacEvents(){
const fileEl=document.getElementById('mac-file');
const btn = document.getElementById('mac-upload');
const showHistoryBtn = document.getElementById('mac-show-history');
const clearDisplayBtn = document.getElementById('mac-clear-display');
// 文件选择后立即验证
addListener(fileEl, 'change', async ()=>{
const file = fileEl.files[0];
if(!file) return;
try{
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/validate/mac-file', {
method: 'POST',
body: formData
});
const result = await res.json();
if(!result.valid){
API.toast(result.message || '文件格式不正确');
fileEl.value = '';
return;
}
API.toast(result.message || '文件验证通过');
}catch(e){
API.toast('文件验证失败');
fileEl.value = '';
}
});
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener(showHistoryBtn, 'click', async ()=>{
const listEl = document.getElementById('mac-list');
try {
const data = await API.listMac();
if(listEl){
if(data.list && data.list.length > 0){
listEl.innerHTML = data.list.map(r=>`<li><span>${r.mac}</span><span class="badge">${r.batch}</span><span style="font-size:11px;color:var(--text-2);margin-left:8px">${new Date(r.ts).toLocaleString('zh-CN')}</span></li>`).join('');
API.toast(`显示全部 ${data.list.length} 条历史记录`);
} else {
listEl.innerHTML = '<li>暂无历史记录</li>';
}
}
} catch(e) {
API.toast('加载历史记录失败');
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
});
// 清空显示按钮
addListener(clearDisplayBtn, 'click', ()=>{
const listEl = document.getElementById('mac-list');
if(listEl){
listEl.innerHTML = '<li>已清空显示</li>';
API.toast('已清空显示(历史记录仍保留)');
}
});
addListener(btn, 'click', async ()=>{
const file = fileEl.files[0];
if(!file){
API.toast('请选择文件');
return;
}
const typeEl = document.getElementById('mac-type');
const uploadType = typeEl ? typeEl.value : 'pdd';
const logContainer = document.getElementById('upload-log');
const logPre = logContainer ? logContainer.querySelector('pre') : null;
try{
btn.disabled = true;
if(logContainer) logContainer.style.display = 'block';
if(logPre) logPre.textContent = '正在上传文件...\n';
const formData = new FormData();
formData.append('file', file);
formData.append('type', uploadType);
const res = await fetch('/api/upload/mac-file', {
method: 'POST',
body: formData
});
const result = await res.json();
if(logPre){
logPre.textContent = result.output || '上传完成';
}
if(result.ok){
API.toast('上传成功');
// 解析并显示成功上传的记录
const output = result.output || '';
const jsonMatch = output.match(/=== 成功导入的数据 ===\n([\s\S]*?)\n=== 数据输出结束 ===/);
if(jsonMatch && jsonMatch[1]){
try{
const records = JSON.parse(jsonMatch[1].trim());
if(records.length > 0){
// 保存到历史记录
saveToHistory(records);
// 显示最新记录
const listEl = document.getElementById('mac-list');
if(listEl){
listEl.innerHTML = records.map(r=>`<li><span>${r.mac}</span><span class="badge">${r.batch}</span></li>`).join('');
}
}
}catch(e){
console.error('解析上传记录失败:', e);
}
}
} else {
API.toast(result.error || '上传失败');
}
if(fileEl) fileEl.value = '';
}catch(e){
API.toast('上传失败: ' + e.message);
if(logPre) logPre.textContent += '\n错误: ' + e.message;
} finally {
if (btn) btn.disabled = false;
}
});
}
async function bindStatsEvents(){
const STATS_STORAGE_KEY = 'stats_upload_history';
const getStatsHistory = () => {
try {
return JSON.parse(localStorage.getItem(STATS_STORAGE_KEY) || '[]');
} catch {
return [];
}
};
const saveToStatsHistory = (record) => {
try {
const history = getStatsHistory();
const newRecord = {
...record,
timestamp: new Date().toISOString()
};
const updated = [newRecord, ...history].slice(0, 100);
localStorage.setItem(STATS_STORAGE_KEY, JSON.stringify(updated));
} catch (e) {
console.error('保存历史记录失败:', e);
}
};
const btn = document.getElementById('stats-upload');
const showHistoryBtn = document.getElementById('stats-show-history');
const clearDisplayBtn = document.getElementById('stats-clear-display');
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener(showHistoryBtn, 'click', async ()=>{
const listEl = document.getElementById('stats-list');
try {
const data = await API.listStats();
if(listEl){
if(data.list && data.list.length > 0){
listEl.innerHTML = data.list.map(r=>{
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[r.platform] || '';
const platformText = platformName ? `${platformName} - ` : '';
let html = `<li><span>${platformText}直通良:${r.fpy_good||0} 良:${r.good} 不良:${r.bad}</span>`;
html += `<span style="font-size:11px;color:var(--text-2);margin-left:8px">${new Date(r.ts).toLocaleString('zh-CN')}</span></li>`;
return html;
}).join('');
API.toast(`显示全部 ${data.list.length} 条历史记录`);
} else {
listEl.innerHTML = '<li>暂无历史记录</li>';
}
}
} catch(e) {
API.toast('加载历史记录失败');
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
});
// 清空显示按钮
addListener(clearDisplayBtn, 'click', ()=>{
const listEl = document.getElementById('stats-list');
if(listEl){
listEl.innerHTML = '<li>已清空显示</li>';
API.toast('已清空显示(历史记录仍保留)');
}
});
addListener(btn, 'click', async ()=>{
const platform = document.getElementById('stats-platform').value;
const fpyGood=parseInt(document.getElementById('fpy-good-count').value||'0',10);
const good=parseInt(document.getElementById('good-count').value||'0',10);
const bad=parseInt(document.getElementById('bad-count').value||'0',10);
const detailsText = document.getElementById('bad-details')?.value.trim() || '';
if(fpyGood<0||good<0||bad<0){return API.toast('数量不能为负数')}
// 解析不良明细
const details = [];
if(detailsText){
const lines = detailsText.split('\n').filter(l => l.trim());
for(const line of lines){
const [mac, batch] = line.split(',').map(s => s.trim());
if(mac && batch){
details.push({mac, batch});
}
}
}
btn.disabled = true;
try{
await API.uploadStats({platform, fpy_good: fpyGood, good, bad, details});
API.toast('上传成功');
// 保存到历史记录
saveToStatsHistory({platform, fpy_good: fpyGood, good, bad, details});
// 显示最新记录
const listEl=document.getElementById('stats-list');
if(listEl){
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[platform] || platform;
let html = `<li><span>${platformName} - 直通良:${fpyGood} 良:${good} 不良:${bad}</span>`;
if(details.length > 0){
html += `<span class="badge">${details.length}条明细</span>`;
}
html += '</li>';
listEl.innerHTML = html;
}
// 清空输入
document.getElementById('fpy-good-count').value = '';
document.getElementById('good-count').value = '';
document.getElementById('bad-count').value = '';
document.getElementById('bad-details').value = '';
}catch(e){
API.toast('上传失败');
} finally {
if (btn) btn.disabled = false;
}
});
}
async function bindRepairsEvents(){
const btn = document.getElementById('repairs-upload');
const showAllBtn = document.getElementById('repairs-show-all');
// 查看全部按钮
addListener(showAllBtn, 'click', async ()=>{
const listEl = document.getElementById('repairs-list');
const data = await API.listRepairs().catch(()=>({list:[]}));
if(listEl){
if(data.list && data.list.length > 0){
listEl.innerHTML = data.list.map(r=>{
const ts = new Date(r.ts).toLocaleString('zh-CN');
return `<li><span>数量: ${r.qty}</span><span class="badge">${r.note||'无备注'}</span><span style="font-size:11px;color:var(--text-2)">${ts}</span></li>`;
}).join('');
API.toast(`显示全部 ${data.list.length} 条记录`);
} else {
listEl.innerHTML = '<li>暂无记录</li>';
}
}
});
addListener(btn, 'click', async ()=>{
const qty = parseInt(document.getElementById('repair-qty').value||'0', 10);
const note = document.getElementById('repair-note')?.value.trim() || '';
if(qty <= 0){
return API.toast('请输入有效的返修数量');
}
btn.disabled = true;
try{
await API.uploadRepairs({qty, note});
API.toast('上传成功');
// 刷新列表
const listEl = document.getElementById('repairs-list');
const data = await API.listRepairs().catch(()=>({list:[]}));
if(listEl){
listEl.innerHTML = (data.list||[]).slice(0,10).map(r=>{
const ts = new Date(r.ts).toLocaleString('zh-CN');
return `<li><span>数量: ${r.qty}</span><span class="badge">${r.note||'无备注'}</span><span style="font-size:11px;color:var(--text-2)">${ts}</span></li>`;
}).join('')||'<li>暂无数据</li>';
}
// 清空输入
document.getElementById('repair-qty').value = '';
document.getElementById('repair-note').value = '';
}catch(e){
API.toast('上传失败');
} finally {
if (btn) btn.disabled = false;
}
});
}
async function bindDefectsEvents(){
const btn = document.getElementById('defects-upload');
addListener(btn, 'click', async ()=>{
try{
const fileEl=document.getElementById('defects-file');
const manual=document.getElementById('defects-manual').value.trim();
let rows=[];
if(fileEl.files[0]){
const text=await readText(fileEl.files[0]);
rows=text.split(/\n+/).map(l=>l.split(','));
rows=rows.map(([mac,batch])=>({mac,batch}));
}else if(manual){
rows=parseManual(manual);
}
if(!rows.length){throw new Error('请提供文件或手动输入')}
btn.disabled = true;
await API.uploadDefects({rows});
API.toast('上传成功');
const listEl=document.getElementById('defects-list');
const data=await API.listDefects().catch(()=>({list:[]}));
if(listEl) listEl.innerHTML=(data.list||[]).slice(0,10).map(r=>`<li><span>${r.mac}</span><span class="badge">${r.batch}</span></li>`).join('')||'<li>暂无数据</li>';
}catch(e){
API.toast('上传失败');
} finally {
if (btn) btn.disabled = false;
}
});
}
})();