fix: improve multi-select functionality in Messages and SelectionBox components

This commit is contained in:
Pleasurecruise 2025-05-24 13:16:20 +08:00 committed by 亢奋猫
parent 1b6cba454d
commit c4e0744806
3 changed files with 71 additions and 69 deletions

View File

@ -80,7 +80,11 @@ export const useChatContext = (activeTopic: Topic) => {
(messageId: string, selected: boolean) => {
dispatch(
setSelectedMessageIds(
selected ? [...selectedMessageIds, messageId] : selectedMessageIds.filter((id) => id !== messageId)
selected
? selectedMessageIds.includes(messageId)
? selectedMessageIds
: [...selectedMessageIds, messageId]
: selectedMessageIds.filter((id) => id !== messageId)
)
)
},

View File

@ -2,11 +2,13 @@ import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
import Scrollbar from '@renderer/components/Scrollbar'
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
import SelectionBox from '@renderer/pages/home/Messages/SelectionBox'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService'
@ -64,7 +66,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic)
const messagesRef = useRef<Message[]>(messages)
// const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
useEffect(() => {
messagesRef.current = messages
@ -313,13 +315,13 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
</NarrowLayout>
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
{/* TODO: 多选功能实现有问题,需要重新改改 */}
{/* <SelectionBox
<SelectionBox
isMultiSelectMode={isMultiSelectMode}
scrollContainerRef={scrollContainerRef}
messageElements={messageElements.current}
handleSelectMessage={handleSelectMessage}
/> */}
/>
</Container>
)
}

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
interface SelectionBoxProps {
@ -18,6 +18,8 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
const dragSelectedIds = useRef<Set<string>>(new Set())
useEffect(() => {
if (!isMultiSelectMode) return
@ -25,96 +27,90 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
const container = scrollContainerRef.current!
if (!container) return { x: 0, y: 0 }
const rect = container.getBoundingClientRect()
const x = e.clientX - rect.left + container.scrollLeft
const y = e.clientY - rect.top + container.scrollTop
return { x, y }
return {
x: e.clientX - rect.left + container.scrollLeft,
y: e.clientY - rect.top + container.scrollTop
}
}
const handleMouseDown = (e: MouseEvent) => {
if ((e.target as HTMLElement).closest('.ant-checkbox-wrapper')) return
if ((e.target as HTMLElement).closest('.MessageFooter')) return
e.preventDefault()
setIsDragging(true)
const pos = updateDragPos(e)
setDragStart(pos)
setDragCurrent(pos)
dragSelectedIds.current.clear()
document.body.classList.add('no-select')
}
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return
setDragCurrent(updateDragPos(e))
const container = scrollContainerRef.current!
if (container) {
const { top, bottom } = container.getBoundingClientRect()
const scrollSpeed = 15
if (e.clientY < top + 50) {
container.scrollBy(0, -scrollSpeed)
} else if (e.clientY > bottom - 50) {
container.scrollBy(0, scrollSpeed)
e.preventDefault()
const pos = updateDragPos(e)
setDragCurrent(pos)
// 计算当前框选矩形
const left = Math.min(dragStart.x, pos.x)
const right = Math.max(dragStart.x, pos.x)
const top = Math.min(dragStart.y, pos.y)
const bottom = Math.max(dragStart.y, pos.y)
// 创建新选中的消息ID集合
const newSelectedIds = new Set<string>()
messageElements.forEach((el, id) => {
// 检查消息是否已被选中(不管是拖动选中还是手动选中)
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement | null
const isAlreadySelected = checkbox?.checked || false
// 如果已经被记录为拖动选中,跳过
if (dragSelectedIds.current.has(id)) return
const rect = el.getBoundingClientRect()
const container = scrollContainerRef.current!
const eTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
const eLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
const eBottom = eTop + rect.height
const eRight = eLeft + rect.width
// 检查消息是否在当前选择框内
const isInSelectionBox = !(eRight < left || eLeft > right || eBottom < top || eTop > bottom)
// 只有在选择框内且未被选中的消息才需要处理
if (isInSelectionBox && !isAlreadySelected) {
handleSelectMessage(id, true)
dragSelectedIds.current.add(id)
newSelectedIds.add(id)
el.classList.add('selection-highlight')
setTimeout(() => el.classList.remove('selection-highlight'), 300)
}
}
})
}
const handleMouseUp = () => {
if (!isDragging) return
const left = Math.min(dragStart.x, dragCurrent.x)
const right = Math.max(dragStart.x, dragCurrent.x)
const top = Math.min(dragStart.y, dragCurrent.y)
const bottom = Math.max(dragStart.y, dragCurrent.y)
const MIN_SELECTION_SIZE = 5
const isValidSelection =
Math.abs(right - left) > MIN_SELECTION_SIZE && Math.abs(bottom - top) > MIN_SELECTION_SIZE
if (isValidSelection) {
messageElements.forEach((element, messageId) => {
try {
const rect = element.getBoundingClientRect()
const container = scrollContainerRef.current!
const elementTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
const elementLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
const elementBottom = elementTop + rect.height
const elementRight = elementLeft + rect.width
const isIntersecting = !(
elementRight < left ||
elementLeft > right ||
elementBottom < top ||
elementTop > bottom
)
if (isIntersecting) {
handleSelectMessage(messageId, true)
element.classList.add('selection-highlight')
setTimeout(() => element.classList.remove('selection-highlight'), 300)
}
} catch (error) {
console.error('Error calculating element intersection:', error)
}
})
}
setIsDragging(false)
document.body.classList.remove('no-select')
}
const container = scrollContainerRef.current!
if (container) {
container.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
}
container?.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
return () => {
if (container) {
container.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
document.body.classList.remove('no-select')
}
container?.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
document.body.classList.remove('no-select')
}
}, [isMultiSelectMode, isDragging, dragStart, dragCurrent, handleSelectMessage, scrollContainerRef, messageElements])
}, [isMultiSelectMode, isDragging, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
if (!isDragging || !isMultiSelectMode) return null