更新新用户登录后台修改密码和实名认证提示框和修改控制台展示和取消控制台中的个人中心跳转
This commit is contained in:
70
src/app/admin/_client/passwordSetupWrapper.tsx
Normal file
70
src/app/admin/_client/passwordSetupWrapper.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
'use client'
|
||||||
|
import {ChangePasswordDialog} from '@/components/composites/dialogs/change-password-dialog'
|
||||||
|
import {RealnameAuthDialog} from '@/components/composites/dialogs/realname-auth-dialog'
|
||||||
|
import {useState, useEffect} from 'react'
|
||||||
|
import {User} from '@/lib/models'
|
||||||
|
|
||||||
|
export function PasswordSetupWrapper({profile}: {profile: User}) {
|
||||||
|
const [showPasswordDialog, setShowPasswordDialog] = useState(false)
|
||||||
|
const [showRealnameDialog, setShowRealnameDialog] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 每次profile变化时都检查是否需要显示弹窗
|
||||||
|
if (!profile.has_password) {
|
||||||
|
setShowPasswordDialog(true)
|
||||||
|
}
|
||||||
|
else if (!profile.id_token) {
|
||||||
|
setShowRealnameDialog(true)
|
||||||
|
}
|
||||||
|
}, [profile.has_password, profile.id_token])
|
||||||
|
|
||||||
|
const handleDismiss = (type: 'password' | 'realname') => {
|
||||||
|
// 可选:使用sessionStorage只在当前会话期间记住关闭状态
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
sessionStorage.setItem(`dismissed${type === 'password' ? 'PasswordSetup' : 'RealnameAuth'}`, 'true')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showPasswordDialog && (
|
||||||
|
<ChangePasswordDialog
|
||||||
|
triggerClassName="hidden"
|
||||||
|
open={showPasswordDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setShowPasswordDialog(open)
|
||||||
|
if (!open) {
|
||||||
|
handleDismiss('password')
|
||||||
|
if (!profile.id_token) {
|
||||||
|
setShowRealnameDialog(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
setShowPasswordDialog(false)
|
||||||
|
if (!profile.id_token) {
|
||||||
|
setShowRealnameDialog(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showRealnameDialog && (
|
||||||
|
<RealnameAuthDialog
|
||||||
|
hasAuthenticated={!!profile.id_token}
|
||||||
|
triggerClassName="hidden"
|
||||||
|
open={showRealnameDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setShowRealnameDialog(open)
|
||||||
|
if (!open) {
|
||||||
|
handleDismiss('realname')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
setShowRealnameDialog(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import Navbar from './_client/navbar'
|
|||||||
import Layout from './_client/layout'
|
import Layout from './_client/layout'
|
||||||
import {getProfile} from '@/actions/auth'
|
import {getProfile} from '@/actions/auth'
|
||||||
import {redirect} from 'next/navigation'
|
import {redirect} from 'next/navigation'
|
||||||
|
import {PasswordSetupWrapper} from './_client/passwordSetupWrapper'
|
||||||
|
|
||||||
export type AdminLayoutProps = {
|
export type AdminLayoutProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -12,6 +13,7 @@ export type AdminLayoutProps = {
|
|||||||
export default async function AdminLayout(props: AdminLayoutProps) {
|
export default async function AdminLayout(props: AdminLayoutProps) {
|
||||||
const resp = await getProfile()
|
const resp = await getProfile()
|
||||||
const profile = resp.success ? resp.data : null
|
const profile = resp.success ? resp.data : null
|
||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
redirect('/login')
|
redirect('/login')
|
||||||
}
|
}
|
||||||
@@ -20,7 +22,15 @@ export default async function AdminLayout(props: AdminLayoutProps) {
|
|||||||
<Layout
|
<Layout
|
||||||
navbar={<Navbar/>}
|
navbar={<Navbar/>}
|
||||||
header={<Header profile={profile}/>}
|
header={<Header profile={profile}/>}
|
||||||
content={props.children}
|
content={(
|
||||||
|
<>
|
||||||
|
{props.children}
|
||||||
|
{/* 需要时显示密码设置和实名认证 */}
|
||||||
|
{(!profile?.has_password && !profile?.id_token) && (
|
||||||
|
<PasswordSetupWrapper profile={profile}/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {useEffect, useRef, useState} from 'react'
|
import {useEffect, useRef} from 'react'
|
||||||
import Page from '@/components/page'
|
import Page from '@/components/page'
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
|
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
|
||||||
import {Form, FormField} from '@/components/ui/form'
|
import {Form, FormField} from '@/components/ui/form'
|
||||||
@@ -16,17 +16,11 @@ import banner from '@/app/admin/identify/_assets/banner.webp'
|
|||||||
import {Input} from '@/components/ui/input'
|
import {Input} from '@/components/ui/input'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import {User} from '@/lib/models'
|
import {User} from '@/lib/models'
|
||||||
import {update, updatePassword} from '@/actions/user'
|
import {update} from '@/actions/user'
|
||||||
import {
|
|
||||||
Dialog, DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {sendSMS} from '@/actions/verify'
|
|
||||||
import RechargeModal from '@/components/composites/recharge'
|
import RechargeModal from '@/components/composites/recharge'
|
||||||
import {useRouter} from 'next/navigation'
|
import {useRouter} from 'next/navigation'
|
||||||
|
import {RealnameAuthDialog} from '@/components/composites/dialogs/realname-auth-dialog'
|
||||||
|
import {ChangePasswordDialog} from '@/components/composites/dialogs/change-password-dialog'
|
||||||
|
|
||||||
export type ProfilePageProps = {}
|
export type ProfilePageProps = {}
|
||||||
|
|
||||||
@@ -83,7 +77,10 @@ export default function ProfilePage(props: ProfilePageProps) {
|
|||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm">为了保障您的账户安全和正常使用服务,请您尽快完成实名认证</p>
|
<p className="text-sm">为了保障您的账户安全和正常使用服务,请您尽快完成实名认证</p>
|
||||||
<Button theme="outline" className="w-24" onClick={() => router.push('/admin/identify')}>去认证</Button>
|
<RealnameAuthDialog
|
||||||
|
hasAuthenticated={!!profile?.id_token}
|
||||||
|
triggerClassName="w-24"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
@@ -109,7 +106,7 @@ export default function ProfilePage(props: ProfilePageProps) {
|
|||||||
<h3 className="font-normal">安全信息</h3>
|
<h3 className="font-normal">安全信息</h3>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<p>{profile.phone}</p>
|
<p>{profile.phone}</p>
|
||||||
<PasswordForm profile={profile}/>
|
<ChangePasswordDialog triggerClassName="w-24 h-9"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -297,196 +294,3 @@ function BasicForm(props: {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PasswordForm(props: {
|
|
||||||
profile: User
|
|
||||||
}) {
|
|
||||||
// ======================
|
|
||||||
// open
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
// ======================
|
|
||||||
// form
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
phone: z.string().regex(/^1\d{10}$/, `请输入正确的手机号`),
|
|
||||||
captcha: z.string().nonempty('请输入验证码'),
|
|
||||||
code: z.string().regex(/^\d{6}$/, `请输入正确的验证码`),
|
|
||||||
password: z.string().min(6, `密码至少6位`),
|
|
||||||
confirm_password: z.string(),
|
|
||||||
}).refine(data => data.password === data.confirm_password, {
|
|
||||||
message: '两次输入的密码不一致',
|
|
||||||
path: ['confirm_password'],
|
|
||||||
})
|
|
||||||
type Schema = z.infer<typeof schema>
|
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
|
||||||
resolver: zodResolver(
|
|
||||||
schema.refine(
|
|
||||||
data =>
|
|
||||||
/^(?=.*[a-z])(?=.*[A-Z]).{6,}$/.test(data.password),
|
|
||||||
{
|
|
||||||
message: '密码需包含大小写字母,且不少于6位',
|
|
||||||
path: ['password'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
defaultValues: {
|
|
||||||
phone: '',
|
|
||||||
captcha: '',
|
|
||||||
code: '',
|
|
||||||
password: '',
|
|
||||||
confirm_password: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const router = useRouter()
|
|
||||||
const handler = form.handleSubmit(async (value) => {
|
|
||||||
try {
|
|
||||||
const resp = await updatePassword({
|
|
||||||
phone: value.phone,
|
|
||||||
code: value.code,
|
|
||||||
password: value.password,
|
|
||||||
})
|
|
||||||
if (!resp.success) {
|
|
||||||
throw new Error(resp.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(`保存成功,请重新登录`)
|
|
||||||
setOpen(false)
|
|
||||||
// 立即跳转到登录页
|
|
||||||
router.replace('/login')
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
toast.error(`保存失败`, {
|
|
||||||
description: e instanceof Error ? e.message : String(e),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ======================
|
|
||||||
// phone code
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
const [captchaUrl, setCaptchaUrl] = useState(`/captcha?t=${new Date().getTime()}`)
|
|
||||||
const [captchaWait, setCaptchaWait] = useState(0)
|
|
||||||
const interval = useRef<NodeJS.Timeout>(null)
|
|
||||||
|
|
||||||
const refreshCaptcha = () => {
|
|
||||||
setCaptchaUrl(`/captcha?t=${new Date().getTime()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendVerifier = async () => {
|
|
||||||
const result = await form.trigger(['phone', 'captcha'])
|
|
||||||
if (!result) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const {phone, captcha} = form.getValues()
|
|
||||||
const resp = await sendSMS({phone, captcha})
|
|
||||||
if (!resp.success) {
|
|
||||||
toast.error(resp.message)
|
|
||||||
refreshCaptcha()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCaptchaWait(60)
|
|
||||||
interval.current = setInterval(() => {
|
|
||||||
setCaptchaWait((wait) => {
|
|
||||||
if (wait <= 1) {
|
|
||||||
clearInterval(interval.current!)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return wait - 1
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
toast.success(`验证码已发送,请注意查收`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================
|
|
||||||
// render
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button theme="outline" className="w-24 h-9">修改密码</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>修改密码</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<Form form={form} handler={handler} className="flex flex-col gap-4 mt-4">
|
|
||||||
|
|
||||||
{/* 手机号 */}
|
|
||||||
<FormField<Schema> name="phone" label="手机号" className="flex-auto">
|
|
||||||
{({field}) => (
|
|
||||||
<Input {...field} placeholder="请输入手机号" autoComplete="tel-national"/>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField<Schema> name="captcha" label="验证码">
|
|
||||||
{({field}) => (
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
|
|
||||||
<Button className="p-0 bg-transparent" onClick={refreshCaptcha} type="button">
|
|
||||||
<img src={captchaUrl} alt="验证码" className="h-10"/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
{/* 短信令牌 */}
|
|
||||||
<FormField<Schema> name="code" label="短信令牌" className="flex-auto">
|
|
||||||
{({field}) => (
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
|
|
||||||
<Button theme="outline" type="button" className="w-36" onClick={() => sendVerifier()}>
|
|
||||||
{captchaWait > 0
|
|
||||||
? `重新发送(${captchaWait})`
|
|
||||||
: `获取短信令牌`
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
{/* 新密码 */}
|
|
||||||
<FormField<Schema> name="password" label="新密码" className="flex-auto">
|
|
||||||
{({field}) => (
|
|
||||||
<Input {...field} placeholder="请输入新密码" type="password" autoComplete="new-password"/>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
{/* 确认密码 */}
|
|
||||||
<FormField<Schema> name="confirm_password" label="确认密码" className="flex-auto">
|
|
||||||
{({field}) => (
|
|
||||||
<Input {...field} placeholder="请再次输入新密码" type="password" autoComplete="new-password"/>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
theme="outline"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(false)
|
|
||||||
form.reset()
|
|
||||||
}}>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
<Button onClick={async (e) => {
|
|
||||||
const result = await handler(e)
|
|
||||||
}}>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
212
src/components/composites/dialogs/change-password-dialog.tsx
Normal file
212
src/components/composites/dialogs/change-password-dialog.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
'use client'
|
||||||
|
import {useEffect, useRef, useState} from 'react'
|
||||||
|
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
|
||||||
|
import {Button} from '@/components/ui/button'
|
||||||
|
import {Form, FormField} from '@/components/ui/form'
|
||||||
|
import {Input} from '@/components/ui/input'
|
||||||
|
import {useForm} from 'react-hook-form'
|
||||||
|
import {zodResolver} from '@hookform/resolvers/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import {toast} from 'sonner'
|
||||||
|
import {useRouter} from 'next/navigation'
|
||||||
|
import {sendSMS} from '@/actions/verify'
|
||||||
|
import {updatePassword} from '@/actions/user'
|
||||||
|
|
||||||
|
interface ChangePasswordDialogProps {
|
||||||
|
triggerClassName?: string
|
||||||
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangePasswordDialog({
|
||||||
|
triggerClassName,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSuccess,
|
||||||
|
}: ChangePasswordDialogProps) {
|
||||||
|
const [internalOpen, setInternalOpen] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const actualOpen = open !== undefined ? open : internalOpen
|
||||||
|
const actualOnOpenChange = onOpenChange || setInternalOpen
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const schema = z.object({
|
||||||
|
phone: z.string().regex(/^1\d{10}$/, `请输入正确的手机号`),
|
||||||
|
captcha: z.string().nonempty('请输入验证码'),
|
||||||
|
code: z.string().regex(/^\d{6}$/, `请输入正确的验证码`),
|
||||||
|
password: z.string().min(6, `密码至少6位`),
|
||||||
|
confirm_password: z.string(),
|
||||||
|
}).refine(data => data.password === data.confirm_password, {
|
||||||
|
message: '两次输入的密码不一致',
|
||||||
|
path: ['confirm_password'],
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
|
// 表单初始化
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
resolver: zodResolver(
|
||||||
|
schema.refine(
|
||||||
|
data => /^(?=.*[a-z])(?=.*[A-Z]).{6,}$/.test(data.password),
|
||||||
|
{
|
||||||
|
message: '密码需包含大小写字母,且不少于6位',
|
||||||
|
path: ['password'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
defaultValues: {
|
||||||
|
phone: '',
|
||||||
|
captcha: '',
|
||||||
|
code: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提交处理
|
||||||
|
const handler = form.handleSubmit(async (value) => {
|
||||||
|
try {
|
||||||
|
const resp = await updatePassword({
|
||||||
|
phone: value.phone,
|
||||||
|
code: value.code,
|
||||||
|
password: value.password,
|
||||||
|
})
|
||||||
|
if (!resp.success) {
|
||||||
|
throw new Error(resp.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(`保存成功,请重新登录`)
|
||||||
|
onSuccess?.()
|
||||||
|
actualOnOpenChange(false)
|
||||||
|
router.replace('/login')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error(`保存失败`, {
|
||||||
|
description: e instanceof Error ? e.message : String(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证码相关状态
|
||||||
|
const [captchaUrl, setCaptchaUrl] = useState(`/captcha?t=${new Date().getTime()}`)
|
||||||
|
const [captchaWait, setCaptchaWait] = useState(0)
|
||||||
|
const interval = useRef<NodeJS.Timeout>(null)
|
||||||
|
|
||||||
|
// 刷新验证码
|
||||||
|
const refreshCaptcha = () => {
|
||||||
|
setCaptchaUrl(`/captcha?t=${new Date().getTime()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送短信验证码
|
||||||
|
const sendVerifier = async () => {
|
||||||
|
const result = await form.trigger(['phone', 'captcha'])
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
const {phone, captcha} = form.getValues()
|
||||||
|
const resp = await sendSMS({phone, captcha})
|
||||||
|
if (!resp.success) {
|
||||||
|
toast.error(resp.message)
|
||||||
|
refreshCaptcha()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCaptchaWait(60)
|
||||||
|
interval.current = setInterval(() => {
|
||||||
|
setCaptchaWait((wait) => {
|
||||||
|
if (wait <= 1) {
|
||||||
|
clearInterval(interval.current!)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return wait - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
toast.success(`验证码已发送,请注意查收`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (interval.current) {
|
||||||
|
clearInterval(interval.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={actualOpen} onOpenChange={actualOnOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button theme="outline" className={triggerClassName || 'w-24 h-9'}>修改密码</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>修改密码</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form form={form} handler={handler} className="flex flex-col gap-4 mt-4">
|
||||||
|
{/* 手机号输入 */}
|
||||||
|
<FormField<Schema> name="phone" label="手机号" className="flex-auto">
|
||||||
|
{({field}) => (
|
||||||
|
<Input {...field} placeholder="请输入手机号" autoComplete="tel-national"/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{/* 图形验证码 */}
|
||||||
|
<FormField<Schema> name="captcha" label="验证码">
|
||||||
|
{({field}) => (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
|
||||||
|
<Button className="p-0 bg-transparent" onClick={refreshCaptcha} type="button">
|
||||||
|
<img src={captchaUrl} alt="验证码" className="h-10"/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{/* 短信验证码 */}
|
||||||
|
<FormField<Schema> name="code" label="短信令牌" className="flex-auto">
|
||||||
|
{({field}) => (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
|
||||||
|
<Button theme="outline" type="button" className="w-36" onClick={() => sendVerifier()}>
|
||||||
|
{captchaWait > 0 ? `重新发送(${captchaWait})` : `获取短信令牌`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{/* 新密码 */}
|
||||||
|
<FormField<Schema> name="password" label="新密码" className="flex-auto">
|
||||||
|
{({field}) => (
|
||||||
|
<Input {...field} placeholder="请输入新密码" type="password" autoComplete="new-password"/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{/* 确认密码 */}
|
||||||
|
<FormField<Schema> name="confirm_password" label="确认密码" className="flex-auto">
|
||||||
|
{({field}) => (
|
||||||
|
<Input {...field} placeholder="请再次输入新密码" type="password" autoComplete="new-password"/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
actualOnOpenChange(false)
|
||||||
|
form.reset()
|
||||||
|
}}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handler}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
65
src/components/composites/dialogs/realname-auth-dialog.tsx
Normal file
65
src/components/composites/dialogs/realname-auth-dialog.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
'use client'
|
||||||
|
import {Button} from '@/components/ui/button'
|
||||||
|
import {Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
|
||||||
|
import {useRouter} from 'next/navigation'
|
||||||
|
import {useState} from 'react'
|
||||||
|
|
||||||
|
interface RealnameAuthDialogProps {
|
||||||
|
hasAuthenticated: boolean
|
||||||
|
triggerClassName?: string
|
||||||
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RealnameAuthDialog({
|
||||||
|
hasAuthenticated,
|
||||||
|
triggerClassName,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSuccess,
|
||||||
|
}: RealnameAuthDialogProps) {
|
||||||
|
const [internalOpen, setInternalOpen] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const actualOpen = open !== undefined ? open : internalOpen
|
||||||
|
const actualOnOpenChange = onOpenChange || setInternalOpen
|
||||||
|
|
||||||
|
if (hasAuthenticated) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={actualOpen} onOpenChange={actualOnOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button theme="outline" className={triggerClassName || 'w-24'}>
|
||||||
|
去认证
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>实名认证</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4 space-y-4">
|
||||||
|
<p>为了保障您的账户安全和正常使用服务,请您尽快完成实名认证</p>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => actualOnOpenChange(false)}
|
||||||
|
>
|
||||||
|
稍后再说
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
router.push('/admin/identify')
|
||||||
|
onSuccess?.()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
立即认证
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import {useProfileStore} from '@/components/stores-provider'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
|
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
|
||||||
import {LoaderIcon, LogOutIcon, UserIcon, UserPenIcon} from 'lucide-react'
|
import {LogOutIcon, UserIcon} from 'lucide-react'
|
||||||
import {usePathname, useRouter} from 'next/navigation'
|
import {usePathname, useRouter} from 'next/navigation'
|
||||||
import {logout} from '@/actions/auth'
|
import {logout} from '@/actions/auth'
|
||||||
import {HoverCard, HoverCardContent, HoverCardTrigger} from '@/components/ui/hover-card'
|
import {HoverCard, HoverCardContent, HoverCardTrigger} from '@/components/ui/hover-card'
|
||||||
@@ -27,8 +27,18 @@ export default function UserCenter(props: UserCenterProps) {
|
|||||||
|
|
||||||
// 展示与跳转
|
// 展示与跳转
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const toAdminPage = () => {
|
const isAdminPage = pathname.startsWith('/admin') // 判断是否在后台页面
|
||||||
if (!pathname.startsWith('/admin')) {
|
const displayName = () => {
|
||||||
|
if (props.profile.name) return props.profile.name // 优先显示用户名
|
||||||
|
if (props.profile.phone) {
|
||||||
|
const phone = props.profile.phone
|
||||||
|
return `${phone.substring(0, 3)}****${phone.substring(7)}`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAvatarClick = () => {
|
||||||
|
if (!isAdminPage) {
|
||||||
router.push('/admin')
|
router.push('/admin')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,23 +49,28 @@ export default function UserCenter(props: UserCenterProps) {
|
|||||||
<Button
|
<Button
|
||||||
theme="ghost"
|
theme="ghost"
|
||||||
className="flex gap-2 items-center h-12"
|
className="flex gap-2 items-center h-12"
|
||||||
onClick={toAdminPage}
|
onClick={handleAvatarClick}
|
||||||
>
|
>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={props.profile.avatar} alt="avatar"/>
|
<AvatarImage src={props.profile.avatar} alt="avatar"/>
|
||||||
<AvatarFallback className="bg-primary-muted"><UserIcon/></AvatarFallback>
|
<AvatarFallback className="bg-primary-muted"><UserIcon/></AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{props.profile.name}</span>
|
{/* 根据是否在后台页面显示不同内容 */}
|
||||||
|
{isAdminPage ? (
|
||||||
|
<span>{displayName() || '用户'}</span> // 后台显示姓名/脱敏手机号/默认"用户"
|
||||||
|
) : (
|
||||||
|
<span>进入控制台</span> // 前台显示"进入控制台"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="w-36 p-1" align="end">
|
<HoverCardContent className="w-36 p-1" align="end">
|
||||||
<Button
|
{/* <Button
|
||||||
theme="ghost"
|
theme="ghost"
|
||||||
className="w-full justify-start text-sm h-9 px-3"
|
className="w-full justify-start text-sm h-9 px-3"
|
||||||
onClick={() => router.push('/admin/profile')}>
|
onClick={() => router.push('/admin/profile')}>
|
||||||
<UserPenIcon/>
|
<UserPenIcon/>
|
||||||
个人中心
|
个人中心
|
||||||
</Button>
|
</Button> */}
|
||||||
<Button
|
<Button
|
||||||
theme="ghost"
|
theme="ghost"
|
||||||
color="fail"
|
color="fail"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type User = {
|
|||||||
id: number
|
id: number
|
||||||
admin_id: number
|
admin_id: number
|
||||||
phone: string
|
phone: string
|
||||||
|
has_password: boolean
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Reference in New Issue
Block a user