ERP/frontend/js/components/upload.js

1548 lines
65 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>上传机种 <span style="color:#ff4444">*</span></label>
<select id="mac-type" class="input">
<option value="">请选择上传机种</option>
<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>
`);
}
// 不良原因选项
const defectReasonOptions = [
'无不良',
'外观不良',
'功能异常',
'连接问题',
'电源故障',
'软件问题',
'物理损坏',
'其他'
];
// 机种选项
const machineTypeOptions = [
'拼多多',
'圆通',
'兔喜',
'美团',
'大润发',
'标准版'
];
async function renderRepairs(){
return section('返修记录上传',`
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">
设备SN <span style="color:#ef4444">*</span>
</label>
<input type="text" id="repair-sn" class="input" placeholder="请输入设备序列号" style="width:100%" />
<div id="repair-sn-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">
机种 <span style="color:#ef4444">*</span>
</label>
<select id="repair-machine-type" class="input" style="width:100%">
<option value="">请选择机种</option>
${machineTypeOptions.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
</select>
<div id="repair-machine-type-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">
不良原因 <span style="color:#ef4444">*</span>
</label>
<select id="repair-defect-reason" class="input" style="width:100%">
<option value="">请选择不良原因</option>
${defectReasonOptions.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
</select>
<input type="text" id="repair-defect-reason-other" class="input" placeholder="请输入其他原因" style="width:100%;margin-top:8px;display:none" />
<div id="repair-defect-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
</div>
<div class="form-group" style="margin-bottom:16px" id="repair-method-group">
<label style="display:block;font-weight:600;margin-bottom:6px">
维修方法 <span id="repair-method-required" style="color:#ef4444;display:none">*</span>
</label>
<textarea id="repair-method" class="input" placeholder="请描述维修方法" rows="3" style="width:100%;resize:vertical"></textarea>
<div id="repair-method-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">图片上传</label>
<div style="background:rgba(79,140,255,0.08);border:1px dashed rgba(79,140,255,0.4);border-radius:8px;padding:16px;text-align:center;cursor:pointer;transition:all 0.2s" id="repair-image-dropzone">
<input type="file" id="repair-images" multiple accept="image/jpeg,image/png,image/gif" style="display:none" />
<div style="color:var(--primary);margin-bottom:8px">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
</div>
<div style="font-size:14px;color:var(--text-1)">点击或拖拽图片到此处上传</div>
<div style="font-size:12px;color:var(--text-2);margin-top:4px">支持 JPG/PNG/GIF单张最大10MB最多10张</div>
</div>
<div id="repair-image-preview" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:12px"></div>
<div id="repair-image-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
</div>
<div class="actions" style="display:flex;gap:12px">
<button class="btn" id="repairs-upload" style="flex:1">
<span id="repairs-upload-text">提交</span>
<span id="repairs-upload-loading" style="display:none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin 1s linear infinite">
<circle cx="12" cy="12" r="10" stroke-opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke-opacity="1"/>
</svg>
提交中...
</span>
</button>
<button class="btn btn-secondary" id="repairs-clear" style="flex:0 0 auto">清空</button>
</div>
<style>
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
#repair-image-dropzone:hover { background:rgba(79,140,255,0.12); border-color:var(--primary); }
#repair-image-dropzone.dragover { background:rgba(79,140,255,0.15); border-color:var(--primary); border-style:solid; }
</style>
<div style="margin-top:20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:8px">
<span style="font-weight:600">历史记录</span>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" id="repairs-search-sn" class="input" placeholder="输入SN搜索" style="width:150px;font-size:12px;padding:4px 8px" />
<button class="btn btn-secondary" id="repairs-search-btn" style="font-size:12px;padding:4px 8px">搜索</button>
<button class="btn btn-secondary" id="repairs-show-all" style="font-size:12px;padding:4px 8px">全部</button>
<button class="btn btn-secondary" id="repairs-export-btn" style="font-size:12px;padding:4px 8px">导出选中</button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div id="repairs-list-info" style="font-size:12px;color:var(--text-2)"></div>
<div style="display:flex;gap:8px;align-items:center">
<label style="font-size:12px;color:var(--text-2);cursor:pointer;display:flex;align-items:center;gap:4px">
<input type="checkbox" id="repairs-select-all" style="cursor:pointer" />
全选
</label>
<span id="repairs-selected-count" style="font-size:12px;color:var(--primary)"></span>
</div>
</div>
<ul id="repairs-list" class="list" style="max-height:500px;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){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
listEl.innerHTML = data.list.slice(0, 10).map(r=>{
const platformName = platformNames[r.platform] || r.platform || '未知';
return `<li><span>${r.mac}</span><span class="badge">${r.batch}</span><span class="badge" style="background:var(--primary-light);color:var(--primary)">${platformName}</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;
});
async function renderSop(){
return section('SOP 文件管理',`
<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)">📄 SOP 说明</div>
<div style="color:var(--text)">
<div>• 所有用户均可在线查看和下载 SOP 文件</div>
<div>• 管理员可以上传新的 SOP 文件(支持 Excel 和 Word 格式)</div>
<div>• 建议为每个 SOP 文件添加清晰的描述说明</div>
</div>
</div>
${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')}
<div class="field">
<label>文件描述(可选)</label>
<textarea id="sop-description" class="input" rows="2" placeholder="例如:拼多多生产流程 SOP v1.0"></textarea>
</div>
<div class="actions"><button class="btn" id="sop-upload">上传</button></div>
<div id="sop-upload-progress" style="display:none;margin-top:12px;padding:12px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span style="font-weight:500">上传进度</span>
<span id="sop-progress-percent">0%</span>
</div>
<div style="width:100%;height:8px;background:var(--bg);border-radius:4px;overflow:hidden">
<div id="sop-progress-bar" style="width:0%;height:100%;background:var(--primary);transition:width 0.3s"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:8px;font-size:12px;color:var(--text-2)">
<span id="sop-progress-size">0 MB / 0 MB</span>
<span id="sop-progress-speed">0 KB/s</span>
</div>
</div>
<div style="margin-top:12px">
<div style="font-weight:600;margin-bottom:8px">SOP 文件列表</div>
<ul id="sop-list" class="list" style="max-height:400px;overflow-y:auto"></ul>
</div>
`);
}
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 ? `<div style="font-size:11px;color:var(--text-2);margin-top:2px">${r.description}</div>` : '';
const uploader = r.uploader ? `<span style="font-size:11px;color:var(--text-2)">上传者: ${r.uploader}</span>` : '';
return `<li style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px">
<div style="flex:1">
<div style="font-weight:500">${r.original_name}</div>
${desc}
<div style="font-size:11px;color:var(--text-2);margin-top:4px">${uploader}${ts}</div>
</div>
<div style="display:flex;gap:8px">
<a href="/api/sop/download/${r.id}" class="btn btn-secondary" style="font-size:12px;padding:4px 12px;text-decoration:none" download>下载</a>
<button class="btn btn-secondary sop-delete" data-id="${r.id}" style="font-size:12px;padding:4px 12px;background:var(--danger);color:white">删除</button>
</div>
</li>`;
}).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 = '<li>暂无 SOP 文件</li>';
}
} catch(e) {
listEl.innerHTML = '<li>加载失败</li>';
}
}
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 `<li><span>${r.mac}</span><span class="badge">${r.batch}</span><span class="badge" style="background:var(--primary-light);color:var(--primary)">${platformName}</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 : '';
// 验证是否选择了上传类型
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=>`<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 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 defectSelect = document.getElementById('repair-defect-reason');
const defectOtherInput = document.getElementById('repair-defect-reason-other');
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 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(defectSelect, 'change', () => {
if(defectSelect.value === '其他') {
defectOtherInput.style.display = 'block';
defectOtherInput.focus();
} else {
defectOtherInput.style.display = 'none';
defectOtherInput.value = '';
}
// 清除错误提示
defectError.style.display = 'none';
methodError.style.display = 'none';
// 条件性显示维修方法:无不良时可选,其他原因时必填
if(defectSelect.value) {
methodGroup.style.opacity = '1';
methodInput.disabled = false;
if(defectSelect.value === '无不良') {
// 无不良时维修方法可选
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 `
<div style="position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
<img src="${url}" style="width:100%;height:100%;object-fit:cover" />
<button type="button" data-index="${index}" class="remove-image-btn" style="position:absolute;top:2px;right:2px;width:20px;height:20px;border-radius:50%;background:rgba(0,0,0,0.6);color:#fff;border:none;cursor:pointer;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center">×</button>
</div>
`;
}).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 = '';
defectSelect.value = '';
defectOtherInput.value = '';
defectOtherInput.style.display = 'none';
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 `
<li style="display:flex;gap:10px;padding:10px 0;border-bottom:1px solid var(--border)" data-id="${r.id}">
<div style="flex:0 0 auto;padding-top:2px">
<input type="checkbox" class="repair-checkbox" data-id="${r.id}" ${isChecked ? 'checked' : ''} style="cursor:pointer;width:16px;height:16px" />
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:4px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-weight:600">SN: ${r.sn}</span>
<span style="font-size:11px;color:var(--text-2)">${ts}</span>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
${r.machine_type ? `<span class="badge" style="background:var(--primary-bg);color:var(--primary)">${r.machine_type}</span>` : ''}
<span class="badge" style="background:${r.defect_reason === '无不良' ? 'var(--success-bg)' : 'var(--danger-bg)'};color:${r.defect_reason === '无不良' ? 'var(--success)' : 'var(--danger)'}">${r.defect_reason}</span>
${imgCount > 0 ? `<span class="badge">${imgCount}张图片</span>` : ''}
</div>
${r.repair_method ? `<div style="font-size:12px;color:var(--text-2)">维修: ${r.repair_method}</div>` : ''}
<div style="display:flex;gap:8px;margin-top:4px">
<button class="btn btn-secondary repair-view-btn" data-id="${r.id}" style="font-size:11px;padding:2px 8px">查看</button>
<button class="btn btn-secondary repair-edit-btn" data-id="${r.id}" style="font-size:11px;padding:2px 8px">编辑</button>
${isSuperAdmin ? `<button class="btn btn-secondary repair-delete-btn" data-id="${r.id}" style="font-size:11px;padding:2px 8px;color:var(--danger)">删除</button>` : ''}
</div>
</div>
</li>
`;
};
// 绑定列表项按钮事件
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 || '网络错误'));
}
}
};
});
};
// 更新全选框状态
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 = '<li>暂无记录</li>';
if(infoEl) {
infoEl.textContent = searchSn ? `未找到包含 "${searchSn}" 的记录` : '';
}
}
}
} catch(e) {
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
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 imagesHtml = (record.images || []).length > 0
? `<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
${record.images.map(img => `
<a href="/api/repair-image/${img}" target="_blank" style="display:block;width:100px;height:100px;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
<img src="/api/repair-image/${img}" style="width:100%;height:100%;object-fit:cover" />
</a>
`).join('')}
</div>`
: '<div style="color:var(--text-2);font-size:12px">无图片</div>';
const ts = new Date(record.ts).toLocaleString('zh-CN');
const isNoDefect = record.defect_reason === '无不良';
modal = document.createElement('div');
modal.id = modalId;
modal.innerHTML = `
<div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px">
<div style="background:var(--bg);border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 40px rgba(0,0,0,0.2)">
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<h3 style="margin:0;font-size:16px">${isEdit ? '编辑返修记录' : '返修记录详情'}</h3>
<button id="repair-modal-close" style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)">&times;</button>
</div>
<div style="padding:20px">
${isEdit ? `
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">设备SN <span style="color:#ef4444">*</span></label>
<input type="text" id="edit-repair-sn" class="input" value="${record.sn}" style="width:100%" />
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">机种 <span style="color:#ef4444">*</span></label>
<select id="edit-repair-machine-type" class="input" style="width:100%">
${machineTypeOptions.map(opt => `<option value="${opt}" ${opt === record.machine_type ? 'selected' : ''}>${opt}</option>`).join('')}
</select>
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">不良原因 <span style="color:#ef4444">*</span></label>
<select id="edit-repair-defect" class="input" style="width:100%">
${defectReasonOptions.map(opt => `<option value="${opt}" ${opt === record.defect_reason ? 'selected' : ''}>${opt}</option>`).join('')}
</select>
<input type="text" id="edit-repair-defect-other" class="input" placeholder="请输入其他原因" style="width:100%;margin-top:8px;display:${record.defect_reason === '其他' || !defectReasonOptions.includes(record.defect_reason) ? 'block' : 'none'}" value="${!defectReasonOptions.includes(record.defect_reason) ? record.defect_reason : ''}" />
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">维修方法 <span id="edit-method-required" style="color:#ef4444;display:${isNoDefect ? 'none' : 'inline'}">*</span></label>
<textarea id="edit-repair-method" class="input" rows="3" style="width:100%;resize:vertical">${record.repair_method || ''}</textarea>
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">图片</label>
${imagesHtml}
<div style="font-size:11px;color:var(--text-2);margin-top:4px">编辑模式暂不支持修改图片</div>
</div>
<div style="display:flex;gap:12px">
<button class="btn" id="edit-repair-save" style="flex:1">保存</button>
<button class="btn btn-secondary" id="edit-repair-cancel">取消</button>
</div>
` : `
<div style="margin-bottom:12px">
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">设备SN</div>
<div style="font-size:14px">${record.sn}</div>
</div>
<div style="margin-bottom:12px">
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">机种</div>
<span class="badge" style="background:var(--primary-bg);color:var(--primary)">${record.machine_type || '未知'}</span>
</div>
<div style="margin-bottom:12px">
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">不良原因</div>
<span class="badge" style="background:${isNoDefect ? 'var(--success-bg)' : 'var(--danger-bg)'};color:${isNoDefect ? 'var(--success)' : 'var(--danger)'}">${record.defect_reason}</span>
</div>
<div style="margin-bottom:12px">
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">维修方法</div>
<div style="font-size:14px">${record.repair_method || '无'}</div>
</div>
<div style="margin-bottom:12px">
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">上传时间</div>
<div style="font-size:14px">${ts}</div>
</div>
<div style="margin-bottom:12px">
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">图片</div>
${imagesHtml}
</div>
<div style="display:flex;gap:12px;margin-top:16px">
<button class="btn btn-secondary" id="view-repair-edit" style="flex:1">编辑</button>
<button class="btn btn-secondary" id="view-repair-close">关闭</button>
</div>
`}
</div>
</div>
</div>
`;
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 editDefectSelect = document.getElementById('edit-repair-defect');
const editDefectOther = document.getElementById('edit-repair-defect-other');
const editMethodRequired = document.getElementById('edit-method-required');
// 不良原因选择变化
editDefectSelect.onchange = () => {
if(editDefectSelect.value === '其他') {
editDefectOther.style.display = 'block';
} else {
editDefectOther.style.display = 'none';
}
editMethodRequired.style.display = editDefectSelect.value === '无不良' ? '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;
let newDefect = editDefectSelect.value;
if(newDefect === '其他') {
newDefect = editDefectOther.value.trim();
}
const newMethod = document.getElementById('edit-repair-method').value.trim();
// 验证
if(!newSn || newSn.length < 6) {
API.toast('SN至少需要6个字符');
return;
}
if(!newMachineType) {
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_reason: newDefect,
repair_method: newMethod
});
if(result.ok) {
API.toast('保存成功');
modal.remove();
refreshRepairsList();
} else {
API.toast(result.error || '保存失败');
}
} 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 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;
}
// 验证不良原因
let defectReason = defectSelect.value;
if(defectReason === '其他') {
defectReason = defectOtherInput.value.trim();
if(!defectReason) {
defectError.textContent = '请输入其他原因';
defectError.style.display = 'block';
hasError = true;
}
} else 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_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 = '';
defectSelect.value = '';
defectOtherInput.value = '';
defectOtherInput.style.display = 'none';
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=>`<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;
}
});
}
})();