前台-GoEasy即时通讯

news2024/9/25 21:23:40

1.先去GoEasy官网下载源码

 

 第一步 App.vue

 

<script setup lang="ts">
	import { watch, ref, markRaw, reactive, nextTick, provide, InjectionKey } from 'vue'
	import headerIndex from '@/Layout/header/headerIndex.vue'
	import purchaseHeaderBig from '@/Layout/header/purchaseHeader/purchaseHeaderBig/index.vue'
	import purchaseHeaderSmall from '@/Layout/header/purchaseHeader/purchaseHeaderSmall/index.vue'
	import { Local } from '@/utils/storage'
	import { ElConfigProvider, ElMessage, ElScrollbar } from 'element-plus'
	import zhCn from 'element-plus/lib/locale/lang/zh-cn'
	import { useRoute, useRouter } from 'vue-router'
	import { useTokenStore, goEasyStore } from '@/store/index'
	import GoEasyUi from '@/components/goEasy/GoEasyUi.vue'
	import SvgIcon from '@/components/SvgIcon/index.vue'
	import GoEasyHome from '@/components/goEasy/Home.vue'
	import { loginInfo, dictionaryList, mouldTypeList } from '@/api/login/index'
	import { getNodeTree } from '@/api/demand/index'
	import { app as VueApp } from './main'
	const userStore = useTokenStore()
	const route = useRoute()
	const router = useRouter()
	const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>()
	let srcollTop = 0
	let beforeSrcollTop = 0
	// 个人认证成功重新请求 获取userInfo
	watch(
		() => route.fullPath,
		(newPath) => {
			// if ((newPath.includes('status=2') && userStore.status != 2) || userStore.isAdmin) {
			// }
			if (newPath.includes('status=2') && userStore.status != 2) {
				loginInfo().then((res) => {
					userStore.heardImgUrl = res.data.heardImgUrl
					Local.set('userInfo', res.data)
					userStore.nickname = res.data.username
					userStore.status = res.data.status
					userStore.appType = res.data.appType
					userStore.isSupplier = res.data.isSupplier
					if (userStore.appType == '1') {
						userStore.isAdmin = true
					} else {
						userStore.isAdmin = res.data.isAdmin
					}
				})
			}
		},
		{ immediate: true }
	)
	watch(
		() => route.path,
		(newPath) => {
			if (newPath === '/') {
				setTimeout(() => {
					scrollbarRef.value?.scrollTo({ left: 0, top: beforeSrcollTop, behavior: 'smooth' })
				}, 20)
			}
			newPath && !_isMobile() ? scrollbarRef.value?.setScrollTop(0) : ''
		}
	)
	// // 捕获异常刷新页面
	// window.addEventListener(
	// 	'error',
	// 	(error) => {
	// 		console.log('捕获异常', error)
	// 		let target: any = error.target || error.srcElement
	// 		if (target instanceof HTMLScriptElement || target instanceof HTMLLinkElement) {
	// 			router.go(0)
	// 		}
	// 	},
	// 	true
	// )

	// if (useTokenStore().token) {
	// 	LayIM.init(getCurrentInstance(), 500)
	// }

	const _isMobile = () => {
		let flag = navigator.userAgent.match(
			/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
		)
		return flag
	}

	if (_isMobile()) {
		router.push({
			name: 'temporaryMobileTerminal',
		})
	}
	dictionaryList().then((res) => {
		// Local.set('dictionary', res.data)
		let data = {} as any
		mouldTypeList().then((result) => {
			res.data.push({
				code: 'MOULD_TYPE',
				item: result.data,
			})
			res.data.forEach((item) => {
				data[item.code] = item
			})
			Local.set('dictionary', data)
		})
	})
	const goEasyService = (): void => {
		if (useTokenStore().token) {
			useTokenStore().goEasyVisible = true
		} else {
			ElMessage.warning('未登录')
			router.push('/login')
		}
	}
	const GoEasyHomeRef = ref<InstanceType<typeof GoEasyHome>>()
	useTokenStore().initGoEasy = useTokenStore().token ? true : false
	watch(
		() => useTokenStore().initGoEasy,
		(newPath) => {
			if (!newPath) {
				GoEasyHomeRef.value?.disconnect()
			} else {
				GoEasyHomeRef.value?.initGoEasy()
			}
		}
	)
	if (useTokenStore().initGoEasy) {
		GoEasyHomeRef.value?.initGoEasy()
	}

	watch(
		() => useTokenStore().backToTopFLag,
		(newValue) => {
			if (newValue) {
				useTokenStore().backToTopFLag = false
				scrollbarRef.value?.scrollTo({
					left: 0,
					top: 0,
					behavior: 'smooth',
				})
			}
		}
	)

	const scroll = (data) => {
		if (route.path === '/') {
			srcollTop = data.scrollTop
		}
		// if (route.fullPath == '/purchase/purchaseLogin' || route.fullPath == '/purchase/login/purchaseRegister') {
		// 	return true
		// }
		// if (route.fullPath.indexOf('purchase') !== -1) {
		// 	if (data.scrollTop >= 100) {
		// 		// 集采平台
		// 		currentHeaderComponent.comp = headerComponent[2].com
		// 		headerHeight.value = 'height:100px'
		// 	} else {
		// 		// 集采平台
		// 		currentHeaderComponent.comp = headerComponent[1].com
		// 		headerHeight.value = 'height:194px'
		// 	}
		// }
	}

	router.beforeEach((to, from) => {
		if (from.path === '/') {
			beforeSrcollTop = srcollTop
		}
	})

	const headerComponent = reactive<any[]>([
		{
			name: '设计制造平台头部',
			com: markRaw(headerIndex),
		},
		{
			name: '集采平台大头部',
			com: markRaw(purchaseHeaderBig),
		},
		{
			name: '集采平台小头部',
			com: markRaw(purchaseHeaderSmall),
		},
	])
	let currentHeaderComponent = reactive<any>({
		comp: headerComponent[0].com,
	})
	const headerHeight = ref('')
	const isShowPurchaseHeader = ref(false)
	watch(
		() => route,
		(newRoute) => {
			if (newRoute.fullPath == '/purchase/purchaseLogin' || newRoute.fullPath == '/purchase/login/purchaseRegister') {
				currentHeaderComponent.comp = headerComponent[0].com
				headerHeight.value = 'height:64px'
				isShowPurchaseHeader.value = false
			} else if (newRoute.fullPath.indexOf('purchase') !== -1) {
				// 集采平台
				// currentHeaderComponent.comp = headerComponent[1].com
				headerHeight.value = 'height:194px;display:none'
				isShowPurchaseHeader.value = true
			} else {
				// 设计制造平台
				currentHeaderComponent.comp = headerComponent[0].com
				headerHeight.value = 'height:64px'
				isShowPurchaseHeader.value = false
			}
		},
		{ immediate: true, deep: true }
	)

	// 全局获取nodeTree
	const map = ref<any>({})
	const mapChildren = ref<any>({})

	const getAllNodeTree = async () => {
		VueApp.config.globalProperties.$treeNodeMap = (id: any) => {
			return map.value[id]
		}
		VueApp.config.globalProperties.$treeNodeMapCode = (id: any) => {
			return mapChildren.value[id]
		}
		const { data } = await getNodeTree()
		Local.set('qt_nodeTree', JSON.stringify(data))
		// 设置全局id和name的字典库
		loopData(data)
	}
	const loopData = (arr) => {
		for (let i = 0; i < arr.length; i++) {
			const current = arr[i]
			map.value[current.nodeId] = current.name
			if (current.children.length > 0)
				mapChildren.value[current.nodeId] = current.children.map((item) => item.nodeCode).filter((item) => item != null)

			if (current.children) {
				loopData(current.children)
			}
		}
	}
	getAllNodeTree()
	const isRouterAlive = ref(true)
	const reload = () => {
		isRouterAlive.value = false
		nextTick(() => {
			isRouterAlive.value = true
		})
	}

	provide(`reload`, reload)
	// console.log(mapChildren.value['1664531691763863553'], 'mapChildren.valuemapChildren.valuemapChildren.value')
</script>

<template>
	<div ref="main-test" class="common-layout">
		<el-container v-if="!_isMobile()">
			<el-header :style="headerHeight"> <component :is="currentHeaderComponent.comp"></component></el-header>
			<el-scrollbar ref="scrollbarRef" class="appMain" @scroll="scroll">
				<el-config-provider :locale="zhCn">
					<el-main style="min-width: 1200px"
						><div class="mainDev">
							<purchaseHeaderBig v-if="isShowPurchaseHeader" />
							<router-view v-if="isRouterAlive" v-slot="{ Component }">
								<component :is="Component" v-if="!route.meta.keepAlive" :key="route.name" />
								<keep-alive>
									<component :is="Component" v-if="route.meta.keepAlive" :key="route.name" />
								</keep-alive>
							</router-view>
							<GoEasyUi v-if="useTokenStore().initGoEasy"></GoEasyUi>
							<!-- <GoEasyHome v-if="useTokenStore().initGoEasy" ref="GoEasyHomeRef" style="display: none"></GoEasyHome> -->
							<div v-if="useTokenStore().initGoEasy" class="service" @click="goEasyService">
								<SvgIcon icon-class="_comments" size="24" />
								<span v-if="goEasyStore().unreadAmount" class="menu-unread">{{ goEasyStore().unreadAmount }}</span>
							</div>
						</div>
					</el-main>
				</el-config-provider>
			</el-scrollbar>
		</el-container>
		<el-container v-else>
			<el-config-provider :locale="zhCn">
				<el-main
					><div class="mainDev">
						<router-view />
					</div>
				</el-main>
			</el-config-provider>
		</el-container>
	</div>
</template>

