完善个人中心页面,重构信息展示与编辑功能
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
重新设计验证逻辑,通过全局 cache 优化请求效率,使用服务端组件实现验证
|
||||||
|
|
||||||
提取后刷新提取页套餐可用余量
|
提取后刷新提取页套餐可用余量
|
||||||
|
|
||||||
提取时检查 IP 和实名状态
|
提取时检查 IP 和实名状态
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default async function DashboardLayout(props: DashboardLayoutProps) {
|
|||||||
)}>
|
)}>
|
||||||
<NavItem href={'/admin'} icon={`🏠`} label={`账户总览`}/>
|
<NavItem href={'/admin'} icon={`🏠`} label={`账户总览`}/>
|
||||||
<NavTitle label={`个人信息`}/>
|
<NavTitle label={`个人信息`}/>
|
||||||
<NavItem href={`/admin`} icon={`📝`} label={`修改信息`}/>
|
<NavItem href={`/admin/profile`} icon={`📝`} label={`个人中心`}/>
|
||||||
<NavItem href={`/admin/identify`} icon={`🆔`} label={`实名认证`}/>
|
<NavItem href={`/admin/identify`} icon={`🆔`} label={`实名认证`}/>
|
||||||
<NavItem href={`/admin/whitelist`} icon={`🔒`} label={`白名单`}/>
|
<NavItem href={`/admin/whitelist`} icon={`🔒`} label={`白名单`}/>
|
||||||
<NavItem href={`/admin/bills`} icon={`💰`} label={`我的账单`}/>
|
<NavItem href={`/admin/bills`} icon={`💰`} label={`我的账单`}/>
|
||||||
|
|||||||
650
src/app/admin/profile/page.tsx
Normal file
650
src/app/admin/profile/page.tsx
Normal file
@@ -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 (
|
||||||
|
<Page>
|
||||||
|
<div className="flex justify-center items-center h-full">
|
||||||
|
<Alert className="max-w-md">
|
||||||
|
<AlertCircle className="h-4 w-4"/>
|
||||||
|
<AlertTitle>加载中</AlertTitle>
|
||||||
|
<AlertDescription>正在加载个人信息,请稍候...</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
|
<div className="lg:col-span-3">
|
||||||
|
<Tabs defaultValue={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
|
<TabsList className="grid grid-cols-4 mb-4">
|
||||||
|
<TabsTrigger value="basic">基本信息</TabsTrigger>
|
||||||
|
<TabsTrigger value="security">安全设置</TabsTrigger>
|
||||||
|
<TabsTrigger value="balance">余额管理</TabsTrigger>
|
||||||
|
<TabsTrigger value="identify">实名认证</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="basic">
|
||||||
|
<BasicInfoTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="security">
|
||||||
|
<SecurityTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="balance">
|
||||||
|
<BalanceTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="identify">
|
||||||
|
<IdentifyTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 侧边栏:客服经理信息 */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<UserIcon size={18}/> 账户概览
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">用户名</p>
|
||||||
|
<p className="font-medium">{profile.name || profile.username}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">手机号</p>
|
||||||
|
<p className="font-medium">{profile.phone ? maskPhone(profile.phone) : '未设置'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">账户余额</p>
|
||||||
|
<p className="font-medium text-xl">¥{profile.balance || 0}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">实名认证</p>
|
||||||
|
<p className="flex items-center gap-2">
|
||||||
|
{profile.id_token ? (
|
||||||
|
<><CheckCircle size={16} className="text-green-500"/> 已认证</>
|
||||||
|
) : (
|
||||||
|
<><AlertCircle size={16} className="text-amber-500"/> 未认证</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{profile.contact_wechat && (
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<QrCode size={18}/> 专属客服经理
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<ServiceManagerQRCode wechat={profile.contact_wechat}/>
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
扫描上方二维码添加客服经理微信<br/>获取更多帮助与支持
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本信息组件
|
||||||
|
function BasicInfoTab(props: {
|
||||||
|
profile: User
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
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<typeof basicInfoSchema>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Form form={form} onSubmit={onSubmit} className="space-y-6">
|
||||||
|
<FormField name="name" label="姓名">
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
placeholder="请输入您的姓名"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField name="email" label="电子邮箱">
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
type="email"
|
||||||
|
placeholder="请输入您的邮箱地址"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? '保存中...' : '保存修改'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全设置组件
|
||||||
|
function SecurityTab(props: {
|
||||||
|
profile: User
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
const {profile, refreshProfile} = props
|
||||||
|
|
||||||
|
const [showPhoneDialog, setShowPhoneDialog] = useState(false)
|
||||||
|
const [showPasswordDialog, setShowPasswordDialog] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between py-3 border-b">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">手机号</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{profile.phone ? maskPhone(profile.phone) : '未设置手机号'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setShowPhoneDialog(true)}>
|
||||||
|
{profile.phone ? '修改' : '设置'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-3 border-b">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">密码</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">定期修改密码可以保障您的账户安全</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setShowPasswordDialog(true)}>修改</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">账号保护</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">开启登录保护,提升账号安全</p>
|
||||||
|
</div>
|
||||||
|
<Button theme="outline">设置</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 修改手机号对话框 */}
|
||||||
|
<ChangePhoneDialog
|
||||||
|
open={showPhoneDialog}
|
||||||
|
onOpenChange={setShowPhoneDialog}
|
||||||
|
currentPhone={profile.phone}
|
||||||
|
refreshProfile={refreshProfile}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 修改密码对话框 */}
|
||||||
|
<ChangePasswordDialog
|
||||||
|
open={showPasswordDialog}
|
||||||
|
onOpenChange={setShowPasswordDialog}
|
||||||
|
refreshProfile={refreshProfile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 余额管理组件
|
||||||
|
function BalanceTab(props: {
|
||||||
|
profile: User
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold">当前余额: ¥{profile.balance || 0}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">您可以随时充值或查看交易记录</p>
|
||||||
|
</div>
|
||||||
|
<RechargeModal/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-medium mb-4">近期交易记录</h3>
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<div className="grid grid-cols-4 bg-muted p-3 font-medium">
|
||||||
|
<div>类型</div>
|
||||||
|
<div>金额</div>
|
||||||
|
<div>日期</div>
|
||||||
|
<div>状态</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y">
|
||||||
|
{transactions.map(tx => (
|
||||||
|
<div key={tx.id} className="grid grid-cols-4 p-3">
|
||||||
|
<div>{tx.type}</div>
|
||||||
|
<div className={tx.amount >= 0 ? 'text-green-600' : 'text-red-600'}>
|
||||||
|
{tx.amount >= 0 ? `+${tx.amount}` : tx.amount}
|
||||||
|
</div>
|
||||||
|
<div>{tx.date}</div>
|
||||||
|
<div>{tx.status}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button theme="outline">查看更多交易记录</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实名认证组件
|
||||||
|
function IdentifyTab(props: {
|
||||||
|
profile: User
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
const {profile} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{profile.id_token ? (
|
||||||
|
<div className="bg-muted rounded-lg p-6 text-center">
|
||||||
|
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-4"/>
|
||||||
|
<h3 className="text-xl font-medium mb-2">您已完成实名认证</h3>
|
||||||
|
<p className="text-muted-foreground mb-6">认证信息已通过验证,可以正常使用所有功能</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 max-w-md mx-auto text-left">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">认证姓名</p>
|
||||||
|
<p className="font-medium">{maskName(profile.name)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">身份证号</p>
|
||||||
|
<p className="font-medium">{maskIdNumber(profile.id_no)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-muted rounded-lg p-6 text-center">
|
||||||
|
<AlertCircle className="w-12 h-12 text-amber-500 mx-auto mb-4"/>
|
||||||
|
<h3 className="text-xl font-medium mb-2">您尚未完成实名认证</h3>
|
||||||
|
<p className="text-muted-foreground mb-6">
|
||||||
|
根据相关法律法规要求,使用HTTP代理服务需要先完成实名认证
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link href="/admin/identify">
|
||||||
|
<Button className="w-40">立即认证</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
<AlertCircle className="h-4 w-4"/>
|
||||||
|
<AlertTitle>重要提示</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
为响应国家相关规定,使用HTTP代理需完成实名认证。
|
||||||
|
认证服务由支付宝提供,您的个人信息将受到严格保护,仅用于账户安全认证。
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改手机号对话框
|
||||||
|
function ChangePhoneDialog(props: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
currentPhone?: string
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
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<typeof phoneChangeSchema>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>修改手机号</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form form={form} onSubmit={onSubmit} className="space-y-4 py-4">
|
||||||
|
{currentPhone && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<FormLabel>当前手机号</FormLabel>
|
||||||
|
<Input value={maskPhone(currentPhone)} disabled/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField name="newPhone" label="新手机号">
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
placeholder="请输入新手机号"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField name="verifyCode" label="验证码">
|
||||||
|
{({id, field}) => (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
className="flex-1"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
disabled={countdown > 0}
|
||||||
|
onClick={sendVerifyCode}
|
||||||
|
>
|
||||||
|
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<DialogFooter className="pt-4">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码对话框
|
||||||
|
function ChangePasswordDialog(props: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
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<typeof passwordChangeSchema>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>修改密码</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form form={form} onSubmit={onSubmit} className="space-y-4 py-4">
|
||||||
|
<FormField name="oldPassword" label="当前密码">
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入当前密码"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField name="newPassword" label="新密码">
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField name="confirmPassword" label="确认新密码">
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<DialogFooter className="pt-4">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客服经理二维码组件
|
||||||
|
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 (
|
||||||
|
<div className="p-2 bg-white rounded-md border">
|
||||||
|
<canvas ref={canvasRef} width="180" height="180"/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:遮盖手机号
|
||||||
|
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')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user