import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import { useMemo, useState } from 'react'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useJobProgress } from '../src/hooks'; import { colors, headerGradientShort } from '../src/theme'; import type { JobProgressCompany, JobProgressStage } from '../src/types'; const TABS: { key: 'all' | JobProgressStage; label: string }[] = [ { key: 'all', label: '全部' }, { key: '待反馈', label: '待反馈' }, { key: '面试中', label: '面试中' }, { key: 'Offer', label: 'Offer' }, { key: '已结束', label: '已结束' }, ]; const STAGE_THEME: Record = { 投递: { bg: colors.primaryLight, text: colors.primaryText }, 待反馈: { bg: colors.infoLight, text: colors.infoText }, 面试中: { bg: colors.actionLight, text: colors.actionText }, Offer: { bg: colors.successLight, text: colors.successText }, 已结束: { bg: colors.pageBg, text: colors.textSecondary }, }; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack, onFilter }: { onBack: () => void; onFilter: () => void }) { return ( 求职进度 ); } /* ===================== 子组件:统计卡片 ===================== */ function StatsCard({ stats }: { stats: import('../src/types').JobProgressStats }) { const items = [ { label: '投递', value: stats.total }, { label: '待反馈', value: stats.pending }, { label: '面试中', value: stats.interviewing }, { label: 'Offer', value: stats.offer }, { label: '已结束', value: stats.ended }, ]; return ( {items.map((it) => ( {it.value} {it.label} ))} ); } /* ===================== 子组件:Offer 即将到期提醒 ===================== */ function OfferAlert({ count }: { count: number }) { if (count <= 0) return null; return ( 你有 {count} 个 Offer 即将到期 ); } /* ===================== 子组件:筛选标签 ===================== */ function FilterTabs({ active, onChange, }: { active: 'all' | JobProgressStage; onChange: (k: 'all' | JobProgressStage) => void; }) { return ( {TABS.map((tab) => { const selected = active === tab.key; return ( onChange(tab.key)} > {tab.label} ); })} ); } /* ===================== 子组件:公司条目 ===================== */ function CompanyRow({ company }: { company: JobProgressCompany }) { const theme = STAGE_THEME[company.stage]; return ( {company.companyName.slice(0, 1)} {company.companyName} {company.positionName} {company.stageLabel} {company.salary} {company.nextAction} ); } /* ===================== 子组件:Offer 卡片 ===================== */ function OfferCard({ offer }: { offer: import('../src/types').JobProgressOffer }) { return ( {offer.companyName} Offer {offer.positionName} {offer.salary} {offer.deadline} · 剩余 {offer.daysLeft} 天 ); } /* ===================== 子组件:月度统计 ===================== */ function MonthlyStats({ stats }: { stats: import('../src/types').JobProgressMonthly[] }) { const max = Math.max(1, ...stats.map((s) => Math.max(s.deliveries, s.interviews, s.offers))); return ( 本月统计 月份 投递 面试 Offer {stats.slice(-3).map((s) => ( {s.month} ))} ); } function Bar({ value, max, color }: { value: number; max: number; color: string }) { return ( {value} ); } /* ===================== 子组件:四态 ===================== */ function Loading() { return ( 正在加载进度… ); } function ErrorState({ onRetry }: { onRetry: () => void }) { return ( 进度加载失败 点击重试 ); } /* ===================== 页面 ===================== */ export default function ProgressScreen() { const router = useRouter(); const { progress, loading, error, refresh } = useJobProgress(); const [activeTab, setActiveTab] = useState<'all' | JobProgressStage>('all'); const companies = useMemo(() => { if (!progress) return []; if (activeTab === 'all') return progress.companies; return progress.companies.filter((c) => c.stage === activeTab); }, [progress, activeTab]); if (loading) { return (
router.back()} onFilter={() => {}} /> ); } if (error || !progress) { return (
router.back()} onFilter={() => {}} /> ); } return (
router.back()} onFilter={() => {}} /> 求职中 {progress.stats.interviewing} {companies.map((company) => ( ))} {progress.offers.length > 0 && ( <> Offer {progress.offers.length} 个 {progress.offers.map((offer) => ( ))} )} ); } 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' }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, body: { padding: 16, paddingBottom: 32 }, statsCard: { borderRadius: 16, padding: 20, marginBottom: 12, }, statsRow: { flexDirection: 'row', justifyContent: 'space-between' }, statItem: { alignItems: 'center', flex: 1 }, statValue: { fontSize: 26, fontWeight: '800', color: colors.textInverse }, statLabel: { fontSize: 12, color: colors.textInverse, opacity: 0.9, marginTop: 4 }, alertCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.white, borderRadius: 12, padding: 14, marginBottom: 12, borderWidth: 1, borderColor: colors.actionBorder, }, alertText: { flex: 1, fontSize: 14, color: colors.actionText, marginLeft: 8 }, tabs: { flexDirection: 'row', gap: 8, marginBottom: 16, }, tab: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: colors.white, borderWidth: 1, borderColor: colors.border, }, tabActive: { backgroundColor: colors.primary, borderColor: colors.primary }, tabText: { fontSize: 13, color: colors.textSecondary }, tabTextActive: { color: colors.white, fontWeight: '600' }, sectionHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, marginTop: 8, }, sectionTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary }, companyCard: { backgroundColor: colors.white, borderRadius: 12, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: colors.borderLight, }, companyTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, companyLeft: { flexDirection: 'row', alignItems: 'center', flex: 1 }, logo: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.primaryLight, alignItems: 'center', justifyContent: 'center', }, logoText: { fontSize: 16, fontWeight: '700', color: colors.primary }, companyName: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, positionName: { fontSize: 13, color: colors.textSecondary, marginTop: 2 }, stageBadge: { borderRadius: 10, paddingHorizontal: 10, paddingVertical: 4 }, stageText: { fontSize: 12, fontWeight: '600' }, companyBottom: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 12, }, salary: { fontSize: 14, fontWeight: '700', color: colors.danger }, nextAction: { fontSize: 12, color: colors.textSecondary }, offerCard: { backgroundColor: colors.successLight, borderRadius: 12, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: colors.successBorder, }, offerHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, offerTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, offerTag: { backgroundColor: colors.success, borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2, }, offerTagText: { fontSize: 11, fontWeight: '700', color: colors.white }, offerPosition: { fontSize: 13, color: colors.textSecondary, marginTop: 4 }, offerSalary: { fontSize: 16, fontWeight: '800', color: colors.successText, marginTop: 8 }, offerFooter: { flexDirection: 'row', alignItems: 'center', marginTop: 8 }, offerDeadline: { fontSize: 12, color: colors.actionText, marginLeft: 4 }, card: { backgroundColor: colors.white, borderRadius: 12, padding: 16, marginTop: 8, borderWidth: 1, borderColor: colors.borderLight, }, cardTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 }, monthGrid: { gap: 10 }, monthHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 }, monthLabel: { width: 40, fontSize: 12, color: colors.textHint }, monthLegend: { flex: 1, fontSize: 11, color: colors.textHint, textAlign: 'center' }, monthRow: { flexDirection: 'row', alignItems: 'center' }, barCell: { flex: 1, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 4 }, barTrack: { flex: 1, height: 8, borderRadius: 4, backgroundColor: colors.borderLight, marginRight: 6, }, barFill: { height: 8, borderRadius: 4 }, barValue: { width: 20, fontSize: 11, color: colors.textSecondary, textAlign: 'right' }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 }, tip: { fontSize: 14, color: colors.textSecondary }, retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight }, retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText }, });