diff --git a/app/_layout.tsx b/app/_layout.tsx
index e2420b7..f906717 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,17 +1,22 @@
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { RoleProvider } from '../src/context/RoleContext';
+import { AuthProvider } from '../src/context/AuthContext';
/**
* 根布局:用 Stack 包裹整个应用,Tab 组不显示顶部 header
*/
export default function RootLayout() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -19,6 +24,7 @@ export default function RootLayout() {
-
+
+
);
}
\ No newline at end of file
diff --git a/app/guide.tsx b/app/guide.tsx
new file mode 100644
index 0000000..316e98e
--- /dev/null
+++ b/app/guide.tsx
@@ -0,0 +1,194 @@
+import { Ionicons } from '@expo/vector-icons';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useRouter } from 'expo-router';
+import { useRef, useState } from 'react';
+import {
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Text,
+ useWindowDimensions,
+ View,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { colors, headerGradientShort } from '../src/theme';
+import { useAuth } from '../src/context/AuthContext';
+
+interface GuideSlide {
+ icon: string;
+ title: string;
+ desc: string;
+}
+
+/** 引导页内容:聚焦职来四大差异化能力 */
+const SLIDES: GuideSlide[] = [
+ {
+ icon: 'compass-outline',
+ title: '双向透明求职生态',
+ desc: '看真实公司评价与行业声誉,让信息差不再成为求职的障碍。',
+ },
+ {
+ icon: 'options-outline',
+ title: '自定义评价维度',
+ desc: '按你的关注点设置权重,实时算出综合得分,找到最匹配的公司。',
+ },
+ {
+ icon: 'game-controller-outline',
+ title: '通关小游戏',
+ desc: '7 关闯关引导你走完「认识自己 → 签约」的完整求职链路。',
+ },
+ {
+ icon: 'shield-checkmark-outline',
+ title: '阿布阿诺 AI 法律服务',
+ desc: '免费咨询与模板,付费律师与合同审核,求职权益有保障。',
+ },
+];
+
+export default function GuideScreen() {
+ const router = useRouter();
+ const { completeOnboarding } = useAuth();
+ const { width } = useWindowDimensions();
+ const [page, setPage] = useState(0);
+ const scrollRef = useRef(null);
+
+ const isLast = page === SLIDES.length - 1;
+
+ const finish = () => {
+ completeOnboarding();
+ router.replace('/login');
+ };
+
+ const goNext = () => {
+ if (isLast) {
+ finish();
+ return;
+ }
+ const next = page + 1;
+ setPage(next);
+ scrollRef.current?.scrollTo({ x: next * width, animated: true });
+ };
+
+ return (
+
+
+
+
+ 跳过
+
+
+
+ {
+ const index = Math.round(e.nativeEvent.contentOffset.x / width);
+ setPage(index);
+ }}
+ style={styles.pager}
+ >
+ {SLIDES.map((slide) => (
+
+
+
+
+ {slide.title}
+ {slide.desc}
+
+ ))}
+
+
+
+
+ {SLIDES.map((slide, i) => (
+
+ ))}
+
+
+
+ {isLast ? '立即体验' : '下一步'}
+
+
+
+ {page + 1} / {SLIDES.length}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1, backgroundColor: colors.white },
+ topBar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ },
+ topSpacer: { width: 40 },
+ skipText: { fontSize: 14, color: colors.textSecondary, paddingHorizontal: 4 },
+ pager: { flex: 1 },
+ slide: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 36,
+ },
+ illus: {
+ width: 132,
+ height: 132,
+ borderRadius: 36,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 36,
+ },
+ slideTitle: {
+ fontSize: 22,
+ fontWeight: '800',
+ color: colors.textPrimary,
+ textAlign: 'center',
+ marginBottom: 14,
+ },
+ slideDesc: {
+ fontSize: 15,
+ color: colors.textSecondary,
+ textAlign: 'center',
+ lineHeight: 24,
+ },
+ bottom: {
+ alignItems: 'center',
+ paddingHorizontal: 36,
+ paddingBottom: 28,
+ },
+ dots: { flexDirection: 'row', gap: 8, marginBottom: 24 },
+ dot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: colors.border,
+ },
+ dotActive: { width: 22, backgroundColor: colors.primary },
+ nextBtn: {
+ width: '100%',
+ height: 50,
+ borderRadius: 25,
+ overflow: 'hidden',
+ },
+ nextGradient: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ nextText: { fontSize: 16, fontWeight: '700', color: colors.textInverse },
+ pageHint: {
+ marginTop: 16,
+ fontSize: 13,
+ color: colors.textHint,
+ },
+});
diff --git a/app/index.tsx b/app/index.tsx
new file mode 100644
index 0000000..51be2f1
--- /dev/null
+++ b/app/index.tsx
@@ -0,0 +1,23 @@
+import { Redirect } from 'expo-router';
+import { useAuth } from '../src/context/AuthContext';
+import { useRole } from '../src/context/RoleContext';
+
+/**
+ * 应用总入口(根路由 /)。
+ * 按「引导 → 登录 → 选择身份 → 对应端」漏斗放行,保证每个页面都有合理的前置入口。
+ */
+export default function Index() {
+ const { onboardingSeen, isLoggedIn } = useAuth();
+ const { role } = useRole();
+
+ if (!onboardingSeen) {
+ return ;
+ }
+ if (!isLoggedIn) {
+ return ;
+ }
+ if (!role) {
+ return ;
+ }
+ return ;
+}
diff --git a/app/login.tsx b/app/login.tsx
new file mode 100644
index 0000000..966e12d
--- /dev/null
+++ b/app/login.tsx
@@ -0,0 +1,264 @@
+import { Ionicons } from '@expo/vector-icons';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useRouter } from 'expo-router';
+import { useEffect, useRef, useState } from 'react';
+import {
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { colors, headerGradientShort } from '../src/theme';
+import { useAuth } from '../src/context/AuthContext';
+import { useLogin } from '../src/hooks';
+
+const PHONE_RE = /^1\d{10}$/;
+
+export default function LoginScreen() {
+ const router = useRouter();
+ const { login: authLogin } = useAuth();
+ const { login, loading, error } = useLogin();
+
+ const [phone, setPhone] = useState('');
+ const [code, setCode] = useState('');
+ const [agreed, setAgreed] = useState(false);
+ const [countdown, setCountdown] = useState(0);
+ const timerRef = useRef | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (timerRef.current) clearInterval(timerRef.current);
+ };
+ }, []);
+
+ const phoneValid = PHONE_RE.test(phone);
+ const canSubmit = phoneValid && code.length >= 4 && agreed && !loading;
+
+ const startCountdown = () => {
+ if (!phoneValid || countdown > 0) return;
+ setCountdown(60);
+ timerRef.current = setInterval(() => {
+ setCountdown((c) => {
+ if (c <= 1) {
+ if (timerRef.current) clearInterval(timerRef.current);
+ return 0;
+ }
+ return c - 1;
+ });
+ }, 1000);
+ };
+
+ const handleLogin = async () => {
+ if (!canSubmit) return;
+ try {
+ const res = await login(phone, code);
+ authLogin(res.token);
+ router.replace('/identity-select');
+ } catch {
+ // 错误已由 error 向上展示
+ }
+ };
+
+ const handleGuest = () => {
+ router.replace('/identity-select');
+ };
+
+ return (
+
+
+
+ 职
+
+ 登录职来
+ 智能求职 · 企业招聘一站式服务平台
+
+
+
+
+ 手机号
+
+
+
+
+ 验证码
+
+
+ 0 || !phoneValid) && styles.codeBtnDisabled]}
+ onPress={startCountdown}
+ disabled={countdown > 0 || !phoneValid}
+ >
+
+ {countdown > 0 ? `${countdown}s 后重发` : '获取验证码'}
+
+
+
+
+
+ {error && {error.message}}
+
+
+ {loading ? '登录中…' : '登录 / 注册'}
+
+
+
+ setAgreed((v) => !v)}
+ hitSlop={6}
+ >
+ {agreed && }
+
+
+ 已阅读并同意
+ router.push('/settings/privacy-policy')}>
+ 《隐私政策》
+
+ 和
+ router.push('/settings/user-agreement')}>
+ 《用户协议》
+
+
+
+
+
+ 其他登录方式
+
+ {}} hitSlop={8}>
+
+
+ {}} hitSlop={8}>
+
+
+
+
+
+
+
+ 暂不登录,先逛逛
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1, backgroundColor: colors.pageBg },
+ header: {
+ alignItems: 'center',
+ paddingTop: 44,
+ paddingBottom: 36,
+ },
+ logoCircle: {
+ width: 64,
+ height: 64,
+ borderRadius: 20,
+ backgroundColor: colors.white,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 14,
+ },
+ logoText: { fontSize: 30, fontWeight: '800', color: colors.primary },
+ headerTitle: { fontSize: 22, fontWeight: '800', color: colors.textInverse },
+ headerSub: { fontSize: 13, color: colors.textInverse, opacity: 0.9, marginTop: 6 },
+ body: { flexGrow: 1, padding: 20, paddingTop: 24 },
+ field: { marginBottom: 18 },
+ label: { fontSize: 14, fontWeight: '600', color: colors.textPrimary, marginBottom: 8 },
+ input: {
+ height: 50,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: colors.border,
+ backgroundColor: colors.white,
+ paddingHorizontal: 14,
+ fontSize: 16,
+ color: colors.textPrimary,
+ },
+ codeRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
+ codeInput: { flex: 1 },
+ codeBtn: {
+ height: 50,
+ paddingHorizontal: 16,
+ borderRadius: 12,
+ backgroundColor: colors.primaryLight,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ codeBtnDisabled: { backgroundColor: colors.borderLight, opacity: 0.7 },
+ codeBtnText: { fontSize: 14, fontWeight: '600', color: colors.primaryText },
+ errorText: {
+ fontSize: 13,
+ color: colors.danger,
+ marginBottom: 12,
+ },
+ submitBtn: {
+ height: 50,
+ borderRadius: 25,
+ backgroundColor: colors.primaryBtn,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: 8,
+ },
+ submitDisabled: { backgroundColor: colors.border, opacity: 0.7 },
+ submitText: { fontSize: 16, fontWeight: '700', color: colors.textInverse },
+ agreement: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginTop: 16,
+ },
+ checkbox: {
+ width: 18,
+ height: 18,
+ borderRadius: 5,
+ borderWidth: 1.5,
+ borderColor: colors.borderStrong,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: 2,
+ marginRight: 8,
+ },
+ checkboxChecked: { backgroundColor: colors.primary, borderColor: colors.primary },
+ agreementText: { flex: 1, fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
+ link: { color: colors.primaryText, fontWeight: '600' },
+ others: { alignItems: 'center', marginTop: 32 },
+ othersTitle: { fontSize: 12, color: colors.textHint, marginBottom: 16 },
+ othersRow: { flexDirection: 'row', gap: 36 },
+ otherItem: {
+ width: 52,
+ height: 52,
+ borderRadius: 26,
+ backgroundColor: colors.white,
+ borderWidth: 1,
+ borderColor: colors.borderLight,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ guest: {
+ alignItems: 'center',
+ paddingVertical: 18,
+ backgroundColor: colors.pageBg,
+ },
+ guestText: { fontSize: 14, color: colors.textHint },
+});