Files
prop-ai-hr/mobile-uni/src/services/api.ts
T

153 lines
5.3 KiB
TypeScript

import type { ApiPayload } from '@/types/api';
import { clearAuth, getAuth } from './auth';
const apiBase = import.meta.env.VITE_API_BASE || '/dev-api';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
export interface RequestOptions {
url: string;
method?: HttpMethod;
data?: unknown;
header?: Record<string, string>;
timeout?: number;
auth?: boolean;
clearAuthOnUnauthorized?: boolean;
}
const normalizeUrl = (url: string) => `${apiBase}${url.startsWith('/') ? url : `/${url}`}`;
export const apiUrl = normalizeUrl;
export const authHeaders = (json = true, includeAuth = true) => {
const auth = getAuth();
return {
...(json ? { 'Content-Type': 'application/json' } : {}),
...(includeAuth && auth.token ? { Authorization: `Bearer ${auth.token}` } : {}),
...(includeAuth && auth.clientId ? { clientid: auth.clientId } : {})
};
};
export const readPayload = <T>(statusCode: number, data: unknown, clearAuthOnUnauthorized = true): T => {
const payload = data as ApiPayload<T>;
const code = typeof payload?.code === 'number' ? payload.code : undefined;
if (statusCode < 200 || statusCode >= 300 || (code !== undefined && code !== 200)) {
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403 || code === 401 || code === 403)) {
clearAuth();
}
throw new Error(payload?.msg || `HTTP ${statusCode}`);
}
return payload?.data as T;
};
export const parsePayloadText = (statusCode: number, text: string, clearAuthOnUnauthorized = true) => {
try {
return JSON.parse(text || '{}');
} catch (error) {
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403)) clearAuth();
throw error instanceof SyntaxError
? new Error(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败')
: error;
}
};
export const readTextPayload = <T>(statusCode: number, text: string): T =>
readPayload<T>(statusCode, parsePayloadText(statusCode, text));
const requestWithXhr = async <T>(options: RequestOptions): Promise<T> => {
const method = options.method || 'GET';
const hasBody = method !== 'GET' && options.data !== undefined;
const response = await new Promise<{ status: number; data: unknown }>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, normalizeUrl(options.url));
xhr.timeout = options.timeout || 30000;
Object.entries({
...authHeaders(hasBody, options.auth !== false),
...options.header
}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
xhr.onload = () => {
try {
resolve({
status: xhr.status,
data: parsePayloadText(xhr.status, xhr.responseText || 'null', options.clearAuthOnUnauthorized !== false)
});
} catch {
reject(new Error(xhr.status >= 400 ? `HTTP ${xhr.status}` : '响应解析失败'));
}
};
xhr.onerror = () => reject(new Error('网络请求失败'));
xhr.ontimeout = () => reject(new Error('网络请求超时'));
xhr.send(hasBody ? JSON.stringify(options.data) : undefined);
});
return readPayload<T>(response.status, response.data, options.clearAuthOnUnauthorized !== false);
};
export const apiRequest = async <T>(options: RequestOptions): Promise<T> => {
if (typeof XMLHttpRequest !== 'undefined') {
return requestWithXhr<T>(options);
}
const response = await new Promise<any>((resolve, reject) => {
uni.request({
url: normalizeUrl(options.url),
method: options.method || 'GET',
data: options.data as string | ArrayBuffer | Record<string, unknown> | undefined,
header: {
...authHeaders(options.method !== 'GET', options.auth !== false),
...options.header
},
timeout: options.timeout || 30000,
success: resolve,
fail: reject
});
});
return readPayload<T>(response.statusCode, response.data, options.clearAuthOnUnauthorized !== false);
};
export const authenticatedFileUrl = async (url: string): Promise<string> => {
const fullUrl = normalizeUrl(url);
if (typeof XMLHttpRequest !== 'undefined' && typeof URL !== 'undefined') {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', fullUrl);
xhr.responseType = 'blob';
Object.entries(authHeaders(false)).forEach(([key, value]) => xhr.setRequestHeader(key, value));
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(URL.createObjectURL(xhr.response));
return;
}
if (xhr.status === 401 || xhr.status === 403) clearAuth();
reject(new Error(`HTTP ${xhr.status}`));
};
xhr.onerror = () => reject(new Error('网络请求失败'));
xhr.send();
});
}
return new Promise((resolve, reject) => {
uni.downloadFile({
url: fullUrl,
header: authHeaders(false),
success: (response) => {
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(response.tempFilePath);
return;
}
if (response.statusCode === 401 || response.statusCode === 403) clearAuth();
reject(new Error(`HTTP ${response.statusCode}`));
},
fail: () => reject(new Error('网络请求失败'))
});
});
};
export const releaseAuthenticatedFileUrl = (url: string) => {
if (url.startsWith('blob:') && typeof URL !== 'undefined') {
URL.revokeObjectURL(url);
}
};