Merge branch 'feature/im' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/im

This commit is contained in:
YunaiV 2024-05-26 09:26:38 +08:00
commit 82b3276204
19 changed files with 341 additions and 315 deletions

View File

@ -0,0 +1,27 @@
import request from '@/config/axios'
export interface ImConversationRespVO {
id: number // 编号
userId: number // 所属用户
conversationType: number // 会话类型
targetId: number // 聊天对象编号
no: string // 会话标志
pinned: boolean // 是否置顶
lastReadTime: string // 最后已读时间
createTime: string // 创建时间
}
// 获得用户的会话列表
export const getConversationList = async () => {
return await request.get({ url: `/im/conversation/list` })
}
// 置顶会话
export const updatePinned = async (data: any) => {
return await request.post({ url: `/im/conversation/update-pinned`, data })
}
// 更新最后已读时间
export const updateLastReadTime = async (data: any) => {
return await request.post({ url: `/im/conversation/update-last-read-time`, data })
}

View File

@ -0,0 +1,42 @@
import request from '@/config/axios'
export interface ImMessageSendReqVO {
clientMessageId: string // 客户端消息编号
receiverId: number // 接收人编号
conversationType: number // 会话类型
contentType: number // 内容类型
content: string // 内容
}
export interface ImMessageSendRespVO {
id: number // 编号
sendTime: string // 发送时间
}
export interface ImMessageRespVO {
id: number // 编号
conversationType: number // 会话类型
senderId: number // 发送人编号
senderNickname: string // 发送人昵称
senderAvatar: string // 发送人头像
receiverId: number // 接收人编号
contentType: number // 内容类型
content: string // 内容
sendTime: string // 发送时间
sequence: number // 序号
}
// 发送消息
export const sendMessage = async (data: ImMessageSendReqVO) => {
return await request.post({ url: `/im/message/send`, data })
}
// 消息列表-拉取大于 sequence 的消息列表
export const pullMessageList = async (params: { sequence: number; size: number }) => {
return await request.get({ url: `/im/message/pull`, params })
}
// 消息列表-根据接收人和发送时间进行分页查询
export const getMessageList = async (params: any) => {
return await request.get({ url: `/im/message/list`, params })
}

View File

@ -97,7 +97,7 @@ $prefix-cls: #{$namespace}--cropper-avatar;
opacity: 0; opacity: 0;
transition: opacity 0.4s; transition: opacity 0.4s;
::v-deep(svg) { :deep(svg) {
margin: auto; margin: auto;
} }
} }

View File

@ -29,7 +29,7 @@ const querySearch = () => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
::v-deep .el-input__wrapper { :deep(.el-input__wrapper) {
box-shadow: none; box-shadow: none;
} }

View File

