1548 lines
65 KiB
JavaScript
1548 lines
65 KiB
JavaScript
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地址,批次号 例如: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)">×</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;
|
||
}
|
||
});
|
||
}
|
||
})();
|