Files
frontend/src/screens/profile/AccountDeletionScreen.tsx
lan 4213d13b8f
Some checks failed
Frontend CI / ota-android (push) Successful in 1m30s
Frontend CI / build-and-push-web (push) Successful in 12m29s
Frontend CI / build-android-apk (push) Failing after 3h10m38s
refactor(ui): implement custom header system and unify screen layouts
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.
2026-05-03 22:01:43 +08:00

407 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;