Browse Source

fix: 修复选身份后『进不去』并补充分端必经步骤

- identity-select 卡片点击即跳转(原仅显示对勾无反应);按 candidateSetupDone/enterpriseVerified 分流到下一步
- 新增 app/job-preference.tsx 求职意向设置页(求职者必经,保存后置位标记进 /(tabs)/jobs,可稍后设置)
- enterprise-certification 提交/稍后认证均 completeEnterpriseVerify 并 replace 到 /(enterprise)/enterprise-overview(原为死胡同)
- index 闸门改用显式落地路由(/(tabs)/jobs、/(enterprise)/enterprise-overview),避免路由组无 index 导致空白
master
doublex001 2 months ago
parent
commit
756960b32e
  1. 20
      app/(enterprise)/enterprise-certification.tsx
  2. 55
      app/identity-select.tsx
  3. 7
      app/index.tsx
  4. 251
      app/job-preference.tsx

20
app/(enterprise)/enterprise-certification.tsx

@ -12,6 +12,7 @@ import {
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useEnterpriseCertification } from '../../src/hooks'; import { useEnterpriseCertification } from '../../src/hooks';
import { useAuth } from '../../src/context/AuthContext';
import { colors } from '../../src/theme'; import { colors } from '../../src/theme';
import type { EnterpriseCertificationForm, EnterpriseCertificationStatus } from '../../src/types'; import type { EnterpriseCertificationForm, EnterpriseCertificationStatus } from '../../src/types';
@ -88,6 +89,7 @@ function Field({
export default function EnterpriseCertificationScreen() { export default function EnterpriseCertificationScreen() {
const router = useRouter(); const router = useRouter();
const { completeEnterpriseVerify } = useAuth();
const { certification, loading, error, refresh, submitting, submit, status } = useEnterpriseCertification(); const { certification, loading, error, refresh, submitting, submit, status } = useEnterpriseCertification();
const [form, setForm] = useState<EnterpriseCertificationForm>({ const [form, setForm] = useState<EnterpriseCertificationForm>({
companyName: '', companyName: '',
@ -132,6 +134,15 @@ export default function EnterpriseCertificationScreen() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!form.companyName || !form.creditCode || !form.contactName || !form.contactPhone) return; if (!form.companyName || !form.creditCode || !form.contactName || !form.contactPhone) return;
await submit(form); await submit(form);
// 提交即视为走完「企业认证」必经步骤,标记后进入企业端(审核中也可浏览)
completeEnterpriseVerify();
router.replace('/(enterprise)/enterprise-overview');
};
const handleSkip = () => {
// 允许稍后认证:标记步骤已完成,先进企业端
completeEnterpriseVerify();
router.replace('/(enterprise)/enterprise-overview');
}; };
return ( return (
@ -166,7 +177,7 @@ export default function EnterpriseCertificationScreen() {
<Ionicons name="checkmark-circle-outline" size={48} color={colors.success} /> <Ionicons name="checkmark-circle-outline" size={48} color={colors.success} />
<Text style={styles.successTitle}>企业认证已通过</Text> <Text style={styles.successTitle}>企业认证已通过</Text>
<Text style={styles.successDesc}>您已进入企业端,可发布岗位并管理招聘流程</Text> <Text style={styles.successDesc}>您已进入企业端,可发布岗位并管理招聘流程</Text>
<Pressable style={styles.successBtn} onPress={() => router.push('/enterprise-overview')}> <Pressable style={styles.successBtn} onPress={() => router.replace('/(enterprise)/enterprise-overview')}>
<Text style={styles.successBtnText}>进入企业端</Text> <Text style={styles.successBtnText}>进入企业端</Text>
</Pressable> </Pressable>
</View> </View>
@ -217,6 +228,13 @@ export default function EnterpriseCertificationScreen() {
> >
<Text style={styles.submitBtnText}>{submitting ? '提交中…' : '提交认证'}</Text> <Text style={styles.submitBtnText}>{submitting ? '提交中…' : '提交认证'}</Text>
</Pressable> </Pressable>
<Pressable
style={{ alignItems: 'center', paddingVertical: 14 }}
onPress={handleSkip}
hitSlop={6}
>
<Text style={{ fontSize: 14, color: colors.textHint }}>稍后认证</Text>
</Pressable>
</View> </View>
)} )}
<View style={styles.bottomPad} /> <View style={styles.bottomPad} />

55
app/identity-select.tsx

@ -12,6 +12,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useIdentityOptions } from '../src/hooks'; import { useIdentityOptions } from '../src/hooks';
import { useRole } from '../src/context/RoleContext'; import { useRole } from '../src/context/RoleContext';
import { useAuth } from '../src/context/AuthContext';
import { colors } from '../src/theme'; import { colors } from '../src/theme';
import type { IdentityOption } from '../src/types'; import type { IdentityOption } from '../src/types';
@ -99,17 +100,27 @@ function ErrorState({ onRetry }: { onRetry: () => void }) {
export default function IdentitySelectScreen() { export default function IdentitySelectScreen() {
const router = useRouter(); const router = useRouter();
const { setRole } = useRole(); const { setRole } = useRole();
const { candidateSetupDone, enterpriseVerified } = useAuth();
const { options, loading, error, refresh } = useIdentityOptions(); const { options, loading, error, refresh } = useIdentityOptions();
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const handleCandidatePress = () => { /**
* 选中身份即进入:先写 role,再进入该身份的下一步(避免点到卡片无反应)。
* - 求职者:未设置求职意向 → 求职意向页;已设置 → 求职端
* - 招聘者:未完成企业认证 → 企业认证页;已完成 → 企业端
* 注意:两个路由组均无 index,故一律用显式落地路由。
*/
const handleSelect = (option: IdentityOption) => {
setSelectedId(option.id);
if (option.role === 'candidate') {
setRole('candidate'); setRole('candidate');
router.replace('/(tabs)'); router.replace(candidateSetupDone ? '/(tabs)/jobs' : '/job-preference');
}; } else {
const handleEnterprisePress = () => {
setRole('enterprise'); setRole('enterprise');
router.replace('/(enterprise)'); router.replace(
enterpriseVerified ? '/(enterprise)/enterprise-overview' : '/enterprise-certification',
);
}
}; };
return ( return (
@ -129,27 +140,12 @@ export default function IdentitySelectScreen() {
key={opt.id} key={opt.id}
option={opt} option={opt}
selected={opt.id === selectedId} selected={opt.id === selectedId}
onSelect={() => setSelectedId(opt.id)} onSelect={() => handleSelect(opt)}
/> />
))} ))}
</View> </View>
<View style={styles.actions}> <Text style={styles.hint}>点击上方身份卡片,立即进入对应端</Text>
<Pressable
style={[styles.actionBtn, styles.candidateBtn]}
onPress={handleCandidatePress}
hitSlop={6}
>
<Text style={styles.actionText}>加入求职端</Text>
</Pressable>
<Pressable
style={[styles.actionBtn, styles.enterpriseBtn]}
onPress={handleEnterprisePress}
hitSlop={6}
>
<Text style={styles.actionText}>进入企业端</Text>
</Pressable>
</View>
</> </>
)} )}
</ScrollView> </ScrollView>
@ -201,15 +197,12 @@ const styles = StyleSheet.create({
cardTitleSelected: { color: colors.primaryText }, cardTitleSelected: { color: colors.primaryText },
cardDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 6, lineHeight: 18 }, cardDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 6, lineHeight: 18 },
check: { position: 'absolute', top: 12, right: 12 }, check: { position: 'absolute', top: 12, right: 12 },
actions: { gap: 12 }, hint: {
actionBtn: { marginTop: 8,
borderRadius: 14, fontSize: 13,
paddingVertical: 14, color: colors.textHint,
alignItems: 'center', textAlign: 'center',
}, },
candidateBtn: { backgroundColor: colors.primaryBtn },
enterpriseBtn: { backgroundColor: colors.successBtn },
actionText: { fontSize: 16, fontWeight: '700', color: colors.textInverse },
manageLink: { manageLink: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',

7
app/index.tsx

@ -19,5 +19,10 @@ export default function Index() {
if (!role) { if (!role) {
return <Redirect href="/identity-select" />; return <Redirect href="/identity-select" />;
} }
return <Redirect href={role === 'candidate' ? '/(tabs)' : '/(enterprise)'} />; // 两个路由组均无 index,必须用显式落地路由,否则落地页空白
return (
<Redirect
href={role === 'candidate' ? '/(tabs)/jobs' : '/(enterprise)/enterprise-overview'}
/>
);
} }

