diff --git a/src/views/Pages/Account/Invoice/index.tsx b/src/views/Pages/Account/Invoice/index.tsx index da78e25e..8e1b3b6e 100644 --- a/src/views/Pages/Account/Invoice/index.tsx +++ b/src/views/Pages/Account/Invoice/index.tsx @@ -1,7 +1,7 @@ /** * 发票管理页面 */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from "react"; import { Box, Flex, @@ -31,38 +31,49 @@ import { StatLabel, StatNumber, StatHelpText, -} from '@chakra-ui/react'; -import { FileText, Plus, RefreshCw, Clock, CheckCircle, AlertCircle } from 'lucide-react'; -import Card from '@components/Card/Card'; -import CardHeader from '@components/Card/CardHeader'; -import { InvoiceCard, InvoiceApplyModal } from '@components/Invoice'; +} from "@chakra-ui/react"; +import { + FileText, + Plus, + RefreshCw, + Clock, + CheckCircle, + AlertCircle, +} from "lucide-react"; +import Card from "@components/Card/Card"; +import CardHeader from "@components/Card/CardHeader"; +import { InvoiceCard, InvoiceApplyModal } from "@components/Invoice"; import { getInvoiceList, getInvoiceStats, cancelInvoice, downloadInvoice, -} from '@/services/invoiceService'; -import type { InvoiceInfo, InvoiceStatus, InvoiceStats } from '@/types/invoice'; +} from "@/services/invoiceService"; +import type { InvoiceInfo, InvoiceStatus, InvoiceStats } from "@/types/invoice"; -type TabType = 'all' | 'pending' | 'processing' | 'completed'; +type TabType = "all" | "pending" | "processing" | "completed"; const tabConfig: { key: TabType; label: string; status?: InvoiceStatus }[] = [ - { key: 'all', label: '全部' }, - { key: 'pending', label: '待处理', status: 'pending' }, - { key: 'processing', label: '处理中', status: 'processing' }, - { key: 'completed', label: '已完成', status: 'completed' }, + { key: "all", label: "全部" }, + { key: "pending", label: "待处理", status: "pending" }, + { key: "processing", label: "处理中", status: "processing" }, + { key: "completed", label: "已完成", status: "completed" }, ]; -export default function InvoicePage() { +interface InvoicePageProps { + embedded?: boolean; +} + +export default function InvoicePage({ embedded = false }: InvoicePageProps) { const [invoices, setInvoices] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState('all'); + const [activeTab, setActiveTab] = useState("all"); const [cancelingId, setCancelingId] = useState(null); const toast = useToast(); - const textColor = useColorModeValue('gray.700', 'white'); - const bgCard = useColorModeValue('white', 'gray.800'); + const textColor = useColorModeValue("gray.700", "white"); + const bgCard = useColorModeValue("white", "gray.800"); const cancelDialogRef = React.useRef(null); const { @@ -87,11 +98,11 @@ export default function InvoicePage() { setInvoices(res.data.list || []); } } catch (error) { - console.error('加载发票列表失败:', error); + console.error("加载发票列表失败:", error); toast({ - title: '加载失败', - description: '无法获取发票列表', - status: 'error', + title: "加载失败", + description: "无法获取发票列表", + status: "error", duration: 3000, }); } finally { @@ -107,7 +118,7 @@ export default function InvoicePage() { setStats(res.data); } } catch (error) { - console.error('加载发票统计失败:', error); + console.error("加载发票统计失败:", error); } }, []); @@ -124,25 +135,25 @@ export default function InvoicePage() { const res = await cancelInvoice(cancelingId); if (res.code === 200) { toast({ - title: '取消成功', - status: 'success', + title: "取消成功", + status: "success", duration: 2000, }); loadInvoices(); loadStats(); } else { toast({ - title: '取消失败', + title: "取消失败", description: res.message, - status: 'error', + status: "error", duration: 3000, }); } } catch (error) { toast({ - title: '取消失败', - description: '网络错误', - status: 'error', + title: "取消失败", + description: "网络错误", + status: "error", duration: 3000, }); } finally { @@ -156,7 +167,7 @@ export default function InvoicePage() { try { const blob = await downloadInvoice(invoice.id); const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; a.download = `发票_${invoice.invoiceNo || invoice.id}.pdf`; document.body.appendChild(a); @@ -165,9 +176,9 @@ export default function InvoicePage() { window.URL.revokeObjectURL(url); } catch (error) { toast({ - title: '下载失败', - description: '无法下载发票文件', - status: 'error', + title: "下载失败", + description: "无法下载发票文件", + status: "error", duration: 3000, }); } @@ -186,11 +197,17 @@ export default function InvoicePage() { }; return ( - + {/* 统计卡片 */} {stats && ( - + 全部申请 {stats.total} @@ -200,7 +217,13 @@ export default function InvoicePage() { - + 待处理 {stats.pending} @@ -210,7 +233,13 @@ export default function InvoicePage() { - + 处理中 {stats.processing} @@ -220,7 +249,13 @@ export default function InvoicePage() { - + 已完成 {stats.completed} @@ -234,7 +269,12 @@ export default function InvoicePage() { )} {/* 主内容区 */} - + @@ -340,7 +380,9 @@ export default function InvoicePage() { 取消开票申请 - 确定要取消这个开票申请吗?取消后可重新申请。 + + 确定要取消这个开票申请吗?取消后可重新申请。 + - )} - {onBack && ( - - )} - + {(status === 'failed' || status === 'cancelled') && onRetry && ( + + )} {/* 支付中提示 */} {status === 'paying' && ( diff --git a/src/views/Settings/SettingsPage.js b/src/views/Settings/SettingsPage.js index 92dc7168..5e0ef2ad 100644 --- a/src/views/Settings/SettingsPage.js +++ b/src/views/Settings/SettingsPage.js @@ -1,611 +1,1611 @@ // src/views/Settings/SettingsPage.js -import React, { useState } from 'react'; +import React, { useState, useCallback } from "react"; import { - Box, - VStack, - HStack, - Text, - Heading, - Button, - Input, - FormControl, - FormLabel, - Card, - CardBody, - CardHeader, - useToast, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalBody, - ModalFooter, - ModalCloseButton, - useDisclosure, - Badge, - Tabs, - TabList, - TabPanels, - Tab, - TabPanel, - PinInput, - PinInputField -} from '@chakra-ui/react'; -import { Link2, Trash2, Pencil, Smartphone, Mail, FileText, CreditCard } from 'lucide-react'; -import { WechatOutlined } from '@ant-design/icons'; -import { useAuth } from '../../contexts/AuthContext'; -import { getApiBase } from '../../utils/apiConfig'; -import { logger } from '../../utils/logger'; -import { useProfileEvents } from '../../hooks/useProfileEvents'; -import { useNavigate } from 'react-router-dom'; + Box, + VStack, + HStack, + Text, + Heading, + Button, + Input, + FormControl, + FormLabel, + Card, + CardBody, + CardHeader, + useToast, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + ModalCloseButton, + useDisclosure, + Badge, + PinInput, + PinInputField, + Icon, + Avatar, + Textarea, + Select, + SimpleGrid, +} from "@chakra-ui/react"; +import { + Link2, + Trash2, + Pencil, + Smartphone, + Mail, + FileText, + CreditCard, + ChevronRight, + User, + Settings, + Shield, + ArrowLeft, + Calendar, + Clock, + Camera, + Save, + RotateCcw, + TrendingUp, +} from "lucide-react"; +import { WechatOutlined } from "@ant-design/icons"; +import { Form, Input as AntInput, Select as AntSelect, DatePicker, ConfigProvider, Modal as AntModal, Upload, message, Button as AntButton, Space } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; +import zhCN from "antd/locale/zh_CN"; +import dayjs from "dayjs"; +import InvoicePage from "../Pages/Account/Invoice"; +import { useAuth } from "../../contexts/AuthContext"; +import { getApiBase } from "../../utils/apiConfig"; +import { logger } from "../../utils/logger"; +import { useProfileEvents } from "../../hooks/useProfileEvents"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { useSubscription } from "../../hooks/useSubscription"; + +/** + * 左侧菜单项组件 + */ +const SideMenuItem = ({ icon, label, isActive, onClick }) => ( + + + {label} + +); + +/** + * 胶囊按钮组件 - 用于标签选择 + */ +const TagButton = ({ label, isSelected, onClick }) => ( + + {label} + +); + +/** + * 市场磁贴组件 - 用于偏好市场多选 + */ +const MarketTile = ({ label, isSelected, onClick }) => ( + + {isSelected && "✓ "}{label} + +); + +/** + * 操作按钮组件 - 金色边框金色文字,紧随内容 + */ +const ActionButtons = ({ onSave, onReset, isLoading, hasChanges }) => ( + + + + +); + +/** + * 会员权益条组件 + */ +const MembershipBar = ({ onNavigate }) => { + const { subscriptionInfo } = useSubscription(); + const { type, days_left } = subscriptionInfo; + + const getMemberText = () => { + if (type === "free") return "基础版"; + if (type === "pro") return "Pro会员"; + return "Max会员"; + }; + + const getMemberIcon = () => { + if (type === "free") return "✨"; + if (type === "pro") return "💎"; + return "👑"; + }; + + const gradientBg = + type === "free" + ? "linear(to-r, gray.700, gray.600)" + : "linear(to-r, #F6E5A3, #D4AF37)"; + + return ( + + + + {getMemberIcon()} + + {getMemberText()} + {type !== "free" && ( + + {" "} + · {days_left}天后到期 + + )} + + + + + + ); +}; + +/** + * 手机号脱敏 - 135****0810 + */ +const maskPhone = (phone) => { + if (!phone || typeof phone !== "string") return "未绑定"; + if (phone.length < 7) return phone; + return phone.substring(0, 3) + "****" + phone.substring(phone.length - 4); +}; + +/** + * 邮箱脱敏 - abc***@xxx.com + */ +const maskEmail = (email) => { + if (!email || typeof email !== "string") return "未绑定"; + const atIndex = email.indexOf("@"); + if (atIndex <= 0) return email; + const localPart = email.substring(0, atIndex); + const domain = email.substring(atIndex); + const visibleChars = Math.min(3, localPart.length); + return localPart.substring(0, visibleChars) + "***" + domain; +}; + +/** + * 账号安全面板(手机/邮箱/微信绑定) + */ +const SecurityPanel = ({ + user, + updateUser, + toast, + isLoading, + setIsLoading, + onPhoneOpen, + onEmailOpen, + cardBg, + borderColor, + headingColor, + textColor, + subTextColor, + profileEvents, +}) => { + return ( + + {/* 手机号绑定 */} + + + + 手机号绑定 + + + + + + + + + {user?.phone ? maskPhone(user.phone) : "未绑定手机号"} + + {user?.phone_confirmed && ( + + 已验证 + + )} + + + 绑定手机号可用于登录和接收重要通知 + + + {user?.phone ? ( + + ) : ( + + )} + + + + + {/* 邮箱绑定 */} + + + + 邮箱设置 + + + + + + + + + {user?.email ? maskEmail(user.email) : "未绑定邮箱"} + + {user?.email_confirmed && ( + + 已验证 + + )} + + + 邮箱用于登录和接收重要通知 + + + + + + + + {/* 微信绑定 */} + + + + 微信绑定 + + + + + + + + + {user?.has_wechat ? "已绑定微信" : "未绑定微信"} + + {user?.has_wechat && ( + + 已绑定 + + )} + + + 绑定微信可使用微信一键登录 + + + {user?.has_wechat ? ( + + ) : ( + + )} + + + + + {/* 账户信息 - 页脚化处理 */} + + + 注册时间: + {user?.created_at + ? new Date(user.created_at).toLocaleDateString("zh-CN") + : "2024/2/1"} + + + 最后活跃: + {user?.last_active_at + ? new Date(user.last_active_at).toLocaleDateString("zh-CN") + : "2025/12/25"} + + + + ); +}; + +/** + * 订阅与发票面板 + */ +const BillingPanel = ({ navigate }) => { + return ( + + {/* 会员权益条 */} + navigate("/home/pages/account/subscription")} + /> + + {/* 直接展示发票组件 */} + + + + + ); +}; + +/** + * 个人资料面板 - 双栏布局优化版 + */ +const ProfilePanel = ({ + user, + profileForm, + setProfileForm, + onSave, + onReset, + isLoading, + hasChanges, + cardBg, + borderColor, + headingColor, + subTextColor, +}) => { + const { subscriptionInfo } = useSubscription(); + const [avatarModalOpen, setAvatarModalOpen] = useState(false); + const [uploadLoading, setUploadLoading] = useState(false); + + // 性别选项 + const genderOptions = [ + { value: "male", label: "男" }, + { value: "female", label: "女" }, + { value: "secret", label: "保密" }, + ]; + + // 省份选项 + const locationOptions = [ + "北京", "上海", "广东", "浙江", "江苏", "四川", "湖北", "湖南", + "山东", "河南", "福建", "陕西", "重庆", "天津", "其他", + ]; + + // 处理头像上传 + const handleAvatarUpload = async (file) => { + const isImage = file.type.startsWith("image/"); + if (!isImage) { + message.error("只能上传图片文件"); + return false; + } + const isLt2M = file.size / 1024 / 1024 < 2; + if (!isLt2M) { + message.error("图片大小不能超过 2MB"); + return false; + } + + setUploadLoading(true); + try { + const formData = new FormData(); + formData.append("avatar", file); + const res = await fetch(getApiBase() + "/api/account/avatar", { + method: "POST", + credentials: "include", + body: formData, + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "上传失败"); + + setProfileForm(prev => ({ ...prev, avatar: data.avatar_url })); + message.success("头像上传成功"); + setAvatarModalOpen(false); + } catch (error) { + message.error(error.message || "上传失败"); + } finally { + setUploadLoading(false); + } + return false; // 阻止默认上传行为 + }; + + // antd 深色主题 token - 更亮的文字颜色 + const darkTheme = { + token: { + colorBgContainer: "#374151", + colorBorder: "#4B5563", + colorText: "#F9FAFB", + colorTextPlaceholder: "#9CA3AF", + colorPrimary: "#D4AF37", + colorBgElevated: "#374151", + borderRadius: 6, + }, + }; + + // 标签文字颜色 - 更亮 + const labelColor = "#E5E7EB"; + + return ( + + + + {/* 头部用户信息卡片 - 紧凑版 */} + + setAvatarModalOpen(true)} + _hover={{ opacity: 0.8 }} + transition="opacity 0.2s" + > + + + + + + + + {user?.nickname || user?.username || "设置昵称"} + + setAvatarModalOpen(true)} + > + 点击头像更换 + + + + + {/* 头像更换弹窗 - 黑金主题 */} + + setAvatarModalOpen(false)} + footer={null} + centered + closeIcon={×} + styles={{ + header: { background: "#0D0D0D", borderBottom: "1px solid #D4AF37", color: "#D4AF37" }, + body: { background: "#0D0D0D", padding: "24px" }, + content: { background: "#0D0D0D", borderRadius: 12, border: "1px solid #D4AF37" }, + }} + > + + {/* 当前头像预览 */} + + + {/* 上传组件 */} + + + + + {uploadLoading ? "上传中..." : "选择图片"} + + + + + + 支持 JPG、PNG 格式,大小不超过 2MB + + + + + + {/* 基本信息 - 双栏布局 */} + + + + 基本信息 + + + +
+ {/* 双栏布局 */} + + {/* 左栏 */} + + {/* 昵称 */} + 昵称} + style={{ marginBottom: 0 }} + > + setProfileForm(prev => ({ ...prev, nickname: e.target.value }))} + placeholder="请输入昵称" + maxLength={20} + /> + + + {/* 性别 */} + + 性别 + + {genderOptions.map((option) => ( + setProfileForm(prev => ({ ...prev, gender: option.value }))} + /> + ))} + + + + + {/* 右栏 */} + + {/* 所在地 */} + 所在地} + style={{ marginBottom: 0 }} + > + setProfileForm(prev => ({ ...prev, location: value }))} + placeholder="请选择所在地" + options={locationOptions.map(loc => ({ value: loc, label: loc }))} + style={{ width: "100%" }} + /> + + + {/* 生日 */} + 生日} + style={{ marginBottom: 0 }} + > + setProfileForm(prev => ({ ...prev, birthday: date ? date.format("YYYY-MM-DD") : "" }))} + placeholder="请选择生日" + style={{ width: "100%" }} + /> + + + + + {/* 底部通栏 - 个人简介 */} + + 个人简介} + style={{ marginBottom: 0 }} + > + setProfileForm(prev => ({ ...prev, bio: e.target.value }))} + placeholder="介绍一下自己..." + rows={4} + maxLength={200} + showCount + /> + + + + {/* 操作按钮 - 在表单内部对齐 */} + + +
+
+
+
+
+ ); +}; + +/** + * 投资画像面板 - 标签选择交互 + */ +const InvestmentPortraitPanel = ({ + investmentForm, + setInvestmentForm, + onSave, + onReset, + isLoading, + hasChanges, + cardBg, + borderColor, + headingColor, + subTextColor, +}) => { + // 交易经验选项 + const experienceOptions = [ + { value: "beginner", label: "新手(<1年)" }, + { value: "intermediate", label: "有一定经验(1-3年)" }, + { value: "expert", label: "资深(3年以上)" }, + ]; + + // 投资风格选项 + const styleOptions = [ + { value: "short", label: "短线" }, + { value: "medium", label: "中长线" }, + { value: "value", label: "价值投资" }, + ]; + + // 风险偏好选项 + const riskOptions = [ + { value: "conservative", label: "保守型" }, + { value: "moderate", label: "稳健型" }, + { value: "aggressive", label: "激进型" }, + ]; + + // 投资金额选项 + const amountOptions = [ + { value: "small", label: "10万以下" }, + { value: "medium", label: "10-50万" }, + { value: "large", label: "50万以上" }, + ]; + + // 偏好市场选项 + const marketOptions = [ + { value: "a_stock", label: "A股" }, + { value: "hk_stock", label: "港股" }, + { value: "us_stock", label: "美股" }, + { value: "crypto", label: "加密货币" }, + { value: "fund", label: "基金" }, + { value: "bond", label: "债券" }, + ]; + + // 处理市场多选 + const toggleMarket = (market) => { + setInvestmentForm(prev => { + const markets = prev.preferredMarkets || []; + const isSelected = markets.includes(market); + return { + ...prev, + preferredMarkets: isSelected + ? markets.filter(m => m !== market) + : [...markets, market], + }; + }); + }; + + return ( + + + + + + + + 投资画像 + + + + 完善投资画像,获取更精准的投资建议 + + + + + {/* 交易经验 */} + + + 交易经验 + + + {experienceOptions.map((option) => ( + setInvestmentForm(prev => ({ ...prev, experience: option.value }))} + /> + ))} + + + + {/* 投资风格 */} + + + 投资风格 + + + {styleOptions.map((option) => ( + setInvestmentForm(prev => ({ ...prev, style: option.value }))} + /> + ))} + + + + {/* 风险偏好 */} + + + 风险偏好 + + + {riskOptions.map((option) => ( + setInvestmentForm(prev => ({ ...prev, riskPreference: option.value }))} + /> + ))} + + + + {/* 投资金额 */} + + + 投资金额 + + + {amountOptions.map((option) => ( + setInvestmentForm(prev => ({ ...prev, investmentAmount: option.value }))} + /> + ))} + + + + {/* 偏好市场 - 多选磁贴 */} + + + 偏好市场(可多选) + + + {marketOptions.map((option) => ( + toggleMarket(option.value)} + /> + ))} + + + + {/* 操作按钮 - 在卡片内部对齐 */} + + + + + + + ); +}; export default function SettingsPage() { - const { user, updateUser } = useAuth(); - const toast = useToast(); - const navigate = useNavigate(); + const { user, updateUser } = useAuth(); + const toast = useToast(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); - // 深色模式固定颜色(Settings 页面始终使用深色主题) - const headingColor = 'white'; - const textColor = 'gray.100'; - const subTextColor = 'gray.400'; - const cardBg = 'gray.800'; - const borderColor = 'gray.600'; + // 当前选中的菜单项 + const currentTab = searchParams.get("tab") || "billing"; - // 🎯 初始化设置页面埋点Hook - const profileEvents = useProfileEvents({ pageType: 'settings' }); + // 深色模式固定颜色 + const headingColor = "white"; + const textColor = "gray.100"; + const subTextColor = "gray.400"; + const cardBg = "gray.800"; + const borderColor = "gray.600"; - // 模态框状态 - const { isOpen: isPhoneOpen, onOpen: onPhoneOpen, onClose: onPhoneClose } = useDisclosure(); - const { isOpen: isEmailOpen, onOpen: onEmailOpen, onClose: onEmailClose } = useDisclosure(); + const profileEvents = useProfileEvents({ pageType: "settings" }); - // 表单状态 - const [isLoading, setIsLoading] = useState(false); - const [phoneForm, setPhoneForm] = useState({ - phone: '', - verificationCode: '' - }); - const [emailForm, setEmailForm] = useState({ - email: '', - verificationCode: '' - }); + // 模态框状态 + const { + isOpen: isPhoneOpen, + onOpen: onPhoneOpen, + onClose: onPhoneClose, + } = useDisclosure(); + const { + isOpen: isEmailOpen, + onOpen: onEmailOpen, + onClose: onEmailClose, + } = useDisclosure(); - // 发送验证码 - 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 [isLoading, setIsLoading] = useState(false); + const [phoneForm, setPhoneForm] = useState({ + phone: "", + verificationCode: "", + }); + const [emailForm, setEmailForm] = useState({ + email: "", + verificationCode: "", + }); - 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); + // 个人资料表单状态 + const [profileForm, setProfileForm] = useState({ + nickname: user?.nickname || "", + gender: user?.gender || "secret", + location: user?.location || "", + birthday: user?.birthday || "", + bio: user?.bio || "", + avatar: user?.avatar || "", + }); - 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) + '***@***' }); + // 投资画像表单状态 + const [investmentForm, setInvestmentForm] = useState({ + experience: user?.investment_profile?.experience || "", + style: user?.investment_profile?.style || "", + riskPreference: user?.investment_profile?.risk_preference || "", + investmentAmount: user?.investment_profile?.investment_amount || "", + preferredMarkets: user?.investment_profile?.preferred_markets || [], + }); - // 使用绑定邮箱的验证码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); + // 初始表单状态(用于检测变化和重置) + const [initialProfileForm] = useState(profileForm); + const [initialInvestmentForm] = useState(investmentForm); - if (!res.ok) throw new Error(data.error || '发送失败'); - } + // 检测表单是否有变化 + const hasProfileChanges = JSON.stringify(profileForm) !== JSON.stringify(initialProfileForm); + const hasInvestmentChanges = JSON.stringify(investmentForm) !== JSON.stringify(initialInvestmentForm); - // ❌ 移除验证码发送成功toast - logger.info('SettingsPage', `${type === 'phone' ? '短信' : '邮件'}验证码已发送`); - } catch (error) { - logger.error('SettingsPage', 'sendVerificationCode', error, { type }); + // 保存个人资料 + const handleSaveProfile = async () => { + setIsLoading(true); + try { + const res = await fetch(getApiBase() + "/api/account/profile", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(profileForm), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "保存失败"); + updateUser(profileForm); + toast({ + title: "个人资料已保存", + status: "success", + duration: 3000, + isClosable: true, + }); + } catch (error) { + toast({ + title: "保存失败", + description: error.message, + status: "error", + duration: 3000, + isClosable: true, + }); + } finally { + setIsLoading(false); + } + }; - toast({ - title: "发送失败", - description: error.message, - status: "error", - duration: 3000, - isClosable: true, - }); - } finally { - setIsLoading(false); - } + // 重置个人资料 + const handleResetProfile = () => { + setProfileForm(initialProfileForm); + }; + + // 保存投资画像 + const handleSaveInvestment = async () => { + setIsLoading(true); + try { + const res = await fetch(getApiBase() + "/api/account/investment-profile", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(investmentForm), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "保存失败"); + 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 handleResetInvestment = () => { + setInvestmentForm(initialInvestmentForm); + }; + + // 切换菜单 + const handleTabChange = (tab) => { + setSearchParams({ tab }); + }; + + // 发送验证码 + 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) + "***@***", + }); + 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 || "发送失败"); + } + 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 { + 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, + }); + profileEvents.trackAccountBound("email", true); + toast({ + title: "邮箱绑定成功", + status: "success", + duration: 3000, + isClosable: true, + }); + setEmailForm({ email: "", verificationCode: "" }); + onEmailClose(); + } catch (error) { + profileEvents.trackAccountBound("email", false); + toast({ + title: "绑定失败", + description: error.message, + status: "error", + duration: 3000, + isClosable: true, + }); + } finally { + setIsLoading(false); + } + }; + + // 菜单配置 + const menuItems = [ + { id: "profile", label: "个人资料", icon: User }, + { id: "preference", label: "投资画像", icon: TrendingUp }, + { id: "security", label: "账号安全", icon: Shield }, + { id: "billing", label: "订阅与发票", icon: CreditCard }, + ]; + + // 渲染右侧内容 + const renderContent = () => { + const commonProps = { + cardBg, + borderColor, + headingColor, + textColor, + subTextColor, }; - // 绑定手机号 - 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 || '绑定失败'); + switch (currentTab) { + case "profile": + return ( + + ); + case "preference": + return ( + + ); + case "security": + return ( + + ); + case "billing": + default: + return ; + } + }; - updateUser({ - phone: phoneForm.phone, - phone_confirmed: true - }); + return ( + + {/* 页面标题 */} + + 账户设置 + - toast({ - title: "手机号绑定成功", - status: "success", - duration: 3000, - isClosable: true, - }); + + {/* 左侧菜单 */} + + {menuItems.map((item) => ( + handleTabChange(item.id)} + /> + ))} + - setPhoneForm({ phone: '', verificationCode: '' }); - onPhoneClose(); - } catch (error) { - toast({ - title: "绑定失败", - description: error.message, - status: "error", - duration: 3000, - isClosable: true, - }); - } finally { - setIsLoading(false); - } - }; + {/* 右侧内容区 */} + {renderContent()} + - // 更换邮箱 - 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 - }) - }); + {/* 绑定手机号模态框 - antd 黑金主题 */} + + ×} + footer={ + + + 取消 + + + 确认绑定 + + + } + styles={{ + header: { background: "#0D0D0D", borderBottom: "1px solid #D4AF37", paddingBottom: 16, color: "#D4AF37" }, + body: { background: "#0D0D0D", padding: "24px 24px 16px" }, + content: { background: "#0D0D0D", borderRadius: 12, border: "1px solid #D4AF37" }, + footer: { background: "#0D0D0D", borderTop: "1px solid #333", paddingTop: 16 }, + }} + > +
+ 手机号}> + setPhoneForm((prev) => ({ ...prev, phone: e.target.value }))} + placeholder="请输入11位手机号" + maxLength={11} + /> + + 验证码}> + + setPhoneForm((prev) => ({ ...prev, verificationCode: value }))} + /> + sendVerificationCode("phone")} + loading={isLoading} + > + 发送验证码 + + + +
+
- const data = await res.json(); - if (!res.ok) { - throw new Error(data.error || '绑定失败'); - } - - // 更新用户信息 - updateUser({ - email: data.user.email, - email_confirmed: data.user.email_confirmed - }); - - // 🎯 追踪邮箱绑定成功 - profileEvents.trackAccountBound('email', true); - - toast({ - title: "邮箱绑定成功", - status: "success", - duration: 3000, - isClosable: true, - }); - - setEmailForm({ email: '', verificationCode: '' }); - onEmailClose(); - } catch (error) { - // 🎯 追踪邮箱绑定失败 - profileEvents.trackAccountBound('email', false); - - toast({ - title: "绑定失败", - description: error.message, - status: "error", - duration: 3000, - isClosable: true, - }); - } finally { - setIsLoading(false); - } - }; - - return ( - - - {/* 页面标题 */} - 账户设置 - - - - 账户绑定 - 账单与发票 - - - - {/* 账户绑定 */} - - - {/* 手机号绑定 */} - - - 手机号绑定 - - - - - - - - {user?.phone || '未绑定手机号'} - - {user?.phone_confirmed && ( - 已验证 - )} - - - 绑定手机号可用于登录和接收重要通知 - - - {user?.phone ? ( - - ) : ( - - )} - - - - - {/* 邮箱绑定 */} - - - 邮箱设置 - - - - - - - {user?.email} - {user?.email_confirmed && ( - 已验证 - )} - - - 邮箱用于登录和接收重要通知 - - - - - - - - {/* 微信绑定 */} - - - 微信绑定 - - - - - - - - {user?.has_wechat ? '已绑定微信' : '未绑定微信'} - - {user?.has_wechat && ( - 已绑定 - )} - - - 绑定微信可使用微信一键登录 - - - {user?.has_wechat ? ( - - ) : ( - - )} - - - - - - - - {/* 账单与发票 */} - - - {/* 订阅管理 */} - - - 订阅管理 - - - - - - - - {user?.subscription_type === 'max' ? 'Max 会员' : - user?.subscription_type === 'pro' ? 'Pro 会员' : '免费版'} - - {user?.subscription_type && user?.subscription_type !== 'free' && ( - 有效 - )} - - - 管理您的会员订阅和续费 - - - - - - - - {/* 发票管理 */} - - - 发票管理 - - - - - - - - 电子发票申请与下载 - - - - 已支付的订单可申请开具电子发票 - - - - - - - - - - - - - {/* 绑定手机号模态框 */} - - - - 绑定手机号 - - - - - 手机号 - setPhoneForm(prev => ({ - ...prev, - phone: e.target.value - }))} - placeholder="请输入11位手机号" - /> - - - - 验证码 - - - setPhoneForm(prev => ({ - ...prev, - verificationCode: value - }))} - > - - - - - - - - - - - - - - - - - - - - - {/* 更换邮箱模态框 */} - - - - 更换邮箱 - - - - - 新邮箱 - setEmailForm(prev => ({ - ...prev, - email: e.target.value - }))} - placeholder="请输入新邮箱地址" - /> - - - - 验证码 - - - setEmailForm(prev => ({ - ...prev, - verificationCode: value - }))} - > - - - - - - - - - - - - - - - - - - - - - - ); -} \ No newline at end of file + {/* 更换邮箱模态框 - antd 黑金主题 */} + ×} + footer={ + + + 取消 + + + 确认更换 + + + } + styles={{ + header: { background: "#0D0D0D", borderBottom: "1px solid #D4AF37", paddingBottom: 16, color: "#D4AF37" }, + body: { background: "#0D0D0D", padding: "24px 24px 16px" }, + content: { background: "#0D0D0D", borderRadius: 12, border: "1px solid #D4AF37" }, + footer: { background: "#0D0D0D", borderTop: "1px solid #333", paddingTop: 16 }, + }} + > +
+ 新邮箱}> + setEmailForm((prev) => ({ ...prev, email: e.target.value }))} + placeholder="请输入新邮箱地址" + /> + + 验证码}> + + setEmailForm((prev) => ({ ...prev, verificationCode: value }))} + /> + sendVerificationCode("email")} + loading={isLoading} + > + 发送验证码 + + + +
+
+
+
+ ); +}