feat: prevent duplicate @ insertion in mention input with visual feedback

This commit is contained in:
lyzno1 2025-10-11 13:46:18 +08:00
parent 6af1fea232
commit e9cdc96c74
No known key found for this signature in database

View File

@ -277,6 +277,19 @@ export const MentionInput: FC<MentionInputProps> = memo(({
) )
}, [mentionUsers, mentionQuery]) }, [mentionUsers, mentionQuery])
const shouldDisableMentionButton = useMemo(() => {
if (showMentionDropdown)
return true
const textarea = textareaRef.current
if (!textarea)
return false
const cursorPosition = textarea.selectionStart || 0
const textBeforeCursor = value.slice(0, cursorPosition)
return /@\w*$/.test(textBeforeCursor)
}, [showMentionDropdown, value])
const dropdownPosition = useMemo(() => { const dropdownPosition = useMemo(() => {
if (!showMentionDropdown || !textareaRef.current) if (!showMentionDropdown || !textareaRef.current)
return { x: 0, y: 0, placement: 'bottom' as const } return { x: 0, y: 0, placement: 'bottom' as const }
@ -328,9 +341,20 @@ export const MentionInput: FC<MentionInputProps> = memo(({
e.stopPropagation() e.stopPropagation()
const textarea = textareaRef.current const textarea = textareaRef.current
if (!textarea) return if (!textarea)
return
const cursorPosition = textarea.selectionStart || 0 const cursorPosition = textarea.selectionStart || 0
const textBeforeCursor = value.slice(0, cursorPosition)
// 🔒 如果已经在 mention 模式,不插入新的 @
if (showMentionDropdown)
return
// 🔒 如果光标前已有未完成的 @,不插入新的 @
if (/@\w*$/.test(textBeforeCursor))
return
const newContent = `${value.slice(0, cursorPosition)}@${value.slice(cursorPosition)}` const newContent = `${value.slice(0, cursorPosition)}@${value.slice(cursorPosition)}`
onChange(newContent) onChange(newContent)
@ -352,7 +376,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
}) })
} }
}, 0) }, 0)
}, [value, onChange, evaluateContentLayout, syncHighlightScroll]) }, [value, onChange, evaluateContentLayout, syncHighlightScroll, showMentionDropdown])
const insertMention = useCallback((user: UserProfile) => { const insertMention = useCallback((user: UserProfile) => {
const textarea = textareaRef.current const textarea = textareaRef.current
@ -500,8 +524,13 @@ export const MentionInput: FC<MentionInputProps> = memo(({
className="absolute bottom-0 right-1 z-20 flex items-end gap-1" className="absolute bottom-0 right-1 z-20 flex items-end gap-1"
> >
<div <div
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover" className={cn(
onClick={handleMentionButtonClick} 'z-20 flex h-8 w-8 items-center justify-center rounded-lg transition-opacity',
shouldDisableMentionButton
? 'cursor-not-allowed opacity-40'
: 'cursor-pointer hover:bg-state-base-hover',
)}
onClick={shouldDisableMentionButton ? undefined : handleMentionButtonClick}
> >
<RiAtLine className="h-4 w-4 text-text-tertiary" /> <RiAtLine className="h-4 w-4 text-text-tertiary" />
</div> </div>
@ -522,8 +551,13 @@ export const MentionInput: FC<MentionInputProps> = memo(({
className="absolute bottom-0 left-1 right-1 z-20 flex items-end justify-between" className="absolute bottom-0 left-1 right-1 z-20 flex items-end justify-between"
> >
<div <div
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover" className={cn(
onClick={handleMentionButtonClick} 'z-20 flex h-8 w-8 items-center justify-center rounded-lg transition-opacity',
shouldDisableMentionButton
? 'cursor-not-allowed opacity-40'
: 'cursor-pointer hover:bg-state-base-hover',
)}
onClick={shouldDisableMentionButton ? undefined : handleMentionButtonClick}
> >
<RiAtLine className="h-4 w-4 text-text-tertiary" /> <RiAtLine className="h-4 w-4 text-text-tertiary" />
</div> </div>