轻松聘
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.
 
 
 

272 lines
9.9 KiB

import { useLocalSearchParams, useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { PrimaryButton } from '../../components/onboarding/ui/PrimaryButton';
import {
CenterState,
EditHeader,
Field,
FormNote,
IconAction,
PageFooter,
} from '../../components/resume/EditKit';
import { useResume } from '../../src/hooks';
import { colors } from '../../src/theme';
import type { ResumeEducation } from '../../src/types';
/**
* 教育背景编辑页:一页两态(列表态 / 表单态)。
*
* 入口三种:首页「添加教育经历」带 `?new=1` 直接进表单、条目「编辑」带 `?id=` 进表单、
* 首页「编辑 >」不带参数进列表。删除与编辑都在本页完成并**即时落盘**,
* 不存在「改了没保存就走」的困惑。
*/
export default function ResumeEducationScreen() {
const router = useRouter();
const params = useLocalSearchParams<{ new?: string; id?: string }>();
const { resume, loading, error, refresh, saving, saveEducation } = useResume();
/** 非空 = 表单态;null = 列表态 */
const [editing, setEditing] = useState<EduForm | null>(null);
/** 入口参数只解析一次:解析后再取数(或保存后的刷新)不该把用户拉回表单/列表 */
const [entryApplied, setEntryApplied] = useState(false);
useEffect(() => {
if (!resume || entryApplied) return;
setEntryApplied(true);
if (params.new === '1') {
setEditing(EMPTY_FORM);
return;
}
if (params.id !== undefined && params.id !== '') {
const found = resume.education.find((it) => it.id === params.id);
// 找不到就留在列表态:编一条空表单让用户改,等于凭空多出一条教育经历
if (found) setEditing(toForm(found));
}
}, [resume, entryApplied, params.new, params.id]);
const list = resume?.education ?? [];
const header = (
<EditHeader title={editing === null ? '教育背景' : editing.id === undefined ? '添加教育经历' : '编辑教育经历'} onBack={() => (editing === null ? router.back() : setEditing(null))} />
);
if (resume === null) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
{header}
{error !== null ? (
<CenterState icon="alert-circle-outline" text="加载失败" actionLabel="点击重试" onAction={refresh} />
) : (
<CenterState text={loading ? '正在加载…' : '简历数据为空'} />
)}
</SafeAreaView>
);
}
/* ===================== 表单态 ===================== */
if (editing !== null) {
const school = editing.school.trim();
const startError = editing.startYear !== '' && !YEAR_RE.test(editing.startYear) ? '请输入 4 位年份' : '';
const endError = editing.endYear !== '' && !YEAR_RE.test(editing.endYear) ? '请输入 4 位年份' : '';
const orderError =
startError === '' && endError === '' && editing.startYear !== '' && editing.endYear !== '' && editing.startYear > editing.endYear
? '毕业年份不能早于入学年份'
: '';
const canSave = school !== '' && startError === '' && endError === '' && orderError === '';
const update = (field: keyof EduForm, text: string) =>
setEditing((prev) => ({ ...(prev ?? editing), [field]: text }));
const handleSubmit = async () => {
if (!canSave || saving) return;
const fields = {
school,
major: editing.major.trim(),
degree: editing.degree.trim(),
startYear: editing.startYear,
endYear: editing.endYear,
};
// 交回整表。新增项**不带 id 字段**(而不是给空串):API 层用 `?? ` 判缺省,
// 空串不是 nullish,会把 id 写成空的
const next =
editing.id === undefined
? [...list, fields]
: list.map((it) => (it.id === editing.id ? { ...fields, id: editing.id } : it));
await saveEducation(next);
// 保存后回到列表态,让用户看见结果(不 router.back(),否则看不到自己刚加的那条)
setEditing(null);
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
{header}
<ScrollView
contentContainerStyle={styles.body}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<Field
label="学校"
required
value={editing.school}
onChangeText={(v) => update('school', v)}
placeholder="请输入学校名称"
maxLength={30}
autoFocus
/>
<Field
label="专业"
value={editing.major}
onChangeText={(v) => update('major', v)}
placeholder="如:计算机科学与技术"
maxLength={30}
/>
<Field
label="学历"
value={editing.degree}
onChangeText={(v) => update('degree', v)}
placeholder="如:本科 / 硕士 非全日制"
maxLength={20}
hint="引导流程采集到的学历会带学习形式(如「硕士 非全日制」),这里可以照原样填"
/>
<View style={styles.yearRow}>
<View style={styles.yearItem}>
<Field
label="入学年份"
value={editing.startYear}
onChangeText={(v) => update('startYear', v.replace(/[^\d]/g, ''))}
placeholder="如:2018"
keyboardType="number-pad"
maxLength={4}
error={startError}
/>
</View>
<View style={styles.yearItem}>
<Field
label="毕业年份"
value={editing.endYear}
onChangeText={(v) => update('endYear', v.replace(/[^\d]/g, ''))}
placeholder="如:2022"
keyboardType="number-pad"
maxLength={4}
error={endError}
/>
</View>
</View>
{orderError !== '' && <Text style={styles.formError}>{orderError}</Text>}
<FormNote text="学校为必填;专业与年份没填就不在简历里显示,不会留下孤零零的分隔符。" />
</ScrollView>
<PageFooter>
<PrimaryButton label={saving ? '保存中…' : '保存'} active={canSave} onPress={handleSubmit} loading={saving} />
</PageFooter>
</SafeAreaView>
);
}
/* ===================== 列表态 ===================== */
const handleDelete = (item: ResumeEducation) => {
Alert.alert('删除这条教育经历?', item.school, [
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
// 即时落盘:删掉就走也不用担心没保存
onPress: () => {
void saveEducation(list.filter((it) => it.id !== item.id));
},
},
]);
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
{header}
{list.length === 0 ? (
<CenterState icon="school-outline" text="还没有教育经历" />
) : (
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
{list.map((edu) => {
// 专业 / 学历 / 年份可能为空(引导只采集学校和学历),拼出来会剩「· 本科」这种半截值
const majorDegree = [edu.major, edu.degree].filter(Boolean).join(' · ');
const years = [edu.startYear, edu.endYear].filter(Boolean).join('-');
return (
<View key={edu.id} style={styles.row}>
<View style={styles.rowBody}>
<Text style={styles.rowTitle}>{edu.school}</Text>
{majorDegree !== '' && <Text style={styles.rowSub}>{majorDegree}</Text>}
{years !== '' && <Text style={styles.rowSub}>{years}</Text>}
</View>
<IconAction
icon="create-outline"
label={`编辑 ${edu.school}`}
color={colors.primary}
onPress={() => setEditing(toForm(edu))}
/>
<IconAction
icon="trash-outline"
label={`删除 ${edu.school}`}
color={colors.danger}
onPress={() => handleDelete(edu)}
/>
</View>
);
})}
</ScrollView>
)}
<PageFooter>
<PrimaryButton label="添加教育经历" onPress={() => setEditing(EMPTY_FORM)} />
</PageFooter>
</SafeAreaView>
);
}
/* ===================== 表单与条目互转 ===================== */
interface EduForm {
/** undefined = 新增项(由 API 层补 id) */
id?: string;
school: string;
major: string;
degree: string;
startYear: string;
endYear: string;
}
const EMPTY_FORM: EduForm = { school: '', major: '', degree: '', startYear: '', endYear: '' };
const YEAR_RE = /^\d{4}$/;
const toForm = (edu: ResumeEducation): EduForm => ({
id: edu.id,
school: edu.school,
major: edu.major,
degree: edu.degree,
startYear: edu.startYear,
endYear: edu.endYear,
});
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 20, paddingBottom: 24 },
row: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 14,
paddingVertical: 14,
paddingLeft: 16,
paddingRight: 8,
marginBottom: 10,
borderWidth: 1,
borderColor: colors.borderLight,
},
rowBody: { flex: 1, gap: 4 },
rowTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
rowSub: { fontSize: 13, color: colors.textSecondary },
yearRow: { flexDirection: 'row', gap: 12 },
yearItem: { flex: 1 },
formError: { fontSize: 13, color: colors.dangerText, marginBottom: 8 },
});