411 lines
13 KiB
TypeScript
411 lines
13 KiB
TypeScript
|
|
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 } 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;
|
|||
|
|
|
|||
|
|
// 胡萝卜橙主题色
|
|||
|
|
const THEME_COLORS = {
|
|||
|
|
primary: '#FF6B35',
|
|||
|
|
primaryLight: '#FF8C5A',
|
|||
|
|
primaryDark: '#E55A2B',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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: '#F5F5F7',
|
|||
|
|
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: THEME_COLORS.primary,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
},
|
|||
|
|
primaryButtonText: {
|
|||
|
|
color: '#fff',
|
|||
|
|
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: '#fff',
|
|||
|
|
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={['bottom']}>
|
|||
|
|
<View style={styles.loadingWrap}>
|
|||
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|||
|
|
</View>
|
|||
|
|
</SafeAreaView>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|||
|
|
<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="#fff" />
|
|||
|
|
) : (
|
|||
|
|
<Text style={styles.dangerButtonText}>确认注销</Text>
|
|||
|
|
)}
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
</View>
|
|||
|
|
</ScrollView>
|
|||
|
|
</SafeAreaView>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default AccountDeletionScreen;
|