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, Modal, Pressable, ScrollView, StyleSheet, Switch, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { buildChecklistText } from '../src/api'; import { useOnboardingChecklist } from '../src/hooks'; import { shareText } from '../src/lib/shareFile'; 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, onRemoveItem, togglingId, expanded, onToggleExpand, }: { group: ChecklistGroup; onToggleItem: (itemId: string, done: boolean) => void; onRemoveItem: (itemId: string) => 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; // 只有「我的自定义待办」这一组可以删条目:模板待办删了下次进页面还会回来 const removable = group.id === 'custom'; 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 && ( 必填 )} {removable && ( onRemoveItem(item.id)} hitSlop={8}> )} ))} )} ); } /* ===================== 子组件:每日提醒 ===================== */ 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, addItem, removeItem, mutating } = useOnboardingChecklist(); /** 本地清单状态(勾选后即时更新,避免等待接口往返) */ const [localChecklist, setLocalChecklist] = useState(null); const [expandedIds, setExpandedIds] = useState([]); const [reminderEnabled, setReminderEnabled] = useState(false); /** 「添加自定义待办」弹窗 */ const [addVisible, setAddVisible] = useState(false); const [draftTitle, setDraftTitle] = useState(''); 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 handleAddItem = async () => { try { const updated = await addItem(draftTitle); setLocalChecklist(updated); setDraftTitle(''); setAddVisible(false); } catch (e) { Alert.alert('添加失败', e instanceof Error ? e.message : '请稍后重试'); } }; /** 删除自定义待办(模板条目接口层会拒绝) */ const handleRemoveItem = async (itemId: string) => { try { const updated = await removeItem(itemId); setLocalChecklist(updated); } catch (e) { Alert.alert('删除失败', e instanceof Error ? e.message : '请稍后重试'); } }; /** * 导出 / 分享清单:把清单拼成文本交给系统分享面板。 * `ok` 只代表面板已拉起,不能据此提示「分享成功」(见 lib/shareFile 的说明)。 */ const handleExport = async () => { if (!display) return; const outcome = await shareText(buildChecklistText(display), '入职准备清单'); if (!outcome.ok) Alert.alert('无法导出', outcome.message); }; if (loading) { return (
router.back()} onShare={() => void handleExport()} /> ); } if (error || !display) { return (
router.back()} onShare={() => void handleExport()} /> ); } return (
router.back()} onShare={() => void handleExport()} /> {display.groups.map((group) => ( setExpandedIds((prev) => prev.includes(group.id) ? prev.filter((id) => id !== group.id) : [...prev, group.id], ) } /> ))} setAddVisible(true)} disabled={mutating} > 添加自定义待办 void handleExport()}> 导出清单 {/* 添加自定义待办:Alert.prompt 只有 iOS 有,这里用自绘弹窗(与岗位评价回复一致) */} setAddVisible(false)}> 添加自定义待办 setAddVisible(false)}> 取消 void handleAddItem()} disabled={draftTitle.trim() === '' || mutating} > {mutating ? '添加中…' : '添加'} ); } 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: colors.onBrandTrack, 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 }, footerBtnDisabled: { opacity: 0.5 }, modalMask: { flex: 1, backgroundColor: colors.scrim, justifyContent: 'center', paddingHorizontal: 32, }, modalCard: { backgroundColor: colors.white, borderRadius: 16, padding: 20 }, modalTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary }, modalInput: { marginTop: 14, borderWidth: 1, borderColor: colors.border, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, color: colors.textPrimary, }, modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 12, marginTop: 18 }, modalCancel: { paddingHorizontal: 16, paddingVertical: 8 }, modalCancelText: { fontSize: 14, color: colors.textSecondary }, modalConfirm: { backgroundColor: colors.primaryBtn, borderRadius: 18, paddingHorizontal: 20, paddingVertical: 8, }, modalConfirmText: { fontSize: 14, fontWeight: '700', color: colors.white }, });