251
app/job-preference.tsx

@ -0,0 +1,251 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '../src/theme';
import { useAuth } from '../src/context/AuthContext';
import { useJobPreference } from '../src/hooks';
import type { JobPreference } from '../src/types';
/** 各维度可选项(用于岗位推荐匹配) */
const CITIES = ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '南京', '西安', '苏州'];
const SALARIES = ['不限', '5-10K', '10-20K', '20-30K', '30-50K', '50K以上'];
const WORK_TYPES = ['全职', '兼职', '实习', '远程'];
const STATUSES = ['积极求职中', '观望好机会', '在职暂不考虑'];
/* ===================== 子组件:单选项 chips ===================== */
function ChipGroup({
title,
options,
value,
onSelect,
}: {
title: string;
options: string[];
value: string;
onSelect: (v: string) => void;
}) {
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.chips}>
{options.map((opt) => {
const selected = opt === value;
return (
<Pressable
key={opt}
style={[styles.chip, selected && styles.chipSelected]}
onPress={() => onSelect(opt)}
>
<Text style={[styles.chipText, selected && styles.chipTextSelected]}>{opt}</Text>
</Pressable>
);
})}
</View>
</View>
);
}
/* ===================== 子组件:四态 ===================== */
function Loading() {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.tip}>正在加载求职意向…</Text>
</View>
);
}
function ErrorState({ onRetry }: { onRetry: () => void }) {
return (
<View style={styles.center}>
<Ionicons name="alert-circle-outline" size={40} color={colors.textHint} />
<Text style={styles.tip}>加载失败</Text>
<Pressable style={styles.retryBtn} onPress={onRetry} hitSlop={6}>
<Text style={styles.retryText}>点击重试</Text>
</Pressable>
</View>
);
}
/* ===================== 页面 ===================== */
export default function JobPreferenceScreen() {
const router = useRouter();
const { completeCandidateSetup } = useAuth();
const { preference, loading, error, refresh, save } = useJobPreference();
const [form, setForm] = useState<JobPreference | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (preference && !form) setForm(preference);
}, [preference, form]);
if (loading || !form) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<Loading />
</SafeAreaView>
);
}
if (error) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<ErrorState onRetry={refresh} />
</SafeAreaView>
);
}
const canSave = form.position.trim().length > 0;
const update = <K extends keyof JobPreference>(field: K, value: JobPreference[K]) => {
setForm((prev) => (prev ? { ...prev, [field]: value } : prev));
};
const handleSave = async () => {
if (!canSave || saving) return;
setSaving(true);
try {
await save(form);
completeCandidateSetup();
router.replace('/(tabs)/jobs');
} finally {
setSaving(false);
}
};
const handleSkip = () => {
router.replace('/(tabs)/jobs');
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
<View style={styles.header}>
<Text style={styles.headerTitle}>设置求职意向</Text>
<Text style={styles.headerDesc}>告诉我们你的期望,才能推荐更匹配的岗位</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>期望职位</Text>
<TextInput
style={styles.input}
value={form.position}
onChangeText={(v) => update('position', v)}
placeholder="如:前端开发工程师"
placeholderTextColor={colors.textHint}
/>
</View>
<ChipGroup
title="期望城市"
options={CITIES}
value={form.city}
onSelect={(v) => update('city', v)}
/>
<ChipGroup
title="期望薪资"
options={SALARIES}
value={form.salary}
onSelect={(v) => update('salary', v)}
/>
<ChipGroup
title="工作类型"
options={WORK_TYPES}
value={form.workType}
onSelect={(v) => update('workType', v)}
/>
<ChipGroup
title="求职状态"
options={STATUSES}
value={form.status}
onSelect={(v) => update('status', v)}
/>
</ScrollView>
<View style={styles.footer}>
<Pressable
style={[styles.saveBtn, (!canSave || saving) && styles.saveBtnDisabled]}
onPress={handleSave}
disabled={!canSave || saving}
>
<Text style={styles.saveText}>{saving ? '保存中…' : '保存并进入求职端'}</Text>
</Pressable>
<Pressable style={styles.skipBtn} onPress={handleSkip} hitSlop={6}>
<Text style={styles.skipText}>稍后设置</Text>
</Pressable>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 20, paddingBottom: 24 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
header: { marginBottom: 20 },
headerTitle: { fontSize: 22, fontWeight: '800', color: colors.textPrimary },
headerDesc: { fontSize: 13, color: colors.textSecondary, marginTop: 6 },
section: { marginBottom: 20 },
sectionTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 10 },
input: {
height: 50,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.white,
paddingHorizontal: 14,
fontSize: 16,
color: colors.textPrimary,
},
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
chip: {
paddingHorizontal: 14,
paddingVertical: 9,
borderRadius: 18,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
},
chipSelected: { backgroundColor: colors.primaryLight, borderColor: colors.primary },
chipText: { fontSize: 13, color: colors.textSecondary },
chipTextSelected: { color: colors.primaryText, fontWeight: '600' },
footer: {
paddingHorizontal: 20,
paddingVertical: 14,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
saveBtn: {
height: 50,
borderRadius: 25,
backgroundColor: colors.primaryBtn,
alignItems: 'center',
justifyContent: 'center',
},
saveBtnDisabled: { backgroundColor: colors.border, opacity: 0.7 },
saveText: { fontSize: 16, fontWeight: '700', color: colors.textInverse },
skipBtn: { alignItems: 'center', paddingVertical: 14 },
skipText: { fontSize: 14, color: colors.textHint },
});
Loading…
Cancel
Save