轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

630 lines
17 KiB

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 { useEnterpriseCommunication } from '../../src/hooks';
import { colors } from '../../src/theme';
import type { EnterpriseChatItem, EnterpriseCommunicationTab, PendingTask } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
function Header({ companyName, onBack }: { companyName: string; onBack: () => void }) {
const router = useRouter();
return (
<View style={styles.header}>
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</Pressable>
<View style={styles.headerCenter}>
<Text style={styles.headerCompany}>{companyName}</Text>
<Text style={styles.headerTitle}>沟通</Text>
</View>
<Pressable
style={styles.startBtn}
onPress={() => router.navigate('/talent-recommendation')}
hitSlop={6}
>
<Ionicons name="add" size={16} color={colors.white} />
<Text style={styles.startBtnText}>发起沟通</Text>
</Pressable>
</View>
);
}
/* ===================== 子组件:Tab 栏 ===================== */
function TabBar({
tabs,
active,
onChange,
}: {
tabs: EnterpriseCommunicationTab[];
active: string;
onChange: (key: string) => void;
}) {
return (
<View style={styles.tabBar}>
{tabs.map((tab) => {
const isActive = tab.key === active;
return (
<Pressable key={tab.key} style={styles.tabItem} onPress={() => onChange(tab.key)}>
<Text style={[styles.tabLabel, isActive && styles.tabLabelActive]}>
{tab.label}
<Text style={[styles.tabCount, isActive && styles.tabCountActive]}>({tab.count})</Text>
</Text>
<View style={[styles.tabIndicator, isActive && styles.tabIndicatorActive]} />
</Pressable>
);
})}
</View>
);
}
/* ===================== 子组件:待处理任务 ===================== */
function PendingTasks({ tasks }: { tasks: PendingTask[] }) {
return (
<View style={styles.pendingCard}>
<Text style={styles.pendingTitle}>待处理</Text>
<View style={styles.pendingGrid}>
{tasks.map((task) => {
const token =
task.color === 'action'
? colors.action
: task.color === 'compareOrange'
? colors.compareOrange
: colors.primary;
return (
<Pressable key={task.id} style={styles.pendingItem}>
<View style={[styles.pendingIconWrap, { backgroundColor: token + '15' }]}>
<Ionicons name={task.icon as keyof typeof Ionicons.glyphMap} size={22} color={token} />
</View>
<View style={styles.pendingBody}>
<View style={styles.pendingLabelRow}>
<Text style={styles.pendingLabel}>{task.label}</Text>
<View style={styles.pendingBadge}>
<Text style={styles.pendingBadgeText}>{task.count}人</Text>
</View>
</View>
{task.hint ? <Text style={styles.pendingHint}>{task.hint}</Text> : null}
</View>
<Ionicons name="chevron-forward" size={16} color={colors.textHint} />
</Pressable>
);
})}
</View>
</View>
);
}
/* ===================== 子组件:聊天列表项 ===================== */
function ChatRow({ chat }: { chat: EnterpriseChatItem }) {
const router = useRouter();
return (
<Pressable style={styles.chatRow} onPress={() => router.push('/chat/' + chat.id)}>
<View style={styles.chatAvatar}>
<Text style={styles.chatAvatarText}>{chat.avatar}</Text>
</View>
<View style={styles.chatBody}>
<View style={styles.chatHeader}>
<View style={styles.chatNameWrap}>
<Text style={styles.chatName}>{chat.name}</Text>
{chat.labels.length > 0 ? (
<View style={styles.chatLabelTag}>
<Text style={styles.chatLabelTagText}>{chat.labels[0]}</Text>
</View>
) : null}
</View>
<Text style={styles.chatTime}>{chat.time}</Text>
</View>
<Text style={styles.chatTitle}>{chat.title}</Text>
<View style={styles.chatFooter}>
<Text style={styles.chatLastMessage} numberOfLines={1}>
{chat.lastMessage}
</Text>
{chat.unreadCount > 0 ? (
<View style={styles.unreadBadge}>
<Text style={styles.unreadBadgeText}>{chat.unreadCount}</Text>
</View>
) : null}
</View>
</View>
</Pressable>
);
}
/* ===================== 子组件:搜索栏 ===================== */
function SearchBar() {
return (
<View style={styles.searchBar}>
<Ionicons name="search-outline" size={18} color={colors.textHint} />
<Text style={styles.searchPlaceholder}>搜索候选人...</Text>
</View>
);
}
/* ===================== 子组件:快捷操作栏 ===================== */
/** 单个快捷操作:有 route 则跳转,无对应页面时由页面层给出「功能开发中」提示 */
type QuickActionItem = { icon: string; label: string; route?: string };
function QuickActions({
actions,
onPressAction,
}: {
actions: QuickActionItem[];
onPressAction: (action: QuickActionItem) => void;
}) {
return (
<View style={styles.quickActionsCard}>
{actions.map((action) => (
<Pressable key={action.label} style={styles.quickAction} onPress={() => onPressAction(action)}>
<Ionicons name={action.icon as keyof typeof Ionicons.glyphMap} size={20} color={colors.textSecondary} />
<Text style={styles.quickActionText}>{action.label}</Text>
</Pressable>
))}
</View>
);
}
/* ===================== 子组件:空状态 ===================== */
function EmptyState({
dataReference,
onDiscover,
}: {
dataReference: { label: string; value: string }[];
onDiscover: () => void;
}) {
return (
<View style={styles.emptyCard}>
<View style={styles.emptyIconWrap}>
<Ionicons name="chatbubbles-outline" size={48} color={colors.primary} />
</View>
<Text style={styles.emptyTitle}>还没有沟通记录</Text>
<Text style={styles.emptySubtitle}>从人才库中发现合适的候选人,主动发起沟通吧</Text>
<Pressable style={styles.emptyBtn} onPress={onDiscover} hitSlop={6}>
<Text style={styles.emptyBtnText}>去发现人才</Text>
</Pressable>
<View style={styles.dataCard}>
<Text style={styles.dataTitle}>数据参考</Text>
{dataReference.map((item, index) => (
<View key={index} style={styles.dataRow}>
<Ionicons name="trending-up-outline" size={16} color={colors.success} />
<Text style={styles.dataText}>
{item.label}
<Text style={styles.dataValue}> {item.value}</Text>
</Text>
</View>
))}
</View>
</View>
);
}
/* ===================== 页面 ===================== */
const QUICK_ACTIONS: QuickActionItem[] = [
{ icon: 'mail-outline', label: '批量发邀请', route: '/talent-recommendation' },
{ icon: 'document-text-outline', label: '批量发Offer' },
{ icon: 'calendar-outline', label: '面试评分', route: '/interview-score' },
{ icon: 'notifications-outline', label: '群发通知' },
];
export default function EnterpriseChatScreen() {
const router = useRouter();
const { communication, loading, error, refresh } = useEnterpriseCommunication();
const [activeTab, setActiveTab] = useState('all');
// 快捷操作分发:有目标路由则跳转,无对应页面则提示「功能开发中」
const handleQuickAction = (action: QuickActionItem) => {
if (action.route) {
router.push(action.route);
} else {
Alert.alert('提示', `${action.label}功能开发中,敬请期待`);
}
};
if (loading) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<Header companyName="智联科技" onBack={() => router.back()} />
<View style={styles.center}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
</SafeAreaView>
);
}
if (error || !communication) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<Header companyName="智联科技" onBack={() => router.back()} />
<View style={styles.center}>
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} />
<Text style={styles.tip}>加载失败</Text>
<Pressable style={styles.retryBtn} onPress={refresh}>
<Text style={styles.retryText}>重试</Text>
</Pressable>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<Header companyName={communication.companyName} onBack={() => router.back()} />
<TabBar tabs={communication.tabs} active={activeTab} onChange={setActiveTab} />
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
{communication.hasData ? (
<View>
<PendingTasks tasks={communication.pendingTasks} />
<SearchBar />
<View style={styles.chatListCard}>
{communication.chats.map((chat, index) => {
const isLast = index === communication.chats.length - 1;
return (
<View key={chat.id} style={isLast ? undefined : styles.chatBorder}>
<ChatRow chat={chat} />
</View>
);
})}
</View>
<QuickActions actions={QUICK_ACTIONS} onPressAction={handleQuickAction} />
</View>
) : (
<EmptyState
dataReference={communication.dataReference}
onDiscover={() => router.navigate('/talent-recommendation')}
/>
)}
</ScrollView>
</SafeAreaView>
);
}
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 },
startBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primaryBtn,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 16,
},
startBtnText: {
color: colors.white,
fontSize: 12,
fontWeight: '600',
marginLeft: 2,
},
tabBar: {
flexDirection: 'row',
backgroundColor: colors.white,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
tabItem: {
flex: 1,
alignItems: 'center',
paddingTop: 8,
},
tabLabel: {
fontSize: 13,
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: 20,
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' },
pendingCard: {
backgroundColor: colors.white,
borderRadius: 12,
padding: 16,
marginBottom: 12,
},
pendingTitle: {
fontSize: 15,
fontWeight: '700',
color: colors.textPrimary,
marginBottom: 12,
},
pendingGrid: {
gap: 12,
},
pendingItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.pageBg,
borderRadius: 10,
padding: 12,
},
pendingIconWrap: {
width: 40,
height: 40,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
pendingBody: {
flex: 1,
},
pendingLabelRow: {
flexDirection: 'row',
alignItems: 'center',
},
pendingLabel: {
fontSize: 14,
fontWeight: '600',
color: colors.textPrimary,
},
pendingBadge: {
backgroundColor: colors.actionLight,
borderRadius: 10,
paddingHorizontal: 8,
paddingVertical: 2,
marginLeft: 8,
},
pendingBadgeText: {
fontSize: 12,
fontWeight: '700',
color: colors.actionText,
},
pendingHint: {
fontSize: 12,
color: colors.textSecondary,
marginTop: 2,
},
searchBar: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
marginBottom: 12,
},
searchPlaceholder: {
marginLeft: 8,
fontSize: 14,
color: colors.textHint,
},
chatListCard: {
backgroundColor: colors.white,
borderRadius: 12,
overflow: 'hidden',
marginBottom: 12,
},
chatBorder: {
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
chatRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 14,
},
chatAvatar: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
chatAvatarText: {
fontSize: 18,
fontWeight: '700',
color: colors.primary,
},
chatBody: {
flex: 1,
},
chatHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
chatNameWrap: {
flexDirection: 'row',
alignItems: 'center',
},
chatName: {
fontSize: 15,
fontWeight: '700',
color: colors.textPrimary,
},
chatLabelTag: {
backgroundColor: colors.compareOrange + '20',
borderRadius: 8,
paddingHorizontal: 6,
paddingVertical: 2,
marginLeft: 8,
},
chatLabelTagText: {
fontSize: 11,
color: colors.compareOrange,
fontWeight: '600',
},
chatTime: {
fontSize: 12,
color: colors.textHint,
},
chatTitle: {
fontSize: 13,
color: colors.textSecondary,
marginTop: 2,
},
chatFooter: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
},
chatLastMessage: {
flex: 1,
fontSize: 13,
color: colors.textHint,
},
unreadBadge: {
minWidth: 18,
height: 18,
borderRadius: 9,
backgroundColor: colors.danger,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 8,
},
unreadBadgeText: {
fontSize: 11,
color: colors.white,
fontWeight: '700',
},
quickActionsCard: {
flexDirection: 'row',
backgroundColor: colors.white,
borderRadius: 12,
paddingVertical: 14,
},
quickAction: {
flex: 1,
alignItems: 'center',
},
quickActionText: {
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',
},
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',
},
});