655 lines
20 KiB
TypeScript
655 lines
20 KiB
TypeScript
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;
|