diff --git a/src/simapi.core.ts b/src/simapi.core.ts index bbcabf4..566d29d 100644 --- a/src/simapi.core.ts +++ b/src/simapi.core.ts @@ -11,314 +11,318 @@ */ import { - type SimApiVersions, - type SimApiAuthConfig, - type SimApiApiConfig, - type SimApiOptions, - type SimApiBaseResponse, + type SimApiVersions, + type SimApiAuthConfig, + type SimApiApiConfig, + type SimApiOptions, + type SimApiBaseResponse, } from './types' export type { - SimApiVersions, - SimApiAuthConfig, - SimApiApiConfig, - SimApiOptions, - SimApiBaseResponse, + SimApiVersions, + SimApiAuthConfig, + SimApiApiConfig, + SimApiOptions, + SimApiBaseResponse, } from './types' declare const SimApiVersion: string; declare const AppVersion: string; + // ── Helper: Fetch with Timeout ──────────────────────────────────────── function fetchWithTimeout( - url: string, - options: RequestInit, - timeout: number = 10000 + url: string, + options: RequestInit, + timeout: number = 10000 ): Promise { - return Promise.race([ - fetch(url, options), - new Promise((_, reject) => - setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout) - ), - ]) + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout) + ), + ]) } // ── Helper: Fetch POST with JSON body ──────────────────────────────────── async function fetchPost( - url: string, - body: any, - headers: Record, - timeout: number + url: string, + body: any, + headers: Record, + timeout: number ): Promise> { - const options: RequestInit = { - method: 'POST', - headers: headers as HeadersInit, - body: body instanceof FormData ? body : JSON.stringify(body), - credentials: 'omit', // 从不发送 Cookie - } - - const response = await fetchWithTimeout(url, options, timeout) - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})) - throw { - status: response.status, - statusText: response.statusText, - data: errorData, - message: `HTTP ${response.status}: ${response.statusText}`, + const options: RequestInit = { + method: 'POST', + headers: headers as HeadersInit, + body: body instanceof FormData ? body : JSON.stringify(body), + credentials: 'omit', // 从不发送 Cookie } - } - return response.json() + const response = await fetchWithTimeout(url, options, timeout) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw { + status: response.status, + statusText: response.statusText, + data: errorData, + message: `HTTP ${response.status}: ${response.statusText}`, + } + } + + return response.json() } // ── SimApiCore ──────────────────────────────────────── export class SimApiCore { - debug: boolean = true - auth: SimApiAuthConfig = { - token_name: 'simapi-auth-token', - check_url: '/auth/check', - logout_url: '/auth/logout', - login_url: '/auth/login', - } - - api: SimApiApiConfig = { - endpoints: { default: '' }, - defaultEndpoint: 'default', - businessCallback: { - 401: () => localStorage.removeItem(this.auth.token_name), - common: () => {}, - }, - responseCallback: { - success: (response: any) => response, - error: (_err: any) => {}, - }, - timeout: 10000, - } - - constructor(options?: SimApiOptions) { - if (options) { - this.configure(options) + debug: boolean = true + auth: SimApiAuthConfig = { + token_name: 'simapi-auth-token', + check_url: '/auth/check', + logout_url: '/auth/logout', + login_url: '/auth/login', } - } - /** - * 从 window.simapi 读取配置并初始化 - * - * 支持字段:endpoints, defaultEndpoint, debug - * 业务回调(businessCallback / responseCallback)需在代码中处理 - */ - autoInit(): void { - const config = (window as any).simapi - if (!config) return - - if (config.debug !== undefined) { - this.debug = config.debug + api: SimApiApiConfig = { + endpoints: {default: ''}, + defaultEndpoint: 'default', + businessCallback: { + 401: () => localStorage.removeItem(this.auth.token_name), + common: () => { + }, + }, + responseCallback: { + success: (response: any) => response, + error: (_err: any) => { + }, + }, + timeout: 10000, } - if (config.endpoints) { - this.api.endpoints = { ...this.api.endpoints, ...config.endpoints } - } - if (config.defaultEndpoint) { - this.api.defaultEndpoint = config.defaultEndpoint - } - } - configure(options: SimApiOptions): void { - if (options.debug !== undefined) { - this.debug = options.debug - } - if (options.auth) { - this.auth = { ...this.auth, ...options.auth } - } - if (options.api) { - this.api = { - ...this.api, - ...options.api, - endpoints: { ...this.api.endpoints, ...(options.api.endpoints ?? {}) }, - businessCallback: { ...this.api.businessCallback, ...(options.api.businessCallback ?? {}) }, - responseCallback: { ...this.api.responseCallback, ...(options.api.responseCallback ?? {}) }, - } - } - } - - setEndpoints(endpoints: { [name: string]: string }): void { - this.api.endpoints = { ...this.api.endpoints, ...endpoints } - } - - getEndpoint(name?: string): string { - return this.api.endpoints[name ?? this.api.defaultEndpoint] ?? '' - } - - setBusinessCallback(code: number | string, callback: (data: any) => void): void { - this.api.businessCallback[code] = callback - } - - getToken(): string { - return localStorage.getItem(this.auth.token_name) ?? '' - } - - setToken(token: string): void { - localStorage.setItem(this.auth.token_name, token) - } - - removeToken(): void { - localStorage.removeItem(this.auth.token_name) - } - - get isLoggedIn(): boolean { - return !!localStorage.getItem(this.auth.token_name) - } - - genS4(): string { - return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0) - .toString(16) - .substring(1) - } - - /** - * 日志工具(仅在 debug 模式下输出) - * - * @example - * api.logDebug('用户登录', { id: 1, name: 'test' }) - * api.logDebug('请求开始', uri, params) - */ - logDebug(...args: any[]): void { - if (!this.debug) return - console.log('[DEBUG]', ...args) - } - - /** - * 获取版本信息 - * - * @param endpointName - 指定从哪个 endpoint 获取版本,默认使用 default endpoint - * @returns 版本信息对象 - * - * @example - * // 从默认 endpoint 获取 - * const versions = await api.getVersion() - * - * // 从指定 endpoint 获取 - * const versions = await api.getVersion('backup') - */ - async getVersion(endpointName?: string): Promise { - const versions: SimApiVersions = { - uiApp: typeof AppVersion === 'undefined' ? "0.0.0-develop" : AppVersion, - uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion, - apiApp: '0.0.0', - apiSimApi: '0.0.0', - apiAppFull: '0.0.0', - apiSimApiFull: '0.0.0', - }; - try { - const resp = await this.query('/versions', {}, endpointName) - if (resp?.data) { - const d = resp.data - versions.apiApp= d.App?.split('+')[0] ?? '0.0.0'; - versions.apiSimApi= d.SimApi?.split('+')[0] ?? '0.0.0'; - versions.apiAppFull= d.App ?? '0.0.0'; - versions.apiSimApiFull= d.SimApi ?? '0.0.0'; - if (this.debug) { - console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`) + constructor(options?: SimApiOptions) { + if (options) { + this.configure(options) } - return versions - } - } catch { - // 版本获取失败返回默认值 - } - return versions; - } - - async query( - uri: string, - params: any = {}, - endpointKey?: string, - extraHeaders?: Record - ): Promise> { - const headers: Record = { ...extraHeaders, ...{} } - const queryId = this.genS4() - - if (!(params instanceof FormData)) { - headers['Content-Type'] = 'application/json' } - const token = this.getToken() - if (token) { - headers['Token'] = token + /** + * 从 window.simapi 读取配置并初始化 + * + * 支持字段:endpoints, defaultEndpoint, debug + * 业务回调(businessCallback / responseCallback)需在代码中处理 + */ + autoInit(): void { + const config = (window as any).simapi + if (!config) return + + if (config.debug !== undefined) { + this.debug = config.debug + } + if (config.endpoints) { + this.api.endpoints = {...this.api.endpoints, ...config.endpoints} + } + if (config.defaultEndpoint) { + this.api.defaultEndpoint = config.defaultEndpoint + } } - if (this.debug) { - headers['Query-Id'] = queryId - console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', localStorage.getItem(this.auth.token_name)) + configure(options: SimApiOptions): void { + if (options.debug !== undefined) { + this.debug = options.debug + } + if (options.auth) { + this.auth = {...this.auth, ...options.auth} + } + if (options.api) { + this.api = { + ...this.api, + ...options.api, + endpoints: {...this.api.endpoints, ...(options.api.endpoints ?? {})}, + businessCallback: {...this.api.businessCallback, ...(options.api.businessCallback ?? {})}, + responseCallback: {...this.api.responseCallback, ...(options.api.responseCallback ?? {})}, + } + } } - const url = this.getEndpoint(endpointKey) + uri - - try { - const respData = await fetchPost( - url, - params, - headers, - this.api.timeout ?? 10000 - ) - if (this.debug) { - console.log('[RESPONSE]', queryId, '->', respData) - } - const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse - - // 业务回调处理 - if (this.api.businessCallback.hasOwnProperty(processedData.code)) { - this.api.businessCallback[processedData.code](processedData) - } else if (this.api.businessCallback['common'] && processedData.code !== 200) { - this.api.businessCallback['common'](processedData) - } - - // code != 200 时抛出业务错误 - if (processedData.code !== 200) { - throw processedData - } - return processedData - - } catch (error: any) { - if (this.debug) { - console.log('[RESPONSE]', queryId, '->', error) - } - // 网络/HTTP 错误:包装成标准响应格式抛出 - if (!error?.code) { - this.api.responseCallback.error(error) - throw { - code: -1, - message: error?.message || '网络错误', - data: error, - } as SimApiBaseResponse - } - // 业务错误直接抛出 - throw error + setEndpoints(endpoints: { [name: string]: string }): void { + this.api.endpoints = {...this.api.endpoints, ...endpoints} } - } - async login(request: Record): Promise> { - const result = await this.query(this.auth.login_url, request) - if (result?.data) { - this.setToken(result.data) + getEndpoint(name?: string): string { + return this.api.endpoints[name ?? this.api.defaultEndpoint] ?? '' } - return result - } - async logout(url?: string | null): Promise { - this.removeToken() - if (url !== null) { - return this.query(url ?? this.auth.logout_url).catch(() => true) + setBusinessCallback(code: number | string, callback: (data: any) => void): void { + this.api.businessCallback[code] = callback } - return true - } - async checkLogin(url?: string | null): Promise { - if (url !== null) { - await this.query(url ?? this.auth.check_url).catch(() => {}) - } else if (this.getToken()) { - this.api.businessCallback[401]?.(null) + getToken(): string { + return localStorage.getItem(this.auth.token_name) ?? '' + } + + setToken(token: string): void { + localStorage.setItem(this.auth.token_name, token) + } + + removeToken(): void { + localStorage.removeItem(this.auth.token_name) + } + + get isLoggedIn(): boolean { + return !!localStorage.getItem(this.auth.token_name) + } + + genS4(): string { + return (((1 + Math.random()) * 0x10000 * Date.parse(new Date().toString())) | 0) + .toString(16) + .substring(1) + } + + /** + * 日志工具(仅在 debug 模式下输出) + * + * @example + * api.logDebug('用户登录', { id: 1, name: 'test' }) + * api.logDebug('请求开始', uri, params) + */ + logDebug(...args: any[]): void { + if (!this.debug) return + console.log('[DEBUG]', ...args) + } + + /** + * 获取版本信息 + * + * @param endpointName - 指定从哪个 endpoint 获取版本,默认使用 default endpoint + * @returns 版本信息对象 + * + * @example + * // 从默认 endpoint 获取 + * const versions = await api.getVersion() + * + * // 从指定 endpoint 获取 + * const versions = await api.getVersion('backup') + */ + async getVersion(endpointName?: string): Promise { + const versions: SimApiVersions = { + uiApp: typeof AppVersion === 'undefined' ? "0.0.0-develop" : AppVersion, + uiSimApi: typeof SimApiVersion === 'undefined' ? "0.0.0-develop" : SimApiVersion, + apiApp: '0.0.0', + apiSimApi: '0.0.0', + apiAppFull: '0.0.0', + apiSimApiFull: '0.0.0', + }; + try { + const resp = await this.query('/versions', {}, endpointName) + if (resp?.data) { + const d = resp.data + versions.apiApp = d.App?.split('+')[0] ?? '0.0.0'; + versions.apiSimApi = d.SimApi?.split('+')[0] ?? '0.0.0'; + versions.apiAppFull = d.App ?? '0.0.0'; + versions.apiSimApiFull = d.SimApi ?? '0.0.0'; + if (this.debug) { + console.log(`UI主应用版本: ${versions.uiApp}\nUISimApi版本: ${versions.uiSimApi}\nAPI主应用版本: ${versions.apiApp}\nAPISimApi版本: ${versions.apiSimApi}`) + } + return versions + } + } catch { + // 版本获取失败返回默认值 + } + return versions; + } + + async query( + uri: string, + params: any = {}, + endpointKey?: string, + extraHeaders?: Record + ): Promise> { + const headers: Record = {...extraHeaders, ...{}} + const queryId = this.genS4() + + if (!(params instanceof FormData)) { + headers['Content-Type'] = 'application/json' + } + + const token = this.getToken() + if (token) { + headers['Token'] = token + } + + if (this.debug) { + headers['Query-Id'] = queryId + console.log('[REQUEST*]', queryId, '->', uri, 'AUTH:', localStorage.getItem(this.auth.token_name)) + } + + const url = this.getEndpoint(endpointKey) + uri + + try { + const respData = await fetchPost( + url, + params, + headers, + this.api.timeout ?? 10000 + ) + if (this.debug) { + console.log('[RESPONSE]', queryId, '->', respData) + } + const processedData = this.api.responseCallback.success(respData) as SimApiBaseResponse + + // 业务回调处理 + if (this.api.businessCallback.hasOwnProperty(processedData.code)) { + this.api.businessCallback[processedData.code](processedData) + } else if (this.api.businessCallback['common'] && processedData.code !== 200) { + this.api.businessCallback['common'](processedData) + } + + // code != 200 时抛出业务错误 + if (processedData.code !== 200) { + throw processedData + } + return processedData + + } catch (error: any) { + if (this.debug) { + console.log('[RESPONSE]', queryId, '->', error) + } + // 网络/HTTP 错误:包装成标准响应格式抛出 + if (!error?.code) { + this.api.responseCallback.error(error) + throw { + code: -1, + message: error?.message || '网络错误', + data: error, + } as SimApiBaseResponse + } + // 业务错误直接抛出 + throw error + } + } + + async login(request: Record): Promise> { + const result = await this.query(this.auth.login_url, request) + if (result?.data) { + this.setToken(result.data) + } + return result + } + + async logout(url?: string | null): Promise { + if (url !== null) { + return this.query(url ?? this.auth.logout_url).catch(() => true) + } + this.removeToken() + return true + } + + async checkLogin(url?: string | null): Promise { + if (url !== null) { + await this.query(url ?? this.auth.check_url).catch(() => { + }) + } else if (this.getToken()) { + this.api.businessCallback[401]?.(null) + } } - } }