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);