1486 lines
74 KiB
JavaScript
1486 lines
74 KiB
JavaScript
// src/views/Settings/SettingsPage.js
|
||
import React, { useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import {
|
||
Box,
|
||
Container,
|
||
VStack,
|
||
HStack,
|
||
Text,
|
||
Heading,
|
||
Button,
|
||
Input,
|
||
FormControl,
|
||
FormLabel,
|
||
Switch,
|
||
Card,
|
||
CardBody,
|
||
CardHeader,
|
||
Divider,
|
||
useToast,
|
||
Alert,
|
||
AlertIcon,
|
||
AlertTitle,
|
||
AlertDescription,
|
||
Modal,
|
||
ModalOverlay,
|
||
ModalContent,
|
||
ModalHeader,
|
||
ModalBody,
|
||
ModalFooter,
|
||
ModalCloseButton,
|
||
useDisclosure,
|
||
Badge,
|
||
IconButton,
|
||
Textarea,
|
||
Select,
|
||
useColorMode,
|
||
Tabs,
|
||
TabList,
|
||
TabPanels,
|
||
Tab,
|
||
TabPanel,
|
||
InputGroup,
|
||
InputRightElement,
|
||
PinInput,
|
||
PinInputField,
|
||
SimpleGrid,
|
||
Checkbox,
|
||
UnorderedList,
|
||
ListItem,
|
||
OrderedList,
|
||
Spacer
|
||
} from '@chakra-ui/react';
|
||
import {
|
||
EditIcon,
|
||
ViewIcon,
|
||
ViewOffIcon,
|
||
LinkIcon,
|
||
DeleteIcon, // 替换 UnlinkIcon
|
||
WarningIcon,
|
||
CheckIcon,
|
||
CloseIcon
|
||
} from '@chakra-ui/icons';
|
||
import { FaWeixin, FaMobile, FaEnvelope } from 'react-icons/fa';
|
||
import { useAuth } from '../../contexts/AuthContext';
|
||
|
||
export default function SettingsPage() {
|
||
const { user, updateUser, logout } = useAuth();
|
||
const { colorMode, toggleColorMode } = useColorMode();
|
||
const toast = useToast();
|
||
const navigate = useNavigate();
|
||
|
||
// 模态框状态
|
||
const { isOpen: isPasswordOpen, onOpen: onPasswordOpen, onClose: onPasswordClose } = useDisclosure();
|
||
const { isOpen: isPhoneOpen, onOpen: onPhoneOpen, onClose: onPhoneClose } = useDisclosure();
|
||
const { isOpen: isEmailOpen, onOpen: onEmailOpen, onClose: onEmailClose } = useDisclosure();
|
||
const { isOpen: isNoticeOpen, onOpen: onNoticeOpen, onClose: onNoticeClose } = useDisclosure();
|
||
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
|
||
|
||
// 表单状态
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [passwordForm, setPasswordForm] = useState({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
});
|
||
|
||
// 密码状态 - 默认假设是普通用户,获取到数据后再更新
|
||
const [passwordStatus, setPasswordStatus] = useState({
|
||
isWechatUser: false,
|
||
hasPassword: true,
|
||
needsFirstTimeSetup: false
|
||
});
|
||
const [passwordStatusLoading, setPasswordStatusLoading] = useState(true);
|
||
const [phoneForm, setPhoneForm] = useState({
|
||
phone: '',
|
||
verificationCode: ''
|
||
});
|
||
const [emailForm, setEmailForm] = useState({
|
||
email: '',
|
||
verificationCode: ''
|
||
});
|
||
|
||
// 注销相关状态
|
||
const [hasAgreedToNotice, setHasAgreedToNotice] = useState(false);
|
||
const [deleteConfirmText, setDeleteConfirmText] = useState('');
|
||
|
||
const [blockedKeywords, setBlockedKeywords] = useState(user?.blocked_keywords || '');
|
||
|
||
// 通知设置状态
|
||
const [notifications, setNotifications] = useState({
|
||
email_notifications: user?.email_notifications ?? true,
|
||
sms_notifications: user?.sms_notifications ?? false,
|
||
wechat_notifications: user?.wechat_notifications ?? false,
|
||
system_updates: true,
|
||
investment_alerts: true,
|
||
community_activities: true,
|
||
marketing_emails: false
|
||
});
|
||
|
||
// 隐私设置状态
|
||
const [privacy, setPrivacy] = useState({
|
||
privacy_level: user?.privacy_level || 'public',
|
||
show_investment_data: true,
|
||
show_trading_history: false,
|
||
allow_friend_requests: true
|
||
});
|
||
|
||
// 获取密码状态
|
||
const fetchPasswordStatus = async () => {
|
||
try {
|
||
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||
? ""
|
||
: process.env.REACT_APP_API_URL || "http://49.232.185.254:5000";
|
||
|
||
const response = await fetch(`${API_BASE_URL}/api/account/password-status`, {
|
||
method: 'GET',
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
console.log('密码状态数据:', data.data); // 调试信息
|
||
setPasswordStatus(data.data);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取密码状态失败:', error);
|
||
} finally {
|
||
setPasswordStatusLoading(false);
|
||
}
|
||
};
|
||
|
||
// 组件加载时获取密码状态
|
||
React.useEffect(() => {
|
||
fetchPasswordStatus();
|
||
}, []);
|
||
|
||
// 修改密码
|
||
const handlePasswordChange = async () => {
|
||
const isFirstTimeSetup = passwordStatus.needsFirstTimeSetup;
|
||
|
||
// 如果不是首次设置且未提供当前密码
|
||
if (!isFirstTimeSetup && !passwordForm.currentPassword) {
|
||
toast({
|
||
title: "请输入当前密码",
|
||
description: "修改密码需要验证当前密码",
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||
toast({
|
||
title: "密码不匹配",
|
||
description: "新密码与确认密码不一致",
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (passwordForm.newPassword.length < 6) {
|
||
toast({
|
||
title: "密码太短",
|
||
description: "密码至少需要6个字符",
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
// 调用后端API修改密码
|
||
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||
? ""
|
||
: process.env.REACT_APP_API_URL || "http://49.232.185.254:5000";
|
||
|
||
const response = await fetch(`${API_BASE_URL}/api/account/change-password`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include', // 包含认证信息
|
||
body: JSON.stringify({
|
||
currentPassword: passwordForm.currentPassword,
|
||
newPassword: passwordForm.newPassword,
|
||
isFirstSet: passwordStatus.needsFirstTimeSetup
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.success) {
|
||
const isFirstSet = passwordStatus.needsFirstTimeSetup;
|
||
|
||
toast({
|
||
title: isFirstSet ? "密码设置成功" : "密码修改成功",
|
||
description: isFirstSet ? "您现在可以使用手机号+密码登录了" : "请重新登录",
|
||
status: "success",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
|
||
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||
onPasswordClose();
|
||
|
||
// 刷新密码状态
|
||
fetchPasswordStatus();
|
||
|
||
// 如果是修改密码(非首次设置),需要重新登录
|
||
if (!isFirstSet) {
|
||
setTimeout(() => {
|
||
logout();
|
||
}, 1000);
|
||
}
|
||
} else {
|
||
throw new Error(data.error || '密码修改失败');
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
title: "修改失败",
|
||
description: error.message,
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 发送验证码
|
||
const sendVerificationCode = async (type) => {
|
||
setIsLoading(true);
|
||
try {
|
||
if (type === 'phone') {
|
||
const res = await fetch((process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001') + '/api/account/phone/send-code', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ phone: phoneForm.phone })
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || '发送失败');
|
||
} else {
|
||
// 使用绑定邮箱的验证码API
|
||
const res = await fetch((process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001') + '/api/account/email/send-bind-code', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ email: emailForm.email })
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || '发送失败');
|
||
}
|
||
|
||
toast({
|
||
title: "验证码已发送",
|
||
description: `请查收${type === 'phone' ? '短信' : '邮件'}`,
|
||
status: "success",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} catch (error) {
|
||
toast({
|
||
title: "发送失败",
|
||
description: error.message,
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 绑定手机号
|
||
const handlePhoneBind = async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
const res = await fetch((process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001') + '/api/account/phone/bind', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ phone: phoneForm.phone, code: phoneForm.verificationCode })
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || '绑定失败');
|
||
|
||
updateUser({
|
||
phone: phoneForm.phone,
|
||
phone_confirmed: true
|
||
});
|
||
|
||
toast({
|
||
title: "手机号绑定成功",
|
||
status: "success",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
|
||
setPhoneForm({ phone: '', verificationCode: '' });
|
||
onPhoneClose();
|
||
} catch (error) {
|
||
toast({
|
||
title: "绑定失败",
|
||
description: error.message,
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 更换邮箱
|
||
const handleEmailBind = async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
// 调用真实的邮箱绑定API
|
||
const res = await fetch((process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001') + '/api/account/email/bind', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
email: emailForm.email,
|
||
code: emailForm.verificationCode
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (!res.ok) {
|
||
throw new Error(data.error || '绑定失败');
|
||
}
|
||
|
||
// 更新用户信息
|
||
updateUser({
|
||
email: data.user.email,
|
||
email_confirmed: data.user.email_confirmed
|
||
});
|
||
|
||
toast({
|
||
title: "邮箱绑定成功",
|
||
status: "success",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
|
||
setEmailForm({ email: '', verificationCode: '' });
|
||
onEmailClose();
|
||
} catch (error) {
|
||
toast({
|
||
title: "绑定失败",
|
||
description: error.message,
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 保存通知设置
|
||
const saveNotificationSettings = async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
// 这里应该调用后端API保存设置
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
updateUser(notifications);
|
||
|
||
toast({
|
||
title: "通知设置已保存",
|
||
status: "success",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} catch (error) {
|
||
toast({
|
||
title: "保存失败",
|
||
description: error.message,
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 保存隐私设置
|
||
const savePrivacySettings = async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
// 这里应该调用后端API保存设置
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
updateUser({
|
||
privacy_level: privacy.privacy_level,
|
||
blocked_keywords: blockedKeywords
|
||
});
|
||
|
||
toast({
|
||
title: "隐私设置已保存",
|
||
status: "success",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} catch (error) {
|
||
toast({
|
||
title: "保存失败",
|
||
description: error.message,
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 打开注销须知弹窗
|
||
const handleOpenCancellationNotice = () => {
|
||
setHasAgreedToNotice(false);
|
||
onNoticeOpen();
|
||
};
|
||
|
||
// 从须知弹窗进入确认注销流程
|
||
const handleProceedToDelete = () => {
|
||
if (!hasAgreedToNotice) {
|
||
toast({
|
||
title: "请先阅读并同意注销须知",
|
||
status: "warning",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
return;
|
||
}
|
||
onNoticeClose();
|
||
setDeleteConfirmText('');
|
||
onDeleteOpen();
|
||
};
|
||
|
||
// 注销账户
|
||
const handleDeleteAccount = async () => {
|
||
if (deleteConfirmText !== '确认注销') {
|
||
toast({
|
||
title: "请输入正确的确认文本",
|
||
description: "请输入'确认注销'来确认操作",
|
||
status: "warning",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
// 这里应该调用后端API注销账户
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
||
toast({
|
||
title: "账户已注销",
|
||
description: "感谢您的使用",
|
||
status: "info",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
|
||
logout();
|
||
} catch (error) {
|
||
toast({
|
||
title: "注销失败",
|
||
description: error.message,
|
||
status: "error",
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Container maxW="container.xl" py={8}>
|
||
<VStack spacing={8} align="stretch">
|
||
{/* 页面标题 */}
|
||
<Heading size="lg" color="gray.800">账户设置</Heading>
|
||
|
||
<Tabs variant="enclosed" colorScheme="blue">
|
||
<TabList>
|
||
<Tab>账户安全</Tab>
|
||
<Tab>通知设置</Tab>
|
||
<Tab>隐私设置</Tab>
|
||
<Tab>界面设置</Tab>
|
||
<Tab>危险操作</Tab>
|
||
</TabList>
|
||
|
||
<TabPanels>
|
||
{/* 账户安全 */}
|
||
<TabPanel>
|
||
<VStack spacing={6} align="stretch">
|
||
{/* 密码设置 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">密码设置</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">
|
||
{passwordStatus.needsFirstTimeSetup ? '设置登录密码' : '登录密码'}
|
||
</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
{passwordStatus.needsFirstTimeSetup
|
||
? '您通过微信登录,建议设置密码以便其他方式登录'
|
||
: '定期更换密码,保护账户安全'
|
||
}
|
||
</Text>
|
||
{passwordStatus.isWechatUser && (
|
||
<Text fontSize="xs" color="blue.500">
|
||
微信用户
|
||
</Text>
|
||
)}
|
||
</VStack>
|
||
<Button
|
||
leftIcon={<EditIcon />}
|
||
onClick={onPasswordOpen}
|
||
isLoading={passwordStatusLoading}
|
||
>
|
||
{passwordStatus.needsFirstTimeSetup ? '设置密码' : '修改密码'}
|
||
</Button>
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 手机号绑定 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">手机号绑定</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<HStack>
|
||
<FaMobile />
|
||
<Text fontWeight="medium">
|
||
{user?.phone || '未绑定手机号'}
|
||
</Text>
|
||
{user?.phone_confirmed && (
|
||
<Badge colorScheme="green" size="sm">已验证</Badge>
|
||
)}
|
||
</HStack>
|
||
<Text fontSize="sm" color="gray.600">
|
||
绑定手机号可用于登录和接收重要通知
|
||
</Text>
|
||
</VStack>
|
||
{user?.phone ? (
|
||
<Button
|
||
leftIcon={<DeleteIcon />}
|
||
colorScheme="red"
|
||
variant="outline"
|
||
onClick={async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
const res = await fetch((process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001') + '/api/account/phone/unbind', {
|
||
method: 'POST',
|
||
credentials: 'include'
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || '解绑失败');
|
||
updateUser({ phone: null, phone_confirmed: false });
|
||
toast({ title: '手机号解绑成功', status: 'success', duration: 3000, isClosable: true });
|
||
} catch (e) {
|
||
toast({ title: '解绑失败', description: e.message, status: 'error', duration: 3000, isClosable: true });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}}
|
||
isLoading={isLoading}
|
||
>
|
||
解绑
|
||
</Button>
|
||
) : (
|
||
<Button leftIcon={<LinkIcon />} onClick={onPhoneOpen}>
|
||
绑定手机号
|
||
</Button>
|
||
)}
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 邮箱绑定 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">邮箱设置</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<HStack>
|
||
<FaEnvelope />
|
||
<Text fontWeight="medium">{user?.email}</Text>
|
||
{user?.email_confirmed && (
|
||
<Badge colorScheme="green" size="sm">已验证</Badge>
|
||
)}
|
||
</HStack>
|
||
<Text fontSize="sm" color="gray.600">
|
||
邮箱用于登录和接收重要通知
|
||
</Text>
|
||
</VStack>
|
||
<Button leftIcon={<EditIcon />} onClick={onEmailOpen}>
|
||
更换邮箱
|
||
</Button>
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 微信绑定 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">微信绑定</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<HStack>
|
||
<FaWeixin color="#1aad19" />
|
||
<Text fontWeight="medium">
|
||
{user?.has_wechat ? '已绑定微信' : '未绑定微信'}
|
||
</Text>
|
||
{user?.has_wechat && (
|
||
<Badge colorScheme="green" size="sm">已绑定</Badge>
|
||
)}
|
||
</HStack>
|
||
<Text fontSize="sm" color="gray.600">
|
||
绑定微信可使用微信一键登录
|
||
</Text>
|
||
</VStack>
|
||
{user?.has_wechat ? (
|
||
<Button
|
||
leftIcon={<DeleteIcon />}
|
||
colorScheme="red"
|
||
variant="outline"
|
||
onClick={async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
const res = await fetch((process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001') + '/api/account/wechat/unbind', {
|
||
method: 'POST',
|
||
credentials: 'include'
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || '解绑失败');
|
||
updateUser({ has_wechat: false });
|
||
toast({ title: '解绑成功', status: 'success', duration: 3000, isClosable: true });
|
||
} catch (e) {
|
||
toast({ title: '解绑失败', description: e.message, status: 'error', duration: 3000, isClosable: true });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}}
|
||
>
|
||
解绑微信
|
||
</Button>
|
||
) : (
|
||
<Button leftIcon={<LinkIcon />} colorScheme="green" onClick={async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||
const res = await fetch(base + '/api/account/wechat/qrcode', { method: 'GET', credentials: 'include' });
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || '获取二维码失败');
|
||
// 打开新的窗口进行扫码
|
||
window.open(data.auth_url, '_blank');
|
||
// 轮询绑定状态
|
||
const sessionId = data.session_id;
|
||
const start = Date.now();
|
||
const poll = async () => {
|
||
if (Date.now() - start > 300000) return; // 5分钟
|
||
const r = await fetch(base + '/api/account/wechat/check', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ session_id: sessionId })
|
||
});
|
||
const j = await r.json();
|
||
if (j.status === 'bind_ready') {
|
||
updateUser({ has_wechat: true });
|
||
toast({ title: '微信绑定成功', status: 'success', duration: 3000, isClosable: true });
|
||
} else if (j.status === 'bind_conflict' || j.status === 'bind_failed' || j.status === 'expired') {
|
||
toast({ title: '绑定未完成', description: j.status, status: 'error', duration: 3000, isClosable: true });
|
||
} else {
|
||
setTimeout(poll, 2000);
|
||
}
|
||
};
|
||
setTimeout(poll, 1500);
|
||
} catch (e) {
|
||
toast({ title: '获取二维码失败', description: e.message, status: 'error', duration: 3000, isClosable: true });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}}>
|
||
绑定微信
|
||
</Button>
|
||
)}
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 两步验证 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">两步验证</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">安全验证</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
开启两步验证,提高账户安全性
|
||
</Text>
|
||
</VStack>
|
||
<Switch size="lg" />
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
{/* 通知设置 */}
|
||
<TabPanel>
|
||
<VStack spacing={6} align="stretch">
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">通知方式</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<VStack spacing={4} align="stretch">
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">邮件通知</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
接收邮件通知
|
||
</Text>
|
||
</VStack>
|
||
<Switch
|
||
isChecked={notifications.email_notifications}
|
||
onChange={(e) => setNotifications(prev => ({
|
||
...prev,
|
||
email_notifications: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">短信通知</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
接收短信通知(需绑定手机号)
|
||
</Text>
|
||
</VStack>
|
||
<Switch
|
||
isChecked={notifications.sms_notifications}
|
||
isDisabled={!user?.phone}
|
||
onChange={(e) => setNotifications(prev => ({
|
||
...prev,
|
||
sms_notifications: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">微信通知</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
接收微信通知(需绑定微信)
|
||
</Text>
|
||
</VStack>
|
||
<Switch
|
||
isChecked={notifications.wechat_notifications}
|
||
isDisabled={!user?.has_wechat}
|
||
onChange={(e) => setNotifications(prev => ({
|
||
...prev,
|
||
wechat_notifications: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">通知类型</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<VStack spacing={4} align="stretch">
|
||
<HStack justify="space-between">
|
||
<Text>系统更新通知</Text>
|
||
<Switch
|
||
isChecked={notifications.system_updates}
|
||
onChange={(e) => setNotifications(prev => ({
|
||
...prev,
|
||
system_updates: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<Text>投资提醒</Text>
|
||
<Switch
|
||
isChecked={notifications.investment_alerts}
|
||
onChange={(e) => setNotifications(prev => ({
|
||
...prev,
|
||
investment_alerts: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<Text>社区动态</Text>
|
||
<Switch
|
||
isChecked={notifications.community_activities}
|
||
onChange={(e) => setNotifications(prev => ({
|
||
...prev,
|
||
community_activities: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<Text>营销邮件</Text>
|
||
<Switch
|
||
isChecked={notifications.marketing_emails}
|
||
onChange={(e) => setNotifications(prev => ({
|
||
...prev,
|
||
marketing_emails: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
</VStack>
|
||
|
||
<Divider my={4} />
|
||
|
||
<Button
|
||
colorScheme="blue"
|
||
onClick={saveNotificationSettings}
|
||
isLoading={isLoading}
|
||
>
|
||
保存通知设置
|
||
</Button>
|
||
</CardBody>
|
||
</Card>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
{/* 隐私设置 */}
|
||
<TabPanel>
|
||
<VStack spacing={6} align="stretch">
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">隐私级别</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<VStack spacing={4} align="stretch">
|
||
<FormControl>
|
||
<FormLabel>个人资料可见性</FormLabel>
|
||
<Select
|
||
value={privacy.privacy_level}
|
||
onChange={(e) => setPrivacy(prev => ({
|
||
...prev,
|
||
privacy_level: e.target.value
|
||
}))}
|
||
>
|
||
<option value="public">公开</option>
|
||
<option value="friends">仅好友可见</option>
|
||
<option value="private">私密</option>
|
||
</Select>
|
||
</FormControl>
|
||
|
||
<Divider />
|
||
|
||
<VStack spacing={3} align="stretch">
|
||
<HStack justify="space-between">
|
||
<Text>显示投资数据</Text>
|
||
<Switch
|
||
isChecked={privacy.show_investment_data}
|
||
onChange={(e) => setPrivacy(prev => ({
|
||
...prev,
|
||
show_investment_data: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<Text>显示交易历史</Text>
|
||
<Switch
|
||
isChecked={privacy.show_trading_history}
|
||
onChange={(e) => setPrivacy(prev => ({
|
||
...prev,
|
||
show_trading_history: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<Text>允许好友请求</Text>
|
||
<Switch
|
||
isChecked={privacy.allow_friend_requests}
|
||
onChange={(e) => setPrivacy(prev => ({
|
||
...prev,
|
||
allow_friend_requests: e.target.checked
|
||
}))}
|
||
/>
|
||
</HStack>
|
||
</VStack>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">屏蔽设置</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<FormControl>
|
||
<FormLabel>屏蔽关键词</FormLabel>
|
||
<Textarea
|
||
value={blockedKeywords}
|
||
onChange={(e) => setBlockedKeywords(e.target.value)}
|
||
placeholder="输入要屏蔽的关键词,用逗号分隔"
|
||
rows={4}
|
||
/>
|
||
<Text fontSize="sm" color="gray.500" mt={2}>
|
||
包含这些关键词的内容将不会显示给您
|
||
</Text>
|
||
</FormControl>
|
||
|
||
<Divider my={4} />
|
||
|
||
<Button
|
||
colorScheme="blue"
|
||
onClick={savePrivacySettings}
|
||
isLoading={isLoading}
|
||
>
|
||
保存隐私设置
|
||
</Button>
|
||
</CardBody>
|
||
</Card>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
{/* 界面设置 */}
|
||
<TabPanel>
|
||
<VStack spacing={6} align="stretch">
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">外观设置</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<VStack spacing={4} align="stretch">
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">深色模式</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
切换到深色主题
|
||
</Text>
|
||
</VStack>
|
||
<Switch
|
||
isChecked={colorMode === 'dark'}
|
||
onChange={toggleColorMode}
|
||
/>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">语言</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
选择界面语言
|
||
</Text>
|
||
</VStack>
|
||
<Select maxW="200px" defaultValue="zh-CN">
|
||
<option value="zh-CN">简体中文</option>
|
||
<option value="zh-TW">繁体中文</option>
|
||
<option value="en-US">English</option>
|
||
</Select>
|
||
</HStack>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md">数据管理</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<VStack spacing={4} align="stretch">
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">数据导出</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
导出您的个人数据
|
||
</Text>
|
||
</VStack>
|
||
<Button variant="outline">
|
||
导出数据
|
||
</Button>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontWeight="medium">清除缓存</Text>
|
||
<Text fontSize="sm" color="gray.600">
|
||
清除本地缓存数据
|
||
</Text>
|
||
</VStack>
|
||
<Button variant="outline">
|
||
清除缓存
|
||
</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
{/* 危险操作 */}
|
||
<TabPanel>
|
||
<VStack spacing={6} align="stretch">
|
||
<Alert status="warning" borderRadius="md">
|
||
<AlertIcon />
|
||
<AlertTitle>危险操作区域</AlertTitle>
|
||
<AlertDescription>
|
||
以下操作不可逆,请谨慎操作
|
||
</AlertDescription>
|
||
</Alert>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<Heading size="md" color="red.600">注销账户</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<VStack spacing={4} align="stretch">
|
||
<Text color="gray.600">
|
||
注销账户将永久删除您的所有数据,包括:
|
||
</Text>
|
||
<Box pl={4}>
|
||
<Text fontSize="sm" color="gray.600">• 个人资料和设置</Text>
|
||
<Text fontSize="sm" color="gray.600">• 投资记录和分析数据</Text>
|
||
<Text fontSize="sm" color="gray.600">• 社区发布的内容</Text>
|
||
<Text fontSize="sm" color="gray.600">• 关注和粉丝关系</Text>
|
||
</Box>
|
||
<Text fontSize="sm" color="red.600" fontWeight="medium">
|
||
此操作不可恢复,请确认您真的要注销账户。
|
||
</Text>
|
||
|
||
<Button
|
||
colorScheme="red"
|
||
leftIcon={<WarningIcon />}
|
||
onClick={handleOpenCancellationNotice}
|
||
maxW="200px"
|
||
>
|
||
注销账户
|
||
</Button>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
</VStack>
|
||
</TabPanel>
|
||
</TabPanels>
|
||
</Tabs>
|
||
</VStack>
|
||
|
||
{/* 修改密码模态框 */}
|
||
<Modal isOpen={isPasswordOpen} onClose={onPasswordClose}>
|
||
<ModalOverlay />
|
||
<ModalContent>
|
||
<ModalHeader>
|
||
{passwordStatus.needsFirstTimeSetup ? '设置登录密码' : '修改密码'}
|
||
</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody>
|
||
<VStack spacing={4}>
|
||
{/* 微信用户说明 */}
|
||
{passwordStatus.isWechatUser && passwordStatus.needsFirstTimeSetup && (
|
||
<Alert status="info" borderRadius="md">
|
||
<AlertIcon />
|
||
<Box>
|
||
<AlertTitle>设置密码以便多种方式登录</AlertTitle>
|
||
<AlertDescription fontSize="sm">
|
||
您当前通过微信登录,设置密码后可以使用手机号+密码的方式登录。
|
||
</AlertDescription>
|
||
</Box>
|
||
</Alert>
|
||
)}
|
||
|
||
{/* 当前密码 - 仅非首次设置且非加载状态时显示 */}
|
||
{!passwordStatusLoading && !passwordStatus.needsFirstTimeSetup && (
|
||
<FormControl>
|
||
<FormLabel>当前密码</FormLabel>
|
||
<InputGroup>
|
||
<Input
|
||
type={showPassword ? "text" : "password"}
|
||
value={passwordForm.currentPassword}
|
||
onChange={(e) => setPasswordForm(prev => ({
|
||
...prev,
|
||
currentPassword: e.target.value
|
||
}))}
|
||
placeholder="请输入当前密码"
|
||
/>
|
||
<InputRightElement>
|
||
<IconButton
|
||
variant="ghost"
|
||
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
|
||
onClick={() => setShowPassword(!showPassword)}
|
||
/>
|
||
</InputRightElement>
|
||
</InputGroup>
|
||
</FormControl>
|
||
)}
|
||
|
||
<FormControl>
|
||
<FormLabel>新密码</FormLabel>
|
||
<Input
|
||
type="password"
|
||
value={passwordForm.newPassword}
|
||
onChange={(e) => setPasswordForm(prev => ({
|
||
...prev,
|
||
newPassword: e.target.value
|
||
}))}
|
||
/>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>确认新密码</FormLabel>
|
||
<Input
|
||
type="password"
|
||
value={passwordForm.confirmPassword}
|
||
onChange={(e) => setPasswordForm(prev => ({
|
||
...prev,
|
||
confirmPassword: e.target.value
|
||
}))}
|
||
/>
|
||
</FormControl>
|
||
</VStack>
|
||
</ModalBody>
|
||
<ModalFooter>
|
||
<Button variant="ghost" mr={3} onClick={onPasswordClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
colorScheme="blue"
|
||
onClick={handlePasswordChange}
|
||
isLoading={isLoading}
|
||
>
|
||
{passwordStatus.needsFirstTimeSetup ? '设置密码' : '确认修改'}
|
||
</Button>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
|
||
{/* 绑定手机号模态框 */}
|
||
<Modal isOpen={isPhoneOpen} onClose={onPhoneClose}>
|
||
<ModalOverlay />
|
||
<ModalContent>
|
||
<ModalHeader>绑定手机号</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody>
|
||
<VStack spacing={4}>
|
||
<FormControl>
|
||
<FormLabel>手机号</FormLabel>
|
||
<Input
|
||
value={phoneForm.phone}
|
||
onChange={(e) => setPhoneForm(prev => ({
|
||
...prev,
|
||
phone: e.target.value
|
||
}))}
|
||
placeholder="请输入11位手机号"
|
||
/>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>验证码</FormLabel>
|
||
<HStack>
|
||
<HStack spacing={2} flex="1">
|
||
<PinInput
|
||
value={phoneForm.verificationCode}
|
||
onChange={(value) => setPhoneForm(prev => ({
|
||
...prev,
|
||
verificationCode: value
|
||
}))}
|
||
>
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
</PinInput>
|
||
</HStack>
|
||
<Button
|
||
size="sm"
|
||
onClick={() => sendVerificationCode('phone')}
|
||
isLoading={isLoading}
|
||
>
|
||
发送验证码
|
||
</Button>
|
||
</HStack>
|
||
</FormControl>
|
||
</VStack>
|
||
</ModalBody>
|
||
<ModalFooter>
|
||
<Button variant="ghost" mr={3} onClick={onPhoneClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
colorScheme="blue"
|
||
onClick={handlePhoneBind}
|
||
isLoading={isLoading}
|
||
>
|
||
确认绑定
|
||
</Button>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
|
||
{/* 更换邮箱模态框 */}
|
||
<Modal isOpen={isEmailOpen} onClose={onEmailClose}>
|
||
<ModalOverlay />
|
||
<ModalContent>
|
||
<ModalHeader>更换邮箱</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody>
|
||
<VStack spacing={4}>
|
||
<FormControl>
|
||
<FormLabel>新邮箱</FormLabel>
|
||
<Input
|
||
type="email"
|
||
value={emailForm.email}
|
||
onChange={(e) => setEmailForm(prev => ({
|
||
...prev,
|
||
email: e.target.value
|
||
}))}
|
||
placeholder="请输入新邮箱地址"
|
||
/>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>验证码</FormLabel>
|
||
<HStack>
|
||
<HStack spacing={2} flex="1">
|
||
<PinInput
|
||
value={emailForm.verificationCode}
|
||
onChange={(value) => setEmailForm(prev => ({
|
||
...prev,
|
||
verificationCode: value
|
||
}))}
|
||
>
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
<PinInputField />
|
||
</PinInput>
|
||
</HStack>
|
||
<Button
|
||
size="sm"
|
||
onClick={() => sendVerificationCode('email')}
|
||
isLoading={isLoading}
|
||
>
|
||
发送验证码
|
||
</Button>
|
||
</HStack>
|
||
</FormControl>
|
||
</VStack>
|
||
</ModalBody>
|
||
<ModalFooter>
|
||
<Button variant="ghost" mr={3} onClick={onEmailClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
colorScheme="blue"
|
||
onClick={handleEmailBind}
|
||
isLoading={isLoading}
|
||
>
|
||
确认更换
|
||
</Button>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
|
||
{/* 账户注销须知模态框 */}
|
||
<Modal
|
||
isOpen={isNoticeOpen}
|
||
onClose={onNoticeClose}
|
||
size="xl"
|
||
scrollBehavior="inside"
|
||
>
|
||
<ModalOverlay />
|
||
<ModalContent maxH="90vh">
|
||
<ModalHeader color="red.600" borderBottom="1px" borderColor="gray.200">
|
||
价值前沿账户注销须知
|
||
</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody py={6}>
|
||
<VStack spacing={6} align="stretch">
|
||
<Alert status="warning" borderRadius="md">
|
||
<AlertIcon />
|
||
<Box>
|
||
<AlertTitle>特别提示</AlertTitle>
|
||
<AlertDescription>
|
||
当您提出申请注销,即表示您已充分阅读、理解并接受本《注销须知》的全部内容。
|
||
您注销账户的行为会给您的售后维权带来诸多不便,且注销价值前沿账户后,
|
||
您的个人信息我们会在价值前沿系统中去除,使其保持不可被检索、访问的状态。
|
||
</AlertDescription>
|
||
</Box>
|
||
</Alert>
|
||
|
||
<Text fontSize="sm" color="gray.700">
|
||
亲爱的用户:您在申请注销前,应当认真阅读《价值前沿账户注销须知》(以下称"《注销须知》")。
|
||
请您务必审慎阅读、充分理解《注销须知》中相关条款内容,其中包括:
|
||
</Text>
|
||
|
||
<Box>
|
||
<Text fontWeight="medium" mb={2}>如果您仍执意注销账户,您的账户需同时满足以下条件:</Text>
|
||
<OrderedList spacing={2} fontSize="sm" color="gray.700">
|
||
<ListItem>
|
||
<Text as="span" fontWeight="medium">自愿放弃账户在价值前沿系统中的资产和虚拟权益</Text>
|
||
<Text fontSize="xs" color="gray.600">(包括但不限于账户余额、VIP权益、付费工具使用权限等)</Text>
|
||
</ListItem>
|
||
<ListItem>账户当前为有效账户,非冻结状态</ListItem>
|
||
</OrderedList>
|
||
</Box>
|
||
|
||
<Box>
|
||
<Text fontWeight="medium" mb={2} color="red.600">注销后您将失去的权益和数据:</Text>
|
||
<UnorderedList spacing={1} fontSize="sm" color="gray.700">
|
||
<ListItem>无法登录、使用本价值前沿账户</ListItem>
|
||
<ListItem>个人资料和历史信息(用户名、头像、购买记录、关注信息等)将无法找回</ListItem>
|
||
<ListItem>通过价值前沿账户使用的所有记录将无法找回</ListItem>
|
||
<ListItem>曾获得的余额、优惠券、积分、权益、订单等视为自行放弃</ListItem>
|
||
<ListItem>无法继续使用相关服务,价值前沿无法协助您重新恢复</ListItem>
|
||
</UnorderedList>
|
||
</Box>
|
||
|
||
<Alert status="error" borderRadius="md">
|
||
<AlertIcon />
|
||
<Box>
|
||
<AlertTitle>重要警告</AlertTitle>
|
||
<AlertDescription fontSize="sm">
|
||
<Text>价值前沿账户一旦被注销将不可恢复,请您在操作之前自行备份相关的所有信息和数据。</Text>
|
||
</AlertDescription>
|
||
</Box>
|
||
</Alert>
|
||
|
||
<Box>
|
||
<Text fontWeight="medium" mb={2}>其他重要条款:</Text>
|
||
<UnorderedList spacing={2} fontSize="sm" color="gray.700">
|
||
<ListItem>
|
||
在价值前沿账户注销期间,如果您的账户涉及争议纠纷(包括但不限于投诉、举报、诉讼、仲裁、国家有权机关调查等),
|
||
价值前沿有权自行终止本账户的注销而无需另行得到您的同意。
|
||
</ListItem>
|
||
<ListItem>
|
||
<Text as="span" fontWeight="medium">注销时效:</Text>
|
||
在注销账号后,除非根据法律法规或监管部门要求保留或存储您的个人信息,
|
||
否则账号注销处理时效为即时,我们将即时删除您的个人信息。
|
||
</ListItem>
|
||
<ListItem>
|
||
<Text as="span" fontWeight="medium" color="red.600">免责声明:</Text>
|
||
注销本价值前沿账户并不代表账户注销前的账户行为和相关责任得到豁免或减轻。
|
||
</ListItem>
|
||
</UnorderedList>
|
||
</Box>
|
||
|
||
<Divider />
|
||
|
||
<VStack spacing={4}>
|
||
<Text fontSize="sm" color="gray.600" textAlign="center">
|
||
如您对本《注销须知》有任何疑问,可联系在线客服查询。
|
||
</Text>
|
||
|
||
<Checkbox
|
||
isChecked={hasAgreedToNotice}
|
||
onChange={(e) => setHasAgreedToNotice(e.target.checked)}
|
||
colorScheme="red"
|
||
size="lg"
|
||
>
|
||
<Text fontSize="sm" fontWeight="medium">
|
||
我已仔细阅读并充分理解上述内容,同意按照《价值前沿账户注销须知》的条款进行账户注销
|
||
</Text>
|
||
</Checkbox>
|
||
</VStack>
|
||
</VStack>
|
||
</ModalBody>
|
||
<ModalFooter borderTop="1px" borderColor="gray.200">
|
||
<Button variant="ghost" mr={3} onClick={onNoticeClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
colorScheme="red"
|
||
onClick={handleProceedToDelete}
|
||
isDisabled={!hasAgreedToNotice}
|
||
>
|
||
我已同意,继续注销
|
||
</Button>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
|
||
{/* 注销账户确认模态框 */}
|
||
<Modal isOpen={isDeleteOpen} onClose={onDeleteClose}>
|
||
<ModalOverlay />
|
||
<ModalContent>
|
||
<ModalHeader color="red.600">注销账户确认</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody>
|
||
<VStack spacing={4}>
|
||
<Alert status="error">
|
||
<AlertIcon />
|
||
<Box>
|
||
<AlertTitle>警告!</AlertTitle>
|
||
<AlertDescription>
|
||
此操作将永久删除您的账户和所有数据,且无法恢复。
|
||
</AlertDescription>
|
||
</Box>
|
||
</Alert>
|
||
|
||
<Text>
|
||
如果您确定要注销账户,请在下方输入 "确认注销" 来确认此操作:
|
||
</Text>
|
||
|
||
<Input
|
||
placeholder="请输入:确认注销"
|
||
value={deleteConfirmText}
|
||
onChange={(e) => setDeleteConfirmText(e.target.value)}
|
||
/>
|
||
</VStack>
|
||
</ModalBody>
|
||
<ModalFooter>
|
||
<Button variant="ghost" mr={3} onClick={onDeleteClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
colorScheme="red"
|
||
onClick={handleDeleteAccount}
|
||
isLoading={isLoading}
|
||
>
|
||
确认注销账户
|
||
</Button>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
</Container>
|
||
);
|
||
} |