import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { 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 { LoadingState, ErrorState } from '../src/components/StateView'; import {colors, spacing} 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 ( 评分趋势 {[75, 80, 85, 90, 95].map((s) => { const y = padding.top + chartH - ((s - minScore) / (maxScore - minScore)) * chartH; return ; })} {points.map((p, i) => ( ))} {points.map((p, i) => ( {p.month} ))} ); } /* ===================== 子组件:八维能力雷达图 ===================== */ 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 ( 八维能力评分 {[0.2, 0.4, 0.6, 0.8, 1].map((scale) => ( { const px = center + (a.x - center) * scale; const py = center + (a.y - center) * scale; return `${px},${py}`; }).join(' ')} fill="none" stroke={colors.borderLight} strokeWidth={1} /> ))} {axisPoints.map((a, i) => ( ))} {axisPoints.map((a, i) => { const labelX = center + (a.x - center) * 1.18; const labelY = center + (a.y - center) * 1.18; return ( {a.label} ); })} {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 { id } = useLocalSearchParams<{ id: string }>(); const candidateId = (id && id.length > 0) ? id : 'candidate-1'; const { candidate, loading, error, refresh, invite, inviting, invited } = useCandidateDetail(candidateId); if (loading) { return ( ); } if (error || !candidate) { return ( ); } return (
router.back()} /> {/* 发起沟通:进该候选人的会话页(此前是空 Pressable,点了没反应) */} router.push(`/chat/${candidate.id}`)}> 发起沟通 {invited ? '已邀请' : inviting ? '邀请中...' : '邀请面试'} ); } /* ===================== 样式 ===================== */ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: colors.pageBg, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.framework, 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: spacing.card, paddingBottom: 100, }, infoCard: { backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card, marginBottom: 8, }, 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: spacing.card, paddingHorizontal: spacing.card, paddingVertical: spacing.card, }, 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: spacing.card, padding: spacing.card, marginBottom: 8, 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: spacing.card, padding: spacing.card, marginBottom: 8, }, 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: spacing.card, 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: spacing.card, borderRadius: spacing.card, borderWidth: 1, borderColor: colors.borderLight, marginBottom: 8, }, 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: 8, padding: spacing.card, borderRadius: spacing.card, 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: spacing.framework, 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, }, });