import { useAppConfig } from '@vben/hooks'; import { preferences } from '@vben/preferences'; import { unwrapAbpResponse } from '#/api/th/abp-unwrap'; /** 绑定门店(与 ThAppLoginOutputDto.locations 一致) */ export interface ThAppBoundLocationDto { id: string; locationCode: string; locationName: string; fullAddress: string; state: boolean; } export interface ThAppLoginInput { tenantId: string; email: string; password: string; uuid?: string; code?: string; } export interface ThAppLoginOutputDto { token: string; refreshToken: string; tenantId: string; tenantName: string; locations: ThAppBoundLocationDto[]; } const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD); /** 防止重复提交导致前一个请求被 Abort、后一个已在 Network 成功但 UI 仍报错 */ let loginInFlight: Promise | null = null; function getApiBase(): string { const raw = (import.meta.env.VITE_GLOB_API_URL as string | undefined) ?? '/dev-api'; return raw.replace(/^"|"$/g, '').replace(/\/$/, ''); } function normalizeLocation(raw: Record): ThAppBoundLocationDto { return { id: String(raw.id ?? raw.Id ?? ''), locationCode: String(raw.locationCode ?? raw.LocationCode ?? ''), locationName: String(raw.locationName ?? raw.LocationName ?? ''), fullAddress: String(raw.fullAddress ?? raw.FullAddress ?? ''), state: raw.state !== false && raw.State !== false, }; } function normalizeLoginOutput(raw: unknown): ThAppLoginOutputDto { const o = (raw && typeof raw === 'object' ? raw : {}) as Record; const locs = o.locations ?? o.Locations; const arr = Array.isArray(locs) ? locs : []; return { token: String(o.token ?? o.Token ?? ''), refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''), tenantId: String(o.tenantId ?? o.TenantId ?? ''), tenantName: String(o.tenantName ?? o.TenantName ?? ''), locations: arr.map((x) => normalizeLocation(x as Record), ), }; } function normalizeLocationList(raw: unknown): ThAppBoundLocationDto[] { const arr = Array.isArray(raw) ? raw : []; return arr.map((x) => normalizeLocation(x as Record)); } /** * 使用 XHR 登录:dev 代理下 fetch/axios 常出现「Network 已有 body 但 JS 一直等连接结束」。 * XHR onload 在收齐 responseText 后即触发,不依赖 fetch 的 body 流结束。 */ function thAppLoginXhr(input: ThAppLoginInput): Promise { const url = `${getApiBase()}/th-app-auth/login`; const language = preferences.app.locale.replace('-', '_'); const body = JSON.stringify({ tenantId: input.tenantId, email: input.email.trim(), password: input.password, ...(input.uuid ? { uuid: input.uuid } : {}), ...(input.code != null && input.code !== '' ? { code: input.code } : {}), }); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.timeout = 60_000; xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8'); xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Accept-Language', language); xhr.setRequestHeader('Content-Language', language); if (clientId) { xhr.setRequestHeader('ClientID', clientId); } xhr.onload = () => { try { const text = xhr.responseText ?? ''; const json = text ? JSON.parse(text) : null; if (xhr.status < 200 || xhr.status >= 300) { try { unwrapAbpResponse(json); } catch (e) { reject(e instanceof Error ? e : new Error(`登录失败 HTTP ${xhr.status}`)); return; } reject(new Error(`登录失败 HTTP ${xhr.status}`)); return; } const data = unwrapAbpResponse(json); resolve(normalizeLoginOutput(data)); } catch (e) { reject( e instanceof Error ? e : new Error('登录响应解析失败,请查看 Network 中 login 的 Response'), ); } }; xhr.onerror = () => { reject( new Error( '登录网络异常:请确认 dev 服务已启动且代理地址正确(/dev-api → saas-test)', ), ); }; xhr.ontimeout = () => { reject( new Error( '登录请求超时:若 Network 中已有 token 响应,请刷新页面后只点一次登录', ), ); }; xhr.send(body); }); } /** POST /api/app/th-app-auth/login(匿名) */ export async function thAppLogin( input: ThAppLoginInput, ): Promise { if (loginInFlight) { return loginInFlight; } loginInFlight = thAppLoginXhr(input).finally(() => { loginInFlight = null; }); return loginInFlight; } /** GET /api/app/th-app-auth/my-locations(Bearer + 租户上下文) */ export async function thAppMyLocations(): Promise { const { requestClient } = await import('#/api/request'); const raw = await requestClient.get('th-app-auth/my-locations', { timeout: 30_000, }); return normalizeLocationList(raw); }