2025-11-21 13:27:40 +00:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-22 12:40:46 +00:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-21 13:27:40 +00:00
|
|
|
function toast(msg) {
|
|
|
|
|
const t = document.getElementById('toast');
|
|
|
|
|
t.textContent = msg;
|
|
|
|
|
t.classList.add('show');
|
|
|
|
|
setTimeout(() => t.classList.remove('show'), 2000);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
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 }) }),
|
2025-11-23 01:15:37 +00:00
|
|
|
deleteUser: username => request('/admin/delete-user', { method: 'POST', body: JSON.stringify({ username }) }),
|
2025-11-21 13:27:40 +00:00
|
|
|
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' }),
|
2025-11-22 12:40:46 +00:00
|
|
|
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' })
|
2025-11-21 13:27:40 +00:00
|
|
|
};
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
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) : '';
|
|
|
|
|
}
|