mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 22:39:36 +08:00
feat: add multi-select mode wrapper for message component (#8653)
* feat: add multi-select mode wrapper for message component * fix: update * update * update * chore: minor updates * fix: add drag threshold
This commit is contained in:
parent
10b7c70a59
commit
0113447481
@ -2,6 +2,7 @@ import { loggerService } from '@logger'
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||||
import { useModel } from '@renderer/hooks/useModel'
|
import { useModel } from '@renderer/hooks/useModel'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@ -38,6 +39,16 @@ interface Props {
|
|||||||
|
|
||||||
const logger = loggerService.withContext('MessageItem')
|
const logger = loggerService.withContext('MessageItem')
|
||||||
|
|
||||||
|
const WrapperContainer = ({
|
||||||
|
isMultiSelectMode,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
isMultiSelectMode: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
return isMultiSelectMode ? <label style={{ cursor: 'pointer' }}>{children}</label> : children
|
||||||
|
}
|
||||||
|
|
||||||
const MessageItem: FC<Props> = ({
|
const MessageItem: FC<Props> = ({
|
||||||
message,
|
message,
|
||||||
topic,
|
topic,
|
||||||
@ -49,6 +60,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
|
const { isMultiSelectMode } = useChatContext(topic)
|
||||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||||
const { messageFont, fontSize, messageStyle } = useSettings()
|
const { messageFont, fontSize, messageStyle } = useSettings()
|
||||||
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
||||||
@ -122,7 +134,15 @@ const MessageItem: FC<Props> = ({
|
|||||||
|
|
||||||
if (message.type === 'clear') {
|
if (message.type === 'clear') {
|
||||||
return (
|
return (
|
||||||
<NewContextMessage className="clear-context-divider" onClick={() => EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)}>
|
<NewContextMessage
|
||||||
|
isMultiSelectMode={isMultiSelectMode}
|
||||||
|
className="clear-context-divider"
|
||||||
|
onClick={() => {
|
||||||
|
if (isMultiSelectMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
|
||||||
|
}}>
|
||||||
<Divider dashed style={{ padding: '0 20px' }} plain>
|
<Divider dashed style={{ padding: '0 20px' }} plain>
|
||||||
{t('chat.message.new.context')}
|
{t('chat.message.new.context')}
|
||||||
</Divider>
|
</Divider>
|
||||||
@ -131,56 +151,64 @@ const MessageItem: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContainer
|
<WrapperContainer isMultiSelectMode={isMultiSelectMode}>
|
||||||
key={message.id}
|
<MessageContainer
|
||||||
className={classNames({
|
key={message.id}
|
||||||
message: true,
|
className={classNames({
|
||||||
'message-assistant': isAssistantMessage,
|
message: true,
|
||||||
'message-user': !isAssistantMessage
|
'message-assistant': isAssistantMessage,
|
||||||
})}
|
'message-user': !isAssistantMessage
|
||||||
ref={messageContainerRef}>
|
})}
|
||||||
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} topic={topic} />
|
ref={messageContainerRef}>
|
||||||
{isEditing && (
|
<MessageHeader
|
||||||
<MessageEditor
|
|
||||||
message={message}
|
message={message}
|
||||||
topicId={topic.id}
|
assistant={assistant}
|
||||||
onSave={handleEditSave}
|
model={model}
|
||||||
onResend={handleEditResend}
|
key={getModelUniqId(model)}
|
||||||
onCancel={handleEditCancel}
|
topic={topic}
|
||||||
/>
|
/>
|
||||||
)}
|
{isEditing && (
|
||||||
{!isEditing && (
|
<MessageEditor
|
||||||
<>
|
message={message}
|
||||||
<MessageContentContainer
|
topicId={topic.id}
|
||||||
className="message-content-container"
|
onSave={handleEditSave}
|
||||||
style={{
|
onResend={handleEditResend}
|
||||||
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
onCancel={handleEditCancel}
|
||||||
fontSize,
|
/>
|
||||||
overflowY: 'visible'
|
)}
|
||||||
}}>
|
{!isEditing && (
|
||||||
<MessageErrorBoundary>
|
<>
|
||||||
<MessageContent message={message} />
|
<MessageContentContainer
|
||||||
</MessageErrorBoundary>
|
className="message-content-container"
|
||||||
</MessageContentContainer>
|
style={{
|
||||||
{showMenubar && (
|
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
||||||
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}>
|
fontSize,
|
||||||
<MessageMenubar
|
overflowY: 'visible'
|
||||||
message={message}
|
}}>
|
||||||
assistant={assistant}
|
<MessageErrorBoundary>
|
||||||
model={model}
|
<MessageContent message={message} />
|
||||||
index={index}
|
</MessageErrorBoundary>
|
||||||
topic={topic}
|
</MessageContentContainer>
|
||||||
isLastMessage={isLastMessage}
|
{showMenubar && (
|
||||||
isAssistantMessage={isAssistantMessage}
|
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}>
|
||||||
isGrouped={isGrouped}
|
<MessageMenubar
|
||||||
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
message={message}
|
||||||
setModel={setModel}
|
assistant={assistant}
|
||||||
/>
|
model={model}
|
||||||
</MessageFooter>
|
index={index}
|
||||||
)}
|
topic={topic}
|
||||||
</>
|
isLastMessage={isLastMessage}
|
||||||
)}
|
isAssistantMessage={isAssistantMessage}
|
||||||
</MessageContainer>
|
isGrouped={isGrouped}
|
||||||
|
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
||||||
|
setModel={setModel}
|
||||||
|
/>
|
||||||
|
</MessageFooter>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MessageContainer>
|
||||||
|
</WrapperContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,9 +260,11 @@ const MessageFooter = styled.div<{ $isLastMessage: boolean; $messageStyle: 'plai
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const NewContextMessage = styled.div`
|
const NewContextMessage = styled.div<{ isMultiSelectMode: boolean }>`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
${({ isMultiSelectMode }) => isMultiSelectMode && 'cursor: default;'}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default memo(MessageItem)
|
export default memo(MessageItem)
|
||||||
|
|||||||
@ -17,9 +17,14 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
|||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||||
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
|
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
|
||||||
|
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||||
|
|
||||||
const dragSelectedIds = useRef<Set<string>>(new Set())
|
const dragSelectedIds = useRef<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// 拖拽阈值,只有移动距离超过这个值才开始框选
|
||||||
|
// 避免触控板点击触发拖拽
|
||||||
|
const DRAG_THRESHOLD = 5
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMultiSelectMode) return
|
if (!isMultiSelectMode) return
|
||||||
|
|
||||||
@ -39,20 +44,30 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
|||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
setIsDragging(true)
|
setIsMouseDown(true)
|
||||||
const pos = updateDragPos(e)
|
const pos = updateDragPos(e)
|
||||||
setDragStart(pos)
|
setDragStart(pos)
|
||||||
setDragCurrent(pos)
|
setDragCurrent(pos)
|
||||||
dragSelectedIds.current.clear()
|
dragSelectedIds.current.clear()
|
||||||
document.body.classList.add('no-select')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isMouseDown) return
|
||||||
|
|
||||||
|
const pos = updateDragPos(e)
|
||||||
|
|
||||||
|
const deltaX = Math.abs(pos.x - dragStart.x)
|
||||||
|
const deltaY = Math.abs(pos.y - dragStart.y)
|
||||||
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||||
|
|
||||||
|
if (!isDragging && distance > DRAG_THRESHOLD) {
|
||||||
|
setIsDragging(true)
|
||||||
|
document.body.classList.add('no-select')
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDragging) return
|
if (!isDragging) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const pos = updateDragPos(e)
|
|
||||||
setDragCurrent(pos)
|
setDragCurrent(pos)
|
||||||
|
|
||||||
// 计算当前框选矩形
|
// 计算当前框选矩形
|
||||||
@ -69,6 +84,9 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
|||||||
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement | null
|
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement | null
|
||||||
const isAlreadySelected = checkbox?.checked || false
|
const isAlreadySelected = checkbox?.checked || false
|
||||||
|
|
||||||
|
// 清除上下文这类消息也会被选中,所以需要跳过
|
||||||
|
if (!checkbox) return
|
||||||
|
|
||||||
// 如果已经被记录为拖动选中,跳过
|
// 如果已经被记录为拖动选中,跳过
|
||||||
if (dragSelectedIds.current.has(id)) return
|
if (dragSelectedIds.current.has(id)) return
|
||||||
|
|
||||||
@ -94,9 +112,11 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
if (!isDragging) return
|
setIsMouseDown(false)
|
||||||
setIsDragging(false)
|
if (isDragging) {
|
||||||
document.body.classList.remove('no-select')
|
setIsDragging(false)
|
||||||
|
document.body.classList.remove('no-select')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = scrollContainerRef.current!
|
const container = scrollContainerRef.current!
|
||||||
@ -110,7 +130,7 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
|||||||
window.removeEventListener('mouseup', handleMouseUp)
|
window.removeEventListener('mouseup', handleMouseUp)
|
||||||
document.body.classList.remove('no-select')
|
document.body.classList.remove('no-select')
|
||||||
}
|
}
|
||||||
}, [isMultiSelectMode, isDragging, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
|
}, [isMultiSelectMode, isDragging, isMouseDown, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
|
||||||
|
|
||||||
if (!isDragging || !isMultiSelectMode) return null
|
if (!isDragging || !isMultiSelectMode) return null
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user