Files
frontend/src/screens/profile/AccountDeletionScreen.tsx

655 lines
20 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, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
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, 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',
},
keyboardView: {
flex: 1,
},
scrollContent: {
paddingVertical: spacing.lg,
},
content: {
maxWidth: CONTENT_MAX_WIDTH,
alignSelf: 'center',
width: '100%',
},
// 顶部叙述区:左对齐、有呼吸感
heroSection: {
paddingHorizontal: spacing['2xl'],
paddingTop: spacing.md,
paddingBottom: spacing.xl,
},
heroEyebrow: {
fontSize: 13,
color: colors.text.hint,
letterSpacing: 1,
marginBottom: spacing.sm,
},
heroTitle: {
fontSize: 24,
fontWeight: '700',
color: colors.text.primary,
lineHeight: 32,
marginBottom: spacing.sm,
},
heroDesc: {
fontSize: 15,
color: colors.text.secondary,
lineHeight: 22,
},
// 倒计时区:圆环 + 数字 + 文案,替代"大数字 + 标签"卡片
countdownSection: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing['2xl'],
paddingVertical: spacing.lg,
},
countdownRing: {
width: 88,
height: 88,
borderRadius: 44,
borderWidth: 4,
borderColor: colors.warning.main,
alignItems: 'center',
justifyContent: 'center',
marginRight: spacing.lg,
},
countdownNumber: {
fontSize: 32,
fontWeight: '700',
color: colors.warning.dark,
lineHeight: 36,
},
countdownUnit: {
fontSize: 11,
color: colors.text.secondary,
marginTop: -2,
},
countdownTextWrap: {
flex: 1,
},
countdownTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.text.primary,
marginBottom: 4,
},
countdownDesc: {
fontSize: 13,
color: colors.text.secondary,
lineHeight: 19,
},
// 引导文 + 列表(无背景、无圆角,自然排版)
guideSection: {
paddingHorizontal: spacing['2xl'],
paddingTop: spacing.lg,
},
guideLabel: {
fontSize: 13,
color: colors.text.secondary,
fontWeight: '500',
marginBottom: spacing.md,
},
guideItem: {
flexDirection: 'row',
alignItems: 'flex-start',
paddingVertical: 10,
},
guideIcon: {
marginRight: spacing.md,
marginTop: 2,
},
guideTextWrap: {
flex: 1,
},
guideTitle: {
fontSize: 15,
fontWeight: '500',
color: colors.text.primary,
marginBottom: 2,
},
guideDesc: {
fontSize: 13,
color: colors.text.secondary,
lineHeight: 19,
},
guideDivider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.divider,
marginLeft: spacing['2xl'] + 22 + spacing.md,
},
// 注意事项(无填色,仅左侧色条)
noticeSection: {
flexDirection: 'row',
alignItems: 'flex-start',
paddingHorizontal: spacing['2xl'],
paddingTop: spacing.lg,
paddingBottom: spacing.md,
},
noticeBar: {
width: 3,
alignSelf: 'stretch',
backgroundColor: colors.error.main,
borderRadius: 2,
marginRight: spacing.md,
},
noticeTextWrap: {
flex: 1,
},
noticeTitle: {
fontSize: 14,
fontWeight: '600',
color: colors.error.main,
marginBottom: 4,
},
noticeDesc: {
fontSize: 13,
color: colors.text.secondary,
lineHeight: 20,
},
// 表单区(与 AccountSecurity 风格一致:分节标题 + 输入框)
formSection: {
marginTop: spacing.lg,
},
sectionHeader: {
paddingHorizontal: spacing['2xl'],
marginBottom: spacing.sm,
marginTop: spacing.sm,
},
sectionTitle: {
fontWeight: '600',
fontSize: fontSizes.sm,
color: colors.text.secondary,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// 输入框
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: 14,
paddingHorizontal: spacing.lg,
height: 56,
marginHorizontal: spacing['2xl'],
marginBottom: spacing.md,
},
inputIcon: {
marginRight: spacing.sm,
},
input: {
flex: 1,
color: colors.text.primary,
fontSize: fontSizes.md,
height: 56,
},
eyeButton: {
padding: 4,
marginLeft: 4,
},
// 按钮行:次按钮在左、危险按钮在右,与全站保持一致
buttonRow: {
flexDirection: 'row',
gap: spacing.md,
marginTop: spacing.lg,
marginHorizontal: spacing['2xl'],
},
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.5,
},
primaryButton: {
height: 56,
borderRadius: 14,
backgroundColor: colors.primary.main,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: spacing['2xl'],
marginTop: spacing.md,
},
primaryButtonText: {
color: colors.text.inverse,
fontSize: fontSizes.md,
fontWeight: '600',
},
// 页脚:联系客服
footer: {
alignItems: 'center',
marginTop: spacing.xl,
paddingHorizontal: spacing['2xl'],
},
footerText: {
fontSize: 13,
color: colors.text.hint,
lineHeight: 20,
textAlign: 'center',
},
footerLink: {
color: colors.primary.main,
fontWeight: '500',
},
});
}
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((s) => s.logout);
const [status, setStatus] = useState<DeletionStatusDTO | null>(null);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
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>
);
}
// 待注销状态
if (status?.is_pending_deletion) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="注销账号" onBack={() => router.back()} />
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding },
]}
showsVerticalScrollIndicator={false}
>
<View style={styles.content}>
{/* 顶部叙述 */}
<View style={styles.heroSection}>
<Text style={styles.heroEyebrow}>ACCOUNT · </Text>
<Text style={styles.heroTitle}></Text>
<Text style={styles.heroDesc}>
使
</Text>
</View>
{/* 倒计时 */}
<View style={styles.countdownSection}>
<View style={styles.countdownRing}>
<Text style={styles.countdownNumber}>{status.cool_down_days || 90}</Text>
<Text style={styles.countdownUnit}></Text>
</View>
<View style={styles.countdownTextWrap}>
<Text style={styles.countdownTitle}></Text>
<Text style={styles.countdownDesc}>
</Text>
</View>
</View>
{/* 取消按钮 */}
<TouchableOpacity
style={[
styles.primaryButton,
submitting && styles.buttonDisabled,
]}
onPress={handleCancelDeletion}
disabled={submitting}
activeOpacity={0.9}
>
{submitting ? (
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.primaryButtonText}></Text>
)}
</TouchableOpacity>
{/* 页脚 */}
<View style={styles.footer}>
<Text style={styles.footerText}>
<Text style={styles.footerLink}> </Text>
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
// 正常状态:申请注销
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="注销账号" onBack={() => router.back()} />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding },
]}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={styles.content}>
{/* 顶部叙述 */}
<View style={styles.heroSection}>
<Text style={styles.heroEyebrow}>ACCOUNT · </Text>
<Text style={styles.heroTitle}></Text>
<Text style={styles.heroDesc}>
90
</Text>
</View>
{/* 注销影响 - 列表式(无背景) */}
<View style={styles.guideSection}>
<Text style={styles.guideLabel}></Text>
<View style={styles.guideItem}>
<MaterialCommunityIcons
name="account-remove-outline"
size={22}
color={colors.error.main}
style={styles.guideIcon}
/>
<View style={styles.guideTextWrap}>
<Text style={styles.guideTitle}></Text>
<Text style={styles.guideDesc}></Text>
</View>
</View>
<View style={styles.guideDivider} />
<View style={styles.guideItem}>
<MaterialCommunityIcons
name="message-text-outline"
size={22}
color={colors.warning.main}
style={styles.guideIcon}
/>
<View style={styles.guideTextWrap}>
<Text style={styles.guideTitle}></Text>
<Text style={styles.guideDesc}></Text>
</View>
</View>
<View style={styles.guideDivider} />
<View style={styles.guideItem}>
<MaterialCommunityIcons
name="heart-broken-outline"
size={22}
color={colors.error.main}
style={styles.guideIcon}
/>
<View style={styles.guideTextWrap}>
<Text style={styles.guideTitle}></Text>
<Text style={styles.guideDesc}></Text>
</View>
</View>
<View style={styles.guideDivider} />
<View style={styles.guideItem}>
<MaterialCommunityIcons
name="clock-time-four-outline"
size={22}
color={colors.text.secondary}
style={styles.guideIcon}
/>
<View style={styles.guideTextWrap}>
<Text style={styles.guideTitle}>90 </Text>
<Text style={styles.guideDesc}></Text>
</View>
</View>
</View>
{/* 红色提示 - 左侧细线代替大色块 */}
<View style={styles.noticeSection}>
<View style={styles.noticeBar} />
<View style={styles.noticeTextWrap}>
<Text style={styles.noticeTitle}></Text>
<Text style={styles.noticeDesc}>
90
</Text>
</View>
</View>
{/* 密码确认 */}
<View style={styles.formSection}>
<View style={styles.sectionHeader}>
<Text variant="caption" style={styles.sectionTitle}>
</Text>
</View>
<View style={styles.inputWrapper}>
<MaterialCommunityIcons
name="lock-outline"
size={20}
color={colors.text.secondary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="请输入登录密码以确认"
placeholderTextColor={colors.text.hint}
secureTextEntry={!showPassword}
value={password}
onChangeText={setPassword}
autoCapitalize="none"
returnKeyType="done"
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeButton}
>
<MaterialCommunityIcons
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
size={20}
color={colors.text.hint}
/>
</TouchableOpacity>
</View>
</View>
{/* 按钮行 */}
<View style={styles.buttonRow}>
<TouchableOpacity
style={styles.secondaryButton}
onPress={() => router.back()}
activeOpacity={0.8}
>
<Text style={styles.secondaryButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.dangerButton,
(submitting || !password.trim()) && styles.buttonDisabled,
]}
onPress={handleRequestDeletion}
disabled={submitting || !password.trim()}
activeOpacity={0.9}
>
{submitting ? (
<ActivityIndicator size="small" color={colors.text.inverse} />
) : (
<Text style={styles.dangerButtonText}></Text>
)}
</TouchableOpacity>
</View>
{/* 页脚 */}
<View style={styles.footer}>
<Text style={styles.footerText}>
<Text style={styles.footerLink}> </Text>
</Text>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
export default AccountDeletionScreen;