<style scoped lang="scss">
	:deep(.el-scrollbar__wrap) {
		background: #f5f4f4;
	}
	.el-overlay {
		z-index: 9999999999998 !important;
	}
	#app {
		width: 100%;
		font-size: 14px;
	}
	.common-layout {
		overflow: hidden !important;
	}

	.logo {
		height: 6em;
		padding: 1.5em;
		will-change: filter;
	}
	.text {
		color: aqua;
	}
	.logo:hover {
		filter: drop-shadow(0 0 2em #646cffaa);
	}
	.logo.vue:hover {
		filter: drop-shadow(0 0 2em #42b883aa);
	}
	.el-scrollbar__view {
		height: auto;
	}
	.service {
		position: fixed;
		display: flex;
		right: 0;
		width: 42px;
		height: 42px;
		bottom: 183px;
		border-radius: 50%;
		background: radial-gradient(50% 50% at 50% 50%, #ffffff 0%, #fafafa 58%, #f6f6f6 100%);
		box-sizing: border-box;
		border: 1px solid #ffffff;
		box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
		align-items: center;
		justify-content: center;
		margin-right: 16px;
		cursor: pointer;
		z-index: 99;
		.svg-icon {
			width: 24px !important;
			height: 24px;
		}
	}
	.menu-unread {
		position: absolute;
		top: -5px;
		right: 5px;
		width: 18px;
		height: 18px;
		line-height: 18px;
		text-align: center;
		border-radius: 50%;
		background-color: #d02129;
		color: #ffffff;
	}
</style>

第二步 聊天弹框   GoEasyUi.vue

<script setup lang="ts" name="">
	import { ref, watch } from 'vue'
	import GoEasyHome from '@/components/goEasy/Home.vue'
	import { useTokenStore, goEasyStore } from '@/store/index'
	const dialogVisible = ref(false)
	watch(
		() => useTokenStore().goEasyVisible,
		(newValue) => {
			if (newValue) {
				dialogVisible.value = newValue
			}
		},
		{ immediate: true }
	)
	const GoEasyHomeRef = ref<InstanceType<typeof GoEasyHome>>()
	const handleClose = (): void => {
		dialogVisible.value = false
		useTokenStore().messageHistory = false
		useTokenStore().goEasyVisible = false
		// goEasyStore().privateChatVisible = false
	}
</script>
<template>
	<el-dialog
		v-model="dialogVisible"
		:close-on-click-modal="false"
		:modal="false"
		destroy-on-close
		width="850px"
		:loading="!goEasyStore().privateChatVisible"
		:class="useTokenStore().messageHistory ? 'goEasy goEasys' : 'goEasy'"
		@close="handleClose">
		<div class="chat">
			<el-container>
				<el-main>
					<GoEasyHome ref="GoEasyHomeRef"></GoEasyHome>
				</el-main>
			</el-container>
		</div>
	</el-dialog>
</template>

第三步  home.vue

<script setup lang="ts">
	import { currentUserType, currentUserInfoType } from './type/goEasyType'
	import { Local } from '@/utils/storage'
	import { ref, getCurrentInstance, markRaw } from 'vue'
	import { goEasyOtpApi } from './api/goEasyApi'
	import Conversations from './Conversations.vue'
	import Contacts from './Contacts.vue'
	import { goEasyStore, useTokenStore } from '@/store/index'
	const currentUser = ref({} as currentUserType)
	const goEasy: any = getCurrentInstance()?.proxy?.goEasy
	const GoEasy: any = getCurrentInstance()?.proxy?.GoEasy
	const goEasyData = ref(Local.get('userInfo') as currentUserInfoType)
	const goEasyOtpData = ref<string>('')
	const initGoEasy = (): void => {
		currentUser.value.id = goEasyData.value?.tenantCode
		currentUser.value.avatar = goEasyData.value?.avatar
		currentUser.value.phone = goEasyData.value?.mobile
		currentUser.value.email = goEasyData.value?.email
		currentUser.value.name = goEasyData.value?.tenantName
		goEasyOtpApi().then((result) => {
			goEasyOtpData.value = result.data
			console.log(result.data, 'goEasyOtpData.value')

			console.log(goEasy.getConnectionStatus(), useTokenStore().initGoEasy, 478392749238)

			if (goEasy.getConnectionStatus() === 'disconnected' && useTokenStore().initGoEasy) {
				connectGoEasy()
			}
			menuChange(itemNames.value)
			goEasy.im?.on(GoEasy?.IM_EVENT?.CONVERSATIONS_UPDATED, setUnreadNumber)
		})
	}
	const disconnect = (): void => {
		goEasy.disconnect({
			onSuccess: function () {
				console.log('GoEasy disconnect successfully.')
			},
			onFailed: function (error) {
				console.log('Failed to disconnect GoEasy, code:' + error.code + ',error:' + error.content)
			},
		})
	}
	const currentComponentList = ref([
		{
			name: '当前会话',
			com: markRaw(Conversations),
		},
		{
			name: '好友列表',
			com: markRaw(Contacts),
		},
	])
	const currentComponent = ref()
	initGoEasy()
	defineExpose({
		initGoEasy,
		disconnect,
	})
	const connectGoEasy = () => {
		goEasy.connect({
			otp: goEasyOtpData.value,
			id: currentUser.value.id,
			data: { name: currentUser.value.name, avatar: currentUser.value.avatar },
			onSuccess: function () {
				//连接成功
				console.log('GoEasy connect successfully.') //连接成功
			},
			onFailed: function (error: { code: string; content: string }) {
				//连接失败
				console.log('Failed to connect GoEasy, code:' + error.code + ',error:' + error.content)
			},
			onProgress: function (attempts: any) {
				//连接或自动重连中
				console.log('GoEasy is connecting', attempts)
			},
		})
	}
	const setUnreadNumber = (content: { conversations: any[]; unreadTotal: string }) => {
		goEasyStore().unreadAmount = 0
		console.log(content.conversations, 'content.conversations')

		content.conversations.map((item) => {
			if (item.userId === goEasyStore().userId) {
				item.unread = 0
			}

			goEasyStore().unreadAmount += item.unread
		})
	}
	const itemNames = ref('当前会话')
	const menuChange = (itemName: string) => {
		itemNames.value = itemName
		currentComponent.value = currentComponentList.value.find((v) => v.name == itemName)?.com
	}
</script>

<template>
	<div class="home">
		<div class="home-container">
			<div class="home-menu">
				<div class="menu-header">
					<img class="user-avatar" :src="currentUser.avatar" />
					<div class="user-profile">
						<div class="user-profile-main">
							<div class="user-profile-header">
								<img :src="currentUser.avatar" />
								<div>{{ currentUser.name }}</div>
							</div>
							<div class="user-profile-info">
								<div class="user-profile-info-title">邮箱</div>
								<div>{{ currentUser.email }}</div>
							</div>
							<div class="user-profile-info">
								<div class="user-profile-info-title">手机</div>
								<div>{{ currentUser.phone }}</div>
							</div>
						</div>
					</div>
				</div>
				<div class="menu-box">
					<div class="menu-list">
						<div :class="itemNames == '当前会话' ? 'menu-item i_t' : 'menu-item'" @click="menuChange('当前会话')">
							<i class="iconfont icon-zaixiankefu"></i>
							<span v-if="goEasyStore().unreadAmount" class="menu-unread">{{ goEasyStore().unreadAmount }}</span>
						</div>
						<!-- <div :class="itemNames == '好友列表' ? 'menu-item i_t' : 'menu-item'" @click="menuChange('好友列表')">
							<i class="iconfont icon-haoyou"></i>
						</div> -->
					</div>
				</div>
			</div>
			<div class="home-main">
				<component :is="currentComponent" v-if="useTokenStore().goEasyVisible"></component>
				<!-- <router-view /> -->
			</div>
		</div>
	</div>
</template>

<style scoped>
	@media screen and (max-height: 720px) {
		.home-container {
			min-width: 850px;

			height: 500px;
		}
	}
	@media screen and (min-height: 720px) {
		.home-container {
			min-width: 850px;
			height: 600px;
		}
	}

	.home {
		width: 100%;
		height: 100%;
		display: flex;
	}

	.home-container {
		/* width: 959px; */
		background: #ffffff;
		display: flex;
		position: relative;
		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
	}

	.home-menu {
		width: 60px;
		background-color: #f7f7f7;
		border-right: 1px solid #eeeeee;
		display: flex;
		flex-direction: column;
		align-items: center;
	}

	.menu-header {
		margin: 20px auto;
	}

	.user-avatar {
		width: 28px;
		height: 28px;
		border-radius: 10px;
		cursor: pointer;
	}

	.user-avatar:hover + .user-profile {
		display: block;
	}

	.user-profile {
		display: none;
		color: #ffffff;
		position: absolute;
		top: 0;
		left: 59px;
		width: 250px;
		height: 200px;
		background: #ffffff;
		z-index: 999;
	}

	.user-profile-main {
		border: 1px solid #ebeef5;
		background-color: #fff;
		color: #303133;
		border-radius: 4px;
	}

	.user-profile-header {
		padding: 18px 20px;
		border-bottom: 1px solid #ebeef5;
		display: flex;
		flex-direction: column;
		align-items: center;
		font-size: 15px;
		font-weight: bold;
	}

	.user-profile-header img {
		width: 45px;
		height: 45px;
	}

	.user-profile-info {
		display: flex;
		padding: 10px 20px;
		font-size: 14px;
		color: #666666;
		line-height: 28px;
	}

	.user-profile-info-title {
		width: 70px;
	}

	.menu-box {
		padding: 40px 0;
		flex: 1;
		display: flex;
		flex-direction: column;
		justify-content: space-between;
		align-items: center;
	}

	.menu-list {
		display: flex;
		flex-direction: column;
		align-items: center;
	}

	.menu-item {
		color: #303133;
		cursor: pointer;
		height: 56px;
		position: relative;
	}

	.menu-unread {
		position: absolute;
		top: -5px;
		right: 5px;
		width: 18px;
		height: 18px;
		line-height: 18px;
		text-align: center;
		border-radius: 50%;
		background-color: #d02129;
		color: #ffffff;
	}

	.i_t > i {
		color: #d02129 !important;
	}

	.iconfont {
		padding: 15px;
		font-size: 18px;
		color: #606266;
		cursor: pointer;
	}

	.iconfont:active {
		color: #d02129;
	}

	.home-main {
		padding: 0;
		flex: 1;
	}
</style>

第四步 当前会话组件  Conversations.vue

<template>
	<div class="conversations">
		<div class="conversation-list">
			<div class="conversation-list-container">
				<div class="conversation-list-content">
					<div v-if="conversations.length">
						<div v-for="(conversation, key) in conversations" :key="key" replace @click="chatLocation(conversation)">
							<div
								class="conversation"
								:style="{
									background:
										conversation.id == goEasyStore().userId || conversation.userId == goEasyStore().userId
											? '#EBEEF5'
											: '',
								}"
								@contextmenu.prevent.stop="(e) => showRightClickMenu(e, conversation)">
								<div class="avatar">
									<el-image style="width: 40px; height: 40px" :src="conversation.data.avatar" fit="scale-down" />
									<!-- <img :src="conversation.data.avatar" width="40" height="40" /> -->
									<div v-if="conversation.unread > 0" class="unread-count">
										<span class="unread">{{ conversation.unread }}</span>
									</div>
								</div>
								<div class="conversation-message">
									<div class="conversation-top">
										<span class="conversation-name">{{ conversation.data.name }}</span>
										<div class="conversation-time">
											<div>{{ formatDate(conversation.lastMessage?.timestamp) }}</div>
										</div>
									</div>
									<div v-if="conversation.lastMessage" class="conversation-bottom">
										<div v-if="conversation.lastMessage.recalled" class="conversation-content">
											<div v-if="conversation.type === 'private'">
												{{
													conversation.lastMessage.senderId === currentUser.id
														? '你'
														: `"${conversation.data.name}"`
												}}撤回了一条消息
											</div>
											<div v-if="conversation.type === 'group'">
												{{
													conversation.lastMessage.senderId === currentUser.id
														? '你'
														: `"${conversation.lastMessage.senderData.name}"`
												}}撤回了一条消息
											</div>
										</div>
										<div v-else class="conversation-content">
											<div
												v-if="
													conversation.lastMessage.read === false &&
													conversation.lastMessage.senderId === currentUser.id
												"
												class="unread-text">
												[未读]
											</div>
											<div v-if="conversation.type === 'private'">
												{{ conversation.lastMessage.senderId === currentUser.id ? '我' : conversation.data.name }}:
											</div>
											<div v-else>
												{{
													conversation.lastMessage.senderId === currentUser.id
														? '我'
														: conversation.lastMessage.senderData.name
												}}:
											</div>
											<span v-if="conversation.lastMessage.type === 'text'" class="text">{{
												conversation.lastMessage.payload.text
											}}</span>
											<span v-else-if="conversation.lastMessage.type === 'video'">[视频消息]</span>
											<span v-else-if="conversation.lastMessage.type === 'audio'">[语音消息]</span>
											<span v-else-if="conversation.lastMessage.type === 'qt_image'">[图片消息]</span>
											<span v-else-if="conversation.lastMessage.type === 'qt_file'">[文件消息]</span>
											<span v-else-if="conversation.lastMessage.type === 'order'">[订单消息]</span>
										</div>
									</div>
								</div>
							</div>
						</div>
					</div>
					<div v-else class="no-conversation">- 当前没有会话 -</div>
				</div>
			</div>
			<div v-if="rightClickMenu.visible" :style="{ left: rightClickMenu.x + 'px', top: rightClickMenu.y + 'px' }" class="action-box">
				<div class="action-item" @click="topConversation">{{ rightClickMenu?.conversation?.top ? '取消置顶' : '置顶' }}</div>
				<div class="action-item" @click="deleteConversation">删除聊天</div>
			</div>
		</div>
		<div class="chat">
			<PrivateChat v-if="goEasyStore().privateChatVisible" :key="goEasyStore().userId"></PrivateChat>
			<!-- <router-view :key="$route.params.id" /> -->
		</div>
	</div>
</template>

<script setup lang="ts">
	import { formatDate } from '@/utils/GoEasyData'
	import PrivateChat from './PrivateChat.vue'
	import { conversationsType, currentUserInfoType, currentUserType, friendType } from './type/goEasyType'
	import { Local } from '@/utils/storage'
	import { useTokenStore, goEasyStore } from '@/store/index'
	import { goEasyServiceId } from './api/goEasyApi'
	import { ref, getCurrentInstance, onBeforeUnmount } from 'vue'
	// const privateChatVisible = ref(goEasyStore().privateChatVisible)
	// console.log('🚀 ~ file: Conversations.vue:95 ~ privateChatVisible:', privateChatVisible.value)
	const currentUser = ref({} as currentUserType)
	const conversations = ref<Array<conversationsType>>([])
	const conversationPorp = ref({} as conversationsType)
	const goEasy: any = getCurrentInstance()?.proxy?.goEasy
	const GoEasy: any = getCurrentInstance()?.proxy?.GoEasy
	const goEasyData = ref(Local.get('userInfo') as currentUserInfoType)
	const rightClickMenu = ref<any>({
		conversation: '',
		visible: false,
		x: null,
		y: null,
	})
	currentUser.value.id = goEasyData.value.tenantCode
	currentUser.value.avatar = goEasyData.value.avatar
	currentUser.value.phone = goEasyData.value.mobile
	currentUser.value.name = goEasyData.value.tenantName
	const serviceIdData = ref({} as friendType)
	document.querySelector('.home')?.addEventListener('click', () => {
		hideRightClickMenu()
	})
	onBeforeUnmount(() => {
		goEasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, renderConversations)
	})
	formatDate
	const loadConversations = async () => {
		console.log(serviceIdData.value, 'fsdklfjsdklfjksdl')

		await goEasyServiceId()
			.then((result) => {
				serviceIdData.value = result.data
				console.log('111')

				console.log('🚀 ~ file: Conversations.vue:138 ~ goEasyServiceId111111111111 ~ result:', result)
			})
			.catch((err) => {
				console.log('🚀 ~ file: Conversations.vue:141 ~ goEasyServiceId ~ err:', err)
			})
		goEasy.im.latestConversations({
			onSuccess: (result: { content: any }) => {
				console.log('🚀 ~ file: Conversations.vue:133 ~ loadConversations ~ result:', result)
				let content = result.content
				renderConversations(content)
			},
			onFailed: (error) => {
				console.log('获取最新会话列表失败, code:' + error.code + 'content:' + error.content)
			},
		})
	}
	const listenConversationUpdate = () => {
		// 监听会话列表变化
		goEasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, renderConversations)
	}

	const renderConversations = (content: { conversations: any }) => {
		console.log('2222')

		let serviceData = false
		content.conversations.map((item: { type: string; unread: number; top: boolean }) => {
			if (item.type === 'cs') {
				console.log('true')

				serviceData = true
			}
		})
		console.log(content.conversations)

		console.log(serviceData, 'serviceData')

		if (!serviceData) {
			content.conversations.unshift({
				data: {
					name: serviceIdData.value.name,
					avatar: serviceIdData.value.avatar,
				},
				top: true,
				type: 'cs',
				unread: 0,
				id: serviceIdData.value.id,
			})
		}

		conversations.value = content.conversations
	}
	// const subscribeGroup = () => {
	// 	let groups = restApi.findGroups(currentUser.value)
	// 	let groupIds = groups.map((item) => item.id)
	// 	goEasy.im.subscribeGroup({
	// 		groupIds: groupIds,
	// 		onSuccess: function () {
	// 			console.log('订阅群消息成功')
	// 		},
	// 		onFailed: function (error) {
	// 			console.log('订阅群消息失败:', error)
	// 		},
	// 	})
	// }
	const showRightClickMenu = (e: any, conversation: any) => {
		if (conversation.type == 'cs') return
		rightClickMenu.value.conversation = conversation
		rightClickMenu.value.visible = true
		rightClickMenu.value.x = e.pageX
		rightClickMenu.value.y = e.pageY
	}
	const hideRightClickMenu = () => {
		rightClickMenu.value.visible = false
	}
	const topConversation = () => {
		let conversation: any = rightClickMenu.value.conversation
		let description = conversation.top ? '取消置顶' : '置顶'
		goEasy.im.topConversation({
			conversation: conversation,
			top: !conversation.top,
			onSuccess: function () {
				console.log(description, '成功')
			},
			onFailed: function (error) {
				console.log(description, '失败:', error)
			},
		})
	}
	const deleteConversation = () => {
		if (confirm('确认要删除这条会话吗?')) {
			let conversation = rightClickMenu.value.conversation
			goEasy.im.removeConversation({
				conversation: conversation,
				onSuccess: function () {
					console.log('删除会话成功')
				},
				onFailed: function (error: any) {
					console.log(error)
				},
			})
		}
	}
	const chatLocation = (conversation: conversationsType) => {
		conversationPorp.value = conversation
		console.log('🚀 ~ file: Conversations.vue:211 ~ chatLocation ~ conversation:', conversation)
		goEasyStore().$patch({
			userId: conversation.type == 'cs' ? conversation.id : conversation.userId,
			name: conversation.data.name,
			avatar: conversation.data.avatar,
			privateChatVisible: true,
			goEasyType: conversation.type,
			receiverId: conversation.type == 'cs' ? conversation.id : conversation.userId,
		})
		console.log(goEasyStore().receiverId, 'goEasyStore()goEasyStore()goEasyStore()')

		useTokenStore().messageHistory = false
	}
	listenConversationUpdate() //监听会话列表变化
	loadConversations() //加载会话列表
	// subscribeGroup() //订阅群消息
</script>

<style scoped>
	.conversations {
		width: 100%;
		height: 100%;
		position: relative;
		display: flex;
		color: #333333;
	}

	.conversation-list {
		width: 220px;
	}

	.conversation-list-container {
		height: 100%;
		display: flex;
		flex-direction: column;
		background-color: white;
		border-right: #dbd6d6 1px solid;
	}

	.conversation-list-content {
		flex: 1;
		overflow-y: auto;
		padding: 10px 0;
		scrollbar-width: none;
		-ms-overflow-style: none;
	}

	.conversation-list-content::-webkit-scrollbar {
		display: none;
	}

	.no-conversation {
		text-align: center;
		color: #666666;
	}

	.conversation {
		display: flex;
		padding: 10px 5px;
		cursor: pointer;
	}

	.unread-count {
		position: absolute;
		top: -10px;
		left: 30px;
		width: 18px;
		height: 18px;
		border-radius: 50%;
		color: white;
		background: #d02129;
	}

	.unread-count .unread {
		display: block;
		font-size: 12px;
		text-align: center;
		line-height: 18px;
	}

	.conversation-message {
		flex: 1;
		padding-left: 5px;
		display: flex;
		width: 160px;
		flex-direction: column;
		justify-content: space-around;
		font-size: 12px;
	}

	.conversation-top {
		display: flex;
		align-items: center;
		justify-content: space-between;
		text-align: right;
	}

	.conversation-name {
		width: 89px;
		white-space: nowrap;
		text-overflow: ellipsis;
		overflow: hidden;
		font-size: 12px;
		font-weight: 500;
		text-align: left;
	}

	.conversation-time {
		width: 80px;
		color: #b9b9b9;
		font-size: 12px;
		display: flex;
		flex-direction: column;
	}

	.conversation-bottom {
		display: flex;
		color: #666666;
	}

	.conversation-content {
		display: flex;
		width: 160px;
		color: #b3b3b3;
	}

	.conversation-content .text {
		overflow: hidden;
		text-overflow: ellipsis;
		flex: 1;
		white-space: nowrap;
		word-break: break-all;
	}

	.conversation-bottom .unread-text {
		color: #d02129;
		/* width: 35px !important; */
	}

	.conversation .avatar {
		width: 40px;
		height: 40px;
		position: relative;
	}

	.conversation .avatar img {
		width: 100%;
		border-radius: 10%;
	}

	.router-link-active {
		background: #eeeeee;
	}

	.action-box {
		width: 100px;
		height: 60px;
		background: #ffffff;
		border: 1px solid #cccccc;
		position: fixed;
		z-index: 100;
		border-radius: 5px;
	}

	.action-box .action-item {
		padding-left: 15px;
		line-height: 30px;
		font-size: 13px;
		color: #262628;
		cursor: pointer;
	}

	.action-box .action-item:hover {
		background: #dddddd;
	}

	.chat {
		min-width: 570px;
		display: flex;
	}
</style>

第五步 私聊组件  PrivateChat.vue

<!-- eslint-disable @typescript-eslint/no-non-null-assertion -->
<!-- eslint-disable vue/no-setup-props-destructure -->
<template>
	<div class="chat-container">
		<div class="chat-title">
			<!-- <img :src="friend.avatar" class="chat-avatar" /> -->
			<div class="chat-name">{{ friend.name }}</div>
		</div>
		<div ref="scrollView" class="chat-main">
			<div ref="messageList" class="message-list">
				<div v-if="history.loading" class="history-loading">
					<img src="@/assets/images/pending.gif" />
				</div>
				<div v-else :class="history.allLoaded ? 'history-loaded' : 'load'" @click="loadHistoryMessage(false)">
					{{ history.allLoaded ? '已经没有更多的历史消息' : '获取历史消息' }}
				</div>
				<div v-for="(message, index) in history.messages" :key="index">
					<div class="time-tips">{{ renderMessageDate(message, index) }}</div>
					<div v-if="message.recalled" class="message-recalled">
						<div v-if="message.senderId !== currentUser.id">{{ friend.name }}撤回了一条消息</div>
						<div v-else class="message-recalled-self">
							<div>你撤回了一条消息</div>
							<span
								v-if="message.type === 'text' && Date.now() - message.timestamp < 60 * 1000"
								@click="editRecalledMessage(message.payload.text)"
								>重新编辑</span
							>
						</div>
					</div>
					<div v-else class="message-item">
						<div v-if="messageSelector.visible && message.status !== 'sending'" class="message-item-checkbox">
							<input
								v-model="messageSelector.ids"
								class="input-checkbox"
								type="checkbox"
								:value="message.messageId"
								@click="selectMessages" />
						</div>
						<div v-if="message.type === 'CS_ACCEPT'" class="accept-message">{{ message.senderData.name }}已接入</div>

						<div v-else-if="message.type === 'CS_END'" class="accept-message">{{ message.senderData.name }}已结束会话</div>
						<div v-else-if="message.type === 'CS_PRE_AUTO_END'" class="accept-message">
							请问还在线吗?如没有问题,系统将在{{ message.payload.text }}秒后自动结束会话
						</div>
						<div v-else-if="message.type === 'CS_TRANSFER'" class="accept-message">
							{{ message.senderData.name + `已转接给` + message.payload.transferTo.data.name }}
						</div>
						<div class="message-item-content" :class="{ self: message.senderId === currentUser.id }">
							<div v-if="message.senderData" class="sender-info">
								<img v-if="currentUser.id === message.senderId" :src="message.senderData.avatar" class="sender-avatar" />
								<img v-else :src="message.senderData.avatar" class="sender-avatar" />
							</div>
							<div v-else class="sender-info">
								<img v-if="currentUser.id === message.senderId" :src="currentUser.avatar" class="sender-avatar" />
								<img v-else :src="friend.avatar" class="sender-avatar" />
							</div>
							<div class="message-content" @click.right="showActionPopup(message)">
								<div class="message-payload">
									<div v-if="message.status === 'sending'" class="pending"></div>
									<div v-if="message.status === 'fail'" class="send-fail"></div>
									<!-- eslint-disable-next-line vue/no-v-html -->
									<div
										v-if="message.type === 'text'"
										class="content-text"
										v-html="emoji.decoder.decode(message.payload.text)"></div>
									<div
										v-if="message.type === 'qt_image'"
										class="content-image"
										@click="showImagePreviewPopup(message.payload.url)">
										<img
											:src="message.payload.url"
											:style="{ height: getImageHeight(message.payload.width, message.payload.height) + 'px' }" />
										<!--  -->
									</div>
									<a v-if="message.type === 'qt_file'" :href="message.payload.url" target="_blank" download="download">
										<div class="content-file" title="点击下载">
											<div class="file-info">
												<span class="file-name">{{ message.payload.name }}</span>
												<span class="file-size">{{ (message.payload.size / 1024).toFixed(2) }}KB</span>
											</div>
											<img class="file-img" src="../../assets/images/file.png" />
										</div>
									</a>
									<div v-if="message.type === 'audio'" class="content-audio" @click="playAudio(message)">
										<div class="audio-facade" :style="{ width: Math.ceil(message.payload.duration) * 7 + 50 + 'px' }">
											<div
												class="audio-facade-bg"
												:class="{ 'play-icon': audioPlayer.playingMessage === message }"></div>
											<div>{{ Math.ceil(message.payload.duration) || 1 }}<span>"</span></div>
										</div>
									</div>
									<!-- <goeasy-video-player
										v-if="message.type === 'videos'"
										:thumbnail="message.payload.thumbnail"
										:src="message.payload.video.url" /> -->
									<div v-if="message.type === 'order'" class="content-order">
										<div class="order-id">订单号:{{ message.payload.id }}</div>
										<div class="order-body">
											<img :src="message.payload.url" class="order-img" />
											<div class="order-name">{{ message.payload.name }}</div>
											<div>
												<div class="order-price">{{ message.payload.price }}</div>
												<div class="order-count">共{{ message.payload.count }}件</div>
											</div>
										</div>
									</div>
								</div>
								<div v-if="currentUser.id === message.senderId" :class="message.read ? 'message-read' : 'message-unread'">
									<div v-if="message.senderId === currentUser.id && goEasyStore().goEasyType != 'cs'">
										{{ message.read ? '已读' : '未读' }}
									</div>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
		<div class="chat-footer">
			<div v-if="messageSelector.visible" class="action-delete">
				<img class="delete-btn" src="../../assets/images/delete.png" @click="deleteMultipleMessages" />
				<div>删除</div>
			</div>
			<div v-else class="action-box">
				<div class="action-bar">
					<!-- 表情 -->
					<div class="action-item">
						<div v-if="emoji.visible" class="emoji-box">
							<img
								v-for="(emojiItem, emojiKey, index) in emoji.map"
								:key="index"
								class="emoji-item"
								:src="emoji.url + emojiItem"
								@click="chooseEmoji(emojiKey)" />
						</div>
						<i class="iconfont icon-smile" title="表情" @click="emoji.visible = !emoji.visible"></i>
					</div>
					<!-- 图片 -->
					<div class="action-item">
						<label for="img-input">
							<i class="iconfont icon-picture" title="图片"></i>
						</label>
						<!-- <up-load-avatar ref="upLoadAvatarRef" title="" :font-size="14" @upload-url="getLogoUrl"></up-load-avatar> -->

						<input v-show="false" id="img-input" accept="image/*" multiple type="file" @change="sendImageMessage" />
					</div>
					<!-- 视频 -->
					<!-- <div class="action-item">
						<label for="video-input"><i class="iconfont icon-film" title="视频"></i></label>
						<input v-show="false" id="video-input" accept="video/*" type="file" @change="sendVideoMessage" />
					</div> -->
					<!-- 文件 -->
					<div class="action-item">
						<label for="file-input">
							<i class="iconfont icon-wj-wjj" title="文件"></i>
						</label>
						<input v-show="false" id="file-input" type="file" @change="sendFileMessage" />
					</div>
					<!-- 自定义-订单消息 -->
					<!-- <div class="action-item">
						<i class="iconfont icon-liebiao" title="订单" @click="showOrderMessageList"></i>
					</div> -->
					<div
						class="action-item last-action-item"
						:style="{ color: useTokenStore().messageHistory ? '#3760F1' : '' }"
						@click="messageHistory">
						<SvgIcon v-if="useTokenStore().messageHistory" icon-class="active_history" />
						<SvgIcon v-else icon-class="_history" />
						聊天记录
						<!-- <i class="iconfont icon-liebiao" title="订单" @click="showOrderMessageList"></i> -->
					</div>
				</div>

				<!-- GoEasyIM最大支持3k的文本消息,如需发送长文本,需调整输入框maxlength值 -->
				<div class="input-box">
					<textarea
						ref="input"
						v-model="text"
						maxlength="700"
						autocomplete="off"
						class="input-content"
						@focus="onInputFocus"
						@keyup.enter="sendTextMessage"></textarea>
				</div>
				<div class="send-box">
					<el-button type="primary" @click="sendTextMessage">发送</el-button>
				</div>
			</div>
		</div>
		<!-- 语音播放器 -->
		<audio ref="audioPlayerRef" @ended="onAudioPlayEnd" @pause="onAudioPlayEnd"></audio>
		<!-- 图片预览弹窗 -->
		<el-image-viewer v-if="imagePreview.visible" :url-list="[imagePreview.url]" @close="hideImagePreviewPopup" />
		<!-- <div v-if="imagePreview.visible" class="image-preview">
			<img :src="imagePreview.url" alt="图片" />
			<span class="close" @click="hideImagePreviewPopup">×</span>
		</div> -->
		<!-- 消息删除撤回弹窗 -->
		<!-- <div v-if="actionPopup.visible" class="action-popup" @click="actionPopup.visible = false">
			<div class="action-popup-main">
				<div class="action-item" @click="deleteSingleMessage">删除</div>
				<div v-if="actionPopup.recallable" class="action-item" @click="recallMessage">撤回</div>
				<div class="action-item" @click="showCheckBox">多选</div>
				<div class="action-item" @click="actionPopup.visible = false">取消</div>
			</div>
		</div> -->
		<!-- 订单弹窗 -->
		<!-- <div v-if="orderList.visible" class="order-box">
			<div class="order-list">
				<div class="title">
					<div>请选择一个订单</div>
					<span @click="closeOrderMessageList">×</span>
				</div>
				<div v-for="(order, index) in orderList.orders" :key="index" class="order-item" @click="sendOrderMessage(order)">
					<div class="order-id">订单号:{{ order.id }}</div>
					<div class="order-body">
						<img :src="order.url" class="order-img" />
						<div class="order-name">{{ order.name }}</div>
						<div>
							<div class="order-price">{{ order.price }}</div>
							<div class="order-count">共{{ order.count }}件</div>
						</div>
					</div>
				</div>
			</div>
		</div> -->
	</div>
	<div v-if="useTokenStore().messageHistory" class="home-record">
		<div class="home-record-title"></div>
		<div class="home-record-tabList">
			<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
				<el-tab-pane label="全部" name="all">
					<el-scrollbar wrap-class="scrollbar-wrapper" style="height: 100%">
						<ul v-infinite-scroll="load" class="messageHistory-list">
							<li v-for="(item, index) in messageHistoryData" :key="index" class="messageHistory-list-item">
								<div style="display: flex">
									<div class="messageHistory-list-item-avatar">
										<img :src="item.avatar" alt="" srcset="" />
									</div>
									<div class="messageHistory-list-item-right">
										<div class="messageHistory-list-item-name">
											{{ item.username }}
											<span>{{ formatDate(item.timestamp) }}</span>
										</div>
										<div
											v-if="item.type == '1'"
											class="messageHistory-list-item-content-text"
											v-html="emoji.decoder.decode(JSON.parse(item.content).text)"></div>
										<div
											v-if="item.type == '2'"
											class="messageHistory-list-item-content-qt_image"
											@click="showImagePreviewPopup(JSON.parse(item.content).url)">
											<div class="content-image">
												<img
													:src="JSON.parse(item.content).url"
													:style="{
														height:
															getImageHeight(
																JSON.parse(item.content).width,
																JSON.parse(item.content).height
															) + 'px',
													}" />
												<!--  -->
											</div>
										</div>
										<a v-if="item.type == '3'" :href="JSON.parse(item.content).url" target="_blank" download="download">
											<div class="content-file" title="点击下载">
												<div class="file-info">
													<span class="file-name">{{ JSON.parse(item.content).name }}</span>
													<span class="file-size">{{ (JSON.parse(item.content).size / 1024).toFixed(2) }}KB</span>
												</div>
												<img class="file-img" src="../../assets/images/file.png" />
											</div>
										</a>
									</div>
								</div>
							</li>
						</ul>
					</el-scrollbar>
				</el-tab-pane>
				<el-tab-pane label="图片" name="image">
					<el-scrollbar wrap-class="scrollbar-wrapper" style="height: 100%">
						<ul v-infinite-scroll="load" class="messageHistory-list">
							<li v-for="(item, index) in messageHistoryData" :key="index" class="messageHistory-list-item">
								<div style="display: flex">
									<div class="messageHistory-list-item-avatar">
										<img :src="item.avatar" alt="" srcset="" />
									</div>
									<div class="messageHistory-list-item-right">
										<div class="messageHistory-list-item-name">
											{{ item.username }}
											<span>{{ formatDate(item.timestamp) }}</span>
										</div>
										<div v-if="item.type == '1'" class="messageHistory-list-item-content-text">
											{{ JSON.parse(item.content).text }}
										</div>
										<div
											v-if="item.type == '2'"
											class="messageHistory-list-item-content-qt_image"
											@click="showImagePreviewPopup(JSON.parse(item.content).url)">
											<div class="content-image">
												<img
													:src="JSON.parse(item.content).url"
													:style="{
														height:
															getImageHeight(
																JSON.parse(item.content).width,
																JSON.parse(item.content).height
															) + 'px',
													}" />
												<!--  -->
											</div>
										</div>
										<a v-if="item.type == '3'" :href="JSON.parse(item.content).url" target="_blank" download="download">
											<div class="content-file" title="点击下载">
												<div class="file-info">
													<span class="file-name">{{ JSON.parse(item.content).name }}</span>
													<span class="file-size">{{ (JSON.parse(item.content).size / 1024).toFixed(2) }}KB</span>
												</div>
												<img class="file-img" src="../../assets/images/file.png" />
											</div>
										</a>
									</div>
								</div>
							</li>
						</ul>
					</el-scrollbar>
				</el-tab-pane>
				<el-tab-pane label="文件" name="file">
					<el-scrollbar wrap-class="scrollbar-wrapper" style="height: 100%">
						<ul v-infinite-scroll="load" class="messageHistory-list">
							<li v-for="(item, index) in messageHistoryData" :key="index" class="messageHistory-list-item">
								<div style="display: flex">
									<div class="messageHistory-list-item-avatar">
										<img :src="item.avatar" alt="" srcset="" />
									</div>
									<div class="messageHistory-list-item-right">
										<div class="messageHistory-list-item-name">
											{{ item.username }}
											<span>{{ formatDate(item.timestamp) }}</span>
										</div>
										<div v-if="item.type == '1'" class="messageHistory-list-item-content-text">
											{{ JSON.parse(item.content).text }}
										</div>
										<div
											v-if="item.type == '2'"
											class="messageHistory-list-item-content-qt_image"
											@click="showImagePreviewPopup(JSON.parse(item.content).url)">
											<div class="content-image">
												<img
													:src="JSON.parse(item.content).url"
													:style="{
														height:
															getImageHeight(
																JSON.parse(item.content).width,
																JSON.parse(item.content).height
															) + 'px',
													}" />
												<!--  -->
											</div>
										</div>
										<a v-if="item.type == '3'" :href="JSON.parse(item.content).url" target="_blank" download="download">
											<div class="content-file" title="点击下载">
												<div class="file-info">
													<span class="file-name">{{ JSON.parse(item.content).name }}</span>
													<span class="file-size">{{ (JSON.parse(item.content).size / 1024).toFixed(2) }}KB</span>
												</div>
												<img class="file-img" src="../../assets/images/file.png" />
											</div>
										</a>
									</div>
								</div>
							</li>
						</ul>
					</el-scrollbar>
				</el-tab-pane>
			</el-tabs>
		</div>
	</div>
</template>

<script setup lang="ts">
	import { formatDate } from '@/utils/GoEasyData'
	import { ElScrollbar } from 'element-plus'
	import { goEasyHistoryMessageApi } from './api/goEasyApi'
	import EmojiDecoder from './utils/EmojiDecoder'
	import SvgIcon from '@/components/SvgIcon/index.vue'
	import { useTokenStore, goEasyStore } from '@/store/index'
	import axios from 'axios'
	import { getImPublicSignedUrl, SignReq } from '@/api/upDate/file'
	import type { TabsPaneContext } from 'element-plus'
	import {
		currentUserInfoType,
		currentUserType,
		historyType,
		friendType,
		toType,
		historyMessagesType,
		queryType,
		messageHistory,
	} from './type/goEasyType'
	import { Local } from '@/utils/storage'
	import { ref, getCurrentInstance, onBeforeUnmount, nextTick } from 'vue'
	const IMAGE_MAX_WIDTH = 200
	const IMAGE_MAX_HEIGHT = 150
	// 图片拼接地址

	const emojiUrl = '/goEasyEmoji/'
	const emojiMap = {
		'[微笑]': 'smiling-face-with-smiling-eyes.png',
		'[笑哭]': 'face-with-tears-of-joy.png',
		'[龇牙]': 'miling-eyes.png',
		'[惊讶]': 'flushed-face.png',
		'[得意]': 'smiling-face-with-sunglasses.png',
		'[呵呵]': 'smirking-face.png',
		'[大笑]': 'happy-face.png',
		'[天使]': 'smiling-face-with-halo.png',
		'[害怕]': 'open-mouth.png',
		'[惊吓]': 'face-screaming-in-fear.png',
		'[大哭]': 'loudly-crying-face.png',
		'[眨眼]': 'winking-face.png',
		'[难过]': 'disappointed-face-2.png',
		'[悲伤]': 'tired-face.png',
		'[亲亲]': 'kissing-face-with-closed-eyes.png',
		'[心累]': 'sleepy-face.png',
		'[愤怒]': 'angry-face.png',
		'[头晕]': 'face-with-spiral-eyes.png',
		'[无奈]': 'crying-face.png',
		'[开心]': 'smiling-face-with-heart-eyes.png',
		'[哼]': 'confused-face.png',
		'[没眼看]': 'dizzy-face.png',
		'[庆祝]': 'Partying-Face.png',
		'[生气]': 'face-with-steam-from-nose.png',
		'[高兴]': 'grinning-face-with-smiling-eyes.png',
		'[汗颜]': 'downcast-face-with-sweat.png',
		'[惶恐]': 'anxious-face-with-sweat.png',
		'[憋住]': 'expressionless-face.png',
		'[气愤]': 'pouting-face.png',
		'[无语]': 'neutral-face.png',
		'[难受]': 'confounded-face.png',
		'[不开心]': 'disappointed-face.png',
	}
	const activeName = ref('all')
	const audioPlayerRef = ref<InstanceType<typeof Audio>>()
	const scrollView = ref<InstanceType<typeof HTMLDivElement>>()
	const messageList = ref<InstanceType<typeof HTMLDivElement>>()
	const currentUser = ref({} as currentUserType)
	const goEasyData = ref(Local.get('userInfo') as currentUserInfoType)
	currentUser.value.id = goEasyData.value.tenantCode
	currentUser.value.avatar = goEasyData.value.avatar
	currentUser.value.phone = goEasyData.value.mobile
	currentUser.value.name = goEasyData.value.tenantName
	const pageNum = ref(1)
	const query = ref<queryType>({
		size: 15,
		userId: goEasyStore().receiverId,
		type: '',
	})
	const totalPage = ref(0)
	const messageHistoryData = ref<Array<messageHistory>>([])
	const goEasy: any = getCurrentInstance()?.proxy?.goEasy
	const GoEasy: any = getCurrentInstance()?.proxy?.GoEasy
	const friend = ref({
		id: goEasyStore().userId,
		name: goEasyStore().name,
		avatar: goEasyStore().avatar,
	} as friendType)
	//用于创建消息时传入
	const to = ref({
		type: goEasyStore().goEasyType == 'cs' ? GoEasy.IM_SCENE.CS : GoEasy.IM_SCENE.PRIVATE,
		id: goEasyStore().userId,
		data: {
			name: goEasyStore().name,
			avatar: goEasyStore().avatar,
		},
	} as toType)
	const history = ref({
		messages: [],
		allLoaded: false,
		loading: true,
	} as historyType)
	const text = ref('')
	//定义表情列表
	const emoji = ref({
		url: emojiUrl,
		map: emojiMap,
		visible: false,
		decoder: new EmojiDecoder(emojiUrl, emojiMap),
	})
	// 订单消息
	// const orderList = ref({
	// 	orders: [],
	// 	visible: false,
	// } as orderListType)
	// 图片预览弹出框
	const imagePreview = ref({
		visible: false,
		url: '',
	})
	const audioPlayer = ref({
		playingMessage: null,
	})
	// 展示消息删除弹出框
	const actionPopup = ref({
		visible: false,
		message: null,
		recallable: false,
	})
	const signReq = ref({
		method: 'put',
	} as SignReq)
	const messageSelector = ref({ visible: false, ids: [] as Array<string> })
	/**
	 * 获取消息以便于标记已读
	 * @param message 父组件传递的消息主体
	 */
	const onReceivedPrivateMessage = (message: historyMessagesType): void => {
		let messages = ref(message)
		console.log('🚀 ~ file: PrivateChat.vue:360 ~ onReceivedPrivateMessage ~ message:', message)

		if (messages.value.senderId === goEasyStore().userId || messages.value.teamId === goEasyStore().userId) {
			history.value.messages.push(messages.value)
			markPrivateMessageAsRead()
		}
		scrollToBottom()
	}
	const goEasyType = ref(
		goEasyStore().goEasyType == 'cs' ? GoEasy.IM_EVENT.CS_MESSAGE_RECEIVED : GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED
	)
	goEasy.im.on(goEasyType.value, onReceivedPrivateMessage)

	onBeforeUnmount(() => {
		goEasy.im.off(goEasyType.value, onReceivedPrivateMessage)
	})
	formatDate
	/**
	 * 核心就是设置高度,产生明确占位
	 *
	 * 小  (宽度和高度都小于预设尺寸)
	 *    设高=原始高度
	 * 宽 (宽度>高度)
	 *    高度= 根据宽度等比缩放
	 * 窄  (宽度<高度)或方(宽度=高度)
	 *    设高=MAX height
	 *
	 * @param width,height
	 * @returns number
	 */
	const getImageHeight = (width: number, height: number): number | undefined => {
		if (width < IMAGE_MAX_WIDTH && height < IMAGE_MAX_HEIGHT) {
			return height
		} else if (width > height) {
			return (IMAGE_MAX_WIDTH / width) * height
		} else if (width === height || width < height) {
			return IMAGE_MAX_HEIGHT
		}
	}
	/**
	 * 用于调起语音
	 * @param audioMessage 语音组件的消息主体
	 */
	const playAudio = (audioMessage: any): void => {
		let playingMessage = audioPlayer.value.playingMessage
		if (playingMessage) {
			audioPlayerRef.value?.pause()
			// 如果点击的消息正在播放,就认为是停止播放操作
			if (playingMessage === audioMessage) {
				return
			}
		}
		if (audioPlayerRef.value) {
			audioPlayer.value.playingMessage = audioMessage
			;(audioPlayerRef.value as InstanceType<typeof Audio>).src = audioMessage.payload.url
			audioPlayerRef.value?.load()
			;(audioPlayerRef.value as InstanceType<typeof Audio>).currentTime = 0
			audioPlayerRef.value?.play()
		}
	}
	/**
	 * 调起播放语音
	 */
	const onAudioPlayEnd = (): void => {
		audioPlayer.value.playingMessage = null
	}
	/**
	 * 发送按钮函数,发送消息
	 */
	const sendTextMessage = (): void => {
		if (!text.value.trim()) {
			console.log('输入为空')
			return
		}
		goEasy.im.createTextMessage({
			text: text.value,
			to: to.value,
			onSuccess: (message: historyMessagesType) => {
				console.log(message, 'message')

				sendMessage(message)
				text.value = ''
			},
			onFailed: (err) => {
				console.log('创建消息err:', err)
			},
		})
	}
	/**
	 * 消息输入匡
	 */
	const onInputFocus = (): void => {
		emoji.value.visible = false
	}
	/**
	 * 表情函数,用于拼接表情的url
	 * @param emojiKey 标签的key
	 */
	const chooseEmoji = (emojiKey: string): void => {
		text.value += emojiKey
		emoji.value.visible = false
	}
	/**
	 * 发送图片的函数
	 * @param image 图片
	 */
	const sendImageMessage = (image: Event): void => {
		let fileList: any = [...(<HTMLInputElement>image.target).files]
		const imageObj = new Image()
		fileList &&
			fileList.forEach(async (file) => {
				let imageData = {
					fileName: '',
					url: '',
					size: '',
					width: 0,
					height: 0,
					contentType: file.type,
				}
				signReq.value.fileName = file.name
				signReq.value.size = file.size
				imageData.size = file.size
				imageData.fileName = file.name
				await getImPublicSignedUrl(signReq.value).then(async (res) => {
					imageData.url = res.data.downloadUrl
					await upImFile(res.data.actualSignedRequestHeaders, res.data.signedUrl, file)
					imageObj.src = res.data.downloadUrl
				})
				imageObj.addEventListener('load', () => {
					imageData.width = imageObj.width
					imageData.height = imageObj.height
					const imageMessage = goEasy.im.createCustomMessage({
						type: 'qt_image',
						payload: imageData,
						to: to.value,
					})
					sendMessage(imageMessage)
				})
			})
	}
	/**
	 * 上传图片
	 * @param header 请求头
	 * @param url 请求地址
	 * @param data 包含参数
	 */
	const upImFile = async (header: any, url: string, data: any) => {
		const config = {
			method: 'put',
			url: url,
			headers: header,
			data: data,
		}
		await axios(config)
			.then(function (response) {
				console.log(JSON.stringify(response.data), 999999999999)
			})
			.catch(function (error) {
				console.log(error)
			})
	}
	/**
	 * 发送文件函数
	 * @param files 文件
	 */
	const sendFileMessage = async (files: Event): Promise<void> => {
		const file: any = (<HTMLInputElement>files.target).files?.[0]
		const filesData = {
			name: '',
			url: '',
			size: 0,
			contentType: file.type,
		}
		signReq.value.fileName = file.name
		signReq.value.size = file.size
		filesData.size = file.size
		filesData.name = file.name
		await getImPublicSignedUrl(signReq.value).then(async (res) => {
			filesData.url = res.data.downloadUrl
			// 上传文件或图片
			await upImFile(res.data.actualSignedRequestHeaders, res.data.signedUrl, file)
		})
		const filesMessage = goEasy.im.createCustomMessage({
			type: 'qt_file',
			payload: filesData,
			to: to.value,
		})
		sendMessage(filesMessage)
	}
	/**
	 * 发送消息函数
	 * @param message 消息主体详情
	 */
	const sendMessage = (message: historyMessagesType): void => {
		let messages = ref(message)
		history.value.messages.push(messages.value)
		console.log(history.value.messages, 'history.value.messages')

		scrollToBottom()
		goEasy.im.sendMessage({
			message: messages.value,
			onSuccess: (message: any) => {
				console.log('发送成功', message)
			},
			onFailed: function (error: { code: number }) {
				console.log('发送失败', error)
			},
		})
	}
	/**
	 * 聊天记录函数
	 */
	const messageHistory = (): void => {
		useTokenStore().messageHistory = !useTokenStore().messageHistory
		if (useTokenStore().messageHistory) {
			historyMessageFn()
		}
	}
	/**
	 * 获取聊天记录函数
	 */
	const historyMessageFn = (): void => {
		goEasyHistoryMessageApi(pageNum.value, query.value).then((result) => {
			result.data.list.map((item) => messageHistoryData.value.push(item))
			totalPage.value = result.data.totalPage
		})
	}

	/**
	 *	el-tab-pane点击事件
	 * @param tab  el-tab-pane的name
	 * @param event el-tab-pane的元素
	 */
	const handleClick = (tab: TabsPaneContext) => {
		if (tab.props.name == 'all') {
			query.value.type = ''
			pageNum.value = 1
			messageHistoryData.value = []
			historyMessageFn()
		} else if (tab.props.name == 'image') {
			query.value.type = '2'
			pageNum.value = 1
			messageHistoryData.value = []
			historyMessageFn()
		} else {
			query.value.type = '3'
			pageNum.value = 1
			messageHistoryData.value = []

			historyMessageFn()
		}
	}
	/**
	 * 滚动加载函数
	 */
	const load = () => {
		pageNum.value += 1
		if (pageNum.value <= totalPage.value) {
			historyMessageFn()
		}
	}
	/**
	 * 弹出框
	 * @param message 消息主体
	 */
	const showActionPopup = (message: historyMessagesType): void => {
		const MAX_RECALLABLE_TIME = 3 * 60 * 1000 //3分钟以内的消息才可以撤回
		messageSelector.value.ids.push(message.messageId)
		if (
			Date.now() - message.timestamp < MAX_RECALLABLE_TIME &&
			message.senderId === currentUser.value.id &&
			message.status === 'success'
		) {
			actionPopup.value.recallable = true
		} else {
			actionPopup.value.recallable = false
		}
		actionPopup.value.visible = true
	}
	/**
	 * 删除多个
	 */
	const deleteMultipleMessages = (): void => {
		if (messageSelector.value.ids.length > 0) {
			messageSelector.value.visible = false
			deleteMessage()
		}
	}
	/**
	 * 删除消息函数
	 */
	const deleteMessage = (): void => {
		let conf = confirm('确认删除?')
		if (conf === true) {
			let selectedMessages = [] as Array<historyMessagesType>
			selectedMessages = history.value.messages.filter((message) => {
				return messageSelector.value.ids.includes(message.messageId)
			})
			// history.value.messages.forEach((message) => {
			// 	if (messageSelector.value.ids.includes(message.messageId)) {
			// 		selectedMessages.push(message)
			// 	}
			// })
			goEasy.im.deleteMessage({
				messages: selectedMessages,
				onSuccess: () => {
					selectedMessages.forEach((message) => {
						let index = history.value.messages.indexOf(message)
						if (index > -1) {
							history.value.messages.splice(index, 1)
						}
					})
					messageSelector.value.ids = []
				},
				onFailed: (error) => {
					console.log('error:', error)
				},
			})
		} else {
			messageSelector.value.ids = []
		}
	}
	/**
	 * 重新编辑函数
	 * @param texts 重新编辑的消息
	 */
	const editRecalledMessage = (texts: string): void => {
		text.value = texts
	}
	/**
	 * 图片弹出框
	 * @param url 地址
	 */
	const showImagePreviewPopup = (url: string): void => {
		imagePreview.value.visible = true
		imagePreview.value.url = url
	}
	/**
	 * 图片预览弹框
	 */
	const hideImagePreviewPopup = (): void => {
		imagePreview.value.visible = false
	}
	/**
	 * 消息多选函数
	 * @param e 传递进来消息的多选
	 */
	const selectMessages = (e: Event): void => {
		let event = <HTMLInputElement>e.target
		if (event.checked) {
			messageSelector.value.ids.push(event.value)
		} else {
			let index = messageSelector.value.ids.indexOf(event.value)
			if (index > -1) {
				messageSelector.value.ids.splice(index, 1)
			}
		}
	}
	/**
	 * 更多消息点击函数
	 * @param scrollToBottoms 更多消息参数
	 */
	const loadHistoryMessage = (scrollToBottoms: boolean): void => {
		history.value.loading = true
		//历史消息
		let lastMessageTimeStamp = null as null | number
		let lastMessage = history.value.messages[0]
		if (lastMessage) {
			lastMessageTimeStamp = lastMessage.timestamp
		}

		goEasy.im.history({
			id: friend.value.id,
			lastTimestamp: lastMessageTimeStamp,
			limit: 10,
			type: to.value.type,
			onSuccess: (result: { content: any }) => {
				history.value.loading = false
				let messages = result.content
				if (messages.length === 0) {
					history.value.allLoaded = true
				} else {
					if (lastMessageTimeStamp) {
						history.value.messages = messages.concat(history.value.messages)
					} else {
						history.value.messages = messages
					}
					if (messages.length < 10) {
						history.value.allLoaded = true
					}
					if (scrollToBottoms) {
						scrollToBottom()
						console.log(2222222)

						//收到的消息设置为已读
						markPrivateMessageAsRead()
					}
				}
			},
			onFailed: (error: { code: string; content: string }) => {
				//获取失败
				history.value.loading = false
				console.log('获取历史消息失败, code:' + error.code + ',错误信息:' + error.content)
			},
		})
	}
	loadHistoryMessage(true)
	/**
	 * 消息已读函数
	 */
	const markPrivateMessageAsRead = (): void => {
		goEasy.im.markMessageAsRead({
			id: to.value.id,
			type: to.value.type,
			onSuccess: function () {
				console.log(history.value.messages)

				console.log('标记私聊已读成功')
			},
			onFailed: function (error: any) {
				console.log('标记私聊已读失败', error)
			},
		})
	}
	/**
	 * 消息轮动函数
	 */
	const scrollToBottom = (): void => {
		nextTick(() => {
			;(scrollView.value as HTMLDivElement).scrollTop = (messageList.value as HTMLDivElement).scrollHeight
		})
	}
	/**
	 * 每一条消息上的时间
	 * @param message 消息
	 * @param index 下标
	 */
	const renderMessageDate = (message: { timestamp: number }, index: number): string => {
		if (index === 0) {
			return formatDate(message.timestamp)
		} else {
			if (message.timestamp - history.value.messages[index - 1].timestamp > 5 * 60 * 1000) {
				return formatDate(message.timestamp)
			}
		}
		return ''
	}
</script>

<style scoped lang="scss">
	.chat-container {
		min-width: 570px;
		height: 100%;
		display: flex;
		flex-direction: column;
		position: relative;
	}

	.chat-title {
		height: 48px;
		padding: 0;
		display: flex;
		align-items: center;
		font-size: 18px;
		border-bottom: 1px solid #dcdfe6;
	}

	.chat-avatar {
		width: 35px;
		height: 35px;
	}

	.chat-name {
		width: 400px;

		margin-left: 10px;
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
		word-break: break-all;
		font-family: AlibabaPuHuiTiM;
		font-size: 16px;
		font-weight: 700;
		line-height: 24px;
		color: #3d3d3d;
		letter-spacing: 0px;
	}

	.chat-main {
		display: flex;
		flex-direction: column;
		overflow-y: auto;
		flex: 1;
		scrollbar-width: thin;
		border-right: 1px solid #dcdfe6;
	}

	.chat-main::-webkit-scrollbar {
		width: 0;
	}

	.chat-main .history-loaded {
		text-align: center;
		font-size: 12px;
		color: #cccccc;
		line-height: 20px;
	}

	.chat-main .load {
		text-align: center;
		font-size: 12px;
		color: #d02129;
		line-height: 20px;
		cursor: pointer;
	}

	.history-loading {
		width: 100%;
		text-align: center;
	}

	.time-tips {
		color: #999;
		text-align: center;
		font-size: 12px;
	}

	.message-list {
		padding: 0 10px;
	}

	.message-item {
		display: flex;
	}

	.message-item-checkbox {
		height: 50px;
		margin-right: 15px;
		display: flex;
		align-items: center;
	}

	.input-checkbox {
		position: relative;
	}

	.message-item-checkbox input[type='checkbox']::before,
	.message-item-checkbox input[type='checkbox']:checked::before {
		content: '';
		position: absolute;
		top: -3px;
		left: -3px;
		background: #ffffff;
		width: 18px;
		height: 18px;
		border: 1px solid #cccccc;
		border-radius: 50%;
	}

	.message-item-checkbox input[type='checkbox']:checked::before {
		content: '\2713';
		background-color: #d02129;
		width: 18px;
		color: #ffffff;
		text-align: center;
		font-weight: bold;
	}

	.message-item-content {
		flex: 1;
		margin: 5px 0;
		overflow: hidden;
		display: flex;
	}

	.sender-info {
		margin: 0 5px;
	}

	.sender-avatar {
		width: 40px;
		height: 40px;
		border-radius: 50%;
	}

	.message-content {
		max-width: calc(100% - 100px);
	}

	.message-payload {
		display: flex;
		flex-direction: row;
		justify-content: flex-end;
		align-items: center;
	}

	.pending {
		background: url('@/assets/images/pending.gif') no-repeat center;
		background-size: 13px;
		width: 15px;
		height: 15px;
	}

	.send-fail {
		background: url('@/assets/images/failed.png') no-repeat center;
		background-size: 15px;
		width: 18px;
		height: 18px;
		margin-right: 3px;
	}

	.message-read {
		color: gray;
		font-size: 12px;
		text-align: end;
		height: 16px;
	}

	.message-unread {
		color: #d02129;
		font-size: 12px;
		text-align: end;
		height: 16px;
	}

	.content-text {
		display: flex;
		align-items: center;
		text-align: left;
		background: #eeeeee;
		font-size: 14px;
		font-weight: 500;
		padding: 6px 8px;
		margin: 3px 0;
		line-height: 25px;
		white-space: pre-line;
		overflow-wrap: anywhere;
		border-radius: 8px;
		word-break: break-all;
	}

	.content-image {
		display: block;
		cursor: pointer;
	}

	.content-image img {
		height: 100%;
	}

	.content-audio {
		-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
	}

	.content-audio .audio-facade {
		min-width: 12px;
		background: #eeeeee;
		border-radius: 7px;
		display: flex;
		font-size: 14px;
		padding: 8px;
		margin: 5px 0;
		line-height: 25px;
		cursor: pointer;
	}

	.content-audio .audio-facade-bg {
		background: url('../../assets/images/voice.png') no-repeat center;
		background-size: 15px;
		width: 20px;
	}

	.content-audio .audio-facade-bg.play-icon {
		background: url('../../assets/images/play.gif') no-repeat center;
		background-size: 20px;
	}

	.content-order {
		border-radius: 5px;
		border: 1px solid #eeeeee;
		padding: 8px;
		display: flex;
		flex-direction: column;
	}

	.content-order .order-id {
		font-size: 12px;
		color: #666666;
		margin-bottom: 5px;
	}

	.content-order .order-body {
		display: flex;
		font-size: 13px;
		padding: 5px;
	}

	.content-order .order-img {
		width: 70px;
		height: 70px;
		border-radius: 5px;
	}

	.content-order .order-name {
		margin-left: 10px;
		width: 135px;
		color: #606164;
	}

	.content-order .order-count {
		font-size: 12px;
		color: #666666;
		flex: 1;
	}

	.content-file {
		width: 240px;
		height: 65px;
		font-size: 15px;
		background: #ffffff;
		border: 1px solid #eeeeee;
		display: flex;
		align-items: center;
		padding: 10px;
		border-radius: 5px;
		cursor: pointer;
	}

	.content-file:hover {
		background: #f1f1f1;
	}

	.file-info {
		width: 194px;
		text-align: left;
	}

	.file-name {
		text-overflow: ellipsis;
		overflow: hidden;
		display: -webkit-box;
		word-break: break-all;
		-webkit-line-clamp: 2;
		-webkit-box-orient: vertical;
	}

	.file-size {
		font-size: 12px;
		color: #ccc;
	}

	.file-img {
		width: 50px;
		height: 50px;
	}

	.message-item .self {
		overflow: hidden;
		display: flex;
		justify-content: flex-start;
		flex-direction: row-reverse;
	}

	.message-item .self .audio-facade {
		flex-direction: row-reverse;
	}

	.message-item .self .audio-facade-bg {
		background: url('../assets/images/voice.png') no-repeat center;
		background-size: 15px;
		width: 20px;
		-moz-transform: rotate(180deg);
		-webkit-transform: rotate(180deg);
		-o-transform: rotate(180deg);
		transform: rotate(180deg);
	}

	.message-item .self .play-icon {
		background: url('../assets/images/play.gif') no-repeat center;
		background-size: 20px;
		-moz-transform: rotate(180deg);
		-webkit-transform: rotate(180deg);
		-o-transform: rotate(180deg);
		transform: rotate(180deg);
	}

	.message-recalled {
		display: flex;
		align-items: center;
		justify-content: center;
		line-height: 28px;
		font-size: 13px;
		text-align: center;
		color: grey;
		margin-top: 10px;
	}

	.message-recalled-self {
		display: flex;
	}

	.message-recalled-self span {
		margin-left: 5px;
		color: #d02129;
		cursor: pointer;
	}

	.chat-footer {
		border-top: 1px solid #dcdfe6;
		width: 100%;
		height: 150px;
		background: #ffffff;
		border-right: 1px solid #dcdfe6;
	}

	.action-delete {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		width: 100%;
		height: 100%;
		background-color: #ffffff;
	}

	.delete-btn {
		width: 25px;
		height: 25px;
		padding: 10px;
		background: #f5f5f5;
		border-radius: 50%;
		cursor: pointer;
		margin-bottom: 10px;
	}

	.action-box {
		width: 100%;
		height: 100%;
		display: flex;
		flex-direction: column;
	}

	.action-bar {
		display: flex;
		flex-direction: row;
		padding: 0 10px;
	}

	.action-bar .action-item {
		text-align: left;
		// padding: 10px 0;
		position: relative;
	}
	.last-action-item {
		display: flex;
		align-items: center;
		float: right;
		position: relative;
		left: 342px;
		cursor: pointer;
		/* color: #3760f1; */
	}
	.svg-icon {
		margin-right: 4px;
	}
	.action-bar .action-item .iconfont {
		font-size: 22px;
		margin: 0 10px;
		z-index: 3;
		color: #606266;
		cursor: pointer;
	}

	.action-bar .action-item .iconfont:focus {
		outline: none;
	}

	.action-bar .action-item .iconfont:hover {
		color: #d02129;
	}

	.emoji-box {
		width: 284px;
		position: absolute;
		top: -147px;
		left: -10px;
		z-index: 2007;
		background: #fff;
		border: 1px solid #ebeef5;
		padding: 12px;
		text-align: justify;
		font-size: 14px;
		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
		word-break: break-all;
		border-radius: 4px;
	}
	.emoji-item {
		width: 24px;
		height: 24px;
		margin: 3px 4px;
		display: inline-block;
	}

	.input-box {
		padding: 0 10px;
		flex: 1;
	}

	.input-content {
		border: none;
		resize: none;
		display: block;
		padding: 5px 15px;
		box-sizing: border-box;
		width: 100%;
		height: 70px;
		color: #606266;
		outline: none;
		background: #ffffff;
		word-break: break-all;
	}

	.send-box {
		padding: 5px 10px 10px 10px;
		text-align: right;
	}

	.send-button {
		width: 70px;
		height: 30px;
		font-size: 15px;
		border: 1px solid #d02129;
		background-color: #ffffff;
		color: #d02129;
		border-radius: 5px;
	}

	.action-popup {
		width: 851px;
		height: 100%;
		position: absolute;
		top: 0;
		left: -281px;
		background: rgba(51, 51, 51, 0.5);
		display: flex;
		align-items: center;
		justify-content: center;
	}

	.action-popup-main {
		width: 150px;
		height: 120px;
		background: #ffffff;
		z-index: 100;
		border-radius: 10px;
		overflow: hidden;
	}

	.action-popup-main .action-item {
		text-align: center;
		line-height: 40px;
		font-size: 15px;
		color: #262628;
		border-bottom: 1px solid #efefef;
		cursor: pointer;
	}

	.image-preview {
		max-width: 750px;
		max-height: 500px;
		background: rgba(0, 0, 0, 0.8);
		display: flex;
		align-items: center;
		justify-content: center;
		position: fixed;
		margin: auto;
		top: 0;
		bottom: 0;
		left: 0;
		right: 0;
		z-index: 9998;
	}

	.image-preview img {
		max-width: 750px;
		max-height: 500px;
	}

	.image-preview .close {
		font-size: 50px;
		line-height: 24px;
		cursor: pointer;
		color: #ffffff;
		position: absolute;
		top: 10px;
		right: 5px;
		z-index: 1002;
	}

	.order-box {
		width: 848px;
		position: absolute;
		left: -281px;
		right: 0;
		top: 0;
		bottom: 0;
		z-index: 2007;
		font-size: 14px;
		display: flex;
		align-items: center;
		justify-content: center;
		background: rgba(33, 33, 33, 0.7);
	}

	.order-list {
		width: 300px;
		background: #f1f1f1;
		border-radius: 5px;
	}

	.order-list .title {
		font-weight: 600;
		font-size: 15px;
		color: #000000;
		margin-left: 10px;
		margin-right: 10px;
		display: flex;
		align-items: center;
		justify-content: space-between;
	}

	.order-list .title span {
		font-size: 28px;
		font-weight: 400;
		cursor: pointer;
	}

	.order-list .order-item {
		padding: 10px;
		background: #ffffff;
		margin: 10px;
		border-radius: 5px;
		cursor: pointer;
	}

	.order-list .order-id {
		font-size: 12px;
		color: #666666;
		margin-bottom: 5px;
	}

	.order-list .order-body {
		display: flex;
		font-size: 13px;
		justify-content: space-between;
	}

	.order-list .order-img {
		width: 50px;
		height: 50px;
		border-radius: 5px;
	}

	.order-list .order-name {
		width: 160px;
	}

	.order-list .order-count {
		font-size: 12px;
		color: #666666;
		flex: 1;
	}
	.home-record {
		/* border: 1px red solid; */
		width: 318px;
		/* padding: ; */
	}
	.home-record-title {
		height: 48px;
		padding: 0;
		display: flex;
		align-items: center;
		font-size: 18px;
		border-bottom: 1px solid #dcdfe6;
	}
	.home-record-tabList {
		width: 100%;
		display: flex;
	}
	:deep(.el-tabs__nav) {
		margin-left: 60px;
	}
	:deep(.el-tabs__nav-wrap) {
		width: 318px;
		border-right: 1px solid #dcdfe6;
	}
	:deep(.el-tabs) {
		--el-tabs-header-height: 48px;
	}
	:deep(.el-tabs__content) {
		padding: 0 10px;
	}
	.messageHistory-list {
		// border: 1px red solid;
		height: 530px;
		.messageHistory-list-item {
			// height: 50px;
			padding: 10px 0;

			.messageHistory-list-item-avatar {
				height: 30px;
				img {
					width: 40px;
					height: 40px;
				}
				margin-right: 10px;
			}
			.messageHistory-list-item-name {
				font-size: 10px;
				color: #64748b;
				margin-bottom: 2px;
				span {
					float: right;
					margin-right: 10px;
				}
			}
			.messageHistory-list-item-content-text {
				display: block;
			}
			.messageHistory-list-item-right {
				width: 100%;
				padding-bottom: 10px;
				border-bottom: 1px #ebeef5 solid;
			}
		}
	}
	:deep(.scrollbar-wrapper) {
		overflow-x: hidden !important;
		background-color: #fff;
	}
	.accept-message {
		width: 100%;
		text-align: center;
		color: #606164;
		line-height: 25px;
	}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/712662.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV(Mat类)

