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 (
);
}
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',
},
});