新增:对话页面

This commit is contained in:
安浩浩 2024-04-27 23:10:03 +08:00
parent de27cfa8f6
commit a7e8433b27
33 changed files with 4008 additions and 91 deletions

View File

@ -39,7 +39,6 @@
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0",
"camunda-bpmn-moddle": "^7.0.1",
"components": "link:@/components",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
@ -51,6 +50,7 @@
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"min-dash": "^4.1.1",
"mitt": "^3.0.1",
@ -63,6 +63,7 @@
"url": "^0.11.3",
"video.js": "^7.21.5",
"vue": "3.4.20",
"vue-at": "3.0.0-alpha.2",
"vue-dompurify-html": "^4.1.4",
"vue-i18n": "9.9.1",
"vue-router": "^4.3.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
/* 单聊用户在线状态 */
import { ref } from 'vue'
const userInfoStatus = ref({
style: '',
label: '',
onlineDeviceCount: 1, //线
deviceType: '' //线
})
</script>
<template>
<div class="user_status_box">
<span class="status_icon" :style="userInfoStatus.style"></span>
<span class="os_type">{{
userInfoStatus.onlineDeviceCount > 1
? `多设备${userInfoStatus.label}`
: `${userInfoStatus.deviceType.toUpperCase()}${userInfoStatus.label}`
}}</span>
</div>
</template>
<style scoped lang="scss">
.user_status_box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 100px;
height: 100%;
font-size: 7px;
.status_icon {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin: 0 3px;
}
}
</style>

322
src/constant/emojis.js Normal file
View File

@ -0,0 +1,322 @@
const emojis = [
'😀',
'😃',
'😄',
'😁',
'😆',
'😅',
'🤣',
'😂',
'🙂',
'🙃',
'😉',
'😊',
'😇',
'😍',
'🤩',
'😘',
'😗',
'😚',
'😙',
'😋',
'😛',
'😜',
'🤪',
'😝',
'🤑',
'🤗',
'🤭',
'🤫',
'🤔',
'🤐',
'🤨',
'😐',
'😑',
'😶',
'😏',
'😒',
'🙄',
'😬',
'🤥',
'😌',
'😔',
'😪',
'🤤',
'😴',
'😷',
'🤒',
'🤕',
'🤢',
'🤮',
'🤧',
'😵',
'🤯',
'🤠',
'😎',
'🤓',
'🧐',
'😕',
'😟',
'🙁',
'😮',
'😯',
'😲',
'😳',
'😦',
'😧',
'😨',
'😰',
'😥',
'😢',
'😭',
'😱',
'😖',
'😣',
'😞',
'😓',
'😩',
'😫',
'😤',
'😡',
'😠',
'🤬',
'😈',
'👿',
'💀',
'💩',
'🤡',
'👹',
'👺',
'👻',
'👽',
'👾',
'🤖',
'😺',
'😸',
'😹',
'😻',
'😼',
'😽',
'🙀',
'😿',
'😾',
'💋',
'👋',
'🤚',
'🖐',
'✋',
'🖖',
'👌',
'🤞',
'🤟',
'🤘',
'🤙',
'👈',
'👉',
'👆',
'🖕',
'👇',
'👍',
'👎',
'✊',
'👊',
'🤛',
'🤜',
'👏',
'🙌',
'👐',
'🤲',
'🤝',
'🙏',
'💅',
'🤳',
'💪',
'👂',
'👃',
'🧠',
'👀',
'👁',
'👅',
'👄',
'👶',
'🧒',
'👦',
'👧',
'🧑',
'👱',
'👨',
'🧔',
'👱‍',
'👨‍',
'👨‍',
'👩',
'👱‍',
'👩‍',
'👩‍',
'👩‍',
'👩‍',
'🧓',
'👴',
'👵',
'🙍',
'🙅',
'🙆',
'💁',
'🙋',
'🙇',
'🙇‍',
'🙇‍',
'🤦',
'🤷',
'🤷‍',
'🤷‍',
'👨‍⚕️',
'👩‍⚕️',
'👨‍🎓',
'👩‍🎓',
'👨‍🏫',
'👩‍🏫',
'👨‍⚖️',
'👩‍⚖️',
'👨‍🌾',
'👩‍🌾',
'👨‍🍳',
'👩‍🍳',
'👨‍🔧',
'👩‍🔧',
'👨‍🏭',
'👩‍🏭',
'👨‍💼',
'👩‍💼',
'👨‍🔬',
'👩‍🔬',
'👨‍💻',
'👩‍💻',
'👨‍🎤',
'👩‍🎤',
'👨‍🎨',
'👩‍🎨',
'👨‍✈️',
'👩‍✈️',
'👨‍🚀',
'👩‍🚀',
'👨‍🚒',
'👩‍🚒',
'👮',
'👮‍♂️',
'👮‍♀️',
'🕵',
'🕵️‍♂️',
'🕵️‍♀️',
'💂',
'💂‍',
'💂‍',
'👷',
'👷‍',
'👷‍',
'🤴',
'👸',
'👳',
'👳‍',
'👳‍',
'👲',
'🧕',
'🤵',
'👰',
'🤰',
'🤱',
'👼',
'🎅',
'🤶',
'🧙',
'🧚',
'🧛',
'🧜',
'🧝',
'🧞',
'🧟',
'💆',
'💇',
'🚶',
'🏃',
'💃',
'🕺',
'🕴',
'👯',
'🧖',
'🧖‍',
'🧖‍',
'🧘',
'👭',
'👫',
'👬',
'💏',
'👨‍',
'👩‍',
'💑',
'👨‍',
'👩‍',
'👪',
'👨‍👩‍👦',
'👨‍👩‍👧',
'👨‍👩‍👧‍👦',
'👨‍👩‍👦‍👦',
'👨‍👩‍👧‍👧',
'👨‍👨‍👦',
'👨‍👨‍👧',
'👨‍👨‍👧‍👦',
'👩‍👩‍👦',
'👩‍👩‍👧',
'👩‍👩‍👧‍👦',
'👩‍👩‍👦‍👦',
'👩‍👩‍👧‍👧',
'👨‍👦',
'👨‍👦‍👦',
'👨‍👧',
'👨‍👧‍👦',
'👨‍👧‍👧',
'👩‍👦',
'👩‍👦‍👦',
'👩‍👧',
'👩‍👧‍👦',
'👩‍👧‍👧',
'🗣',
'👤',
'👥',
'👣',
'🌂',
'☂',
'👓',
'🕶',
'👔',
'👕',
'👖',
'🧣',
'🧤',
'🧥',
'🧦',
'👗',
'👘',
'👙',
'👚',
'👛',
'👜',
'👝',
'🎒',
'👞',
'👟',
'👠',
'👡',
'👢',
'👑',
'👒',
'🎩',
'🎓',
'🧢',
'⛑',
'💄',
'💍',
'💼',
]
export default emojis

77
src/constant/errorCode.js Normal file
View File

@ -0,0 +1,77 @@
// const ERROR_TYPE = {
// login: 1,
// };
export default {
/*
*/
0: {
'none': '未知错误!'
},
1: {
'invalid password': '密码错误!',
'login failed': '登陆失败!',
'user not found': '该用户不存在!',
},
17: {
'duplicate_unique_property_exists': 'id已存在',
'resource_limited': '注册已达上限请开通企业版!',
'unauthorized': '未开放授权注册!',
'resource_not_found': '账号不存在!',
},
28: {
'appkey or token error': '未登录!',
},
101: {
'file exceeding maximum limit': '文件大小超出限制默认10M',
'none': '文件相关未知错误!'
},
217: {
'the user was kicked by other device': '其他端踢出了该账号!',
},
/* 群组相关 */
602: {
'not in group or chatroom': '已不再该群组中!',
},
605: {
'The chat room dose not exist.': '此群不存在!',
},
/* 消息相关 */
221: {
'not contact': '非好友关系,不可发送消息!',
},
400: {
'UserId password error.': '用户密码错误!',
'Please wait a moment while trying to send.': '验证码在有效期内,请勿重复发送!',
'Image verification code error.': '图片验证码错误,请更换验证码或重新输入!',
'Image code id cannot be empty.': '请填入图片验证码!',
'Phone number cannot be empty.': '获取图片验证码请填入手机号!',
'UserId hfp already exists.': '用户已注册!',
'phone number illegal': '手机号不合法!',
'Please send SMS to get mobile phone verification code.': '请发送短信获取手机验证码!',
'SMS verification code error.': '验证码错误!'
},
603: {
'blocked': '对方已将您加入黑名单!',
'blacklist': '已在该群黑名单当中!无法加入该群。',
'already': '已加入该群!'
},
504: {
'exceed recall time limit': '消息超过可撤回时间!',
},
507: {
'muted': '已被禁言!'
},
508: {
'moderation': '内容审核不通过!请检查发送内容。'
}
// e.type === '603' 被拉黑
// e.type === '605' 群组不存在
// e.type === '602' 不在群组或聊天室中
// e.type === '504' 撤回消息时超出撤回时间
// e.type === '505' 未开通消息撤回
// e.type === '506' 没有在群组或聊天室白名单
// e.type === '501' 消息包含敏感词
// e.type === '502' 被设置的自定义拦截捕获
// e.type === '503' 未知错误
}

14
src/constant/index.js Normal file
View File

@ -0,0 +1,14 @@
import errorCode from './errorCode'
import onLineStatus from './onLineStatus'
import messageType from './messageType'
import informType from './informType'
import emojis from './emojis'
import warningText from './warningText'
export {
errorCode,
onLineStatus,
messageType,
informType,
emojis,
warningText,
}

View File

