import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Circle, Rect } from 'react-native-svg'; import { useRankData } from '../../src/hooks'; import { buildRankReportText } from '../../src/api'; import { shareText } from '../../src/lib/shareFile'; import { colors } from '../../src/theme'; import { formatThousands } from '../../src/utils'; import type { DimensionPercentile, RankData, RankEmptyState, TopCandidate } from '../../src/types'; /** 分布条刻度(0/25/50/75/100) */ const DISTRIBUTION_TICKS = [0, 25, 50, 75, 100]; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack }: { onBack: () => void }) { return ( 同岗位竞争力排名 ); } /* ===================== 子组件:综合排名卡片 ===================== */ function RankSummary({ rank, onExport }: { rank: RankData; onExport: () => void }) { return ( 第 {rank.rank} 名 超越 {rank.percentile}% 的候选人 综合评分 {rank.overallScore} 分 {rank.scoreLevel} 百分位排名:{rank.rankPosition} {/* 本页已把维度百分位、TOP 候选人与全部提升建议都渲染出来了, 没有「更详细」的隐藏内容可跳,因此这里是导出/分享而非跳转 —— 文案与图标如实反映 */} 导出分析报告 ); } /* ===================== 子组件:百分位分布图(SVG) ===================== */ function DistributionChart({ percentile }: { percentile: number }) { const clamped = Math.max(0, Math.min(100, percentile)); const width = 300; const height = 40; const padding = 20; const trackWidth = width - padding * 2; const fillWidth = (trackWidth * clamped) / 100; const dotX = padding + fillWidth; return ( {/* 轨道 */} {/* 已超越填充 */} {/* 当前所在位置圆点 */} {DISTRIBUTION_TICKS.map((tick) => ( {tick}% ))} ); } /* ===================== 子组件:维度百分位行 ===================== */ function DimensionRow({ dim, isLast }: { dim: DimensionPercentile; isLast: boolean }) { return ( {dim.name} {dim.percentile} {dim.score} 分 ); } /* ===================== 子组件:TOP 候选人行 ===================== */ function CandidateRow({ candidate, rank }: { candidate: TopCandidate; rank: number }) { return ( {rank} {candidate.name} {candidate.score}分 {candidate.tags.join(' · ')} ); } /* ===================== 子组件:空状态 ===================== */ function EmptyState({ state, onAction }: { state: RankEmptyState; onAction: () => void }) { return ( {state.title} {state.subtitle} {state.actionText} 简历完善进度 {state.resumeProgress.map((step, index) => ( {step.completed ? ( ) : ( {index + 1} )} {index < state.resumeProgress.length - 1 && ( )} {step.label} {step.tip ? {step.tip} : null} ))} ); } /* ===================== 子组件:提升建议 ===================== */ function TipItem({ title, description, type, onAction, }: { title: string; description: string; type: 'tip' | 'action'; onAction?: () => void; }) { if (type === 'action') { return ( {title} ); } return ( {title} {description ? {description} : null} ); } /* ===================== 页面 ===================== */ export default function RankScreen() { const router = useRouter(); const { jobId } = useLocalSearchParams<{ jobId: string }>(); const { rankData, dimensions, candidates, tips, emptyState, hasData, loading, error, refresh } = useRankData(jobId || '1'); /** * 导出竞争力分析报告:把本页已展示的排名、维度百分位与提升建议拼成文本经系统分享面板发出。 * `ok` 只代表面板已拉起,不能据此提示「导出成功」,只在失败时说明原因(见 lib/shareFile)。 */ const handleExport = async () => { if (!rankData) return; const outcome = await shareText( buildRankReportText({ hasData: true, rankData, dimensions, tips }), '竞争力分析报告', ); if (!outcome.ok) Alert.alert('无法导出', outcome.message); }; if (loading) { return (
router.back()} /> ); } if (error) { return (
router.back()} /> 加载失败 重试 ); } if (!hasData && emptyState) { return (
router.back()} /> router.push('/resume')} /> ); } if (!rankData) { return (
router.back()} /> 暂无数据 重试 ); } return (
router.back()} /> {/* 模块2:岗位信息 */} {rankData.jobTitle} · {rankData.company} 共 {formatThousands(rankData.totalCandidates)} 位候选人申请 {/* 模块3:综合排名卡片 */} {/* 模块4:竞争力百分位分布图 */} | 竞争力百分位分布 {/* 模块5:各维度百分位排名 */} | 各维度百分位排名 {dimensions.map((dim, index) => ( ))} {/* 模块6:TOP 10 候选人 */} TOP 10 候选人 {candidates.map((candidate, index) => ( ))} {/* 模块7:提升建议 */} 提升建议 {tips.map((tip) => tip.type === 'action' && tip.route ? ( router.push(tip.route as string)} /> ) : ( ), )} ); } /* ===================== 样式 ===================== */ const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg, }, /* ---- 顶部导航 ---- */ header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, paddingVertical: 4, }, headerBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', }, headerTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, }, /* ---- 状态 ---- */ centerContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, errorText: { marginTop: 12, fontSize: 16, color: colors.danger, }, retryButton: { marginTop: 16, backgroundColor: colors.primary, paddingHorizontal: 32, paddingVertical: 10, borderRadius: 24, }, retryButtonText: { color: colors.white, fontSize: 16, fontWeight: '600', }, /* ---- 滚动内容 ---- */ scrollView: { flex: 1, }, scrollContent: { paddingHorizontal: 16, paddingBottom: 40, }, bottomSpacer: { height: 20, }, /* ---- 岗位信息 ---- */ jobInfoWrap: { paddingVertical: 12, alignItems: 'center', }, jobTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary, }, candidateCount: { marginTop: 4, fontSize: 13, color: colors.textSecondary, }, /* ---- 综合排名卡片 ---- */ rankCard: { backgroundColor: colors.primaryLight, borderRadius: 12, padding: 16, borderWidth: 1, borderColor: colors.primaryBorder, }, rankNumber: { fontSize: 28, fontWeight: '700', color: colors.primary, marginBottom: 4, }, rankPercentile: { fontSize: 20, fontWeight: '600', color: colors.textPrimary, marginBottom: 8, }, scoreRow: { flexDirection: 'row', alignItems: 'center', marginTop: 4, }, scoreLabel: { fontSize: 14, color: colors.textSecondary, }, scoreValue: { fontSize: 16, fontWeight: '600', color: colors.textPrimary, marginLeft: 8, }, levelBadge: { backgroundColor: colors.successLight, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, marginLeft: 8, }, levelText: { fontSize: 12, color: colors.successText, fontWeight: '600', }, rankPosition: { marginTop: 8, fontSize: 13, color: colors.textSecondary, }, detailLink: { flexDirection: 'row', alignItems: 'center', marginTop: 10, }, detailLinkText: { fontSize: 14, color: colors.primary, fontWeight: '500', marginRight: 2, }, /* ---- 分区 ---- */ section: { backgroundColor: colors.white, borderRadius: 12, padding: 16, marginTop: 16, }, sectionTitle: { fontSize: 16, fontWeight: '600', color: colors.textPrimary, marginBottom: 12, }, /* ---- 百分位分布图 ---- */ distributionLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4, }, distributionLabel: { fontSize: 11, color: colors.textSecondary, }, /* ---- 维度行 ---- */ dimensionItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.border, }, dimensionItemLast: { borderBottomWidth: 0, }, dimensionName: { fontSize: 14, color: colors.textPrimary, }, dimensionRight: { flexDirection: 'row', alignItems: 'center', }, dimensionPercentile: { fontSize: 13, color: colors.primary, fontWeight: '500', marginRight: 12, }, dimensionScore: { fontSize: 14, color: colors.textSecondary, width: 42, textAlign: 'right', }, /* ---- TOP 候选人 ---- */ candidateItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.border, }, candidateRank: { width: 28, height: 28, borderRadius: 14, backgroundColor: colors.pageBg, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, candidateRankText: { fontSize: 12, fontWeight: '600', color: colors.textSecondary, }, candidateInfo: { flex: 1, }, candidateHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, candidateName: { fontSize: 14, fontWeight: '600', color: colors.textPrimary, }, candidateScore: { fontSize: 14, fontWeight: '700', color: colors.primary, }, candidateTagLine: { marginTop: 4, fontSize: 12, color: colors.textSecondary, }, /* ---- 空状态 ---- */ emptyContent: { paddingHorizontal: 16, paddingTop: 24, paddingBottom: 40, }, emptyCard: { backgroundColor: colors.white, borderRadius: 16, padding: 28, alignItems: 'center', }, emptyIconWrap: { width: 88, height: 88, borderRadius: 44, backgroundColor: colors.primaryLight, alignItems: 'center', justifyContent: 'center', marginBottom: 16, }, emptyTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, marginBottom: 8, }, emptySubtitle: { fontSize: 13, color: colors.textSecondary, textAlign: 'center', lineHeight: 20, marginBottom: 20, }, emptyBtn: { backgroundColor: colors.primaryBtn, paddingHorizontal: 32, paddingVertical: 12, borderRadius: 24, }, emptyBtnText: { color: colors.white, fontSize: 15, fontWeight: '600', }, progressCard: { backgroundColor: colors.white, borderRadius: 16, padding: 20, marginTop: 16, }, progressTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, marginBottom: 16, }, progressRow: { flexDirection: 'row', minHeight: 56, }, progressLeft: { width: 24, alignItems: 'center', }, progressDot: { width: 24, height: 24, borderRadius: 12, alignItems: 'center', justifyContent: 'center', borderWidth: 1, }, progressDotDone: { backgroundColor: colors.success, borderColor: colors.success, }, progressDotTodo: { backgroundColor: colors.white, borderColor: colors.borderStrong, }, progressDotFirst: { borderColor: colors.success, }, progressDotText: { fontSize: 12, color: colors.textSecondary, }, progressLine: { width: 2, flex: 1, backgroundColor: colors.border, marginVertical: 4, }, progressLineDone: { backgroundColor: colors.success, }, progressBody: { flex: 1, marginLeft: 12, paddingBottom: 16, }, progressLabel: { fontSize: 14, fontWeight: '600', color: colors.textPrimary, }, progressTip: { fontSize: 12, color: colors.textSecondary, marginTop: 4, lineHeight: 18, }, /* ---- 提升建议 ---- */ tipItem: { paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.border, }, tipTitle: { fontSize: 14, fontWeight: '500', color: colors.textPrimary, }, tipDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 4, }, tipAction: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: colors.primaryLight, borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12, marginTop: 8, }, tipActionText: { fontSize: 14, color: colors.primary, fontWeight: '500', }, });