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, spacing} 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 (
);
}
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: 8,
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: 8,
padding: 8,
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: 8,
padding: 8,
marginTop: 8,
},
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: spacing.framework,
paddingTop: 24,
paddingBottom: 40,
},
emptyCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
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: 8,
padding: 8,
marginTop: 8,
},
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',
},
});