This commit is contained in:
2025-10-11 12:10:00 +08:00
parent 8107dee8d3
commit 4d0dc109bc
109 changed files with 152150 additions and 8037 deletions

View File

@@ -1,6 +1,5 @@
// src/views/Settings/SettingsPage.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Box,
Container,
@@ -44,12 +43,7 @@ import {
InputRightElement,
PinInput,
PinInputField,
SimpleGrid,
Checkbox,
UnorderedList,
ListItem,
OrderedList,
Spacer
SimpleGrid
} from '@chakra-ui/react';
import {
EditIcon,
@@ -68,13 +62,11 @@ 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();
// 表单状态
@@ -85,14 +77,6 @@ export default function SettingsPage() {
newPassword: '',
confirmPassword: ''
});
// 密码状态 - 默认假设是普通用户,获取到数据后再更新
const [passwordStatus, setPasswordStatus] = useState({
isWechatUser: false,
hasPassword: true,
needsFirstTimeSetup: false
});
const [passwordStatusLoading, setPasswordStatusLoading] = useState(true);
const [phoneForm, setPhoneForm] = useState({
phone: '',
verificationCode: ''
@@ -101,11 +85,6 @@ export default function SettingsPage() {
email: '',
verificationCode: ''
});
// 注销相关状态
const [hasAgreedToNotice, setHasAgreedToNotice] = useState(false);
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const [blockedKeywords, setBlockedKeywords] = useState(user?.blocked_keywords || '');
// 通知设置状态
@@ -127,53 +106,8 @@ export default function SettingsPage() {
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: "密码不匹配",
@@ -198,52 +132,24 @@ export default function SettingsPage() {
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
})
// 这里应该调用后端API修改密码
await new Promise(resolve => setTimeout(resolve, 1000));
toast({
title: "密码修改成功",
description: "请重新登录",
status: "success",
duration: 3000,
isClosable: true,
});
const data = await response.json();
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
onPasswordClose();
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 || '密码修改失败');
}
// 修改密码后需要重新登录
setTimeout(() => {
logout();
}, 1000);
} catch (error) {
toast({
title: "修改失败",
@@ -261,26 +167,8 @@ export default function SettingsPage() {
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 || '发送失败');
}
// 这里应该调用后端API发送验证码
await new Promise(resolve => setTimeout(resolve, 1000));
toast({
title: "验证码已发送",
@@ -306,14 +194,8 @@ export default function SettingsPage() {
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 || '绑定失败');
// 这里应该调用后端API绑定手机号
await new Promise(resolve => setTimeout(resolve, 1000));
updateUser({
phone: phoneForm.phone,
@@ -346,30 +228,16 @@ export default function SettingsPage() {
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
})
});
// 这里应该调用后端API更换邮箱
await new Promise(resolve => setTimeout(resolve, 1000));
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || '绑定失败');
}
// 更新用户信息
updateUser({
email: data.user.email,
email_confirmed: data.user.email_confirmed
email: emailForm.email,
email_confirmed: true
});
toast({
title: "邮箱绑定成功",
title: "邮箱更换成功",
status: "success",
duration: 3000,
isClosable: true,
@@ -379,7 +247,7 @@ export default function SettingsPage() {
onEmailClose();
} catch (error) {
toast({
title: "绑定失败",
title: "更换失败",
description: error.message,
status: "error",
duration: 3000,
@@ -449,41 +317,8 @@ export default function SettingsPage() {
}
};
// 打开注销须知弹窗
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注销账户
@@ -538,27 +373,13 @@ export default function SettingsPage() {
<CardBody>
<HStack justify="space-between">
<VStack align="start" spacing={1}>
<Text fontWeight="medium">
{passwordStatus.needsFirstTimeSetup ? '设置登录密码' : '登录密码'}
</Text>
<Text fontWeight="medium">登录密码</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 leftIcon={<EditIcon />} onClick={onPasswordOpen}>
修改密码
</Button>
</HStack>
</CardBody>
@@ -590,23 +411,7 @@ export default function SettingsPage() {
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);
}
}}
onClick={handlePhoneUnbind}
isLoading={isLoading}
>
解绑
@@ -672,64 +477,11 @@ export default function SettingsPage() {
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 leftIcon={<LinkIcon />} colorScheme="green">
绑定微信
</Button>
)}
@@ -1087,7 +839,7 @@ export default function SettingsPage() {
<Button
colorScheme="red"
leftIcon={<WarningIcon />}
onClick={handleOpenCancellationNotice}
onClick={onDeleteOpen}
maxW="200px"
>
注销账户
@@ -1105,49 +857,30 @@ export default function SettingsPage() {
<Modal isOpen={isPasswordOpen} onClose={onPasswordClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{passwordStatus.needsFirstTimeSetup ? '设置登录密码' : '修改密码'}
</ModalHeader>
<ModalHeader>修改密码</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="请输入当前密码"
<FormControl>
<FormLabel>当前密码</FormLabel>
<InputGroup>
<Input
type={showPassword ? "text" : "password"}
value={passwordForm.currentPassword}
onChange={(e) => setPasswordForm(prev => ({
...prev,
currentPassword: e.target.value
}))}
/>
<InputRightElement>
<IconButton
variant="ghost"
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
/>
<InputRightElement>
<IconButton
variant="ghost"
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
</InputGroup>
</FormControl>
)}
</InputRightElement>
</InputGroup>
</FormControl>
<FormControl>
<FormLabel>新密码</FormLabel>
@@ -1183,7 +916,7 @@ export default function SettingsPage() {
onClick={handlePasswordChange}
isLoading={isLoading}
>
{passwordStatus.needsFirstTimeSetup ? '设置密码' : '确认修改'}
确认修改
</Button>
</ModalFooter>
</ModalContent>
@@ -1320,124 +1053,6 @@ export default function SettingsPage() {
</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 />
@@ -1460,11 +1075,7 @@ export default function SettingsPage() {
如果您确定要注销账户请在下方输入 "确认注销" 来确认此操作
</Text>
<Input
placeholder="请输入:确认注销"
value={deleteConfirmText}
onChange={(e) => setDeleteConfirmText(e.target.value)}
/>
<Input placeholder="请输入:确认注销" />
</VStack>
</ModalBody>
<ModalFooter>