Initial LisgloSIPS V2 implementation
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
const API_BASE = (import.meta.env.VITE_LISGLOSIPS_API_BASE || '/api/v2').replace(/\/$/, '');
|
||||
const ACCESS_TOKEN_KEY = 'lisglosips.accessToken';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, { status, code } = {}) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const token = window.localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
const headers = new Headers(options.headers || {});
|
||||
headers.set('Accept', 'application/json');
|
||||
if (options.body && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
});
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const payload = contentType.includes('application/json') ? await response.json() : await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(errorMessage(payload, response.status), {
|
||||
status: response.status,
|
||||
code: typeof payload === 'object' && payload ? payload.code : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function errorMessage(payload, status) {
|
||||
if (payload && typeof payload === 'object') {
|
||||
return payload.message || payload.error || `API request failed with HTTP ${status}.`;
|
||||
}
|
||||
return payload || `API request failed with HTTP ${status}.`;
|
||||
}
|
||||
|
||||
function jsonBody(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function idempotencyKey(scope) {
|
||||
const random = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
return `${scope}:${random}`;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
dashboardSummary: () => request('/dashboard/summary'),
|
||||
dashboardTrends: (params = { hours: 24, bucketMinutes: 60 }) => request(`/dashboard/trends?${new URLSearchParams(params)}`),
|
||||
customers: () => request('/customers'),
|
||||
createCustomer: (body) => request('/customers', { method: 'POST', body: jsonBody(body) }),
|
||||
updateCustomer: (id, body) => request(`/customers/${encodeURIComponent(id)}`, { method: 'PATCH', body: jsonBody(body) }),
|
||||
rechargeCustomer: (id, body) =>
|
||||
request(`/customers/${encodeURIComponent(id)}/recharges`, {
|
||||
method: 'POST',
|
||||
body: jsonBody({ ...body, idempotencyKey: idempotencyKey('customer-recharge') }),
|
||||
}),
|
||||
vendors: () => request('/vendors'),
|
||||
createVendor: (body) => request('/vendors', { method: 'POST', body: jsonBody(body) }),
|
||||
updateVendor: (id, body) => request(`/vendors/${encodeURIComponent(id)}`, { method: 'PATCH', body: jsonBody(body) }),
|
||||
rechargeVendor: (id, body) =>
|
||||
request(`/vendors/${encodeURIComponent(id)}/recharges`, {
|
||||
method: 'POST',
|
||||
body: jsonBody({ ...body, idempotencyKey: idempotencyKey('vendor-recharge') }),
|
||||
}),
|
||||
recharges: () => request('/recharges?take=100'),
|
||||
users: () => request('/users'),
|
||||
roles: () => request('/roles'),
|
||||
auditLogs: () => request('/audit-logs?take=100'),
|
||||
};
|
||||
|
||||
export function explainApiError(error) {
|
||||
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
|
||||
return 'API 已启用鉴权,请先通过登录接口获取会话或在同源环境使用有效 Cookie。';
|
||||
}
|
||||
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
|
||||
}
|
||||
Reference in New Issue
Block a user