@ -0,0 +1,52 @@
const INFORM_NAME = {
FRIEND_INVITE: '好友申请',
FRIEND_BUILD: '已成为好友',
FRIEND_DELETED: '好友关系解除',
FRIEND_APPLY_REFUSE: '好友申请被拒绝',
FRIEND_APPLY_AGREE: '好友申请已通过',
GROUP_JOIN_SUCCESS: '成员入群成功',
GROUP_QUIT_SUCCESS: '成员退出群组成功',
GROUP_INVITE_JOIN: '邀请加入群组',
GROUP_REQUESTTOJOIN: '申请加入群组',
GROUP_REMOVE_MEMBER: '移出了群成员',
GROUP_DIRECT_MEMBER: '被直接拉入群组',
GROUP_UPDATE_ANNOUNCEMENT: '更新了群组公告',
GROUP_SET_ADMIN: '设定为管理员',
GROUP_REMOVE_ADMIN: '移除管理员',
GROUP_MUTE_MEMBER: '禁言成员',
GROUP_UNMUTE_MEMBER: '移除成员禁言',
GROUP_DESTORY: '解散群组',
GROUP_ACCEPTREQUEST: '同意入群申请',
GROUP_UPDATE_INFO: '更新群组信息',
GROUP_UPDATE_MEMBER_ATTRIBUTES: '群组成员属性更新'
}
const INFORM_TYPE = {
subscribe: INFORM_NAME.FRIEND_INVITE,
subscribed: INFORM_NAME.FRIEND_BUILD,
unsubscribed: INFORM_NAME.FRIEND_DELETED,
other_person_refuse: INFORM_NAME.FRIEND_APPLY_REFUSE,
other_person_agree: INFORM_NAME.FRIEND_APPLY_AGREE,
memberPresence: INFORM_NAME.GROUP_JOIN_SUCCESS,
memberAbsence: INFORM_NAME.GROUP_QUIT_SUCCESS,
inviteToJoin: INFORM_NAME.GROUP_INVITE_JOIN,
removeMember: INFORM_NAME.GROUP_REMOVE_MEMBER,
directJoined: INFORM_NAME.GROUP_DIRECT_MEMBER,
updateAnnouncement: INFORM_NAME.GROUP_UPDATE_ANNOUNCEMENT,
setAdmin: INFORM_NAME.GROUP_SET_ADMIN,
removeAdmin: INFORM_NAME.GROUP_REMOVE_ADMIN,
muteMember: INFORM_NAME.GROUP_MUTE_MEMBER,
unmuteMember: INFORM_NAME.GROUP_UNMUTE_MEMBER,
destroy: INFORM_NAME.GROUP_DESTORY,
requestToJoin: INFORM_NAME.GROUP_REQUESTTOJOIN,
acceptRequest: INFORM_NAME.GROUP_ACCEPTREQUEST,
updateInfo: INFORM_NAME.GROUP_UPDATE_INFO,
memberAttributesUpdate: INFORM_NAME.GROUP_UPDATE_MEMBER_ATTRIBUTES
}
const INFORM_FROM = {
FRIEND: 'friend',
GROUP: 'group'
}
export default {
INFORM_TYPE,
INFORM_FROM
}

View File

@ -0,0 +1,43 @@
const SESSION_MESSAGE_TYPE = {
img: '[图片]',
file: '[文件]',
audio: '[语音]',
loc: '[位置]'
}
const CUSTOM_TYPE = {
userCard: '个人名片'
}
const ALL_MESSAGE_TYPE = {
TEXT: 'txt',
IMAGE: 'img',
AUDIO: 'audio',
LOCAL: 'loc',
VIDEO: 'video',
FILE: 'file',
CUSTOM: 'custom',
CMD: 'cmd',
INFORM: 'inform' //这个类型不在环信消息类型内,属于自己定义的一种系统通知类的消息。
}
const CHAT_TYPE = {
SINGLE: 'singleChat',
GROUP: 'groupChat'
}
const MENTION_ALL = {
TEXT: '所有人',
VALUE: 'ALL'
}
const CHANGE_MESSAGE_BODAY_TYPE = {
RECALL: 0,
DELETE: 1,
MODIFY: 2
}
export default {
SESSION_MESSAGE_TYPE,
CUSTOM_TYPE,
ALL_MESSAGE_TYPE,
CHAT_TYPE,
MENTION_ALL,
CHANGE_MESSAGE_BODAY_TYPE
}

View File

@ -0,0 +1,11 @@
const onLineStatus = {
Online: { label: '在线', style: 'background-color:#49FD1D' },
Leave: { label: '离开', style: 'background-color:#4E4239' },
Cloaking: {
label: '勿扰',
style: 'background-color:#F27014',
},
Offline: { label: '离线', style: 'background-color:#BEC1BD' },
}
export default onLineStatus

View File

@ -0,0 +1,39 @@
const SWINDLER_GO_DIE = [
'时刻绷紧防范之弦,谨防新型电信诈骗。',
'号码陌⽣勿轻接,虚拟电话设陷阱。',
'飞来⼤奖莫惊喜,让您掏钱洞⽆底。',
'不存贪婪⼼,诈骗难得逞。',
'提⾼防骗意识,增强防范能⼒,构筑电信诈骗“防⽕墙。',
'骗⼈之⼼不可有,防骗之⼼不可⽆。',
'⽹上汇款需警惕,电话核实莫⼤意。',
'执法办案有规范,怎会汇款到个⼈。',
'不明电话及时挂,可疑短信不要回。',
'⽹络购物便利多,⽀付流程要仔细。',
'投资理财和股票,多是骗⼦设的套。',
'不信陌⽣短信,拒接陌⽣来电,让骗⼦⽆从下⼿。',
'⼀不贪⼆不占,诈骗再诡玩不转。',
'遇到恐吓要淡定说你违法莫慌张⼀旦难分真与假警方电话110。',
'陌⽣来电要提防,多⽅确认防上当。',
'致富⼗年功,诈骗⼀场空。',
'积极加强⾃我防范意识,共同提⾼识骗防骗能⼒。',
'防范⽹络的骗术,不贪便宜要记住。',
' 和谐⽹络你我共享,电信诈骗⼤家共防。',
'真假⽹店难分辨,购物不慎就被骗。',
'个⼈信息顶重要,密码账号保管好。',
'飞来⼤奖莫惊喜,让你掏钱洞⽆底。',
'安全账户⼦虚有,⼤额汇款要三思。',
'异地刷卡消费现,不要着急忙给钱。',
'电话通知接传票,实为骗钱设圈套。',
'刷卡消费莫离眼,防⽌盗刷盯着点。',
'⼼中⽆贪念,骗局远⾝边。',
'转账汇款须谨慎,万元以上到柜⾯。',
'陌⽣电话勿轻信,对⽅⾝份要核清。',
'电信诈骗不难防,不给不要不上当。',
'陌⽣信息不要理,以防害⼈⼜害⼰。',
]
const EASEIM_HINT =
'【安全提示】本应用仅用于环信产品功能开发测试,请勿用于非法用途。任何涉及转账、汇款、裸聊、网恋、网购退款、投资理财等统统都是诈骗,请勿相信!'
const WARM_TIP = '【温馨提示】该群仅供试用72小时后将被删除'
export default { SWINDLER_GO_DIE, EASEIM_HINT, WARM_TIP }

View File

