@@ -46,8 +46,9 @@ import { Label } from "@/components/ui/label"
4646import { Spinner } from "@/components/ui/spinner"
4747import { Button } from "@/components/ui/button"
4848import { toast } from "sonner"
49- import { IconLockCode , IconLogout , IconUserHexagon , IconUpload } from "@tabler/icons-react"
49+ import { IconLockCode , IconLogout , IconMail , IconUserHexagon , IconUpload } from "@tabler/icons-react"
5050import { useCommonData } from "@/components/console/data-provider"
51+ import { isValidEmail } from "@/utils/common"
5152
5253export default function NavUser ( ) {
5354 const { isMobile } = useSidebar ( )
@@ -57,6 +58,9 @@ export default function NavUser() {
5758 const [ newPassword , setNewPassword ] = React . useState < string > ( '' ) ;
5859 const [ confirmPassword , setConfirmPassword ] = React . useState < string > ( '' ) ;
5960 const [ changingPassword , setChangingPassword ] = React . useState < boolean > ( false ) ;
61+ const [ showBindEmailDialog , setShowBindEmailDialog ] = React . useState ( false ) ;
62+ const [ bindEmail , setBindEmail ] = React . useState < string > ( '' ) ;
63+ const [ bindingEmail , setBindingEmail ] = React . useState < boolean > ( false ) ;
6064 const [ showChangeNameDialog , setShowChangeNameDialog ] = React . useState ( false ) ;
6165 const [ newName , setNewName ] = React . useState < string > ( '' ) ;
6266 const [ changingName , setChangingName ] = React . useState < boolean > ( false ) ;
@@ -66,6 +70,8 @@ export default function NavUser() {
6670 const [ changingAvatar , setChangingAvatar ] = React . useState < boolean > ( false ) ;
6771 const navigate = useNavigate ( )
6872 const { user, reloadUser } = useCommonData ( )
73+ const requiresCurrentPassword = ! ! user ?. has_password
74+ const passwordActionLabel = requiresCurrentPassword ? '修改密码' : '设置密码'
6975
7076 const handleLogout = ( ) => {
7177 apiRequest ( 'v1UsersLogoutCreate' , { } , [ ] , ( resp ) => {
@@ -78,23 +84,27 @@ export default function NavUser() {
7884 } ;
7985
8086 const handleChangePassword = async ( ) => {
87+ if ( requiresCurrentPassword && ! currentPassword ) {
88+ toast . error ( '请输入当前密码' ) ;
89+ return ;
90+ }
91+
8192 // 验证新密码和确认密码是否一致
8293 if ( newPassword !== confirmPassword ) {
8394 toast . error ( '新密码和确认密码不一致' ) ;
8495 return ;
8596 }
8697
8798 // 验证密码长度
88- if ( newPassword . length < 6 ) {
89- toast . error ( '新密码长度至少为6位 ' ) ;
99+ if ( newPassword . length < 8 ) {
100+ toast . error ( '新密码长度至少为8位 ' ) ;
90101 return ;
91102 }
92103
93104 setChangingPassword ( true ) ;
94105 await apiRequest ( 'v1UsersPasswordsChangeUpdate' , {
95- current_password : currentPassword ,
106+ current_password : requiresCurrentPassword ? currentPassword : undefined ,
96107 new_password : newPassword ,
97- confirm_password : confirmPassword ,
98108 } , [ ] , ( resp ) => {
99109 if ( resp ?. code === 0 ) {
100110 toast . success ( '密码修改成功' ) ;
@@ -109,6 +119,28 @@ export default function NavUser() {
109119 setChangingPassword ( false ) ;
110120 } ;
111121
122+ const handleBindEmail = async ( ) => {
123+ const email = bindEmail . trim ( ) ;
124+ if ( ! isValidEmail ( email ) ) {
125+ toast . error ( '请输入正确的邮箱地址' ) ;
126+ return ;
127+ }
128+
129+ setBindingEmail ( true ) ;
130+ await apiRequest ( 'v1UsersEmailBindRequestUpdate' , {
131+ email,
132+ } , [ ] , ( resp ) => {
133+ if ( resp ?. code === 0 ) {
134+ toast . success ( '绑定邮件已发送,请前往邮箱完成验证' ) ;
135+ setShowBindEmailDialog ( false ) ;
136+ setBindEmail ( '' ) ;
137+ } else {
138+ toast . error ( `绑定邮箱失败:${ resp ?. message || '未知错误' } ` ) ;
139+ }
140+ } ) ;
141+ setBindingEmail ( false ) ;
142+ } ;
143+
112144 const handleChangeName = async ( ) => {
113145 // 验证昵称不能为空
114146 if ( ! newName . trim ( ) ) {
@@ -233,9 +265,21 @@ export default function NavUser() {
233265 < IconUserHexagon />
234266 修改昵称
235267 </ DropdownMenuItem >
268+ < DropdownMenuItem
269+ disabled = { ! ! user ?. email }
270+ onClick = { ( ) => {
271+ if ( ! user ?. email ) {
272+ setBindEmail ( '' ) ;
273+ setShowBindEmailDialog ( true ) ;
274+ }
275+ } }
276+ >
277+ < IconMail />
278+ 绑定邮箱
279+ </ DropdownMenuItem >
236280 < DropdownMenuItem onClick = { ( ) => setShowChangePasswordDialog ( true ) } >
237281 < IconLockCode />
238- 修改密码
282+ { passwordActionLabel }
239283 </ DropdownMenuItem >
240284 < DropdownMenuSeparator />
241285 < DropdownMenuItem onClick = { ( ) => setShowLogoutDialog ( true ) } >
@@ -303,20 +347,22 @@ export default function NavUser() {
303347 < Dialog open = { showChangePasswordDialog } onOpenChange = { setShowChangePasswordDialog } >
304348 < DialogContent >
305349 < DialogHeader >
306- < DialogTitle > 修改密码 </ DialogTitle >
350+ < DialogTitle > { passwordActionLabel } </ DialogTitle >
307351 </ DialogHeader >
308352 < div className = "space-y-4" >
309- < div className = "space-y-2" >
310- < Label htmlFor = "current-password" > 当前密码</ Label >
311- < Input
312- id = "current-password"
313- type = "password"
314- placeholder = "请输入当前密码"
315- value = { currentPassword }
316- onChange = { ( e ) => setCurrentPassword ( e . target . value ) }
317- autoComplete = "current-password"
318- />
319- </ div >
353+ { requiresCurrentPassword && (
354+ < div className = "space-y-2" >
355+ < Label htmlFor = "current-password" > 当前密码</ Label >
356+ < Input
357+ id = "current-password"
358+ type = "password"
359+ placeholder = "请输入当前密码"
360+ value = { currentPassword }
361+ onChange = { ( e ) => setCurrentPassword ( e . target . value ) }
362+ autoComplete = "current-password"
363+ />
364+ </ div >
365+ ) }
320366 < div className = "space-y-2" >
321367 < Label htmlFor = "new-password" > 新密码</ Label >
322368 < Input
@@ -354,14 +400,52 @@ export default function NavUser() {
354400 </ Button >
355401 < Button
356402 onClick = { handleChangePassword }
357- disabled = { changingPassword || ! currentPassword || ! newPassword || ! confirmPassword }
403+ disabled = { changingPassword || ( requiresCurrentPassword && ! currentPassword ) || ! newPassword || ! confirmPassword }
358404 >
359405 { changingPassword && < Spinner className = "size-4 mr-2" /> }
360406 确认修改
361407 </ Button >
362408 </ DialogFooter >
363409 </ DialogContent >
364410 </ Dialog >
411+ < Dialog open = { showBindEmailDialog } onOpenChange = { setShowBindEmailDialog } >
412+ < DialogContent >
413+ < DialogHeader >
414+ < DialogTitle > 绑定邮箱</ DialogTitle >
415+ </ DialogHeader >
416+ < div className = "space-y-4" >
417+ < div className = "space-y-2" >
418+ < Label htmlFor = "bind-email" > 邮箱</ Label >
419+ < Input
420+ id = "bind-email"
421+ type = "email"
422+ placeholder = "请输入要绑定的邮箱"
423+ value = { bindEmail }
424+ onChange = { ( e ) => setBindEmail ( e . target . value ) }
425+ autoComplete = "email"
426+ />
427+ </ div >
428+ </ div >
429+ < DialogFooter >
430+ < Button
431+ variant = "outline"
432+ onClick = { ( ) => {
433+ setShowBindEmailDialog ( false ) ;
434+ setBindEmail ( '' ) ;
435+ } }
436+ >
437+ 取消
438+ </ Button >
439+ < Button
440+ onClick = { handleBindEmail }
441+ disabled = { bindingEmail || ! bindEmail . trim ( ) }
442+ >
443+ { bindingEmail && < Spinner className = "size-4 mr-2" /> }
444+ 发送验证邮件
445+ </ Button >
446+ </ DialogFooter >
447+ </ DialogContent >
448+ </ Dialog >
365449 < Dialog open = { showChangeAvatarDialog } onOpenChange = { setShowChangeAvatarDialog } >
366450 < DialogContent className = "sm:max-w-md" >
367451 < DialogHeader >
0 commit comments