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 `
`;
}
function filePicker(id,label,accept){
return `${label}
`;
}
function numberInput(id,label){return `${label}
`}
function textarea(id,label,placeholder=''){return `${label}
`}
async function renderMac(){
return section('MAC与批次(MAC与批次对应关系表)',`
上传机种 *
请选择上传机种
拼多多
圆通
兔喜
${filePicker('mac-file','批量导入(Excel)','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel')}
上传
`);
}
async function renderStats(){
return section('良/不良统计',`
📊 数据说明
直通良品数: 一次检测就通过的产品数量
良品数: 最终通过检测的产品总数(包含直通良品 + 返修后通过的产品)
不良品数: 最终未通过检测的产品数量(报废或待返修)
💡 计算公式:
直通良品率 = 直通良品数 / (良品数 + 不良品数) × 100%
总良品率 = 良品数 / (良品数 + 不良品数) × 100%
平台类型
拼多多
圆通
兔喜
${numberInput('fpy-good-count','直通良品数量(一次检测通过)')}
${numberInput('good-count','良品数量(最终通过检测的总数)')}
${numberInput('bad-count','不良品数量(最终未通过)')}
不良明细(可选)
上传
`);
}
// 不良原因选项
const defectReasonOptions = [
'无不良',
'外观不良',
'功能异常',
'连接问题',
'电源故障',
'软件问题',
'物理损坏',
'其他'
];
// 机种选项
const machineTypeOptions = [
'拼多多',
'圆通',
'兔喜',
'美团',
'大润发',
'标准版'
];
async function renderRepairs(){
return section('返修记录上传',`
`);
}
async function renderDefects(){
return section('不良明细',`
${filePicker('defects-file','批量导入不良MAC与批次(Excel/CSV)','text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel')}
${textarea('defects-manual','手动输入','AA:BB:...,BATCH-xyz; ...')}
上传
`);
}
Router.register('/upload/mac', async () => {
const html = await renderMac();
setTimeout(bindMacEvents,0);
setTimeout(async ()=>{
// 从服务器获取最新10条记录
const listEl=document.getElementById('mac-list');
try {
const data = await API.listMac();
if(listEl && data.list && data.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
listEl.innerHTML = data.list.slice(0, 10).map(r=>{
const platformName = platformNames[r.platform] || r.platform || '未知';
return `${r.mac} ${r.batch} ${platformName} `;
}).join('');
} else if(listEl) {
listEl.innerHTML = '暂无数据 ';
}
} catch(e) {
if(listEl) listEl.innerHTML = '加载失败 ';
}
},0);
return html;
});
Router.register('/upload/stats', async () => {
const html = await renderStats();
setTimeout(bindStatsEvents,0);
setTimeout(async ()=>{
// 从服务器获取最新10条记录
const listEl=document.getElementById('stats-list');
try {
const data = await API.listStats();
if(listEl && data.list && data.list.length > 0){
listEl.innerHTML = data.list.slice(0, 10).map(r=>{
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[r.platform] || '';
const platformText = platformName ? `${platformName} - ` : '';
let html = `${platformText}直通良:${r.fpy_good||0} 良:${r.good} 不良:${r.bad} `;
html += ' ';
return html;
}).join('');
} else if(listEl) {
listEl.innerHTML = '暂无数据 ';
}
} catch(e) {
console.error('加载历史记录失败:', e);
if(listEl) listEl.innerHTML = '加载失败 ';
}
},0);
return html;
});
Router.register('/upload/repairs', async () => {
const html = await renderRepairs();
setTimeout(bindRepairsEvents,0);
setTimeout(async ()=>{
const listEl=document.getElementById('repairs-list');
const data=await API.listRepairs().catch(()=>({list:[]}));
listEl.innerHTML=(data.list||[]).slice(0,10).map(r=>{
const ts = new Date(r.ts).toLocaleString('zh-CN');
return `数量: ${r.qty} ${r.note||'无备注'} ${ts} `;
}).join('')||'暂无数据 ';
},0);
return html;
});
Router.register('/upload/defects', async () => {
const html = await renderDefects();
setTimeout(bindDefectsEvents,0);
setTimeout(async ()=>{
const listEl=document.getElementById('defects-list');
const data=await API.listDefects().catch(()=>({list:[]}));
listEl.innerHTML=(data.list||[]).slice(0,10).map(r=>`${r.mac} ${r.batch} `).join('')||'暂无数据 ';
},0);
return html;
});
async function renderSop(){
return section('SOP 文件管理',`
📄 SOP 说明
• 所有用户均可在线查看和下载 SOP 文件
• 管理员可以上传新的 SOP 文件(支持 Excel 和 Word 格式)
• 建议为每个 SOP 文件添加清晰的描述说明
${filePicker('sop-file','上传 SOP 文件 (Excel/Word)','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,text/csv,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/msword')}
文件描述(可选)
上传
上传进度
0%
0 MB / 0 MB
0 KB/s
`);
}
Router.register('/upload/sop', async () => {
const html = await renderSop();
setTimeout(bindSopEvents,0);
setTimeout(loadSopList,0);
return html;
});
async function loadSopList(){
const listEl = document.getElementById('sop-list');
if(!listEl) return;
try {
const data = await API.listSopFiles();
if(data.list && data.list.length > 0){
listEl.innerHTML = data.list.map(r=>{
const ts = new Date(r.ts).toLocaleString('zh-CN');
const desc = r.description ? `${r.description}
` : '';
const uploader = r.uploader ? `上传者: ${r.uploader} ` : '';
return `
${r.original_name}
${desc}
${uploader} • ${ts}
`;
}).join('');
// 绑定删除按钮事件
document.querySelectorAll('.sop-delete').forEach(btn => {
addListener(btn, 'click', async (e) => {
const id = e.target.dataset.id;
if(!confirm('确定要删除这个 SOP 文件吗?')) return;
try {
await API.deleteSopFile(id);
API.toast('删除成功');
loadSopList();
} catch(e) {
API.toast('删除失败');
}
});
});
} else {
listEl.innerHTML = '暂无 SOP 文件 ';
}
} catch(e) {
listEl.innerHTML = '加载失败 ';
}
}
async function bindSopEvents(){
const fileEl = document.getElementById('sop-file');
const btn = document.getElementById('sop-upload');
const descEl = document.getElementById('sop-description');
addListener(btn, 'click', async ()=>{
const file = fileEl.files[0];
if(!file){
API.toast('请选择文件');
return;
}
const description = descEl ? descEl.value.trim() : '';
// 获取进度显示元素
const progressContainer = document.getElementById('sop-upload-progress');
const progressBar = document.getElementById('sop-progress-bar');
const progressPercent = document.getElementById('sop-progress-percent');
const progressSize = document.getElementById('sop-progress-size');
const progressSpeed = document.getElementById('sop-progress-speed');
let startTime = Date.now();
let lastLoaded = 0;
let lastTime = Date.now();
try {
btn.disabled = true;
// 显示进度条
if(progressContainer) progressContainer.style.display = 'block';
await API.uploadSopFileWithProgress(file, description, (progress) => {
const { percent, loaded, total } = progress;
// 更新进度条
if(progressBar) progressBar.style.width = percent + '%';
if(progressPercent) progressPercent.textContent = percent + '%';
// 更新大小
const loadedMB = (loaded / 1024 / 1024).toFixed(2);
const totalMB = (total / 1024 / 1024).toFixed(2);
if(progressSize) progressSize.textContent = `${loadedMB} MB / ${totalMB} MB`;
// 计算速度
const now = Date.now();
const timeDiff = (now - lastTime) / 1000; // 秒
if(timeDiff > 0.1) { // 每100ms更新一次速度
const loadedDiff = loaded - lastLoaded;
const speed = loadedDiff / timeDiff; // bytes/s
let speedText;
if(speed > 1024 * 1024) {
speedText = (speed / 1024 / 1024).toFixed(2) + ' MB/s';
} else if(speed > 1024) {
speedText = (speed / 1024).toFixed(2) + ' KB/s';
} else {
speedText = speed.toFixed(0) + ' B/s';
}
if(progressSpeed) progressSpeed.textContent = speedText;
lastLoaded = loaded;
lastTime = now;
}
});
API.toast('上传成功');
// 清空输入
if(fileEl) fileEl.value = '';
if(descEl) descEl.value = '';
// 隐藏进度条
setTimeout(() => {
if(progressContainer) progressContainer.style.display = 'none';
if(progressBar) progressBar.style.width = '0%';
if(progressPercent) progressPercent.textContent = '0%';
if(progressSize) progressSize.textContent = '0 MB / 0 MB';
if(progressSpeed) progressSpeed.textContent = '0 KB/s';
}, 2000);
// 刷新列表
loadSopList();
} catch(e) {
API.toast('上传失败');
// 隐藏进度条
if(progressContainer) progressContainer.style.display = 'none';
} finally {
if(btn) btn.disabled = false;
}
});
}
function readText(file){return new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=reject;r.readAsText(file)})}
function parseManual(text){
return text.split(/\n+/).map(l=>l.trim()).filter(Boolean).map(l=>{
const [mac,batch]=l.split(',');
return { mac, batch };
});
}
async function bindMacEvents(){
const fileEl=document.getElementById('mac-file');
const btn = document.getElementById('mac-upload');
const showHistoryBtn = document.getElementById('mac-show-history');
const clearDisplayBtn = document.getElementById('mac-clear-display');
// 文件选择后立即验证
addListener(fileEl, 'change', async ()=>{
const file = fileEl.files[0];
if(!file) return;
try{
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/validate/mac-file', {
method: 'POST',
body: formData
});
const result = await res.json();
if(!result.valid){
API.toast(result.message || '文件格式不正确');
fileEl.value = '';
return;
}
API.toast(result.message || '文件验证通过');
}catch(e){
API.toast('文件验证失败');
fileEl.value = '';
}
});
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener(showHistoryBtn, 'click', async ()=>{
const listEl = document.getElementById('mac-list');
try {
const data = await API.listMac();
if(listEl){
if(data.list && data.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
listEl.innerHTML = data.list.map(r=>{
const platformName = platformNames[r.platform] || r.platform || '未知';
return `${r.mac} ${r.batch} ${platformName} ${new Date(r.ts).toLocaleString('zh-CN')} `;
}).join('');
API.toast(`显示全部 ${data.list.length} 条历史记录`);
} else {
listEl.innerHTML = '暂无历史记录 ';
}
}
} catch(e) {
API.toast('加载历史记录失败');
if(listEl) listEl.innerHTML = '加载失败 ';
}
});
// 清空显示按钮
addListener(clearDisplayBtn, 'click', ()=>{
const listEl = document.getElementById('mac-list');
if(listEl){
listEl.innerHTML = '已清空显示 ';
API.toast('已清空显示(历史记录仍保留)');
}
});
addListener(btn, 'click', async ()=>{
const file = fileEl.files[0];
if(!file){
API.toast('请选择文件');
return;
}
const typeEl = document.getElementById('mac-type');
const uploadType = typeEl ? typeEl.value : '';
// 验证是否选择了上传类型
if(!uploadType){
API.toast('请选择上传机种(拼多多/圆通/兔喜)');
return;
}
// 确认上传类型
const typeNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
const typeName = typeNames[uploadType] || uploadType;
const confirmed = confirm(`请确认上传机种:${typeName}\n\n文件名:${file.name}\n\n确认无误后点击"确定"继续上传`);
if(!confirmed){
return;
}
const logContainer = document.getElementById('upload-log');
const logPre = logContainer ? logContainer.querySelector('pre') : null;
try{
btn.disabled = true;
if(logContainer) logContainer.style.display = 'block';
if(logPre) logPre.textContent = '正在上传文件...\n';
const formData = new FormData();
formData.append('file', file);
formData.append('type', uploadType);
const res = await fetch('/api/upload/mac-file', {
method: 'POST',
body: formData
});
const result = await res.json();
if(logPre){
logPre.textContent = result.output || '上传完成';
}
if(result.ok){
API.toast('上传成功');
// 解析并显示成功上传的记录
const output = result.output || '';
const jsonMatch = output.match(/=== 成功导入的数据 ===\n([\s\S]*?)\n=== 数据输出结束 ===/);
if(jsonMatch && jsonMatch[1]){
try{
const records = JSON.parse(jsonMatch[1].trim());
if(records.length > 0){
// 保存到历史记录
saveToHistory(records);
// 显示最新记录
const listEl = document.getElementById('mac-list');
if(listEl){
listEl.innerHTML = records.map(r=>`${r.mac} ${r.batch} `).join('');
}
}
}catch(e){
console.error('解析上传记录失败:', e);
}
}
} else {
API.toast(result.error || '上传失败');
}
if(fileEl) fileEl.value = '';
}catch(e){
API.toast('上传失败: ' + e.message);
if(logPre) logPre.textContent += '\n错误: ' + e.message;
} finally {
if (btn) btn.disabled = false;
}
});
}
async function bindStatsEvents(){
const STATS_STORAGE_KEY = 'stats_upload_history';
const getStatsHistory = () => {
try {
return JSON.parse(localStorage.getItem(STATS_STORAGE_KEY) || '[]');
} catch {
return [];
}
};
const saveToStatsHistory = (record) => {
try {
const history = getStatsHistory();
const newRecord = {
...record,
timestamp: new Date().toISOString()
};
const updated = [newRecord, ...history].slice(0, 100);
localStorage.setItem(STATS_STORAGE_KEY, JSON.stringify(updated));
} catch (e) {
console.error('保存历史记录失败:', e);
}
};
const btn = document.getElementById('stats-upload');
const showHistoryBtn = document.getElementById('stats-show-history');
const clearDisplayBtn = document.getElementById('stats-clear-display');
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener(showHistoryBtn, 'click', async ()=>{
const listEl = document.getElementById('stats-list');
try {
const data = await API.listStats();
if(listEl){
if(data.list && data.list.length > 0){
listEl.innerHTML = data.list.map(r=>{
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[r.platform] || '';
const platformText = platformName ? `${platformName} - ` : '';
let html = `${platformText}直通良:${r.fpy_good||0} 良:${r.good} 不良:${r.bad} `;
html += `${new Date(r.ts).toLocaleString('zh-CN')} `;
return html;
}).join('');
API.toast(`显示全部 ${data.list.length} 条历史记录`);
} else {
listEl.innerHTML = '暂无历史记录 ';
}
}
} catch(e) {
API.toast('加载历史记录失败');
if(listEl) listEl.innerHTML = '加载失败 ';
}
});
// 清空显示按钮
addListener(clearDisplayBtn, 'click', ()=>{
const listEl = document.getElementById('stats-list');
if(listEl){
listEl.innerHTML = '已清空显示 ';
API.toast('已清空显示(历史记录仍保留)');
}
});
addListener(btn, 'click', async ()=>{
const platform = document.getElementById('stats-platform').value;
const fpyGood=parseInt(document.getElementById('fpy-good-count').value||'0',10);
const good=parseInt(document.getElementById('good-count').value||'0',10);
const bad=parseInt(document.getElementById('bad-count').value||'0',10);
const detailsText = document.getElementById('bad-details')?.value.trim() || '';
if(fpyGood<0||good<0||bad<0){return API.toast('数量不能为负数')}
// 解析不良明细
const details = [];
if(detailsText){
const lines = detailsText.split('\n').filter(l => l.trim());
for(const line of lines){
const [mac, batch] = line.split(',').map(s => s.trim());
if(mac && batch){
details.push({mac, batch});
}
}
}
btn.disabled = true;
try{
await API.uploadStats({platform, fpy_good: fpyGood, good, bad, details});
API.toast('上传成功');
// 保存到历史记录
saveToStatsHistory({platform, fpy_good: fpyGood, good, bad, details});
// 显示最新记录
const listEl=document.getElementById('stats-list');
if(listEl){
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[platform] || platform;
let html = `${platformName} - 直通良:${fpyGood} 良:${good} 不良:${bad} `;
if(details.length > 0){
html += `${details.length}条明细 `;
}
html += ' ';
listEl.innerHTML = html;
}
// 清空输入
document.getElementById('fpy-good-count').value = '';
document.getElementById('good-count').value = '';
document.getElementById('bad-count').value = '';
document.getElementById('bad-details').value = '';
}catch(e){
API.toast('上传失败');
} finally {
if (btn) btn.disabled = false;
}
});
}
async function bindRepairsEvents(){
const btn = document.getElementById('repairs-upload');
const clearBtn = document.getElementById('repairs-clear');
const showAllBtn = document.getElementById('repairs-show-all');
const snInput = document.getElementById('repair-sn');
const machineTypeSelect = document.getElementById('repair-machine-type');
const symptomInput = document.getElementById('repair-defect-symptom');
const defectInput = document.getElementById('repair-defect-reason');
const methodInput = document.getElementById('repair-method');
const methodGroup = document.getElementById('repair-method-group');
const imageInput = document.getElementById('repair-images');
const dropzone = document.getElementById('repair-image-dropzone');
const previewContainer = document.getElementById('repair-image-preview');
const snError = document.getElementById('repair-sn-error');
const machineTypeError = document.getElementById('repair-machine-type-error');
const symptomError = document.getElementById('repair-symptom-error');
const defectError = document.getElementById('repair-defect-error');
const methodError = document.getElementById('repair-method-error');
const methodRequired = document.getElementById('repair-method-required');
const imageError = document.getElementById('repair-image-error');
const uploadText = document.getElementById('repairs-upload-text');
const uploadLoading = document.getElementById('repairs-upload-loading');
// 存储待上传的图片文件
let pendingImages = [];
// 根据不良原因决定维修方法是否必填
addListener(defectInput, 'input', () => {
// 清除错误提示
defectError.style.display = 'none';
methodError.style.display = 'none';
const defectValue = defectInput.value.trim();
// 条件性显示维修方法:无不良时可选,其他原因时必填
if(defectValue) {
methodGroup.style.opacity = '1';
methodInput.disabled = false;
if(defectValue === '无不良') {
// 无不良时维修方法可选
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法(可选)';
} else {
// 其他原因时维修方法必填
methodRequired.style.display = 'inline';
methodInput.placeholder = '请描述维修方法(必填)';
}
} else {
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
}
});
// 初始化维修方法状态
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
// SN输入验证
addListener(snInput, 'input', () => {
snError.style.display = 'none';
});
// 图片上传处理
const validateAndAddImages = (files) => {
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
const maxSize = 10 * 1024 * 1024; // 10MB
const maxCount = 10;
for(const file of files) {
if(pendingImages.length >= maxCount) {
imageError.textContent = `最多上传${maxCount}张图片`;
imageError.style.display = 'block';
return;
}
if(!validTypes.includes(file.type)) {
imageError.textContent = `不支持的格式: ${file.name},仅支持JPG/PNG/GIF`;
imageError.style.display = 'block';
continue;
}
if(file.size > maxSize) {
imageError.textContent = `图片 ${file.name} 超过10MB限制`;
imageError.style.display = 'block';
continue;
}
// 检查是否已存在同名文件
if(pendingImages.some(f => f.name === file.name && f.size === file.size)) {
continue;
}
pendingImages.push(file);
imageError.style.display = 'none';
}
renderImagePreviews();
};
const renderImagePreviews = () => {
previewContainer.innerHTML = pendingImages.map((file, index) => {
const url = URL.createObjectURL(file);
return `
×
`;
}).join('');
// 绑定删除按钮事件
previewContainer.querySelectorAll('.remove-image-btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
const index = parseInt(btn.dataset.index);
pendingImages.splice(index, 1);
renderImagePreviews();
};
});
};
// 点击上传区域
addListener(dropzone, 'click', () => {
imageInput.click();
});
// 文件选择
addListener(imageInput, 'change', () => {
if(imageInput.files.length > 0) {
validateAndAddImages(Array.from(imageInput.files));
imageInput.value = ''; // 清空以便重复选择同一文件
}
});
// 拖拽上传
addListener(dropzone, 'dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
addListener(dropzone, 'dragleave', () => {
dropzone.classList.remove('dragover');
});
addListener(dropzone, 'drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
if(e.dataTransfer.files.length > 0) {
validateAndAddImages(Array.from(e.dataTransfer.files));
}
});
// 清空按钮
addListener(clearBtn, 'click', () => {
snInput.value = '';
machineTypeSelect.value = '';
defectInput.value = '';
methodInput.value = '';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
pendingImages = [];
previewContainer.innerHTML = '';
snError.style.display = 'none';
machineTypeError.style.display = 'none';
defectError.style.display = 'none';
methodError.style.display = 'none';
imageError.style.display = 'none';
API.toast('已清空表单');
});
// 获取当前用户角色
let userRole = window.__userRole || localStorage.getItem('userRole') || '';
let isSuperAdmin = userRole === 'superadmin';
// 如果没有缓存角色,从服务器获取
if (!userRole) {
try {
const userRes = await fetch('/api/auth/me', { credentials: 'include' });
const userData = await userRes.json();
userRole = userData.role || '';
isSuperAdmin = userRole === 'superadmin';
window.__userRole = userRole;
localStorage.setItem('userRole', userRole);
} catch (e) {
console.error('Failed to get user role:', e);
}
}
// 存储选中的记录ID
let selectedIds = new Set();
// 更新选中计数
const updateSelectedCount = () => {
const countEl = document.getElementById('repairs-selected-count');
if(countEl) {
countEl.textContent = selectedIds.size > 0 ? `已选 ${selectedIds.size} 条` : '';
}
};
// 渲染单条记录的HTML
const renderRecordItem = (r) => {
const ts = new Date(r.ts).toLocaleString('zh-CN');
const imgCount = (r.images || []).length;
const isChecked = selectedIds.has(r.id);
return `
SN: ${r.sn}
${ts}
${r.tracking_number ? `
📦 ${r.tracking_number} ` : ''}
${r.machine_type ? `${r.machine_type} ` : ''}
${r.defect_symptom ? `${r.defect_symptom} ` : ''}
${r.defect_reason}
${imgCount > 0 ? `${imgCount}张图片 ` : ''}
${r.repair_method ? `
维修: ${r.repair_method}
` : ''}
查看
编辑
${isSuperAdmin ? `删除 ` : ''}
`;
};
// 绑定列表项按钮事件
const bindListItemEvents = () => {
// 勾选框
document.querySelectorAll('.repair-checkbox').forEach(cb => {
cb.onchange = () => {
const id = parseInt(cb.dataset.id);
if(cb.checked) {
selectedIds.add(id);
} else {
selectedIds.delete(id);
}
updateSelectedCount();
updateSelectAllState();
};
});
// 查看按钮
document.querySelectorAll('.repair-view-btn').forEach(btn => {
btn.onclick = async () => {
const id = btn.dataset.id;
await showRepairDetailModal(id, false);
};
});
// 编辑按钮
document.querySelectorAll('.repair-edit-btn').forEach(btn => {
btn.onclick = async () => {
const id = btn.dataset.id;
await showRepairDetailModal(id, true);
};
});
// 删除按钮(仅超级管理员可见)
document.querySelectorAll('.repair-delete-btn').forEach(btn => {
btn.onclick = async () => {
const id = btn.dataset.id;
if(confirm('确定要删除这条返修记录吗?此操作不可恢复。')) {
try {
const result = await API.deleteRepairRecord(id);
if(result.ok) {
API.toast('删除成功');
selectedIds.delete(parseInt(id));
updateSelectedCount();
refreshRepairsList();
} else {
API.toast(result.error || '删除失败');
}
} catch(e) {
API.toast('删除失败: ' + (e.message || '网络错误'));
}
}
};
});
// 快递单号点击查询物流
document.querySelectorAll('.tracking-link').forEach(link => {
link.onclick = async () => {
const trackingNumber = link.dataset.tracking;
if(!trackingNumber) return;
// 显示加载弹窗
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
modal.innerHTML = `
`;
document.body.appendChild(modal);
document.getElementById('tracking-modal-close').onclick = () => modal.remove();
modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
try {
const result = await API.queryTracking(trackingNumber);
const contentEl = document.getElementById('tracking-content');
if(result.ok && result.data && result.data.length > 0) {
// 快递公司名称映射
const comNames = {
'shunfeng': '顺丰速运', 'yuantong': '圆通速递', 'zhongtong': '中通快递',
'yunda': '韵达快递', 'jd': '京东物流', 'ems': 'EMS', 'shentong': '申通快递',
'debangkuaidi': '德邦快递', 'auto': '快递'
};
const comName = comNames[result.com] || result.com || '快递';
const stateNames = {'0': '在途', '1': '揽收', '2': '疑难', '3': '签收', '4': '退签', '5': '派件', '6': '退回', '7': '转投'};
const stateName = stateNames[result.state] || '未知';
const stateColor = result.state === '3' ? 'var(--success)' : (result.state === '5' ? 'var(--primary)' : 'var(--text-2)');
contentEl.innerHTML = `
${comName}
${trackingNumber}
${stateName}
${result.data.map((item, idx) => `
${item.time || item.ftime || ''}
${item.context || ''}
`).join('')}
`;
} else {
contentEl.innerHTML = `
📭
${result.error || '暂无物流信息'}
`;
}
} catch(e) {
document.getElementById('tracking-content').innerHTML = `
❌
查询失败: ${e.message || '网络错误'}
`;
}
};
});
};
// 更新全选框状态
const updateSelectAllState = () => {
const selectAllCb = document.getElementById('repairs-select-all');
const checkboxes = document.querySelectorAll('.repair-checkbox');
if(selectAllCb && checkboxes.length > 0) {
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
const someChecked = Array.from(checkboxes).some(cb => cb.checked);
selectAllCb.checked = allChecked;
selectAllCb.indeterminate = someChecked && !allChecked;
}
};
// 刷新列表(支持搜索参数)
const refreshRepairsList = async (searchSn = null, showAll = false) => {
const listEl = document.getElementById('repairs-list');
const infoEl = document.getElementById('repairs-list-info');
try {
let params = {};
if(searchSn) {
params.sn = searchSn;
} else if(!showAll) {
params.limit = 10;
}
const data = await API.listRepairRecords(params);
if(listEl && data.list) {
if(data.list.length > 0) {
listEl.innerHTML = data.list.map(renderRecordItem).join('');
bindListItemEvents();
if(infoEl) {
if(searchSn) {
infoEl.textContent = `搜索 "${searchSn}" 找到 ${data.list.length} 条记录`;
} else if(showAll) {
infoEl.textContent = `共 ${data.list.length} 条记录`;
} else {
infoEl.textContent = `显示最新 ${data.list.length} 条记录`;
}
}
} else {
listEl.innerHTML = '暂无记录 ';
if(infoEl) {
infoEl.textContent = searchSn ? `未找到包含 "${searchSn}" 的记录` : '';
}
}
}
} catch(e) {
if(listEl) listEl.innerHTML = '加载失败 ';
if(infoEl) infoEl.textContent = '';
}
};
// 显示详情/编辑弹窗
const showRepairDetailModal = async (id, isEdit) => {
try {
const record = await API.getRepairRecord(id);
if(record.error) {
API.toast(record.error);
return;
}
const modalId = 'repair-detail-modal';
let modal = document.getElementById(modalId);
if(modal) modal.remove();
const images = record.images || [];
const imagesHtml = images.length > 0
? `
${record.images.map(img => `
`).join('')}
`
: '无图片
';
const ts = new Date(record.ts).toLocaleString('zh-CN');
const isNoDefect = record.defect_reason === '无不良';
modal = document.createElement('div');
modal.id = modalId;
modal.innerHTML = `
${isEdit ? '编辑返修记录' : '返修记录详情'}
×
${isEdit ? `
设备SN *
机种 *
${machineTypeOptions.map(opt => `${opt} `).join('')}
不良现象 *
不良原因 *
维修方法 *
快递单号
保存
取消
` : `
机种
${record.machine_type || '未知'}
不良现象
${record.defect_symptom || '未填写'}
不良原因
${record.defect_reason}
维修方法
${record.repair_method || '无'}
快递单号
${record.tracking_number ? `📦 ${record.tracking_number} ` : '未填写'}
编辑
关闭
`}
`;
document.body.appendChild(modal);
// 绑定弹窗事件
document.getElementById('repair-modal-close').onclick = () => modal.remove();
modal.querySelector('div').onclick = (e) => { if(e.target === modal.querySelector('div')) modal.remove(); };
if(isEdit) {
const editDefectInput = document.getElementById('edit-repair-defect');
const editMethodRequired = document.getElementById('edit-method-required');
// 图片管理相关
let keepImages = [...images]; // 要保留的现有图片
let newImageFiles = []; // 新添加的图片文件
// 删除现有图片
document.querySelectorAll('.edit-image-delete').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
const filename = btn.dataset.filename;
keepImages = keepImages.filter(img => img !== filename);
btn.closest('.edit-image-item').remove();
};
});
// 添加新图片
const editDropzone = document.getElementById('edit-image-dropzone');
const editNewImagesInput = document.getElementById('edit-new-images');
const editNewImagesPreview = document.getElementById('edit-new-images-preview');
editDropzone.onclick = () => editNewImagesInput.click();
editDropzone.ondragover = (e) => { e.preventDefault(); editDropzone.style.borderColor = 'var(--primary)'; };
editDropzone.ondragleave = () => { editDropzone.style.borderColor = 'rgba(79,140,255,0.4)'; };
editDropzone.ondrop = (e) => {
e.preventDefault();
editDropzone.style.borderColor = 'rgba(79,140,255,0.4)';
handleNewImages(e.dataTransfer.files);
};
editNewImagesInput.onchange = () => handleNewImages(editNewImagesInput.files);
function handleNewImages(files) {
for(const file of files) {
if(!file.type.startsWith('image/')) continue;
if(keepImages.length + newImageFiles.length >= 10) {
API.toast('最多10张图片');
break;
}
newImageFiles.push(file);
// 预览
const reader = new FileReader();
reader.onload = (e) => {
const idx = newImageFiles.length - 1;
const div = document.createElement('div');
div.className = 'new-image-item';
div.dataset.idx = idx;
div.style.cssText = 'position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)';
div.innerHTML = `
×
`;
editNewImagesPreview.appendChild(div);
div.querySelector('.new-image-delete').onclick = (ev) => {
ev.stopPropagation();
const i = parseInt(div.dataset.idx);
newImageFiles[i] = null;
div.remove();
};
};
reader.readAsDataURL(file);
}
}
// 不良原因输入变化
editDefectInput.oninput = () => {
const defectValue = editDefectInput.value.trim();
editMethodRequired.style.display = defectValue === '无不良' ? 'none' : 'inline';
};
document.getElementById('edit-repair-cancel').onclick = () => modal.remove();
document.getElementById('edit-repair-save').onclick = async () => {
const newSn = document.getElementById('edit-repair-sn').value.trim();
const newMachineType = document.getElementById('edit-repair-machine-type').value;
const newSymptom = document.getElementById('edit-repair-symptom').value.trim();
const newDefect = editDefectInput.value.trim();
const newMethod = document.getElementById('edit-repair-method').value.trim();
const newTracking = document.getElementById('edit-repair-tracking').value.trim();
// 验证
if(!newSn || newSn.length < 6) {
API.toast('SN至少需要6个字符');
return;
}
if(!newMachineType) {
API.toast('请选择机种');
return;
}
if(!newSymptom) {
API.toast('请输入不良现象');
return;
}
if(!newDefect) {
API.toast('请输入不良原因');
return;
}
if(newDefect !== '无不良' && !newMethod) {
API.toast('填写了不良原因时,维修方法为必填项');
return;
}
try {
// 先更新基本信息
const result = await API.updateRepairRecord(id, {
sn: newSn,
machine_type: newMachineType,
defect_symptom: newSymptom,
defect_reason: newDefect,
repair_method: newMethod,
tracking_number: newTracking
});
if(!result.ok) {
API.toast(result.error || '保存失败');
return;
}
// 如果图片有变化,更新图片
const actualNewFiles = newImageFiles.filter(f => f !== null);
const imagesChanged = keepImages.length !== images.length || actualNewFiles.length > 0;
if(imagesChanged) {
const formData = new FormData();
keepImages.forEach(img => formData.append('keep_images', img));
actualNewFiles.forEach(file => formData.append('new_images', file));
const imgResult = await API.updateRepairImages(id, formData);
if(!imgResult.ok) {
API.toast('图片更新失败: ' + (imgResult.error || '未知错误'));
return;
}
}
API.toast('保存成功');
modal.remove();
refreshRepairsList();
} catch(e) {
API.toast('保存失败: ' + (e.message || '网络错误'));
}
};
} else {
document.getElementById('view-repair-close').onclick = () => modal.remove();
document.getElementById('view-repair-edit').onclick = () => {
modal.remove();
showRepairDetailModal(id, true);
};
}
} catch(e) {
API.toast('加载详情失败');
}
};
// 搜索框和按钮
const searchInput = document.getElementById('repairs-search-sn');
const searchBtn = document.getElementById('repairs-search-btn');
// 搜索按钮点击
addListener(searchBtn, 'click', async () => {
const searchSn = searchInput.value.trim();
if(!searchSn) {
API.toast('请输入SN进行搜索');
return;
}
await refreshRepairsList(searchSn);
});
// 搜索框回车
addListener(searchInput, 'keypress', async (e) => {
if(e.key === 'Enter') {
const searchSn = searchInput.value.trim();
if(searchSn) {
await refreshRepairsList(searchSn);
}
}
});
// 查看全部按钮
addListener(showAllBtn, 'click', async () => {
searchInput.value = '';
await refreshRepairsList(null, true);
API.toast('显示全部记录');
});
// 全选复选框
const selectAllCb = document.getElementById('repairs-select-all');
addListener(selectAllCb, 'change', () => {
const checkboxes = document.querySelectorAll('.repair-checkbox');
checkboxes.forEach(cb => {
cb.checked = selectAllCb.checked;
const id = parseInt(cb.dataset.id);
if(selectAllCb.checked) {
selectedIds.add(id);
} else {
selectedIds.delete(id);
}
});
updateSelectedCount();
});
// 添加快递单号按钮
const trackingBtn = document.getElementById('repairs-tracking-btn');
addListener(trackingBtn, 'click', async () => {
if(selectedIds.size === 0) {
API.toast('请先勾选要添加快递单号的记录');
return;
}
// 创建弹窗
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
modal.innerHTML = `
添加快递单号
×
已选择 ${selectedIds.size} 条记录
快递单号 *
确定
取消
`;
document.body.appendChild(modal);
document.getElementById('tracking-modal-close').onclick = () => modal.remove();
document.getElementById('tracking-cancel').onclick = () => modal.remove();
document.getElementById('tracking-submit').onclick = async () => {
const trackingNumber = document.getElementById('tracking-number-input').value.trim();
if(!trackingNumber) {
API.toast('请输入快递单号');
return;
}
try {
const result = await API.batchUpdateTracking(Array.from(selectedIds), trackingNumber);
if(result.ok) {
API.toast(`成功更新 ${result.updated} 条记录的快递单号`);
modal.remove();
refreshRepairsList();
} else {
API.toast(result.error || '更新失败');
}
} catch(e) {
API.toast('更新失败: ' + (e.message || '网络错误'));
}
};
});
// 导出按钮
const exportBtn = document.getElementById('repairs-export-btn');
addListener(exportBtn, 'click', async () => {
if(selectedIds.size === 0) {
API.toast('请先勾选要导出的记录');
return;
}
exportBtn.disabled = true;
exportBtn.textContent = '导出中...';
try {
const blob = await API.exportRepairRecords(Array.from(selectedIds));
// 下载文件
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `返修记录_${new Date().toISOString().slice(0,10)}.xlsx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
API.toast(`成功导出 ${selectedIds.size} 条记录`);
} catch(e) {
API.toast('导出失败: ' + (e.error || e.message || '网络错误'));
} finally {
exportBtn.disabled = false;
exportBtn.textContent = '导出选中';
}
});
// 提交按钮
addListener(btn, 'click', async () => {
// 表单验证
let hasError = false;
// 验证SN
const sn = snInput.value.trim();
if(!sn) {
snError.textContent = '请输入设备SN';
snError.style.display = 'block';
hasError = true;
} else if(sn.length < 6) {
snError.textContent = 'SN至少需要6个字符';
snError.style.display = 'block';
hasError = true;
} else if(!/^[A-Za-z0-9\-_:]+$/.test(sn)) {
snError.textContent = 'SN只能包含字母、数字、横线、下划线或冒号';
snError.style.display = 'block';
hasError = true;
}
// 验证机种
const machineType = machineTypeSelect.value;
if(!machineType) {
machineTypeError.textContent = '请选择机种';
machineTypeError.style.display = 'block';
hasError = true;
}
// 验证不良现象
const defectSymptom = symptomInput.value.trim();
if(!defectSymptom) {
symptomError.textContent = '请输入不良现象';
symptomError.style.display = 'block';
hasError = true;
}
// 验证不良原因
const defectReason = defectInput.value.trim();
if(!defectReason) {
defectError.textContent = '请输入不良原因';
defectError.style.display = 'block';
hasError = true;
}
// 验证维修方法:非"无不良"时必填
const repairMethod = methodInput.value.trim();
if(defectReason && defectReason !== '无不良' && !repairMethod) {
methodError.textContent = '选择了不良原因时,维修方法为必填项';
methodError.style.display = 'block';
hasError = true;
}
if(hasError) return;
// 构建FormData
const formData = new FormData();
formData.append('sn', sn);
formData.append('machine_type', machineType);
formData.append('defect_symptom', defectSymptom);
formData.append('defect_reason', defectReason);
formData.append('repair_method', methodInput.value.trim());
// 添加图片
pendingImages.forEach(file => {
formData.append('images', file);
});
// 显示加载状态
btn.disabled = true;
uploadText.style.display = 'none';
uploadLoading.style.display = 'inline-flex';
try {
const result = await API.uploadRepairRecord(formData);
if(result.ok) {
API.toast('提交成功');
// 清空表单
snInput.value = '';
machineTypeSelect.value = '';
symptomInput.value = '';
defectInput.value = '';
methodInput.value = '';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
pendingImages = [];
previewContainer.innerHTML = '';
// 刷新列表
await refreshRepairsList();
} else {
API.toast(result.error || '提交失败');
}
} catch(e) {
API.toast('提交失败: ' + (e.message || '网络错误'));
} finally {
btn.disabled = false;
uploadText.style.display = 'inline';
uploadLoading.style.display = 'none';
}
});
// 初始加载最新记录
setTimeout(refreshRepairsList, 100);
}
async function bindDefectsEvents(){
const btn = document.getElementById('defects-upload');
addListener(btn, 'click', async ()=>{
try{
const fileEl=document.getElementById('defects-file');
const manual=document.getElementById('defects-manual').value.trim();
let rows=[];
if(fileEl.files[0]){
const text=await readText(fileEl.files[0]);
rows=text.split(/\n+/).map(l=>l.split(','));
rows=rows.map(([mac,batch])=>({mac,batch}));
}else if(manual){
rows=parseManual(manual);
}
if(!rows.length){throw new Error('请提供文件或手动输入')}
btn.disabled = true;
await API.uploadDefects({rows});
API.toast('上传成功');
const listEl=document.getElementById('defects-list');
const data=await API.listDefects().catch(()=>({list:[]}));
if(listEl) listEl.innerHTML=(data.list||[]).slice(0,10).map(r=>`${r.mac} ${r.batch} `).join('')||'暂无数据 ';
}catch(e){
API.toast('上传失败');
} finally {
if (btn) btn.disabled = false;
}
});
}
})();