mirror of
https://gitee.com/HuLaSpark/HuLa.git
synced 2024-11-29 18:28:30 +08:00
feat(components): ✨ 实现群聊回复表情功能
This commit is contained in:
parent
cf4820bffb
commit
1fb3530cbd
@ -60,7 +60,7 @@ module.exports = {
|
||||
emojiAlign: "center",
|
||||
useAI: false,
|
||||
aiNumber: 1,
|
||||
themeColorCode: "",
|
||||
themeColorCode: "38;5;168",
|
||||
scopes: [],
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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)">
|
||||
<!-- <!– 渲染消息内容体 –>-->
|
||||
<!-- <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
|
||||
|
@ -3,7 +3,7 @@
|
||||
*/
|
||||
// 表情
|
||||
const expressionEmojis =
|
||||
'😀😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😗😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀💩🤡👹👺👻'
|
||||
'😀😄😁😆😅🤣😂🙂🙃😉😊😇🫡🫥😶🥰😍🤩😘😗😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀💩🤡👹👺👻'
|
||||
|
||||
// 小动物
|
||||
const animalEmojis =
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
Loading…
Reference in New Issue
Block a user