diff --git a/mobile-uni/src/pages/auth/position/index.vue b/mobile-uni/src/pages/auth/position/index.vue index f8d0021f..93fa8cf5 100644 --- a/mobile-uni/src/pages/auth/position/index.vue +++ b/mobile-uni/src/pages/auth/position/index.vue @@ -27,6 +27,10 @@ + + + 退出重新登录 + @@ -34,12 +38,13 @@ import { computed, ref } from 'vue'; import { onLoad, onShow } from '@dcloudio/uni-app'; import { + clearAuth, getAuth, isLoggedIn, rememberLoginRedirect, type UserPosition } from '@/services/auth'; -import { goToMobileTarget, hydratePositionFromOrg } from '@/services/position'; +import { fetchOrgPositionResult, goToMobileTarget } from '@/services/position'; import { resetPageScroll } from '@/services/navigation'; const redirect = ref('/pages/user/today/index'); @@ -52,22 +57,49 @@ const finish = () => { goToMobileTarget(redirect.value || '/pages/user/today/index'); }; +const goLogin = () => { + rememberLoginRedirect(redirect.value); + uni.redirectTo({ url: '/pages/auth/login/index' }); +}; + +const logoutAndRelogin = () => { + uni.showModal({ + title: '退出重新登录', + content: '将清除当前登录状态并返回登录页,确认退出?', + confirmText: '退出', + cancelText: '再想想', + success: (res) => { + if (!res.confirm) return; + clearAuth(); + goLogin(); + } + }); +}; + const autoFillFromOrg = async () => { if (saving.value) return; if (!isLoggedIn()) { - rememberLoginRedirect(redirect.value); - uni.redirectTo({ url: '/pages/auth/login/index' }); + goLogin(); return; } saving.value = true; message.value = ''; try { - const officialPosition = await hydratePositionFromOrg(); - if (officialPosition) { - selectedPosition.value = officialPosition; + const result = await fetchOrgPositionResult(); + if (result.status === 'matched') { + selectedPosition.value = result.position; finish(); return; } + if (result.status === 'unauthorized') { + clearAuth(); + goLogin(); + return; + } + if (result.status === 'error') { + message.value = '网络异常,暂时没有同步到岗位。请检查网络后点击「重新同步岗位」重试。'; + return; + } message.value = '人力系统中未匹配到当前手机号的在职岗位,请联系 HR 核对手机号、在职状态和岗位信息。'; } finally { saving.value = false; @@ -208,4 +240,16 @@ onShow(() => { .confirm-button[disabled] { opacity: 0.45; } + +.logout-row { + margin-top: 22px; + text-align: center; +} + +.logout-link { + color: #8a93a0; + font-size: 15px; + text-decoration: underline; + padding: 10px 14px; +} diff --git a/mobile-uni/src/services/api.ts b/mobile-uni/src/services/api.ts index 31dfc9c6..c2fe5ee9 100644 --- a/mobile-uni/src/services/api.ts +++ b/mobile-uni/src/services/api.ts @@ -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 = (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 (options: RequestOptions): Promise => { 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('网络请求失败')); diff --git a/mobile-uni/src/services/position.ts b/mobile-uni/src/services/position.ts index b6704abe..36d9e575 100644 --- a/mobile-uni/src/services/position.ts +++ b/mobile-uni/src/services/position.ts @@ -1,5 +1,5 @@ import type { MobileIdentity, RoleKey } from '@/types/api'; -import { apiRequest } from './api'; +import { apiRequest, isAuthApiError } from './api'; import { clearOrgPosition, getSelectedPosition, @@ -63,7 +63,13 @@ export const goToMobileTarget = (url: string) => { uni.redirectTo({ url }); }; -export const hydratePositionFromOrg = async (): Promise => { +export type OrgPositionResult = + | { status: 'matched'; position: UserPosition } + | { status: 'unmatched' } + | { status: 'unauthorized' } + | { status: 'error' }; + +export const fetchOrgPositionResult = async (): Promise => { try { const identity = await apiRequest({ url: '/api/aihr/mobile/me', @@ -72,13 +78,23 @@ export const hydratePositionFromOrg = async (): Promise => { clearAuthOnUnauthorized: false }); const officialPosition = saveOrgPosition(identity.positionName); - if (!officialPosition) clearOrgPosition(); - return officialPosition; - } catch { - return hasOrgPosition() ? getSelectedPosition() : ''; + if (!officialPosition) { + clearOrgPosition(); + return { status: 'unmatched' }; + } + return { status: 'matched', position: officialPosition }; + } catch (error) { + return { status: isAuthApiError(error) ? 'unauthorized' : 'error' }; } }; +export const hydratePositionFromOrg = async (): Promise => { + const result = await fetchOrgPositionResult(); + if (result.status === 'matched') return result.position; + if (result.status === 'unmatched') return ''; + return hasOrgPosition() ? getSelectedPosition() : ''; +}; + export const nextUserRouteAfterLogin = async (redirect: string) => { const role = await hydrateMobileRole(); const roleRoute = routeForMobileRole(role, redirect);