import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import {
ActivityIndicator,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Line, Path, Polygon, Text as SvgText } from 'react-native-svg';
import { useCandidateDetail } from '../src/hooks';
import { colors } from '../src/theme';
import type { CandidateAbility, CandidateReview } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
function Header({ onBack }: { onBack: () => void }) {
return (
候选人详情
);
}
/* ===================== 子组件:基本信息卡 ===================== */
function BasicInfoCard({
name,
matchPercent,
info,
}: {
name: string;
matchPercent: number;
info: {
age: number;
gender: string;
experience: string;
education: string;
city: string;
phone: string;
email: string;
jobStatus: string;
};
}) {
return (
{name[0]}
{name}
{info.age}岁 · {info.gender} · {info.experience} · {info.education} · {info.city}
{matchPercent}%
匹配度
{info.phone}
{info.email}
{info.jobStatus}
);
}
/* ===================== 子组件:综合评分卡 ===================== */
function OverallScoreCard({ score, percentile }: { score: number; percentile: number }) {
return (
{score}
综合评分
超过 {percentile}% 同岗位候选人
);
}
/* ===================== 子组件:趋势图 ===================== */
function TrendChart({ trend }: { trend: { month: string; score: number }[] }) {
const width = 320;
const height = 120;
const padding = { top: 16, right: 24, bottom: 24, left: 32 };
const chartW = width - padding.left - padding.right;
const chartH = height - padding.top - padding.bottom;
const minScore = 70;
const maxScore = 100;
const points = trend.map((t, i) => {
const x = padding.left + (i / (trend.length - 1)) * chartW;
const y = padding.top + chartH - ((t.score - minScore) / (maxScore - minScore)) * chartH;
return { x, y, month: t.month };
});
const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
return (
评分趋势
);
}
/* ===================== 子组件:八维能力雷达图 ===================== */
function AbilityRadar({ abilities }: { abilities: CandidateAbility[] }) {
const size = 220;
const center = size / 2;
const radius = 80;
const angleStep = (Math.PI * 2) / abilities.length;
const axisPoints = abilities.map((_, i) => {
const angle = i * angleStep - Math.PI / 2;
return {
x: center + radius * Math.cos(angle),
y: center + radius * Math.sin(angle),
label: abilities[i].name,
};
});
const dataPoints = abilities.map((d, i) => {
const angle = i * angleStep - Math.PI / 2;
const r = (d.score / d.maxScore) * radius;
return {
x: center + r * Math.cos(angle),
y: center + r * Math.sin(angle),
};
});
const polygonPoints = dataPoints.map((p) => `${p.x},${p.y}`).join(' ');
return (
八维能力评分
{abilities.map((a) => (
{a.name}
{a.score}
))}
);
}
/* ===================== 子组件:面试评价 ===================== */
function ReviewList({ reviews }: { reviews: CandidateReview[] }) {
return (
面试评价
{reviews.map((review) => (
{review.interviewer}
{review.position}
{review.rating}.0
{review.content}
{review.tags.map((tag) => (
{tag}
))}
{review.date}
))}
);
}
/* ===================== 子组件:录用建议 ===================== */
function HireSuggestionCard({
suggestions,
salary,
}: {
suggestions: { type: string; title: string; desc: string; selected?: boolean }[];
salary: { range: string; level: string; note: string };
}) {
const typeColor: Record = {
strong: colors.success,
consider: colors.compareOrange,
reject: colors.danger,
};
return (
录用建议
{suggestions.map((s) => (
{s.title}
{s.desc}
{s.selected && }
))}
薪资建议
{salary.range}
{salary.level}
{salary.note}
);
}
/* ===================== 页面主组件 ===================== */
export default function CandidateDetailScreen() {
const router = useRouter();
const { candidate, loading, error, invite, inviting, invited } = useCandidateDetail('candidate-1');
if (loading) {
return (
);
}
if (error || !candidate) {
return (
加载失败,请稍后重试
);
}
return (
发起沟通
{invited ? '已邀请' : inviting ? '邀请中...' : '邀请面试'}
);
}
/* ===================== 样式 ===================== */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.pageBg,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.pageBg,
},
errorText: {
color: colors.textSecondary,
fontSize: 14,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: colors.white,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
headerBtn: {
width: 36,
height: 36,
justifyContent: 'center',
alignItems: 'center',
},
headerTitle: {
fontSize: 17,
fontWeight: '600',
color: colors.textPrimary,
},
scroll: {
flex: 1,
},
scrollContent: {
padding: 16,
paddingBottom: 100,
},
infoCard: {
backgroundColor: colors.white,
borderRadius: 16,
padding: 16,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 6,
elevation: 2,
},
infoHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 14,
},
avatarBox: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
avatarText: {
fontSize: 22,
fontWeight: '700',
color: colors.primary,
},
infoMain: {
flex: 1,
},
infoName: {
fontSize: 18,
fontWeight: '700',
color: colors.textPrimary,
marginBottom: 4,
},
infoSub: {
fontSize: 13,
color: colors.textSecondary,
},
matchBox: {
alignItems: 'center',
backgroundColor: colors.successLight,
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 8,
},
matchValue: {
fontSize: 20,
fontWeight: '700',
color: colors.success,
},
matchLabel: {
fontSize: 11,
color: colors.successText,
},
contactRow: {
flexDirection: 'row',
marginBottom: 12,
},
contactItem: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 16,
},
contactText: {
fontSize: 13,
color: colors.textSecondary,
marginLeft: 4,
},
statusTag: {
alignSelf: 'flex-start',
backgroundColor: colors.infoLight,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
fontSize: 12,
color: colors.infoText,
fontWeight: '500',
},
scoreCard: {
backgroundColor: colors.white,
borderRadius: 16,
padding: 20,
marginBottom: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
scoreValue: {
fontSize: 38,
fontWeight: '700',
color: colors.primary,
},
scoreLabel: {
fontSize: 14,
color: colors.textSecondary,
marginTop: 2,
},
scoreMeta: {
fontSize: 13,
color: colors.textHint,
marginTop: 6,
},
scoreIcon: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: colors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
},
card: {
backgroundColor: colors.white,
borderRadius: 16,
padding: 16,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 6,
elevation: 2,
},
cardTitle: {
fontSize: 16,
fontWeight: '700',
color: colors.textPrimary,
marginBottom: 12,
},
radarWrap: {
alignItems: 'center',
},
abilityList: {
marginTop: 16,
},
abilityItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
abilityName: {
width: 70,
fontSize: 13,
color: colors.textSecondary,
},
abilityTrack: {
flex: 1,
height: 8,
borderRadius: 4,
backgroundColor: colors.borderLight,
marginHorizontal: 10,
},
abilityFill: {
height: 8,
borderRadius: 4,
backgroundColor: colors.primary,
},
abilityScore: {
width: 28,
fontSize: 13,
fontWeight: '600',
color: colors.textPrimary,
textAlign: 'right',
},
reviewItem: {
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
reviewHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
reviewUser: {
flexDirection: 'row',
alignItems: 'center',
},
reviewName: {
fontSize: 14,
fontWeight: '600',
color: colors.textPrimary,
marginRight: 8,
},
reviewRole: {
fontSize: 12,
color: colors.textSecondary,
},
reviewRating: {
flexDirection: 'row',
alignItems: 'center',
},
reviewScore: {
fontSize: 14,
fontWeight: '700',
color: colors.compareOrange,
marginRight: 4,
},
reviewContent: {
fontSize: 14,
color: colors.textPrimary,
lineHeight: 20,
marginBottom: 10,
},
tagRow: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 8,
},
reviewTag: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: colors.primaryLight,
marginRight: 8,
marginBottom: 6,
},
reviewTagText: {
fontSize: 11,
color: colors.primaryText,
},
reviewDate: {
fontSize: 12,
color: colors.textHint,
},
suggestionRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.borderLight,
marginBottom: 10,
},
suggestionDot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 10,
},
suggestionBody: {
flex: 1,
},
suggestionTitle: {
fontSize: 14,
fontWeight: '600',
color: colors.textPrimary,
marginBottom: 2,
},
suggestionDesc: {
fontSize: 12,
color: colors.textSecondary,
},
salaryBox: {
marginTop: 12,
padding: 14,
borderRadius: 12,
backgroundColor: colors.infoLight,
},
salaryLabel: {
fontSize: 13,
color: colors.infoText,
marginBottom: 6,
},
salaryRange: {
fontSize: 24,
fontWeight: '700',
color: colors.info,
},
salaryLevel: {
fontSize: 13,
color: colors.infoText,
marginTop: 2,
},
salaryNote: {
fontSize: 12,
color: colors.textSecondary,
marginTop: 6,
},
footer: {
flexDirection: 'row',
padding: 16,
paddingBottom: 24,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
chatBtn: {
flex: 1,
height: 46,
borderRadius: 24,
borderWidth: 1,
borderColor: colors.border,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
chatText: {
fontSize: 15,
fontWeight: '600',
color: colors.textPrimary,
},
inviteBtn: {
flex: 2,
height: 46,
borderRadius: 24,
backgroundColor: colors.primaryBtn,
justifyContent: 'center',
alignItems: 'center',
},
inviteBtnDisabled: {
opacity: 0.6,
},
inviteText: {
fontSize: 15,
fontWeight: '600',
color: colors.white,
},
});