🐛 fix(system): 修复回复功能的一些问题

This commit is contained in:
nongyehong 2024-04-18 23:53:58 +08:00
parent e8164e080e
commit e1eb827b58
4 changed files with 123 additions and 63 deletions

View File

@ -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>

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;