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.
253 lines
7.6 KiB
253 lines
7.6 KiB
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 {
|
|
EXPECTED_CITY_OPTIONS,
|
|
EXPECTED_SALARY_OPTIONS,
|
|
JOB_STATUS_OPTIONS,
|
|
WORK_TYPE_OPTIONS,
|
|
} from '../src/constants';
|
|
import { withCurrentOption } from '../src/utils';
|
|
import type { JobPreference } from '../src/types';
|
|
|
|
/* ===================== 子组件:单选项 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={EXPECTED_CITY_OPTIONS}
|
|
value={form.city}
|
|
onSelect={(v) => update('city', v)}
|
|
/>
|
|
<ChipGroup
|
|
title="期望薪资"
|
|
// 引导 P7 的两个阶梯能拼出快捷档覆盖不到的组合,补进当前值避免「一个都没选中」
|
|
options={withCurrentOption(EXPECTED_SALARY_OPTIONS, form.salary)}
|
|
value={form.salary}
|
|
onSelect={(v) => update('salary', v)}
|
|
/>
|
|
<ChipGroup
|
|
title="工作类型"
|
|
options={WORK_TYPE_OPTIONS}
|
|
value={form.workType}
|
|
onSelect={(v) => update('workType', v)}
|
|
/>
|
|
<ChipGroup
|
|
title="求职状态"
|
|
options={JOB_STATUS_OPTIONS}
|
|
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: 8, 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 },
|
|
});
|
|
|