@ -593,7 +593,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
children: [
{
// 会话详情
path: 'informdetails',
path: 'informDetails',
name: 'InformDetails',
meta: {
title: '通知详情',

View File

@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -0,0 +1,345 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>iconfont Demo</title>
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i2/O1CN01ZyAlrn1MwaMhqz36G_!!6000000001499-73-tps-64-64.ico" type="image/x-icon"/>
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01EYTRnJ297D6vehehJ_!!6000000008020-55-tps-64-64.svg"/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
<script src="iconfont.js"></script>
<!-- jQuery -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
<!-- 代码高亮 -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
<style>
.main .logo {
margin-top: 0;
height: auto;
}
.main .logo a {
display: flex;
align-items: center;
}
.main .logo .sub-title {
margin-left: 0.5em;
font-size: 22px;
color: #fff;
background: linear-gradient(-45deg, #3967FF, #B500FE);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div class="main">
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
</a></h1>
<div class="nav-tabs">
<ul id="tabs" class="dib-box">
<li class="dib active"><span>Unicode</span></li>
<li class="dib"><span>Font class</span></li>
<li class="dib"><span>Symbol</span></li>
</ul>
</div>
<div class="tab-container">
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe64a;</span>
<div class="name">emoji</div>
<div class="code-name">&amp;#xe64a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xf0203;</span>
<div class="name">3.1电话</div>
<div class="code-name">&amp;#xf0203;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe610;</span>
<div class="name">语音</div>
<div class="code-name">&amp;#xe610;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe61f;</span>
<div class="name">视频</div>
<div class="code-name">&amp;#xe61f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe615;</span>
<div class="name">垃圾桶</div>
<div class="code-name">&amp;#xe615;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe69f;</span>
<div class="name">文件</div>
<div class="code-name">&amp;#xe69f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe712;</span>
<div class="name">图库</div>
<div class="code-name">&amp;#xe712;</div>
</li>
</ul>
<div class="article markdown">
<h2 id="unicode-">Unicode 引用</h2>
<hr>
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
</ul>
<blockquote>
<p>注意:新版 iconfont 支持两种方式引用多色图标SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
</blockquote>
<p>Unicode 使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.ttf?t=1654496599109') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
<pre><code class="language-css"
>.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre>
<code class="language-html"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-emoji"></span>
<div class="name">
emoji
</div>
<div class="code-name">.icon-emoji
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-31dianhua"></span>
<div class="name">
3.1电话
</div>
<div class="code-name">.icon-31dianhua
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-01"></span>
<div class="name">
语音
</div>
<div class="code-name">.icon-01
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-video"></span>
<div class="name">
视频
</div>
<div class="code-name">.icon-video
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-lajitong"></span>
<div class="name">
垃圾桶
</div>
<div class="code-name">.icon-lajitong
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-wenjian"></span>
<div class="name">
文件
</div>
<div class="code-name">.icon-wenjian
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-tuku"></span>
<div class="name">
图库
</div>
<div class="code-name">.icon-tuku
</div>
</li>
</ul>
<div class="article markdown">
<h2 id="font-class-">font-class 引用</h2>
<hr>
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
<p>与 Unicode 使用方式相比,具有如下特点:</p>
<ul>
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"
iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-emoji"></use>
</svg>
<div class="name">emoji</div>
<div class="code-name">#icon-emoji</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-31dianhua"></use>
</svg>
<div class="name">3.1电话</div>
<div class="code-name">#icon-31dianhua</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-01"></use>
</svg>
<div class="name">语音</div>
<div class="code-name">#icon-01</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-video"></use>
</svg>
<div class="name">视频</div>
<div class="code-name">#icon-video</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-lajitong"></use>
</svg>
<div class="name">垃圾桶</div>
<div class="code-name">#icon-lajitong</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-wenjian"></use>
</svg>
<div class="name">文件</div>
<div class="code-name">#icon-wenjian</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-tuku"></use>
</svg>
<div class="name">图库</div>
<div class="code-name">#icon-tuku</div>
</li>
</ul>
<div class="article markdown">
<h2 id="symbol-">Symbol 引用</h2>
<hr>
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</code></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('.tab-container .content:first').show()
$('#tabs li').click(function (e) {
var tabContent = $('.tab-container .content')
var index = $(this).index()
if ($(this).hasClass('active')) {
return
} else {
$('#tabs li').removeClass('active')
$(this).addClass('active')
tabContent.hide().eq(index).fadeIn()
}
})
})
</script>
</body>
</html>

View File

@ -0,0 +1,41 @@
@font-face {
font-family: "iconfont"; /* Project id */
src: url('iconfont.ttf?t=1654496599109') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-emoji:before {
content: "\e64a";
}
.icon-31dianhua:before {
content: "\f0203";
}
.icon-01:before {
content: "\e610";
}
.icon-video:before {
content: "\e61f";
}
.icon-lajitong:before {
content: "\e615";
}
.icon-wenjian:before {
content: "\e69f";
}
.icon-tuku:before {
content: "\e712";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,58 @@
{
"id": "",
"name": "",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "29929",
"name": "emoji",
"font_class": "emoji",
"unicode": "e64a",
"unicode_decimal": 58954
},
{
"icon_id": "201577",
"name": "3.1电话",
"font_class": "31dianhua",
"unicode": "f0203",
"unicode_decimal": 983555
},
{
"icon_id": "1236846",
"name": "语音",
"font_class": "01",
"unicode": "e610",
"unicode_decimal": 58896
},
{
"icon_id": "3878694",
"name": "视频",
"font_class": "video",
"unicode": "e61f",
"unicode_decimal": 58911
},
{
"icon_id": "7587956",
"name": "垃圾桶",
"font_class": "lajitong",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "20710439",
"name": "文件",
"font_class": "wenjian",
"unicode": "e69f",
"unicode_decimal": 59039
},
{
"icon_id": "27334037",
"name": "图库",
"font_class": "tuku",
"unicode": "e712",
"unicode_decimal": 59154
}
]
}

Binary file not shown.

View File

@ -1,31 +1,49 @@
<script setup lang="ts">
import { formatDate } from '@/utils/formatTime'
import { reactive } from 'vue'
//(使)
const friendList = reactive([
{
friendKey: {
avatarurl: '',
nickName: ''
/* 头像相关 */
import informIcon from '@/assets/imgs/avatar/inform.png'
/* route */
const route = useRoute()
/* router */
const router = useRouter()
//
const informDetail = computed(() => {
const informDetailArr = reactive([
{
from: '系统通知',
desc: '您有一条新的通知',
time: new Date(),
untreated: 1
}
}
])
//
const conversationList = reactive({
conversationKey: {
conversationInfo: {
avatarUrl: '',
name: '',
conversationType: 0
},
latestMessage: {
msg: ''
},
latestSendTime: 0,
unreadMessageNum: 0,
isMention: false
])
const lastInformDeatail = informDetailArr[0] || {}
const untreated = 1
return { untreated, lastInformDeatail }
})
//(使)
const friendList = reactive({
1: {
avatarurl: 'https://img.yzcdn.cn/vant/cat.jpeg'
}
})
//
const conversationList = reactive([
{
conversationKey: 1,
conversationInfo: { avatarUrl: 'https://img.yzcdn.cn/vant/cat.jpeg' },
name: '好友1',
conversationType: 2,
latestMessage: {
msg: 'hello word!'
},
latestSendTime: new Date(),
unreadMessageNum: 5,
isMention: false
}
])
//name
const handleConversationName = computed(() => {
@ -35,74 +53,112 @@ const handleConversationName = computed(() => {
const handleLastMsgNickName = computed(() => {
return ''
})
const emit = defineEmits(['toInformDetails', 'toChatMessage'])
//
const checkedConverItemIndex = ref(null)
const toChatMessage = (item, itemKey, index) => {
checkedConverItemIndex.value = index
console.log('选中的会话key', itemKey)
//
emit('toChatMessage', itemKey, item.conversationType)
}
//
const deleteConversation = (itemKey) => {}
const deleteConversation = (itemKey) => {
console.log('选中的会话key', itemKey)
}
</script>
<template>
<!-- 普通会话 -->
<template v-if="Object.keys(conversationList).length > 0">
<el-scrollbar class="session_list" style="overflow: auto" tag="ul">
<!-- 系统通知会话 -->
<li
v-for="(item, itemKey, index) in conversationList"
:key="itemKey"
@click="toChatMessage(item, itemKey, index)"
:style="{
background: checkedConverItemIndex === index ? '#E5E5E5' : ''
}"
v-if="JSON.stringify(informDetail.lastInformDeatail) !== '{}' && informDetail.untreated >= 1"
class="session_list_item"
@click="$emit('toInformDetails')"
>
<el-popover
popper-class="conversation_popover"
placement="right-end"
trigger="contextmenu"
:show-arrow="false"
:offset="-10"
>
<template #reference>
<div class="session_list_item">
<div class="item_body item_left">
<div class="session_other_avatar">
<el-avatar
:size="34"
:src="
friendList[item.conversationKey] && friendList[item.conversationKey].avatarurl
? friendList[item.conversationKey].avatarurl
: item.conversationInfo.avatarUrl
"
/>
</div>
</div>
<div class="item_body item_main">
<div class="name"> 好友 </div>
<div class="last_msg_body">
<span class="last_msg_body_mention" v-if="item.isMention">[有人@]</span>
<span v-show="item.conversationType === 2">好友</span>
{{ item.latestMessage.msg }}
</div>
</div>
<div class="item_body item_right">
<span class="time">{{ formatDate(item.latestSendTime, 'MM/DD/HH:mm') }}</span>
<span class="unReadNum_box" v-if="item.unreadMessageNum >= 1">
<sup
class="unReadNum_count"
v-text="item.unreadMessageNum >= 99 ? '99+' : item.unreadMessageNum"
></sup>
</span>
</div>
</div>
</template>
<template #default>
<div class="session_list_delete" @click="deleteConversation(itemKey)"> 删除会话 </div>
</template>
</el-popover>
<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>
<template v-else>
<el-empty description="暂无最近会话" />
</template>
<!-- 普通会话 -->
<template v-if="Object.keys(conversationList).length > 0">
<li
v-for="(item, itemKey, index) in conversationList"
:key="itemKey"
@click="toChatMessage(item, itemKey, index)"
:style="{
background: checkedConverItemIndex === index ? '#E5E5E5' : ''
}"
>
<el-popover
popper-class="conversation_popover"
placement="right-end"
trigger="contextmenu"
:show-arrow="false"
:offset="-10"
>
<template #reference>
<div class="session_list_item">
<div class="item_body item_left">
<div class="session_other_avatar">
<el-avatar
:size="34"
:src="
friendList[item.conversationKey] && friendList[item.conversationKey].avatarurl
? friendList[item.conversationKey].avatarurl
: item.conversationInfo.avatarUrl
"
/>
</div>
</div>
<div class="item_body item_main">
<div class="name"> 好友 </div>
<div class="last_msg_body">
<span class="last_msg_body_mention" v-if="item.isMention">[有人@]</span>
<span v-show="item.conversationType === 2">好友</span>
{{ item.latestMessage.msg }}
</div>
</div>
<div class="item_body item_right">
<span class="time">{{ formatDate(item.latestSendTime, 'MM/DD/HH:mm') }}</span>
<span class="unReadNum_box" v-if="item.unreadMessageNum >= 1">
<sup
class="unReadNum_count"
v-text="item.unreadMessageNum >= 99 ? '99+' : item.unreadMessageNum"
></sup>
</span>
</div>
</div>
</template>
<template #default>
<div class="session_list_delete" @click="deleteConversation(itemKey)"> 删除会话 </div>
</template>
</el-popover>
</li>
</template>
<template v-else>
<el-empty description="暂无最近会话" />
</template>
</el-scrollbar>
</template>
<style scoped lang="scss">

View File

@ -4,6 +4,24 @@ import SearchInput from '@/components/SearchInput/index.vue'
/* 欢迎页 */
import Welcome from '@/components/Welcome/index.vue'
import ConversationList from '../Conversation/components/ConversationList.vue'
import router from '@/router'
//-
const toInformDetails = () => {
router.push('/im/conversation/informDetails')
}
//-
const toChatMessage = (id, chatType) => {
console.log('>>>>>>>id', id)
router.push({
path: '/im/conversation/message',
query: {
id,
chatType
}
})
}
</script>
<template>
<el-container style="height: 100%">
@ -11,7 +29,7 @@ import ConversationList from '../Conversation/components/ConversationList.vue'
<!-- 搜索组件 -->
<SearchInput :searchType="'conversation'" />
<div class="chat_conversation_list">
<ConversationList />
<ConversationList @toInformDetails="toInformDetails" @toChatMessage="toChatMessage" />
</div>
</el-aside>
<el-main class="chat_conversation_main_box">

View File

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

View File

@ -0,0 +1,107 @@
.chat_func_box {
position: relative;
display: flex;
align-items: center;
height: 42px;
width: 100%;
background-color: #f7f7f7;
border-top: 1px solid #e6e6e6;
border-bottom: 1px solid #e6e6e6;
line-height: 12px;
.chat_func_icon {
width: 25px;
height: 25px;
}
.emojis_box {
position: absolute;
left: 15px;
top: -180px;
width: 330px;
height: 150px;
border-radius: 5px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 15px 5px;
.emoji {
display: inline-block;
width: 25px;
height: 25px;
text-align: center;
line-height: 25px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(1.2);
}
}
}
.loading_box {
position: absolute;
right: 5px;
top: 0;
width: 50px;
height: 100%;
font-size: 15px;
}
}
/* loading svg大小调整 */
::v-deep .circular {
margin-top: 8px;
width: 25px;
height: 25px;
}
.chat_content_editable {
font-family: 'PingFang SC';
width: 100%;
box-sizing: border-box;
min-height: 100px;
border: none;
background: none;
letter-spacing: 0.5px;
resize: none;
padding: 10px 20px;
font-size: 14px;
line-height: 10px;
}
.no_content_send_btn {
position: absolute;
bottom: 20px;
right: 20px;
width: 80px;
opacity: 0.5;
}
.chat_send_btn {
position: absolute;
bottom: 20px;
right: 20px;
width: 80px;
}
.iconfont {
margin-right: 12px;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: scale(1.2);
color: #1b83f9;
}
}
.record_box {
width: 250px;
height: 180px;
}

View File

@ -0,0 +1,453 @@
<script setup>
import { ElLoading, ElMessageBox } from 'element-plus'
import { emojis } from '@/constant'
import { messageType } from '@/constant'
import _ from 'lodash'
/* 组件 */
import PreviewSendImg from '../suit/previewSendImg.vue'
import VueAt from 'vue-at/dist/vue-at-textarea' // for textarea
const { ALL_MESSAGE_TYPE, CHAT_TYPE, MENTION_ALL } = messageType
const nowPickInfo = ref({
id: '',
chatType: ''
})
const atMembersList = ref([])
//
const loadingBox = ref(null)
const isAtAll = ref(false)
const atMembers = ref([])
//@
const onInsert = (target) => {
// if (!) return false
console.log('onInset', target)
if (_.map(atMembers.value, 'value').includes(target.value)) return false
if (target.value === MENTION_ALL.VALUE) {
return (isAtAll.value = true)
} else {
atMembers.value.push({ ...target })
}
}
//@
const checkAtMembers = (text) => {
if (!text) {
return false
}
//@ALLfalse
const patternAtAll = new RegExp(`@${MENTION_ALL.TEXT}`)
console.log('patternAtAll', patternAtAll)
if (isAtAll.value && !patternAtAll.test(text)) {
isAtAll.value = false
}
if (atMembers.value.length !== 0) {
//AT@
_.map(atMembers.value, 'text').forEach((item, index) => {
console.log('atMembers item', item, index)
const pattern = new RegExp(`@${item}`)
const result = pattern.test(text)
if (!result) {
console.log('文本中不满足条件')
//@
atMembers.value.splice(index, 1)
console.log('>>>>>已删除', atMembers.value)
}
})
}
}
//emojis
const isShowEmojisBox = ref(false)
const emojisBox = ref(null)
const showEmojisBox = () => {
console.log('>>>>>展开模态框')
isShowEmojisBox.value = true
}
//emoji
const addOneEmoji = (emoji) => {
console.log('>>>>>>emoji', emoji)
textContent.value = textContent.value + emoji
}
//
const messageQuoteRef = ref(null)
const handleQuoteMessage = (msgBody) => {
messageQuoteRef.value && messageQuoteRef.value.setQuoteContent(msgBody)
}
//
const insertNewLine = () => (textContent.value += '\n')
//
const textContent = ref('')
const sendTextMessage = _.debounce(async () => {
//
if (textContent.value.match(/^\s*$/)) return
console.log('atMembers.value', atMembers.value)
checkAtMembers(textContent.value)
const msgOptions = {
id: nowPickInfo.value.id,
chatType: nowPickInfo.value.chatType,
msg: textContent.value,
ext: {
em_at_list: isAtAll.value ? MENTION_ALL.VALUE : _.map(atMembers.value, 'value')
}
}
//
if (messageQuoteRef.value?.isShowQuoteMsgBox) {
}
textContent.value = ''
messageQuoteRef.value?.clearQuoteContent()
try {
console.log('msgOptions', msgOptions)
// await store.dispatch('sendShowTypeMessage', {
// msgType: ALL_MESSAGE_TYPE.TEXT,
// msgOptions
// })
} catch (error) {
//handleSDKErrorNotifi(error.type, error.message)
console.log('>>>>>>>发送失败+++++++', error)
} finally {
isAtAll.value = false
atMembers.value = []
}
}, 50)
//enter,shift+enter
const onTextInputKeyDown = (event) => {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault()
//
sendTextMessage()
} else if (event.keyCode === 13 && event.shiftKey) {
//
insertNewLine()
}
}
/* 图片消息相关 */
//
const uploadImgs = ref(null)
const chooseImages = () => {
uploadImgs.value.click()
console.log('uploadImgs')
}
//
const sendImagesMessage = async (type) => {
const file = {
data: null, // file
filename: '', //
filetype: '' //
}
const url = window.URL || window.webkitURL
const img = new Image() //Image
const msgOptions = {
id: nowPickInfo.value.id,
chatType: nowPickInfo.value.chatType,
file: file,
width: 0,
height: 0
}
if (type === 'common') {
//
const imgFile = uploadImgs.value.files[0]
file.data = imgFile
file.filename = imgFile.name
file.filetype = imgFile.type
console.log('imgFile', file)
img.src = url.createObjectURL(imgFile) //Imageurl
img.onload = async () => {
const loadingInstance = ElLoading.service({
target: loadingBox.value,
background: '#f7f7f7'
})
msgOptions.width = img.width
msgOptions.height = img.height
console.log('height:' + img.height + '----' + img.width)
try {
// await store.dispatch('sendShowTypeMessage', {
// msgType: ALL_MESSAGE_TYPE.IMAGE,
// msgOptions: _.cloneDeep(msgOptions)
// })
loadingInstance.close()
uploadImgs.value.value = null
} catch (error) {
console.log('>>>>>发送失败', error)
if (error.type && error?.data) {
handleSDKErrorNotifi(error.type, error.data.error || 'none')
} else {
handleSDKErrorNotifi(0, 'none')
}
loadingInstance.close()
uploadImgs.value.value = null
}
}
} else if (type === 'other') {
console.log('fileObjfileObjfileObj', fileObj)
const imgFile = fileObj
file.data = imgFile
file.filename = imgFile.name
file.filetype = imgFile.type
console.log('imgFile', file)
img.src = url.createObjectURL(imgFile) //Imageurl
img.onload = async () => {
const loadingInstance = ElLoading.service({
target: loadingBox.value,
background: '#f7f7f7'
})
msgOptions.width = img.width
msgOptions.height = img.height
console.log('height:' + img.height + '----' + img.width)
try {
await store.dispatch('sendShowTypeMessage', {
msgType: ALL_MESSAGE_TYPE.IMAGE,
msgOptions: _.cloneDeep(msgOptions)
})
loadingInstance.close()
uploadImgs.value.value = null
} catch (error) {
console.log('>>>>>发送失败', error)
if (error.type && error?.data) {
handleSDKErrorNotifi(error.type, error.data.error || 'none')
} else {
handleSDKErrorNotifi(0, 'none')
}
loadingInstance.close()
uploadImgs.value.value = null
}
}
}
}
//
const previewSendImg = ref(null)
const onPasteImage = (event) => {
console.log('>>>>>>监听粘贴事件', event)
const data = event.clipboardData || window.clipboardData
//
const imgContent = data.items[0].getAsFile()
//
const isImg = (imgContent && 1) || -1
const reader = new FileReader()
if (isImg >= 0) {
// DataURL
reader.readAsDataURL(imgContent)
}
//
reader.onload = (event) => {
//base64
const base64_str = event.target.result
const imgInfo = {
imgFile: imgContent,
tempFilePath: base64_str
}
previewSendImg.value.showPreviewImgModal({ ...imgInfo })
console.log('>>>>>获取到粘贴到的文本', imgInfo)
}
}
/* 文件消息相关 */
//
const uploadFiles = ref(null)
const chooseFiles = () => {
uploadFiles.value.click()
}
//
const sendFilesMessages = async () => {
const commonFile = uploadFiles.value.files[0]
const file = {
data: commonFile, // file
filename: commonFile.name, //
filetype: commonFile.type, //
size: commonFile.size
}
console.log('>>>>>调用发送文件', file)
const msgOptions = {
id: nowPickInfo.value.id,
chatType: nowPickInfo.value.chatType,
file: file
}
const loadingInstance = ElLoading.service({
target: loadingBox.value,
background: '#f7f7f7'
})
try {
// await store.dispatch('sendShowTypeMessage', {
// msgType: ALL_MESSAGE_TYPE.FILE,
// msgOptions: _.cloneDeep(msgOptions)
// })
loadingInstance.close()
uploadFiles.value.value = null
} catch (error) {
console.log('>>>>file error', error)
if (error.type && error?.data) {
handleSDKErrorNotifi(error.type, error.data.error || 'none')
} else {
handleSDKErrorNotifi(0, 'none')
}
uploadFiles.value.value = null
loadingInstance.close()
}
}
/* 语音消息相关 */
//
const isHttps = window.location.protocol === 'https:' || window.location.hostname === 'localhost'
const isShowRecordBox = ref(false)
const recordBox = ref(null)
const showRecordBox = () => {
isShowRecordBox.value = true
}
const sendAudioMessages = async (audioData) => {
const file = {
// url: EaseChatSDK.utils.parseDownloadResponse(audioData.src),
filename: '录音',
filetype: '.amr',
data: audioData.src
}
console.log('>>>>>audioData', audioData, file)
const msgOptions = {
id: nowPickInfo.value.id,
chatType: nowPickInfo.value.chatType,
file: file,
length: audioData.length
}
try {
// await store.dispatch('sendShowTypeMessage', {
// msgType: ALL_MESSAGE_TYPE.AUDIO,
// msgOptions: _.cloneDeep(msgOptions)
// })
isShowRecordBox.value = false
} catch (error) {
// if (error.type && error?.data) {
// handleSDKErrorNotifi(error.type, error.data.error || 'none')
// } else {
// handleSDKErrorNotifi(0, 'none')
// }
isShowRecordBox.value = false
}
}
/*清除屏幕*/
const clearScreen = () => {
ElMessageBox.confirm('确认清空当前消息内容?', '消息清屏', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
const key = nowPickInfo.value.id
// store.commit('CLEAR_SOMEONE_MESSAGE', key)
})
.catch(() => {
return false
})
}
//func icon class
const all_func = [
{
className: 'icon-emoji',
style: 'font-size:20px;margin-left: 20px;',
title: '选择表情',
methodName: showEmojisBox
},
{
className: 'icon-tuku',
style: 'font-size: 26px;',
title: '发送图片',
methodName: chooseImages
},
{
className: 'icon-wenjian',
style: 'font-size: 20px;',
title: '发送文件',
methodName: chooseFiles
},
{
className: 'icon-01',
style: 'font-size: 20px;',
title: '发送语音',
methodName: showRecordBox
},
{
className: 'icon-lajitong',
style: 'font-size: 23px;',
title: '清屏',
methodName: clearScreen
}
]
defineExpose({
textContent,
handleQuoteMessage
})
</script>
<template>
<div class="chat_func_box">
<span
v-for="iconItem in all_func"
:class="['iconfont', iconItem.className]"
:key="iconItem.className"
:style="iconItem.style"
:title="iconItem.title"
@click.stop="iconItem.methodName"
></span>
<!-- 表情框 -->
<el-scrollbar ref="emojisBox" v-if="isShowEmojisBox" class="emojis_box" tag="div">
<span
class="emoji"
v-for="(emoji, index) in emojis"
:key="index"
@click="addOneEmoji(emoji)"
>{{ emoji }}</span
>
</el-scrollbar>
<!-- 图片附件choose -->
<input
ref="uploadImgs"
type="file"
style="display: none"
@change="sendImagesMessage('common')"
accept="image/*"
/>
<!-- 文件附件choose -->
<input ref="uploadFiles" type="file" style="display: none" @change="sendFilesMessages" />
<!-- 录音采集框 -->
<el-card ref="recordBox" v-if="isShowRecordBox" class="record_box" shadow="always">
<p v-if="!isHttps"> 由于浏览器限制,录音功能必须为https环境或者为localhost环境下使用 </p>
<!-- <CollectAudio v-else @sendAudioMessages="sendAudioMessages" />-->
</el-card>
<!-- 附件上传加载容器 -->
<div ref="loadingBox" class="loading_box"></div>
</div>
<template v-if="nowPickInfo.chatType === CHAT_TYPE.SINGLE">
<textarea
ref="editable"
v-model="textContent"
class="chat_content_editable"
spellcheck="false"
contenteditable="true"
placeholder="请输入消息内容..."
@keydown="onTextInputKeyDown"
@paste="onPasteImage"
>
</textarea>
</template>
<template v-else-if="nowPickInfo.chatType === CHAT_TYPE.GROUP">
<vue-at :members="atMembersList" name-key="text" @insert="onInsert">
<textarea
ref="editable"
v-model="textContent"
class="chat_content_editable"
spellcheck="false"
contenteditable="true"
placeholder="请输入消息内容..."
@keydown="onTextInputKeyDown"
@paste="onPasteImage"
>
</textarea>
</vue-at>
</template>
<el-button
:class="[textContent === '' ? 'no_content_send_btn' : 'chat_send_btn']"
type="primary"
@click="sendTextMessage"
>发送</el-button
>
<PreviewSendImg ref="previewSendImg" @send-images-message="sendImagesMessage" />
</template>
<style lang="scss" scoped>
@import './index.scss';
@import '@/styles/iconfont/iconfont.css';
</style>

View File

@ -0,0 +1,303 @@
.messageList_box {
width: 100%;
.message_box_item {
position: relative;
display: flex;
margin: 32px auto;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.4px;
color: #333333;
.message_item_time {
position: absolute;
top: -25px;
left: 0;
right: 0;
margin: auto;
width: 74px;
height: 20px;
color: #adadad;
font-weight: 400;
font-size: 10px;
line-height: 20px;
}
.message_item_avator {
width: 38px;
height: 38px;
}
.message_box_card {
display: flex;
flex-direction: column;
max-width: 50%;
min-height: 34px;
}
.message_box_nickname {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.4px;
color: #9a9a9a;
margin: 0 10px;
}
.message_box_content {
display: flex;
align-items: center;
margin: 0 6px;
word-break: break-all;
/* 通用音频播放样式 */
.message_box_content_audio {
display: flex;
justify-content: flex-end;
align-items: center;
max-width: 250px;
min-width: 80px;
font-size: 12px;
.audio_length_text {
font-family: 'Avenir';
font-style: normal;
font-weight: 400;
font-size: 12px;
}
}
/* 对方音频播放样式 */
.message_box_content_audio_other {
flex-direction: row;
@keyframes other_play_icon {
0% {
background: url('@/assets/images/playAudio/msg_recv_audio02@3x.png')
no-repeat;
background-size: 100% 100%;
}
50% {
background: url('@/assets/images/playAudio/msg_recv_audio01@3x.png')
no-repeat;
background-size: 100% 100%;
}
100% {
background: url('@/assets/images/playAudio/msg_recv_audio@3x.png')
no-repeat;
background-size: 100% 100%;
}
}
.play_audio_icon_other {
width: 30px;
height: 30px;
background: url('@/assets/images/playAudio/msg_recv_audio@3x.png')
no-repeat;
margin-right: 10px;
}
.start_play_audio {
animation: other_play_icon 2s;
animation-iteration-count: infinite;
}
}
/* 己方音频播放样式 */
.message_box_content_audio_mine {
flex-direction: row-reverse;
@keyframes mine_play_icon {
0% {
background: url('@/assets/images/playAudio/msg_send_audio02@3x.png')
no-repeat;
background-size: 100% 100%;
}
50% {
background: url('@/assets/images/playAudio/msg_send_audio01@3x.png')
no-repeat;
background-size: 100% 100%;
}
100% {
background: url('@/assets/images/playAudio/msg_send_audio@3x.png')
no-repeat;
background-size: 100% 100%;
}
}
.play_audio_icon_mine {
width: 30px;
height: 30px;
background-size: 100% 100%;
background: url('@/assets/images/playAudio/msg_send_audio@3x.png')
no-repeat;
margin-left: 10px;
}
.start_play_audio {
animation: mine_play_icon 2s;
animation-iteration-count: infinite;
}
}
/* 文件消息样式 */
.message_box_content_file {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 200px;
min-height: 60px;
max-height: 120px;
padding: 10px;
.file_text_box {
width: 75%;
height: 80%;
display: flex;
flex-direction: column;
justify-content: space-around;
.file_name {
width: 120px;
white-space: wrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 15px;
font-weight: bold;
}
.file_size {
font-size: 13px;
}
.file_download {
width: 100%;
color: #333333;
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(0.9);
}
}
}
.icon-wenjian {
font-size: 50px;
color: #8d8a8a;
}
}
/* 自定义消息 */
.message_box_content_custom {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 200px;
min-height: 60px;
max-height: 120px;
padding: 10px;
overflow: hidden;
.user_card_main {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
color: #333333;
font-size: 17px;
.nickname {
display: inline-block;
// width: 100%;
margin-left: 10px;
height: 35px;
line-height: 35px;
}
}
}
/* 个人名片 */
}
.quote_msg_avtive {
animation: twinkle 0.4s infinite alternate;
}
.quote_msg_avtive ::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 5px;
background-color: rgba(247, 169, 35, 0.5);
}
@keyframes twinkle {
0% {
opacity: 0.3;
}
50% {
opacity: 0.6;
}
100% {
opacity: 0.9;
}
}
.message_box_content_other {
background: #fff;
border-radius: 8px 8px 8px 0px;
}
.message_box_content_mine {
background: #c1e3fc;
border-radius: 8px 0px 8px 8px;
}
}
/* 撤回或者系统通知类消息 */
.recall_style,
.inform_style {
height: 60px;
text-align: center;
color: #aaaaaa;
font-size: 10px;
margin: 5px 0;
.reEdit {
color: #3e91fa;
margin-left: 3px;
cursor: pointer;
}
}
}
.message_quote_box {
padding: 5px 10px;
font-size: 7px;
background-color: #e7e7e7;
border-radius: 5px;
margin-top: 5px;
color: #a0a0a0;
cursor: pointer;
p {
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
:deep(.el-input__wrapper) {
border-radius: 5px;
}
:deep(.el-dialog__header) {
background: #f2f2f2;
margin: 0;
}

View File

@ -0,0 +1,303 @@
<script setup>
import { formatDate } from '@/utils/formatTime'
/* 默认头像 */
import defaultAvatar from '@/assets/imgs/avatar.gif'
const messageData = ref([
{
id: 1,
type: 'text',
isRecall: false,
time: '2024-04-01 12:00:00',
from: '1',
msg: 'Hello, world!',
modifiedInfo: {
operationCount: 1
}
},
{
id: 2,
type: 'text',
isRecall: false,
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 = {
TEXT: 'txt',
IMAGE: 'img',
AUDIO: 'audio',
LOCAL: 'loc',
VIDEO: 'video',
FILE: 'file',
CUSTOM: 'custom',
CMD: 'cmd',
INFORM: 'inform' //
}
/* 处理时间显示间隔 */
const handleMsgTimeShow = (time, index) => {
const msgList = Array.from(messageData.value)
if (index !== 0) {
const lastTime = msgList[index - 1].time
return time - lastTime > 50000 ? formatDate(time, 'MM/DD/HH:mm') : false
} else {
return formatDate(time, 'MM/DD/HH:mm')
}
}
/* computed-- 消息来源是否为自己 */
const isMyself = (msgBody) => {
return msgBody.from === '1'
}
/* 获取自己的用户信息 */
const loginUserInfo = {
avatarurl: 'https://avatars.githubusercontent.com/u/1?v=4'
}
/* 获取他人的用户信息 */
const otherUserInfo = (from) => {
return {
avatarurl: 'https://avatars.githubusercontent.com/u/2?v=4'
}
}
//
const handleNickName = (from) => {
return from === '1' ? '我' : '对方'
}
//
let clickQuoteMsgId = ref('')
//
const audioPlayStatus = reactive({
isPlaying: false, //
playMsgId: '' //id,
})
//
const startplayAudio = (msgBody) => {
const armRec = new BenzAMRRecorder()
const src = msgBody.url
audioPlayStatus.playMsgId = msgBody.id
console.log('>>>>>开始播放音频', msgBody.url)
//
armRec.initWithUrl(src).then(() => {
if (!audioPlayStatus.isPlaying) {
armRec.play()
}
})
//
armRec.onPlay(() => {
audioPlayStatus.isPlaying = true
audioPlayStatus.playMsgId = msgBody.id
})
//
armRec.onStop(() => {
audioPlayStatus.isPlaying = false
audioPlayStatus.playMsgId = ''
})
}
</script>
<template>
<div>
<div
class="messageList_box"
v-for="(msgBody, index) in messageData"
:key="msgBody.id"
:data-mid="msgBody.id"
>
<!-- 普通消息气泡 -->
<div
v-if="!msgBody.isRecall && msgBody.type !== ALL_MESSAGE_TYPE.INFORM"
class="message_box_item"
:style="{
flexDirection: isMyself(msgBody) ? 'row-reverse' : 'row'
}"
>
<div class="message_item_time">
{{ handleMsgTimeShow(msgBody.time, index) || '' }}
</div>
<el-avatar
class="message_item_avator"
:src="
isMyself(msgBody)
? loginUserInfo.avatarurl
: otherUserInfo(msgBody.from).avatarurl || defaultAvatar
"
/>
<!-- 普通消息内容 -->
<div class="message_box_card">
<span v-show="!isMyself(msgBody)" class="message_box_nickname">{{
handleNickName(msgBody.from)
}}</span>
<el-dropdown
class="message_box_content"
:class="[
isMyself(msgBody) ? 'message_box_content_mine' : 'message_box_content_other',
clickQuoteMsgId === msgBody.id && 'quote_msg_avtive'
]"
trigger="contextmenu"
placement="bottom-end"
>
<!-- 文本类型消息 -->
<p
style="padding: 10px; line-height: 20px"
v-if="msgBody.type === ALL_MESSAGE_TYPE.TEXT"
>
<template v-if="!isLink(msgBody.msg)">
{{ msgBody.msg }}
<!-- 已编辑 -->
<sup
style="font-size: 7px; color: #707784"
v-show="msgBody?.modifiedInfo?.operationCount"
>已编辑</sup
>
</template>
<template v-else> <span v-html="paseLink(msgBody.msg).msg"> </span></template>
</p>
<!-- 图片类型消息 -->
<!-- <div> -->
<el-image
v-if="msgBody.type === ALL_MESSAGE_TYPE.IMAGE"
style="border-radius: 5px"
:src="msgBody.thumb"
:preview-src-list="[msgBody.url]"
:initial-index="1"
fit="cover"
/>
<!-- </div> -->
<!-- 语音类型消息 -->
<div
:class="[
'message_box_content_audio',
isMyself(msgBody)
? 'message_box_content_audio_mine'
: 'message_box_content_audio_other'
]"
v-if="msgBody.type === ALL_MESSAGE_TYPE.AUDIO"
@click="startplayAudio(msgBody)"
:style="`width:${msgBody.length * 10}px`"
>
<span class="audio_length_text"> {{ msgBody.length }} </span>
<div
:class="[
isMyself(msgBody) ? 'play_audio_icon_mine' : 'play_audio_icon_other',
audioPlayStatus.playMsgId === msgBody.id && 'start_play_audio'
]"
style="background-size: 100% 100%"
></div>
</div>
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.LOCAL">
<p style="padding: 10px">[暂不支持位置消息展示]</p>
</div>
<!-- 文件类型消息 -->
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.FILE" class="message_box_content_file">
<div class="file_text_box">
<div class="file_name">
{{ msgBody.filename }}
</div>
<div class="file_size">
{{ fileSizeFormat(msgBody.file_length) }}
</div>
<a class="file_download" :href="msgBody.url" download>点击下载</a>
</div>
<span class="iconfont icon-wenjian"></span>
</div>
<!-- 自定义类型消息 -->
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.CUSTOM" class="message_box_content_custom">
<template v-if="msgBody.customEvent && CUSTOM_TYPE[msgBody.customEvent]">
<div class="user_card">
<div class="user_card_main">
<!-- 头像 -->
<el-avatar
shape="circle"
:size="50"
:src="
(msgBody.customExts && msgBody.customExts.avatarurl) ||
msgBody.customExts.avatar ||
defaultAvatar
"
fit="cover"
/>
<!-- 昵称 -->
<span class="nickname">{{
(msgBody.customExts && msgBody.customExts.nickname) || msgBody.customExts.uid
}}</span>
</div>
<el-divider style="margin: 5px 0; border-top: 1px solid black" />
<p style="font-size: 8px">个人名片</p>
</div>
</template>
</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>
<!-- 引用消息展示框 -->
<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 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>
<ReportMessage ref="reportMessage" />
<ModifyMessage ref="modifyMessageRef" />
</div>
</template>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,237 @@
<script setup>
import { ref } from 'vue'
import BenzAMRRecorder from 'benz-amr-recorder'
import { ElNotification } from 'element-plus'
const voice = ref({
interval: null, //
type: 0, // 0 1 2 3
length: 0, //
src: null //
})
const amrRec = ref(null)
const timer = ref({
interval: null,
tim: 60
}) //
const showCountDown = ref(false)
const benginTimer = () => {
showCountDown.value = false
timer.value.interval = setInterval(() => {
if (timer.value.tim === 0) {
clearInterval(timer.value.interval)
clearInterval(voice.value.interval)
//
recordOver()
return
}
else if (false) {
//
timer.value.tim = 60
return
} else {
timer.value.tim--
}
if (timer.value.tim < 11) {
showCountDown.value = true
}
}, 1000)
}
const startRecord = () => {
if (voice.value.type === 0) {
amrRec.value = new BenzAMRRecorder()
console.log(amrRec)
amrRec.value
.initWithRecord()
.then(() => {
amrRec.value.startRecord() //
voice.value.type = 1
benginTimer()
//
voice.value.interval = setInterval(() => {
voice.value.length++
}, 1000)
})
.catch(e => {
console.log(e)
voice.value.type = 0
ElNotification({
title: '',
message: '录音失败,请检查相关权限和设备',
type: 'error',
})
// $Toast("");
})
}
}
const emit = defineEmits(['sendAudioMessages'])
const recordOver = () => {
amrRec.value
.finishRecord()
.then(() => {
if (voice.value.length <= 1) {
clearInterval(timer.value.interval)
clearInterval(voice.value.interval)
initVocie()
//
amrRec.value.cancelRecord()
ElNotification({
title: '',
message: '录音时间较短',
type: 'warning',
})
} else {
voice.value.length = Math.ceil(amrRec.value.getDuration())
//
voice.value.src = amrRec.value.getBlob()
emit('sendAudioMessages', {
src: voice.value.src,
length: voice.value.length > 60 ? 60 : voice.value.length //
})
clearInterval(timer.value.interval)
clearInterval(voice.value.interval)
initVocie()
}
})
.catch(() => {
ElNotification({
title: '',
message: '录音失败,请检查相关权限',
type: 'error',
})
})
}
const initVocie = () => {
voice.value.interval = null
voice.value.length = 0
voice.value.type = 0
timer.value.tim = 60
showCountDown.value = false
timer.value.interval = null
}
// const filterRecordVoicTime = (len) => {
// let min = Math.floor(len / 60),
// sec = len % 60;
// ("1:30");
// return min + ":" + (sec < 10 ? "0" + sec : sec);
// }
// const cancelRecord = (e) => {
// amrRec.value.cancelRecord();
// clearInterval(timer.value.interval);
// clearInterval(voice.value.interval);
// initVocie();
// ElNotification({
// title: '',
// message: '',
// type: 'success',
// });
// }
const closeDialog = () => {
console.log('关闭子组件的定时器')
clearInterval(timer.value.interval)
clearInterval(voice.value.interval)
initVocie()
}
defineExpose({ closeDialog })
</script>
<template>
<div>
<div class="collect_box">
<div v-show="!voice.type" class="start">
<span class="title">单击开始录音最长可录制60秒</span>
<svg @click="startRecord()" width="58" height="58" viewBox="0 0 58 58" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M29 57.2363C44.6152 57.2363 57.3887 44.4629 57.3887 28.8477C57.3887 13.2324 44.6152 0.458984 29 0.458984C13.3848 0.458984 0.611328 13.2324 0.611328 28.8477C0.611328 44.4629 13.3848 57.2363 29 57.2363ZM29 55.8008C14.1465 55.8008 2.04688 43.7012 2.04688 28.8477C2.04688 13.9648 14.1465 1.89453 29 1.89453C43.8828 1.89453 55.9531 13.9648 55.9531 28.8477C55.9531 43.7012 43.8828 55.8008 29 55.8008ZM29 34.5312C31.9883 34.5312 33.8926 32.2168 33.8926 29.1699V16.1621C33.8926 13.1152 31.9883 10.8008 29 10.8008C26.0117 10.8008 24.1074 13.1152 24.1074 16.1621V29.1699C24.1074 32.2168 26.0117 34.5312 29 34.5312ZM22.1445 46.9531H35.8848C36.2949 46.9531 36.6172 46.6309 36.6172 46.2207C36.6172 45.8105 36.2656 45.4883 35.8848 45.4883H29.7324V40.0098C35.5332 39.6875 39.6055 35.4688 39.6055 29.9316V27.0312C39.6055 26.6211 39.2832 26.2988 38.873 26.2988C38.4922 26.2988 38.1406 26.6211 38.1406 27.0312V29.9316C38.1406 35.2344 34.7422 38.5742 29 38.5742C23.2578 38.5742 19.918 35.2344 19.918 29.9316V27.0312C19.918 26.6211 19.5664 26.2988 19.1855 26.2988C18.7754 26.2988 18.4238 26.6211 18.4238 27.0312V29.9316C18.4238 35.4688 22.4668 39.6875 28.2676 40.0098V45.4883H22.1445C21.7344 45.4883 21.4121 45.8105 21.4121 46.2207C21.4121 46.6309 21.7344 46.9531 22.1445 46.9531Z"
fill="#04D0A4" />
</svg>
<span>{{ timer.tim }}</span>
</div>
<div @click="recordOver()" v-show="voice.type && !showCountDown" class="send">
<span class="title">再次单击发送点空白处取消</span>
<svg width="58" height="58" viewBox="0 0 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M29 57.2363C44.6152 57.2363 57.3887 44.4629 57.3887 28.8477C57.3887 13.2324 44.6152 0.458984 29 0.458984C13.3848 0.458984 0.611328 13.2324 0.611328 28.8477C0.611328 44.4629 13.3848 57.2363 29 57.2363ZM29 55.8008C14.1465 55.8008 2.04688 43.7012 2.04688 28.8477C2.04688 13.9648 14.1465 1.89453 29 1.89453C43.8828 1.89453 55.9531 13.9648 55.9531 28.8477C55.9531 43.7012 43.8828 55.8008 29 55.8008ZM20.8848 39.2773H37.1152C38.5508 39.2773 39.4297 38.3691 39.4297 36.9629V20.7324C39.4297 19.2969 38.5508 18.418 37.1152 18.418H20.8848C19.4785 18.418 18.5703 19.2969 18.5703 20.7324V36.9629C18.5703 38.3691 19.4785 39.2773 20.8848 39.2773Z"
fill="#7F7F7F" />
</svg>
<span>{{ `${voice.length}` }}</span>
</div>
<div v-show="showCountDown" class="send">
<span class="title">{{ timer.tim }}秒后自动发送</span>
<svg width="58" height="58" viewBox="0 0 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M29 57.2363C44.6152 57.2363 57.3887 44.4629 57.3887 28.8477C57.3887 13.2324 44.6152 0.458984 29 0.458984C13.3848 0.458984 0.611328 13.2324 0.611328 28.8477C0.611328 44.4629 13.3848 57.2363 29 57.2363ZM29 55.8008C14.1465 55.8008 2.04688 43.7012 2.04688 28.8477C2.04688 13.9648 14.1465 1.89453 29 1.89453C43.8828 1.89453 55.9531 13.9648 55.9531 28.8477C55.9531 43.7012 43.8828 55.8008 29 55.8008ZM20.8848 39.2773H37.1152C38.5508 39.2773 39.4297 38.3691 39.4297 36.9629V20.7324C39.4297 19.2969 38.5508 18.418 37.1152 18.418H20.8848C19.4785 18.418 18.5703 19.2969 18.5703 20.7324V36.9629C18.5703 38.3691 19.4785 39.2773 20.8848 39.2773Z"
fill="#FF4D4F" />
</svg>
<span>{{ `${voice.length}` }}</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.collect_box {
display: flex;
flex-direction: column;
align-items: space-around;
.start,
.send {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
span {
margin-bottom: 22px;
}
svg {
margin-bottom: 15px;
}
}
.title{
margin-top: 10px;
font-size: 14px;
}
}
.collect_box>h3 {
user-select: none;
margin-bottom: .03rem;
}
.recordTime {
margin: .09rem 0;
}
.collect_btn {
user-select: none;
display: flex;
justify-content: center;
align-items: center;
width: .8rem;
height: .8rem;
border: .03rem solid #474747;
border-radius: 50%;
}
.collect_btn>img {
pointer-events: none;
/* 禁止长按图片保存 */
width: .6rem;
height: .6rem;
}
.reacordingStyle {
width: .5rem;
height: .5rem;
background: red;
border-radius: 50%;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<el-dialog v-model="dialogVisible" title="编辑消息" width="30%">
<el-input
class="modifymessage_input"
v-model="editMessageContent.msg"
:autosize="{ minRows: 2, maxRows: 4 }"
type="textarea"
/>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false" :icon="Close"
>取消</el-button
>
<el-button
type="primary"
:loading="loading"
@click="saveEditedMessage"
:icon="Check"
>
{{ loading ? '更新中' : '保存' }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, nextTick } from 'vue'
import { useStore } from 'vuex'
import { messageType } from '@/constant'
import { ElMessage } from 'element-plus'
import { Check, Close } from '@element-plus/icons-vue'
const { CHAT_TYPE } = messageType
const store = useStore()
const dialogVisible = ref(false)
const editMessageContent = reactive({
msg: '',
to: '',
id: '',
chatType: CHAT_TYPE.SINGLE
})
const loading = ref(false)
const saveEditedMessage = async () => {
loading.value = true
if (!editMessageContent.msg) {
ElMessage.warning('消息内容不能为空')
}
try {
await store.dispatch('modifyMessage', { ...editMessageContent })
} catch (error) {
if (error?.type === 50) {
ElMessage({
type: 'error',
message: '该消息可编辑次数已达上限',
center: true
})
} else {
ElMessage({
type: 'error',
message: '消息编辑失败请稍后重试',
center: true
})
}
} finally {
initModifyMessage()
loading.value = false
dialogVisible.value = false
}
}
const initModifyMessage = (msgBody) => {
//initModifyMessage true
dialogVisible.value = true
nextTick(() => {
if (msgBody) {
const { id, msg, to, chatType } = msgBody
// console.log('>>>>>>', id, msg, to, chatType)
editMessageContent.msg = msg
editMessageContent.to = to
editMessageContent.id = id
editMessageContent.chatType = chatType
} else {
editMessageContent.msg = ''
editMessageContent.to = ''
editMessageContent.id = ''
editMessageContent.chatType = CHAT_TYPE.SINGLE
}
})
}
defineExpose({
initModifyMessage
})
</script>
<style lang="scss" scoped>
.modify_input_container {
width: 100%;
box-sizing: border-box;
padding: 10px 15px;
}
.modify_input_btn_container {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
margin-top: 5px;
}
.modify_input_btn {
width: 15px;
height: 15px;
cursor: pointer;
}
.modify_input_btn:hover {
transform: scale(1.2);
}
:deep(.el-textarea__inner) {
border-radius: 5px;
resize: none;
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<div v-if="isShowQuoteMsgBox" class="message_quote_container">
<span> {{ msgQuote.msgSender || '' }}</span>
<div class="quote_from_content">
<template v-if="msgQuote.msgType === ALL_MESSAGE_TYPE.IMAGE">
<el-image
v-show="quoteImageUrl.imageUrl"
style="width: 35px; height: 35px"
:src="quoteImageUrl.thumb || quoteImageUrl.imageUrl"
:preview-src-list="[quoteImageUrl.imageUrl]"
/>
<p v-show="!quoteImageUrl">{{ msgQuote.msgPreview }}</p>
</template>
<template v-else>
<p class="quote_text" :title="msgQuote.msg">
{{ msgQuote.msgPreview || '' }}
</p>
</template>
</div>
<div class="quote_close_icon" @click="clearQuoteContent">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-ea893728="">
<path
fill="currentColor"
d="m466.752 512-90.496-90.496a32 32 0 0 1 45.248-45.248L512 466.752l90.496-90.496a32 32 0 1 1 45.248 45.248L557.248 512l90.496 90.496a32 32 0 1 1-45.248 45.248L512 557.248l-90.496 90.496a32 32 0 0 1-45.248-45.248L466.752 512z"
/>
<path
fill="currentColor"
d="M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768zm0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896z"
/>
</svg>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import messageType from '@/constant/messageType'
const { ALL_MESSAGE_TYPE, SESSION_MESSAGE_TYPE, CHAT_TYPE } = messageType
/* stores */
console.log(store.state)
const loginUserInfo = computed(() => {
return store.state.loginUserInfo
})
//
const isShowQuoteMsgBox = ref(false)
//
let msgQuote = reactive({
msgID: '', //id
msgPreview: '', //
msgSender: '', //
msgType: '' //
})
//
const quoteImageUrl = reactive({
thumb: '',
imageUrl: ''
})
//
const extractMessageBodyValue = (sourceMsg) => {
const { type, msg: msgContent, id: mid, from } = sourceMsg
msgQuote.msgID = mid
msgQuote.msgType = type
if (from === loginUserInfo.value.hxId) {
msgQuote.msgSender = getLoginNickNameById()
} else {
//
const groupId = sourceMsg.chatType === CHAT_TYPE.GROUP ? sourceMsg.to : ''
msgQuote.msgSender = getTheGroupNickNameById(groupId, from)
}
if (type === ALL_MESSAGE_TYPE.IMAGE) {
quoteImageUrl.thumb = sourceMsg.thumb
quoteImageUrl.imageUrl = sourceMsg.url
}
if (type === ALL_MESSAGE_TYPE.TEXT) {
msgQuote.msgPreview = msgContent
} else {
msgQuote.msgPreview = SESSION_MESSAGE_TYPE[type]
}
}
const setQuoteContent = (msg) => {
msg && extractMessageBodyValue(msg)
isShowQuoteMsgBox.value = true
}
//
const clearQuoteContent = () => {
msgQuote.msgID = ''
msgQuote.msgPreview = ''
msgQuote.msgType = ''
msgQuote.msgSender = ''
quoteImageUrl.imageUrl = ''
quoteImageUrl.thumb = ''
isShowQuoteMsgBox.value = false
}
defineExpose({
isShowQuoteMsgBox,
msgQuote,
setQuoteContent,
clearQuoteContent
})
</script>
<style lang="scss" scoped>
.message_quote_container {
position: absolute;
left: 15px;
bottom: 10px;
min-width: 20%;
max-width: 45%;
height: 20%;
border-radius: 3px;
background: #e7e7e690;
color: #8e8e8e;
padding: 5px;
display: flex;
flex-direction: row;
align-items: center;
font-size: 13px;
}
.quote_file_box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
}
.quote_close_icon {
position: absolute;
top: 0;
bottom: 0;
right: -8%;
margin: auto;
width: 15px;
height: 15px;
cursor: pointer;
}
.quote_close_icon:hover {
transform: scale(1.1);
}
.quote_text {
width: 100%;
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 17px;
}
.quote_file_icon {
width: 15px;
height: 15px;
}
</style>

View File

@ -0,0 +1,68 @@
<script setup>
import { ref } from 'vue'
// import fileSizeFormat from '@/utils/fileSizeFormat'
const emits = defineEmits(['sendImagesMessage'])
let fileObj = null
const imgPaths = ref('')
const imgName = ref('')
const imgSize = ref('')
const dialogTableVisible = ref(false)
const showPreviewImgModal = (imgObj) => {
imgPaths.value = imgObj.tempFilePath
imgName.value = imgObj.imgFile.name
imgSize.value = imgObj.imgFile.size
fileObj = imgObj.imgFile
dialogTableVisible.value = true
}
const sendTheImg = () => {
emits('sendImagesMessage', 'other', fileObj)
dialogTableVisible.value = false
}
defineExpose({
showPreviewImgModal
})
</script>
<template>
<el-dialog v-model="dialogTableVisible" title="发送图片" width="300px">
<el-image class="img_box" :src="imgPaths">
<template #placeholder>
<div class="image-slot">Loading<span class="dot">...</span></div>
</template>
</el-image>
<div class="img_infos">
<span class="img_name">{{ imgName }}</span>
<!-- <span class="img_size">{{ fileSizeFormat(imgSize) }}</span>-->
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogTableVisible = false">取消</el-button>
<el-button type="primary" @click="sendTheImg"> 发送 </el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.img_box {
max-width: 500px;
}
.img_infos {
margin: 7px;
line-height: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.img_name {
font-size: 17px;
font-weight: bold;
}
.img_size {
font-size: 13px;
font-weight: 400;
}
}
</style>

View File

@ -0,0 +1,145 @@
<script setup>
import { ref, reactive, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { EaseChatClient } from '@/IM/initwebsdk'
const ReportTypeOptions = [
{
key: '1',
value: '涉政'
},
{
key: '2',
value: '涉黄'
},
{
key: '3',
value: '广告'
},
{
key: '4',
value: '辱骂'
},
{
key: '5',
value: '暴恐'
},
{
key: '6',
value: '违禁'
},
{
key: '7',
value: '其他'
}
]
const dialogVisible = ref(false)
const reportMessageForm = reactive({
mid: '',
reportType: '涉政',
reportReason: ''
})
const rules = reactive({
reportReason: [
{ required: true, message: '请描述举报原因!', trigger: 'blur' },
]
})
const alertReportMsgModal = (msgBody) => {
const msg = Object.assign({}, msgBody)
console.log('>>>>调用弹出', msgBody)
if (msg.id) {
reportMessageForm.mid = msg.id
console.log('reportMessageForm.mid', reportMessageForm.mid)
dialogVisible.value = true
}
}
const reportMsgForm = ref(null)
const confimReportMessage = (formEl) => {
console.log('formEl', formEl)
if (!formEl) return
formEl.validate(async (valid) => {
if (valid) {
console.log('submit!')
try {
console.log('confimReportMessage', reportMessageForm.mid)
const params = {
reportType: reportMessageForm.reportType, //
reportReason: reportMessageForm.reportReason, //
messageId: reportMessageForm.mid.toString() // ID
}
console.log('>>>>>>要传入的举报参数', params)
await EaseChatClient.reportMessage({ ...params })
cannelReport(formEl)
ElMessage({
type: 'success',
message: '已收到您的举报申请!',
center: true
})
} catch (error) {
console.log('举报error', error)
ElMessage({
type: 'error',
message: '举报失败!',
center: true
})
}
} else {
return false
}
})
}
const cannelReport = (formEl) => {
if (!formEl) return
formEl.resetFields()
dialogVisible.value = false
}
defineExpose({
alertReportMsgModal
})
</script>
<template>
<el-dialog v-model="dialogVisible" title="消息举报" width="500px" :show-close="false" :close-on-press-escape="false"
:close-on-click-modal="false">
<el-form ref="reportMsgForm" :model="reportMessageForm" :rules="rules" label-position="top" label-width="100px">
<el-form-item label="举报类别:">
<el-select v-model="reportMessageForm.reportType">
<el-option v-for="item in ReportTypeOptions" :key="item.key" :label="item.value"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="举报原因:" prop="reportReason">
<el-input v-model="reportMessageForm.reportReason" maxlength="150" placeholder="请描述举报原因..."
show-word-limit type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="cannelReport(reportMsgForm)">取消</el-button>
<el-button type="primary" @click="confimReportMessage(reportMsgForm)">
确认
</el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.dialog-footer button:first-child {
margin-right: 10px;
}
:deep(.el-textarea__inner) {
border-radius: 5px;
resize: none;
}
:deep(.el-input) {
height: 40px;
}
</style>

View File

@ -0,0 +1,129 @@
.app_container {
height: 100%;
border-left: 1px solid #e6e6e6;
}
.chat_message_header {
position: relative;
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
height: 61px;
background: #f9f9f9;
border-radius: 0 3px 0 0;
border-bottom: 1px solid #e6e6e6;
.chat_user_box {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 20px;
max-width: 80%;
.chat_user_name {
font-family: 'PingFang SC';
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-style: normal;
font-weight: 400;
font-size: 17px;
line-height: 20px;
letter-spacing: 0.3px;
color: #333333;
}
}
.more {
display: flex;
width: 35px;
height: 100%;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
}
}
.easeim_safe_tips {
position: relative;
padding: 12px 20px;
background-color: #fff4e6;
color: #ff8c39;
line-height: 18px;
font-family: PingFang SC;
font-style: normal;
font-weight: 400;
text-align: justify;
font-size: 12px;
border: none;
.easeim_close_tips {
position: absolute;
right: 10px;
top: 10px;
}
}
.chat_message_main {
padding: 0;
background: #f9f9f9;
.main_container {
padding: 0 20px;
height: 100%;
// overflow-y: scroll;
.chat_message_tips {
margin-top: 5px;
width: 100%;
height: 30px;
text-align: center;
line-height: 30px;
.load_more_msg {
width: 200px;
height: 30px;
border-radius: 20px;
margin: 0 auto;
background: rgba(114, 112, 112, 0.143);
font-size: 13px;
letter-spacing: 0.5px;
// box-shadow: 1px 1px 1px 1px rgba(128, 128, 128, 0.193);
}
}
}
}
.chat_message_inputbar {
position: relative;
width: 100%;
height: 25%;
padding: 0;
background-color: #f9f9f9;
border-radius: 0 0 3px 0;
}
::v-deep .el-drawer {
margin-top: 60px;
width: 150px;
height: calc(100% - 60px);
border-radius: 5px 0 0 5px;
.el-drawer__header {
margin-bottom: 0;
padding-top: 0;
}
.el-drawer__body {
padding: 0;
// padding-left: 16px;
}
}

View File

@ -1,11 +1,182 @@
<script setup lang="ts">
import UserStatus from '@/components/UserStatus/index.vue'
import MessageList from './components/messageList/index.vue'
import InputBox from './components/inputBox/index.vue'
/* header 操作 */
const drawer = ref(false) //
const handleDrawer = () => {
drawer.value = !drawer.value
}
//
const delTheFriend = () => {
console.log(nowPickInfo.value)
if (nowPickInfo.value?.id) {
const targetId = nowPickInfo.value.id
// EaseChatClient.deleteContact(targetId)
// ElMessage({ type: 'success', center: true, message: '~' })
}
}
//
const nowPickInfo = ref({
id: '1',
chatType: 1,
userInfo: {
nickname: '好友1',
userStatus: '1'
},
groupDetail: {
name: '',
affiliations_count: '',
custom: ''
}
})
//
const groupDetail = computed(() => {
return nowPickInfo.value.groupDetail
})
//id
const messageData = computed(() => [
{
type: 'text'
}
])
/* 消息相关 */
const loadingHistoryMsg = ref(false) //
const isMoreHistoryMsg = ref(true) //
const notScrollBottom = ref(false) //
//
const fechHistoryMessage = (loadType) => {
console.log(loadType)
console.log('加载更多')
}
//
const scrollMessageList = (direction) => {
console.log(direction)
}
//
const inputBox = ref(null)
const reEditMessage = (msg) => (inputBox.value.textContent = msg)
//
const messageQuote = (msg) => inputBox.value.handleQuoteMessage(msg)
</script>
<template>
<el-container class="app_container">
<el-header class="chat_message_header">
<template v-if="nowPickInfo.chatType === 1">
<div v-if="nowPickInfo.userInfo" class="chat_user_box">
<span class="chat_user_name"> {{ nowPickInfo.userInfo.nickname || nowPickInfo.id }}</span>
<UserStatus :userStatus="nowPickInfo.userInfo.userStatus" />
</div>
<div v-else> {{ nowPickInfo.id }}<span style="font-size: 10px">(非好友)</span> </div>
</template>
<template v-if="nowPickInfo.chatType === 2">
<div v-if="nowPickInfo.groupDetail" class="chat_user_box">
<span class="chat_user_name">
{{ groupDetail.name || '' }}
{{ `(${groupDetail?.affiliations_count || ''})` }}
</span>
</div>
<div v-else class="chat_user_box">
<span class="chat_user_name">
{{ groupDetail.name || nowPickInfo.id }}
</span>
</div>
</template>
<!-- 群组展示抽屉 -->
<span
class="more"
v-if="nowPickInfo.groupDetail && nowPickInfo.chatType === 2"
@click="handleDrawer"
>
<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>
</span>
<!-- 单人展示删除拉黑 -->
<span class="more" v-if="nowPickInfo.chatType === 1">
<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-main class="chat_message_main">
<el-scrollbar class="main_container" ref="messageContainer">
<div class="innerRef">
<div v-show="isMoreHistoryMsg" class="chat_message_tips">
<div
v-show="messageData?.length && messageData[0].type !== 'inform'"
class="load_more_msg"
>
<el-link
v-show="!loadingHistoryMsg"
:disabled="!isMoreHistoryMsg"
:underline="false"
@click="fechHistoryMessage('loadFirst')"
>
加载更多
</el-link>
<el-link v-show="loadingHistoryMsg" disabled>消息加载中...</el-link>
</div>
</div>
<MessageList
:nowPickInfo="nowPickInfo"
:messageData="messageData"
@scrollMessageList="scrollMessageList"
@reEditMessage="reEditMessage"
@messageQuote="messageQuote"
/>
</div>
</el-scrollbar>
</el-main>
<el-footer class="chat_message_inputbar">
<InputBox ref="inputBox" :nowPickInfo="nowPickInfo" />
</el-footer>
<el-drawer
v-if="nowPickInfo.chatType === 2"
v-model="drawer"
:show-close="false"
:close-on-click-modal="true"
direction="rtl"
:modal="true"
size="280px"
>
<GroupsDetails
:nowGroupId="nowPickInfo.id"
:groupDetail="groupDetail"
@handleDrawer="handleDrawer"
/>
</el-drawer>
</el-container>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>