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.
270 lines
9.9 KiB
270 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: 8, paddingBottom: 24 },
|
|
row: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.white,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
marginBottom: 8,
|
|
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 },
|
|
});
|
|
|