feat(components): 实现群聊回复表情功能

This commit is contained in:
nongyehong 2024-04-24 00:00:16 +08:00
parent cf4820bffb
commit 1fb3530cbd
8 changed files with 121 additions and 29 deletions

View File

@ -60,7 +60,7 @@ module.exports = {
emojiAlign: "center",
useAI: false,
aiNumber: 1,
themeColorCode: "",
themeColorCode: "38;5;168",
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,

View File

@ -7,7 +7,7 @@
<title>HuLa</title>
<!--引入iconpark图标库-->
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_77.6625fcfb23b027973a50f66bbc7126de.js"></script>
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_78.2ef5ae05e210de3f66b0fe5c58a7a130.js"></script>
</head>
<body>

View File

@ -2,7 +2,28 @@
<div ref="containerRef">
<slot></slot>
<Teleport to="body">
<Transition @beforeEnter="handleBeforeEnter" @enter="handleEnter" @afterEnter="handleAfterEnter">
<transition-group @beforeEnter="handleBeforeEnter" @enter="handleEnter" @afterEnter="handleAfterEnter">
<!-- 群聊emoji表情菜单 -->
<div
v-if="showMenu && emoji && emoji.length > 0"
class="context-menu"
style="display: flex; height: fit-content"
:style="{
left: `${pos.posX}px`,
top: `${pos.posY - 42}px`
}">
<n-flex
v-for="(item, index) in emoji as any[]"
:key="index"
align="center"
justify="space-between"
class="emoji-list">
<n-flex :size="0" align="center" justify="center" class="emoji-item" @click="handleReplyEmoji(item)">
{{ item.label }}
</n-flex>
</n-flex>
</div>
<!-- 普通右键菜单 -->
<div
v-if="showMenu"
class="context-menu"
@ -10,7 +31,7 @@
left: `${pos.posX}px`,
top: `${pos.posY}px`
}">
<div v-resize="handleSize" v-if="menu.length > 0" class="menu-list">
<div v-resize="handleSize" v-if="menu && menu.length > 0" class="menu-list">
<div v-for="(item, index) in menu as any[]" :key="index">
<!-- 禁止的菜单选项需要禁止点击事件 -->
<div class="menu-item-disabled" v-if="item.disabled" @click.prevent="$event.preventDefault()">
@ -33,7 +54,7 @@
</div>
</div>
</div>
</Transition>
</transition-group>
</Teleport>
</div>
</template>
@ -42,20 +63,24 @@
import { useContextMenu } from '@/hooks/useContextMenu.ts'
import { useViewport } from '@/hooks/useViewport.ts'
const { menu, specialMenu } = defineProps({
const { menu, emoji, specialMenu } = defineProps({
menu: {
type: Array,
default: () => []
type: Array
},
emoji: {
type: Array
},
specialMenu: {
type: Array,
default: () => []
}
})
/** 判断是否传入了menu */
const isNull = computed(() => menu === void 0)
const containerRef = ref(null)
const emit = defineEmits(['select'])
const emit = defineEmits(['select', 'reply-emoji'])
/** 获取鼠标位置和是否显示右键菜单 */
const { x, y, showMenu } = useContextMenu(containerRef)
const { x, y, showMenu } = useContextMenu(containerRef, isNull)
/** 获取视口的宽高 */
const { vw, vh } = useViewport()
/** 定义右键菜单尺寸 */
@ -90,6 +115,12 @@ const handleClick = (item: string) => {
emit('select', item)
}
/** 处理回复表情事件 */
const handleReplyEmoji = (item: string) => {
showMenu.value = false
emit('reply-emoji', item)
}
const handleBeforeEnter = (el: any) => {
el.style.height = 0
}
@ -127,6 +158,12 @@ const handleAfterEnter = (el: any) => {
}
.context-menu {
@include menu-item-style();
.emoji-list {
@apply size-fit p-4px select-none;
.emoji-item {
@apply size-28px rounded-4px text-16px cursor-pointer hover:bg-[--emoji-hover];
}
}
.menu-list {
padding: 5px;
display: flex;

View File

@ -16,7 +16,7 @@
:key="item.key"
class="flex-y-center min-h-58px"
:class="[
[activeItem.type === RoomTypeEnum.GROUP ? 'p-[18px_20px]' : 'chat-single p-[4px_20px_10px_20px]'],
[isGroup ? 'p-[18px_20px]' : 'chat-single p-[4px_20px_10px_20px]'],
{ 'active-reply': activeReply === item.key }
]">
<!-- 好友或者群聊的信息 -->
@ -44,7 +44,7 @@
<template #trigger>
<ContextMenu
@select="$event.click(item, 'Main')"
:menu="activeItem.type === RoomTypeEnum.GROUP ? optionsList : []"
:menu="isGroup ? optionsList : void 0"
:special-menu="report">
<n-avatar
lazy
@ -71,11 +71,8 @@
: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">
<ContextMenu @select="$event.click(item)" :menu="isGroup ? optionsList : []" :special-menu="report">
<span class="text-12px select-none color-#909090" v-if="isGroup">
{{ item.value }}
</span>
</ContextMenu>
@ -85,7 +82,9 @@
:data-key="item.accountId === userId ? `U${item.key}` : `Q${item.key}`"
@select="$event.click(item)"
:menu="handleItemType(item.type)"
:emoji="isGroup ? emojiList : []"
:special-menu="specialMenuList"
@reply-emoji="handleEmojiSelect($event.label, item.key)"
@click="handleMsgClick(item)">
<!-- &lt;!&ndash; 渲染消息内容体 &ndash;&gt;-->
<!-- <RenderMessage :message="message" />-->
@ -163,6 +162,15 @@
{{ item.reply.imgCount }}
</div>
</n-flex>
<!-- 群聊回复emoji表情 -->
<n-flex
v-if="replyEmoji.content && isGroup && item.key === replyEmoji.index"
align="center"
:size="6"
class="relative rounded-8px p-4px cursor-pointer select-none bg-#13987f text-14px w-fit">
{{ replyEmoji.content }}
</n-flex>
</n-flex>
</div>
</article>
@ -188,7 +196,7 @@
</n-modal>
<!-- 悬浮按钮提示(头部悬浮) // TODO (nyh -> 2024-03-07 01:27:22)-->
<header class="float-header" :class="activeItem.type === RoomTypeEnum.GROUP ? 'right-220px' : 'right-50px'">
<header class="float-header" :class="isGroup ? 'right-220px' : 'right-50px'">
<div class="float-box">
<n-flex justify="space-between" align="center">
<n-icon :color="'#13987f'">
@ -200,10 +208,7 @@
</header>
<!-- 悬浮按钮提示(底部悬浮) -->
<footer
class="float-footer"
v-if="floatFooter && newMsgNum > 0"
:class="activeItem.type === RoomTypeEnum.GROUP ? 'right-220px' : 'right-50px'">
<footer class="float-footer" v-if="floatFooter && newMsgNum > 0" :class="isGroup ? 'right-220px' : 'right-50px'">
<div class="float-box" :class="{ max: newMsgNum > 99 }" @click="scrollBottom">
<n-flex justify="space-between" align="center">
<n-icon :color="newMsgNum > 99 ? '#ce304f' : '#13987f'">
@ -240,12 +245,19 @@ const { login } = storeToRefs(settingStore)
const { createWebviewWindow } = useWindow()
/** 跳转回复消息后选中效果 */
const activeReply = ref(-1)
/** 当前信息是否是群聊信息 */
const isGroup = computed(() => activeItem.type === RoomTypeEnum.GROUP)
/** item最小高度用于计算滚动大小和位置 */
const itemSize = computed(() => (activeItem.type === RoomTypeEnum.GROUP ? 98 : 70))
const itemSize = computed(() => (isGroup.value ? 98 : 70))
/** 虚拟列表 */
const virtualListInst = ref<VirtualListInst>()
/** 手动触发Popover显示 */
const infoPopover = ref(false)
/** 群聊的回复表情内容以及下标 */
const replyEmoji = ref({
content: '',
index: -1
})
const { removeTag } = useCommon()
const {
handleScroll,
@ -264,7 +276,8 @@ const {
itemComputed,
optionsList,
report,
selectKey
selectKey,
emojiList
} = useChatMain(activeItem)
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main')
// // TextBody
@ -296,6 +309,12 @@ watchEffect(() => {
activeItemRef.value = { ...activeItem }
})
/** 处理emoji表情回应 */
const handleEmojiSelect = (label: any, key: any) => {
replyEmoji.value.content = label
replyEmoji.value.index = key
}
/** 处理回复消息中的 AIT 标签 */
const handleReply = (content: string) => {
return content.includes('id="aitSpan"') ? removeTag(content) : content

View File

@ -3,7 +3,7 @@
*/
// 表情
const expressionEmojis =
'😀😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😗😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀💩🤡👹👺👻'
'😀😄😁😆😅🤣😂🙂🙃😉😊😇🫡🫥😶‍🥰😍🤩😘😗😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀💩🤡👹👺👻'
// 小动物
const animalEmojis =

View File

@ -137,7 +137,7 @@ export const useChatMain = (activeItem: MockItem) => {
}
}
])
/** 右键用户信息菜单(聊的时候显示) */
/** 右键用户信息菜单(聊的时候显示) */
const optionsList = ref([
{
label: '发送信息',
@ -172,6 +172,21 @@ export const useChatMain = (activeItem: MockItem) => {
click: () => {}
}
])
/** emoji表情菜单 */
const emojiList = ref([
{
label: '👍'
},
{
label: '😆'
},
{
label: '🥳'
},
{
label: '🤯'
}
])
/**
*
@ -275,6 +290,7 @@ export const useChatMain = (activeItem: MockItem) => {
itemComputed,
optionsList,
report,
selectKey
selectKey,
emojiList
}
}

View File

@ -1,6 +1,12 @@
import type { Ref } from 'vue'
export const useContextMenu = (containerRef: Ref) => {
/**
*
* @param containerRef
* @param isNull
*/
export const useContextMenu = (containerRef: Ref, isNull?: Ref<boolean>) => {
const showMenu = ref(false)
const x = ref(0)
const y = ref(0)
@ -20,6 +26,7 @@ export const useContextMenu = (containerRef: Ref) => {
const handleContextMenu = (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (isNull?.value) return
handleVirtualListScroll(true)
showMenu.value = true
x.value = e.clientX

View File

@ -13,10 +13,20 @@
<n-qr-code
v-else
:size="180"
class="rounded-12px"
class="rounded-12px relative"
:class="{ blur: scanSuccess }"
:value="QRCode"
icon-src="/logo.png"
error-correction-level="H" />
<n-flex
v-if="scanSuccess"
vertical
:size="12"
align="center"
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<svg class="size-42px"><use href="#success"></use></svg>
<span class="text-(16px #e3e3e3)">扫码成功</span>
</n-flex>
</n-flex>
<n-flex justify="center" class="mt-15px text-(14px #808080)">{{ loadText }}</n-flex>
@ -46,6 +56,7 @@ const { createWebviewWindow } = useWindow()
const loading = ref(true)
const loadText = ref('加载中...')
const QRCode = ref()
const scanSuccess = ref(false)
const toLogin = () => {
router.push('/login')
@ -59,6 +70,8 @@ onMounted(() => {
loadText.value = '请使用微信扫码登录'
})
Mitt.on(WsResEnum.LOGIN_SUCCESS, (e: any) => {
scanSuccess.value = true
loadText.value = '登录中...'
delay(async () => {
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', false, true)
settingStore.setAccountInfo({