/** * 企业端 · 收到的投递(工作台列表) * * 这是企业侧投递链路的第一层:一行一个**投递**(不是候选人), * 展示状态、岗位、时间与下一步动作。状态变更与候选人反馈都从详情页发起。 * * 设计取舍: * - 筛选条上的计数来自**独立于筛选的**计数口,切到「面试中」不会让其他状态计数归零 * - 用 useFocusEffect 在返回时刷新:详情页改完状态后回到列表,徽标必须已经跟着变, * 否则用户会以为刚才那次操作没生效 */ import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect, useRouter } from 'expo-router'; import { useCallback, useRef, useState } from 'react'; import { Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { EmptyState, ErrorState, LoadingState } from '../src/components/StateView'; import { APPLICATION_STATUS_LABELS, COMPANY_APPLICATION_FILTERS, ENTERPRISE_DISPLAY_NAME, } from '../src/constants'; import { useCompanyApplications } from '../src/hooks'; import { applicationStatusColors, colors, spacing } from '../src/theme'; import type { ApplicationStatus, CompanyApplicationItem } from '../src/types'; /** 把 ISO 时间压成「9-28 10:12」这种一眼能排序的相对短格式 */ function formatAppliedAt(iso?: string): string { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; const mm = date.getMonth() + 1; const dd = date.getDate(); const hh = date.getHours().toString().padStart(2, '0'); const mi = date.getMinutes().toString().padStart(2, '0'); return `${mm}-${dd} ${hh}:${mi}`; } /* ===================== 子组件 ===================== */ function StatusBadge({ status }: { status: ApplicationStatus }) { const tone = applicationStatusColors[status]; return ( {APPLICATION_STATUS_LABELS[status]} ); } /** 单行投递。整行可点 → 进详情做状态流转与反馈 */ function ApplicationRow({ item, onPress, }: { item: CompanyApplicationItem; onPress: () => void; }) { return ( {item.candidateName.slice(0, 1)} {item.candidateName} {[item.candidateSchool, item.candidateEducation, item.candidateGraduationYear ? `${item.candidateGraduationYear} 届` : undefined] .filter(Boolean) .join(' · ') || '学历信息未公开'} {item.jobTitle} {formatAppliedAt(item.createdAt)} ); } /* ===================== 页面 ===================== */ export default function EnterpriseApplicationsScreen() { const router = useRouter(); const [status, setStatus] = useState(null); const [keyword, setKeyword] = useState(''); const { applications, total, counts, loading, error, refresh, isEmpty } = useCompanyApplications({ status, keyword }); /** * 从详情页返回时刷新:状态改了、反馈写了,列表上的数字与徽标都要跟着变。 * * refresh 的引用会随筛选条件(keyword)变化,直接写进 useCallback 依赖的话, * 每敲一个字都会重跑一次 focus effect。故用 ref 取最新值,effect 依赖留空。 */ const refreshRef = useRef(refresh); refreshRef.current = refresh; useFocusEffect( useCallback(() => { refreshRef.current(); }, []), ); const allCount = Object.values(counts).reduce((sum, n) => sum + n, 0); return ( {/* 顶部导航 */} router.back()} hitSlop={8}> {ENTERPRISE_DISPLAY_NAME} 收到的投递 {/* 关键词搜索 */} {keyword.length > 0 ? ( setKeyword('')} hitSlop={8} accessibilityLabel="清空搜索"> ) : null} {/* 状态筛选:计数来自独立计数口,不随当前筛选变化 */} {COMPANY_APPLICATION_FILTERS.map((filter) => { const active = filter.value === status; const count = filter.value === null ? allCount : counts[filter.value] ?? 0; return ( setStatus(filter.value)} > {filter.label} {count} ); })} {/* 列表区:四态齐全 */} {loading && applications.length === 0 ? ( ) : error ? ( ) : isEmpty ? ( { setKeyword(''); setStatus(null); } : undefined } /> ) : ( 共 {total} 条投递 {status ? ` · 已按「${APPLICATION_STATUS_LABELS[status]}」筛选` : ''} {applications.map((item) => ( router.push(`/application-detail?id=${item.applicationId}`)} /> ))} )} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg, }, /* ---- 顶部导航 ---- */ header: { height: 48, flexDirection: 'row', alignItems: 'center', }, headerBtn: { width: 48, height: 48, alignItems: 'center', justifyContent: 'center', }, headerCenter: { flex: 1, alignItems: 'center', }, headerCompany: { fontSize: 11, color: colors.textHint, }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary, }, /* ---- 搜索 ---- */ searchWrap: { flexDirection: 'row', alignItems: 'center', gap: 6, height: 36, marginHorizontal: spacing.framework, paddingHorizontal: spacing.card, borderRadius: spacing.card, backgroundColor: colors.white, borderWidth: 1, borderColor: colors.border, }, searchInput: { flex: 1, fontSize: 13, color: colors.textPrimary, padding: 0, }, /* ---- 筛选 ---- */ filterRow: { paddingHorizontal: spacing.framework, paddingVertical: spacing.card, gap: spacing.card, }, filterChip: { paddingHorizontal: 12, height: 30, borderRadius: 15, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.white, borderWidth: 1, borderColor: colors.border, }, filterChipActive: { backgroundColor: colors.primaryLight, borderColor: colors.primary, }, filterText: { fontSize: 12, color: colors.textSecondary, }, filterTextActive: { color: colors.primaryText, fontWeight: '600', }, /* ---- 列表 ---- */ list: { flex: 1, }, listContent: { paddingHorizontal: spacing.framework, paddingBottom: 24, }, summary: { marginBottom: spacing.card, fontSize: 12, color: colors.textHint, }, row: { flexDirection: 'row', alignItems: 'center', gap: spacing.card, backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card + 4, marginBottom: spacing.card, }, avatar: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.primaryLight, }, avatarText: { fontSize: 16, fontWeight: '700', color: colors.primaryText, }, rowMain: { flex: 1, }, rowTop: { flexDirection: 'row', alignItems: 'center', gap: spacing.card, }, name: { flexShrink: 1, fontSize: 15, fontWeight: '700', color: colors.textPrimary, }, badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: spacing.card, }, badgeText: { fontSize: 11, fontWeight: '600', }, rowMeta: { marginTop: 4, fontSize: 12, color: colors.textSecondary, }, rowBottom: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 6, }, jobChip: { flexDirection: 'row', alignItems: 'center', gap: 4, flexShrink: 1, }, jobChipText: { fontSize: 11, color: colors.textSecondary, }, time: { fontSize: 11, color: colors.textHint, }, });