From 540d1f4c479bb2c5f2aa7a2223fcec847cf6d8bd Mon Sep 17 00:00:00 2001 From: luorijun Date: Sat, 19 Apr 2025 12:56:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E9=A1=B5=E9=9D=A2=EF=BC=8C=E9=87=8D=E6=9E=84=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=B1=95=E7=A4=BA=E4=B8=8E=E7=BC=96=E8=BE=91=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + src/app/admin/layout.tsx | 2 +- src/app/admin/profile/page.tsx | 650 +++++++++++++++++++++++++++++++++ 3 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 src/app/admin/profile/page.tsx diff --git a/README.md b/README.md index 76c88a2..c00dfe3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## TODO +重新设计验证逻辑,通过全局 cache 优化请求效率,使用服务端组件实现验证 + 提取后刷新提取页套餐可用余量 提取时检查 IP 和实名状态 diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 7b8b118..4d82b57 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -53,7 +53,7 @@ export default async function DashboardLayout(props: DashboardLayoutProps) { )}> - + diff --git a/src/app/admin/profile/page.tsx b/src/app/admin/profile/page.tsx new file mode 100644 index 0000000..959afe8 --- /dev/null +++ b/src/app/admin/profile/page.tsx @@ -0,0 +1,650 @@ +'use client' +import {useEffect, useState, useContext, useRef} from 'react' +import Page from '@/components/page' +import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs' +import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from '@/components/ui/card' +import {Form, FormField, FormLabel} from '@/components/ui/form' +import {Button} from '@/components/ui/button' +import {Input} from '@/components/ui/input' +import {useForm} from 'react-hook-form' +import {zodResolver} from '@hookform/resolvers/zod' +import * as z from 'zod' +import {AuthContext} from '@/components/providers/AuthProvider' +import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog' +import {toast} from 'sonner' +import {Alert, AlertDescription, AlertTitle} from '@/components/ui/alert' +import {CheckCircle, AlertCircle, Shield, Wallet, CreditCard, QrCode, UserIcon} from 'lucide-react' +import * as qrcode from 'qrcode' +import Link from 'next/link' +import RechargeModal from '@/components/composites/purchase/_client/recharge' +import {User} from '@/lib/models' + +export type ProfilePageProps = {} + +export default function ProfilePage(props: ProfilePageProps) { + const authCtx = useContext(AuthContext) + const profile = authCtx.profile + + // 默认选中的Tab + const [activeTab, setActiveTab] = useState('basic') + + if (!profile) { + return ( + +
+ + + 加载中 + 正在加载个人信息,请稍候... + +
+
+ ) + } + + return ( + +
+
+ + + 基本信息 + 安全设置 + 余额管理 + 实名认证 + + + + + + + + + + + + + + + + + + +
+ + {/* 侧边栏:客服经理信息 */} +
+ + + + 账户概览 + + + +
+

用户名

+

{profile.name || profile.username}

+
+ +
+

手机号

+

{profile.phone ? maskPhone(profile.phone) : '未设置'}

+
+ +
+

账户余额

+

¥{profile.balance || 0}

+
+ +
+

实名认证

+

+ {profile.id_token ? ( + <> 已认证 + ) : ( + <> 未认证 + )} +

+
+
+
+ + {profile.contact_wechat && ( + + + + 专属客服经理 + + + +
+ +

+ 扫描上方二维码添加客服经理微信
获取更多帮助与支持 +

+
+
+
+ )} +
+
+
+ ) +} + +// 基本信息组件 +function BasicInfoTab(props: { + profile: User + refreshProfile: () => Promise +}) { + const {profile, refreshProfile} = props + + const basicInfoSchema = z.object({ + name: z.string().min(2, '名称至少需要2个字符'), + email: z.string().email('请输入有效的邮箱地址').or(z.string().length(0)), + }) + + type BasicInfoShema = z.infer + + const form = useForm({ + resolver: zodResolver(basicInfoSchema), + defaultValues: { + name: profile.name || '', + email: profile.email || '', + }, + }) + + const [isSubmitting, setIsSubmitting] = useState(false) + + const onSubmit = async (data: BasicInfoShema) => { + setIsSubmitting(true) + try { + // 这里会调用更新用户信息的API + // 示例: await updateUserProfile(data) + + toast.success('基本信息更新成功') + await refreshProfile() + } + catch (error) { + toast.error('更新失败', { + description: (error as Error).message || '请稍后重试', + }) + } + finally { + setIsSubmitting(false) + } + } + + return ( +
+ + {({id, field}) => ( + + )} + + + + {({id, field}) => ( + + )} + + +
+ +
+
+ ) +} + +// 安全设置组件 +function SecurityTab(props: { + profile: User + refreshProfile: () => Promise +}) { + const {profile, refreshProfile} = props + + const [showPhoneDialog, setShowPhoneDialog] = useState(false) + const [showPasswordDialog, setShowPasswordDialog] = useState(false) + + return ( +
+
+
+

