import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useState } from 'react'; import { ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useTalentRecommendation } from '../../src/hooks'; import { colors } from '../../src/theme'; import type { TalentCandidate, TalentRecommendationTab } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ companyName, onBack }: { companyName: string; onBack: () => void }) { return ( {companyName} 人才推荐 ); } /* ===================== 子组件:Tab 栏 ===================== */ function TabBar({ tabs, active, onChange, }: { tabs: TalentRecommendationTab[]; active: string; onChange: (key: string) => void; }) { return ( {tabs.map((tab) => { const isActive = tab.key === active; return ( onChange(tab.key)}> {tab.label} ({tab.count}) ); })} ); } /* ===================== 子组件:今日推荐头部 ===================== */ function TodayHeader({ count, onReport }: { count: number; onReport: () => void }) { return ( 今日为你推荐 {count} 位优质候选人 查看人才市场报告 ); } /* ===================== 子组件:候选人卡片 ===================== */ function CandidateCard({ candidate }: { candidate: TalentCandidate }) { const router = useRouter(); return ( {candidate.avatar} {candidate.name} {candidate.matchPercent}% {candidate.title} {candidate.age}岁 · {candidate.school} {candidate.expectedSalary} 综合评分 {candidate.overallScore} 超越 {candidate.percentile}% 候选人 {candidate.tags.map((tag) => ( {tag} ))} router.navigate('/enterprise-chat')}> 发起沟通 router.push('/candidate-detail?id=' + candidate.id)}> 查看详情 ); } /* ===================== 子组件:人才池数据 ===================== */ function TalentPool({ stats, actions, onPressAction }: { stats: { id: string; value: string; label: string }[]; actions: PoolActionItem[]; onPressAction: (action: PoolActionItem) => void }) { return ( 人才池数据 {stats.map((stat) => ( {stat.value} {stat.label} ))} {actions.map((action) => ( onPressAction(action)}> {action.label} ))} ); } /* ===================== 子组件:空状态 ===================== */ function EmptyState({ actions, dataReference, onFilter, }: { actions: string[]; dataReference: { label: string; value: string }[]; onFilter: () => void; }) { return ( 暂无匹配候选人 当前筛选条件下没有较适合的候选人,试试调整筛选条件 调整筛选条件 推荐操作 {actions.map((action, index) => ( {action} ))} 数据参考 {dataReference.map((item, index) => ( {item.label} {item.value} ))} ); } /* ===================== 页面 ===================== */ /** * 人才池快捷操作项:带 route 的跳转目标页,无对应页面的弹「功能开发中」提示。 * 与 enterprise-chat.tsx 的 QuickActionItem 保持同一约定。 */ type PoolActionItem = { icon: string; label: string; route?: string }; const POOL_ACTIONS: PoolActionItem[] = [ { icon: 'mail-outline', label: '批量发邀请' }, { icon: 'document-text-outline', label: '批量发Offer' }, { icon: 'calendar-outline', label: '面试评分', route: '/interview-score' }, { icon: 'notifications-outline', label: '群发通知' }, ]; export default function TalentRecommendationScreen() { const router = useRouter(); const { recommendation, loading, error, refresh } = useTalentRecommendation(); const [activeTab, setActiveTab] = useState('all'); // 人才池快捷操作分发:有目标路由则跳转,无对应页面则提示「功能开发中」, // 避免按钮点击后无任何反馈。与 enterprise-chat.tsx 的 handleQuickAction 同约定。 const handlePoolAction = (action: PoolActionItem) => { if (action.route) { router.push(action.route); } else { Alert.alert('提示', `${action.label}功能开发中,敬请期待`); } }; // 以下两处原先绑的是空函数(() => {}),按钮点击后无任何反馈。 // 暂无对应页面,按项目约定弹「功能开发中」提示。 const handleReport = () => { Alert.alert('提示', '人才市场报告功能开发中,敬请期待'); }; const handleFilter = () => { Alert.alert('提示', '筛选条件功能开发中,敬请期待'); }; if (loading) { return (
router.back()} /> ); } if (error || !recommendation) { return (
router.back()} /> 加载失败 重试 ); } return (
router.back()} /> {recommendation.hasData ? ( {recommendation.candidates.map((candidate) => ( ))} ) : ( )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, paddingVertical: 12, backgroundColor: colors.white, }, headerBtn: { width: 40, alignItems: 'center' }, headerCenter: { alignItems: 'center' }, headerCompany: { fontSize: 12, color: colors.textSecondary }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary, marginTop: 2 }, headerActions: { flexDirection: 'row', alignItems: 'center', }, headerActionBtn: { width: 36, alignItems: 'center', }, tabBar: { flexDirection: 'row', backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, tabItem: { flex: 1, alignItems: 'center', paddingTop: 8, }, tabLabel: { fontSize: 14, color: colors.textSecondary, paddingBottom: 8, }, tabLabelActive: { color: colors.textPrimary, fontWeight: '600', }, tabCount: { fontSize: 12, color: colors.textHint, marginLeft: 2, }, tabCountActive: { color: colors.primary, }, tabIndicator: { height: 2, width: 24, borderRadius: 1, backgroundColor: 'transparent', marginBottom: -1, }, tabIndicatorActive: { backgroundColor: colors.primary, }, body: { padding: 16, paddingBottom: 24, }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, }, tip: { fontSize: 14, color: colors.textSecondary }, retryBtn: { marginTop: 8, paddingHorizontal: 24, paddingVertical: 8, borderRadius: 18, backgroundColor: colors.primaryBtn, }, retryText: { color: colors.white, fontSize: 13, fontWeight: '600' }, todayHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, }, todayTitle: { fontSize: 13, color: colors.textSecondary, }, todayCountRow: { flexDirection: 'row', alignItems: 'baseline', marginTop: 2, }, todayCount: { fontSize: 28, fontWeight: '800', color: colors.primary, }, todayUnit: { fontSize: 13, color: colors.textSecondary, marginLeft: 4, }, reportBtn: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.primaryLight, borderRadius: 14, paddingHorizontal: 10, paddingVertical: 6, }, reportBtnText: { fontSize: 12, color: colors.primary, fontWeight: '600', marginRight: 2, }, candidateCard: { backgroundColor: colors.white, borderRadius: 12, padding: 16, marginBottom: 12, }, candidateHeader: { flexDirection: 'row', }, avatar: { width: 52, height: 52, borderRadius: 26, backgroundColor: colors.primaryLight, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, avatarText: { fontSize: 20, fontWeight: '700', color: colors.primary, }, candidateInfo: { flex: 1, }, nameRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, candidateName: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, }, matchTag: { backgroundColor: colors.successLight, borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2, }, matchTagText: { fontSize: 12, fontWeight: '800', color: colors.successText, }, candidateTitle: { fontSize: 13, color: colors.textSecondary, marginTop: 2, }, candidateMeta: { fontSize: 12, color: colors.textHint, marginTop: 4, }, candidateSalary: { fontSize: 13, color: colors.action, fontWeight: '600', marginTop: 4, }, scoreRow: { flexDirection: 'row', alignItems: 'center', marginTop: 12, }, scoreLabel: { fontSize: 13, color: colors.textSecondary, }, scoreValue: { fontSize: 14, fontWeight: '800', color: colors.primary, marginLeft: 6, }, percentileText: { fontSize: 12, color: colors.textHint, marginLeft: 8, }, tagRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10, }, tag: { backgroundColor: colors.pageBg, borderRadius: 6, paddingHorizontal: 8, paddingVertical: 3, }, tagText: { fontSize: 11, color: colors.textSecondary, }, candidateActions: { flexDirection: 'row', marginTop: 14, gap: 10, }, chatBtn: { flex: 1, backgroundColor: colors.primaryBtn, borderRadius: 18, paddingVertical: 10, alignItems: 'center', }, chatBtnText: { color: colors.white, fontSize: 14, fontWeight: '600', }, detailBtn: { flex: 1, backgroundColor: colors.primaryLight, borderRadius: 18, paddingVertical: 10, alignItems: 'center', }, detailBtnText: { color: colors.primary, fontSize: 14, fontWeight: '600', }, poolCard: { backgroundColor: colors.white, borderRadius: 12, padding: 16, marginTop: 4, }, poolTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 12, }, poolStatsRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16, }, poolStat: { flex: 1, alignItems: 'center', }, poolStatValue: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, }, poolStatLabel: { fontSize: 11, color: colors.textSecondary, marginTop: 4, }, poolActions: { flexDirection: 'row', justifyContent: 'space-around', paddingTop: 14, borderTopWidth: 1, borderTopColor: colors.borderLight, }, poolAction: { alignItems: 'center', }, poolActionText: { fontSize: 11, color: colors.textSecondary, marginTop: 4, }, 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', marginBottom: 20, }, emptyBtn: { backgroundColor: colors.primaryBtn, paddingHorizontal: 32, paddingVertical: 12, borderRadius: 24, marginBottom: 24, }, emptyBtnText: { color: colors.white, fontSize: 15, fontWeight: '600', }, actionCard: { width: '100%', backgroundColor: colors.actionLight, borderRadius: 12, padding: 16, marginBottom: 16, alignItems: 'flex-start', }, actionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, actionTitle: { fontSize: 14, fontWeight: '700', color: colors.actionText, marginLeft: 8, }, actionRow: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 10, }, actionDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: colors.action, marginTop: 6, marginRight: 10, }, actionText: { flex: 1, fontSize: 13, color: colors.textPrimary, lineHeight: 18, }, dataCard: { width: '100%', backgroundColor: colors.primaryLight, borderRadius: 12, padding: 16, alignItems: 'flex-start', }, dataTitle: { fontSize: 14, fontWeight: '700', color: colors.primaryText, marginBottom: 12, }, dataRow: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 10, }, dataText: { flex: 1, fontSize: 13, color: colors.textPrimary, marginLeft: 10, lineHeight: 18, }, dataValue: { color: colors.success, fontWeight: '700', }, });