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.
223 lines
7.2 KiB
223 lines
7.2 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 , spacing } from '../../src/theme';
|
|
import type { ResumeProject } from '../../src/types';
|
|
|
|
/**
|
|
* 项目经历编辑页:一页两态(列表态 / 表单态)。
|
|
*
|
|
* 本页不提供「AI 优化」按钮:项目经历在首页只有「已量化」徽标,
|
|
* 徽标的置位要等后端真能改写正文,不能让用户点出一个立刻显示「已优化」的假状态。
|
|
*/
|
|
export default function ResumeProjectScreen() {
|
|
const router = useRouter();
|
|
const params = useLocalSearchParams<{ new?: string; id?: string }>();
|
|
const { resume, loading, error, refresh, saving, saveProjects } = useResume();
|
|
|
|
const [editing, setEditing] = useState<ProjForm | 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.projects.find((it) => it.id === params.id);
|
|
if (found) setEditing(toForm(found));
|
|
}
|
|
}, [resume, entryApplied, params.new, params.id]);
|
|
|
|
const list = resume?.projects ?? [];
|
|
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 name = editing.name.trim();
|
|
const canSave = name !== '';
|
|
|
|
const update = (patch: Partial<ProjForm>) => setEditing((prev) => ({ ...(prev ?? editing), ...patch }));
|
|
|
|
const handleSubmit = async () => {
|
|
if (!canSave || saving) return;
|
|
const fields = {
|
|
name,
|
|
role: editing.role.trim(),
|
|
description: editing.description.trim(),
|
|
aiOptimized: false,
|
|
};
|
|
const next =
|
|
editing.id === undefined
|
|
? [...list, fields]
|
|
: list.map((it) => (it.id === editing.id ? { ...fields, id: editing.id } : it));
|
|
await saveProjects(next);
|
|
setEditing(null);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
{header}
|
|
<ScrollView
|
|
contentContainerStyle={styles.body}
|
|
showsVerticalScrollIndicator={false}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<Field
|
|
label="项目名称"
|
|
required
|
|
value={editing.name}
|
|
onChangeText={(v) => update({ name: v })}
|
|
placeholder="请输入项目名称"
|
|
maxLength={40}
|
|
autoFocus
|
|
/>
|
|
<Field
|
|
label="担任角色"
|
|
value={editing.role}
|
|
onChangeText={(v) => update({ role: v })}
|
|
placeholder="如:前端负责人"
|
|
maxLength={20}
|
|
/>
|
|
<Field
|
|
label="项目描述"
|
|
value={editing.description}
|
|
onChangeText={(v) => update({ description: v })}
|
|
placeholder="做了什么、结果如何(有数字更好,如:首屏从 3.5s 降至 1.2s)"
|
|
multiline
|
|
maxLength={500}
|
|
/>
|
|
<FormNote text="项目名称为必填;角色与描述没填就不在简历里显示空行。" />
|
|
</ScrollView>
|
|
<PageFooter>
|
|
<PrimaryButton label={saving ? '保存中…' : '保存'} active={canSave} onPress={handleSubmit} loading={saving} />
|
|
</PageFooter>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
/* ===================== 列表态 ===================== */
|
|
|
|
const handleDelete = (item: ResumeProject) => {
|
|
Alert.alert('删除这条项目经历?', item.name, [
|
|
{ text: '取消', style: 'cancel' },
|
|
{
|
|
text: '删除',
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
void saveProjects(list.filter((it) => it.id !== item.id));
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
{header}
|
|
{list.length === 0 ? (
|
|
<CenterState icon="albums-outline" text="还没有项目经历" />
|
|
) : (
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
{list.map((proj) => (
|
|
<View key={proj.id} style={styles.row}>
|
|
<View style={styles.rowBody}>
|
|
<Text style={styles.rowTitle}>{proj.name}</Text>
|
|
{proj.role !== '' && <Text style={styles.rowSub}>{proj.role}</Text>}
|
|
{proj.description !== '' && (
|
|
<Text style={styles.rowDesc} numberOfLines={2}>
|
|
{proj.description}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
<IconAction
|
|
icon="create-outline"
|
|
label={`编辑 ${proj.name}`}
|
|
color={colors.primary}
|
|
onPress={() => setEditing(toForm(proj))}
|
|
/>
|
|
<IconAction
|
|
icon="trash-outline"
|
|
label={`删除 ${proj.name}`}
|
|
color={colors.danger}
|
|
onPress={() => handleDelete(proj)}
|
|
/>
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
)}
|
|
<PageFooter>
|
|
<PrimaryButton label="添加项目经历" onPress={() => setEditing(EMPTY_FORM)} />
|
|
</PageFooter>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
/* ===================== 表单与条目互转 ===================== */
|
|
|
|
interface ProjForm {
|
|
/** undefined = 新增项(由 API 层补 id) */
|
|
id?: string;
|
|
name: string;
|
|
role: string;
|
|
description: string;
|
|
}
|
|
|
|
const EMPTY_FORM: ProjForm = { name: '', role: '', description: '' };
|
|
|
|
const toForm = (proj: ResumeProject): ProjForm => ({
|
|
id: proj.id,
|
|
name: proj.name,
|
|
role: proj.role,
|
|
description: proj.description,
|
|
});
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.pageBg },
|
|
body: { padding: spacing.card, paddingBottom: 24 },
|
|
row: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.white,
|
|
borderRadius: spacing.card,
|
|
padding: spacing.card,
|
|
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 },
|
|
});
|
|
|