cool-admin-vue/cool/modules/chat/components/box.vue
2021-02-28 22:24:54 +08:00

808 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>