@ -1,43 +1,80 @@
const SESSION_MESSAGE_TYPE = { const SESSION_MESSAGE_TYPE = {
img: '[图片]', img: '[图片]',
file: '[文件]', file: '[文件]',
audio: '[语音]', audio: '[语音]',
loc: '[位置]' loc: '[位置]'
} }
const CUSTOM_TYPE = { const CUSTOM_TYPE = {
userCard: '个人名片' userCard: '个人名片'
} }
// const ALL_MESSAGE_TYPE = {
// TEXT: 'txt',
// IMAGE: 'img',
// AUDIO: 'audio',
// LOCAL: 'loc',
// VIDEO: 'video',
// FILE: 'file',
// CUSTOM: 'custom',
// CMD: 'cmd',
// INFORM: 'inform' //这个类型不在环信消息类型内,属于自己定义的一种系统通知类的消息。
// }
const ALL_MESSAGE_TYPE = { const ALL_MESSAGE_TYPE = {
TEXT: 'txt', TEXT: 101,
IMAGE: 'img', IMAGE: 102,
AUDIO: 'audio', AUDIO: 103,
LOCAL: 'loc', VIDEO: 104,
VIDEO: 'video', FILE: 105,
FILE: 'file', AT_TEXT: 106,
CUSTOM: 'custom', MERGE: 107,
CMD: 'cmd', CARD: 108,
INFORM: 'inform' //这个类型不在环信消息类型内,属于自己定义的一种系统通知类的消息。 LOCATION: 109,
CUSTOM: 110,
REVOKE_RECEIPT: 111,
C2C_RECEIPT: 112,
TYPING: 113,
QUOTE: 114,
FACE: 115,
ADVANCED_REVOKE: 118,
FRIEND_ADDED: 1201,
OA_NOTIFICATION: 1400,
GROUP_CREATED: 1501,
GROUP_INFO_CHANGED: 1502,
MEMBER_QUIT: 1504,
GROUP_OWNER_CHANGED: 1507,
MEMBER_KICKED: 1508,
MEMBER_INVITED: 1509,
MEMBER_ENTER: 1510,
GROUP_DISMISSED: 1511,
GROUP_MEMBER_MUTED: 1512,
GROUP_MEMBER_CANCEL_MUTED: 1513,
GROUP_MUTED: 1514,
GROUP_CANCEL_MUTED: 1515,
GROUP_ANNOUNCEMENT_UPDATED: 1519,
GROUP_NAME_UPDATED: 1520,
BURN_CHANGE: 1701,
REVOKE: 2101
} }
const CHAT_TYPE = { const CHAT_TYPE = {
SINGLE: 'singleChat', SINGLE: 1,
GROUP: 'groupChat' GROUP: 3,
NOTIFICATION: 4
} }
const MENTION_ALL = { const MENTION_ALL = {
TEXT: '所有人', TEXT: '所有人',
VALUE: 'ALL' VALUE: 'ALL'
} }
const CHANGE_MESSAGE_BODAY_TYPE = { const CHANGE_MESSAGE_BODAY_TYPE = {
RECALL: 0, RECALL: 0,
DELETE: 1, DELETE: 1,
MODIFY: 2 MODIFY: 2
} }
export default { export default {
SESSION_MESSAGE_TYPE, SESSION_MESSAGE_TYPE,
CUSTOM_TYPE, CUSTOM_TYPE,
ALL_MESSAGE_TYPE, ALL_MESSAGE_TYPE,
CHAT_TYPE, CHAT_TYPE,
MENTION_ALL, MENTION_ALL,
CHANGE_MESSAGE_BODAY_TYPE CHANGE_MESSAGE_BODAY_TYPE
} }

View File

@ -1,28 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { Dialog } from '@/components/Dialog' import { Dialog } from '@/components/Dialog'
import { ref } from 'vue' import { shallowRef, defineAsyncComponent, DefineComponent } from 'vue'
import IM from '@/views/im/index.vue'
import { useRouter } from 'vue-router' // useRouter
defineOptions({ name: 'ImChat' }) //
const IMComponent = defineAsyncComponent(() => import('@/views/im/index.vue'))
const dialogVisible = ref(false) const dialogVisible = shallowRef(false)
const router = useRouter() // router const currentComponent = shallowRef<DefineComponent | null>(null)
// // IM
function handleClick() { function openDialog() {
dialogVisible.value = !dialogVisible.value dialogVisible.value = true
router.push('/im/conversation') // 设置路由为 /im/conversation currentComponent.value = IMComponent // IM
} }
</script> </script>
<template> <template>
<div class="custom-hover" v-bind="$attrs"> <div class="custom-hover" v-bind="$attrs" @click="openDialog">
<ElBadge> <ElBadge>
<Icon :size="18" class="cursor-pointer" icon="ep:chat-round" @click="handleClick" /> <Icon :size="18" class="cursor-pointer" icon="ep:chat-round" />
</ElBadge> </ElBadge>
</div> </div>
<Dialog v-model="dialogVisible" width="90%" top="10vh"> <Dialog v-model="dialogVisible" width="90%" top="10vh">
<IM /> <component :is="currentComponent" />
<!-- 使用动态组件 -->
</Dialog> </Dialog>
</template> </template>

View File

@ -591,18 +591,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
}, },
component: () => import('@/views/im/Conversation/index.vue'), component: () => import('@/views/im/Conversation/index.vue'),
children: [ children: [
{
// 会话详情
path: 'informDetails',
name: 'InformDetails',
meta: {
title: '通知详情',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/InformDetails/index.vue')
},
{ {
//聊天对话框 //聊天对话框
path: 'message', path: 'message',

View File

@ -0,0 +1,6 @@
// fileSizeFormat.ts
export default function fileSizeFormat(value: number): string {
const s = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
const e = Math.floor(Math.log(value) / Math.log(1024))
return (value / Math.pow(1024, Math.floor(e))).toFixed(2) + ' ' + s[e]
}

20
src/utils/paseLink.ts Normal file
View File

@ -0,0 +1,20 @@
interface ParsedLinkResult {
isLink: boolean
msg: string
}
const paseLink = (msg: string): ParsedLinkResult => {
let isLink = false
const reg =
/(https?\:\/\/|www\.)([a-zA-Z0-9-]+(\.[a-zA-Z0-9]+)+)(\:[0-9]{2,4})?\/?((\.[:_0-9a-zA-Z-]+)|[:_0-9a-zA-Z-]*\/?)*\??[:_#@*&%0-9a-zA-Z-/=]*/gm
msg = msg.replace(reg, function (v: string): string {
const prefix = /^https?/gm.test(v)
isLink = prefix
return "<a href='" + (prefix ? v : '//' + v) + "' target='_blank'>" + v + '</a>'
})
return { isLink, msg }
}
export default paseLink

View File

@ -1,26 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { reactive } from 'vue' import { reactive } from 'vue'
/* 头像相关 */ import * as ConversationApi from '@/api/im/conversation'
import informIcon from '@/assets/imgs/im/avatar/inform.png'
/* route */ /* route */
const route = useRoute() const route = useRoute()
/* router */ /* router */
const router = useRouter() const router = useRouter()
//
const informDetail = computed(() => {
const informDetailArr = reactive([
{
from: '系统通知',
desc: '您有一条新的通知',
time: new Date(),
untreated: 1
}
])
const lastInformDeatail = informDetailArr[0] || {}
const untreated = 1
return { untreated, lastInformDeatail }
})
//(使) //(使)
const friendList = reactive({ const friendList = reactive({
@ -40,10 +25,22 @@ const conversationList = reactive([
msg: 'hello word!' msg: 'hello word!'
}, },
latestSendTime: new Date(), latestSendTime: new Date(),
unreadMessageNum: 5, unreadMessageNum: 5
isMention: false
} }
]) ])
// let conversationList = reactive({})
// //
// const getConversationList = async () => {
// const res = await ConversationApi.getConversationList()
// if (res.code === 200) {
// console.log('', res.data)
// conversationList = res.data
// }
// }
// //
// onMounted(() => {
// getConversationList()
// })
//name //name
const handleConversationName = computed(() => { const handleConversationName = computed(() => {
@ -53,7 +50,7 @@ const handleConversationName = computed(() => {
const handleLastMsgNickName = computed(() => { const handleLastMsgNickName = computed(() => {
return '' return ''
}) })
const emit = defineEmits(['toInformDetails', 'toChatMessage']) const emit = defineEmits(['toChatMessage'])
// //
const checkedConverItemIndex = ref(null) const checkedConverItemIndex = ref(null)
const toChatMessage = (item, itemKey, index) => { const toChatMessage = (item, itemKey, index) => {
@ -69,36 +66,6 @@ const deleteConversation = (itemKey) => {
</script> </script>
<template> <template>
<el-scrollbar class="session_list" style="overflow: auto" tag="ul"> <el-scrollbar class="session_list" style="overflow: auto" tag="ul">
<!-- 系统通知会话 -->
<li
v-if="JSON.stringify(informDetail.lastInformDeatail) !== '{}' && informDetail.untreated >= 1"
class="session_list_item"
@click="$emit('toInformDetails')"
>
<div class="item_body item_left">
<!-- 通知头像 -->
<div class="session_other_avatar">
<el-avatar :size="34" :src="informIcon" />
</div>
</div>
<div class="item_body item_main">
<div class="name">系统通知</div>
<div class="last_msg_body">
{{ informDetail.lastInformDeatail.from }}:{{ informDetail.lastInformDeatail.desc }}
</div>
</div>
<div class="item_body item_right">
<span class="time">{{
formatDate(informDetail.lastInformDeatail.time, 'MM/DD/HH:mm')
}}</span>
<span class="unReadNum_box" v-if="informDetail.untreated >= 1">
<sup
class="unReadNum_count"
v-text="informDetail.untreated >= 99 ? '99+' : informDetail.untreated"
></sup>
</span>
</div>
</li>
<!-- 普通会话 --> <!-- 普通会话 -->
<template v-if="Object.keys(conversationList).length > 0"> <template v-if="Object.keys(conversationList).length > 0">
<li <li
@ -133,7 +100,6 @@ const deleteConversation = (itemKey) => {
<div class="item_body item_main"> <div class="item_body item_main">
<div class="name"> 好友 </div> <div class="name"> 好友 </div>
<div class="last_msg_body"> <div class="last_msg_body">
<span class="last_msg_body_mention" v-if="item.isMention">[有人@]</span>
<span v-show="item.conversationType === 2">好友</span> <span v-show="item.conversationType === 2">好友</span>
{{ item.latestMessage.msg }} {{ item.latestMessage.msg }}
</div> </div>

View File

@ -1,40 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
/* 搜索框组件 */ import { defineAsyncComponent, shallowRef } from 'vue'
import { SearchInput } from '@/components/Im/SearchInput' import { SearchInput } from '@/components/Im/SearchInput'
/* 欢迎页 */
import { Welcome } from '@/components/Im/Welcome'
import ConversationList from '../Conversation/components/ConversationList.vue' import ConversationList from '../Conversation/components/ConversationList.vue'
import router from '@/router' import { Welcome } from '@/components/Im/Welcome'
//- const MessageComponent = defineAsyncComponent(() => import('@/views/im/Message/index.vue'))
const toInformDetails = () => {
router.push('/im/conversation/informDetails')
}
//- const currentComponent = shallowRef(Welcome) //
const toChatMessage = (id, chatType) => {
const toChatMessage = (id) => {
console.log('>>>>>>>id', id) console.log('>>>>>>>id', id)
router.push({ currentComponent.value = MessageComponent //
path: '/im/conversation/message',
query: {
id,
chatType
}
})
} }
</script> </script>
<template> <template>
<el-container style="height: 100%"> <el-container style="height: 100%">
<el-aside class="chat_conversation_box"> <el-aside class="chat_conversation_box">
<!-- 搜索组件 --> <!-- 搜索组件 -->
<SearchInput :searchType="'conversation'" /> <SearchInput :searchType="'conversation'" />
<div class="chat_conversation_list"> <div class="chat_conversation_list">
<ConversationList @to-inform-details="toInformDetails" @to-chat-message="toChatMessage" /> <ConversationList @to-chat-message="toChatMessage" />
</div> </div>
</el-aside> </el-aside>
<el-main class="chat_conversation_main_box"> <el-main class="chat_conversation_main_box">
<router-view /> <component :is="currentComponent" />
<Welcome />
</el-main> </el-main>
</el-container> </el-container>
</template> </template>

View File

@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<h2>系统通知</h2>
</template>
<style scoped lang="scss"></style>

View File

@ -55,7 +55,7 @@
} }
/* loading svg大小调整 */ /* loading svg大小调整 */
::v-deep .circular { :deep(.circular) {
margin-top: 8px; margin-top: 8px;
width: 25px; width: 25px;
height: 25px; height: 25px;

View File

@ -7,6 +7,8 @@ import { onClickOutside } from '@vueuse/core'
/* 组件 */ /* 组件 */
import PreviewSendImg from '../suit/previewSendImg.vue' import PreviewSendImg from '../suit/previewSendImg.vue'
import VueAt from 'vue-at/dist/vue-at-textarea' // for textarea import VueAt from 'vue-at/dist/vue-at-textarea' // for textarea
import * as MessageApi from '@/api/im/message'
import { generateUUID } from '@/utils'
const props = defineProps({ const props = defineProps({
nowPickInfo: { nowPickInfo: {
type: Object, type: Object,
@ -100,17 +102,21 @@ const sendTextMessage = _.debounce(async () => {
em_at_list: isAtAll.value ? MENTION_ALL.VALUE : _.map(atMembers.value, 'value') em_at_list: isAtAll.value ? MENTION_ALL.VALUE : _.map(atMembers.value, 'value')
} }
} }
const imMessageSendReqVO = {
clientMessageId: generateUUID(),
receiverId: nowPickInfo.value.id,
conversationType: nowPickInfo.value.chatType,
contentType: ALL_MESSAGE_TYPE.TEXT,
content: textContent.value
}
// //
if (messageQuoteRef.value?.isShowQuoteMsgBox) { if (messageQuoteRef.value?.isShowQuoteMsgBox) {
} }
textContent.value = '' textContent.value = ''
messageQuoteRef.value?.clearQuoteContent() messageQuoteRef.value?.clearQuoteContent()
try { try {
console.log('msgOptions', msgOptions) console.log('imMessageSendReqVO', imMessageSendReqVO)
// await store.dispatch('sendShowTypeMessage', { await MessageApi.sendMessage(imMessageSendReqVO)
// msgType: ALL_MESSAGE_TYPE.TEXT,
// msgOptions
// })
} catch (error) { } catch (error) {
//handleSDKErrorNotifi(error.type, error.message) //handleSDKErrorNotifi(error.type, error.message)
console.log('>>>>>>>发送失败+++++++', error) console.log('>>>>>>>发送失败+++++++', error)

View File

@ -2,60 +2,37 @@
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
/* 默认头像 */ /* 默认头像 */
import defaultAvatar from '@/assets/imgs/avatar.gif' import defaultAvatar from '@/assets/imgs/avatar.gif'
/* emits */ import { useUserStore } from '@/store/modules/user'
const emit = defineEmits(['scroll-message-list', 're-edit-message', 'message-quote']) import avatarImg from '@/assets/imgs/avatar.gif'
const messageData = ref([ import paseLink from '@/utils/paseLink.ts'
{ import fileSizeFormat from '@/utils/fileSizeFormat'
id: 1, import { messageType } from '@/constant/im'
type: 'text', const { ALL_MESSAGE_TYPE, CUSTOM_TYPE } = messageType
isRecall: false, //
time: '2024-04-01 12:00:00', const userStore = useUserStore()
from: '1', const avatar = computed(() => userStore.user.avatar ?? avatarImg)
msg: 'Hello, world!', /* props */
modifiedInfo: { const props = defineProps({
operationCount: 1 messageData: {
} type: [Array, Object],
default: () => []
}, },
{ nowPickInfo: {
id: 2, type: Object,
type: 'text', default: () => ({}),
isRecall: false, required: true
time: '2024-04-01 12:00:01',
from: '2',
msg: 'Hi, there!',
modifiedInfo: {
operationCount: 0
}
},
{
id: 3,
type: 'text',
isRecall: true,
time: '2024-04-01 12:00:02',
from: '1',
msg: 'Hello, world!',
modifiedInfo: {
operationCount: 0
}
} }
]) })
const ALL_MESSAGE_TYPE = { const { nowPickInfo } = toRefs(props)
TEXT: 'txt', const { messageData } = toRefs(props)
IMAGE: 'img',
AUDIO: 'audio',
LOCAL: 'loc',
VIDEO: 'video',
FILE: 'file',
CUSTOM: 'custom',
CMD: 'cmd',
INFORM: 'inform' //
}
/* 处理时间显示间隔 */ /* 处理时间显示间隔 */
const handleMsgTimeShow = (time, index) => { const handleMsgTimeShow = (time, index) => {
console.log('>>>>>时间显示', time, index)
const msgList = Array.from(messageData.value) const msgList = Array.from(messageData.value)
if (index !== 0) { if (index !== 0) {
const lastTime = msgList[index - 1].time const lastTime = msgList[index - 1].time
return time - lastTime > 50000 ? formatDate(time, 'MM/DD/HH:mm') : false console.log('>>>>>时间间隔', time - lastTime, time, lastTime)
return time - lastTime > 50000 ? formatDate(time, 'MM/DD/HH:mm') : ''
} else { } else {
return formatDate(time, 'MM/DD/HH:mm') return formatDate(time, 'MM/DD/HH:mm')
} }
@ -64,9 +41,15 @@ const handleMsgTimeShow = (time, index) => {
const isMyself = (msgBody) => { const isMyself = (msgBody) => {
return msgBody.from === '1' return msgBody.from === '1'
} }
/* 文本中是否包含link */
const isLink = computed(() => {
return (msg) => {
return paseLink(msg).isLink
}
})
/* 获取自己的用户信息 */ /* 获取自己的用户信息 */
const loginUserInfo = { const loginUserInfo = {
avatarurl: 'https://avatars.githubusercontent.com/u/1?v=4' avatarurl: avatar.value
} }
/* 获取他人的用户信息 */ /* 获取他人的用户信息 */
const otherUserInfo = (from) => { const otherUserInfo = (from) => {
@ -108,10 +91,6 @@ const startplayAudio = (msgBody) => {
audioPlayStatus.playMsgId = '' audioPlayStatus.playMsgId = ''
}) })
} }
//
const reEdit = (msg) => emit('reEditMessage', msg)
//
const onMsgQuote = (msg) => emit('messageQuote', msg)
</script> </script>
<template> <template>
<div> <div>
@ -123,14 +102,14 @@ const onMsgQuote = (msg) => emit('messageQuote', msg)
> >
<!-- 普通消息气泡 --> <!-- 普通消息气泡 -->
<div <div
v-if="!msgBody.isRecall && msgBody.type !== ALL_MESSAGE_TYPE.INFORM" v-if="!msgBody.isRecall && msgBody.type !== ALL_MESSAGE_TYPE.OA_NOTIFICATION"
class="message_box_item" class="message_box_item"
:style="{ :style="{
flexDirection: isMyself(msgBody) ? 'row-reverse' : 'row' flexDirection: isMyself(msgBody) ? 'row-reverse' : 'row'
}" }"
> >
<div class="message_item_time"> <div class="message_item_time">
{{ handleMsgTimeShow(msgBody.time, index) || '' }} {{ handleMsgTimeShow(msgBody.time, index) }}
</div> </div>
<el-avatar <el-avatar
class="message_item_avator" class="message_item_avator"
@ -161,12 +140,6 @@ const onMsgQuote = (msg) => emit('messageQuote', msg)
> >
<template v-if="!isLink(msgBody.msg)"> <template v-if="!isLink(msgBody.msg)">
{{ msgBody.msg }} {{ msgBody.msg }}
<!-- 已编辑 -->
<sup
style="font-size: 7px; color: #707784"
v-show="msgBody?.modifiedInfo?.operationCount"
>已编辑</sup
>
</template> </template>
<template v-else> <span v-html="paseLink(msgBody.msg).msg"> </span></template> <template v-else> <span v-html="paseLink(msgBody.msg).msg"> </span></template>
</p> </p>
@ -244,62 +217,10 @@ const onMsgQuote = (msg) => emit('messageQuote', msg)
</div> </div>
</template> </template>
</div> </div>
<!-- 右键点击弹起更多功能栏 -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="msgBody.type === ALL_MESSAGE_TYPE.TEXT && isSupported"
@click="copyTextMessages(msgBody.msg)"
>
复制
</el-dropdown-item>
<el-dropdown-item v-if="isMyself(msgBody)" @click="recallMessage(msgBody)">
撤回
</el-dropdown-item>
<el-dropdown-item
v-if="msgBody.type === ALL_MESSAGE_TYPE.TEXT && isMyself(msgBody)"
@click="showModifyMsgModal(msgBody)"
>
编辑
</el-dropdown-item>
<el-dropdown-item @click="onMsgQuote(msgBody)"> 引用 </el-dropdown-item>
<el-dropdown-item @click="deleteMessage(msgBody)"> 删除 </el-dropdown-item>
<el-dropdown-item v-if="!isMyself(msgBody)" @click="informOnMessage(msgBody)">
举报
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> </el-dropdown>
<!-- 引用消息展示框 -->
<div
class="message_quote_box"
v-if="msgBody?.ext?.msgQuote"
@click="clickQuoteMessage(msgBody.ext.msgQuote)"
>
<p>
{{ msgBody?.ext?.msgQuote?.msgSender }}{{ msgBody?.ext?.msgQuote?.msgPreview }}
</p>
</div>
</div> </div>
</div> </div>
<!-- 撤回消息通知通知 -->
<div v-if="msgBody.isRecall" class="recall_style">
{{ isMyself(msgBody) ? '你' : `${msgBody.from}` }}撤回了一条消息<span
class="reEdit"
v-show="isMyself(msgBody) && msgBody.type === ALL_MESSAGE_TYPE.TEXT"
@click="reEdit(msgBody.msg)"
>重新编辑</span
>
</div>
<!-- 灰色系统通知 -->
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.INFORM" class="inform_style">
<p>
{{ msgBody.msg }}
</p>
</div>
</div> </div>
<ReportMessage ref="reportMessage" />
<ModifyMessage ref="modifyMessageRef" />
</div> </div>
</template> </template>

View File

@ -111,7 +111,7 @@
border-radius: 0 0 3px 0; border-radius: 0 0 3px 0;
} }
::v-deep .el-drawer { :deep(.el-drawer) {
margin-top: 60px; margin-top: 60px;
width: 150px; width: 150px;
height: calc(100% - 60px); height: calc(100% - 60px);

View File

@ -4,11 +4,11 @@ import { messageType } from '@/constant/im'
/* 组件 */ /* 组件 */
import MessageList from './components/messageList/index.vue' import MessageList from './components/messageList/index.vue'
import InputBox from './components/inputBox/index.vue' import InputBox from './components/inputBox/index.vue'
import * as MessageApi from '@/api/im/message'
const { push, currentRoute } = useRouter() //
const { query } = useRoute() // const { query } = useRoute() //
const { CHAT_TYPE } = messageType const { CHAT_TYPE, ALL_MESSAGE_TYPE } = messageType
/* header 操作 */ /* header 操作 */
const drawer = ref(false) // const drawer = ref(false) //
const handleDrawer = () => { const handleDrawer = () => {
@ -25,10 +25,10 @@ const delTheFriend = () => {
} }
// //
const nowPickInfo = ref({ const nowPickInfo = ref({
id: '1', id: 1,
chatType: CHAT_TYPE.SINGLE, chatType: CHAT_TYPE.SINGLE,
userInfo: { userInfo: {
nickname: '好友1', nickname: '芋道源码',
userStatus: '1' userStatus: '1'
}, },
groupDetail: { groupDetail: {
@ -45,27 +45,76 @@ const groupDetail = computed(() => {
//id //id
const messageData = computed(() => [ const messageData = computed(() => [
{ {
type: 'text' id: 1,
} type: ALL_MESSAGE_TYPE.TEXT,
]) isRecall: false,
//getIdInfo time: '1711944110000',
const stopWatchRoute = watch( from: '1',
() => query, msg: 'Hello, world!111',
(routeVal) => { modifiedInfo: {
console.log('>>>>>>>>监听到路由参数变化', routeVal) operationCount: 1
if (routeVal) { },
// nowPickInfo.value = { ...routeVal } customExts: {
// loginState.value && getIdInfo(routeVal) nickname: '芋道源码',
} avatar: 'https://avatars.githubusercontent.com/u/2?v=4'
},
customEvent: {
type: '1',
data: {
type: '1',
data: 'https://avatars.githubusercontent.com/u/2?v=4'
}
},
file_length: 0
}, },
{ {
immediate: true id: 2,
type: ALL_MESSAGE_TYPE.TEXT,
isRecall: false,
time: '1711944221000',
from: '2',
msg: 'Hi, there!222',
modifiedInfo: {
operationCount: 0
},
customExts: {
nickname: '芋道源码',
avatar: 'https://avatars.githubusercontent.com/u/2?v=4'
},
customEvent: {
type: '1',
data: {
type: '1',
data: 'https://avatars.githubusercontent.com/u/2?v=4'
}
},
file_length: 0
},
{
id: 3,
type: ALL_MESSAGE_TYPE.TEXT,
isRecall: false,
time: '1711944332000',
from: '1',
msg: 'Hello, world!333',
modifiedInfo: {
operationCount: 0
},
customExts: {
nickname: '芋道源码',
avatar: 'https://avatars.githubusercontent.com/u/2?v=4'
},
customEvent: {
type: '1',
data: {
type: '1',
data: 'https://avatars.githubusercontent.com/u/2?v=4'
}
},
file_length: 0
} }
) ])
//route
onBeforeRouteLeave(() => {
stopWatchRoute()
})
/* 消息相关 */ /* 消息相关 */
const loadingHistoryMsg = ref(false) // const loadingHistoryMsg = ref(false) //
const isMoreHistoryMsg = ref(true) // const isMoreHistoryMsg = ref(true) //
@ -74,6 +123,10 @@ const notScrollBottom = ref(false) //是否滚动置底
const fechHistoryMessage = (loadType) => { const fechHistoryMessage = (loadType) => {
console.log(loadType) console.log(loadType)
console.log('加载更多') console.log('加载更多')
loadingHistoryMsg.value = true
setTimeout(() => {
loadingHistoryMsg.value = false
}, 1000)
} }
// //
const scrollMessageList = (direction) => { const scrollMessageList = (direction) => {
@ -96,39 +149,12 @@ const messageQuote = (msg) => inputBox.value.handleQuoteMessage(msg)
</div> </div>
<div v-else> {{ nowPickInfo.id }}<span style="font-size: 10px">(非好友)</span> </div> <div v-else> {{ nowPickInfo.id }}<span style="font-size: 10px">(非好友)</span> </div>
</template> </template>
<!-- 单人展示删除拉黑 -->
<span class="more" v-if="nowPickInfo.chatType === CHAT_TYPE.SINGLE">
<el-dropdown placement="bottom-end" trigger="click">
<svg
width="18"
height="4"
viewBox="0 0 18 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="2" cy="2" r="2" fill="#333333" />
<circle cx="9" cy="2" r="2" fill="#333333" />
<circle cx="16" cy="2" r="2" fill="#333333" />
</svg>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="delTheFriend"> 删除好友 </el-dropdown-item>
<!-- <el-dropdown-item @click="addFriendToBlackList">
加入黑名单
</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</el-header> </el-header>
<el-main class="chat_message_main"> <el-main class="chat_message_main">
<el-scrollbar class="main_container" ref="messageContainer"> <el-scrollbar class="main_container" ref="messageContainer">
<div class="innerRef"> <div class="innerRef">
<div v-show="isMoreHistoryMsg" class="chat_message_tips"> <div v-show="isMoreHistoryMsg" class="chat_message_tips">
<div <div v-show="messageData?.length" class="load_more_msg">
v-show="messageData?.length && messageData[0].type !== 'inform'"
class="load_more_msg"
>
<el-link <el-link
v-show="!loadingHistoryMsg" v-show="!loadingHistoryMsg"
:disabled="!isMoreHistoryMsg" :disabled="!isMoreHistoryMsg"
@ -140,13 +166,7 @@ const messageQuote = (msg) => inputBox.value.handleQuoteMessage(msg)
<el-link v-show="loadingHistoryMsg" disabled>消息加载中...</el-link> <el-link v-show="loadingHistoryMsg" disabled>消息加载中...</el-link>
</div> </div>
</div> </div>
<MessageList <MessageList :nowPickInfo="nowPickInfo" :messageData="messageData" />
:nowPickInfo="nowPickInfo"
:messageData="messageData"
@scroll-message-list="scrollMessageList"
@re-edit-message="reEditMessage"
@message-quote="messageQuote"
/>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-main> </el-main>

View File

@ -250,36 +250,36 @@ watch(
} }
.components { .components {
::v-deep .edit_userinfo_diglog { :deep(.edit_userinfo_diglog) {
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
} }
::v-deep .setting_func_diglog > .el-dialog__body { .setting_func_diglog :deep(.el-dialog__body) {
padding: 28px 24px 24px 24px; padding: 28px 24px 24px 24px;
} }
::v-deep .setting_func_diglog > .el-dialog__header { .setting_func_diglog :deep(.el-dialog__header) {
background: #f2f2f2; background: #f2f2f2;
margin: 0; margin: 0;
} }
::v-deep .edit_userinfo_diglog > .el-dialog__header { .edit_userinfo_diglog :deep(.el-dialog__header) {
padding: 0; padding: 0;
margin-right: 0; margin-right: 0;
} }
::v-deep .edit_userinfo_diglog > .el-dialog__body { .edit_userinfo_diglog :deep(.el-dialog__body) {
padding: 0; padding: 0;
border-radius: 4px; border-radius: 4px;
} }
::v-deep .login_diglog > .el-dialog__header { .login_diglog :deep(.el-dialog__header) {
background: #f2f2f2; background: #f2f2f2;
margin: 0; margin: 0;
} }
::v-deep .personal_setting_card > .el-dialog__header { .personal_setting_card :deep(.el-dialog__header) {
background: #f2f2f2; background: #f2f2f2;
margin: 0; margin: 0;
} }

View File

@ -1,8 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef, defineAsyncComponent, DefineComponent } from 'vue'
import NavBar from './NavBar/index.vue' import NavBar from './NavBar/index.vue'
//
const ConversationComponent = defineAsyncComponent(
() => import('@/views/im/Conversation/index.vue')
)
const currentComponent = shallowRef<DefineComponent | null>(ConversationComponent) //
defineOptions({ name: 'IM' }) defineOptions({ name: 'IM' })
</script> </script>
<template> <template>
<div class="app-container"> <div class="app-container">
<el-container class="chat_container"> <el-container class="chat_container">
@ -10,11 +19,12 @@ defineOptions({ name: 'IM' })
<NavBar /> <NavBar />
</el-aside> </el-aside>
<el-main class="chat_main_box"> <el-main class="chat_main_box">
<router-view /> <component :is="currentComponent" />
</el-main> </el-main>
</el-container> </el-container>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.app-container { .app-container {
position: fixed; position: fixed;