import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import { useEffect, useState } from 'react'; import { ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Switch, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useOnboardingChecklist } from '../src/hooks'; import { colors, headerGradientShort } from '../src/theme'; import type { ChecklistGroup, OnboardingChecklist, OnboardingTip } from '../src/types'; /** 分组图标配色轮转(全部来自主题 token) */ const GROUP_ICON_THEME: Record = { material: { bg: colors.primaryLight, color: colors.primary }, procedure: { bg: colors.infoLight, color: colors.info }, study: { bg: colors.successLight, color: colors.success }, life: { bg: colors.actionLight, color: colors.action }, daily: { bg: colors.primaryLight, color: colors.primaryText }, }; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack, onShare }: { onBack: () => void; onShare: () => void }) { return ( 入职准备清单 ); } /* ===================== 子组件:倒计时卡片 ===================== */ function CountdownCard({ daysLeft, company, position, onboardingDate, completed, total, }: { daysLeft: number; company: string; position: string; onboardingDate: string; completed: number; total: number; }) { const percent = total > 0 ? Math.round((completed / total) * 100) : 0; return ( 入职倒计时 {daysLeft} 天 {company} · {position} 入职日期 {onboardingDate} 总体进度 {completed}/{total} ); } /* ===================== 子组件:清单分组 ===================== */ function ChecklistGroupCard({ group, onToggleItem, togglingId, expanded, onToggleExpand, }: { group: ChecklistGroup; onToggleItem: (itemId: string, done: boolean) => void; togglingId: string | null; expanded: boolean; onToggleExpand: () => void; }) { const theme = GROUP_ICON_THEME[group.id] ?? GROUP_ICON_THEME.material; const doneCount = group.items.filter((i) => i.done).length; return ( {group.title} {doneCount}/{group.items.length} {expanded && ( {group.items.map((item) => ( onToggleItem(item.id, !item.done)} disabled={togglingId === item.id} > {item.title} {item.required && !item.done && ( 必填 )} ))} )} ); } /* ===================== 子组件:每日提醒 ===================== */ function DailyReminderCard({ enabled, time, onToggle, }: { enabled: boolean; time: string; onToggle: (v: boolean) => void; }) { return ( 入职每日提醒 每天 {time} 提醒你完成待办 ); } /* ===================== 子组件:入职小贴士 ===================== */ function TipsCard({ tips }: { tips: OnboardingTip[] }) { return ( 入职小贴士 {tips.map((tip) => ( {tip.title} {tip.desc} ))} ); } /* ===================== 子组件:四态 ===================== */ function Loading() { return ( 正在加载清单… ); } function ErrorState({ onRetry }: { onRetry: () => void }) { return ( 清单加载失败 点击重试 ); } /* ===================== 页面 ===================== */ export default function OnboardingScreen() { const router = useRouter(); const { checklist, loading, error, refresh, toggle, togglingId } = useOnboardingChecklist(); /** 本地清单状态(勾选后即时更新,避免等待接口往返) */ const [localChecklist, setLocalChecklist] = useState(null); const [expandedIds, setExpandedIds] = useState([]); const [reminderEnabled, setReminderEnabled] = useState(false); useEffect(() => { if (checklist && !localChecklist) { setLocalChecklist(checklist); setExpandedIds(checklist.groups.filter((g) => g.expanded).map((g) => g.id)); setReminderEnabled(checklist.dailyReminder.enabled); } }, [checklist, localChecklist]); const display = localChecklist ?? checklist; const handleToggleItem = async (itemId: string, done: boolean) => { setLocalChecklist((prev) => { if (!prev) return prev; const groups = prev.groups.map((g) => ({ ...g, items: g.items.map((item) => (item.id === itemId ? { ...item, done } : item)), })); const completed = groups.reduce((sum, g) => sum + g.items.filter((i) => i.done).length, 0); return { ...prev, groups, completed }; }); await toggle(itemId, done); }; // 右上角「分享」暂无对应页面,按项目约定弹提示(此前绑定空函数,点击无任何反馈) const handleShare = () => { Alert.alert('提示', '分享入职清单功能开发中,敬请期待'); }; if (loading) { return (
router.back()} onShare={handleShare} /> ); } if (error || !display) { return (
router.back()} onShare={handleShare} /> ); } return (
router.back()} onShare={handleShare} /> {display.groups.map((group) => ( setExpandedIds((prev) => prev.includes(group.id) ? prev.filter((id) => id !== group.id) : [...prev, group.id], ) } /> ))} 添加自定义待办 导出清单 ); } 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: { paddingHorizontal: 16, paddingBottom: 100, paddingTop: 12 }, 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 }, countdownCard: { borderRadius: 16, padding: 18, marginBottom: 12 }, countdownTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, countdownLabel: { fontSize: 13, color: colors.textInverse, opacity: 0.9 }, countdownValueRow: { flexDirection: 'row', alignItems: 'baseline', marginTop: 6 }, countdownValue: { fontSize: 44, fontWeight: '800', color: colors.textInverse }, countdownUnit: { fontSize: 15, color: colors.textInverse, marginLeft: 4 }, countdownCompany: { fontSize: 14, color: colors.textInverse, marginTop: 8, fontWeight: '600' }, countdownDate: { fontSize: 12, color: colors.textInverse, opacity: 0.85, marginTop: 2 }, countdownIcon: { width: 60, height: 60, borderRadius: 30, backgroundColor: colors.white, alignItems: 'center', justifyContent: 'center', }, countdownProgress: { marginTop: 18 }, countdownProgressHead: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8, }, countdownProgressLabel: { fontSize: 13, color: colors.textInverse, opacity: 0.9 }, countdownProgressValue: { fontSize: 13, fontWeight: '700', color: colors.textInverse }, countdownTrack: { height: 8, borderRadius: 4, backgroundColor: 'rgba(255,255,255,0.35)', overflow: 'hidden', }, countdownFill: { height: 8, borderRadius: 4, backgroundColor: colors.white }, groupCard: { backgroundColor: colors.white, borderRadius: 12, marginBottom: 10, borderWidth: 1, borderColor: colors.borderLight, overflow: 'hidden', }, groupHead: { flexDirection: 'row', alignItems: 'center', padding: 14, }, groupIcon: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', }, groupTitleWrap: { flex: 1, marginLeft: 12 }, groupTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, groupCount: { fontSize: 12, color: colors.textHint, marginTop: 2 }, groupBody: { paddingHorizontal: 14, paddingBottom: 12 }, itemRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderTopWidth: 1, borderTopColor: colors.borderLight, }, itemTitle: { flex: 1, fontSize: 14, color: colors.textPrimary, marginLeft: 10 }, itemTitleDone: { textDecorationLine: 'line-through', color: colors.textHint }, requiredTag: { backgroundColor: colors.dangerLight, borderRadius: 8, paddingHorizontal: 6, paddingVertical: 2, }, requiredText: { fontSize: 10, color: colors.dangerText, fontWeight: '600' }, 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 }, reminderRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, reminderText: { flex: 1 }, reminderTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, reminderDesc: { fontSize: 12, color: colors.textHint, marginTop: 2 }, tipRow: { flexDirection: 'row', paddingVertical: 10, borderTopWidth: 1, borderTopColor: colors.borderLight, }, tipBody: { flex: 1, marginLeft: 10 }, tipTitle: { fontSize: 14, fontWeight: '600', color: colors.textPrimary }, tipDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2, lineHeight: 18 }, footer: { position: 'absolute', left: 0, right: 0, bottom: 0, flexDirection: 'row', paddingHorizontal: 16, paddingVertical: 12, backgroundColor: colors.white, borderTopWidth: 1, borderTopColor: colors.borderLight, }, addBtn: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: colors.white, borderRadius: 24, paddingVertical: 14, borderWidth: 1, borderColor: colors.border, marginRight: 12, }, addBtnText: { fontSize: 14, fontWeight: '700', color: colors.primary, marginLeft: 4 }, exportBtn: { flex: 1, backgroundColor: colors.primaryBtn, borderRadius: 24, paddingVertical: 14, alignItems: 'center', }, exportBtnText: { fontSize: 15, fontWeight: '700', color: colors.white }, });