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.
323 lines
12 KiB
323 lines
12 KiB
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { useEffect, useState } from 'react';
|
|
import { Alert, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { PrimaryButton } from '../../components/onboarding/ui/PrimaryButton';
|
|
import { YearMonthWheel } from '../../components/onboarding/ui/WheelPicker';
|
|
import {
|
|
CenterState,
|
|
EditHeader,
|
|
Field,
|
|
FormNote,
|
|
IconAction,
|
|
PageFooter,
|
|
} from '../../components/resume/EditKit';
|
|
import { useResume } from '../../src/hooks';
|
|
import { colors } from '../../src/theme';
|
|
import type { ResumeExperience } from '../../src/types';
|
|
import { splitYearMonth, yearMonthValue } from '../../src/utils';
|
|
|
|
/**
|
|
* 工作经历编辑页:一页两态(列表态 / 表单态)。
|
|
*
|
|
* 时间用 `YearMonthWheel`,存储格式与引导采集一致(`'2023年2月'`),
|
|
* 回填靠 `splitYearMonth`;解析不出(空串 /「至今」)时两个滚轮都停在未选中态,
|
|
* **不给兜底索引** —— 给索引会静默把用户的入职年月改成别的年份。
|
|
*/
|
|
export default function ResumeExperienceScreen() {
|
|
const router = useRouter();
|
|
const params = useLocalSearchParams<{ new?: string; id?: string }>();
|
|
const { resume, loading, error, refresh, saving, saveExperience } = useResume();
|
|
|
|
const [editing, setEditing] = useState<ExpForm | 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.experience.find((it) => it.id === params.id);
|
|
if (found) setEditing(toForm(found));
|
|
}
|
|
}, [resume, entryApplied, params.new, params.id]);
|
|
|
|
const list = resume?.experience ?? [];
|
|
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 company = editing.company.trim();
|
|
const startValue = yearMonthValue(editing.startYear, editing.startMonth);
|
|
const endValue = yearMonthValue(editing.endYear, editing.endMonth);
|
|
const startError = startValue === null ? '请选择开始时间' : '';
|
|
const endError = editing.untilNow ? '' : endValue === null ? '请选择结束时间' : '';
|
|
const orderError =
|
|
startValue !== null && endValue !== null && endValue < startValue ? '结束时间不能早于开始时间' : '';
|
|
const canSave = company !== '' && startError === '' && endError === '' && orderError === '';
|
|
|
|
const update = (patch: Partial<ExpForm>) => setEditing((prev) => ({ ...(prev ?? editing), ...patch }));
|
|
|
|
const handleSubmit = async () => {
|
|
if (!canSave || saving) return;
|
|
const fields = {
|
|
company,
|
|
position: editing.position.trim(),
|
|
startDate: `${editing.startYear}${editing.startMonth}`,
|
|
// 「至今」才写「至今」;否则存选中的年月。没选完的情况已被上面的校验拦住
|
|
endDate: editing.untilNow ? UNTIL_NOW : `${editing.endYear}${editing.endMonth}`,
|
|
description: editing.description.trim(),
|
|
// AI 改写未接入,用户手改的文字不该被标成「已优化」
|
|
aiOptimized: false,
|
|
};
|
|
const next =
|
|
editing.id === undefined
|
|
? [...list, fields]
|
|
: list.map((it) => (it.id === editing.id ? { ...fields, id: editing.id } : it));
|
|
await saveExperience(next);
|
|
setEditing(null);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
{header}
|
|
<ScrollView
|
|
contentContainerStyle={styles.body}
|
|
showsVerticalScrollIndicator={false}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<Field
|
|
label="公司名称"
|
|
required
|
|
value={editing.company}
|
|
onChangeText={(v) => update({ company: v })}
|
|
placeholder="请输入公司名称"
|
|
maxLength={40}
|
|
autoFocus
|
|
/>
|
|
<Field
|
|
label="职位"
|
|
value={editing.position}
|
|
onChangeText={(v) => update({ position: v })}
|
|
placeholder="如:高级前端工程师"
|
|
maxLength={30}
|
|
/>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.label}>
|
|
开始时间<Text style={styles.requiredMark}> *</Text>
|
|
</Text>
|
|
<YearMonthWheel
|
|
year={editing.startYear}
|
|
month={editing.startMonth}
|
|
onYearChange={(v) => update({ startYear: v })}
|
|
onMonthChange={(v) => update({ startMonth: v })}
|
|
/>
|
|
{startError !== '' && <Text style={styles.fieldError}>{startError}</Text>}
|
|
</View>
|
|
|
|
<View style={styles.section}>
|
|
<View style={styles.switchRow}>
|
|
<Text style={styles.label}>至今</Text>
|
|
<Switch
|
|
value={editing.untilNow}
|
|
onValueChange={(v) =>
|
|
// 打开「至今」时清掉已选的结束时间:留着会让存进去的 endDate 与开关不一致
|
|
update(v ? { untilNow: true, endYear: '', endMonth: '' } : { untilNow: false })
|
|
}
|
|
trackColor={{ false: colors.border, true: colors.primaryBorder }}
|
|
thumbColor={editing.untilNow ? colors.primaryBtn : colors.white}
|
|
/>
|
|
</View>
|
|
{editing.untilNow ? (
|
|
<FormNote text="在职中:简历里结束时间显示为「至今」。" />
|
|
) : (
|
|
<>
|
|
<Text style={[styles.label, styles.labelTop]}>
|
|
结束时间<Text style={styles.requiredMark}> *</Text>
|
|
</Text>
|
|
<YearMonthWheel
|
|
year={editing.endYear}
|
|
month={editing.endMonth}
|
|
onYearChange={(v) => update({ endYear: v })}
|
|
onMonthChange={(v) => update({ endMonth: v })}
|
|
/>
|
|
{endError !== '' && <Text style={styles.fieldError}>{endError}</Text>}
|
|
</>
|
|
)}
|
|
</View>
|
|
|
|
<Field
|
|
label="工作内容"
|
|
value={editing.description}
|
|
onChangeText={(v) => update({ description: v })}
|
|
placeholder="负责了什么、做成了什么(AI 改写需后端接入)"
|
|
multiline
|
|
maxLength={500}
|
|
/>
|
|
{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: ResumeExperience) => {
|
|
Alert.alert('删除这条工作经历?', item.company, [
|
|
{ text: '取消', style: 'cancel' },
|
|
{
|
|
text: '删除',
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
void saveExperience(list.filter((it) => it.id !== item.id));
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
{header}
|
|
{list.length === 0 ? (
|
|
<CenterState icon="briefcase-outline" text="还没有工作经历" />
|
|
) : (
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
{list.map((exp) => {
|
|
const dates = [exp.startDate, exp.endDate].filter(Boolean).join(' - ');
|
|
return (
|
|
<View key={exp.id} style={styles.row}>
|
|
<View style={styles.rowBody}>
|
|
<Text style={styles.rowTitle}>{exp.company}</Text>
|
|
{exp.position !== '' && <Text style={styles.rowSub}>{exp.position}</Text>}
|
|
{dates !== '' && <Text style={styles.rowSub}>{dates}</Text>}
|
|
{exp.description !== '' && (
|
|
<Text style={styles.rowDesc} numberOfLines={2}>
|
|
{exp.description}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
<IconAction
|
|
icon="create-outline"
|
|
label={`编辑 ${exp.company}`}
|
|
color={colors.primary}
|
|
onPress={() => setEditing(toForm(exp))}
|
|
/>
|
|
<IconAction
|
|
icon="trash-outline"
|
|
label={`删除 ${exp.company}`}
|
|
color={colors.danger}
|
|
onPress={() => handleDelete(exp)}
|
|
/>
|
|
</View>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
)}
|
|
<PageFooter>
|
|
<PrimaryButton label="添加工作经历" onPress={() => setEditing(EMPTY_FORM)} />
|
|
</PageFooter>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
/* ===================== 表单与条目互转 ===================== */
|
|
|
|
interface ExpForm {
|
|
/** undefined = 新增项(由 API 层补 id) */
|
|
id?: string;
|
|
company: string;
|
|
position: string;
|
|
startYear: string;
|
|
startMonth: string;
|
|
untilNow: boolean;
|
|
endYear: string;
|
|
endMonth: string;
|
|
description: string;
|
|
}
|
|
|
|
/** 结束时间的「至今」哨兵值,与引导派生出来的格式一致 */
|
|
const UNTIL_NOW = '至今';
|
|
|
|
const EMPTY_FORM: ExpForm = {
|
|
company: '',
|
|
position: '',
|
|
startYear: '',
|
|
startMonth: '',
|
|
// 默认按「在职」预置不成立:新的一条究竟是哪一段经历只有用户知道,默认 false 由他显式选
|
|
untilNow: false,
|
|
endYear: '',
|
|
endMonth: '',
|
|
description: '',
|
|
};
|
|
|
|
const toForm = (exp: ResumeExperience): ExpForm => {
|
|
const start = splitYearMonth(exp.startDate);
|
|
const untilNow = exp.endDate === UNTIL_NOW;
|
|
const end = splitYearMonth(untilNow ? '' : exp.endDate);
|
|
return {
|
|
id: exp.id,
|
|
company: exp.company,
|
|
position: exp.position,
|
|
startYear: start.year,
|
|
startMonth: start.month,
|
|
untilNow,
|
|
endYear: end.year,
|
|
endMonth: end.month,
|
|
description: exp.description,
|
|
};
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.pageBg },
|
|
body: { padding: 20, paddingBottom: 24 },
|
|
section: { marginBottom: 18 },
|
|
label: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
labelTop: { marginTop: 14, marginBottom: 10 },
|
|
requiredMark: { color: colors.danger },
|
|
fieldError: { fontSize: 12, color: colors.dangerText, marginTop: 6 },
|
|
formError: { fontSize: 13, color: colors.dangerText, marginBottom: 8 },
|
|
switchRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
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 },
|
|
rowDesc: { fontSize: 13, color: colors.textHint, lineHeight: 18 },
|
|
});
|
|
|