import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useCallback, useState } from 'react'; import { ActivityIndicator, Alert, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useResumeVersions } from '../src/hooks'; import { colors, spacing } from '../src/theme'; import type { ResumeVersion } from '../src/types'; import { formatBytes, formatDateTime } from '../src/utils'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack, onNew, }: { onBack: () => void; onNew: () => void; }) { return ( 简历版本 ); } /* ===================== 子组件:版本卡 ===================== */ function VersionCard({ version, onSetDefault, onExport, onDelete, }: { version: ResumeVersion; onSetDefault: () => void; onExport: () => void; onDelete: () => void; }) { return ( {version.name} {version.isDefault && ( 默认 )} v{version.versionNo} {version.fileName} · {formatBytes(version.fileSize)} 更新于 {formatDateTime(version.updatedAt)} {!version.isDefault && ( 设为默认 )} 导出 {!version.isDefault && ( 删除 )} ); } /* ===================== 子组件:新建版本弹窗 ===================== */ function NewVersionModal({ visible, onClose, onCreate, }: { visible: boolean; onClose: () => void; onCreate: (name: string) => void; }) { const [name, setName] = useState(''); const handleConfirm = () => { onCreate(name.trim()); setName(''); }; return ( 新建简历版本 {/* affordance-pending-backend:真实文件上传需后端 multipart 接口,当前以占位文件创建 */} 文件上传需后端支持,当前以占位文件创建版本 取消 创建 ); } /* ===================== 页面 ===================== */ export default function ResumeVersionsScreen() { const router = useRouter(); const { versions, loading, error, refresh, busy, create, setDefault, remove, exportAndPoll } = useResumeVersions(); const [newVisible, setNewVisible] = useState(false); const handleNew = useCallback( (name: string) => { setNewVisible(false); void create({ name: name || undefined, fileName: `${name || '简历'}版本.pdf`, fileSize: 200_000, }); }, [create], ); const handleSetDefault = useCallback( (id: string, name: string) => { Alert.alert('设为默认', `将「${name}」设为默认简历?`, [ { text: '取消', style: 'cancel' }, { text: '设为默认', onPress: () => void setDefault(id) }, ]); }, [setDefault], ); const handleDelete = useCallback( (id: string, name: string) => { Alert.alert('删除版本', `确定删除「${name}」吗?默认版本需先设其他版本为默认。`, [ { text: '取消', style: 'cancel' }, { text: '删除', style: 'destructive', onPress: () => void remove(id) }, ]); }, [remove], ); const handleExport = useCallback( async (id: string, name: string) => { const task = await exportAndPoll(id); if (!task) { Alert.alert('导出失败', '导出任务异常,请稍后重试'); return; } if (task.status === 'SUCCESS') { Alert.alert('导出完成', `「${name}」已导出${task.outputUrl ? `:${task.outputUrl}` : ''}`); } else if (task.status === 'FAILED') { Alert.alert('导出失败', task.error || '导出任务失败'); } else { Alert.alert('导出中', '任务仍在处理,请稍后到导出记录查看'); } }, [exportAndPoll], ); return (
router.back()} onNew={() => setNewVisible(true)} /> {loading ? ( ) : error ? ( 加载失败 重试 ) : versions.length === 0 ? ( 还没有简历版本 setNewVisible(true)}> 新建版本 ) : ( {versions.map((v) => ( handleSetDefault(v.id, v.name)} onExport={() => void handleExport(v.id, v.name)} onDelete={() => handleDelete(v.id, v.name)} /> ))} )} {busy && ( )} setNewVisible(false)} onCreate={handleNew} /> ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.card, height: 52, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, headerBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary, }, headerActionBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', }, scroll: { flex: 1, }, scrollContent: { padding: spacing.card, paddingBottom: 24, }, card: { backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card, marginBottom: spacing.card, }, cardDefault: { borderWidth: 1, borderColor: colors.primary, }, cardTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, cardTitleWrap: { flexDirection: 'row', alignItems: 'center', flex: 1, marginRight: 8, }, cardTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, }, defaultBadge: { marginLeft: 8, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, backgroundColor: colors.primaryLight, }, defaultBadgeText: { fontSize: 11, fontWeight: '700', color: colors.primary, }, cardVersion: { fontSize: 13, color: colors.textSecondary, }, cardMeta: { marginTop: 6, fontSize: 13, color: colors.textSecondary, }, cardTime: { marginTop: 2, fontSize: 12, color: colors.textHint, }, cardActions: { flexDirection: 'row', marginTop: 12, paddingTop: 12, borderTopWidth: 1, borderTopColor: colors.borderLight, }, actionBtn: { flexDirection: 'row', alignItems: 'center', marginRight: 20, }, actionText: { marginLeft: 4, fontSize: 13, color: colors.textSecondary, }, stateBox: { flex: 1, alignItems: 'center', justifyContent: 'center', }, stateText: { marginTop: 12, fontSize: 14, color: colors.textSecondary, }, retryBtn: { marginTop: 16, paddingHorizontal: 24, paddingVertical: spacing.card, borderRadius: 18, backgroundColor: colors.primaryBtn, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.white, }, busyMask: { ...StyleSheet.absoluteFill, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.scrimLight, }, modalMask: { flex: 1, backgroundColor: colors.scrim, justifyContent: 'flex-end', }, modalSheet: { backgroundColor: colors.white, borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: spacing.card, paddingBottom: 24, }, modalTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, marginBottom: 12, }, modalInput: { borderWidth: 1, borderColor: colors.borderLight, borderRadius: spacing.card, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, color: colors.textPrimary, }, modalHint: { marginTop: 8, fontSize: 12, color: colors.textHint, }, modalActions: { flexDirection: 'row', marginTop: 16, }, modalCancel: { flex: 1, alignItems: 'center', paddingVertical: 12, marginRight: 8, borderRadius: spacing.card, backgroundColor: colors.pageBg, }, modalCancelText: { fontSize: 14, color: colors.textSecondary, }, modalConfirm: { flex: 1, alignItems: 'center', paddingVertical: 12, marginLeft: 8, borderRadius: spacing.card, backgroundColor: colors.primaryBtn, }, modalConfirmText: { fontSize: 14, fontWeight: '700', color: colors.white, }, });