目录 1、什么是Mat类 2、Mat类组成 3、Mat类能存储的数据类型 4.Mat类的创建 4.1 利用矩阵宽、高和类型参数创建 4.2 利用矩阵Size()结构和数据类型参数来创建 4.3 利用已有Mat类来创建 5、Mat类的赋值 5.1 创建时赋值 5.2 类方法赋值 5.3 枚举赋值 6、Mat类数据的读取 …

【Linux】Redis 集群部署

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 Redis 集群部署 Redis 主从复制主从复制的作用主从复制的流程搭建Redis 主从复制安装 Redis修改 Redis 配置文件&#xff08;Master节点操作&#xff09;修改 Redis 配置文件…

基于redhat发行版mysql8.0的卸载与重装mysql5.7

文章目录 一、软件的选择与下载二、卸载mysql8.01.查看my.cnf中的部署信息2.卸载mysql8.03.卸载完毕安装包后删除相关数据 三、mysql5.7的安装1.解压安装包2.初始化mysql数据库3.修改root密码 四、安装mysql5.7客户端附&#xff1a;创建数据库以及用户 本次案例是卸载mysql8.0然…

Michael.W基于Foundry精读Openzeppelin第1期——Address.sol

Michael.W基于Foundry精读Openzeppelin第1期——Address.sol 0. 版本0.1 Address.sol 1. 目标合约2. 代码精读2.1 isContract(address)2.2 sendValue(address, uint256)2.3 functionCall(address, bytes memory) && functionCall( address, bytes memory, string memor…

JavaSE基础(上)

目录 第一章 java入门 环境配置 常用cmd命令 打开cmd 安装JDK&#xff1a;Java工具包 IDEA 1 IDEA概述&#xff08;1&#xff09;IDEA概述&#xff1a;IDEA全称IntelliJ IDEA&#xff0c;是用于Java语言开发的集成环境&#xff0c;是业界公认的目前用于Java程序开发最…

ABeam News | 乘云而上,扬帆起航——ABeam Consulting 2023 RISE+BTP私享会圆满落幕

当今中国&#xff0c;数字经济大潮风起云涌&#xff0c;数字化转型已经成为企业发展的“必修课”。对于企业来说&#xff0c;如何在数字化浪潮中奋楫扬帆&#xff0c;借助技术和管理手段助力企业“提质、降本、增效”&#xff0c;成为发展中所亟待解决的问题。 6月1日&#xf…

LED显示屏四大连接方式

LED显示屏的四大连接方式是数据连接、电源连接、信号输入连接和控制系统连接。以下是对每种连接方式的详细说明&#xff1a; 1,数据连接&#xff1a; 数据连接用于传输显示内容的数据信号到LED显示屏。常见的数据连接方式包括&#xff1a; 串行连接&#xff08;Serial Connecti…

离开Kubernetes也能玩转Dapr

Dapr 被设计成一个面向开发者的企业级微服务编程平台&#xff0c;它独立于具体的技术平台&#xff0c;可以运行在“任何地方”。Dapr本身并不提供“基础设施&#xff08;infrastructure&#xff09;”&#xff0c;而是利用自身的扩展来适配具体的部署环境。就目前的状态来说&am…

nacos身份认证绕过漏洞

1.影响范围 Nacos < 2.0.0-ALPHA.1 2.验证漏洞是否存在 http://example/nacos/v1/auth/users/?pageNo1&pageSize5 如果列出了用户名密码,即证明此漏洞存在 3.向系统中添加一个新的用户 http://example/nacos/v1/auth/users/?usernamesectest&passwordsectest…

目录爆破工具(dirb、dirsearch)

一、dirb概述。 dirb是一个基于字典的web目录扫描工具&#xff0c;采用递归的方式来获取更多的目录&#xff0c;可以查找到已知的和隐藏的目录&#xff0c;它还支持代理和http认证限制访问的网站。 二、dirb常用参数。 三、基础操作。 1.直接扫描 dirb http://192.168.84.1…

Python财经股票数据获取, 保存表格文件

目录标题 前言环境使用:模块使用]:代码展示尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 环境使用: Python 3.8 解释器 Pycharm 编辑器 模块使用]: import requests —> 数据请求模块 pip install requests import csv 第三方模块安装: win R 输入cmd 输…

