From a2fa2bad44362c1acbbc58f606cff24550f47a25 Mon Sep 17 00:00:00 2001 From: doublex001 <16157008+doublex001@user.noreply.gitee.com> Date: Mon, 31 Aug 2026 17:49:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E9=A1=B5/=E7=99=BB=E5=BD=95=E9=A1=B5=E5=B9=B6=E6=8E=A5?= =?UTF-8?q?=E9=80=9A=E5=85=A5=E5=8F=A3=E6=BC=8F=E6=96=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/index.tsx 总闸门:引导→登录→选择身份→对应端(Redirect 漏斗) - app/guide.tsx 引导页(4 屏职来核心能力,分页+跳过+立即体验) - app/login.tsx 登录页(手机号+验证码倒计时、协议勾选、其他登录、游客浏览) - _layout 包 AuthProvider 并注册 guide/login/index 屏 - 避开 onboarding.tsx(入职清单功能页),引导页命名为 guide.tsx --- app/_layout.tsx | 18 ++-- app/guide.tsx | 194 +++++++++++++++++++++++++++++++++++ app/index.tsx | 23 +++++ app/login.tsx | 264 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 493 insertions(+), 6 deletions(-) create mode 100644 app/guide.tsx create mode 100644 app/index.tsx create mode 100644 app/login.tsx 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 }, +});