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 `
${title}
${inner}
`; } function filePicker(id,label,accept){ return `
`; } function numberInput(id,label){return `
`} function textarea(id,label,placeholder=''){return `
`} async function renderMac(){ return section('MAC与批次(MAC与批次对应关系表)',`
${filePicker('mac-file','批量导入(Excel)','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel')}
最新记录
`); } async function renderStats(){ return section('良/不良统计',`
📊 数据说明
直通良品数:一次检测就通过的产品数量
良品数:最终通过检测的产品总数(包含直通良品 + 返修后通过的产品)
不良品数:最终未通过检测的产品数量(报废或待返修)
💡 计算公式:
直通良品率 = 直通良品数 / (良品数 + 不良品数) × 100%
总良品率 = 良品数 / (良品数 + 不良品数) × 100%
${numberInput('fpy-good-count','直通良品数量(一次检测通过)')} ${numberInput('good-count','良品数量(最终通过检测的总数)')} ${numberInput('bad-count','不良品数量(最终未通过)')}
最新记录
`); } // 不良原因选项 const defectReasonOptions = [ '无不良', '外观不良', '功能异常', '连接问题', '电源故障', '软件问题', '物理损坏', '其他' ]; // 机种选项 const machineTypeOptions = [ '拼多多', '圆通', '兔喜', '美团', '大润发', '标准版' ]; async function renderRepairs(){ return section('返修记录上传',`
点击或拖拽图片到此处上传
支持 JPG/PNG/GIF,单张最大10MB,最多10张
历史记录
`); } 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; ...')}
最新记录
`); } 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){ const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}; listEl.innerHTML = data.list.slice(0, 10).map(r=>{ const platformName = platformNames[r.platform] || r.platform || '未知'; return `
  • ${r.mac}${r.batch}${platformName}
  • `; }).join(''); } else if(listEl) { listEl.innerHTML = '
  • 暂无数据
  • '; } } catch(e) { if(listEl) listEl.innerHTML = '
  • 加载失败
  • '; } },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 = `
  • ${platformText}直通良:${r.fpy_good||0} 良:${r.good} 不良:${r.bad}`; html += '
  • '; return html; }).join(''); } else if(listEl) { listEl.innerHTML = '
  • 暂无数据
  • '; } } catch(e) { console.error('加载历史记录失败:', e); if(listEl) listEl.innerHTML = '
  • 加载失败
  • '; } },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 `
  • 数量: ${r.qty}${r.note||'无备注'}${ts}
  • `; }).join('')||'
  • 暂无数据
  • '; },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=>`
  • ${r.mac}${r.batch}
  • `).join('')||'
  • 暂无数据
  • '; },0); return html; }); async function renderSop(){ return section('SOP 文件管理',`
    📄 SOP 说明
    • 所有用户均可在线查看和下载 SOP 文件
    • 管理员可以上传新的 SOP 文件(支持 Excel 和 Word 格式)
    • 建议为每个 SOP 文件添加清晰的描述说明
    ${filePicker('sop-file','上传 SOP 文件 (Excel/Word)','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,text/csv,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/msword')}
    SOP 文件列表
    `); } Router.register('/upload/sop', async () => { const html = await renderSop(); setTimeout(bindSopEvents,0); setTimeout(loadSopList,0); return html; }); async function loadSopList(){ const listEl = document.getElementById('sop-list'); if(!listEl) return; try { const data = await API.listSopFiles(); if(data.list && data.list.length > 0){ listEl.innerHTML = data.list.map(r=>{ const ts = new Date(r.ts).toLocaleString('zh-CN'); const desc = r.description ? `
    ${r.description}
    ` : ''; const uploader = r.uploader ? `上传者: ${r.uploader}` : ''; return `
  • ${r.original_name}
    ${desc}
    ${uploader} • ${ts}
    下载
  • `; }).join(''); // 绑定删除按钮事件 document.querySelectorAll('.sop-delete').forEach(btn => { addListener(btn, 'click', async (e) => { const id = e.target.dataset.id; if(!confirm('确定要删除这个 SOP 文件吗?')) return; try { await API.deleteSopFile(id); API.toast('删除成功'); loadSopList(); } catch(e) { API.toast('删除失败'); } }); }); } else { listEl.innerHTML = '
  • 暂无 SOP 文件
  • '; } } catch(e) { listEl.innerHTML = '
  • 加载失败
  • '; } } async function bindSopEvents(){ const fileEl = document.getElementById('sop-file'); const btn = document.getElementById('sop-upload'); const descEl = document.getElementById('sop-description'); addListener(btn, 'click', async ()=>{ const file = fileEl.files[0]; if(!file){ API.toast('请选择文件'); return; } const description = descEl ? descEl.value.trim() : ''; // 获取进度显示元素 const progressContainer = document.getElementById('sop-upload-progress'); const progressBar = document.getElementById('sop-progress-bar'); const progressPercent = document.getElementById('sop-progress-percent'); const progressSize = document.getElementById('sop-progress-size'); const progressSpeed = document.getElementById('sop-progress-speed'); let startTime = Date.now(); let lastLoaded = 0; let lastTime = Date.now(); try { btn.disabled = true; // 显示进度条 if(progressContainer) progressContainer.style.display = 'block'; await API.uploadSopFileWithProgress(file, description, (progress) => { const { percent, loaded, total } = progress; // 更新进度条 if(progressBar) progressBar.style.width = percent + '%'; if(progressPercent) progressPercent.textContent = percent + '%'; // 更新大小 const loadedMB = (loaded / 1024 / 1024).toFixed(2); const totalMB = (total / 1024 / 1024).toFixed(2); if(progressSize) progressSize.textContent = `${loadedMB} MB / ${totalMB} MB`; // 计算速度 const now = Date.now(); const timeDiff = (now - lastTime) / 1000; // 秒 if(timeDiff > 0.1) { // 每100ms更新一次速度 const loadedDiff = loaded - lastLoaded; const speed = loadedDiff / timeDiff; // bytes/s let speedText; if(speed > 1024 * 1024) { speedText = (speed / 1024 / 1024).toFixed(2) + ' MB/s'; } else if(speed > 1024) { speedText = (speed / 1024).toFixed(2) + ' KB/s'; } else { speedText = speed.toFixed(0) + ' B/s'; } if(progressSpeed) progressSpeed.textContent = speedText; lastLoaded = loaded; lastTime = now; } }); API.toast('上传成功'); // 清空输入 if(fileEl) fileEl.value = ''; if(descEl) descEl.value = ''; // 隐藏进度条 setTimeout(() => { if(progressContainer) progressContainer.style.display = 'none'; if(progressBar) progressBar.style.width = '0%'; if(progressPercent) progressPercent.textContent = '0%'; if(progressSize) progressSize.textContent = '0 MB / 0 MB'; if(progressSpeed) progressSpeed.textContent = '0 KB/s'; }, 2000); // 刷新列表 loadSopList(); } catch(e) { API.toast('上传失败'); // 隐藏进度条 if(progressContainer) progressContainer.style.display = 'none'; } finally { if(btn) btn.disabled = false; } }); } 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){ const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}; listEl.innerHTML = data.list.map(r=>{ const platformName = platformNames[r.platform] || r.platform || '未知'; return `
  • ${r.mac}${r.batch}${platformName}${new Date(r.ts).toLocaleString('zh-CN')}
  • `; }).join(''); API.toast(`显示全部 ${data.list.length} 条历史记录`); } else { listEl.innerHTML = '
  • 暂无历史记录
  • '; } } } catch(e) { API.toast('加载历史记录失败'); if(listEl) listEl.innerHTML = '
  • 加载失败
  • '; } }); // 清空显示按钮 addListener(clearDisplayBtn, 'click', ()=>{ const listEl = document.getElementById('mac-list'); if(listEl){ listEl.innerHTML = '
  • 已清空显示
  • '; 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 : ''; // 验证是否选择了上传类型 if(!uploadType){ API.toast('请选择上传机种(拼多多/圆通/兔喜)'); return; } // 确认上传类型 const typeNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}; const typeName = typeNames[uploadType] || uploadType; const confirmed = confirm(`请确认上传机种:${typeName}\n\n文件名:${file.name}\n\n确认无误后点击"确定"继续上传`); if(!confirmed){ return; } 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=>`
  • ${r.mac}${r.batch}
  • `).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 = `
  • ${platformText}直通良:${r.fpy_good||0} 良:${r.good} 不良:${r.bad}`; html += `${new Date(r.ts).toLocaleString('zh-CN')}
  • `; return html; }).join(''); API.toast(`显示全部 ${data.list.length} 条历史记录`); } else { listEl.innerHTML = '
  • 暂无历史记录
  • '; } } } catch(e) { API.toast('加载历史记录失败'); if(listEl) listEl.innerHTML = '
  • 加载失败
  • '; } }); // 清空显示按钮 addListener(clearDisplayBtn, 'click', ()=>{ const listEl = document.getElementById('stats-list'); if(listEl){ listEl.innerHTML = '
  • 已清空显示
  • '; 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 = `
  • ${platformName} - 直通良:${fpyGood} 良:${good} 不良:${bad}`; if(details.length > 0){ html += `${details.length}条明细`; } html += '
  • '; 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 clearBtn = document.getElementById('repairs-clear'); const showAllBtn = document.getElementById('repairs-show-all'); const snInput = document.getElementById('repair-sn'); const machineTypeSelect = document.getElementById('repair-machine-type'); const symptomInput = document.getElementById('repair-defect-symptom'); const defectInput = document.getElementById('repair-defect-reason'); const methodInput = document.getElementById('repair-method'); const methodGroup = document.getElementById('repair-method-group'); const imageInput = document.getElementById('repair-images'); const dropzone = document.getElementById('repair-image-dropzone'); const previewContainer = document.getElementById('repair-image-preview'); const snError = document.getElementById('repair-sn-error'); const machineTypeError = document.getElementById('repair-machine-type-error'); const symptomError = document.getElementById('repair-symptom-error'); const defectError = document.getElementById('repair-defect-error'); const methodError = document.getElementById('repair-method-error'); const methodRequired = document.getElementById('repair-method-required'); const imageError = document.getElementById('repair-image-error'); const uploadText = document.getElementById('repairs-upload-text'); const uploadLoading = document.getElementById('repairs-upload-loading'); // 存储待上传的图片文件 let pendingImages = []; // 根据不良原因决定维修方法是否必填 addListener(defectInput, 'input', () => { // 清除错误提示 defectError.style.display = 'none'; methodError.style.display = 'none'; const defectValue = defectInput.value.trim(); // 条件性显示维修方法:无不良时可选,其他原因时必填 if(defectValue) { methodGroup.style.opacity = '1'; methodInput.disabled = false; if(defectValue === '无不良') { // 无不良时维修方法可选 methodRequired.style.display = 'none'; methodInput.placeholder = '请描述维修方法(可选)'; } else { // 其他原因时维修方法必填 methodRequired.style.display = 'inline'; methodInput.placeholder = '请描述维修方法(必填)'; } } else { methodGroup.style.opacity = '0.5'; methodInput.disabled = true; methodRequired.style.display = 'none'; methodInput.placeholder = '请描述维修方法'; } }); // 初始化维修方法状态 methodGroup.style.opacity = '0.5'; methodInput.disabled = true; // SN输入验证 addListener(snInput, 'input', () => { snError.style.display = 'none'; }); // 图片上传处理 const validateAndAddImages = (files) => { const validTypes = ['image/jpeg', 'image/png', 'image/gif']; const maxSize = 10 * 1024 * 1024; // 10MB const maxCount = 10; for(const file of files) { if(pendingImages.length >= maxCount) { imageError.textContent = `最多上传${maxCount}张图片`; imageError.style.display = 'block'; return; } if(!validTypes.includes(file.type)) { imageError.textContent = `不支持的格式: ${file.name},仅支持JPG/PNG/GIF`; imageError.style.display = 'block'; continue; } if(file.size > maxSize) { imageError.textContent = `图片 ${file.name} 超过10MB限制`; imageError.style.display = 'block'; continue; } // 检查是否已存在同名文件 if(pendingImages.some(f => f.name === file.name && f.size === file.size)) { continue; } pendingImages.push(file); imageError.style.display = 'none'; } renderImagePreviews(); }; const renderImagePreviews = () => { previewContainer.innerHTML = pendingImages.map((file, index) => { const url = URL.createObjectURL(file); return `
    `; }).join(''); // 绑定删除按钮事件 previewContainer.querySelectorAll('.remove-image-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const index = parseInt(btn.dataset.index); pendingImages.splice(index, 1); renderImagePreviews(); }; }); }; // 点击上传区域 addListener(dropzone, 'click', () => { imageInput.click(); }); // 文件选择 addListener(imageInput, 'change', () => { if(imageInput.files.length > 0) { validateAndAddImages(Array.from(imageInput.files)); imageInput.value = ''; // 清空以便重复选择同一文件 } }); // 拖拽上传 addListener(dropzone, 'dragover', (e) => { e.preventDefault(); dropzone.classList.add('dragover'); }); addListener(dropzone, 'dragleave', () => { dropzone.classList.remove('dragover'); }); addListener(dropzone, 'drop', (e) => { e.preventDefault(); dropzone.classList.remove('dragover'); if(e.dataTransfer.files.length > 0) { validateAndAddImages(Array.from(e.dataTransfer.files)); } }); // 清空按钮 addListener(clearBtn, 'click', () => { snInput.value = ''; machineTypeSelect.value = ''; defectInput.value = ''; methodInput.value = ''; methodGroup.style.opacity = '0.5'; methodInput.disabled = true; methodRequired.style.display = 'none'; methodInput.placeholder = '请描述维修方法'; pendingImages = []; previewContainer.innerHTML = ''; snError.style.display = 'none'; machineTypeError.style.display = 'none'; defectError.style.display = 'none'; methodError.style.display = 'none'; imageError.style.display = 'none'; API.toast('已清空表单'); }); // 获取当前用户角色 let userRole = window.__userRole || localStorage.getItem('userRole') || ''; let isSuperAdmin = userRole === 'superadmin'; // 如果没有缓存角色,从服务器获取 if (!userRole) { try { const userRes = await fetch('/api/auth/me', { credentials: 'include' }); const userData = await userRes.json(); userRole = userData.role || ''; isSuperAdmin = userRole === 'superadmin'; window.__userRole = userRole; localStorage.setItem('userRole', userRole); } catch (e) { console.error('Failed to get user role:', e); } } // 存储选中的记录ID let selectedIds = new Set(); // 更新选中计数 const updateSelectedCount = () => { const countEl = document.getElementById('repairs-selected-count'); if(countEl) { countEl.textContent = selectedIds.size > 0 ? `已选 ${selectedIds.size} 条` : ''; } }; // 渲染单条记录的HTML const renderRecordItem = (r) => { const ts = new Date(r.ts).toLocaleString('zh-CN'); const imgCount = (r.images || []).length; const isChecked = selectedIds.has(r.id); return `
  • SN: ${r.sn}
    ${ts}
    ${r.tracking_number ? `📦 ${r.tracking_number}` : ''}
    ${r.machine_type ? `${r.machine_type}` : ''} ${r.defect_symptom ? `${r.defect_symptom}` : ''} ${r.defect_reason} ${imgCount > 0 ? `${imgCount}张图片` : ''}
    ${r.repair_method ? `
    维修: ${r.repair_method}
    ` : ''}
    ${isSuperAdmin ? `` : ''}
  • `; }; // 绑定列表项按钮事件 const bindListItemEvents = () => { // 勾选框 document.querySelectorAll('.repair-checkbox').forEach(cb => { cb.onchange = () => { const id = parseInt(cb.dataset.id); if(cb.checked) { selectedIds.add(id); } else { selectedIds.delete(id); } updateSelectedCount(); updateSelectAllState(); }; }); // 查看按钮 document.querySelectorAll('.repair-view-btn').forEach(btn => { btn.onclick = async () => { const id = btn.dataset.id; await showRepairDetailModal(id, false); }; }); // 编辑按钮 document.querySelectorAll('.repair-edit-btn').forEach(btn => { btn.onclick = async () => { const id = btn.dataset.id; await showRepairDetailModal(id, true); }; }); // 删除按钮(仅超级管理员可见) document.querySelectorAll('.repair-delete-btn').forEach(btn => { btn.onclick = async () => { const id = btn.dataset.id; if(confirm('确定要删除这条返修记录吗?此操作不可恢复。')) { try { const result = await API.deleteRepairRecord(id); if(result.ok) { API.toast('删除成功'); selectedIds.delete(parseInt(id)); updateSelectedCount(); refreshRepairsList(); } else { API.toast(result.error || '删除失败'); } } catch(e) { API.toast('删除失败: ' + (e.message || '网络错误')); } } }; }); // 快递单号点击查询物流 document.querySelectorAll('.tracking-link').forEach(link => { link.onclick = async () => { const trackingNumber = link.dataset.tracking; if(!trackingNumber) return; // 显示加载弹窗 const modal = document.createElement('div'); modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000'; modal.innerHTML = `

    📦 物流查询

    正在查询物流信息...
    `; document.body.appendChild(modal); document.getElementById('tracking-modal-close').onclick = () => modal.remove(); modal.onclick = (e) => { if(e.target === modal) modal.remove(); }; try { const result = await API.queryTracking(trackingNumber); const contentEl = document.getElementById('tracking-content'); if(result.ok && result.data && result.data.length > 0) { // 快递公司名称映射 const comNames = { 'shunfeng': '顺丰速运', 'yuantong': '圆通速递', 'zhongtong': '中通快递', 'yunda': '韵达快递', 'jd': '京东物流', 'ems': 'EMS', 'shentong': '申通快递', 'debangkuaidi': '德邦快递', 'auto': '快递' }; const comName = comNames[result.com] || result.com || '快递'; const stateNames = {'0': '在途', '1': '揽收', '2': '疑难', '3': '签收', '4': '退签', '5': '派件', '6': '退回', '7': '转投'}; const stateName = stateNames[result.state] || '未知'; const stateColor = result.state === '3' ? 'var(--success)' : (result.state === '5' ? 'var(--primary)' : 'var(--text-2)'); contentEl.innerHTML = `
    ${comName}
    ${trackingNumber}
    ${stateName}
    ${result.data.map((item, idx) => `
    ${item.time || item.ftime || ''}
    ${item.context || ''}
    `).join('')}
    `; } else { contentEl.innerHTML = `
    📭
    ${result.error || '暂无物流信息'}
    `; } } catch(e) { document.getElementById('tracking-content').innerHTML = `
    查询失败: ${e.message || '网络错误'}
    `; } }; }); }; // 更新全选框状态 const updateSelectAllState = () => { const selectAllCb = document.getElementById('repairs-select-all'); const checkboxes = document.querySelectorAll('.repair-checkbox'); if(selectAllCb && checkboxes.length > 0) { const allChecked = Array.from(checkboxes).every(cb => cb.checked); const someChecked = Array.from(checkboxes).some(cb => cb.checked); selectAllCb.checked = allChecked; selectAllCb.indeterminate = someChecked && !allChecked; } }; // 刷新列表(支持搜索参数) const refreshRepairsList = async (searchSn = null, showAll = false) => { const listEl = document.getElementById('repairs-list'); const infoEl = document.getElementById('repairs-list-info'); try { let params = {}; if(searchSn) { params.sn = searchSn; } else if(!showAll) { params.limit = 10; } const data = await API.listRepairRecords(params); if(listEl && data.list) { if(data.list.length > 0) { listEl.innerHTML = data.list.map(renderRecordItem).join(''); bindListItemEvents(); if(infoEl) { if(searchSn) { infoEl.textContent = `搜索 "${searchSn}" 找到 ${data.list.length} 条记录`; } else if(showAll) { infoEl.textContent = `共 ${data.list.length} 条记录`; } else { infoEl.textContent = `显示最新 ${data.list.length} 条记录`; } } } else { listEl.innerHTML = '
  • 暂无记录
  • '; if(infoEl) { infoEl.textContent = searchSn ? `未找到包含 "${searchSn}" 的记录` : ''; } } } } catch(e) { if(listEl) listEl.innerHTML = '
  • 加载失败
  • '; if(infoEl) infoEl.textContent = ''; } }; // 显示详情/编辑弹窗 const showRepairDetailModal = async (id, isEdit) => { try { const record = await API.getRepairRecord(id); if(record.error) { API.toast(record.error); return; } const modalId = 'repair-detail-modal'; let modal = document.getElementById(modalId); if(modal) modal.remove(); const images = record.images || []; const imagesHtml = images.length > 0 ? `
    ${record.images.map(img => ` `).join('')}
    ` : '
    无图片
    '; const ts = new Date(record.ts).toLocaleString('zh-CN'); const isNoDefect = record.defect_reason === '无不良'; modal = document.createElement('div'); modal.id = modalId; modal.innerHTML = `

    ${isEdit ? '编辑返修记录' : '返修记录详情'}

    ${isEdit ? `
    ${images.map(img => `
    `).join('')}
    点击或拖拽添加新图片
    最多10张图片,点击×删除
    ` : `
    设备SN
    ${record.sn}
    机种
    ${record.machine_type || '未知'}
    不良现象
    ${record.defect_symptom || '未填写'}
    不良原因
    ${record.defect_reason}
    维修方法
    ${record.repair_method || '无'}
    快递单号
    ${record.tracking_number ? `📦 ${record.tracking_number}` : '未填写'}
    上传时间
    ${ts}
    图片
    ${imagesHtml}
    `}
    `; document.body.appendChild(modal); // 绑定弹窗事件 document.getElementById('repair-modal-close').onclick = () => modal.remove(); modal.querySelector('div').onclick = (e) => { if(e.target === modal.querySelector('div')) modal.remove(); }; if(isEdit) { const editDefectInput = document.getElementById('edit-repair-defect'); const editMethodRequired = document.getElementById('edit-method-required'); // 图片管理相关 let keepImages = [...images]; // 要保留的现有图片 let newImageFiles = []; // 新添加的图片文件 // 删除现有图片 document.querySelectorAll('.edit-image-delete').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const filename = btn.dataset.filename; keepImages = keepImages.filter(img => img !== filename); btn.closest('.edit-image-item').remove(); }; }); // 添加新图片 const editDropzone = document.getElementById('edit-image-dropzone'); const editNewImagesInput = document.getElementById('edit-new-images'); const editNewImagesPreview = document.getElementById('edit-new-images-preview'); editDropzone.onclick = () => editNewImagesInput.click(); editDropzone.ondragover = (e) => { e.preventDefault(); editDropzone.style.borderColor = 'var(--primary)'; }; editDropzone.ondragleave = () => { editDropzone.style.borderColor = 'rgba(79,140,255,0.4)'; }; editDropzone.ondrop = (e) => { e.preventDefault(); editDropzone.style.borderColor = 'rgba(79,140,255,0.4)'; handleNewImages(e.dataTransfer.files); }; editNewImagesInput.onchange = () => handleNewImages(editNewImagesInput.files); function handleNewImages(files) { for(const file of files) { if(!file.type.startsWith('image/')) continue; if(keepImages.length + newImageFiles.length >= 10) { API.toast('最多10张图片'); break; } newImageFiles.push(file); // 预览 const reader = new FileReader(); reader.onload = (e) => { const idx = newImageFiles.length - 1; const div = document.createElement('div'); div.className = 'new-image-item'; div.dataset.idx = idx; div.style.cssText = 'position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)'; div.innerHTML = ` `; editNewImagesPreview.appendChild(div); div.querySelector('.new-image-delete').onclick = (ev) => { ev.stopPropagation(); const i = parseInt(div.dataset.idx); newImageFiles[i] = null; div.remove(); }; }; reader.readAsDataURL(file); } } // 不良原因输入变化 editDefectInput.oninput = () => { const defectValue = editDefectInput.value.trim(); editMethodRequired.style.display = defectValue === '无不良' ? 'none' : 'inline'; }; document.getElementById('edit-repair-cancel').onclick = () => modal.remove(); document.getElementById('edit-repair-save').onclick = async () => { const newSn = document.getElementById('edit-repair-sn').value.trim(); const newMachineType = document.getElementById('edit-repair-machine-type').value; const newSymptom = document.getElementById('edit-repair-symptom').value.trim(); const newDefect = editDefectInput.value.trim(); const newMethod = document.getElementById('edit-repair-method').value.trim(); const newTracking = document.getElementById('edit-repair-tracking').value.trim(); // 验证 if(!newSn || newSn.length < 6) { API.toast('SN至少需要6个字符'); return; } if(!newMachineType) { API.toast('请选择机种'); return; } if(!newSymptom) { API.toast('请输入不良现象'); return; } if(!newDefect) { API.toast('请输入不良原因'); return; } if(newDefect !== '无不良' && !newMethod) { API.toast('填写了不良原因时,维修方法为必填项'); return; } try { // 先更新基本信息 const result = await API.updateRepairRecord(id, { sn: newSn, machine_type: newMachineType, defect_symptom: newSymptom, defect_reason: newDefect, repair_method: newMethod, tracking_number: newTracking }); if(!result.ok) { API.toast(result.error || '保存失败'); return; } // 如果图片有变化,更新图片 const actualNewFiles = newImageFiles.filter(f => f !== null); const imagesChanged = keepImages.length !== images.length || actualNewFiles.length > 0; if(imagesChanged) { const formData = new FormData(); keepImages.forEach(img => formData.append('keep_images', img)); actualNewFiles.forEach(file => formData.append('new_images', file)); const imgResult = await API.updateRepairImages(id, formData); if(!imgResult.ok) { API.toast('图片更新失败: ' + (imgResult.error || '未知错误')); return; } } API.toast('保存成功'); modal.remove(); refreshRepairsList(); } catch(e) { API.toast('保存失败: ' + (e.message || '网络错误')); } }; } else { document.getElementById('view-repair-close').onclick = () => modal.remove(); document.getElementById('view-repair-edit').onclick = () => { modal.remove(); showRepairDetailModal(id, true); }; } } catch(e) { API.toast('加载详情失败'); } }; // 搜索框和按钮 const searchInput = document.getElementById('repairs-search-sn'); const searchBtn = document.getElementById('repairs-search-btn'); // 搜索按钮点击 addListener(searchBtn, 'click', async () => { const searchSn = searchInput.value.trim(); if(!searchSn) { API.toast('请输入SN进行搜索'); return; } await refreshRepairsList(searchSn); }); // 搜索框回车 addListener(searchInput, 'keypress', async (e) => { if(e.key === 'Enter') { const searchSn = searchInput.value.trim(); if(searchSn) { await refreshRepairsList(searchSn); } } }); // 查看全部按钮 addListener(showAllBtn, 'click', async () => { searchInput.value = ''; await refreshRepairsList(null, true); API.toast('显示全部记录'); }); // 全选复选框 const selectAllCb = document.getElementById('repairs-select-all'); addListener(selectAllCb, 'change', () => { const checkboxes = document.querySelectorAll('.repair-checkbox'); checkboxes.forEach(cb => { cb.checked = selectAllCb.checked; const id = parseInt(cb.dataset.id); if(selectAllCb.checked) { selectedIds.add(id); } else { selectedIds.delete(id); } }); updateSelectedCount(); }); // 添加快递单号按钮 const trackingBtn = document.getElementById('repairs-tracking-btn'); addListener(trackingBtn, 'click', async () => { if(selectedIds.size === 0) { API.toast('请先勾选要添加快递单号的记录'); return; } // 创建弹窗 const modal = document.createElement('div'); modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000'; modal.innerHTML = `

    添加快递单号

    已选择 ${selectedIds.size} 条记录
    `; document.body.appendChild(modal); document.getElementById('tracking-modal-close').onclick = () => modal.remove(); document.getElementById('tracking-cancel').onclick = () => modal.remove(); document.getElementById('tracking-submit').onclick = async () => { const trackingNumber = document.getElementById('tracking-number-input').value.trim(); if(!trackingNumber) { API.toast('请输入快递单号'); return; } try { const result = await API.batchUpdateTracking(Array.from(selectedIds), trackingNumber); if(result.ok) { API.toast(`成功更新 ${result.updated} 条记录的快递单号`); modal.remove(); refreshRepairsList(); } else { API.toast(result.error || '更新失败'); } } catch(e) { API.toast('更新失败: ' + (e.message || '网络错误')); } }; }); // 导出按钮 const exportBtn = document.getElementById('repairs-export-btn'); addListener(exportBtn, 'click', async () => { if(selectedIds.size === 0) { API.toast('请先勾选要导出的记录'); return; } exportBtn.disabled = true; exportBtn.textContent = '导出中...'; try { const blob = await API.exportRepairRecords(Array.from(selectedIds)); // 下载文件 const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `返修记录_${new Date().toISOString().slice(0,10)}.xlsx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); API.toast(`成功导出 ${selectedIds.size} 条记录`); } catch(e) { API.toast('导出失败: ' + (e.error || e.message || '网络错误')); } finally { exportBtn.disabled = false; exportBtn.textContent = '导出选中'; } }); // 提交按钮 addListener(btn, 'click', async () => { // 表单验证 let hasError = false; // 验证SN const sn = snInput.value.trim(); if(!sn) { snError.textContent = '请输入设备SN'; snError.style.display = 'block'; hasError = true; } else if(sn.length < 6) { snError.textContent = 'SN至少需要6个字符'; snError.style.display = 'block'; hasError = true; } else if(!/^[A-Za-z0-9\-_:]+$/.test(sn)) { snError.textContent = 'SN只能包含字母、数字、横线、下划线或冒号'; snError.style.display = 'block'; hasError = true; } // 验证机种 const machineType = machineTypeSelect.value; if(!machineType) { machineTypeError.textContent = '请选择机种'; machineTypeError.style.display = 'block'; hasError = true; } // 验证不良现象 const defectSymptom = symptomInput.value.trim(); if(!defectSymptom) { symptomError.textContent = '请输入不良现象'; symptomError.style.display = 'block'; hasError = true; } // 验证不良原因 const defectReason = defectInput.value.trim(); if(!defectReason) { defectError.textContent = '请输入不良原因'; defectError.style.display = 'block'; hasError = true; } // 验证维修方法:非"无不良"时必填 const repairMethod = methodInput.value.trim(); if(defectReason && defectReason !== '无不良' && !repairMethod) { methodError.textContent = '选择了不良原因时,维修方法为必填项'; methodError.style.display = 'block'; hasError = true; } if(hasError) return; // 构建FormData const formData = new FormData(); formData.append('sn', sn); formData.append('machine_type', machineType); formData.append('defect_symptom', defectSymptom); formData.append('defect_reason', defectReason); formData.append('repair_method', methodInput.value.trim()); // 添加图片 pendingImages.forEach(file => { formData.append('images', file); }); // 显示加载状态 btn.disabled = true; uploadText.style.display = 'none'; uploadLoading.style.display = 'inline-flex'; try { const result = await API.uploadRepairRecord(formData); if(result.ok) { API.toast('提交成功'); // 清空表单 snInput.value = ''; machineTypeSelect.value = ''; symptomInput.value = ''; defectInput.value = ''; methodInput.value = ''; methodGroup.style.opacity = '0.5'; methodInput.disabled = true; methodRequired.style.display = 'none'; methodInput.placeholder = '请描述维修方法'; pendingImages = []; previewContainer.innerHTML = ''; // 刷新列表 await refreshRepairsList(); } else { API.toast(result.error || '提交失败'); } } catch(e) { API.toast('提交失败: ' + (e.message || '网络错误')); } finally { btn.disabled = false; uploadText.style.display = 'inline'; uploadLoading.style.display = 'none'; } }); // 初始加载最新记录 setTimeout(refreshRepairsList, 100); } 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=>`
  • ${r.mac}${r.batch}
  • `).join('')||'
  • 暂无数据
  • '; }catch(e){ API.toast('上传失败'); } finally { if (btn) btn.disabled = false; } }); } })();