Refactor the navigation and header strategy by replacing native stack headers with a custom `SimpleHeader` component across most profile and detail screens. This provides a more consistent UI/UX and better control over layout behavior. - Implement `SimpleHeader` component in `src/components/common`. - Disable native header rendering in `app/(app)/(tabs)/profile/_layout.tsx` and `app/_layout.tsx`. - Update profile sub-screens to use `SimpleHeader` and `StatusBar` from `expo-status-bar`. - Refactor `PostDetailScreen` to use a custom header implementation for better integration with the post author information. - Update `UserScreen` to wrap content in `SafeAreaView` with the new header. - Adjust `AppBackButton` to support transparent backgrounds.
407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
import { StatusBar } from 'expo-status-bar';
|
||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import {
|
||
ActivityIndicator,
|
||
Alert,
|
||
ScrollView,
|
||
StyleSheet,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
View,
|
||
} from 'react-native';
|
||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { useRouter } from 'expo-router';
|
||
import { authService } from '../../services';
|
||
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
|
||
import { Text, SimpleHeader } from '../../components/common';
|
||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||
import { useAuthStore } from '../../stores';
|
||
import type { DeletionStatusDTO } from '../../types/dto';
|
||
import * as hrefs from '../../navigation/hrefs';
|
||
|
||
const CONTENT_MAX_WIDTH = 720;
|
||
|
||
function createAccountDeletionStyles(colors: AppColors) {
|
||
return StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: colors.background.paper,
|
||
},
|
||
loadingWrap: {
|
||
flex: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
scrollContent: {
|
||
paddingVertical: spacing.lg,
|
||
},
|
||
content: {
|
||
maxWidth: CONTENT_MAX_WIDTH,
|
||
alignSelf: 'center',
|
||
width: '100%',
|
||
},
|
||
section: {
|
||
marginBottom: spacing['2xl'],
|
||
},
|
||
// 警告卡片 - 扁平化风格
|
||
warningCard: {
|
||
backgroundColor: colors.error.light + '20',
|
||
borderRadius: 14,
|
||
padding: spacing.lg,
|
||
marginHorizontal: spacing['2xl'],
|
||
marginBottom: spacing.xl,
|
||
},
|
||
warningTitle: {
|
||
fontWeight: '700',
|
||
fontSize: fontSizes.md,
|
||
marginBottom: spacing.sm,
|
||
color: colors.error.main,
|
||
},
|
||
warningText: {
|
||
color: colors.error.dark,
|
||
marginBottom: spacing.xs,
|
||
fontSize: fontSizes.sm,
|
||
},
|
||
// 状态卡片 - 扁平化风格
|
||
statusCard: {
|
||
backgroundColor: colors.warning.light + '30',
|
||
borderRadius: 14,
|
||
padding: spacing.lg,
|
||
marginHorizontal: spacing['2xl'],
|
||
marginBottom: spacing.xl,
|
||
},
|
||
statusTitle: {
|
||
fontWeight: '700',
|
||
fontSize: fontSizes.md,
|
||
marginBottom: spacing.sm,
|
||
color: colors.warning.dark,
|
||
},
|
||
daysText: {
|
||
fontSize: 32,
|
||
fontWeight: '700',
|
||
color: colors.warning.dark,
|
||
textAlign: 'center',
|
||
marginVertical: spacing.md,
|
||
},
|
||
// 内容区域
|
||
sectionContent: {
|
||
marginHorizontal: spacing['2xl'],
|
||
marginBottom: spacing.xl,
|
||
},
|
||
sectionTitle: {
|
||
fontWeight: '600',
|
||
fontSize: fontSizes.sm,
|
||
color: colors.text.secondary,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 0.5,
|
||
marginBottom: spacing.md,
|
||
},
|
||
listItem: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-start',
|
||
marginBottom: spacing.sm,
|
||
},
|
||
listBullet: {
|
||
marginRight: spacing.sm,
|
||
marginTop: 2,
|
||
},
|
||
// 输入框 - 扁平化风格
|
||
input: {
|
||
backgroundColor: colors.background.default,
|
||
borderRadius: 14,
|
||
padding: spacing.md,
|
||
fontSize: 16,
|
||
color: colors.text.primary,
|
||
height: 56,
|
||
},
|
||
// 按钮行
|
||
buttonRow: {
|
||
flexDirection: 'row',
|
||
gap: spacing.md,
|
||
marginTop: spacing.lg,
|
||
marginHorizontal: spacing['2xl'],
|
||
},
|
||
// 扁平化按钮
|
||
primaryButton: {
|
||
flex: 1,
|
||
height: 56,
|
||
borderRadius: 14,
|
||
backgroundColor: colors.primary.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
primaryButtonText: {
|
||
color: colors.text.inverse,
|
||
fontSize: fontSizes.md,
|
||
fontWeight: '600',
|
||
},
|
||
secondaryButton: {
|
||
flex: 1,
|
||
height: 56,
|
||
borderRadius: 14,
|
||
backgroundColor: 'transparent',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
borderWidth: 1.5,
|
||
borderColor: colors.divider,
|
||
},
|
||
secondaryButtonText: {
|
||
color: colors.text.primary,
|
||
fontSize: fontSizes.md,
|
||
fontWeight: '600',
|
||
},
|
||
dangerButton: {
|
||
flex: 1,
|
||
height: 56,
|
||
borderRadius: 14,
|
||
backgroundColor: colors.error.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
dangerButtonText: {
|
||
color: colors.text.inverse,
|
||
fontSize: fontSizes.md,
|
||
fontWeight: '600',
|
||
},
|
||
buttonDisabled: {
|
||
opacity: 0.6,
|
||
},
|
||
cancelButton: {
|
||
marginTop: spacing.lg,
|
||
marginHorizontal: spacing['2xl'],
|
||
},
|
||
});
|
||
}
|
||
|
||
export const AccountDeletionScreen: React.FC = () => {
|
||
const colors = useAppColors();
|
||
const styles = useMemo(() => createAccountDeletionStyles(colors), [colors]);
|
||
const router = useRouter();
|
||
const { isMobile } = useResponsive();
|
||
const insets = useSafeAreaInsets();
|
||
const { logout } = useAuthStore();
|
||
const [status, setStatus] = useState<DeletionStatusDTO | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [password, setPassword] = useState('');
|
||
|
||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
||
|
||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||
|
||
const loadStatus = useCallback(async () => {
|
||
try {
|
||
const result = await authService.getDeletionStatus();
|
||
setStatus(result);
|
||
} catch (error) {
|
||
console.error('加载注销状态失败:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadStatus();
|
||
}, [loadStatus]);
|
||
|
||
const handleRequestDeletion = useCallback(async () => {
|
||
if (!password.trim()) {
|
||
Alert.alert('提示', '请输入密码确认');
|
||
return;
|
||
}
|
||
|
||
Alert.alert(
|
||
'确认注销',
|
||
'注销后,您的账号将在90天后永久删除。在此期间,您可以通过重新登录来取消注销。确定要继续吗?',
|
||
[
|
||
{ text: '取消', style: 'cancel' },
|
||
{
|
||
text: '确定注销',
|
||
style: 'destructive',
|
||
onPress: async () => {
|
||
setSubmitting(true);
|
||
try {
|
||
const result = await authService.requestAccountDeletion(password);
|
||
if (result) {
|
||
setStatus(result);
|
||
setPassword('');
|
||
Alert.alert('已申请注销', '您的账号将在90天后永久删除。在此期间登录即可取消注销。');
|
||
await logout();
|
||
router.replace(hrefs.hrefAuthLogin());
|
||
} else {
|
||
Alert.alert('失败', '密码错误或申请失败,请重试');
|
||
}
|
||
} catch (error) {
|
||
console.error('申请注销失败:', error);
|
||
Alert.alert('失败', '申请注销失败,请稍后重试');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
},
|
||
},
|
||
]
|
||
);
|
||
}, [password, logout, router]);
|
||
|
||
const handleCancelDeletion = useCallback(async () => {
|
||
Alert.alert(
|
||
'取消注销',
|
||
'确定要取消注销申请吗?您的账号将恢复正常使用。',
|
||
[
|
||
{ text: '继续注销', style: 'cancel' },
|
||
{
|
||
text: '确定取消',
|
||
onPress: async () => {
|
||
setSubmitting(true);
|
||
try {
|
||
const ok = await authService.cancelAccountDeletion();
|
||
if (ok) {
|
||
setStatus({ is_pending_deletion: false });
|
||
Alert.alert('已取消', '注销申请已取消,您的账号已恢复正常使用');
|
||
} else {
|
||
Alert.alert('失败', '取消注销失败,请稍后重试');
|
||
}
|
||
} catch (error) {
|
||
console.error('取消注销失败:', error);
|
||
Alert.alert('失败', '取消注销失败,请稍后重试');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
},
|
||
},
|
||
]
|
||
);
|
||
}, []);
|
||
|
||
if (loading) {
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||
<View style={styles.loadingWrap}>
|
||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||
<StatusBar style="auto" />
|
||
<SimpleHeader title="注销账号" onBack={() => router.back()} />
|
||
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
|
||
<View style={styles.content}>
|
||
{status?.is_pending_deletion ? (
|
||
<View style={styles.section}>
|
||
<View style={styles.statusCard}>
|
||
<Text variant="body" style={styles.statusTitle}>
|
||
账号注销申请中
|
||
</Text>
|
||
<Text variant="body" color={colors.text.secondary}>
|
||
您的账号将在以下天数后永久删除:
|
||
</Text>
|
||
<Text variant="body" style={styles.daysText}>
|
||
{status.cool_down_days || 90} 天
|
||
</Text>
|
||
<Text variant="caption" color={colors.text.secondary}>
|
||
在此期间,您可以通过重新登录或点击下方按钮来取消注销申请。
|
||
</Text>
|
||
<TouchableOpacity
|
||
style={[styles.secondaryButton, styles.cancelButton, submitting && styles.buttonDisabled]}
|
||
onPress={handleCancelDeletion}
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? (
|
||
<ActivityIndicator size="small" color={colors.text.primary} />
|
||
) : (
|
||
<Text style={styles.secondaryButtonText}>取消注销申请</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
) : (
|
||
<View style={styles.section}>
|
||
{/* 警告卡片 */}
|
||
<View style={styles.warningCard}>
|
||
<Text variant="body" style={styles.warningTitle}>
|
||
警告:账号注销后将无法恢复
|
||
</Text>
|
||
<Text variant="body" style={styles.warningText}>
|
||
注销后,您的账号将在90天后永久删除。
|
||
</Text>
|
||
<Text variant="body" style={styles.warningText}>
|
||
在此期间,您可以通过重新登录来取消注销。
|
||
</Text>
|
||
</View>
|
||
|
||
{/* 注销说明 */}
|
||
<View style={styles.sectionContent}>
|
||
<Text style={styles.sectionTitle}>注销后将发生什么</Text>
|
||
<View style={styles.listItem}>
|
||
<MaterialCommunityIcons name="close-circle" size={16} color={colors.error.main} style={styles.listBullet} />
|
||
<Text variant="body" color={colors.text.secondary}>
|
||
您的个人资料将被删除
|
||
</Text>
|
||
</View>
|
||
<View style={styles.listItem}>
|
||
<MaterialCommunityIcons name="information" size={16} color={colors.warning.main} style={styles.listBullet} />
|
||
<Text variant="body" color={colors.text.secondary}>
|
||
您发布的帖子、评论将保留,但显示为「已注销用户」
|
||
</Text>
|
||
</View>
|
||
<View style={styles.listItem}>
|
||
<MaterialCommunityIcons name="close-circle" size={16} color={colors.error.main} style={styles.listBullet} />
|
||
<Text variant="body" color={colors.text.secondary}>
|
||
您的关注、粉丝关系将被清除
|
||
</Text>
|
||
</View>
|
||
<View style={styles.listItem}>
|
||
<MaterialCommunityIcons name="close-circle" size={16} color={colors.error.main} style={styles.listBullet} />
|
||
<Text variant="body" color={colors.text.secondary}>
|
||
您的收藏、点赞记录将被删除
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 密码确认 */}
|
||
<View style={styles.sectionContent}>
|
||
<Text style={styles.sectionTitle}>请输入密码确认</Text>
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="请输入密码"
|
||
placeholderTextColor={colors.text.hint}
|
||
secureTextEntry
|
||
value={password}
|
||
onChangeText={setPassword}
|
||
/>
|
||
</View>
|
||
|
||
{/* 按钮行 */}
|
||
<View style={styles.buttonRow}>
|
||
<TouchableOpacity
|
||
style={styles.secondaryButton}
|
||
onPress={() => router.back()}
|
||
>
|
||
<Text style={styles.secondaryButtonText}>返回</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={[styles.dangerButton, submitting && styles.buttonDisabled]}
|
||
onPress={handleRequestDeletion}
|
||
disabled={submitting || !password.trim()}
|
||
>
|
||
{submitting ? (
|
||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||
) : (
|
||
<Text style={styles.dangerButtonText}>确认注销</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
);
|
||
};
|
||
|
||
export default AccountDeletionScreen;
|