完善个人中心页面,重构信息展示与编辑功能
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
## TODO
|
||||
|
||||
重新设计验证逻辑,通过全局 cache 优化请求效率,使用服务端组件实现验证
|
||||
|
||||
提取后刷新提取页套餐可用余量
|
||||
|
||||
提取时检查 IP 和实名状态
|
||||
|
||||
@@ -53,7 +53,7 @@ export default async function DashboardLayout(props: DashboardLayoutProps) {
|
||||
)}>
|
||||
<NavItem href={'/admin'} icon={`🏠`} label={`账户总览`}/>
|
||||
<NavTitle label={`个人信息`}/>
|
||||
<NavItem href={`/admin`} icon={`📝`} label={`修改信息`}/>
|
||||
<NavItem href={`/admin/profile`} icon={`📝`} label={`个人中心`}/>
|
||||
<NavItem href={`/admin/identify`} icon={`🆔`} label={`实名认证`}/>
|
||||
<NavItem href={`/admin/whitelist`} 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