Browse Source
- 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
4 changed files with 302 additions and 35 deletions
@ -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…
Reference in new issue