mirror of
https://gitee.com/HuLaSpark/HuLa.git
synced 2024-11-29 18:28:30 +08:00
🐛 fix(system): 修复回复功能的一些问题
This commit is contained in:
parent
e8164e080e
commit
e1eb827b58
@ -9,14 +9,14 @@
|
||||
class="relative h-100vh"
|
||||
item-resizable
|
||||
:padding-top="10"
|
||||
:item-size="activeItem.type === RoomTypeEnum.GROUP ? 98 : 70"
|
||||
:item-size="itemSize"
|
||||
:items="items">
|
||||
<template #default="{ item }">
|
||||
<main
|
||||
:key="item.key"
|
||||
class="item-main"
|
||||
class="flex-y-center min-h-58px"
|
||||
:class="[
|
||||
[activeItem.type === RoomTypeEnum.GROUP ? 'p-[18px_20px]' : 'chat-single p-[2px_20px_10px_20px]'],
|
||||
[activeItem.type === RoomTypeEnum.GROUP ? 'p-[18px_20px]' : 'chat-single p-[4px_20px_10px_20px]'],
|
||||
{ 'active-reply': activeReply === item.key }
|
||||
]">
|
||||
<!-- 好友或者群聊的信息 -->
|
||||
@ -25,10 +25,7 @@
|
||||
:class="{
|
||||
'items-end': item.accountId === userId
|
||||
}">
|
||||
<div
|
||||
class="flex items-start flex-1"
|
||||
:class="item.accountId === userId ? 'flex-row-reverse' : ''"
|
||||
style="max-width: calc(100% - 54px)">
|
||||
<div class="flex items-start flex-1" :class="item.accountId === userId ? 'flex-row-reverse' : ''">
|
||||
<!-- 回复消息提示的箭头 -->
|
||||
<svg
|
||||
v-if="activeReply === item.key"
|
||||
@ -67,19 +64,23 @@
|
||||
<!-- 用户个人信息框 -->
|
||||
<InfoPopover :info="activeItemRef" />
|
||||
</n-popover>
|
||||
<div
|
||||
class="flex flex-col gap-8px color-[--text-color] flex-1"
|
||||
<n-flex
|
||||
vertical
|
||||
justify="center"
|
||||
:size="8"
|
||||
class="color-[--text-color] flex-1"
|
||||
:class="item.accountId === userId ? 'items-end mr-10px' : ''">
|
||||
<ContextMenu
|
||||
@select="$event.click(item)"
|
||||
:menu="activeItem.type === RoomTypeEnum.GROUP ? optionsList : []"
|
||||
:special-menu="report">
|
||||
<span class="text-12px select-none color-#909090" v-if="activeItem.type === RoomTypeEnum.GROUP">
|
||||
{{ item.accountId === userId ? item.value : activeItem.accountName }}
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</ContextMenu>
|
||||
<!-- 气泡样式 -->
|
||||
<ContextMenu
|
||||
class="size-fit"
|
||||
:data-key="item.accountId === userId ? `U${item.key}` : `Q${item.key}`"
|
||||
@select="$event.click(item)"
|
||||
:menu="handleItemType(item.type)"
|
||||
@ -158,7 +159,7 @@
|
||||
{{ item.reply.content }}
|
||||
</span>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
@ -222,6 +223,7 @@ import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useChatMain } from '@/hooks/useChatMain.ts'
|
||||
import { VirtualListInst } from 'naive-ui'
|
||||
import { delay } from 'lodash-es'
|
||||
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem: MockItem
|
||||
@ -232,6 +234,8 @@ const { createWebviewWindow } = useWindow()
|
||||
const selectKey = ref()
|
||||
/* 跳转回复消息后选中效果 */
|
||||
const activeReply = ref(-1)
|
||||
/* item最小高度,用于计算滚动大小和位置 */
|
||||
const itemSize = computed(() => (activeItem.type === RoomTypeEnum.GROUP ? 98 : 70))
|
||||
/* 虚拟列表 */
|
||||
const virtualListInst = ref<VirtualListInst>()
|
||||
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main')
|
||||
@ -326,7 +330,7 @@ const addToDomUpdateQueue = (index: number, id: number) => {
|
||||
// 使用 nextTick 确保虚拟列表渲染完最新的项目后进行滚动
|
||||
nextTick(() => {
|
||||
if (!floatFooter.value || id === userId.value) {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom' })
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', debounce: true })
|
||||
}
|
||||
/* data-key标识的气泡,添加前缀用于区分用户消息,不然气泡动画会被覆盖 */
|
||||
const dataKey = id === userId.value ? `U${index + 1}` : `Q${index + 1}`
|
||||
@ -347,7 +351,7 @@ const addToDomUpdateQueue = (index: number, id: number) => {
|
||||
/* 点击后滚动到底部 */
|
||||
const scrollBottom = () => {
|
||||
nextTick(() => {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant', debounce: true })
|
||||
})
|
||||
}
|
||||
|
||||
@ -356,7 +360,19 @@ const closeMenu = (event: any) => {
|
||||
activeBubble.value = -1
|
||||
}
|
||||
if (!event.target.matches('.active-reply')) {
|
||||
activeReply.value = -1
|
||||
/* 解决更替交换回复气泡时候没有触发动画的问题 */
|
||||
if (!event.target.matches('.reply-bubble *')) {
|
||||
nextTick(() => {
|
||||
const activeReplyElement = document.querySelector('.active-reply') as HTMLElement
|
||||
if (activeReplyElement) {
|
||||
activeReplyElement.classList.add('reply-exit')
|
||||
delay(() => {
|
||||
activeReplyElement.classList.remove('reply-exit')
|
||||
activeReply.value = -1
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +406,8 @@ onMounted(() => {
|
||||
// accountId: activeItem.accountId,
|
||||
// avatar: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
|
||||
// content: '123',
|
||||
// type: MsgEnum.TEXT
|
||||
// type: MsgEnum.TEXT,
|
||||
// reply: null
|
||||
// }
|
||||
// items.value.push(message)
|
||||
// addToDomUpdateQueue(index - 1, activeItem.accountId)
|
||||
@ -404,35 +421,4 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/scss/chat-main';
|
||||
.item-main {
|
||||
@apply flex-y-center min-h-58px;
|
||||
transition: all 0.4s ease;
|
||||
.reply-bubble {
|
||||
@apply text-12px text-[--reply-color] bg-[--bg-reply-bubble] rounded-8px p-4px cursor-pointer select-none;
|
||||
svg,
|
||||
span {
|
||||
transition: color 0.4s ease-in-out;
|
||||
}
|
||||
&:hover {
|
||||
svg {
|
||||
color: #13987f;
|
||||
}
|
||||
span {
|
||||
color: var(--reply-hover);
|
||||
}
|
||||
}
|
||||
.content-span {
|
||||
width: fit-content;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
.active-reply {
|
||||
background-color: var(--bg-reply-active);
|
||||
border-radius: 8px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
@ -4,6 +4,12 @@ import { createFileOrVideoDom } from '@/utils/CreateDom.ts'
|
||||
|
||||
/** 常用工具类 */
|
||||
export const useCommon = () => {
|
||||
/* 回复消息 */
|
||||
const reply = ref({
|
||||
accountName: '',
|
||||
content: '',
|
||||
key: ''
|
||||
})
|
||||
/** 获取当前光标选取的信息(需要判断是否为空) */
|
||||
|
||||
const getEditorRange = () => {
|
||||
@ -109,7 +115,6 @@ export const useCommon = () => {
|
||||
const divNode = document.createElement('div')
|
||||
divNode.id = 'replyDiv' // 设置id为replyDiv
|
||||
divNode.contentEditable = 'false' // 设置为不可编辑
|
||||
// 设置div标签的样式,设置文本的长度为当前的最大宽度,超过后省略号显示
|
||||
divNode.style.cssText = `
|
||||
background-color: rgba(204, 204, 204, 0.4);
|
||||
font-size: 12px;
|
||||
@ -145,9 +150,25 @@ export const useCommon = () => {
|
||||
padding: 2px;
|
||||
min-width: 0;
|
||||
`
|
||||
// 把正文放到span标签中,并设置span标签的样式
|
||||
const contentSpan = document.createElement('span')
|
||||
contentSpan.style.cssText = `
|
||||
let contentBox
|
||||
// 判断content内容开头是否是data:image/的是图片
|
||||
if (content.startsWith('data:image/')) {
|
||||
// 再创建一个img标签节点,并设置src属性为base64编码的图片
|
||||
contentBox = document.createElement('img')
|
||||
contentBox.src = content
|
||||
contentBox.style.cssText = `
|
||||
max-width: 55px;
|
||||
max-height: 55px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
`
|
||||
// 将img标签节点插入到div标签节点中
|
||||
divNode.appendChild(contentBox)
|
||||
} else {
|
||||
// 把正文放到span标签中,并设置span标签的样式
|
||||
contentBox = document.createElement('span')
|
||||
contentBox.style.cssText = `
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
cursor: default;
|
||||
@ -157,7 +178,8 @@ export const useCommon = () => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
contentSpan.appendChild(document.createTextNode(content))
|
||||
contentBox.appendChild(document.createTextNode(content))
|
||||
}
|
||||
// 在回复信息的右边添加一个关闭信息的按钮
|
||||
const closeBtn = document.createElement('span')
|
||||
closeBtn.style.cssText = `
|
||||
@ -181,17 +203,25 @@ export const useCommon = () => {
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
triggerInputEvent(messageInput)
|
||||
reply.value = { accountName: '', content: '', key: '' }
|
||||
})
|
||||
// 将头部和正文节点插入到div标签节点中
|
||||
divNode.appendChild(headerNode)
|
||||
divNode.appendChild(contentNode)
|
||||
contentNode.appendChild(contentSpan)
|
||||
contentNode.appendChild(contentBox)
|
||||
contentNode.appendChild(closeBtn)
|
||||
// 将div标签节点插入到光标位置
|
||||
range?.insertNode(divNode)
|
||||
// 将光标折叠到Range的末尾(true表示折叠到Range的开始位置,false表示折叠到Range的末尾)
|
||||
range?.collapse(false)
|
||||
const spaceNode = document.createTextNode('\u00A0')
|
||||
// 创建一个span节点作为空格
|
||||
const spaceNode = document.createElement('span')
|
||||
spaceNode.textContent = '\u00A0'
|
||||
// 设置不可编辑
|
||||
spaceNode.contentEditable = 'false'
|
||||
// 不可以选中
|
||||
spaceNode.style.userSelect = 'none'
|
||||
// 将空格节点插入到光标位置
|
||||
range?.insertNode(spaceNode)
|
||||
range?.collapse(false)
|
||||
} else {
|
||||
@ -293,6 +323,7 @@ export const useCommon = () => {
|
||||
triggerInputEvent,
|
||||
handlePaste,
|
||||
removeTag,
|
||||
FileOrVideoPaste
|
||||
FileOrVideoPaste,
|
||||
reply
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ import { MockList } from '@/mock'
|
||||
import { useCommon } from './useCommon.ts'
|
||||
|
||||
export const useMsgInput = (messageInputDom: Ref) => {
|
||||
const { triggerInputEvent, insertNode, getMessageContentType, getEditorRange, imgPaste, removeTag } = useCommon()
|
||||
const { triggerInputEvent, insertNode, getMessageContentType, getEditorRange, imgPaste, removeTag, reply } =
|
||||
useCommon()
|
||||
const settingStore = setting()
|
||||
const { chat } = storeToRefs(settingStore)
|
||||
const chatKey = ref(chat.value.sendKey)
|
||||
@ -31,12 +32,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
})
|
||||
/* 记录当前选中的提及项 key */
|
||||
const selectedAitKey = ref(filteredList.value[0]?.key ?? null)
|
||||
/* 回复消息 */
|
||||
const reply = ref({
|
||||
accountName: '',
|
||||
content: '',
|
||||
key: ''
|
||||
})
|
||||
/* 右键菜单列表 */
|
||||
const menuList = ref([
|
||||
{ label: '剪切', icon: 'screenshot', disabled: true },
|
||||
@ -73,6 +68,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
if (!ait.value && filteredList.value.length > 0) {
|
||||
selectedAitKey.value = 0
|
||||
}
|
||||
// 如果输入框没有值就把回复内容清空
|
||||
if (msgInput.value === '') {
|
||||
reply.value = { accountName: '', content: '', key: '' }
|
||||
}
|
||||
})
|
||||
|
||||
watch(chatKey, (v) => {
|
||||
@ -91,6 +90,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
})
|
||||
/* 监听回复信息的传递 */
|
||||
Mitt.on(MittEnum.REPLY_MEG, (event: any) => {
|
||||
if (reply.value.content) {
|
||||
// TODO 如果已经有就替换原来的内容 (nyh -> 2024-04-18 23:10:56)
|
||||
return
|
||||
}
|
||||
reply.value = { accountName: event.value, content: event.content, key: event.key }
|
||||
if (messageInputDom.value) {
|
||||
nextTick().then(() => {
|
||||
@ -143,6 +146,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
Mitt.emit(MittEnum.SEND_MESSAGE, msg)
|
||||
msgInput.value = ''
|
||||
messageInputDom.value.innerHTML = ''
|
||||
reply.value = { accountName: '', content: '', key: '' }
|
||||
}
|
||||
|
||||
/* 当输入框手动输入值的时候触发input事件(使用vueUse的防抖) */
|
||||
@ -239,6 +243,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgInput,
|
||||
chatKey,
|
||||
menuList,
|
||||
selectedAitKey
|
||||
selectedAitKey,
|
||||
reply
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* 气泡样式 */
|
||||
@mixin bubble {
|
||||
@apply w-fit max-w-55vw min-h-1em p-[8px_12px] text-15px line-height-22px bg-[--bg-bubble] rounded-[2px_18px_18px];
|
||||
@apply w-fit max-w-35vw min-h-1em p-[8px_12px] text-15px line-height-22px bg-[--bg-bubble] rounded-[2px_18px_18px];
|
||||
word-break: break-all; /* 强制连续文本换行 */
|
||||
&.active {
|
||||
background-color: var(--bg-bubble-active);
|
||||
@ -46,9 +46,47 @@
|
||||
background-color: rgb(195, 221, 216);
|
||||
}
|
||||
}
|
||||
/* 浮标达到最大值时候的样式 */
|
||||
.max:hover {
|
||||
background-color: #f5dce1;
|
||||
}
|
||||
/* 回复气泡的样式 */
|
||||
.reply-bubble {
|
||||
@apply text-12px text-[--reply-color] bg-[--bg-reply-bubble] rounded-8px p-4px cursor-pointer select-none;
|
||||
svg,
|
||||
span {
|
||||
transition: color 0.4s ease-in-out;
|
||||
}
|
||||
&:hover {
|
||||
svg {
|
||||
color: #13987f;
|
||||
}
|
||||
span {
|
||||
color: var(--reply-hover);
|
||||
}
|
||||
}
|
||||
.content-span {
|
||||
width: fit-content;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
/* 跳转到回复内容时候显示的样式 */
|
||||
.active-reply {
|
||||
background-color: var(--bg-reply-active);
|
||||
border-radius: 8px;
|
||||
margin: 0 8px;
|
||||
transition: all 0.4s ease-in-out;
|
||||
}
|
||||
/* 跳转到回复内容时候的退出动画 */
|
||||
.reply-exit {
|
||||
margin: 0;
|
||||
background-color: inherit;
|
||||
border-radius: 0;
|
||||
transition: all 0.4s ease-in-out;
|
||||
}
|
||||
/*! 替换ait的字体颜色 */
|
||||
:deep(#aitSpan) {
|
||||
@apply text-inherit cursor-pointer;
|
||||
|
Loading…
Reference in New Issue
Block a user