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(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 = ( (editing === null ? router.back() : setEditing(null))} /> ); if (resume === null) { return ( {header} {error !== null ? ( ) : ( )} ); } /* ===================== 表单态 ===================== */ 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) => 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 ( {header} update({ company: v })} placeholder="请输入公司名称" maxLength={40} autoFocus /> update({ position: v })} placeholder="如:高级前端工程师" maxLength={30} /> 开始时间 * update({ startYear: v })} onMonthChange={(v) => update({ startMonth: v })} /> {startError !== '' && {startError}} 至今 // 打开「至今」时清掉已选的结束时间:留着会让存进去的 endDate 与开关不一致 update(v ? { untilNow: true, endYear: '', endMonth: '' } : { untilNow: false }) } trackColor={{ false: colors.border, true: colors.primaryBorder }} thumbColor={editing.untilNow ? colors.primaryBtn : colors.white} /> {editing.untilNow ? ( ) : ( <> 结束时间 * update({ endYear: v })} onMonthChange={(v) => update({ endMonth: v })} /> {endError !== '' && {endError}} )} update({ description: v })} placeholder="负责了什么、做成了什么(AI 改写需后端接入)" multiline maxLength={500} /> {orderError !== '' && {orderError}} ); } /* ===================== 列表态 ===================== */ 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 ( {header} {list.length === 0 ? ( ) : ( {list.map((exp) => { const dates = [exp.startDate, exp.endDate].filter(Boolean).join(' - '); return ( {exp.company} {exp.position !== '' && {exp.position}} {dates !== '' && {dates}} {exp.description !== '' && ( {exp.description} )} setEditing(toForm(exp))} /> handleDelete(exp)} /> ); })} )} setEditing(EMPTY_FORM)} /> ); } /* ===================== 表单与条目互转 ===================== */ 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: 8, 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: 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 }, rowDesc: { fontSize: 13, color: colors.textHint, lineHeight: 18 }, });