mirror of
https://github.com/cool-team-official/cool-admin-vue.git
synced 2025-12-15 07:27:54 +00:00
优化客服聊天
This commit is contained in:
parent
9c96709e1e
commit
38d07a3361
@ -1,155 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cl-chat__wrap">
|
<div class="cl-chat__wrap">
|
||||||
<!-- 聊天窗口 -->
|
<!-- 聊天窗口 -->
|
||||||
<cl-dialog :visible.sync="visible" v-bind="conf">
|
<cl-dialog
|
||||||
|
:visible.sync="visible"
|
||||||
|
:title="title"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
:props="conf"
|
||||||
|
>
|
||||||
<div class="cl-chat">
|
<div class="cl-chat">
|
||||||
<!-- 会话区域 -->
|
<!-- 会话区域 -->
|
||||||
<div class="cl-chat__session">
|
<chat-session />
|
||||||
<div class="cl-chat__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="cl-chat__session-list scroller1" v-if="sessionList.length > 0">
|
|
||||||
<li
|
|
||||||
class="cl-chat__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 class="cl-chat__session-empty" v-else>
|
|
||||||
没有搜索到内容...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 会话详情 -->
|
<!-- 会话详情 -->
|
||||||
<div class="cl-chat__detail">
|
<div class="cl-chat__detail" v-if="session">
|
||||||
<template v-if="session.current">
|
<chat-message />
|
||||||
<div
|
<chat-input />
|
||||||
class="cl-chat__detail-container scroller1"
|
|
||||||
ref="scroller"
|
|
||||||
v-loading="message.loading"
|
|
||||||
>
|
|
||||||
<!-- 加载更多 -->
|
|
||||||
<div class="cl-chat__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="cl-chat__detail-footer">
|
|
||||||
<!-- 工具栏 -->
|
|
||||||
<div class="cl-chat__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 hidden>
|
|
||||||
<cl-upload
|
|
||||||
accept="image/*"
|
|
||||||
list-type
|
|
||||||
:on-success="onImageSelect"
|
|
||||||
>
|
|
||||||
<img src="../static/images/image.png" alt="" />
|
|
||||||
</cl-upload>
|
|
||||||
</li>
|
|
||||||
<!-- 视频上传 -->
|
|
||||||
<li hidden>
|
|
||||||
<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="cl-chat__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>
|
||||||
</div>
|
</div>
|
||||||
</cl-dialog>
|
</cl-dialog>
|
||||||
@ -164,22 +30,36 @@
|
|||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { mapGetters } from "vuex";
|
import { mapGetters } from "vuex";
|
||||||
import { isString, debounce } from "cl-admin/utils";
|
|
||||||
import io from "socket.io-client";
|
|
||||||
import { socketUrl } from "@/config/env";
|
|
||||||
import Emoji from "./emoji";
|
|
||||||
import Message from "./message";
|
|
||||||
import { parseContent } from "../utils";
|
import { parseContent } from "../utils";
|
||||||
|
|
||||||
|
import io from "socket.io-client";
|
||||||
|
import { socketUrl } from "@/config/env";
|
||||||
|
|
||||||
|
import Session from "./session";
|
||||||
|
import Message from "./message";
|
||||||
|
import Input from "./input";
|
||||||
|
import eventBus from "../utils/event-bus";
|
||||||
|
|
||||||
// 消息模式
|
// 消息模式
|
||||||
const MODES = ["text", "image", "emoji", "voice", "video"];
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "cl-chat",
|
name: "cl-chat",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Message,
|
"chat-session": Session,
|
||||||
Emoji
|
"chat-message": Message,
|
||||||
|
"chat-input": Input
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "650px"
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: "1000px"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -187,55 +67,25 @@ export default {
|
|||||||
visible: false,
|
visible: false,
|
||||||
socket: null,
|
socket: null,
|
||||||
conf: {
|
conf: {
|
||||||
title: "聊天对话框",
|
modal: true,
|
||||||
height: "650px",
|
customClass: "cl-chat__dialog",
|
||||||
width: "1000px",
|
"append-to-body": true,
|
||||||
props: {
|
"close-on-click-modal": false
|
||||||
modal: true,
|
|
||||||
customClass: "cl-chat__dialog",
|
|
||||||
"append-to-body": true,
|
|
||||||
"close-on-click-modal": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
provide() {
|
||||||
...mapGetters(["userInfo", "token"]),
|
return {
|
||||||
|
socket: this.socket
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
sessionList() {
|
computed: {
|
||||||
return this.session.list
|
...mapGetters(["token", "session", "sessionList"]),
|
||||||
.map(e => {
|
|
||||||
let { _text } = parseContent(e);
|
title() {
|
||||||
e.lastMessage = _text;
|
return this.session ? `与 ${this.session.nickname} 聊天中` : "聊天对话框";
|
||||||
return e;
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.updateTime < b.updateTime ? 1 : -1;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -264,227 +114,12 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
open() {
|
open() {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
|
|
||||||
this.refreshSession().then(res => {
|
|
||||||
this.sessionDetail(res.list[0]);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.visible = false;
|
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: (_, 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(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({
|
|
||||||
top: 99999,
|
|
||||||
behavior: "smooth"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 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) {
|
onMessage(msg) {
|
||||||
// 回调
|
// 回调
|
||||||
@ -497,17 +132,17 @@ export default {
|
|||||||
const { contentType, fromId, content, msgId } = JSON.parse(msg);
|
const { contentType, fromId, content, msgId } = JSON.parse(msg);
|
||||||
|
|
||||||
// 是否当前
|
// 是否当前
|
||||||
const same = this.session.current && this.session.current.userId == fromId;
|
const same = this.session && this.session.userId == fromId;
|
||||||
|
|
||||||
if (same) {
|
if (same) {
|
||||||
// 更新消息
|
// 更新消息
|
||||||
this.updateSession({
|
this.$store.commit("UPDATE_SESSION", {
|
||||||
contentType,
|
contentType,
|
||||||
content
|
content
|
||||||
});
|
});
|
||||||
|
|
||||||
// 追加消息
|
// 追加消息
|
||||||
this.append({
|
eventBus.$emit("message-append", {
|
||||||
contentType,
|
contentType,
|
||||||
content: JSON.parse(content),
|
content: JSON.parse(content),
|
||||||
type: 1
|
type: 1
|
||||||
@ -521,7 +156,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查找会话
|
// 查找会话
|
||||||
let item = this.session.list.find(e => e.userId == fromId);
|
const item = this.sessionList.find(e => e.userId == fromId);
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
if (!same) {
|
if (!same) {
|
||||||
@ -535,7 +170,7 @@ export default {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 刷新会话列表
|
// 刷新会话列表
|
||||||
this.refreshSession();
|
eventBus.$emit("session-refresh");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("消息格式异常", e);
|
console.error("消息格式异常", e);
|
||||||
@ -575,76 +210,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
sendMessage({ contentType, content }) {
|
|
||||||
const { id, userId } = this.session.current;
|
|
||||||
|
|
||||||
// 更新消息
|
|
||||||
this.updateSession({
|
|
||||||
contentType,
|
|
||||||
content
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.socket) {
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -671,90 +236,6 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #f7f7f7;
|
background-color: #f7f7f7;
|
||||||
|
|
||||||
&__session {
|
|
||||||
height: calc(100% - 10px);
|
|
||||||
width: 250px;
|
|
||||||
margin: 5px 0 5px 5px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #fff;
|
|
||||||
|
|
||||||
&-search {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-list {
|
|
||||||
height: calc(100% - 52px);
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
list-style: none;
|
|
||||||
padding: 10px;
|
|
||||||
border-left: 5px solid #fff;
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
margin-right: 12px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
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 {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background-color: #eee;
|
|
||||||
border-color: $color-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #eee;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-empty {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__detail {
|
&__detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -762,63 +243,6 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
box-sizing: border-box;
|
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>
|
</style>
|
||||||
|
|||||||
265
src/cool/modules/chat/components/input.vue
Normal file
265
src/cool/modules/chat/components/input.vue
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cl-chat-input">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="cl-chat-input__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 hidden>
|
||||||
|
<cl-upload accept="image/*" list-type :on-success="onImageSelect">
|
||||||
|
<img src="../static/images/image.png" alt="" />
|
||||||
|
</cl-upload>
|
||||||
|
</li>
|
||||||
|
<!-- 视频上传 -->
|
||||||
|
<li hidden>
|
||||||
|
<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="cl-chat-input__content">
|
||||||
|
<el-input
|
||||||
|
v-model="value"
|
||||||
|
placeholder="请描述您想咨询的问题"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
@keyup.enter.native="onTextSend"
|
||||||
|
></el-input>
|
||||||
|
|
||||||
|
<el-button type="primary" size="mini" :disabled="!value" @click="onTextSend"
|
||||||
|
>发送</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
import Emoji from "./emoji";
|
||||||
|
import eventBus from "../utils/event-bus";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Emoji
|
||||||
|
},
|
||||||
|
|
||||||
|
inject: ["socket"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
value: "",
|
||||||
|
emoji: {
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["session"])
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// 上传前
|
||||||
|
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) {
|
||||||
|
const item = this.message.list.find(e => e.uid == file.uid);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
item.progress = e.percent + "%";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 上传成功
|
||||||
|
onUploadSuccess(res, file, key) {
|
||||||
|
const item = this.message.list.find(e => e.uid == file.uid);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
item.loading = false;
|
||||||
|
item.content[`${key}Url`] = res.data;
|
||||||
|
|
||||||
|
this.send(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发送文本内容
|
||||||
|
onTextSend() {
|
||||||
|
if (this.value) {
|
||||||
|
if (this.value.replace(/\n/g, "") !== "") {
|
||||||
|
const data = {
|
||||||
|
type: 0,
|
||||||
|
contentType: 0,
|
||||||
|
content: {
|
||||||
|
text: this.value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.send(data, true);
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.value = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 图片选择
|
||||||
|
onImageSelect(res) {
|
||||||
|
this.send(
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
imageUrl: res.data
|
||||||
|
},
|
||||||
|
type: 0,
|
||||||
|
contentType: 1
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 表情选择
|
||||||
|
onEmojiSelect(url) {
|
||||||
|
this.emoji.visible = false;
|
||||||
|
this.send(
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
imageUrl: url
|
||||||
|
},
|
||||||
|
type: 0,
|
||||||
|
contentType: 2
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 视频选择
|
||||||
|
onVideoSelect(url) {
|
||||||
|
this.send(
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
videoUrl: url
|
||||||
|
},
|
||||||
|
type: 0,
|
||||||
|
contentType: 4
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
send(data, isAppend) {
|
||||||
|
const { id, userId } = this.session;
|
||||||
|
|
||||||
|
// 更新消息
|
||||||
|
// this.updateSession({
|
||||||
|
// contentType,
|
||||||
|
// content
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.emit(`user@${userId}`, {
|
||||||
|
contentType: data.contentType,
|
||||||
|
type: 0,
|
||||||
|
content: JSON.stringify(data.content),
|
||||||
|
sessionId: id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isAppend) {
|
||||||
|
this.append(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAppend) {
|
||||||
|
this.append(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 追加消息
|
||||||
|
append(data) {
|
||||||
|
eventBus.$emit("message-append", data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cl-chat-input {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,100 +1,133 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-box-message">
|
<div class="cl-chat-message" v-loading="loading" element-loading-text="消息加载中">
|
||||||
<div
|
<div
|
||||||
class="chat-box-message__item"
|
class="cl-chat-message__scroller scroller1"
|
||||||
v-for="item in flist"
|
ref="scroller"
|
||||||
:key="item.id || item.uid"
|
:style="{
|
||||||
:class="[item.type == 0 ? `is-right` : `is-left`, `is-${item.mode}`]"
|
opacity: visible ? 1 : 0
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div class="date" v-if="item._date">
|
<!-- 加载更多 -->
|
||||||
<span>{{ item._date }}</span>
|
<div class="cl-chat-message__more" v-if="list.length > 0">
|
||||||
|
<el-button round size="mini" :loading="loading" @click="onLoadmore"
|
||||||
|
>加载更多</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main">
|
<!-- 消息列表 -->
|
||||||
<div class="avatar" @tap="toUserDetail(item)">
|
<div class="cl-chat-message__list">
|
||||||
<img :src="item.avatarUrl" />
|
<div
|
||||||
</div>
|
class="cl-chat-message__item"
|
||||||
|
v-for="item in messageList"
|
||||||
|
:key="item.id || item.uid"
|
||||||
|
:class="[item.type == 0 ? `is-right` : `is-left`, `is-${item.mode}`]"
|
||||||
|
>
|
||||||
|
<div class="date" v-if="item._date">
|
||||||
|
<span>{{ item._date }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="det">
|
<div class="main">
|
||||||
<span class="name">{{ item.nickName }}</span>
|
<div class="avatar" @tap="toUserDetail(item)">
|
||||||
|
<img :src="item.avatarUrl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="det">
|
||||||
class="content"
|
<span class="name">{{ item.nickName }}</span>
|
||||||
v-loading="item.loading"
|
|
||||||
:element-loading-text="item.progress"
|
|
||||||
@click="tapItem(item)"
|
|
||||||
>
|
|
||||||
<!-- 文本 -->
|
|
||||||
<template v-if="item.mode === 'text'">{{ item.content.text }}</template>
|
|
||||||
|
|
||||||
<!-- 图片 -->
|
<div
|
||||||
<template v-else-if="item.mode === 'image'">
|
class="content"
|
||||||
<el-image
|
v-loading="item.loading"
|
||||||
:key="item.uid"
|
:element-loading-text="item.progress"
|
||||||
:src="item.content.imageUrl"
|
@click="onTap(item)"
|
||||||
:preview-src-list="[item.content.imageUrl]"
|
>
|
||||||
></el-image>
|
<!-- 文本 -->
|
||||||
</template>
|
<template v-if="item.mode === 'text'">{{
|
||||||
|
item.content.text
|
||||||
|
}}</template>
|
||||||
|
|
||||||
<!-- 表情 -->
|
<!-- 图片 -->
|
||||||
<template v-else-if="item.mode === 'emoji'">
|
<template v-else-if="item.mode === 'image'">
|
||||||
<img :src="item.content.imageUrl" />
|
<el-image
|
||||||
</template>
|
:key="item.uid"
|
||||||
|
:src="item.content.imageUrl"
|
||||||
|
:preview-src-list="[item.content.imageUrl]"
|
||||||
|
></el-image>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 语音 -->
|
<!-- 表情 -->
|
||||||
<template v-else-if="item.mode === 'voice'">
|
<template v-else-if="item.mode === 'emoji'">
|
||||||
<icon-voice :play="item.isPlay"></icon-voice>
|
<img :src="item.content.imageUrl" />
|
||||||
<span class="duration">{{ item.content.duration | duration }}"</span>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 视频 -->
|
<!-- 语音 -->
|
||||||
<template v-else-if="item.mode === 'video'">
|
<template v-else-if="item.mode === 'voice'">
|
||||||
<div class="item">
|
<icon-voice :play="item.isPlay"></icon-voice>
|
||||||
<video
|
<span class="duration"
|
||||||
:poster="item.content.videoUrl | video_poster"
|
>{{ item.content.duration | duration }}"</span
|
||||||
:src="item.content.videoUrl"
|
>
|
||||||
controls
|
</template>
|
||||||
></video>
|
|
||||||
|
<!-- 视频 -->
|
||||||
|
<template v-else-if="item.mode === 'video'">
|
||||||
|
<div class="item">
|
||||||
|
<video
|
||||||
|
:poster="item.content.videoUrl | video_poster"
|
||||||
|
:src="item.content.videoUrl"
|
||||||
|
controls
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 未知 -->
|
||||||
|
<template v-else>
|
||||||
|
<span>待扩展消息类型</span>
|
||||||
|
<i class="el-icon-warning-outline"></i>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<!-- 未知 -->
|
|
||||||
<template v-else>
|
|
||||||
<span>待扩展消息类型</span>
|
|
||||||
<i class="el-icon-warning-outline"></i>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- voice -->
|
<!-- voice -->
|
||||||
<div class="voice">
|
<div class="voice">
|
||||||
<audio style="display: none" ref="voice" :src="voice.url" controls></audio>
|
<audio style="display: none" ref="voice" :src="voice.url" controls></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
import { isString } from "cl-admin/utils";
|
||||||
|
import eventBus from "../utils/event-bus";
|
||||||
import IconVoice from "./icon-voice";
|
import IconVoice from "./icon-voice";
|
||||||
|
|
||||||
|
// 消息类型
|
||||||
|
const ModeList = ["text", "image", "emoji", "voice", "video"];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
IconVoice
|
IconVoice
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
|
||||||
list: Array
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
loading: false,
|
||||||
|
visible: false,
|
||||||
|
list: [],
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
total: 0
|
||||||
|
},
|
||||||
player: {},
|
player: {},
|
||||||
voice: {
|
voice: {
|
||||||
url: "",
|
url: "",
|
||||||
timer: null
|
timer: null
|
||||||
}
|
},
|
||||||
|
refreshRd: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -104,6 +137,60 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["userInfo", "session"]),
|
||||||
|
|
||||||
|
messageList() {
|
||||||
|
let date = "";
|
||||||
|
|
||||||
|
return this.list.map(e => {
|
||||||
|
// 时间间隔
|
||||||
|
e._date = date
|
||||||
|
? dayjs(e.createTime).isBefore(dayjs(date).add(1, "minute"))
|
||||||
|
? ""
|
||||||
|
: e.createTime
|
||||||
|
: e.createTime;
|
||||||
|
|
||||||
|
// 发送时间
|
||||||
|
date = e.createTime;
|
||||||
|
|
||||||
|
// 解析内容
|
||||||
|
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.nickname;
|
||||||
|
|
||||||
|
// 解析头像
|
||||||
|
const avatarUrl =
|
||||||
|
e.type == 0
|
||||||
|
? this.userInfo.avatarUrl || require("../static/images/custom-avatar.png")
|
||||||
|
: this.session.headimgurl;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
avatarUrl,
|
||||||
|
nickName,
|
||||||
|
mode: ModeList[e.contentType]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
eventBus.$on("message-refresh", this.refresh);
|
||||||
|
eventBus.$on("message-append", this.append);
|
||||||
|
|
||||||
|
if (this.session) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
clearTimeout(this.voice.timer);
|
clearTimeout(this.voice.timer);
|
||||||
|
|
||||||
@ -112,26 +199,10 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
flist() {
|
|
||||||
let date = "";
|
|
||||||
|
|
||||||
return this.list.map(e => {
|
|
||||||
e._date = date
|
|
||||||
? dayjs(e.createTime).isBefore(dayjs(date).add(1, "minute"))
|
|
||||||
? ""
|
|
||||||
: e.createTime
|
|
||||||
: e.createTime;
|
|
||||||
|
|
||||||
date = e.createTime;
|
|
||||||
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
tapItem(item) {
|
// 点击
|
||||||
|
onTap(item) {
|
||||||
|
// 播放语音
|
||||||
if (item.mode == "voice") {
|
if (item.mode == "voice") {
|
||||||
this.list.map(e => {
|
this.list.map(e => {
|
||||||
this.$set(e, "isPlay", e.id == item.id ? e.isPlay : false);
|
this.$set(e, "isPlay", e.id == item.id ? e.isPlay : false);
|
||||||
@ -156,13 +227,114 @@ export default {
|
|||||||
item.isPlay = false;
|
item.isPlay = false;
|
||||||
}, item.content.duration);
|
}, item.content.duration);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
refresh(params) {
|
||||||
|
// 请求随机值
|
||||||
|
const rd = (this.refreshRd = Math.random());
|
||||||
|
|
||||||
|
// 请求参数
|
||||||
|
const data = {
|
||||||
|
...this.pagination,
|
||||||
|
...params,
|
||||||
|
sessionId: this.session.id,
|
||||||
|
order: "createTime",
|
||||||
|
sort: "desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 首页处理
|
||||||
|
if (data.page === 1) {
|
||||||
|
this.loading = true;
|
||||||
|
this.visible = false;
|
||||||
|
this.list = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成
|
||||||
|
const done = () => {
|
||||||
|
this.loading = false;
|
||||||
|
this.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$service.im.message
|
||||||
|
.page(data)
|
||||||
|
.then(res => {
|
||||||
|
// 防止脏数据
|
||||||
|
if (rd != this.refreshRd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
this.pagination = res.pagination;
|
||||||
|
// 追加数据
|
||||||
|
this.prepend.apply(this, res.list);
|
||||||
|
|
||||||
|
if (data.page === 1) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
|
||||||
|
// 首次滚动隐藏
|
||||||
|
setTimeout(done, 0);
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.$message.error(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
onLoadmore() {
|
||||||
|
this.refresh({ page: this.pagination.page + 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
scrollToBottom() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs["scroller"]) {
|
||||||
|
this.$refs["scroller"].scrollTo({
|
||||||
|
top: 99999,
|
||||||
|
behavior: this.visible ? "smooth" : "auto"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 追加数据到开头
|
||||||
|
prepend(...data) {
|
||||||
|
this.list.unshift(...data.reverse());
|
||||||
|
},
|
||||||
|
|
||||||
|
// 追加数据到结尾
|
||||||
|
append(...data) {
|
||||||
|
this.list.push(...data);
|
||||||
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.chat-box-message {
|
.cl-chat-message {
|
||||||
|
height: calc(100% - 5px);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&__scroller {
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px 0px 5px 5px;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
|||||||
241
src/cool/modules/chat/components/session.vue
Normal file
241
src/cool/modules/chat/components/session.vue
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cl-chat-session">
|
||||||
|
<div class="cl-chat-session__search">
|
||||||
|
<el-input
|
||||||
|
v-model="keyWord"
|
||||||
|
placeholder="搜索"
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
@clear="onSearch"
|
||||||
|
@keyup.enter.native="onSearch"
|
||||||
|
></el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 会话列表 -->
|
||||||
|
<ul class="cl-chat-session__list scroller1" v-loading="loading">
|
||||||
|
<li
|
||||||
|
class="cl-chat-session__item"
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="index"
|
||||||
|
:class="{
|
||||||
|
'is-active': session ? item.id == session.id : false
|
||||||
|
}"
|
||||||
|
@click="toDetail(item)"
|
||||||
|
@contextmenu.stop.prevent="openCM($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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
import { parseContent } from "../utils";
|
||||||
|
import eventBus from "../utils/event-bus";
|
||||||
|
import { ContextMenu } from "cl-admin-crud";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 100,
|
||||||
|
total: 0
|
||||||
|
},
|
||||||
|
keyWord: ""
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["sessionList", "session"]),
|
||||||
|
|
||||||
|
list() {
|
||||||
|
return this.sessionList
|
||||||
|
.map(e => {
|
||||||
|
const { _text } = parseContent(e);
|
||||||
|
e.lastMessage = _text;
|
||||||
|
return e;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.updateTime < b.updateTime ? 1 : -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
eventBus.$on("session-refresh", this.refresh);
|
||||||
|
this.refresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// 右键菜单
|
||||||
|
openCM(e, id, index) {
|
||||||
|
ContextMenu.open(e, {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
label: "删除",
|
||||||
|
icon: "el-icon-delete",
|
||||||
|
callback: (_, done) => {
|
||||||
|
this.$service.im.session.delete({
|
||||||
|
ids: id
|
||||||
|
});
|
||||||
|
|
||||||
|
this.list.splice(index, 1);
|
||||||
|
|
||||||
|
if (id == this.session.id) {
|
||||||
|
this.toDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
refresh(params) {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.$service.im.session
|
||||||
|
.page({
|
||||||
|
...this.pagination,
|
||||||
|
keyWord: this.keyWord,
|
||||||
|
params,
|
||||||
|
order: "updateTime",
|
||||||
|
sort: "desc"
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.$store.commit("SET_SESSION_LIST", res.list);
|
||||||
|
this.pagination = res.pagination;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.$message.error(err);
|
||||||
|
})
|
||||||
|
.done(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 搜索关键字
|
||||||
|
onSearch() {
|
||||||
|
this.refresh({ page: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 会话详情
|
||||||
|
toDetail(item) {
|
||||||
|
if (item) {
|
||||||
|
if (!this.session || this.session.id != item.id) {
|
||||||
|
this.$store.commit("SET_SESSION", item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$store.commit("CLEAR_SESSION");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cl-chat-session {
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
width: 250px;
|
||||||
|
margin: 5px 0 5px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
height: calc(100% - 52px);
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 5px solid #fff;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
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 {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: #eee;
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import components from "./components";
|
import components from "./components";
|
||||||
import service from "./service";
|
import service from "./service";
|
||||||
|
import store from "./store";
|
||||||
|
|
||||||
export default { components, service };
|
export default { components, service, store };
|
||||||
|
|||||||
5
src/cool/modules/chat/store/index.js
Normal file
5
src/cool/modules/chat/store/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import session from "./session";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
session
|
||||||
|
};
|
||||||
44
src/cool/modules/chat/store/session.js
Normal file
44
src/cool/modules/chat/store/session.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import eventBus from "../utils/event-bus";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
state: {
|
||||||
|
list: [],
|
||||||
|
current: null
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
// 当前会话
|
||||||
|
session: state => state.current,
|
||||||
|
// 会话列表
|
||||||
|
sessionList: state => state.list
|
||||||
|
},
|
||||||
|
|
||||||
|
mutations: {
|
||||||
|
// 设置会话信息
|
||||||
|
SET_SESSION(state, data) {
|
||||||
|
state.current = data;
|
||||||
|
state.current.serviceUnreadCount = 0;
|
||||||
|
eventBus.$emit("message-refresh", { page: 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空会话信息
|
||||||
|
CLEAR_SESSION(state) {
|
||||||
|
state.session = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新会话信息
|
||||||
|
UPDATE_SESSION(state, data) {
|
||||||
|
Object.assign(state.current, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置会话列表
|
||||||
|
SET_SESSION_LIST(state, data) {
|
||||||
|
state.list = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空会话列表
|
||||||
|
CLEAR_SESSION_LIST(state) {
|
||||||
|
state.list = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
2
src/cool/modules/chat/utils/event-bus.js
Normal file
2
src/cool/modules/chat/utils/event-bus.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import Vue from "vue";
|
||||||
|
export default new Vue();
|
||||||
@ -48,6 +48,10 @@ Mock.mock("/im/session/unreadCount", "get", options => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Mock.setup({
|
||||||
|
timeout: "500-1000"
|
||||||
|
});
|
||||||
|
|
||||||
Mock.mock("/im/message/page", "post", options => {
|
Mock.mock("/im/message/page", "post", options => {
|
||||||
const data = Mock.mock({
|
const data = Mock.mock({
|
||||||
"list|20": [
|
"list|20": [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user