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,
},
});