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.
240 lines
8.9 KiB
240 lines
8.9 KiB
import { useRouter } from 'expo-router';
|
|
import { 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, PageFooter } from '../../components/resume/EditKit';
|
|
import { useResume } from '../../src/hooks';
|
|
import { UNSET_LABEL } from '../../src/constants';
|
|
import { colors , spacing } from '../../src/theme';
|
|
import type { Resume, ResumeEducation, ResumeExperience, ResumeProject } from '../../src/types';
|
|
|
|
/**
|
|
* 简历预览页(只读)。
|
|
*
|
|
* 只渲染简历**正文**:不含评分卡与优化建议 —— 那些是 App 内的辅助信息,不会出现在
|
|
* 导出的文件里,混在一起会让人以为导出物里也有。
|
|
*
|
|
* 空板块**保留标题 + 「未填写 X」**:预览的价值正是让用户看见自己缺什么,
|
|
* 整段隐去反而看不出漏了哪一块。
|
|
*/
|
|
export default function ResumePreviewScreen() {
|
|
const router = useRouter();
|
|
const { resume, loading, error, refresh } = useResume();
|
|
|
|
const header = <EditHeader title="简历预览" onBack={() => router.back()} />;
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
const { basicInfo, jobIntention, education, experience, projects, skills, selfEvaluation } = resume;
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
{header}
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
{/* 纸张:白底 + 描边,与导出效果同一套排版 */}
|
|
<View style={styles.paper}>
|
|
<View style={styles.paperHead}>
|
|
<View style={styles.avatar}>
|
|
{/* TODO(素材):接入后端后这里渲染用户上传的头像图片;
|
|
当前以姓名首字兜底,无姓名时不占位 */}
|
|
<Text style={styles.avatarText}>{basicInfo.avatar !== '' ? basicInfo.avatar : '简'}</Text>
|
|
</View>
|
|
<View style={styles.headBody}>
|
|
<Text style={styles.name}>{basicInfo.name !== '' ? basicInfo.name : UNSET_LABEL}</Text>
|
|
{/* 有值才渲染:空白行比不渲染更让人困惑 */}
|
|
{[basicInfo.phone, basicInfo.email, basicInfo.location].filter((v) => v !== '').length > 0 && (
|
|
<Text style={styles.contact}>
|
|
{[basicInfo.phone, basicInfo.email, basicInfo.location].filter((v) => v !== '').join(' · ')}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<Section title="求职意向">
|
|
{hasIntention(jobIntention) ? (
|
|
<Text style={styles.paragraph}>
|
|
{[
|
|
jobIntention.position,
|
|
jobIntention.city,
|
|
jobIntention.salary,
|
|
jobIntention.statusLabel,
|
|
]
|
|
.filter((v) => v !== '')
|
|
.join(' · ')}
|
|
</Text>
|
|
) : (
|
|
<Empty text="未填写求职意向" />
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="教育背景">
|
|
{education.length === 0 ? (
|
|
<Empty text="未填写教育背景" />
|
|
) : (
|
|
education.map((edu) => <EducationItem key={edu.id} edu={edu} />)
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="工作经历">
|
|
{experience.length === 0 ? (
|
|
<Empty text="未填写工作经历" />
|
|
) : (
|
|
experience.map((exp) => <ExperienceItem key={exp.id} exp={exp} />)
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="项目经历">
|
|
{projects.length === 0 ? (
|
|
<Empty text="未填写项目经历" />
|
|
) : (
|
|
projects.map((proj) => <ProjectItem key={proj.id} proj={proj} />)
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="技能特长">
|
|
{skills.length === 0 ? (
|
|
<Empty text="未填写技能特长" />
|
|
) : (
|
|
<Text style={styles.paragraph}>{skills.map((s) => s.name).join(' · ')}</Text>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="自我评价">
|
|
{selfEvaluation.content === '' ? (
|
|
<Empty text="未填写自我评价" />
|
|
) : (
|
|
<Text style={styles.paragraph}>{selfEvaluation.content}</Text>
|
|
)}
|
|
</Section>
|
|
</View>
|
|
|
|
<Text style={styles.tip}>
|
|
预览为简历正文效果,不含评分与优化建议。模板样式、纸张大小需后端导出服务支持,当前按默认版式呈现。
|
|
</Text>
|
|
</ScrollView>
|
|
|
|
<PageFooter>
|
|
<PrimaryButton label="去导出设置" onPress={() => router.push('/resume/export')} />
|
|
</PageFooter>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件 ===================== */
|
|
|
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>{title}</Text>
|
|
{children}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function Empty({ text }: { text: string }) {
|
|
return <Text style={styles.emptyText}>{text}</Text>;
|
|
}
|
|
|
|
function EducationItem({ edu }: { edu: ResumeEducation }) {
|
|
// 专业 / 学历 / 年份可能为空,拼出来会剩半截值
|
|
const majorDegree = [edu.major, edu.degree].filter(Boolean).join(' · ');
|
|
const years = [edu.startYear, edu.endYear].filter(Boolean).join('-');
|
|
return (
|
|
<View style={styles.item}>
|
|
<View style={styles.itemHead}>
|
|
<Text style={styles.itemTitle}>{edu.school}</Text>
|
|
{years !== '' && <Text style={styles.itemMeta}>{years}</Text>}
|
|
</View>
|
|
{majorDegree !== '' && <Text style={styles.itemSub}>{majorDegree}</Text>}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ExperienceItem({ exp }: { exp: ResumeExperience }) {
|
|
const dates = [exp.startDate, exp.endDate].filter(Boolean).join(' - ');
|
|
return (
|
|
<View style={styles.item}>
|
|
<View style={styles.itemHead}>
|
|
<Text style={styles.itemTitle}>{exp.company}</Text>
|
|
{dates !== '' && <Text style={styles.itemMeta}>{dates}</Text>}
|
|
</View>
|
|
{exp.position !== '' && <Text style={styles.itemSub}>{exp.position}</Text>}
|
|
{exp.description !== '' && <Text style={styles.paragraph}>{exp.description}</Text>}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ProjectItem({ proj }: { proj: ResumeProject }) {
|
|
return (
|
|
<View style={styles.item}>
|
|
<View style={styles.itemHead}>
|
|
<Text style={styles.itemTitle}>{proj.name}</Text>
|
|
{proj.role !== '' && <Text style={styles.itemMeta}>{proj.role}</Text>}
|
|
</View>
|
|
{proj.description !== '' && <Text style={styles.paragraph}>{proj.description}</Text>}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** 求职意向四项全空才算未填写(引导只采集到其中一两项时也算有内容) */
|
|
function hasIntention(intention: Resume['jobIntention']): boolean {
|
|
return (
|
|
[intention.position, intention.city, intention.salary, intention.statusLabel].filter(
|
|
(v) => v !== '',
|
|
).length > 0
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.pageBg },
|
|
body: { padding: spacing.card, paddingBottom: 24 },
|
|
paper: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: spacing.card,
|
|
padding: 20,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
paperHead: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
|
avatar: {
|
|
width: 52,
|
|
height: 52,
|
|
borderRadius: 26,
|
|
backgroundColor: colors.primary,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
avatarText: { fontSize: 22, fontWeight: '700', color: colors.white },
|
|
headBody: { marginLeft: 14, flex: 1 },
|
|
name: { fontSize: 20, fontWeight: '800', color: colors.textPrimary },
|
|
contact: { fontSize: 13, color: colors.textSecondary, marginTop: 6 },
|
|
section: { marginBottom: 18 },
|
|
sectionTitle: {
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
borderLeftWidth: 3,
|
|
borderLeftColor: colors.primary,
|
|
paddingLeft: 8,
|
|
marginBottom: 10,
|
|
},
|
|
item: { marginBottom: 12 },
|
|
itemHead: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
itemTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, flex: 1 },
|
|
itemMeta: { fontSize: 12, color: colors.textHint },
|
|
itemSub: { fontSize: 13, color: colors.textSecondary, marginTop: 4 },
|
|
paragraph: { fontSize: 13, color: colors.textSecondary, lineHeight: 21, marginTop: 6 },
|
|
emptyText: { fontSize: 13, color: colors.textHint },
|
|
tip: { fontSize: 12, color: colors.textHint, lineHeight: 18, marginTop: 4 },
|
|
});
|
|
|