mirror of
https://github.com/cool-team-official/cool-admin-vue.git
synced 2024-11-01 22:20:27 +08:00
808 lines
15 KiB
Vue
808 lines
15 KiB
Vue
<template>
|
||
<div class="chat-wrap">
|
||
<!-- 聊天窗口 -->
|
||
<cl-dialog :visible.sync="visible" v-bind="conf">
|
||
<div class="chat-box">
|
||
<!-- 会话区域 -->
|
||
<div class="chat-box__session">
|
||
<div class="chat-box__session-search">
|
||
<el-input
|
||
v-model="session.keyWord"
|
||
placeholder="搜索"
|
||
prefix-icon="el-icon-search"
|
||
size="small"
|
||
clearable
|
||
@clear="onSearch"
|
||
@keyup.enter.native="onSearch"
|
||
></el-input>
|
||
</div>
|
||
|
||
<!-- 会话列表 -->
|
||
<ul class="chat-box__session-list scroller1">
|
||
<li
|
||
class="chat-box__session-item"
|
||
v-for="(item, index) in sessionList"
|
||
:key="index"
|
||
:class="{
|
||
'is-active': session.current ? item.id == session.current.id : false
|
||
}"
|
||
@click="sessionDetail(item)"
|
||
@contextmenu.stop.prevent="openSessionCM($event, item.id, index)"
|
||
>
|
||
<!-- 头像 -->
|
||
<div class="avatar">
|
||
<el-badge
|
||
:value="item.serviceUnreadCount"
|
||
:hidden="item.serviceUnreadCount === 0"
|
||
:max="99"
|
||
>
|
||
<img :src="item.headimgurl" alt="" />
|
||
</el-badge>
|
||
</div>
|
||
|
||
<!-- 昵称,内容 -->
|
||
<div class="det">
|
||
<p class="name">{{ item.nickname }}</p>
|
||
<p class="content">{{ item.lastMessage }}</p>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- 会话详情 -->
|
||
<div class="chat-box__detail">
|
||
<template v-if="session.current">
|
||
<div
|
||
class="chat-box__detail-container scroller1"
|
||
ref="scroller"
|
||
v-loading="message.loading"
|
||
>
|
||
<!-- 加载更多 -->
|
||
<div class="chat-box__detail-more" v-if="message.list.length > 0">
|
||
<el-button
|
||
round
|
||
size="mini"
|
||
:loading="message.loading"
|
||
@click="onLoadmore"
|
||
>加载更多</el-button
|
||
>
|
||
</div>
|
||
|
||
<!-- 消息列表 -->
|
||
<message :list="message.list" />
|
||
</div>
|
||
|
||
<div class="chat-box__detail-footer">
|
||
<!-- 工具栏 -->
|
||
<div class="chat-box__opbar">
|
||
<ul>
|
||
<!-- 表情 -->
|
||
<li>
|
||
<el-popover
|
||
v-model="emoji.visible"
|
||
placement="top-start"
|
||
width="470"
|
||
trigger="click"
|
||
>
|
||
<emoji @select="onEmojiSelect" />
|
||
<img
|
||
slot="reference"
|
||
src="../static/images/emoji.png"
|
||
alt=""
|
||
/>
|
||
</el-popover>
|
||
</li>
|
||
<!-- 图片上传 -->
|
||
<li>
|
||
<cl-upload
|
||
accept="image/*"
|
||
list-type
|
||
:on-success="onImageSelect"
|
||
>
|
||
<img src="../static/images/image.png" alt="" />
|
||
</cl-upload>
|
||
</li>
|
||
<!-- 视频上传 -->
|
||
<li>
|
||
<cl-upload
|
||
accept="video/*"
|
||
list-type
|
||
:before-upload="
|
||
(f) => {
|
||
onBeforeUpload(f, 'video');
|
||
}
|
||
"
|
||
:on-progress="onUploadProgress"
|
||
:on-success="
|
||
(r, f) => {
|
||
onUploadSuccess(r, f, 'video');
|
||
}
|
||
"
|
||
>
|
||
<img src="../static/images/video.png" alt="" />
|
||
</cl-upload>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- 输入框,发送按钮 -->
|
||
<div class="chat-box__input">
|
||
<el-input
|
||
v-model="message.value"
|
||
placeholder="请描述您想咨询的问题"
|
||
type="textarea"
|
||
:rows="5"
|
||
@keyup.enter.native="onTextSend"
|
||
></el-input>
|
||
|
||
<el-button
|
||
type="primary"
|
||
size="mini"
|
||
:disabled="!message.value"
|
||
@click="onTextSend"
|
||
>发送</el-button
|
||
>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</cl-dialog>
|
||
|
||
<!-- MP3 -->
|
||
<div class="mp3">
|
||
<audio style="display: none" ref="sound" src="../static/notify.mp3" controls></audio>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import dayjs from "dayjs";
|
||
import io from "socket.io-client";
|
||
import { isString, debounce } from "cl-admin/utils";
|
||
import { mapGetters } from "vuex";
|
||
import { socketUrl } from "@/config/env";
|
||
import Emoji from "./emoji";
|
||
import Message from "./message";
|
||
import { parseContent } from "../utils";
|
||
|
||
// 消息模式
|
||
const MODES = ["text", "image", "emoji", "voice", "video"];
|
||
|
||
export default {
|
||
name: "cl-chat",
|
||
|
||
components: {
|
||
Message,
|
||
Emoji
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
visible: false,
|
||
conf: {
|
||
title: "聊天对话框",
|
||
props: {
|
||
modal: true,
|
||
"custom-class": "chat-box__wrap",
|
||
"append-to-body": true,
|
||
"close-on-click-modal": false,
|
||
width: "1000px"
|
||
}
|
||
},
|
||
message: {
|
||
list: [],
|
||
pagination: {
|
||
page: 1,
|
||
size: 20,
|
||
total: 0
|
||
},
|
||
loading: false,
|
||
value: ""
|
||
},
|
||
session: {
|
||
list: [],
|
||
pagination: {
|
||
page: 1,
|
||
size: 100,
|
||
total: 0
|
||
},
|
||
current: null,
|
||
keyWord: ""
|
||
},
|
||
emoji: {
|
||
visible: false
|
||
},
|
||
socket: null
|
||
};
|
||
},
|
||
|
||
computed: {
|
||
...mapGetters(["userInfo", "token"]),
|
||
|
||
sessionList() {
|
||
return this.session.list
|
||
.map((e) => {
|
||
let { _text } = parseContent(e);
|
||
e.lastMessage = _text;
|
||
return e;
|
||
})
|
||
.sort((a, b) => {
|
||
return a.updateTime < b.updateTime ? 1 : -1;
|
||
});
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
this.socket = io(`${socketUrl}?isAdmin=true&token=${this.token}`);
|
||
|
||
this.socket.on("connect", () => {
|
||
console.log("socket connect");
|
||
});
|
||
|
||
this.socket.on("admin", (msg) => {
|
||
this.onMessage(msg);
|
||
});
|
||
|
||
this.socket.on("error", (err) => {
|
||
console.log(err);
|
||
});
|
||
|
||
this.socket.on("disconnect", () => {
|
||
console.log("disconnect connect");
|
||
});
|
||
},
|
||
|
||
destroyed() {
|
||
this.socket.close();
|
||
},
|
||
|
||
methods: {
|
||
open() {
|
||
this.visible = true;
|
||
|
||
this.refreshSession().then((res) => {
|
||
this.sessionDetail(res.list[0]);
|
||
});
|
||
},
|
||
|
||
close() {
|
||
this.visible = false;
|
||
},
|
||
|
||
// 上传前
|
||
onBeforeUpload(file, key) {
|
||
const data = {
|
||
content: {
|
||
[`${key}Url`]: ""
|
||
},
|
||
type: 0,
|
||
contentType: MODES.indexOf(key),
|
||
uid: file.uid,
|
||
loading: true,
|
||
progress: "0%"
|
||
};
|
||
|
||
this.append(data);
|
||
},
|
||
|
||
// 上传中
|
||
onUploadProgress(e, file) {
|
||
let item = this.message.list.find((e) => e.uid == file.uid);
|
||
|
||
if (item) {
|
||
item.progress = e.percent + "%";
|
||
}
|
||
},
|
||
|
||
// 上传成功
|
||
onUploadSuccess(res, file, key) {
|
||
let item = this.message.list.find((e) => e.uid == file.uid);
|
||
|
||
if (item) {
|
||
item.loading = false;
|
||
item.content[`${key}Url`] = res.data;
|
||
|
||
this.sendMessage(item);
|
||
}
|
||
},
|
||
|
||
// 打开会话列表右键菜单
|
||
openSessionCM(e, id, index) {
|
||
this.$crud.openContextMenu(e, {
|
||
list: [
|
||
{
|
||
label: "删除",
|
||
icon: "el-icon-delete",
|
||
callback: (item, done) => {
|
||
this.$service.im.session.delete({
|
||
ids: id
|
||
});
|
||
|
||
this.session.list.splice(index, 1);
|
||
|
||
if (id == this.session.current.id) {
|
||
this.sessionDetail();
|
||
}
|
||
|
||
done();
|
||
}
|
||
}
|
||
]
|
||
});
|
||
},
|
||
|
||
// 刷新会话列表
|
||
refreshSession(params) {
|
||
return this.$service.im.session
|
||
.page({
|
||
...this.session.pagination,
|
||
keyWord: this.session.keyWord,
|
||
params,
|
||
order: "updateTime",
|
||
sort: "desc"
|
||
})
|
||
.then(async (res) => {
|
||
this.session.list = res.list;
|
||
this.session.pagination = res.pagination;
|
||
|
||
return res;
|
||
});
|
||
},
|
||
|
||
// 刷新详情
|
||
async sessionDetail(item) {
|
||
if (item) {
|
||
let { id } = this.session.current || {};
|
||
|
||
if (id != item.id) {
|
||
item.serviceUnreadCount = 0;
|
||
|
||
this.conf.title = `与${item.nickname}聊天中`;
|
||
this.message.loading = true;
|
||
this.message.list = [];
|
||
this.session.current = item;
|
||
|
||
await this.refreshMessage({ page: 1 });
|
||
|
||
this.message.loading = false;
|
||
}
|
||
|
||
this.scrollToBottom();
|
||
} else {
|
||
this.conf.title = "聊天对话框";
|
||
this.message.list = [];
|
||
this.session.current = null;
|
||
}
|
||
},
|
||
|
||
// 刷新消息列表
|
||
refreshMessage(params) {
|
||
return this.$service.im.message
|
||
.page({
|
||
...this.message.pagination,
|
||
...params,
|
||
sessionId: this.session.current.id,
|
||
order: "createTime",
|
||
sort: "desc"
|
||
})
|
||
.then((res) => {
|
||
this.message.pagination = res.pagination;
|
||
this.prepend.apply(this, res.list);
|
||
});
|
||
},
|
||
|
||
// 更新会话消息
|
||
updateSession(data) {
|
||
Object.assign(this.session.current, data);
|
||
},
|
||
|
||
// 搜索关键字
|
||
onSearch() {
|
||
this.refreshSession({ page: 1 });
|
||
},
|
||
|
||
// 加载更多
|
||
onLoadmore() {
|
||
this.refreshMessage({ page: this.message.pagination.page + 1 });
|
||
},
|
||
|
||
// 滚动到底部
|
||
scrollToBottom: debounce(function () {
|
||
this.$nextTick(() => {
|
||
if (this.$refs["scroller"]) {
|
||
this.$refs["scroller"].scrollTo(0, 999999);
|
||
}
|
||
});
|
||
}, 300),
|
||
|
||
// 发送文本内容
|
||
onTextSend() {
|
||
if (this.message.value) {
|
||
if (this.message.value.replace(/\n/g, "") !== "") {
|
||
const data = {
|
||
type: 0,
|
||
contentType: 0,
|
||
content: {
|
||
text: this.message.value
|
||
}
|
||
};
|
||
|
||
this.append(data);
|
||
this.sendMessage(data);
|
||
|
||
this.$nextTick(() => {
|
||
this.message.value = "";
|
||
});
|
||
}
|
||
}
|
||
},
|
||
|
||
// 图片选择
|
||
onImageSelect(res) {
|
||
const data = {
|
||
content: {
|
||
imageUrl: res.data
|
||
},
|
||
type: 0,
|
||
contentType: 1
|
||
};
|
||
this.append(data);
|
||
this.sendMessage(data);
|
||
},
|
||
|
||
// 表情选择
|
||
onEmojiSelect(url) {
|
||
this.emoji.visible = false;
|
||
const data = {
|
||
content: {
|
||
imageUrl: url
|
||
},
|
||
type: 0,
|
||
contentType: 2
|
||
};
|
||
this.append(data);
|
||
this.sendMessage(data);
|
||
},
|
||
|
||
// 视频选择
|
||
onVideoSelect(url) {
|
||
const data = {
|
||
content: {
|
||
videoUrl: url
|
||
},
|
||
type: 0,
|
||
contentType: 4
|
||
};
|
||
this.append(data);
|
||
this.sendMessage(data);
|
||
},
|
||
|
||
// 监听消息
|
||
onMessage(msg) {
|
||
// 回调
|
||
this.$emit("message", this.visible);
|
||
|
||
// 消息通知
|
||
this.notification(msg);
|
||
|
||
try {
|
||
const { contentType, fromId, content, msgId } = JSON.parse(msg);
|
||
|
||
// 是否当前
|
||
const same = this.session.current && this.session.current.userId == fromId;
|
||
|
||
if (same) {
|
||
// 更新消息
|
||
this.updateSession({
|
||
contentType,
|
||
content
|
||
});
|
||
|
||
// 追加消息
|
||
this.append({
|
||
contentType,
|
||
content: JSON.parse(content),
|
||
type: 1
|
||
});
|
||
|
||
// 读消息
|
||
this.$service.im.message.read({
|
||
ids: [msgId],
|
||
session: this.session.current.id
|
||
});
|
||
}
|
||
|
||
// 查找会话
|
||
let item = this.session.list.find((e) => e.userId == fromId);
|
||
|
||
if (item) {
|
||
if (!same) {
|
||
item.serviceUnreadCount += 1;
|
||
}
|
||
// 更新消息
|
||
Object.assign(item, {
|
||
updateTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
||
contentType,
|
||
content
|
||
});
|
||
} else {
|
||
// 刷新会话列表
|
||
this.refreshSession();
|
||
}
|
||
} catch (e) {
|
||
console.error("消息格式异常", e);
|
||
}
|
||
},
|
||
|
||
// 消息通知
|
||
notification(msg) {
|
||
const { _text } = parseContent(JSON.parse(msg));
|
||
|
||
// 播放音乐
|
||
if (this.$refs.sound) {
|
||
this.$refs.sound.play();
|
||
}
|
||
|
||
if (!this.visible) {
|
||
// 页面消息提示
|
||
this.$notify({
|
||
title: "提示",
|
||
message: this.$createElement("span", _text)
|
||
});
|
||
|
||
// 浏览器消息通知
|
||
const NotificationInstance = Notification || window.Notification;
|
||
if (!!NotificationInstance) {
|
||
if (NotificationInstance.permission !== "denied") {
|
||
NotificationInstance.requestPermission((status) => {
|
||
let n = new Notification("COOL-MALL", {
|
||
body: _text,
|
||
icon: "/favicon.ico"
|
||
});
|
||
|
||
setTimeout(() => {
|
||
n.close();
|
||
}, 2000);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// 发送消息
|
||
sendMessage({ contentType, content }) {
|
||
const { id, userId } = this.session.current;
|
||
|
||
// 更新消息
|
||
this.updateSession({
|
||
contentType,
|
||
content
|
||
});
|
||
|
||
this.socket.emit(`user@${userId}`, {
|
||
contentType,
|
||
type: 0,
|
||
content: JSON.stringify(content),
|
||
sessionId: id
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 处理消息数据
|
||
* mode: 消息模式
|
||
* type: 消息类型 0-回复,1-反馈
|
||
* duration: 时常
|
||
* videoUrl: 视频地址
|
||
* videoCoverUrl: 视频封面
|
||
* imageUrl: 图片地址
|
||
* avatarUrl: 头像地址
|
||
* nickName: 昵称
|
||
*/
|
||
handleMessage(e) {
|
||
if (isString(e)) {
|
||
e = JSON.parse(e);
|
||
}
|
||
|
||
if (isString(e.content)) {
|
||
e.content = JSON.parse(e.content);
|
||
}
|
||
|
||
// 昵称
|
||
const nickName = e.type == 0 ? this.userInfo.nickName : this.session.current.nickname;
|
||
// 头像
|
||
const avatarUrl =
|
||
e.type == 0
|
||
? this.userInfo.avatarUrl || require("../static/images/custom-avatar.png")
|
||
: this.session.current.headimgurl;
|
||
|
||
return {
|
||
...e,
|
||
avatarUrl,
|
||
nickName,
|
||
mode: MODES[e.contentType],
|
||
date: dayjs().format("YYYY-MM-DD HH:mm:ss")
|
||
};
|
||
},
|
||
|
||
// 追加数据到开头
|
||
prepend(...data) {
|
||
data.map(this.handleMessage).forEach((e) => {
|
||
this.message.list.unshift(e);
|
||
});
|
||
},
|
||
|
||
// 追加数据到结尾
|
||
append(...data) {
|
||
this.message.list.push(...data.map(this.handleMessage));
|
||
this.scrollToBottom();
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.chat-box__wrap {
|
||
height: 650px;
|
||
min-width: 1000px;
|
||
margin-bottom: 0 !important;
|
||
|
||
.el-dialog__body {
|
||
height: calc(100% - 46px);
|
||
padding: 0;
|
||
|
||
.cl-dialog__container {
|
||
height: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.chat-box {
|
||
display: flex;
|
||
height: 100%;
|
||
background-color: #f7f7f7;
|
||
|
||
&__session {
|
||
height: calc(100% - 10px);
|
||
width: 250px;
|
||
margin: 5px 0 5px 5px;
|
||
border-radius: 5px;
|
||
background-color: #fff;
|
||
|
||
&-search {
|
||
padding: 10px;
|
||
}
|
||
|
||
ul {
|
||
height: calc(100% - 52px);
|
||
overflow: auto;
|
||
|
||
li {
|
||
display: flex;
|
||
list-style: none;
|
||
padding: 10px;
|
||
border-left: 5px solid #fff;
|
||
|
||
.avatar {
|
||
height: 40px;
|
||
width: 40px;
|
||
margin-right: 12px;
|
||
|
||
img {
|
||
display: block;
|
||
height: 100%;
|
||
width: 100%;
|
||
border-radius: 3px;
|
||
background-color: #eee;
|
||
}
|
||
|
||
.el-badge {
|
||
&__content {
|
||
height: 14px;
|
||
line-height: 14px;
|
||
padding: 0 4px;
|
||
background-color: #fa5151;
|
||
border: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.det {
|
||
flex: 1;
|
||
.name {
|
||
font-size: 13px;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.content {
|
||
font-size: 12px;
|
||
margin-top: 5px;
|
||
color: #666;
|
||
}
|
||
|
||
.name,
|
||
.content {
|
||
@include text_ellipsis(1);
|
||
}
|
||
}
|
||
|
||
&.is-active {
|
||
background-color: #eee;
|
||
border-color: $color-main;
|
||
}
|
||
|
||
&:hover {
|
||
background-color: #eee;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
&__detail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
height: 100%;
|
||
padding: 5px;
|
||
box-sizing: border-box;
|
||
|
||
&-container {
|
||
flex: 1;
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
overflow: auto;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
&-more {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
&-footer {
|
||
background-color: #fff;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
}
|
||
}
|
||
|
||
&__message {
|
||
flex: 1;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
&__opbar {
|
||
margin-bottom: 5px;
|
||
ul {
|
||
display: flex;
|
||
li {
|
||
list-style: none;
|
||
margin-right: 10px;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
img {
|
||
height: 26px;
|
||
width: 26px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
&__input {
|
||
position: relative;
|
||
|
||
.el-button {
|
||
position: absolute;
|
||
right: 10px;
|
||
bottom: 10px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|