轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

146 lines
4.4 KiB

import { Redirect, useRouter } from 'expo-router';
import { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
import { useAuth } from '../src/context/AuthContext';
import { colors, spacing } from '../src/theme';
/**
* 应用总入口(根路由 /)。
* 按「引导 → 登录 → 与服务端核对身份 → 对应端」漏斗放行。
*
* **角色不以本地缓存为准**:本地存的 role 只代表上次是什么。冷启动有 Token 时先调
* `/auth/me` 核对,核对成功才跳业务页;核对失败**不**退回本地角色硬跳 ——
* 那会把已被后台改成企业(或已禁用)的账号继续放进求职端,页面按错误身份渲染并一路报错。
* 失败时停在本页给出重试入口,把决定权交回用户。
*/
export default function Index() {
const {
onboardingSeen,
isTokenValid,
isHydrated: authHydrated,
role,
isRoleVerified,
verifySession,
} = useAuth();
const [verifying, setVerifying] = useState(false);
const [verifyError, setVerifyError] = useState<Error | null>(null);
/** 重试计数:仅用于重新触发核对 effect */
const [attempt, setAttempt] = useState(0);
const router = useRouter();
/** 需要核对的条件:本地存储已就绪 + 看过引导 + 有有效 Token + 尚未核对过 */
const needVerify = authHydrated && onboardingSeen && isTokenValid && !isRoleVerified;
useEffect(() => {
if (!needVerify) return;
let active = true;
setVerifying(true);
setVerifyError(null);
verifySession()
.catch((e: unknown) => {
if (active) setVerifyError(e instanceof Error ? e : new Error('身份核对失败'));
})
.finally(() => {
if (active) setVerifying(false);
});
return () => {
active = false;
};
}, [needVerify, verifySession, attempt]);
const retry = useCallback(() => setAttempt((n) => n + 1), []);
// 本地存储读取完成前不做任何跳转决策,否则已登录用户会闪现引导页/注册页
if (!authHydrated) {
return <View style={styles.container} />;
}
if (!onboardingSeen) {
return <Redirect href="/guide" />;
}
if (!isTokenValid) {
return <Redirect href="/register" />;
}
// 有 Token 但角色未与服务器核对:核对中或失败都不能放行
if (!isRoleVerified) {
if (verifying) {
return (
<View style={[styles.container, styles.center]}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.hint}>正在确认登录状态…</Text>
</View>
);
}
return (
<View style={[styles.container, styles.center]}>
<Text style={styles.errorTitle}>无法确认登录状态</Text>
<Text style={styles.hint}>{verifyError?.message ?? '请检查网络后重试'}</Text>
<Pressable style={styles.retryBtn} onPress={retry}>
<Text style={styles.retryText}>重试</Text>
</Pressable>
<Pressable style={styles.linkBtn} onPress={() => router.replace('/register')}>
<Text style={styles.linkText}>重新登录</Text>
</Pressable>
</View>
);
}
// 核对通过但服务器说角色未定(UNASSIGNED)→ 去选身份
if (!role) {
return <Redirect href="/register" />;
}
// 两个路由组均无 index,必须用显式落地路由,否则落地页空白
return (
<Redirect
href={role === 'candidate' ? '/(tabs)/jobs' : '/(enterprise)/job-management'}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
},
center: {
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.framework,
},
hint: {
marginTop: spacing.card,
fontSize: 13,
color: colors.textSecondary,
textAlign: 'center',
},
errorTitle: {
fontSize: 17,
fontWeight: '700',
color: colors.textPrimary,
},
retryBtn: {
marginTop: spacing.framework,
paddingHorizontal: 28,
height: 40,
borderRadius: spacing.card,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primaryBtn,
},
retryText: {
fontSize: 14,
fontWeight: '600',
color: colors.white,
},
linkBtn: {
marginTop: spacing.card,
},
linkText: {
fontSize: 13,
fontWeight: '600',
color: colors.primaryText,
},
});