Pre Merge pull request !451 from 芋道源码/feature/im

pull/451/MERGE
芋道源码 2024-10-27 08:42:33 +00:00 committed by Gitee
commit 12046989b9
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
60 changed files with 5230 additions and 8 deletions

View File

@ -94,7 +94,7 @@
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": false,
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"cSpell.words": [

View File

@ -51,6 +51,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",
"markdown-it": "^14.1.0",
"markmap-common": "^0.16.0",
@ -68,6 +69,7 @@
"url": "^0.11.3",
"video.js": "^7.21.5",
"vue": "3.5.12",
"vue-at": "3.0.0-alpha.2",
"vue-dompurify-html": "^4.1.4",
"vue-i18n": "9.10.2",
"vue-router": "^4.3.0",

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View File

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

View File

@ -0,0 +1,3 @@
import SearchInput from './src/SearchInput.vue'
export { SearchInput }

View File

@ -0,0 +1,173 @@
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
//value
const inputValue = ref('')
//
const isShowResultContent = ref(false)
//
const querySearch = () => {
console.log('>>>>>>>>触发搜索')
}
</script>
<template>
<div class="search_box" ref="searchBox">
<div>
<el-input
v-model.trim="inputValue"
placeholder="搜索"
@focus="isShowResultContent = true"
@clear="isShowResultContent = false"
@input="querySearch"
:prefix-icon="Search"
clearable
/>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.el-input__wrapper) {
box-shadow: none;
}
.search_box {
width: 100%;
height: 60px;
background: #f8f8f8;
padding: 14px 20px;
box-sizing: border-box;
}
.resultContent {
position: absolute;
top: 58px;
left: 0;
width: 100%;
height: calc(100% - 60px);
background-color: #ededed;
z-index: 888;
overflow-y: auto;
.search_history {
.search_history_item {
width: 100%;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
letter-spacing: 0.669643px;
color: #000000;
li {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
padding: 0 15px;
background: #fff;
margin: 1px 0;
height: 32px;
transition: all 0.5s;
cursor: pointer;
&:hover {
background: #e5e5e5;
}
}
}
}
.title {
height: 32px;
line-height: 32px;
padding: 0 15px;
background: #f2f2f2;
font-weight: 400;
font-size: 12px;
letter-spacing: 0.342857px;
color: #333333;
}
.search_history_title {
display: flex;
flex-direction: row;
justify-content: space-between;
.clear_search_history:hover {
color: #00a0fb;
cursor: pointer;
}
}
.search_result_item {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
height: 66px;
background: #fff;
// padding: 0 14px;
cursor: pointer;
.item_left {
padding: 0;
margin-right: 11px;
margin-left: 14px;
}
.item_main {
// width: 25%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-around;
height: 40px;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 500;
font-size: 14px;
.name {
max-width: 100px;
height: 17px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last_msg_body {
max-width: 100px;
height: 17px;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
letter-spacing: 0.3px;
color: #a3a3a3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.time {
position: absolute;
right: 15px;
top: 13px;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.25px;
color: #a3a3a3;
}
}
}
</style>

View File

@ -0,0 +1,3 @@
import UserStatus from './src/UserStatus.vue'
export { UserStatus }

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>

View File

@ -0,0 +1,3 @@
import Welcome from './src/Welcome.vue'
export { Welcome }

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import tang from '@/assets/imgs/im/welcome/Group 78@3x.png'
import maskGroup from '@/assets/imgs/im/welcome/Mask_group2.png'
</script>
<template>
<div class="app_contanier">
<div class="welcome_box">
<img class="tang" :src="tang" alt="" />
<h1 class="welcome_box_title">欢迎体验 芋道 即时通讯</h1>
<p class="welcome_box_text"
>包含单聊群聊添加好友创建群组等功能更多其他功能等你发现快去试试吧
</p>
</div>
<img class="maskGroup" :src="maskGroup" alt="" />
</div>
</template>
<style scoped>
.app_contanier {
position: absolute;
right: 0;
bottom: 0;
z-index: -1;
width: 100%;
height: 100%;
background-size: 100% 100%;
border-radius: 0 5px 5px 0;
overflow: hidden;
}
.welcome_box {
position: relative;
margin-top: 140px;
margin-left: 100px;
width: 637px;
height: 400px;
}
.tang {
position: absolute;
left: -30px;
top: 0;
width: 32px;
height: 32px;
}
.welcome_box_title {
font-family: 'PingFang SC', serif;
font-style: normal;
font-weight: 500;
font-size: 24px;
line-height: 34px;
letter-spacing: 1px;
color: #333333;
}
.welcome_box_text {
margin-top: 12px;
font-family: 'PingFang SC', serif;
font-style: normal;
font-weight: 300;
font-size: 16px;
line-height: 22px;
letter-spacing: 1.5px;
color: #a3a3a3;
}
.maskGroup {
position: absolute;
right: 0;
top: 25%;
}
</style>

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

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

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/im/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,80 @@
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 ALL_MESSAGE_TYPE = {
TEXT: 101,
IMAGE: 102,
AUDIO: 103,
VIDEO: 104,
FILE: 105,
AT_TEXT: 106,
MERGE: 107,
CARD: 108,
LOCATION: 109,
CUSTOM: 110,
REVOKE_RECEIPT: 111,
C2C_RECEIPT: 112,
TYPING: 113,
QUOTE: 114,
FACE: 115,
ADVANCED_REVOKE: 118,
FRIEND_ADDED: 1201,
OA_NOTIFICATION: 1400,
GROUP_CREATED: 1501,
GROUP_INFO_CHANGED: 1502,
MEMBER_QUIT: 1504,
GROUP_OWNER_CHANGED: 1507,
MEMBER_KICKED: 1508,
MEMBER_INVITED: 1509,
MEMBER_ENTER: 1510,
GROUP_DISMISSED: 1511,
GROUP_MEMBER_MUTED: 1512,
GROUP_MEMBER_CANCEL_MUTED: 1513,
GROUP_MUTED: 1514,
GROUP_CANCEL_MUTED: 1515,
GROUP_ANNOUNCEMENT_UPDATED: 1519,
GROUP_NAME_UPDATED: 1520,
BURN_CHANGE: 1701,
REVOKE: 2101
}
const CHAT_TYPE = {
SINGLE: 1,
GROUP: 3,
NOTIFICATION: 4
}
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

@ -0,0 +1,3 @@
import ImChat from './src/ImChat.vue'
export { ImChat }

View File

@ -0,0 +1,110 @@
<script setup lang="ts">
import { Dialog } from '@/components/Dialog'
import { shallowRef, defineAsyncComponent, DefineComponent } from 'vue'
import { getAccessToken } from '@/utils/auth'
import { useWebSocket } from '@vueuse/core'
import { chatStore, ImMessageWebSocket } from '@/store/modules/chatStore'
import * as MessageApi from '@/api/im/message'
// imMessageStore addMessage
const { addMessage, setLocalMaxSeq, loadMessages } = chatStore()
//
const IMComponent = defineAsyncComponent(() => import('@/views/im/index.vue'))
const dialogVisible = shallowRef(false)
const currentComponent = shallowRef<DefineComponent | null>(null)
// IM
function openDialog() {
dialogVisible.value = true
currentComponent.value = IMComponent // IM
}
// WebSocket
function initWebSocket() {
const server =
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getAccessToken()
const { data } = useWebSocket(server, {
autoReconnect: true,
heartbeat: true,
onMessage(ws, event) {
// ws open
if (ws.readyState !== WebSocket.OPEN) return
try {
// 1.
if (data.value === 'pong') {
console.log('websocket 收到心跳包')
return
}
console.log('websocket 收到消息', event.data)
// 2.1 type
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
const content = JSON.parse(jsonMessage.content)
if (!type) {
// message.error('' + data.value)
return
}
// 2.2 demo-message-receive
if (type === 'im-message-receive') {
const message: ImMessageWebSocket = {
id: content.id,
conversationType: content.conversationType,
senderId: content.senderId,
senderNickname: content.senderNickname,
senderAvatar: content.senderAvatar,
receiverId: content.receiverId,
contentType: content.contentType,
content: content.content,
sendTime: content.sendTime,
sequence: content.sequence
}
// pina imMessageStore
addMessage(message)
setLocalMaxSeq(content.sequence)
return
}
} catch (error) {
console.error(error)
}
}
})
console.log('websocket 初始化成功')
}
//
const pullParams = reactive({
sequence: 0, //
size: 10
})
async function loadImMessages() {
//
const list: ImMessageWebSocket[] = await MessageApi.pullMessageList(pullParams)
loadMessages(list)
}
// ========== =========
onMounted(() => {
// websocket
initWebSocket()
// loadMessages
loadImMessages()
})
</script>
<template>
<div class="custom-hover" v-bind="$attrs" @click="openDialog">
<ElBadge>
<Icon :size="18" class="cursor-pointer" icon="ep:chat-round" />
</ElBadge>
</div>
<Dialog v-model="dialogVisible" width="90%" top="10vh">
<component :is="currentComponent" />
<!-- 使用动态组件 -->
</Dialog>
</template>

View File

@ -1,6 +1,7 @@
<script lang="tsx">
import { defineComponent, computed } from 'vue'
import { Message } from '@/layout/components//Message'
import { Message } from '@/layout/components/Message'
import { ImChat } from '@/layout/components/ImChat'
import { Collapse } from '@/layout/components/Collapse'
import { UserInfo } from '@/layout/components/UserInfo'
import { Screenfull } from '@/layout/components/Screenfull'
@ -78,6 +79,7 @@ export default defineComponent({
{message.value ? (
<Message class="custom-hover" color="var(--top-header-text-color)"></Message>
) : undefined}
<ImChat class="custom-hover" color="var(--top-header-text-color)"></ImChat>
<UserInfo></UserInfo>
</div>
</div>

View File

@ -641,6 +641,51 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/iot/device/detail/index.vue')
}
]
},
{
path: '/im',
component: Layout,
name: 'IM',
meta: { hidden: true },
children: [
{
path: 'conversation',
name: 'Conversation',
meta: {
title: '会话',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/Conversation/index.vue'),
children: [
{
//聊天对话框
path: 'message',
name: 'Message',
meta: {
title: '聊天对话框',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/Message/index.vue')
}
]
},
{
path: 'contacts',
name: 'Contacts',
meta: {
title: '联系人',
noCache: true,
hidden: true,
noTagsView: true
},
component: () => import('@/views/im/Contacts/index.vue'),
children: []
}
]
}
]

View File

@ -0,0 +1,46 @@
import { store } from '../index'
import { defineStore } from 'pinia'
export interface ImMessageWebSocket {
id: number // 编号
conversationType: number // 会话类型
senderId: number // 发送人编号
senderNickname: string // 发送人昵称
senderAvatar: string // 发送人头像
receiverId: number // 接收人编号
contentType: number // 内容类型
content: string // 内容
sendTime: string // 发送时间
sequence: number // 序号
}
export const chatStore = defineStore({
id: 'imMessage',
state: () => ({
messages: [] as ImMessageWebSocket[],
localMaxSeq: 0
}),
getters: {
getMessages(): ImMessageWebSocket[] {
return this.messages
},
getLocalMaxSeq(): number {
return this.localMaxSeq
}
},
actions: {
loadMessages(messages: ImMessageWebSocket[]) {
this.messages = messages
},
addMessage(message: ImMessageWebSocket) {
this.messages.push(message)
},
setLocalMaxSeq(seq: number) {
this.localMaxSeq = seq
}
}
})
export const useImMessageStore = () => {
return chatStore(store)
}

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

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

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

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

View File

@ -0,0 +1,150 @@
<script setup>
import { useStore } from 'vuex'
import router from '@/router'
import { useRoute } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { messageType } from '@/constant'
/* 单人头像 */
import defaultSingleAvatar from '@/assets/imgs/im/avatar/theme2x.png'
/* store */
const store = useStore()
/* route */
const route = useRoute()
const { CHAT_TYPE } = messageType
/* 进入会话 */
const toChatMessage = () => {
console.log('>>>>>>>...route.query')
router.push({
path: '/chat/conversation/message',
query: {
id: route.query.id,
chatType: route.query.chatType
}
})
}
</script>
<template>
<div class="app_container">
<el-header class="contactInfo_header">
<el-page-header style="margin-top: 12px" :icon="ArrowLeft" @click="$router.back(-1)" />
<el-divider />
</el-header>
<el-main class="contactInfo_main">
<div class="contactInfo_main_card">
<div class="contactInfo_box">
<div class="avatar">
<el-avatar
class="avatar_img"
:src="nowContactInfo.avatarurl ? nowContactInfo.avatarurl : defaultSingleAvatar"
/>
</div>
<div class="name">
<p>
{{
nowContactInfo.nickname
? `${nowContactInfo.nickname}(${nowContactInfo.hxId})`
: nowContactInfo.hxId
}}
</p>
</div>
</div>
<div class="contaactInfo_btn">
<el-button type="primary" size="large" @click="toChatMessage"> </el-button>
</div>
</div>
</el-main>
</div>
</template>
<style lang="scss" scoped>
.app_container {
background: #f1f2f4;
height: 100%;
border-radius: 0 5px 5px 0;
overflow: hidden;
.contactInfo_header {
display: flex;
flex-direction: column;
height: 60px;
line-height: 60px;
}
.contactInfo_main {
height: 100%;
.contactInfo_main_card {
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 0 auto;
border-radius: 5px;
transition: all 0.5s;
&:hover {
background: #fff;
box-shadow: 12px 12px 2px 1px rgba(125, 125, 126, 0.068);
}
.contactInfo_box {
width: 80%;
min-height: 500px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
.avatar > .avatar_img {
width: 80px;
height: 80px;
}
.name {
margin-top: 15px;
font-size: 22px;
}
.func_box {
width: 100%;
.single_func {
height: 100px;
// background: #000;
margin-top: 25px;
cursor: pointer;
.add_black_list {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 16px;
}
.del_friend {
width: 100%;
color: red;
transition: all 0.3s;
}
}
}
}
.contaactInfo_btn {
width: 80%;
text-align: center;
}
}
}
}
//线
::v-deep .el-page-header__left::after {
width: 0px !important;
}
</style>

View File

@ -0,0 +1,116 @@
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
// import router from '@/router'
import { messageType } from '@/constant'
/* 默认头像 */
import defaultAvatar from '@/assets/images/avatar/theme2x.png'
/* store */
const store = useStore()
//friendList
const classifyFriendList = computed(() => store.getters.sortedFriendList)
//
const { CHAT_TYPE } = messageType
</script>
<template>
<div class="friendItem_container">
<div
v-for="(friendName, friendItemKey) in classifyFriendList"
:key="friendItemKey"
>
<div class="friend_main">
<div class="friend_title">
<p>
{{
friendItemKey === ' '
? '#'
: friendItemKey.toUpperCase()
}}
</p>
<el-divider style="margin: 0" />
</div>
<el-row>
<el-col
class="friendItem_box"
:span="24"
v-for="item in friendName"
:key="item.hxId"
@click="
$emit('toContacts', {
id: item.hxId,
chatType: CHAT_TYPE.SINGLE
})
"
>
<el-avatar
style="margin-right: 11px"
:size="33.03"
:src="
item.avatarurl ? item.avatarurl : defaultAvatar
"
>
</el-avatar>
<span class="friend_name">
{{ item.nickname ? item.nickname : item.hxId }}
</span>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.friendItem_box {
height: 66px;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
// background: #000;
padding: 0 22px;
background: #efefef;
font-weight: 500;
font-size: 14px;
line-height: 20px;
/* identical to box height */
text-align: center;
color: #333333;
cursor: pointer;
.friend_name {
display: inline-block;
text-align: left;
width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
background: #dcdcdc;
}
}
.friend_main {
width: 100%;
.friend_title {
width: 100%;
height: 32px;
mix-blend-mode: normal;
opacity: 0.21;
font-size: 14px;
padding-left: 23px;
font-weight: 400;
font-size: 12px;
line-height: 32px;
letter-spacing: 0.342857px;
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<el-container style="height: 100%">
<el-aside class="contacts_box">
<SearchInput :searchType="'contacts'" />
<el-scrollbar class="contacts_collapse" tag="div" :always="false">
<!-- 联系人群组列表 -->
<el-collapse />
</el-scrollbar>
</el-aside>
<el-main class="contacts_infors_main_box" />
</el-container>
</template>
<script lang="ts" setup>
/* 相关组件 */
import { SearchInput } from '@/components/Im/SearchInput'
import * as UserApi from '@/api/system/user'
//
const friendList = computed(() => {})
const getUserList = async () => {
const data = await UserApi.getSimpleUserList()
console.log('data', data)
friendList.value = data
}
/** 初始化 */
onMounted(() => {
getUserList()
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,285 @@
<script setup lang="ts">
import { formatDate } from '@/utils/formatTime'
import { reactive } from 'vue'
import * as ConversationApi from '@/api/im/conversation'
/* route */
const route = useRoute()
/* router */
const router = useRouter()
//(使)
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
}
])
// let conversationList = reactive({})
// //
// const getConversationList = async () => {
// const res = await ConversationApi.getConversationList()
// if (res.code === 200) {
// console.log('', res.data)
// conversationList = res.data
// }
// }
// //
// onMounted(() => {
// getConversationList()
// })
//name
const handleConversationName = computed(() => {
return ''
})
//lastmsgfrom
const handleLastMsgNickName = computed(() => {
return ''
})
const emit = defineEmits(['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) => {
console.log('选中的会话key', itemKey)
}
</script>
<template>
<el-scrollbar class="session_list" style="overflow: auto" tag="ul">
<!-- 普通会话 -->
<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 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">
.session_list {
position: relative;
height: 100%;
padding: 0;
margin: 0;
}
.offline_hint {
width: 100%;
height: 30px;
text-align: center;
line-height: 30px;
color: #f35f81;
background: #fce7e8;
font-size: 7px;
.plaint_icon {
display: inline-block;
width: 15px;
height: 15px;
color: #e5e5e5;
text-align: center;
line-height: 15px;
font-size: 7px;
font-weight: bold;
background: #e6686e;
border-radius: 50%;
}
}
.session_list .session_list_item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 66px;
background: #f0f0f0;
color: var(--el-color-primary);
border-bottom: 1px solid var(--el-border-color);
cursor: pointer;
&:hover {
background: #e5e5e5;
}
.item_body {
display: flex;
height: 100%;
}
.item_left {
flex-direction: row;
align-items: center;
justify-content: center;
margin-left: 14px;
margin-right: 10px;
}
.item_main {
width: 225px;
max-width: 225px;
height: 34px;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
.name {
min-width: 56px;
max-width: 180px;
height: 17px;
font-weight: 400;
font-size: 14px;
/* identical to box height */
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last_msg_body {
max-width: 185px;
height: 17px;
font-weight: 400;
font-size: 12px;
line-height: 17px;
letter-spacing: 0.3px;
color: #a3a3a3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last_msg_body_mention {
font-size: 12px;
line-height: 17px;
font-weight: bold;
color: red;
}
}
.item_right {
width: 25%;
height: 34px;
flex-direction: column;
align-items: flex-end;
margin-right: 10px;
.time {
font-size: 10px;
font-weight: 400;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.25px;
color: #a3a3a3;
}
.unReadNum_box {
margin-top: 10px;
vertical-align: middle;
.unReadNum_count {
display: inline-block;
min-width: 20px;
height: 20px;
padding: 0 6px;
color: #fff;
font-weight: normal;
font-size: 12px;
line-height: 20px;
white-space: nowrap;
text-align: center;
background: #f5222d;
border-radius: 10px;
box-sizing: border-box;
}
}
}
}
.session_list_item_active {
background: #d2d2d2;
}
.session_list .session_list_item + .list_item {
margin-top: 10px;
}
.session_list_delete {
cursor: pointer;
transition: all 0.5s;
&:hover {
background: #e1e1e1;
}
}
</style>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import { defineAsyncComponent, shallowRef } from 'vue'
import { SearchInput } from '@/components/Im/SearchInput'
import ConversationList from '../Conversation/components/ConversationList.vue'
import { Welcome } from '@/components/Im/Welcome'
const MessageComponent = defineAsyncComponent(() => import('@/views/im/Message/index.vue'))
const currentComponent = shallowRef(Welcome) //
const toChatMessage = (id) => {
console.log('>>>>>>>id', id)
currentComponent.value = MessageComponent //
}
</script>
<template>
<el-container style="height: 100%">
<el-aside class="chat_conversation_box">
<!-- 搜索组件 -->
<SearchInput :searchType="'conversation'" />
<div class="chat_conversation_list">
<ConversationList @to-chat-message="toChatMessage" />
</div>
</el-aside>
<el-main class="chat_conversation_main_box">
<component :is="currentComponent" />
</el-main>
</el-container>
</template>
<style lang="scss" scoped>
.chat_conversation_box {
position: relative;
background: #cfdbf171;
overflow: hidden;
min-width: 324px;
.chat_conversation_list {
height: calc(100% - 60px);
}
}
.chat_conversation_main_box {
position: relative;
width: 100%;
height: 100%;
padding: 0;
}
</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大小调整 */
: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,474 @@
<script setup>
import { ElLoading, ElMessageBox } from 'element-plus'
import { emojis } from '@/constant/im'
import { messageType } from '@/constant/im'
import _ from 'lodash'
import { onClickOutside } from '@vueuse/core'
/* 组件 */
import PreviewSendImg from '../suit/previewSendImg.vue'
import VueAt from 'vue-at/dist/vue-at-textarea' // for textarea
import * as MessageApi from '@/api/im/message'
import { generateUUID } from '@/utils'
const props = defineProps({
nowPickInfo: {
type: Object,
required: true,
default: () => ({})
}
})
const { ALL_MESSAGE_TYPE, CHAT_TYPE, MENTION_ALL } = messageType
const { nowPickInfo } = toRefs(props)
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)
onClickOutside(emojisBox, (event) => {
console.log('>>>>>关闭模态框')
isShowEmojisBox.value = false
event.stopPropagation()
})
const showEmojisBox = () => {
console.log('>>>>>展开模态框')
if (!isShowEmojisBox.value) {
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')
}
}
const imMessageSendReqVO = {
clientMessageId: generateUUID(),
receiverId: nowPickInfo.value.id,
conversationType: nowPickInfo.value.chatType,
contentType: ALL_MESSAGE_TYPE.TEXT,
content: textContent.value
}
//
if (messageQuoteRef.value?.isShowQuoteMsgBox) {
}
textContent.value = ''
messageQuoteRef.value?.clearQuoteContent()
try {
console.log('imMessageSendReqVO', imMessageSendReqVO)
const imMessageSendRespVO = await MessageApi.sendMessage(imMessageSendReqVO)
console.log('>>>>>发送成功', imMessageSendRespVO)
} catch (error) {
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)
onClickOutside(recordBox, () => {
isShowRecordBox.value = false
})
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"> ,httpslocalhost使 </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,141 @@
<script setup>
import { formatDate } from '@/utils/formatTime'
/* 默认头像 */
import defaultAvatar from '@/assets/imgs/avatar.gif'
import { useUserStore } from '@/store/modules/user'
import avatarImg from '@/assets/imgs/avatar.gif'
import paseLink from '@/utils/paseLink.ts'
import fileSizeFormat from '@/utils/fileSizeFormat'
import { messageType } from '@/constant/im'
const { ALL_MESSAGE_TYPE, CUSTOM_TYPE } = messageType
//
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
/* props */
const props = defineProps({
messageData: {
type: [Array, Object],
default: () => []
},
nowPickInfo: {
type: Object,
default: () => ({}),
required: true
}
})
const { nowPickInfo } = toRefs(props)
const { messageData } = toRefs(props)
console.log('>>>>>messageData', messageData)
/* 处理时间显示间隔 */
const handleMsgTimeShow = (sendTime, index) => {
const msgList = Array.from(messageData.value)
if (index !== 0) {
const lastTime = msgList[index - 1].sendTime
return sendTime - lastTime > 50000 ? formatDate(sendTime, 'MM/DD/HH:mm') : ''
} else {
return formatDate(sendTime, 'MM/DD/HH:mm')
}
}
/* computed-- 消息来源是否为自己 */
const isMyself = (msgBody) => {
return msgBody.senderId === userStore.user.id
}
/* 文本中是否包含link */
const isLink = computed(() => {
return (msg) => {
return paseLink(msg).isLink
}
})
/* 获取自己的用户信息 */
const loginUserInfo = {
avatarurl: avatar.value
}
//
const handleNickName = (msgBody) => {
return msgBody.senderId === userStore.user.id ? '我' : '对方'
}
//
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.conversationType !== ALL_MESSAGE_TYPE.OA_NOTIFICATION"
class="message_box_item"
:style="{
flexDirection: isMyself(msgBody) ? 'row-reverse' : 'row'
}"
>
<div class="message_item_time">
{{ handleMsgTimeShow(msgBody.sendTime, index) }}
</div>
<el-avatar class="message_item_avator" :src="loginUserInfo.avatarurl" />
<!-- 普通消息内容 -->
<div class="message_box_card">
<span v-show="!isMyself(msgBody)" class="message_box_nickname">{{
handleNickName(msgBody)
}}</span>
<el-dropdown
class="message_box_content"
:class="[isMyself(msgBody) ? 'message_box_content_mine' : 'message_box_content_other']"
trigger="contextmenu"
placement="bottom-end"
>
<!-- 文本类型消息 -->
<p
style="padding: 10px; line-height: 20px"
v-if="msgBody.contentType === ALL_MESSAGE_TYPE.TEXT"
>
<template v-if="!isLink(msgBody.content)">
{{ msgBody.content }}
</template>
<template v-else> <span v-html="paseLink(msgBody.content).msg"> </span></template>
</p>
<!-- 图片类型消息 -->
<!-- 语音类型消息 -->
<!-- 文件类型消息 -->
</el-dropdown>
</div>
</div>
</div>
</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_input_bar {
position: relative;
width: 100%;
height: 25%;
padding: 0;
background-color: #f9f9f9;
border-radius: 0 0 3px 0;
}
: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

@ -0,0 +1,109 @@
<script setup lang="ts">
import { UserStatus } from '@/components/Im/UserStatus'
import { messageType } from '@/constant/im'
/* 组件 */
import MessageList from './components/messageList/index.vue'
import InputBox from './components/inputBox/index.vue'
import * as MessageApi from '@/api/im/message'
import { chatStore, useImMessageStore } from '@/store/modules/chatStore'
const { addMessage, setLocalMaxSeq } = chatStore()
const { messages } = useImMessageStore()
const { query } = useRoute() //
const { CHAT_TYPE, ALL_MESSAGE_TYPE } = messageType
/* header 操作 */
const drawer = ref(false) //
const handleDrawer = () => {
drawer.value = !drawer.value
}
//
const nowPickInfo = ref({
id: 100,
chatType: CHAT_TYPE.SINGLE,
userInfo: {
nickname: '芋道',
userStatus: '1'
},
groupDetail: {
name: '',
affiliations_count: '',
custom: ''
}
})
//
const pullParams = reactive({
sequence: 0, //
size: 10
})
//
const messageData = ref([])
const getMessageData = async () => {
messageData.value = messages
}
console.log(messageData)
/* 消息相关 */
const loadingHistoryMsg = ref(false) //
const isMoreHistoryMsg = ref(true) //
const notScrollBottom = ref(false) //
//
const fechHistoryMessage = (loadType) => {
console.log(loadType)
console.log('加载更多')
loadingHistoryMsg.value = true
getMessageData()
loadingHistoryMsg.value = false
}
//
const scrollMessageList = (direction) => {
console.log(direction)
}
/** 初始化 **/
onMounted(() => {
getMessageData()
})
</script>
<template>
<el-container class="app_container">
<el-header class="chat_message_header">
<template v-if="nowPickInfo.chatType === CHAT_TYPE.SINGLE">
<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>
</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" 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" />
</div>
</el-scrollbar>
</el-main>
<el-footer class="chat_message_input_bar">
<InputBox ref="inputBox" :nowPickInfo="nowPickInfo" />
</el-footer>
</el-container>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@ -0,0 +1,287 @@
<script setup lang="ts">
/* route */
import { useRoute } from 'vue-router'
/* 取用户头像 */
import router from '@/router'
import highlightConversation from '@/assets/imgs/im/tabbar/highlightconversation.png'
import grayConversation from '@/assets/imgs/im/tabbar/grayconversation.png'
import highlightContacts from '@/assets/imgs/im/tabbar/higtlightcontacts.png'
import grayContacts from '@/assets/imgs/im/tabbar/graycontacts.png'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
/* tabular icon 路由跳转 */
const skipRouterName = ref('conversation')
const changeSkipRouterName = (routerName: string) => {
router.push(`/im/${routerName}`)
}
const route = useRoute()
//
watch(
() => route.path,
(newPath) => {
console.log('>>>>>newPath', newPath)
if (newPath.includes('/im/conversation')) {
skipRouterName.value = 'conversation'
}
if (newPath.includes('/im/contacts')) {
console.log('>>>>>存在赋值为联系人样式')
skipRouterName.value = 'contacts'
}
}
)
</script>
<template>
<!-- 头像 -->
<div class="chat_avatar">
<ElAvatar :src="avatar" alt="" class="w-[calc(var(--logo-height)-25px)] rounded-[50%]" />
</div>
<!-- 去往会话 -->
<div class="chat_conversation chat_icon_box" @click="changeSkipRouterName('conversation')">
<div class="img_box">
<img
:src="skipRouterName === 'conversation' ? highlightConversation : grayConversation"
alt=""
/>
</div>
</div>
<!-- 去往联系人 -->
<div class="chat_contacts chat_icon_box" @click="changeSkipRouterName('contacts')">
<img
class="chat_contacts_icon"
:src="skipRouterName === 'contacts' ? highlightContacts : grayContacts"
alt=""
/>
</div>
</template>
<style lang="scss" scoped>
.chat_avatar {
margin-top: 43px;
position: relative;
width: 44px;
height: 44px;
transition: all 0.3s;
&:hover {
transform: scale(1.3);
}
span {
display: inline-block;
width: 100%;
height: 100%;
}
.online_status {
position: absolute;
right: 2px;
bottom: 2px;
display: inline-block;
width: 6px;
height: 6px;
border: 2px solid #fff;
background: #fff;
border-radius: 50%;
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
}
.chat_icon_box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 67px;
text-align: center;
line-height: 67px;
margin: 4px 0;
}
.chat_conversation {
.img_box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
img {
display: inline-block;
width: 27px;
height: 27px;
transition: all 0.5s;
&:hover {
transform: scale(1.3);
}
}
.badge {
position: absolute;
right: 0;
top: 8px;
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: red;
}
}
}
.chat_contacts {
img {
display: inline-block;
width: 27px;
height: 27px;
transition: all 0.5s;
&:hover {
transform: scale(1.3);
}
}
}
.chat_settings {
position: absolute;
bottom: 92px;
font-size: 30px;
color: #8e8e8e;
cursor: pointer;
transition: all 0.5s;
&:hover {
color: #1b83f9;
transform: scale(1.3);
}
.chat_setting_item {
width: 100%;
height: 30px;
}
}
.more_settings {
position: absolute;
bottom: 46px;
color: #8e8e8e;
cursor: pointer;
transition: all 0.5s;
&:hover {
transform: scale(1.3);
}
}
.setting_fun_list {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.func_item {
display: flex;
flex-direction: row;
align-items: center;
// justify-content: space-around;
width: 101px;
height: 40px;
border-radius: 3px;
&:hover {
background-color: #f2f2f2;
}
.settting_fun_icon {
display: flex;
align-items: center;
justify-content: center;
margin-left: 5px;
img {
width: 20px;
height: 20px;
}
}
.setting_fun_text {
display: inline-block;
text-align: center;
margin-left: 12px;
height: 20px;
width: 58px;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.4px;
color: #333333;
cursor: pointer;
}
.apply_groups {
display: flex;
flex-direction: column;
}
}
}
.settting_fun_icon {
font-size: 20px;
}
.line {
display: inline-block;
width: 69px;
height: 1px;
border: 1px solid rgba(0, 0, 0, 0.0462467);
}
.components {
:deep(.edit_userinfo_diglog) {
border-radius: 4px;
overflow: hidden;
}
.setting_func_diglog :deep(.el-dialog__body) {
padding: 28px 24px 24px 24px;
}
.setting_func_diglog :deep(.el-dialog__header) {
background: #f2f2f2;
margin: 0;
}
.edit_userinfo_diglog :deep(.el-dialog__header) {
padding: 0;
margin-right: 0;
}
.edit_userinfo_diglog :deep(.el-dialog__body) {
padding: 0;
border-radius: 4px;
}
.login_diglog :deep(.el-dialog__header) {
background: #f2f2f2;
margin: 0;
}
.personal_setting_card :deep(.el-dialog__header) {
background: #f2f2f2;
margin: 0;
}
}
</style>

61
src/views/im/index.vue Normal file
View File

@ -0,0 +1,61 @@
<template>
<div class="app-container">
<el-container class="chat_container">
<el-aside class="chat_nav_bar" width="72px">
<NavBar />
</el-aside>
<el-main class="chat_main_box">
<component :is="currentComponent" />
</el-main>
</el-container>
</div>
</template>
<script lang="ts" setup>
import { shallowRef, defineAsyncComponent, DefineComponent } from 'vue'
import NavBar from './NavBar/index.vue'
defineOptions({ name: 'IM' })
//
const conversationComponent = defineAsyncComponent(
() => import('@/views/im/Conversation/index.vue')
)
const currentComponent = shallowRef<DefineComponent | null>(conversationComponent) //
</script>
<style lang="scss" scoped>
.app-container {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-size: cover;
backdrop-filter: blur(5px);
.chat_container {
width: 85%;
height: 95%;
background: #fff;
position: relative;
top: 50%;
transform: translateY(-50%);
margin: auto auto;
border-radius: 5px;
.chat_nav_bar {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 5px 0 0 5px;
width: 80px;
background: #262626;
overflow: hidden;
}
.chat_main_box {
padding: 0;
}
}
}
</style>

View File

@ -1,8 +1,8 @@
import {resolve} from 'path'
import type {ConfigEnv, UserConfig} from 'vite'
import {loadEnv} from 'vite'
import {createVitePlugins} from './build/vite'
import {exclude, include} from "./build/vite/optimize"
import { resolve } from 'path'
import { loadEnv } from 'vite'
import type { UserConfig, ConfigEnv } from 'vite'
import { createVitePlugins } from './build/vite'
import { include, exclude } from "./build/vite/optimize"
// 当前执行node命令时文件夹的地址(工作目录)
const root = process.cwd()