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