SpringBoot 3.1 新版HTTP调用

在SpringBoot3版本发布后 官方便声明了推荐使用了内置声明式的HTTP客户端。 一、声明式HTTP客户端使用(依赖引入) <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></depende…

Dhrystone基准测试程序在Google Pixel4上运行跑分教程

记录一下实验过程&#xff0c;方便后续回顾 一、Dhrystone简介 Dhrystone是测量处理器运算能力的最常见基准程序之一&#xff0c;常用于处理器的整型运算性能的测量。程序是用C语言编写的&#xff0c;因此C编译器的编译效率对测试结果也有很大影响。 但其也有许多不足&#x…

常见人脸检测器, 调用摄像头检测人脸

常见人脸检测器, 调用摄像头检测人脸 文章目录 常见人脸检测器, 调用摄像头检测人脸[TOC](文章目录) 前言一、导入相关包二、Haar检测器三、Hog检测器四、CNN检测器五、SSD检测器六、MTCNN检测器七、Opencv结合检测器检测人脸7.1 Hog 检测器7.2 Haar检测器 前言 主要介绍几种常…

【花雕】全国青少年机器人技术一级考试模拟题(之一)

一.单选题&#xff08;20题&#xff0c;每题3分&#xff09; 1.下列图片中&#xff0c;哪个不是机器人&#xff08;&#xff09; 答案&#xff1a;D 2.机器人的英文单词是&#xff08;&#xff09; A. botre B. boret C. robot D. rebot 答案&#xff1a;C 3.机器人结构中&am…

网络安全进阶学习第六课——服务器解析漏洞

文章目录 1、概念2、Apache解析漏洞 CVE-2017-157153、Apache AddHandler解析漏洞4、IIS6 解析漏洞&#xff08;;&#xff09;5、IIS6 解析漏洞&#xff08;*.asp/目录&#xff09;6、IIS7 解析漏洞&#xff08;ISAP或CGI的模式下&#xff09;7、nginx解析漏洞&#xff08;cgi.…

USB 3.0 Rx Detect之超速U盘的识别

1 USB超速SerDes原理介绍 1.1 SerDes Rx.Detect SerDes Rx.Detect的原理比较简单&#xff0c;就是通过一个逻辑电路比较RC时间常数的大小。 - 当Rx不存在时&#xff0c;RC时间常数较小。 - 当Rx存在时&#xff0c;RC时间常数较大。 下面将详细描述其原理。 Figure 1-1 USB 3.0电…

SuperMap 的 Environment.initialization(this)空指针

如果你把ndk和动态权限等等都设置好了&#xff0c;还发现Environment.initialization(this)还有空指针问题存在。就试试我这个方法 许可文件要用10i的&#xff0c;别用11i的 SuperMap 有个so库文件是放在armeabi-v7a文件夹下的&#xff0c;armeabi-v7a不要放在jniLibs目录下&…

深度理解:Redis Hash(散列表)实现原理

Redis是一种开源的&#xff0c;基于内存的数据结构存储系统&#xff0c;可以用作数据库、缓存和消息代理。它支持多种类型的数据结构&#xff0c;例如字符串、散列表、列表、集合、有序集合等。今天我们将重点讨论Redis的一个重要数据结构&#xff1a;Hash&#xff0c;也叫散列…

Linux网络概念

1.1网络 是由若干结点和连接这些结点的链路组成&#xff0c;网络中的结点可以是计算机&#xff0c;交换机、路由器等设备 网络设备有&#xff1a;交换机、路由器、集线器 传输介质有&#xff1a;双绞线、同轴电缆、光纤 1.2互联网 把多个网络连接起来就构成了互联网&#…