import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import { useState } from 'react'; import { ActivityIndicator, Alert, Linking, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useMessageData } from '../../src/hooks'; import type { Conversation, Interview, Notification } from '../../src/types'; import { colors, headerGradient } from '../../src/theme'; /** Tab 键 */ type TabKey = 'message' | 'notification' | 'interview'; /** Tab 配置 */ const TABS: { key: TabKey; label: string }[] = [ { key: 'message', label: '消息' }, { key: 'notification', label: '通知' }, { key: 'interview', label: '面试' }, ]; /** 会话卡片 */ function ConversationCard({ conversation, onPress, }: { conversation: Conversation; onPress?: () => void; }) { return ( {conversation.avatar} {conversation.unread && } {conversation.contactName} {conversation.lastMessage} {conversation.timestamp} {conversation.matched && ( ✓ 已匹配 )} ); } /** 通知卡片 */ function NotificationCard({ notification }: { notification: Notification }) { return ( {!notification.read && } {notification.title} {notification.content} {notification.timestamp} ); } /** * 面试卡片的跳转目标:视频面试取会议链接,线下面试取地址导航。 * 由接口层给出的字段决定,字段缺失就返回 null —— 页面据此不渲染按钮, * 而不是渲染一个点不动的「加入会议」。 */ function interviewActionTarget(interview: Interview): { url: string; label: string } | null { if (interview.type === 'video') { return interview.meetingUrl ? { url: interview.meetingUrl, label: '加入会议' } : null; } return interview.location ? // 高德 URI API:callnative=1 优先唤起高德 App,未安装则落网页版 { url: `https://uri.amap.com/search?keyword=${encodeURIComponent(interview.location)}&src=youzhi&callnative=1`, label: '导航前往', } : null; } /** 面试卡片 */ function InterviewCard({ interview }: { interview: Interview }) { const isVideo = interview.type === 'video'; const target = interviewActionTarget(interview); /** 打不开就明说,不静默失败(未装地图 / 链接失效都会走到这里) */ const open = () => { if (!target) return; void Linking.openURL(target.url).catch(() => { Alert.alert( isVideo ? '无法打开会议链接' : '无法打开地图', isVideo ? '会议链接可能已失效,请联系 HR 重新获取。' : '请检查是否已安装地图应用后重试。', ); }); }; return ( {interview.companyName}·{interview.positionName} {interview.time} {isVideo ? '视频面试' : '线下面试'} {target ? ( {target.label} ) : ( {isVideo ? '会议链接待 HR 提供' : '面试地址待 HR 提供'} )} ); } /** * 消息/沟通页 */ export default function ChatScreen() { const router = useRouter(); const [tab, setTab] = useState('message'); const { conversations, notifications, interviews, matchResult, pendingOfferCount, loading, error, refresh, } = useMessageData(); // 加载态 if (loading) { return ( ); } // 错误态 if (error) { return ( 加载失败,请重试 重试 ); } return ( {/* 顶部导航 */} 消息 {/* Tab 切换栏 */} {TABS.map((item) => { const active = tab === item.key; return ( setTab(item.key)}> {item.label} ); })} {/* 内容区 */} {tab === 'message' && ( <> {/* 双向匹配成功横幅 */} {/* 双向匹配横幅:只陈述事实,不带「点击查看详情」—— 匹配详情页尚未定义(企业列表之外还要展示什么,属产品决策), 留着链接就是把用户带到一个不存在的地方。 */} {matchResult && ( 恭喜!您与{matchResult.total}家企业双向匹配成功 {matchResult.companies.join('、')} )} {/* 会话列表 */} {conversations.length === 0 ? ( 暂无消息 ) : ( conversations.map((conversation) => ( router.push(`/chat/${conversation.id}`)} /> )) )} )} {tab === 'notification' && ( <> {/* Offer 待处理横幅:数量来自接口层,为 0 时整条不渲染 */} {pendingOfferCount > 0 && ( router.push('/jobs/offer-compare')} > 您有 {pendingOfferCount} 个 Offer 待处理 立即查看 > )} {/* 系统通知列表 */} {notifications.length === 0 ? ( 暂无通知 ) : ( notifications.map((notification) => ( )) )} )} {tab === 'interview' && ( <> {/* 面试日程标题:本 Tab 即全部日程,不再挂一个无处可去的「全部日程 >」 */} 今日面试 {/* 面试卡片 */} {interviews.length === 0 ? ( 暂无面试 ) : ( interviews.map((interview) => ( )) )} )} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg, }, loading: { flex: 1, alignItems: 'center', justifyContent: 'center', }, stateText: { marginTop: 12, fontSize: 14, color: colors.textSecondary, }, retryBtn: { marginTop: 16, paddingHorizontal: 24, paddingVertical: 8, borderRadius: 18, backgroundColor: colors.primaryBtn, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.white, }, emptyText: { paddingVertical: 24, fontSize: 14, color: colors.textSecondary, textAlign: 'center', }, nav: { paddingHorizontal: 8, paddingTop: 8, paddingBottom: 12, }, navTitle: { fontSize: 24, fontWeight: '700', color: colors.textInverse, }, tabBar: { flexDirection: 'row', paddingHorizontal: 8, borderBottomWidth: 1, borderBottomColor: colors.border, }, tabItem: { flex: 1, alignItems: 'center', paddingTop: 4, }, tabLabel: { fontSize: 15, color: colors.textSecondary, paddingBottom: 8, }, tabLabelActive: { color: colors.textPrimary, fontWeight: '600', }, tabIndicator: { height: 2, width: 24, borderRadius: 1, backgroundColor: 'transparent', marginBottom: -1, }, tabIndicatorActive: { backgroundColor: colors.primary, }, scroll: { flex: 1, }, content: { paddingHorizontal: 8, paddingTop: 16, paddingBottom: 24, }, // 匹配横幅 matchBanner: { backgroundColor: colors.primaryLight, borderRadius: 8, padding: 8, marginBottom: 8, }, matchBannerTitle: { fontSize: 15, fontWeight: '700', color: colors.primaryDark, }, matchBannerCompanies: { marginTop: 6, fontSize: 13, color: colors.primaryDark, }, // 会话卡片 convCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, }, avatarWrap: { position: 'relative', }, avatar: { width: 44, height: 44, borderRadius: 22, backgroundColor: colors.primaryLight, alignItems: 'center', justifyContent: 'center', }, avatarText: { fontSize: 16, fontWeight: '700', color: colors.primary, }, unreadDot: { position: 'absolute', top: 0, right: 0, width: 10, height: 10, borderRadius: 5, backgroundColor: colors.danger, borderWidth: 1.5, borderColor: colors.white, }, convBody: { flex: 1, marginLeft: 12, }, contactName: { fontSize: 15, fontWeight: '600', color: colors.textPrimary, }, lastMessage: { marginTop: 4, fontSize: 13, color: colors.textSecondary, }, convRight: { alignItems: 'flex-end', marginLeft: 8, }, time: { fontSize: 12, color: colors.textSecondary, }, matchedTag: { marginTop: 6, backgroundColor: colors.primaryLight, borderRadius: 8, paddingHorizontal: 6, paddingVertical: 2, }, matchedText: { fontSize: 11, color: colors.primaryDark, }, // Offer 横幅 offerBanner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: colors.actionLight, borderRadius: 8, padding: 8, marginBottom: 8, }, offerTitle: { flex: 1, fontSize: 15, fontWeight: '700', color: colors.textPrimary, }, offerLink: { fontSize: 13, fontWeight: '600', color: colors.action, }, // 通知卡片 notiCard: { backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, }, notiTitleRow: { flexDirection: 'row', alignItems: 'center', }, notiDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: colors.danger, marginRight: 6, }, notiTitle: { fontSize: 15, fontWeight: '600', color: colors.textPrimary, }, notiContent: { marginTop: 6, fontSize: 13, lineHeight: 18, color: colors.textSecondary, }, notiTime: { marginTop: 8, fontSize: 12, color: colors.textSecondary, }, // 面试 sectionHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, }, sectionTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, }, sectionLink: { fontSize: 13, color: colors.primary, }, interviewCard: { backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, }, interviewHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, interviewTitle: { flex: 1, fontSize: 15, fontWeight: '700', color: colors.textPrimary, }, interviewTime: { marginLeft: 8, fontSize: 16, fontWeight: '700', color: colors.textPrimary, }, interviewFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, }, typeTag: { borderRadius: 8, paddingHorizontal: 8, paddingVertical: 3, }, typeTagVideo: { backgroundColor: colors.primaryLight, }, typeTagOffline: { backgroundColor: colors.border, }, typeText: { fontSize: 12, fontWeight: '600', }, typeTextVideo: { color: colors.primary, }, typeTextOffline: { color: colors.textSecondary, }, actionBtn: { backgroundColor: colors.primaryBtn, borderRadius: 18, paddingHorizontal: 16, paddingVertical: 8, }, actionText: { fontSize: 13, fontWeight: '600', color: colors.white, }, /** 会议链接 / 面试地址缺失时的说明文案,替代一个点不动的按钮 */ actionMissing: { fontSize: 13, color: colors.textHint, }, });