import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useState } from 'react'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, 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, searching, onToggleSearch, }: { companyName: string; onBack: () => void; /** 是否处于搜索态;加载中/加载失败时没有数据可搜,不传即不渲染搜索按钮 */ searching?: boolean; onToggleSearch?: () => void; }) { return ( {companyName} 人才推荐 {/* 原先这里是「搜索 + 漏斗」两个图标:漏斗没有对应页面,先摘掉; 搜索改成真的关键词过滤(姓名 / 职位 / 技能 / 学校) */} {onToggleSearch && ( )} ); } /* ===================== 子组件:搜索栏 ===================== */ function SearchBar({ value, onChange, onClear, }: { value: string; onChange: (text: string) => void; onClear: () => void; }) { return ( {value !== '' && ( )} ); } /* ===================== 子组件: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 }: { count: number }) { 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` 必填 —— 每个入口都必须有落点, * 批量发邀请 / 批量发Offer / 群发通知三项因项目内没有对应页面,已摘掉。 */ type PoolActionItem = { icon: string; label: string; route: string }; const POOL_ACTIONS: PoolActionItem[] = [ { icon: 'calendar-outline', label: '面试评分', route: '/interview-score' }, ]; export default function TalentRecommendationScreen() { const router = useRouter(); const { recommendation, activeTab, setActiveTab, keyword, setKeyword, candidates, isEmptySearch, loading, error, refresh, } = useTalentRecommendation(); const [searching, setSearching] = useState(false); const toggleSearch = () => { if (searching) setKeyword(''); setSearching((prev) => !prev); }; /** 人才池快捷操作:每个入口都由数据保证有落点,页面不再需要「没页面就弹提示」的分支 */ const handlePoolAction = (action: PoolActionItem) => { router.push(action.route); }; /** 空状态里的「调整筛选条件」:把筛选真正复位(切回全部 + 清关键词) */ const handleFilter = () => { setActiveTab('all'); setKeyword(''); }; if (loading) { return (
router.back()} /> ); } if (error || !recommendation) { return (
router.back()} /> 加载失败 重试 ); } return (
router.back()} searching={searching} onToggleSearch={toggleSearch} /> {searching && ( setKeyword('')} /> )} {recommendation.hasData ? ( {candidates.length === 0 ? ( {isEmptySearch ? `没有匹配「${keyword.trim()}」的候选人` : '这个筛选条件下暂时没有候选人'} 重置筛选 ) : ( 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', }, searchBar: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: colors.white, paddingHorizontal: 16, paddingBottom: 10, }, searchInput: { flex: 1, backgroundColor: colors.pageBg, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 8, fontSize: 14, color: colors.textPrimary, }, emptyList: { alignItems: 'center', gap: 10, paddingVertical: 32, backgroundColor: colors.white, borderRadius: 14, }, emptyListText: { fontSize: 13, color: colors.textSecondary }, emptyListBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight, }, emptyListBtnText: { fontSize: 13, fontWeight: '600', color: colors.primaryText }, 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', }, });