156 lines
4.9 KiB
JavaScript
156 lines
4.9 KiB
JavaScript
const API_KEY = 'your-external-api-key';
|
|
const API_BASE = '/api/v1';
|
|
const REQUEST_TIMEOUT = 30000;
|
|
|
|
let pendingRequests = 0;
|
|
let loadingToast = null;
|
|
let loadingTimer = null;
|
|
|
|
function showNonBlockingLoading(message = '加载中...') {
|
|
pendingRequests++;
|
|
updateLoadingUI(message);
|
|
|
|
if (loadingTimer) clearTimeout(loadingTimer);
|
|
loadingTimer = setTimeout(() => {
|
|
console.warn('Some requests may be stuck, force clearing');
|
|
}, 60000);
|
|
}
|
|
|
|
function hideNonBlockingLoading() {
|
|
pendingRequests = Math.max(0, pendingRequests - 1);
|
|
updateLoadingUI(pendingRequests > 0 ? '处理中...' : null);
|
|
}
|
|
|
|
function updateLoadingUI(message) {
|
|
if (!message) {
|
|
if (loadingToast) {
|
|
loadingToast.remove();
|
|
loadingToast = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!loadingToast) {
|
|
loadingToast = document.createElement('div');
|
|
loadingToast.id = 'loading-toast';
|
|
loadingToast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0,0,0,0.8);
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
z-index: 10000;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
`;
|
|
document.body.appendChild(loadingToast);
|
|
}
|
|
|
|
const spinner = '<div style="width:16px;height:16px;border:2px solid #fff;border-top-color:transparent;border-radius:50%;animation:spin 0.8s linear infinite"></div>';
|
|
const style = '<style>@keyframes spin{to{transform:rotate(360deg)}}</style>';
|
|
if (!document.getElementById('loading-spinner-style')) {
|
|
document.head.insertAdjacentHTML('beforeend', style);
|
|
}
|
|
loadingToast.innerHTML = spinner + `<span>${message}</span><span style="font-size:12px;opacity:0.7">(${pendingRequests})</span>`;
|
|
}
|
|
|
|
async function api(endpoint, options = {}) {
|
|
const method = options.method || 'GET';
|
|
const isMutation = ['POST', 'PUT', 'DELETE'].includes(method);
|
|
const startMsg = `请求${isMutation ? '修改' : '获取'}中...`;
|
|
|
|
showNonBlockingLoading(startMsg);
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => {
|
|
controller.abort();
|
|
showToast('请求超时,请稍后重试', 'error');
|
|
}, REQUEST_TIMEOUT);
|
|
|
|
try {
|
|
const headers = {
|
|
'X-API-Key': API_KEY,
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
};
|
|
|
|
const response = await fetch(API_BASE + endpoint, {
|
|
...options,
|
|
headers,
|
|
signal: controller.signal
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
const error = `HTTP ${response.status}: ${response.statusText}`;
|
|
if (isMutation) {
|
|
showToast('操作失败: ' + error, 'error');
|
|
}
|
|
return { success: false, error };
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (isMutation && data.success === false && data.error) {
|
|
showToast(data.error, 'error');
|
|
}
|
|
|
|
return data;
|
|
|
|
} catch (err) {
|
|
clearTimeout(timeoutId);
|
|
if (err.name === 'AbortError') {
|
|
return { success: false, error: '请求超时' };
|
|
}
|
|
const errorMsg = err.message || '网络错误';
|
|
if (isMutation) {
|
|
showToast(errorMsg, 'error');
|
|
}
|
|
return { success: false, error: errorMsg };
|
|
|
|
} finally {
|
|
hideNonBlockingLoading();
|
|
}
|
|
}
|
|
|
|
const apiClient = {
|
|
get: (endpoint) => api(endpoint),
|
|
post: (endpoint, data) => api(endpoint, { method: 'POST', body: JSON.stringify(data) }),
|
|
put: (endpoint, data) => api(endpoint, { method: 'PUT', body: JSON.stringify(data) }),
|
|
delete: (endpoint) => api(endpoint, { method: 'DELETE' }),
|
|
|
|
nodes: {
|
|
list: () => api('/nodes/'),
|
|
get: (name) => api(`/nodes/${name}`),
|
|
create: (data) => api('/nodes/', { method: 'POST', body: JSON.stringify(data) }),
|
|
update: (name, data) => api(`/nodes/${name}`, { method: 'PUT', body: JSON.stringify(data) }),
|
|
delete: (name) => api(`/nodes/${name}`, { method: 'DELETE' }),
|
|
status: (name) => api(`/nodes/${name}/status`)
|
|
},
|
|
|
|
messages: {
|
|
send: (data) => api('/messages/send', { method: 'POST', body: JSON.stringify(data) }),
|
|
broadcast: (data) => api('/messages/broadcast', { method: 'POST', body: JSON.stringify(data) })
|
|
},
|
|
|
|
config: {
|
|
getMonitor: () => api('/config/monitor'),
|
|
updateMonitor: (data) => api('/config/monitor', { method: 'POST', body: JSON.stringify(data) }),
|
|
getWebhook: () => api('/config/webhook'),
|
|
updateWebhook: (data) => api('/config/webhook', { method: 'POST', body: JSON.stringify(data) })
|
|
},
|
|
|
|
logs: {
|
|
recent: (count = 50) => api(`/logs/recent?count=${count}`)
|
|
}
|
|
};
|
|
|
|
window.api = api;
|
|
window.apiClient = apiClient;
|