手机号

+

+ {profile.phone ? maskPhone(profile.phone) : '未设置手机号'} +

+
+ +
+ +
+
+

密码

+

定期修改密码可以保障您的账户安全

+
+ +
+ +
+
+

账号保护

+

开启登录保护,提升账号安全

+
+ +
+ + {/* 修改手机号对话框 */} + + + {/* 修改密码对话框 */} + +
+ ) +} + +// 余额管理组件 +function BalanceTab(props: { + profile: User + refreshProfile: () => Promise +}) { + const {profile} = props + + // 交易历史记录示例数据 + const transactions = [ + {id: 1, type: '充值', amount: 100, date: '2025-04-18', status: '成功'}, + {id: 2, type: '购买套餐', amount: -50, date: '2025-04-15', status: '成功'}, + {id: 3, type: '系统赠送', amount: 10, date: '2025-04-10', status: '成功'}, + ] + + return ( +
+
+
+

当前余额: ¥{profile.balance || 0}

+

您可以随时充值或查看交易记录

+
+ +
+ +
+

近期交易记录

+
+
+
类型
+
金额
+
日期
+
状态
+
+ +
+ {transactions.map(tx => ( +
+
{tx.type}
+
= 0 ? 'text-green-600' : 'text-red-600'}> + {tx.amount >= 0 ? `+${tx.amount}` : tx.amount} +
+
{tx.date}
+
{tx.status}
+
+ ))} +
+
+ +
+ +
+
+
+ ) +} + +// 实名认证组件 +function IdentifyTab(props: { + profile: User + refreshProfile: () => Promise +}) { + const {profile} = props + + return ( +
+ {profile.id_token ? ( +
+ +

您已完成实名认证

+

认证信息已通过验证,可以正常使用所有功能

+ +
+
+

认证姓名

+

{maskName(profile.name)}

+
+
+

身份证号

+

{maskIdNumber(profile.id_no)}

+
+
+
+ ) : ( +
+ +

您尚未完成实名认证

+

+ 根据相关法律法规要求,使用HTTP代理服务需要先完成实名认证 +

+ + + + +
+ )} + + + + 重要提示 + + 为响应国家相关规定,使用HTTP代理需完成实名认证。 + 认证服务由支付宝提供,您的个人信息将受到严格保护,仅用于账户安全认证。 + + +
+ ) +} + +// 修改手机号对话框 +function ChangePhoneDialog(props: { + open: boolean + onOpenChange: (open: boolean) => void + currentPhone?: string + refreshProfile: () => Promise +}) { + const {open, onOpenChange, currentPhone, refreshProfile} = props + + const phoneChangeSchema = z.object({ + newPhone: z.string().length(11, '请输入11位手机号码'), + verifyCode: z.string().min(4, '请输入验证码'), + }) + + type PhoneChangeSchema = z.infer + + const form = useForm({ + resolver: zodResolver(phoneChangeSchema), + defaultValues: { + newPhone: '', + verifyCode: '', + }, + }) + + const [isSubmitting, setIsSubmitting] = useState(false) + const [countdown, setCountdown] = useState(0) + + const onSubmit = async (data: PhoneChangeSchema) => { + setIsSubmitting(true) + try { + // 这里调用修改手机号的API + // 示例: await changePhone(data.newPhone, data.verifyCode) + + toast.success('手机号更新成功') + await refreshProfile() + onOpenChange(false) + } + catch (error) { + toast.error('更新失败', { + description: (error as Error).message || '请稍后重试', + }) + } + finally { + setIsSubmitting(false) + } + } + + const sendVerifyCode = () => { + const newPhone = form.getValues('newPhone') + if (!newPhone || newPhone.length !== 11) { + form.setError('newPhone', {message: '请输入有效的手机号码'}) + return + } + + // 这里调用发送验证码的API + // 示例: await sendSmsCode(newPhone) + toast.success('验证码已发送') + + // 开始倒计时 + setCountdown(60) + const timer = setInterval(() => { + setCountdown(prev => { + if (prev <= 1) { + clearInterval(timer) + return 0 + } + return prev - 1 + }) + }, 1000) + } + + return ( + + + + 修改手机号 + + +
+ {currentPhone && ( +
+ 当前手机号 + +
+ )} + + + {({id, field}) => ( + + )} + + + + {({id, field}) => ( +
+ + +
+ )} +
+ + + + +
+
+
+ ) +} + +// 修改密码对话框 +function ChangePasswordDialog(props: { + open: boolean + onOpenChange: (open: boolean) => void + refreshProfile: () => Promise +}) { + const {open, onOpenChange, refreshProfile} = props + + const passwordChangeSchema = z.object({ + oldPassword: z.string().min(6, '密码至少需要6个字符'), + newPassword: z.string().min(6, '密码至少需要6个字符'), + confirmPassword: z.string().min(6, '密码至少需要6个字符'), + }).refine(data => data.newPassword === data.confirmPassword, { + message: '两次输入的新密码不一致', + path: ['confirmPassword'], + }) + + type PasswordChangeSchema = z.infer + + const form = useForm({ + resolver: zodResolver(passwordChangeSchema), + defaultValues: { + oldPassword: '', + newPassword: '', + confirmPassword: '', + }, + }) + + const [isSubmitting, setIsSubmitting] = useState(false) + + const onSubmit = async (data: PasswordChangeSchema) => { + setIsSubmitting(true) + try { + // 这里调用修改密码的API + // 示例: await changePassword(data.oldPassword, data.newPassword) + + toast.success('密码修改成功') + onOpenChange(false) + } + catch (error) { + toast.error('修改失败', { + description: (error as Error).message || '请稍后重试', + }) + } + finally { + setIsSubmitting(false) + } + } + + return ( + + + + 修改密码 + + +
+ + {({id, field}) => ( + + )} + + + + {({id, field}) => ( + + )} + + + + {({id, field}) => ( + + )} + + + + + +
+
+
+ ) +} + +// 客服经理二维码组件 +function ServiceManagerQRCode(props: { + wechat?: string +}) { + const {wechat} = props + const canvasRef = useRef(null) + + useEffect(() => { + if (wechat && canvasRef.current) { + qrcode.toCanvas(canvasRef.current, wechat, { + width: 180, + margin: 0, + }).catch(err => { + console.error(err) + }) + } + }, [wechat]) + + return ( +
+ +
+ ) +} + +// 工具函数:遮盖手机号 +function maskPhone(phone?: string) { + if (!phone) return '' + return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') +} + +// 工具函数:遮盖姓名 +function maskName(name?: string) { + if (!name) return '' + if (name.length <= 2) { + return name.charAt(0) + '*' + } + const stars = '*'.repeat(name.length - 2) + return name.charAt(0) + stars + name.charAt(name.length - 1) +} + +// 工具函数:遮盖身份证号 +function maskIdNumber(idNumber?: string) { + if (!idNumber) return '' + return idNumber.replace(/^(.{4})(.*)(.{4})$/, '$1**********$3') +}