fix(web): refine account avatar interactions (#36111)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-05-13 16:36:04 +08:00 committed by GitHub
parent 13c00ecfc4
commit 5edc682c4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 30 additions and 41 deletions

View File

@ -1151,7 +1151,7 @@
},
"web/app/components/base/icons/src/vender/line/general/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 12
"count": 11
}
},
"web/app/components/base/icons/src/vender/line/images/index.ts": {

View File

@ -8,7 +8,6 @@ import { Avatar } from '@langgenius/dify-ui/avatar'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -29,9 +28,9 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false)
const [uploading, setUploading] = useState(false)
const [isShowDeleteConfirm, setIsShowDeleteConfirm] = useState(false)
const [hoverArea, setHoverArea] = useState<string>('left')
const [onAvatarError, setOnAvatarError] = useState(false)
const canDeleteAvatar = !!props.avatar && !onAvatarError
const handleImageInput: OnImageInput = useCallback(async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
setInputImageInfo(
@ -65,11 +64,16 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
}
}, [onSave, t])
const handleDeleteAvatarClick = useCallback(() => {
setIsShowAvatarPicker(false)
setIsShowDeleteConfirm(true)
}, [])
const { handleLocalFileUpload } = useLocalFileUploader({
limit: 3,
disabled: false,
onUpload: (imageFile: ImageFile) => {
if (imageFile.progress === 100) {
if (imageFile.progress === 100 && imageFile.fileId) {
setUploading(false)
setInputImageInfo(undefined)
handleSaveAvatar(imageFile.fileId)
@ -100,36 +104,17 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
return (
<>
<div>
<div className="group relative">
<button
type="button"
aria-label={t('avatar.editAction', { ns: 'common' })}
className="group relative inline-flex overflow-hidden rounded-full border-none bg-transparent p-0 outline-hidden hover:opacity-90 focus-visible:ring-2 focus-visible:ring-components-input-border-hover active:opacity-80"
onClick={() => setIsShowAvatarPicker(true)}
>
<Avatar {...props} onLoadingStatusChange={status => setOnAvatarError(status === 'error')} />
<div
className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
onClick={() => {
if (hoverArea === 'right' && !onAvatarError)
setIsShowDeleteConfirm(true)
else
setIsShowAvatarPicker(true)
}}
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const isRight = x > rect.width / 2
setHoverArea(isRight ? 'right' : 'left')
}}
>
{hoverArea === 'right' && !onAvatarError
? (
<span className="text-xs text-white">
<RiDeleteBin5Line />
</span>
)
: (
<span className="text-xs text-white">
<RiPencilLine />
</span>
)}
</div>
</div>
<span className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-full bg-black/50 text-white opacity-0 group-hover:opacity-100 motion-safe:transition-opacity">
<span aria-hidden="true" className="i-ri-pencil-line size-5" />
</span>
</button>
</div>
<Dialog open={isShowAvatarPicker} onOpenChange={open => !open && setIsShowAvatarPicker(false)}>
@ -138,11 +123,16 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
<Divider className="m-0" />
<div className="flex w-full items-center justify-center gap-2 p-3">
<Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
{canDeleteAvatar && (
<Button tone="destructive" className="shrink-0" onClick={handleDeleteAvatarClick}>
{t('operation.delete', { ns: 'common' })}
</Button>
)}
<Button className="min-w-0 flex-1" onClick={() => setIsShowAvatarPicker(false)}>
{t('iconPicker.cancel', { ns: 'app' })}
</Button>
<Button variant="primary" className="w-full" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
<Button variant="primary" className="min-w-0 flex-1" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
{t('iconPicker.ok', { ns: 'app' })}
</Button>
</div>

View File

@ -10,7 +10,6 @@ import {
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
import PremiumBadge from '@/app/components/base/premium-badge'
import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'
@ -44,8 +43,8 @@ export default function AppSelector() {
<DropdownMenuTrigger
aria-label={userProfile.name}
className={cn(
'inline-flex items-center rounded-[20px] text-sm text-text-primary outline-hidden mobile:px-1',
'hover:bg-components-panel-bg-blur focus-visible:bg-components-panel-bg-blur focus-visible:ring-1 focus-visible:ring-components-input-border-hover data-popup-open:bg-components-panel-bg-blur',
'inline-flex size-8 items-center justify-center rounded-full border-none bg-transparent p-0 text-sm text-text-primary outline-hidden',
'hover:opacity-80 focus-visible:ring-1 focus-visible:ring-components-input-border-hover active:opacity-70 data-popup-open:opacity-80',
)}
>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} />
@ -77,7 +76,7 @@ export default function AppSelector() {
className="h-9 justify-start px-3"
onClick={handleLogout}
>
<LogOut01 className="mr-1 flex h-4 w-4 text-text-tertiary" />
<span aria-hidden="true" className="mr-1 i-custom-vender-line-general-log-out-01 flex size-4 text-text-tertiary" />
<span className="text-[14px] font-normal text-text-secondary">{t('userProfile.logout', { ns: 'common' })}</span>
</DropdownMenuItem>
</div>

View File

@ -5,8 +5,6 @@ export { default as CodeAssistant } from './CodeAssistant'
export { default as Link03 } from './Link03'
export { default as LinkExternal02 } from './LinkExternal02'
export { default as LogOut01 } from './LogOut01'
export { default as MagicEdit } from './MagicEdit'
export { default as Pin02 } from './Pin02'

View File

@ -101,6 +101,7 @@
"appModes.completionApp": "Text Generator",
"avatar.deleteDescription": "Are you sure you want to remove your profile picture? Your account will use the default initial avatar.",
"avatar.deleteTitle": "Remove Avatar",
"avatar.editAction": "Edit Avatar",
"chat.citation.characters": "Characters:",
"chat.citation.hitCount": "Retrieval count:",
"chat.citation.hitScore": "Retrieval Score:",

View File

@ -101,6 +101,7 @@
"appModes.completionApp": "文本生成型应用",
"avatar.deleteDescription": "确定要删除你的个人头像吗?你的账号将使用默认的首字母头像。",
"avatar.deleteTitle": "删除头像",
"avatar.editAction": "编辑头像",
"chat.citation.characters": "字符:",
"chat.citation.hitCount": "召回次数:",
"chat.citation.hitScore": "召回得分:",