Files
vf_react/src/views/Settings/SettingsPage.js
2025-10-20 21:25:33 +08:00

1327 lines
65 KiB
JavaScript
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.

// src/views/Settings/SettingsPage.js
import React, { useState } from 'react';
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
} 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';
import { getApiBase } from '../../utils/apiConfig';
import { logger } from '../../utils/logger';
export default function SettingsPage() {
const { user, updateUser, logout } = useAuth();
const { colorMode, toggleColorMode } = useColorMode();
const toast = useToast();
// 模态框状态
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: 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 [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 = getApiBase();
logger.api.request('GET', '/api/account/password-status', null);
const response = await fetch(`${API_BASE_URL}/api/account/password-status`, {
method: 'GET',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
logger.api.response('GET', '/api/account/password-status', response.status, data);
if (data.success) {
logger.debug('SettingsPage', '密码状态获取成功', data.data);
setPasswordStatus(data.data);
}
}
} catch (error) {
logger.error('SettingsPage', 'fetchPasswordStatus', 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 = getApiBase();
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 url = '/api/account/phone/send-code';
logger.api.request('POST', url, { phone: phoneForm.phone.substring(0, 3) + '****' });
const res = await fetch(getApiBase() + url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ phone: phoneForm.phone })
});
const data = await res.json();
logger.api.response('POST', url, res.status, data);
if (!res.ok) throw new Error(data.error || '发送失败');
} else {
const url = '/api/account/email/send-bind-code';
logger.api.request('POST', url, { email: emailForm.email.substring(0, 3) + '***@***' });
// 使用绑定邮箱的验证码API
const res = await fetch(getApiBase() + url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email: emailForm.email })
});
const data = await res.json();
logger.api.response('POST', url, res.status, data);
if (!res.ok) throw new Error(data.error || '发送失败');
}
// ❌ 移除验证码发送成功toast
logger.info('SettingsPage', `${type === 'phone' ? '短信' : '邮件'}验证码已发送`);
} catch (error) {
logger.error('SettingsPage', 'sendVerificationCode', error, { type });
toast({
title: "发送失败",
description: error.message,
status: "error",
duration: 3000,
isClosable: true,
});
} finally {
setIsLoading(false);
}
};
// 绑定手机号
const handlePhoneBind = async () => {
setIsLoading(true);
try {
const res = await fetch(getApiBase() + '/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(getApiBase() + '/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 {
logger.debug('SettingsPage', '保存通知设置', notifications);
// 这里应该调用后端API保存设置
await new Promise(resolve => setTimeout(resolve, 1000));
updateUser(notifications);
// ❌ 移除设置保存成功toast
logger.info('SettingsPage', '通知设置已保存');
} catch (error) {
logger.error('SettingsPage', 'saveNotificationSettings', error);
toast({
title: "保存失败",
description: error.message,
status: "error",
duration: 3000,
isClosable: true,
});
} finally {
setIsLoading(false);
}
};
// 保存隐私设置
const savePrivacySettings = async () => {
setIsLoading(true);
try {
logger.debug('SettingsPage', '保存隐私设置', { privacy, blockedKeywords });
// 这里应该调用后端API保存设置
await new Promise(resolve => setTimeout(resolve, 1000));
updateUser({
privacy_level: privacy.privacy_level,
blocked_keywords: blockedKeywords
});
// ❌ 移除设置保存成功toast
logger.info('SettingsPage', '隐私设置已保存');
} catch (error) {
logger.error('SettingsPage', 'savePrivacySettings', error);
toast({
title: "保存失败",
description: error.message,
status: "error",
duration: 3000,
isClosable: true,
});
} finally {
setIsLoading(false);
}
};
// 注销账户
const handleDeleteAccount = async () => {
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(getApiBase() + '/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(getApiBase() + '/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 = getApiBase();
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={onDeleteOpen}
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={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="请输入:确认注销" />
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onDeleteClose}>
取消
</Button>
<Button
colorScheme="red"
onClick={handleDeleteAccount}
isLoading={isLoading}
>
确认注销账户
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Container>
);
}