feat(components): 完善右键功能的显示资料

剔除冗余代码
完善回复对超链接的支持
This commit is contained in:
nongyehong 2024-04-22 23:32:09 +08:00
parent fc753b5f05
commit cf4820bffb
14 changed files with 138 additions and 88 deletions

View File

@ -37,12 +37,13 @@
<n-popover <n-popover
@update:show="handlePopoverUpdate(item.key)" @update:show="handlePopoverUpdate(item.key)"
trigger="click" trigger="click"
placement="right-start" placement="right"
:show-arrow="false" :show-arrow="false"
v-model:show="infoPopover"
style="padding: 0; background: var(--bg-info); backdrop-filter: blur(10px)"> style="padding: 0; background: var(--bg-info); backdrop-filter: blur(10px)">
<template #trigger> <template #trigger>
<ContextMenu <ContextMenu
@select="$event.click(item)" @select="$event.click(item, 'Main')"
:menu="activeItem.type === RoomTypeEnum.GROUP ? optionsList : []" :menu="activeItem.type === RoomTypeEnum.GROUP ? optionsList : []"
:special-menu="report"> :special-menu="report">
<n-avatar <n-avatar
@ -62,7 +63,7 @@
</ContextMenu> </ContextMenu>
</template> </template>
<!-- 用户个人信息框 --> <!-- 用户个人信息框 -->
<InfoPopover :info="item.accountId !== userId ? activeItemRef : void 0" /> <InfoPopover v-if="selectKey === item.key" :info="item.accountId !== userId ? activeItemRef : void 0" />
</n-popover> </n-popover>
<n-flex <n-flex
vertical vertical
@ -220,7 +221,6 @@ import { EventEnum, MittEnum, MsgEnum, RoomTypeEnum } from '@/enums'
import { MockItem } from '@/services/types.ts' import { MockItem } from '@/services/types.ts'
import Mitt from '@/utils/Bus.ts' import Mitt from '@/utils/Bus.ts'
import { invoke } from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
import { optionsList, report } from './config.ts'
import { usePopover } from '@/hooks/usePopover.ts' import { usePopover } from '@/hooks/usePopover.ts'
import { useWindow } from '@/hooks/useWindow.ts' import { useWindow } from '@/hooks/useWindow.ts'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
@ -238,15 +238,14 @@ const activeItemRef = ref({ ...activeItem })
const settingStore = setting() const settingStore = setting()
const { login } = storeToRefs(settingStore) const { login } = storeToRefs(settingStore)
const { createWebviewWindow } = useWindow() const { createWebviewWindow } = useWindow()
/** 当前点击的用户的key */
const selectKey = ref()
/** 跳转回复消息后选中效果 */ /** 跳转回复消息后选中效果 */
const activeReply = ref(-1) const activeReply = ref(-1)
/** item最小高度用于计算滚动大小和位置 */ /** item最小高度用于计算滚动大小和位置 */
const itemSize = computed(() => (activeItem.type === RoomTypeEnum.GROUP ? 98 : 70)) const itemSize = computed(() => (activeItem.type === RoomTypeEnum.GROUP ? 98 : 70))
/** 虚拟列表 */ /** 虚拟列表 */
const virtualListInst = ref<VirtualListInst>() const virtualListInst = ref<VirtualListInst>()
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main') /** 手动触发Popover显示 */
const infoPopover = ref(false)
const { removeTag } = useCommon() const { removeTag } = useCommon()
const { const {
handleScroll, handleScroll,
@ -262,8 +261,12 @@ const {
modalShow, modalShow,
userId, userId,
specialMenuList, specialMenuList,
itemComputed itemComputed,
optionsList,
report,
selectKey
} = useChatMain(activeItem) } = useChatMain(activeItem)
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main')
// // TextBody // // TextBody
// const textBody = { // const textBody = {
// content: '123', // content: '123',
@ -403,6 +406,11 @@ onMounted(() => {
Mitt.on(MittEnum.SEND_MESSAGE, (event: any) => { Mitt.on(MittEnum.SEND_MESSAGE, (event: any) => {
handleSendMessage(event) handleSendMessage(event)
}) })
Mitt.on(`${MittEnum.INFO_POPOVER}-Main`, (event: any) => {
selectKey.value = event
infoPopover.value = true
handlePopoverUpdate(event)
})
Mitt.on(MittEnum.MSG_BOX_SHOW, (event: any) => { Mitt.on(MittEnum.MSG_BOX_SHOW, (event: any) => {
activeItemRef.value = event.item activeItemRef.value = event.item
}) })

View File

@ -35,11 +35,12 @@
<n-popover <n-popover
@update:show="handlePopoverUpdate(item.key)" @update:show="handlePopoverUpdate(item.key)"
trigger="click" trigger="click"
placement="left-start" placement="left"
:show-arrow="false" :show-arrow="false"
v-model:show="infoPopover"
style="padding: 0; background: var(--bg-info); backdrop-filter: blur(10px)"> style="padding: 0; background: var(--bg-info); backdrop-filter: blur(10px)">
<template #trigger> <template #trigger>
<ContextMenu @select="$event.click(item)" :menu="optionsList" :special-menu="report"> <ContextMenu @select="$event.click(item, 'Sidebar')" :menu="optionsList" :special-menu="report">
<n-flex @click="selectKey = item.key" :key="item.key" :size="10" align="center" class="item"> <n-flex @click="selectKey = item.key" :key="item.key" :size="10" align="center" class="item">
<n-avatar <n-avatar
lazy lazy
@ -57,30 +58,31 @@
</ContextMenu> </ContextMenu>
</template> </template>
<!-- 用户个人信息框 --> <!-- 用户个人信息框 -->
<InfoPopover :info="item" /> <InfoPopover v-if="selectKey === item.key" :info="item" />
</n-popover> </n-popover>
</template> </template>
</n-virtual-list> </n-virtual-list>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RoomTypeEnum } from '@/enums' import { MittEnum, RoomTypeEnum } from '@/enums'
import { MockItem } from '@/services/types.ts' import { MockItem } from '@/services/types.ts'
import { MockList } from '@/mock' import { MockList } from '@/mock'
import { InputInst } from 'naive-ui' import { InputInst } from 'naive-ui'
import { optionsList, report } from './config.ts'
import { usePopover } from '@/hooks/usePopover.ts' import { usePopover } from '@/hooks/usePopover.ts'
import { useChatMain } from '@/hooks/useChatMain.ts'
/** 当前点击的用户的key */ import Mitt from '@/utils/Bus.ts'
const selectKey = ref()
const isSearch = ref(false)
const searchRef = ref('')
const inputInstRef = ref<InputInst | null>(null)
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-sidebar')
const { activeItem } = defineProps<{ const { activeItem } = defineProps<{
activeItem: MockItem activeItem: MockItem
}>() }>()
const isSearch = ref(false)
const searchRef = ref('')
/** 手动触发Popover显示 */
const infoPopover = ref(false)
const inputInstRef = ref<InputInst | null>(null)
const { optionsList, report, selectKey } = useChatMain(activeItem)
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-sidebar')
const handleSearch = () => { const handleSearch = () => {
isSearch.value = !isSearch.value isSearch.value = !isSearch.value
@ -88,6 +90,14 @@ const handleSearch = () => {
inputInstRef.value?.select() inputInstRef.value?.select()
}) })
} }
onMounted(() => {
Mitt.on(`${MittEnum.INFO_POPOVER}-Sidebar`, (event: any) => {
selectKey.value = event
infoPopover.value = true
handlePopoverUpdate(event)
})
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -1,36 +0,0 @@
// TODO config文件做简单的操作配置如果需求复杂就封装成hooks (nyh -> 2024-03-23 03:35:05)
/** 右键用户信息菜单(单聊的时候显示) */
const optionsList = ref([
{
label: '发送信息',
icon: 'message-action',
click: (item: any) => {
console.log(item)
}
},
{
label: 'TA',
icon: 'aite',
click: () => {}
},
{
label: '查看资料',
icon: 'notes',
click: () => {}
},
{
label: '添加好友',
icon: 'people-plus',
click: () => {}
}
])
/** 举报选项 */
const report = ref([
{
label: '举报',
icon: 'caution',
click: () => {}
}
])
export { optionsList, report }

View File

@ -59,7 +59,9 @@ export enum MittEnum {
/** 消息列表被清空或者暂无消息 */ /** 消息列表被清空或者暂无消息 */
NOT_MSG = 'notMsg', NOT_MSG = 'notMsg',
/** 回复消息 */ /** 回复消息 */
REPLY_MEG = 'replyMeg' REPLY_MEG = 'replyMeg',
/** 手动触发InfoPopover */
INFO_POPOVER = 'infoPopover'
} }
/** 主题类型 */ /** 主题类型 */

View File

@ -9,28 +9,30 @@ export const useChatMain = (activeItem: MockItem) => {
const { removeTag } = useCommon() const { removeTag } = useCommon()
const settingStore = setting() const settingStore = setting()
const { login } = storeToRefs(settingStore) const { login } = storeToRefs(settingStore)
/* 选中的气泡消息 */ /** 选中的气泡消息 */
const activeBubble = ref(-1) const activeBubble = ref(-1)
/* 当前登录的用户id */ /** 当前登录的用户id */
const userId = ref(login.value.accountInfo.uid) const userId = ref(login.value.accountInfo.uid)
/* 提醒框标题 */ /** 提醒框标题 */
const tips = ref() const tips = ref()
/* 是否显示删除信息的弹窗 */ /** 是否显示删除信息的弹窗 */
const modalShow = ref(false) const modalShow = ref(false)
/* 需要删除信息的下标 */ /** 需要删除信息的下标 */
const delIndex = ref(0) const delIndex = ref(0)
/* 悬浮的页脚 */ /** 悬浮的页脚 */
const floatFooter = ref(false) const floatFooter = ref(false)
/* 记录历史消息下标 */ /** 记录历史消息下标 */
const historyIndex = ref(0) const historyIndex = ref(0)
/* 新消息数 */ /** 新消息数 */
const newMsgNum = ref(0) const newMsgNum = ref(0)
/* 计算出触发页脚后的历史消息下标 */ /** 当前点击的用户的key */
const selectKey = ref()
/** 计算出触发页脚后的历史消息下标 */
const itemComputed = computed(() => { const itemComputed = computed(() => {
return items.value.filter((item) => item.accountId !== userId.value).length return items.value.filter((item) => item.accountId !== userId.value).length
}) })
/*! 模拟信息列表 */ /**! 模拟信息列表 */
const items = ref( const items = ref(
Array.from({ length: 5 }, (_, i) => ({ Array.from({ length: 5 }, (_, i) => ({
value: `${i}安老师`, value: `${i}安老师`,
@ -49,7 +51,7 @@ export const useChatMain = (activeItem: MockItem) => {
})) }))
) )
/* 通用右键菜单 */ /** 通用右键菜单 */
const commonMenuList = ref<OPT.RightMenu[]>([ const commonMenuList = ref<OPT.RightMenu[]>([
{ {
label: '转发', label: '转发',
@ -65,7 +67,7 @@ export const useChatMain = (activeItem: MockItem) => {
} }
} }
]) ])
/* 右键消息菜单列表 */ /** 右键消息菜单列表 */
const menuList = ref<OPT.RightMenu[]>([ const menuList = ref<OPT.RightMenu[]>([
{ {
label: '复制', label: '复制',
@ -77,7 +79,7 @@ export const useChatMain = (activeItem: MockItem) => {
}, },
...commonMenuList.value ...commonMenuList.value
]) ])
/* 右键菜单下划线后的列表 */ /** 右键菜单下划线后的列表 */
const specialMenuList = ref<OPT.RightMenu[]>([ const specialMenuList = ref<OPT.RightMenu[]>([
{ {
label: '删除', label: '删除',
@ -89,7 +91,7 @@ export const useChatMain = (activeItem: MockItem) => {
} }
} }
]) ])
/* 文件类型右键菜单 */ /** 文件类型右键菜单 */
const fileMenuList = ref<OPT.RightMenu[]>([ const fileMenuList = ref<OPT.RightMenu[]>([
{ {
label: '预览', label: '预览',
@ -108,7 +110,7 @@ export const useChatMain = (activeItem: MockItem) => {
} }
} }
]) ])
/* 图片类型右键菜单 */ /** 图片类型右键菜单 */
const imageMenuList = ref<OPT.RightMenu[]>([ const imageMenuList = ref<OPT.RightMenu[]>([
{ {
label: '添加到表情', label: '添加到表情',
@ -135,6 +137,41 @@ export const useChatMain = (activeItem: MockItem) => {
} }
} }
]) ])
/** 右键用户信息菜单(单聊的时候显示) */
const optionsList = ref([
{
label: '发送信息',
icon: 'message-action',
click: (item: any) => {
console.log(item)
}
},
{
label: 'TA',
icon: 'aite',
click: () => {}
},
{
label: '查看资料',
icon: 'notes',
click: (item: any, type: string) => {
Mitt.emit(`${MittEnum.INFO_POPOVER}-${type}`, item.key)
}
},
{
label: '添加好友',
icon: 'people-plus',
click: () => {}
}
])
/** 举报选项 */
const report = ref([
{
label: '举报',
icon: 'caution',
click: () => {}
}
])
/** /**
* *
@ -166,7 +203,7 @@ export const useChatMain = (activeItem: MockItem) => {
} }
} }
/* 处理滚动事件(用于页脚显示功能) */ /** 处理滚动事件(用于页脚显示功能) */
const handleScroll = (e: Event) => { const handleScroll = (e: Event) => {
const target = e.target as HTMLElement const target = e.target as HTMLElement
// 获取已滚动的距离,即从顶部到当前滚动位置的距离 // 获取已滚动的距离,即从顶部到当前滚动位置的距离
@ -197,7 +234,7 @@ export const useChatMain = (activeItem: MockItem) => {
return type === MsgEnum.IMAGE ? imageMenuList.value : type === MsgEnum.FILE ? fileMenuList.value : menuList.value return type === MsgEnum.IMAGE ? imageMenuList.value : type === MsgEnum.FILE ? fileMenuList.value : menuList.value
} }
/* 删除信息事件 */ /** 删除信息事件 */
const handleConfirm = () => { const handleConfirm = () => {
// 根据key找到items中对应的下标 // 根据key找到items中对应的下标
const index = items.value.findIndex((item) => item.key === delIndex.value) const index = items.value.findIndex((item) => item.key === delIndex.value)
@ -205,7 +242,7 @@ export const useChatMain = (activeItem: MockItem) => {
modalShow.value = false modalShow.value = false
} }
/* 点击气泡消息时候监听用户是否按下ctrl+c来复制内容 */ /** 点击气泡消息时候监听用户是否按下ctrl+c来复制内容 */
const handleMsgClick = (item: any) => { const handleMsgClick = (item: any) => {
activeBubble.value = item.key activeBubble.value = item.key
// 启用键盘监听 // 启用键盘监听
@ -235,6 +272,9 @@ export const useChatMain = (activeItem: MockItem) => {
modalShow, modalShow,
userId, userId,
specialMenuList, specialMenuList,
itemComputed itemComputed,
optionsList,
report,
selectKey
} }
} }

View File

@ -1,6 +1,7 @@
import { LimitEnum, MsgEnum } from '@/enums' import { LimitEnum, MsgEnum } from '@/enums'
import { Ref } from 'vue' import { Ref } from 'vue'
import { createFileOrVideoDom } from '@/utils/CreateDom.ts' import { createFileOrVideoDom } from '@/utils/CreateDom.ts'
import { RegExp } from '@/utils/RegExp.ts'
/** 常用工具类 */ /** 常用工具类 */
export const useCommon = () => { export const useCommon = () => {
@ -152,6 +153,15 @@ export const useCommon = () => {
min-width: 0; min-width: 0;
` `
let contentBox let contentBox
const { hyperlinkRegex, foundHyperlinks } = RegExp.isHyperlink(content)
// 判断是否包含超链接
if (foundHyperlinks && foundHyperlinks.length > 0) {
content.replace(hyperlinkRegex, (match: string) => {
reply.value.content = match.startsWith('www.') ? 'https://' + match : match
})
// 去掉content中的标签
content = removeTag(content)
}
// 判断content内容是否是data:image/开头的数组 // 判断content内容是否是data:image/开头的数组
if (Array.isArray(content)) { if (Array.isArray(content)) {
// 获取总共有多少张图片 // 获取总共有多少张图片

View File

@ -7,6 +7,7 @@ import { useDebounceFn } from '@vueuse/core'
import Mitt from '@/utils/Bus.ts' import Mitt from '@/utils/Bus.ts'
import { MockList } from '@/mock' import { MockList } from '@/mock'
import { useCommon } from './useCommon.ts' import { useCommon } from './useCommon.ts'
import { RegExp } from '@/utils/RegExp.ts'
export const useMsgInput = (messageInputDom: Ref) => { export const useMsgInput = (messageInputDom: Ref) => {
const { triggerInputEvent, insertNode, getMessageContentType, getEditorRange, imgPaste, removeTag, reply } = const { triggerInputEvent, insertNode, getMessageContentType, getEditorRange, imgPaste, removeTag, reply } =
@ -120,9 +121,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
content: msgInput.value, content: msgInput.value,
reply: contentType === MsgEnum.REPLY ? reply.value : null reply: contentType === MsgEnum.REPLY ? reply.value : null
} }
const hyperlinkRegex = /(\b(?:https?:\/\/|www)[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi
const foundHyperlinks = msg.content.match(hyperlinkRegex)
/** 如果是Reply消息需要将消息的样式修改 */ /** 如果是Reply消息需要将消息的样式修改 */
if (msg.type === MsgEnum.REPLY) { if (msg.type === MsgEnum.REPLY) {
// 先去掉原来的标签 // 先去掉原来的标签
@ -131,6 +129,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
// TODO 不允许用户删除回复消息中最前面的空格或者标志符号 (nyh -> 2024-04-17 06:39:22) // TODO 不允许用户删除回复消息中最前面的空格或者标志符号 (nyh -> 2024-04-17 06:39:22)
msg.content = msg.content.replace(/^[\S\s]*\u00A0/, '') msg.content = msg.content.replace(/^[\S\s]*\u00A0/, '')
} }
const { hyperlinkRegex, foundHyperlinks } = RegExp.isHyperlink(msg.content)
/** 判断是否有超链接 */ /** 判断是否有超链接 */
if (foundHyperlinks && foundHyperlinks.length > 0) { if (foundHyperlinks && foundHyperlinks.length > 0) {
msg.content = msg.content.replace(hyperlinkRegex, (match) => { msg.content = msg.content.replace(hyperlinkRegex, (match) => {

View File

@ -25,7 +25,7 @@ export const MockList = ref<MockItem[]>(
key: i, key: i,
avatar: `${avatars}?${i}`, avatar: `${avatars}?${i}`,
type: type, type: type,
accountId: i, accountId: `${i}`,
accountName: generateRandomString(Math.floor(Math.random() * 10) + 1, type) accountName: generateRandomString(Math.floor(Math.random() * 10) + 1, type)
} }
}) })

View File

@ -4,6 +4,7 @@ import Mitt from '@/utils/Bus.ts'
const { VITE_WEBSOCKET_URL } = import.meta.env const { VITE_WEBSOCKET_URL } = import.meta.env
/** websocket连接对象 */ /** websocket连接对象 */
let ws: WebSocket let ws: WebSocket
/** 初始化websocket连接 */
const initWebSocket = () => { const initWebSocket = () => {
ws = new WebSocket(`${VITE_WEBSOCKET_URL}/`) ws = new WebSocket(`${VITE_WEBSOCKET_URL}/`)
ws.onopen = () => { ws.onopen = () => {

View File

@ -24,7 +24,6 @@ declare module 'vue' {
NAvatar: typeof import('naive-ui')['NAvatar'] NAvatar: typeof import('naive-ui')['NAvatar']
NAvatarGroup: typeof import('naive-ui')['NAvatarGroup'] NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
NBadge: typeof import('naive-ui')['NBadge'] NBadge: typeof import('naive-ui')['NBadge']
NBlockquote: typeof import('naive-ui')['NBlockquote']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup'] NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCheckbox: typeof import('naive-ui')['NCheckbox'] NCheckbox: typeof import('naive-ui')['NCheckbox']

View File

@ -31,11 +31,13 @@ export class RegExp {
} }
/** /**
* *
* @param val * @param content
* @returns hyperlinkRegex foundHyperlinks
*/ */
public static isHyperlink(val: string): boolean { public static isHyperlink(content: string) {
const hyperlinkRegex = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?$/ const hyperlinkRegex = /(\b(?:https?:\/\/|www)[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi
return hyperlinkRegex.test(val) const foundHyperlinks = content.match(hyperlinkRegex)
return { hyperlinkRegex, foundHyperlinks }
} }
} }

15
src/utils/Worker.ts Normal file
View File

@ -0,0 +1,15 @@
// 在 Web Worker 中接收消息
self.onmessage = (event) => {
const number = event.data
// 执行一些耗时的操作
const result = calculateSquare(number)
// 将结果发送回主线程
self.postMessage(result)
}
// 一些耗时的操作
const calculateSquare = (number: any) => {
return number * number
}

View File

@ -56,6 +56,7 @@ onMounted(() => {
Mitt.on(WsResEnum.QRCODE_LOGIN, (e: any) => { Mitt.on(WsResEnum.QRCODE_LOGIN, (e: any) => {
QRCode.value = e.data.loginUrl QRCode.value = e.data.loginUrl
loading.value = false loading.value = false
loadText.value = '请使用微信扫码登录'
}) })
Mitt.on(WsResEnum.LOGIN_SUCCESS, (e: any) => { Mitt.on(WsResEnum.LOGIN_SUCCESS, (e: any) => {
delay(async () => { delay(async () => {
@ -69,7 +70,6 @@ onMounted(() => {
}, 1000) }, 1000)
}) })
delay(() => { delay(() => {
loadText.value = '请使用微信扫码登录'
sendToServer({ type: WsReqEnum.LOGIN }) sendToServer({ type: WsReqEnum.LOGIN })
}, 1000) }, 1000)
}) })

View File

@ -9,7 +9,7 @@ import unocss from '@unocss/vite'
import terser from '@rollup/plugin-terser' import terser from '@rollup/plugin-terser'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
/**! 暂时不需要优化前端打包(如开启gzip这些tauri可能解析不了) */ /**! 不需要优化前端打包(如开启gzip) */
export default defineConfig(({ mode }: ConfigEnv) => { export default defineConfig(({ mode }: ConfigEnv) => {
// 获取当前环境的配置,如何设置第三个参数则加载所有变量而不是以“VITE_”前缀的变量 // 获取当前环境的配置,如何设置第三个参数则加载所有变量而不是以“VITE_”前缀的变量
const config = loadEnv(mode, '/') const config = loadEnv(mode, '/')