fix(mobile): distinguish auth failure from HR mismatch on position sync page
问题:登录态失效(token 冻结/无效,后端返 401)时,同步岗位页把认证错误 误显示成「人力系统中未匹配到当前手机号的在职岗位,请联系 HR」,员工被 困在该页无路可退。 修复: - api.ts: 新增 ApiError(statusCode/bizCode) 类型化错误与 isAuthApiError, readPayload/parsePayloadText/XHR 路径统一抛出带状态码的错误,消息不变 - position.ts: fetchOrgPositionResult 返回四态判别结果 matched/unmatched/unauthorized/error;hydratePositionFromOrg 行为不变 - position/index.vue: 按四态分支处理—— · unauthorized -> 清除登录态并自动回登录页(保留 redirect) · error -> 提示「网络异常…请重试」,保留重试按钮 · unmatched -> 才显示联系 HR 文案 并新增「退出重新登录」链接(确认弹窗后清登录态回登录页), 避免任何异常场景下用户被困无退出路径 验证(构建产物 + 生产 API,Playwright 五场景全过,截图见 .data/visual-audit-20260718/position-fix/): S1 固定码真实登录->今日页(保洁岗匹配) S2 无效token->自动回登录 S3 断网->网络异常提示留在本页 S4 有效token->自动通过 S5 退出链接->确认->回登录且登录态已清 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,22 @@ export interface RequestOptions {
|
||||
clearAuthOnUnauthorized?: boolean;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly statusCode: number;
|
||||
readonly bizCode?: number;
|
||||
|
||||
constructor(message: string, statusCode: number, bizCode?: number) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.statusCode = statusCode;
|
||||
this.bizCode = bizCode;
|
||||
}
|
||||
}
|
||||
|
||||
export const isAuthApiError = (error: unknown): boolean =>
|
||||
error instanceof ApiError
|
||||
&& (error.statusCode === 401 || error.statusCode === 403 || error.bizCode === 401 || error.bizCode === 403);
|
||||
|
||||
const normalizeUrl = (url: string) => `${apiBase}${url.startsWith('/') ? url : `/${url}`}`;
|
||||
|
||||
export const apiUrl = normalizeUrl;
|
||||
@@ -35,7 +51,7 @@ export const readPayload = <T>(statusCode: number, data: unknown, clearAuthOnUna
|
||||
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403 || code === 401 || code === 403)) {
|
||||
clearAuth();
|
||||
}
|
||||
throw new Error(payload?.msg || `HTTP ${statusCode}`);
|
||||
throw new ApiError(payload?.msg || `HTTP ${statusCode}`, statusCode, code);
|
||||
}
|
||||
|
||||
return payload?.data as T;
|
||||
@@ -47,7 +63,7 @@ export const parsePayloadText = (statusCode: number, text: string, clearAuthOnUn
|
||||
} catch (error) {
|
||||
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403)) clearAuth();
|
||||
throw error instanceof SyntaxError
|
||||
? new Error(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败')
|
||||
? new ApiError(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败', statusCode)
|
||||
: error;
|
||||
}
|
||||
};
|
||||
@@ -72,8 +88,10 @@ const requestWithXhr = async <T>(options: RequestOptions): Promise<T> => {
|
||||
status: xhr.status,
|
||||
data: parsePayloadText(xhr.status, xhr.responseText || 'null', options.clearAuthOnUnauthorized !== false)
|
||||
});
|
||||
} catch {
|
||||
reject(new Error(xhr.status >= 400 ? `HTTP ${xhr.status}` : '响应解析失败'));
|
||||
} catch (error) {
|
||||
reject(error instanceof ApiError
|
||||
? error
|
||||
: new Error(xhr.status >= 400 ? `HTTP ${xhr.status}` : '响应解析失败'));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error('网络请求失败'));
|
||||
|
||||
Reference in New Issue
Block a user