const API = (() => { const base = '/api'; async function request(path, opts = {}) { const overlay = document.getElementById('overlay'); overlay.classList.remove('hidden'); try { const res = await fetch(base + path, { headers: { 'Content-Type': 'application/json' }, credentials: 'include', ...opts }); if (!res.ok) throw new Error(await res.text()); return await res.json(); } catch (e) { toast(e.message || '请求失败'); throw e; } finally { overlay.classList.add('hidden'); } } async function requestQuiet(path, opts = {}) { // 创建超时控制器 const controller = opts.signal ? null : new AbortController(); const timeoutId = controller ? setTimeout(() => { controller.abort(); console.warn('[API] 请求超时:', path); }, 10000) : null; // 10秒超时 try { const res = await fetch(base + path, { headers: { 'Content-Type': 'application/json' }, credentials: 'include', signal: opts.signal || (controller ? controller.signal : undefined), ...opts }); if (timeoutId) clearTimeout(timeoutId); if (!res.ok) throw new Error(await res.text()); return await res.json(); } catch (e) { if (timeoutId) clearTimeout(timeoutId); // 忽略取消的请求 if (e.name === 'AbortError') { return { list: [] }; } throw e; } } async function uploadFile(path, formData) { const overlay = document.getElementById('overlay'); overlay.classList.remove('hidden'); try { const res = await fetch(base + path, { method: 'POST', body: formData, credentials: 'include' }); if (!res.ok) throw new Error(await res.text()); return await res.json(); } catch (e) { toast(e.message || '上传失败'); throw e; } finally { overlay.classList.add('hidden'); } } async function uploadFileWithProgress(path, formData, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); // 上传进度 xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable && onProgress) { const percent = Math.round((e.loaded / e.total) * 100); const loaded = e.loaded; const total = e.total; onProgress({ percent, loaded, total }); } }); // 完成 xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { try { const result = JSON.parse(xhr.responseText); resolve(result); } catch (e) { reject(new Error('解析响应失败')); } } else { reject(new Error(xhr.responseText || '上传失败')); } }); // 错误 xhr.addEventListener('error', () => { reject(new Error('网络错误')); }); // 中止 xhr.addEventListener('abort', () => { reject(new Error('上传已取消')); }); xhr.open('POST', base + path); xhr.withCredentials = true; xhr.send(formData); }); } function toast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2000); } return { // 通用HTTP方法 get: (path) => request(path.replace('/api', '')), post: (path, data) => request(path.replace('/api', ''), { method: 'POST', body: JSON.stringify(data) }), put: (path, data) => request(path.replace('/api', ''), { method: 'PUT', body: JSON.stringify(data) }), delete: (path) => request(path.replace('/api', ''), { method: 'DELETE' }), login: (username, password) => request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }), me: () => request('/auth/me'), logout: () => request('/auth/logout', { method: 'POST' }), dashboard: () => request('/dashboard'), overview: () => request('/overview'), uploadMac: data => request('/upload/mac', { method: 'POST', body: JSON.stringify(data) }), uploadMacFile: file => { const fd = new FormData(); fd.append('file', file); return uploadFile('/upload/mac-file', fd); }, uploadStats: data => request('/upload/stats', { method: 'POST', body: JSON.stringify(data) }), uploadRepairs: data => request('/upload/repairs', { method: 'POST', body: JSON.stringify(data) }), uploadDefects: data => request('/upload/defects', { method: 'POST', body: JSON.stringify(data) }), uploadDefectsFile: file => { const fd = new FormData(); fd.append('file', file); return uploadFile('/upload/defects-file', fd); }, uploadShipments: data => request('/upload/shipments', { method: 'POST', body: JSON.stringify(data) }), devices: () => request('/collect/devices'), environment: () => request('/collect/environment'), personnel: () => request('/collect/personnel'), qa: () => request('/collect/qa'), production: () => request('/collect/production'), addPersonnel: (name, role) => request('/collect/personnel', { method: 'POST', body: JSON.stringify({ name, role }) }), listMac: () => request('/list/mac'), listStats: () => request('/list/stats'), listRepairs: () => request('/list/repairs'), listDefects: () => request('/list/defects'), listShipments: () => request('/list/shipments'), auditPdd: (params={}) => request('/audit/pdd' + buildQuery(params)), auditYt: (params={}) => request('/audit/yt' + buildQuery(params)), auditPddQuiet: (params={}) => requestQuiet('/audit/pdd' + buildQuery(params)), auditYtQuiet: (params={}) => requestQuiet('/audit/yt' + buildQuery(params)), exportExcel: params => request('/export/excel', { method: 'POST', body: JSON.stringify(params) }), exportPdf: params => request('/export/pdf', { method: 'POST', body: JSON.stringify(params) }), toast, adminUsers: () => request('/admin/users'), resetPassword: (username, new_password) => request('/admin/reset-password', { method: 'POST', body: JSON.stringify({ username, new_password }) }), changePassword: (username, new_password) => request('/admin/change-password', { method: 'POST', body: JSON.stringify({ username, new_password }) }), deleteUser: username => request('/admin/delete-user', { method: 'POST', body: JSON.stringify({ username }) }), clearModule: module => request('/admin/clear', { method: 'POST', body: JSON.stringify({ module }) }), getNotifications: () => requestQuiet('/notifications'), getUnreadCount: () => requestQuiet('/notifications/unread-count'), markNotificationRead: id => requestQuiet('/notifications/mark-read', { method: 'POST', body: JSON.stringify({ id }) }), markAllNotificationsRead: () => requestQuiet('/notifications/mark-all-read', { method: 'POST' }), deleteReadNotifications: () => requestQuiet('/notifications/delete-read', { method: 'POST' }), updateShipmentsPlatform: () => request('/shipments/update-platform', { method: 'POST' }), listSopFiles: () => request('/sop/list'), uploadSopFile: (file, description) => { const fd = new FormData(); fd.append('file', file); fd.append('description', description); return uploadFile('/sop/upload', fd); }, uploadSopFileWithProgress: (file, description, onProgress) => { const fd = new FormData(); fd.append('file', file); fd.append('description', description); return uploadFileWithProgress('/sop/upload', fd, onProgress); }, deleteSopFile: id => request(`/sop/delete/${id}`, { method: 'POST' }) }; })(); function buildQuery(params){ const q = new URLSearchParams(); if(params.start) q.set('start', params.start); if(params.end) q.set('end', params.end); if(params.limit) q.set('limit', params.limit); if(params.order) q.set('order', params.order); const s = q.toString(); return s ? ('?' + s) : ''; }