update pay ui
This commit is contained in:
14
app.py
14
app.py
@@ -374,13 +374,16 @@ else:
|
|||||||
# Cookie 配置 - 重要:HTTPS 环境必须设置 SECURE=True
|
# Cookie 配置 - 重要:HTTPS 环境必须设置 SECURE=True
|
||||||
app.config['SESSION_COOKIE_SECURE'] = True # 生产环境使用 HTTPS,必须为 True
|
app.config['SESSION_COOKIE_SECURE'] = True # 生产环境使用 HTTPS,必须为 True
|
||||||
app.config['SESSION_COOKIE_HTTPONLY'] = True # 生产环境应设为True,防止XSS攻击
|
app.config['SESSION_COOKIE_HTTPONLY'] = True # 生产环境应设为True,防止XSS攻击
|
||||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # 使用'Lax'以平衡安全性和功能性
|
# SameSite='None' 允许微信内置浏览器在 OAuth 重定向后携带 Cookie
|
||||||
|
# 必须配合 Secure=True 使用(已在上面配置)
|
||||||
|
app.config['SESSION_COOKIE_SAMESITE'] = 'None' # 微信浏览器兼容性:必须为 None
|
||||||
app.config['SESSION_COOKIE_DOMAIN'] = None # 不限制域名
|
app.config['SESSION_COOKIE_DOMAIN'] = None # 不限制域名
|
||||||
app.config['SESSION_COOKIE_PATH'] = '/' # 设置cookie路径
|
app.config['SESSION_COOKIE_PATH'] = '/' # 设置cookie路径
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # session持续7天
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # session持续7天
|
||||||
app.config['REMEMBER_COOKIE_DURATION'] = timedelta(days=30) # 记住登录30天
|
app.config['REMEMBER_COOKIE_DURATION'] = timedelta(days=30) # 记住登录30天
|
||||||
app.config['REMEMBER_COOKIE_SECURE'] = True # 生产环境使用 HTTPS,必须为 True
|
app.config['REMEMBER_COOKIE_SECURE'] = True # 生产环境使用 HTTPS,必须为 True
|
||||||
app.config['REMEMBER_COOKIE_HTTPONLY'] = True # 防止XSS攻击
|
app.config['REMEMBER_COOKIE_HTTPONLY'] = True # 防止XSS攻击
|
||||||
|
app.config['REMEMBER_COOKIE_SAMESITE'] = 'None' # 微信浏览器兼容性
|
||||||
|
|
||||||
# 初始化 Flask-Session(仅在启用 Redis Session 时)
|
# 初始化 Flask-Session(仅在启用 Redis Session 时)
|
||||||
if USE_REDIS_SESSION:
|
if USE_REDIS_SESSION:
|
||||||
@@ -3447,7 +3450,16 @@ def register_with_phone():
|
|||||||
@app.route('/api/account/phone/send-code', methods=['POST'])
|
@app.route('/api/account/phone/send-code', methods=['POST'])
|
||||||
def send_sms_bind_code():
|
def send_sms_bind_code():
|
||||||
"""发送绑定手机验证码(需已登录)"""
|
"""发送绑定手机验证码(需已登录)"""
|
||||||
|
# 调试日志:检查 session 状态
|
||||||
|
user_agent = request.headers.get('User-Agent', '')
|
||||||
|
is_wechat = 'MicroMessenger' in user_agent
|
||||||
|
print(f"[绑定手机验证码] User-Agent: {user_agent[:100]}...")
|
||||||
|
print(f"[绑定手机验证码] 是否微信浏览器: {is_wechat}")
|
||||||
|
print(f"[绑定手机验证码] session 内容: logged_in={session.get('logged_in')}, user_id={session.get('user_id')}")
|
||||||
|
print(f"[绑定手机验证码] Cookie: {request.cookies.get('session', 'None')[:20] if request.cookies.get('session') else 'None'}...")
|
||||||
|
|
||||||
if not session.get('logged_in'):
|
if not session.get('logged_in'):
|
||||||
|
print(f"[绑定手机验证码] ❌ 未登录,拒绝请求")
|
||||||
return jsonify({'error': '未登录'}), 401
|
return jsonify({'error': '未登录'}), 401
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ const MobileDrawer = memo(({
|
|||||||
/>
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||||
<Text fontSize="xs" color={emailTextColor}>{user.email}</Text>
|
{user.phone && (
|
||||||
|
<Text fontSize="xs" color={emailTextColor}>{user.phone}</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
|||||||
{/* 用户信息区 */}
|
{/* 用户信息区 */}
|
||||||
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
||||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||||
<Text fontSize="xs" color="gray.500">{user.email}</Text>
|
|
||||||
{user.phone && (
|
{user.phone && (
|
||||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ const TabletUserMenu = memo(({
|
|||||||
{/* 用户信息区 */}
|
{/* 用户信息区 */}
|
||||||
<Box px={3} py={2} borderBottom="1px" borderColor={borderColor}>
|
<Box px={3} py={2} borderBottom="1px" borderColor={borderColor}>
|
||||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||||
<Text fontSize="xs" color="gray.500">{user.email}</Text>
|
|
||||||
{user.phone && (
|
{user.phone && (
|
||||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,16 +11,10 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Switch,
|
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
Divider,
|
|
||||||
useToast,
|
useToast,
|
||||||
Alert,
|
|
||||||
AlertIcon,
|
|
||||||
AlertTitle,
|
|
||||||
AlertDescription,
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@@ -30,31 +24,17 @@ import {
|
|||||||
ModalCloseButton,
|
ModalCloseButton,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
Badge,
|
Badge,
|
||||||
IconButton,
|
|
||||||
Textarea,
|
|
||||||
Select,
|
|
||||||
useColorMode,
|
|
||||||
useColorModeValue,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanels,
|
TabPanels,
|
||||||
Tab,
|
Tab,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
InputGroup,
|
|
||||||
InputRightElement,
|
|
||||||
PinInput,
|
PinInput,
|
||||||
PinInputField,
|
PinInputField
|
||||||
SimpleGrid
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
EditIcon,
|
|
||||||
ViewIcon,
|
|
||||||
ViewOffIcon,
|
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
DeleteIcon, // 替换 UnlinkIcon
|
DeleteIcon
|
||||||
WarningIcon,
|
|
||||||
CheckIcon,
|
|
||||||
CloseIcon
|
|
||||||
} from '@chakra-ui/icons';
|
} from '@chakra-ui/icons';
|
||||||
import { FaWeixin, FaMobile, FaEnvelope } from 'react-icons/fa';
|
import { FaWeixin, FaMobile, FaEnvelope } from 'react-icons/fa';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
@@ -63,8 +43,7 @@ import { logger } from '../../utils/logger';
|
|||||||
import { useProfileEvents } from '../../hooks/useProfileEvents';
|
import { useProfileEvents } from '../../hooks/useProfileEvents';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { user, updateUser, logout } = useAuth();
|
const { user, updateUser } = useAuth();
|
||||||
const { colorMode, toggleColorMode } = useColorMode();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// 深色模式固定颜色(Settings 页面始终使用深色主题)
|
// 深色模式固定颜色(Settings 页面始终使用深色主题)
|
||||||
@@ -78,27 +57,11 @@ export default function SettingsPage() {
|
|||||||
const profileEvents = useProfileEvents({ pageType: 'settings' });
|
const profileEvents = useProfileEvents({ pageType: 'settings' });
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
const { isOpen: isPasswordOpen, onOpen: onPasswordOpen, onClose: onPasswordClose } = useDisclosure();
|
|
||||||
const { isOpen: isPhoneOpen, onOpen: onPhoneOpen, onClose: onPhoneClose } = useDisclosure();
|
const { isOpen: isPhoneOpen, onOpen: onPhoneOpen, onClose: onPhoneClose } = useDisclosure();
|
||||||
const { isOpen: isEmailOpen, onOpen: onEmailOpen, onClose: onEmailClose } = useDisclosure();
|
const { isOpen: isEmailOpen, onOpen: onEmailOpen, onClose: onEmailClose } = useDisclosure();
|
||||||
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
|
|
||||||
|
|
||||||
// 表单状态
|
// 表单状态
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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({
|
const [phoneForm, setPhoneForm] = useState({
|
||||||
phone: '',
|
phone: '',
|
||||||
verificationCode: ''
|
verificationCode: ''
|
||||||
@@ -107,162 +70,6 @@ export default function SettingsPage() {
|
|||||||
email: '',
|
email: '',
|
||||||
verificationCode: ''
|
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;
|
|
||||||
|
|
||||||
// 🎯 追踪密码修改成功
|
|
||||||
profileEvents.trackPasswordChanged(true);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// 🎯 追踪密码修改失败
|
|
||||||
profileEvents.trackPasswordChanged(false, error.message);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "修改失败",
|
|
||||||
description: error.message,
|
|
||||||
status: "error",
|
|
||||||
duration: 3000,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 发送验证码
|
// 发送验证码
|
||||||
const sendVerificationCode = async (type) => {
|
const sendVerificationCode = async (type) => {
|
||||||
@@ -410,101 +217,6 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存通知设置
|
|
||||||
const saveNotificationSettings = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
logger.debug('SettingsPage', '保存通知设置', notifications);
|
|
||||||
|
|
||||||
// 这里应该调用后端API保存设置
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
updateUser(notifications);
|
|
||||||
|
|
||||||
// 🎯 追踪通知偏好更改
|
|
||||||
profileEvents.trackNotificationPreferencesChanged({
|
|
||||||
email: notifications.email_notifications,
|
|
||||||
push: notifications.system_updates,
|
|
||||||
sms: notifications.sms_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 (
|
return (
|
||||||
<Container maxW="container.xl" py={8}>
|
<Container maxW="container.xl" py={8}>
|
||||||
<VStack spacing={8} align="stretch">
|
<VStack spacing={8} align="stretch">
|
||||||
@@ -513,51 +225,13 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<Tabs variant="enclosed" colorScheme="blue">
|
<Tabs variant="enclosed" colorScheme="blue">
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>账户安全</Tab>
|
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>账户绑定</Tab>
|
||||||
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>通知设置</Tab>
|
|
||||||
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>隐私设置</Tab>
|
|
||||||
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>界面设置</Tab>
|
|
||||||
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>危险操作</Tab>
|
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{/* 账户安全 */}
|
{/* 账户绑定 */}
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<VStack spacing={6} align="stretch">
|
<VStack spacing={6} align="stretch">
|
||||||
{/* 密码设置 */}
|
|
||||||
<Card bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>密码设置</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontWeight="medium" color={textColor}>
|
|
||||||
{passwordStatus.needsFirstTimeSetup ? '设置登录密码' : '登录密码'}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
{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 bg={cardBg} borderColor={borderColor}>
|
<Card bg={cardBg} borderColor={borderColor}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -731,458 +405,12 @@ export default function SettingsPage() {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 两步验证 */}
|
|
||||||
<Card bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>两步验证</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontWeight="medium" color={textColor}>安全验证</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
开启两步验证,提高账户安全性
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<Switch size="lg" />
|
|
||||||
</HStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</VStack>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 通知设置 */}
|
|
||||||
<TabPanel>
|
|
||||||
<VStack spacing={6} align="stretch">
|
|
||||||
<Card bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>通知方式</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontWeight="medium" color={textColor}>邮件通知</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
接收邮件通知
|
|
||||||
</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" color={textColor}>短信通知</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
接收短信通知(需绑定手机号)
|
|
||||||
</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" color={textColor}>微信通知</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
接收微信通知(需绑定微信)
|
|
||||||
</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 bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>通知类型</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 bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>隐私级别</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 bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>屏蔽设置</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 bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>外观设置</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontWeight="medium" color={textColor}>深色模式</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
切换到深色主题
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<Switch
|
|
||||||
isChecked={colorMode === 'dark'}
|
|
||||||
onChange={toggleColorMode}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontWeight="medium" color={textColor}>语言</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
选择界面语言
|
|
||||||
</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 bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={headingColor}>数据管理</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontWeight="medium" color={textColor}>数据导出</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
导出您的个人数据
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<Button variant="outline">
|
|
||||||
导出数据
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontWeight="medium" color={textColor}>清除缓存</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>
|
|
||||||
清除本地缓存数据
|
|
||||||
</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 bg={cardBg} borderColor={borderColor}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color="red.600">注销账户</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<Text color={subTextColor}>
|
|
||||||
注销账户将永久删除您的所有数据,包括:
|
|
||||||
</Text>
|
|
||||||
<Box pl={4}>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>• 个人资料和设置</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>• 投资记录和分析数据</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>• 社区发布的内容</Text>
|
|
||||||
<Text fontSize="sm" color={subTextColor}>• 关注和粉丝关系</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>
|
</VStack>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</VStack>
|
</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}>
|
<Modal isOpen={isPhoneOpen} onClose={onPhoneClose}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
@@ -1314,45 +542,6 @@ export default function SettingsPage() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</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>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ server {
|
|||||||
|
|
||||||
# 上交所实时行情 WebSocket
|
# 上交所实时行情 WebSocket
|
||||||
location /ws/sse {
|
location /ws/sse {
|
||||||
proxy_pass http://49.232.185.254:8765;
|
proxy_pass http://127.0.0.1:8765; # 本机行情服务
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|||||||
Reference in New Issue
Block a user