Commit 618cc34e authored by yaya's avatar yaya

feat: 添加聊天功能

parent 3c7b83f2
......@@ -22,6 +22,7 @@
},
"dependencies": {
"@riophae/vue-treeselect": "0.0.37",
"@tencentcloud/chat": "^3.2.1",
"@tinymce/tinymce-vue": "1.1.0",
"axios": "0.18.0",
"clipboard": "1.7.1",
......@@ -36,6 +37,8 @@
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"screenfull": "3.3.3",
"tim-profanity-filter-plugin": "^1.0.0",
"tim-upload-plugin": "^1.3.0",
"tree-table-vue": "^1.1.0",
"v-charts": "1.19.0",
"vue": "2.5.17",
......@@ -46,9 +49,9 @@
"vue-router": "3.0.1",
"vue2-org-tree": "^1.3.6",
"vued3tree": "^5.1.0",
"vuedraggable": "^2.23.2",
"vuex": "3.0.1",
"xlsx": "^0.11.16",
"vuedraggable": "^2.23.2"
"xlsx": "^0.11.16"
},
"devDependencies": {
"autoprefixer": "8.5.0",
......
import request from '@/utils/request'
export function getUserSig(params) {
return request({
url: '/sysIM/getUserSig',
method: 'post',
data: params
})
}
export function getUserList(params) {
return request({
url: '/sysAiAssistant/find',
method: 'post',
data: params
})
}
import TencentCloudChat from '@tencentcloud/chat'
import TIMUploadPlugin from 'tim-upload-plugin'
import TIMProfanityFilterPlugin from 'tim-profanity-filter-plugin'
// 初始化 SDK 实例
const options = {
SDKAppID: 1400489950 // 接入时需要将0替换为您的即时通信 IM 应用的 SDKAppID
}
// 创建 SDK 实例,`TencentCloudChat.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
const chat = TencentCloudChat.create(options) // SDK 实例通常用 chat 表示
chat.setLogLevel(0) // 普通级别,日志量较多,接入时建议使用
// chat.setLogLevel(1); // release 级别,SDK 输出关键信息,生产环境时建议使用
// 注册腾讯云即时通信 IM 上传插件
chat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin })
// 注册腾讯云即时通信 IM 本地审核插件
chat.registerPlugin({ 'tim-profanity-filter-plugin': TIMProfanityFilterPlugin })
export default chat
......@@ -51,21 +51,61 @@ export function formatTime(time, option) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
} else if (diff < 3600 * 24 * 2 * 7) {
return (
d.getMonth() +
1 +
'-' +
d.getDate() +
' ' +
d.getHours() +
':' +
d.getMinutes()
)
} else if (isYear(time)) {
return (
d.getMonth() +
1 +
'-' +
d.getDate()
)
} else {
return (
d.getFullYear() +
'-' +
d.getMonth() +
'-' +
d.getDate() +
' ' +
d.getHours() +
':' +
d.getMinutes()
)
}
if (option) {
/* if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'' +
d.getMonth()
+
'-' +
d.getDate() +
'' +
' ' +
d.getHours() +
'' +
d.getMinutes() +
''
':' +
d.getMinutes()
)
}*/
}
function isYear(timeValue) { // 是否为今年
const dateyear = new Date(timeValue).getFullYear()
const toyear = new Date().getFullYear()
// console.log(dateyear, toyear)
if (dateyear === toyear) {
return true
} else {
return false
}
}
......
......@@ -5,12 +5,31 @@
<span slot="title">{{ item.title }}</span>
</el-menu-item>
</el-menu>-->
<el-container style="width: 100%;height: 100%; border: 1px solid #eee">
<el-aside style="background-color: white;padding: 10px">
<el-tabs :tab-position="top" style="height: 100%;width: 100%;" @tab-click="handleClick">
<el-tab-pane v-for="item in chatList" :key="item.userId" :label="item.title">{{ item.title }}</el-tab-pane>
<el-container v-loading="loading" style="width: 100%;height: 100%; border: 1px solid #eee">
<el-aside style="width: 25%;background-color: white;padding: 0">
<el-tabs style="width: 100%; padding: 10px 10px 0" @tab-click="handleClick">
<el-tab-pane v-for="item in chatList" :key="item.code" :label="item.name" />
</el-tabs>
<div class="conversation">
<div
v-for="item in conversationList"
:key="item.conversationID"
:class="{'conversation-item-active': currentChatItem.conversationID === item.conversationID}"
class="conversation-item flex f-ai-c"
@click="handleSelectChat(item)">
<div class="conversation-item-image">
<img :src="item.avatar" class="conversation-item-image__avatar">
<div class="conversation-item-image__num">2</div>
</div>
<div class="conversation-item-info">
<div class="conversation-item-info__nick">{{ item.nick }}</div>
<div class="conversation-item-info__message flex f-jc-sb">
<div class="conversation-item-info__message__profile">{{ item.messageForShow }}</div>
<div class="conversation-item-info__message__time">{{ item.lastTime }}</div>
</div>
</div>
</div>
</div>
</el-aside>
<el-main style="background-color: rgb(238, 241, 246); padding: 0; border: 1px solid #eee">
<div class="chat-message">
......@@ -18,6 +37,22 @@
<div class="chat-message-top__title">账号昵称</div>
<el-button type="text">人工介入</el-button>
</div>
<div class="chat-message-list" style="overflow:auto">
<ul
class="chat-message-list__content"
infinite-scroll-disabled="disabled"
style="margin: 0; padding: 0">
<div v-for="messageItem in messageList" :key="messageItem.id" class="chat-message-list__content__item">
<ChatMessageItem :chat-message-item="messageItem" />
</div>
</ul>
</div>
<div class="chat-message-input">
<el-input v-model="messageContent" :rows="4" type="textarea" placeholder="请输入聊天内容" style="border: none"/>
<div class="chat-message-input__send">
<el-button :disabled="sendDisabled" type="primary">发送</el-button>
</div>
</div>
</div>
</el-main>
<el-aside style="width: 20%;background-color: white;padding: 10px">
......@@ -31,45 +66,266 @@
</template>
<script>
import { getUserSig, getUserList } from '@/api/business/chat'
import chat from '@/utils/chat'
import { formatTime } from '@/utils/index'
import TencentCloudChat from '@tencentcloud/chat'
import ChatMessageItem from './components/chatMessageItem'
export default {
name: 'ChatDetail',
components: { ChatMessageItem },
data() {
return {
activeIndex: '1233',
chatList: [
{
title: '老猫',
userId: '1233'
},
{
title: '葛若',
userId: '1323'
},
{
title: '斑胡',
userId: '1133'
},
{
title: '测试1',
userId: '1143'
},
{
title: '测试2',
userId: '1633'
},
{
title: '测试3',
userId: '1933'
}
]
currentChatItem: {}, // 当前聊天
chatList: [],
loading: true,
conversationList: [],
messageContent: '',
count: 10,
currentChatIndex: -1, // 当前聊天
nextReqMessageID: '', // 历史消息下一页
isCompleted: false,
envText: 'cjxc-applet-dev_',
messageList: [] // 历史消息
}
},
computed: {
sendDisabled() {
return !this.messageContent || this.messageContent.length === 0
}
},
created() {
chat.on(TencentCloudChat.EVENT.SDK_READY, this.onSdkReady)
this.handleGetChatList()
},
methods: {
handleSelect(key, keyPath) {
console.log(key, keyPath)
},
/**
* 选择客服
**/
handleClick(tab, event) {
console.log(tab, event)
console.log('====tab=', tab.index, event)
if (this.currentChatIndex === tab.index) {
return
}
this.currentChatIndex = tab.index
this.handleBeforeLogin()
},
/**
* 获取客服列表
**/
handleGetChatList() {
const params = {}
getUserList(params).then(response => {
if (response && response.data && response.data.data) {
this.chatList = response.data.data
}
if (this.chatList.length > 0) {
this.handleBeforeLogin()
}
console.log('===list ===', response.data.data)
}).catch(response => {
this.$notify.error({
title: '失败',
message: response.data.errmsg || '请求失败'
})
})
},
handleBeforeLogin() {
this.showLoading()
if (this.currentChatIndex === -1) {
const chatItem = this.chatList[0]
this.currentChatIndex = 0
this.handleGetUserSign(chatItem.code)
} else {
const promise = chat.logout()
promise.then((imResponse) => {
// console.log('==登出成功=', imResponse.data, this.chatList) // 登出成功
const chatItem = this.chatList[this.currentChatIndex]
this.handleGetUserSign(chatItem.code)
}).catch(function(imError) {
console.warn('====登出失败:', imError) // 登录失败的相关信息
})
}
},
/**
* 获取UserSign
*/
handleGetUserSign(userId) {
const params = {
userId: userId
}
getUserSig(params).then(response => {
this.handleImLoin(userId, response.data.data.userSig)
}).catch(response => {
this.closeLoading()
console.log('====获取userSign失败,', response)
this.$notify.error({
title: '失败',
message: response.data.errmsg || '请求失败'
})
})
},
/**
* IM 登录
*/
handleImLoin(userId, userSign) {
console.log('====用户信息===', userId, userSign) // 登录成功
const promise = chat.login({ userID: this.envText + userId, userSig: userSign })
promise.then((imResponse) => {
this.closeLoading()
console.log('====登录成功===', imResponse.data) // 登录成功
chat.on(TencentCloudChat.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated)
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, this.onMessageReceived)
this.handleGetConversationList()
if (imResponse.data.repeatLogin === true) {
// 标识账号已登录,本次登录操作为重复登录。
console.log(imResponse.data.errorInfo)
}
}).catch((imError) => {
this.closeLoading()
console.warn('====登录失败:', imError) // 登录失败的相关信息
})
},
onSdkReady(event) {
this.handleGetConversationList()
},
onMessageReceived(event) {
const receivedList = event.data
receivedList.forEach((message) => {
if (message.type === TencentCloudChat.TYPES.MSG_TEXT) {
console.log('=====新消息=', message)
// 文本消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.TextPayload
}/* else if (message.type === TencentCloudChat.TYPES.MSG_IMAGE) {
// 图片消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.ImagePayload
} else if (message.type === TencentCloudChat.TYPES.MSG_SOUND) {
// 音频消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.AudioPayload
} else if (message.type === TencentCloudChat.TYPES.MSG_VIDEO) {
// 视频消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.VideoPayload
} else if (message.type === TencentCloudChat.TYPES.MSG_FILE) {
// 文件消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.FilePayload
} else if (message.type === TencentCloudChat.TYPES.MSG_CUSTOM) {
// 自定义消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.CustomPayload
} else if (message.type === TencentCloudChat.TYPES.MSG_MERGER) {
// 合并消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.MergerPayload
} else if (message.type === TencentCloudChat.TYPES.MSG_LOCATION) {
// 地理位置消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.LocationPayload
} else if (message.type === TencentCloudChat.TYPES.MSG_GRP_TIP) {
// 群提示消息 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.GroupTipPayload
} else if (message.type === TencentCloudChat.TYPES.MSG_GRP_SYS_NOTICE) {
// 群系统通知 - https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html#.GroupSystemNoticePayload
}*/
})
},
handleGetConversationList() {
const promise = chat.getConversationList()
promise.then((imResponse) => {
// 全量的会话列表,用该列表覆盖原有的会话列表
const conversationList = imResponse.data.conversationList
this.setConversationList(conversationList)
}).catch((imError) => {
// 获取会话列表失败的相关信息
console.warn('getConversationList error:', imError)
})
},
/**
* 会话列表更改监听
* */
onConversationListUpdated(event) {
console.log('====会话列表更新--', event.data) // 包含 Conversation 实例的数组
this.setConversationList(event.data)
},
setConversationList(list) {
if (!list || list.length === 0) {
this.conversationList = []
} else {
const newList = []
list.map((item) => {
const object = {
conversationID: item.conversationID || '',
messageForShow: item.lastMessage.messageForShow,
lastTime: formatTime(item.lastMessage.lastTime),
avatar: item.userProfile.avatar,
nick: item.userProfile.nick,
fromAccount: item.userProfile.userID,
isPinned: item.isPinned,
unreadCount: item.unreadCount || 0
}
newList.push(object)
})
this.conversationList = newList
}
},
/**
* 选择聊天
* */
handleSelectChat(item) {
this.messageList = []
if (this.currentChatItem !== item) {
this.currentChatItem = item
this.nextReqMessageID = ''
this.handleGetHistory()
}
},
handleGetHistory() {
const userID = this.chatList[this.currentChatIndex].code
if (this.currentChatItem) {
const promise = chat.getMessageList({
conversationID: this.currentChatItem.conversationID,
nextReqMessageID: this.nextReqMessageID
})
promise.then((imResponse) => {
if (!imResponse || !imResponse.data) {
return
}
const list = imResponse.data.messageList || [] // 消息列表。
this.nextReqMessageID = imResponse.data.nextReqMessageID || '' // 用于续拉,分页续拉时需传入该字段。
this.isCompleted = imResponse.data.isCompleted || false // 表示是否已经拉完所有消息。
console.log('====历史消息===', list)
const topList = []
if (list && list.length > 0) {
imResponse.data.messageList.map((item) => {
const object = {
id: item.ID,
messageContent: item.payload.text,
fromUserId: item.from,
toAccount: item.to,
avatar: item.avatar,
msgType: item.from === this.envText + userID ? 1 : 2
}
topList.push(object)
})
}
console.log('====messageList', this.messageList, topList)
this.messageList = topList.concat(this.messageList)
console.log('====历史消息', this.messageList)
}).catch((err) => {
console.log('====err--历史消息', err)
})
}
},
/**
* 打开加载
*/
showLoading() {
this.loading = true
},
/**
* 关闭加载
*/
closeLoading() {
this.loading = false
}
}
}
......@@ -79,6 +335,12 @@ export default {
/deep/ .main {
padding: 0;
}
/deep/ .el-textarea__inner {
border: none;
}
/deep/ .ul {
padding-inline-start: 0;
}
.page-chatDetail {
height: calc(100vh - 84px);
......@@ -87,14 +349,118 @@ export default {
width: 30%;
}
.conversation {
height: 100%;
.conversation-item {
border-bottom: 1px solid #e7eaec;
height: 50px;
background: white;
padding: 0 12px;
.conversation-item-image {
position: relative;
width: 30px;
height: 30px;
margin-right: 8px;
.conversation-item-image__avatar {
width: 30px;
height: 30px;
border-radius: 15px;
}
.conversation-item-image__num {
position: absolute;
right: 0;
top: 0;
height: 12px;
background-color: red;
color: white;
font-size: 12px;
font-weight: bold;
padding: 3px 6px;
border-radius: 50%;
display: none;
}
}
.conversation-item-info {
width: 100%;
.conversation-item-info__nick {
height: 20px;
font-size: 13px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #17191C;
line-height: 20px;
}
.conversation-item-info__message {
width: 100%;
.conversation-item-info__message__profile {
flex: 1;
height: 16px;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #989CA8;
line-height: 16px;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
margin-right: 10px
}
.conversation-item-info__message__time {
height: 16px;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #989CA8;
line-height: 16px;
}
}
}
}
.conversation-item-active {
background: rgb(238, 241, 246);
}
}
.chat-message {
display: flex;
flex-direction: column;
height:100%;
.chat-message-top {
background: white;
padding: 8px;
}
.chat-message-list {
width: 100%;
flex: 1;
.chat-message-list__content {
width: 100%;
.chat-message-list__content__item {
width: 100%;
margin-bottom: 20px;
}
}
}
.chat-message-input {
border: 1px solid #DCDFE6;
border-radius: 4px;
background-color: #FFFFFF;
padding: 12px 0;
.chat-message-input__send {
text-align: end;
margin: 12px 12px 0 0;
}
}
}
}
......
<template>
<div class="message_components">
<div v-if="chatMessageItem.msgType === 1" class="message-self">
<div class="message-self_content">{{ chatMessageItem.messageContent }}</div>
<img :src="chatMessageItem.avatar" class="message-self__avatar">
</div>
<div v-if="chatMessageItem.msgType === 2" class="message-other">
<img :src="chatMessageItem.avatar" class="message-other__avatar">
<div class="message-other_content">
{{ chatMessageItem.messageContent }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ChatMessageItem',
props: {
chatMessageItem: {
type: Object,
required: true
}
}
}
</script>
<style lang="less" scoped>
.message_components {
width: 100%;
display: flex;
flex-direction: column;
.message-self {
display: flex;
flex-direction: row;
.message-self_content {
width: fit-content;
max-width: 60%;
background: #95EC69;
border-radius: 11px 11px 0px 11px;
padding: 10px 12px;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
color: #000000;
line-height: 20px;
margin-left:auto;
}
.message-self__avatar {
width: 30px;
height: 30px;
border-radius: 15px;
margin: 0 10px;
}
}
.message-other {
display: flex;
flex-direction: row;
.message-other_content {
width: fit-content;
max-width: 60%;
background: #FFFFFF;
border-radius: 11px 11px 0px 11px;
padding: 10px 12px;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
color: #000000;
line-height: 20px;
}
.message-other__avatar {
width: 30px;
height: 30px;
border-radius: 15px;
margin: 0 10px;
}
}
}
</style>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment