perf(system): 优化回复功能的实现

支持对图片和其他类型消息回复
This commit is contained in:
nongyehong 2024-04-19 04:27:57 +08:00
parent e1eb827b58
commit c5109d7441
8 changed files with 89 additions and 42 deletions

View File

@ -91,7 +91,7 @@
<script setup lang="ts">
import { useFileDialog } from '@vueuse/core'
import { MsgEnum } from '@/enums'
import { LimitEnum, MsgEnum } from '@/enums'
import { useCommon } from '@/hooks/useCommon.ts'
const { open, onChange } = useFileDialog()
@ -116,8 +116,8 @@ const emojiHandle = (item: string) => {
onChange((files) => {
if (!files) return
if (files.length > 5) {
window.$message.warning('一次性只能上传5个文件')
if (files.length > LimitEnum.COM_COUNT) {
window.$message.warning(`一次性只能上传${LimitEnum.COM_COUNT}个文件或图片`)
return
}
for (let file of files) {

View File

@ -88,9 +88,9 @@
@click="handleMsgClick(item)">
<!-- &lt;!&ndash; 渲染消息内容体 &ndash;&gt;-->
<!-- <RenderMessage :message="message" />-->
<!-- 消息为文本类型 -->
<!-- 消息为文本类型或者回复消息 -->
<div
v-if="item.type === MsgEnum.TEXT"
v-if="item.type === MsgEnum.TEXT || item.type === MsgEnum.REPLY"
style="white-space: pre-wrap"
:class="[
{ active: activeBubble === item.key },
@ -133,17 +133,6 @@
preview-disabled
style="border-radius: 8px"
:src="item.content"></n-image>
<!-- 消息为回复消息 -->
<div
v-if="item.type === MsgEnum.REPLY"
style="white-space: pre-wrap"
:class="[
{ active: activeBubble === item.key },
item.accountId === userId ? 'bubble-oneself' : 'bubble'
]">
<span v-html="item.content"></span>
</div>
</ContextMenu>
<!-- 回复的内容 -->
@ -152,12 +141,26 @@
:size="6"
v-if="item.reply && item.type === MsgEnum.REPLY"
@click="jumpToReplyMsg(item.reply.key)"
class="reply-bubble">
class="reply-bubble relative">
<svg class="size-14px"><use href="#to-top"></use></svg>
<span>{{ `${item.reply.accountName}` }}</span>
<span class="content-span">
{{ item.reply.content }}
<!-- 当回复消息为图片时渲染 -->
<n-image
v-if="item.reply.content.startsWith('data:image/')"
:img-props="{ style: { maxWidth: '50px', maxHeight: '50px' } }"
show-toolbar-tooltip
style="border-radius: 4px"
@click.stop
:fallback-src="'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'"
:src="item.reply.content" />
<!-- 当回复消息为文本时渲染(判断是否有aitSpan标签) -->
<span v-else class="content-span">
{{ handleReply(item.reply.content) }}
</span>
<!-- 多个图片时计数器样式 -->
<div v-if="item.reply.imgCount" class="reply-img-sub">
{{ item.reply.imgCount }}
</div>
</n-flex>
</n-flex>
</div>
@ -224,6 +227,7 @@ import { listen } from '@tauri-apps/api/event'
import { useChatMain } from '@/hooks/useChatMain.ts'
import { VirtualListInst } from 'naive-ui'
import { delay } from 'lodash-es'
import { useCommon } from '@/hooks/useCommon.ts'
const { activeItem } = defineProps<{
activeItem: MockItem
@ -239,6 +243,7 @@ const itemSize = computed(() => (activeItem.type === RoomTypeEnum.GROUP ? 98 : 7
/* 虚拟列表 */
const virtualListInst = ref<VirtualListInst>()
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main')
const { removeTag } = useCommon()
const {
handleScroll,
handleMsgClick,
@ -284,6 +289,11 @@ watchEffect(() => {
activeItemRef.value = { ...activeItem }
})
/* 处理回复消息中的 AIT 标签 */
const handleReply = (content: string) => {
return content.includes('id="aitSpan"') ? removeTag(content) : content
}
/* 发送信息 */
const handleSendMessage = (msg: any) => {
nextTick(() => {

View File

@ -123,3 +123,9 @@ export enum CloseBxEnum {
/** 关闭 */
CLOSE = 'close'
}
/** 限制上传 */
export enum LimitEnum {
/** 通用限制数量 */
COM_COUNT = 5
}

View File

@ -1,4 +1,4 @@
import { MsgEnum } from '@/enums'
import { LimitEnum, MsgEnum } from '@/enums'
import { Ref } from 'vue'
import { createFileOrVideoDom } from '@/utils/CreateDom.ts'
@ -8,7 +8,8 @@ export const useCommon = () => {
const reply = ref({
accountName: '',
content: '',
key: ''
key: '',
imgCount: 0
})
/** 获取当前光标选取的信息(需要判断是否为空) */
@ -128,7 +129,7 @@ export const useCommon = () => {
`
// 把dom中的value值作为回复信息的作者dom中的content作为回复信息的内容
const author = dom.accountName + ''
const content = dom.content
let content = dom.content
// 创建一个div标签节点作为回复信息的头部
const headerNode = document.createElement('div')
headerNode.style.cssText = `
@ -151,6 +152,14 @@ export const useCommon = () => {
min-width: 0;
`
let contentBox
// 判断content内容是否是data:image/开头的数组
if (Array.isArray(content)) {
// 获取总共有多少张图片
const imageCount = content.length
// 获取第一个data:image/开头的图片
content = content.find((item: string) => item.startsWith('data:image/'))
reply.value.imgCount = imageCount
}
// 判断content内容开头是否是data:image/的是图片
if (content.startsWith('data:image/')) {
// 再创建一个img标签节点并设置src属性为base64编码的图片
@ -165,7 +174,14 @@ export const useCommon = () => {
`
// 将img标签节点插入到div标签节点中
divNode.appendChild(contentBox)
// 把图片传入到reply的content属性中
reply.value.content = content
} else {
// 判断是否有@标签
if (content.includes('id="aitSpan"')) {
// 去掉content中的标签
content = removeTag(content)
}
// 把正文放到span标签中并设置span标签的样式
contentBox = document.createElement('span')
contentBox.style.cssText = `
@ -183,6 +199,8 @@ export const useCommon = () => {
// 在回复信息的右边添加一个关闭信息的按钮
const closeBtn = document.createElement('span')
closeBtn.style.cssText = `
display: flex;
align-items: center;
font-size: 12px;
color: #999;
cursor: pointer;
@ -203,7 +221,7 @@ export const useCommon = () => {
selection?.removeAllRanges()
selection?.addRange(range)
triggerInputEvent(messageInput)
reply.value = { accountName: '', content: '', key: '' }
reply.value = { imgCount: 0, accountName: '', content: '', key: '' }
})
// 将头部和正文节点插入到div标签节点中
divNode.appendChild(headerNode)
@ -281,8 +299,8 @@ export const useCommon = () => {
const handlePaste = (e: any, dom: HTMLElement) => {
e.preventDefault()
if (e.clipboardData.files.length > 0) {
if (e.clipboardData.files.length > 5) {
window.$message.warning('一次性只能上传5个文件')
if (e.clipboardData.files.length > LimitEnum.COM_COUNT) {
window.$message.warning(`一次性只能上传${LimitEnum.COM_COUNT}个文件或图片`)
return
}
for (const file of e.clipboardData.files) {

View File

@ -1,4 +1,4 @@
import { MittEnum, MsgEnum } from '@/enums'
import { LimitEnum, MittEnum, MsgEnum } from '@/enums'
import { Ref } from 'vue'
import { MockItem } from '@/services/types.ts'
import { setting } from '@/stores/setting.ts'
@ -70,7 +70,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
}
// 如果输入框没有值就把回复内容清空
if (msgInput.value === '') {
reply.value = { accountName: '', content: '', key: '' }
reply.value = { imgCount: 0, accountName: '', content: '', key: '' }
}
})
@ -94,7 +94,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
// TODO 如果已经有就替换原来的内容 (nyh -> 2024-04-18 23:10:56)
return
}
reply.value = { accountName: event.value, content: event.content, key: event.key }
reply.value = { imgCount: 0, accountName: event.value, content: event.content, key: event.key }
if (messageInputDom.value) {
nextTick().then(() => {
messageInputDom.value.focus()
@ -108,6 +108,11 @@ export const useMsgInput = (messageInputDom: Ref) => {
/* 处理发送信息事件 */
// TODO 输入框中的内容当我切换消息的时候需要记录之前输入框的内容 (nyh -> 2024-03-01 07:03:43)
const send = () => {
// 判断输入框中的图片或者文件数量是否超过限制
if (messageInputDom.value.querySelectorAll('img').length > LimitEnum.COM_COUNT) {
window.$message.warning(`一次性只能上传${LimitEnum.COM_COUNT}个文件或图片`)
return
}
ait.value = false
const contentType = getMessageContentType(messageInputDom)
const msg = {
@ -146,7 +151,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
Mitt.emit(MittEnum.SEND_MESSAGE, msg)
msgInput.value = ''
messageInputDom.value.innerHTML = ''
reply.value = { accountName: '', content: '', key: '' }
reply.value = { imgCount: 0, accountName: '', content: '', key: '' }
}
/* 当输入框手动输入值的时候触发input事件(使用vueUse的防抖) */

View File

@ -91,19 +91,21 @@ export const useWindow = () => {
const checkWinExist = async (L: string) => {
const isExistsWinds = WebviewWindow.getByLabel(L)
if (isExistsWinds) {
// 如果窗口已存在,首先检查是否最小化了
const minimized = await isExistsWinds.isMinimized()
// 检查是否是隐藏
const hidden = await isExistsWinds.isVisible()
if (!hidden) {
await isExistsWinds.show()
}
if (minimized) {
// 如果已最小化,恢复窗口
await isExistsWinds.unminimize()
}
// 如果窗口已存在,则给它焦点,使其在最前面显示
await isExistsWinds.setFocus()
nextTick().then(async () => {
// 如果窗口已存在,首先检查是否最小化了
const minimized = await isExistsWinds.isMinimized()
// 检查是否是隐藏
const hidden = await isExistsWinds.isVisible()
if (!hidden) {
await isExistsWinds.show()
}
if (minimized) {
// 如果已最小化,恢复窗口
await isExistsWinds.unminimize()
}
// 如果窗口已存在,则给它焦点,使其在最前面显示
await isExistsWinds.setFocus()
})
}
}

View File

@ -72,6 +72,10 @@
overflow: hidden;
text-overflow: ellipsis;
}
/* 回复图片的计数器样式 */
.reply-img-sub {
@apply absolute bottom-8px right-6px color-#13987f bg-[--bg-reply-img-count] p-[2px_4px] rounded-6px text-10px;
}
}
/* 跳转到回复内容时候显示的样式 */
.active-reply {

View File

@ -58,6 +58,7 @@
--bg-reply-bubble: #d3d3d3;
--reply-color: #909090;
--reply-hover: #505050;
--bg-reply-img-count: #e3e3e3;
}
html[data-theme='dark'] {
@ -119,6 +120,7 @@ html[data-theme='dark'] {
--bg-reply-bubble: #505050;
--reply-color: #e3e3e3;
--reply-hover: #b1b1b1;
--bg-reply-img-count: #505050;
}
/*! end */
// 线性动画