优化客服聊天

This commit is contained in:
icssoa 2021-03-18 01:24:34 +08:00
parent 9c96709e1e
commit 38d07a3361
9 changed files with 870 additions and 712 deletions

View File

@ -1,155 +1,21 @@
<template>
<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__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>
<chat-session />
<!-- 会话详情 -->
<div class="cl-chat__detail">
<template v-if="session.current">
<div
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 class="cl-chat__detail" v-if="session">
<chat-message />
<chat-input />
</div>
</div>
</cl-dialog>
@ -164,22 +30,36 @@
<script>
import dayjs from "dayjs";
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 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 {
name: "cl-chat",
components: {
Message,
Emoji
"chat-session": Session,
"chat-message": Message,
"chat-input": Input
},
props: {
height: {
type: String,
default: "650px"
},
width: {
type: String,
default: "1000px"
}
},
data() {
@ -187,55 +67,25 @@ export default {
visible: false,
socket: null,
conf: {
title: "聊天对话框",
height: "650px",
width: "1000px",
props: {
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
modal: true,
customClass: "cl-chat__dialog",
"append-to-body": true,
"close-on-click-modal": false
}
};
},
computed: {
...mapGetters(["userInfo", "token"]),
provide() {
return {
socket: this.socket
};
},
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;
});
computed: {
...mapGetters(["token", "session", "sessionList"]),
title() {
return this.session ? `${this.session.nickname} 聊天中` : "聊天对话框";
}
},
@ -264,227 +114,12 @@ export default {
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: (_, 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) {
//
@ -497,17 +132,17 @@ export default {
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) {
//
this.updateSession({
this.$store.commit("UPDATE_SESSION", {
contentType,
content
});
//
this.append({
eventBus.$emit("message-append", {
contentType,
content: JSON.parse(content),
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 (!same) {
@ -535,7 +170,7 @@ export default {
});
} else {
//
this.refreshSession();
eventBus.$emit("session-refresh");
}
} catch (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%;
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 {
display: flex;
flex-direction: column;
@ -762,63 +243,6 @@ export default {
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>

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

View File

@ -1,100 +1,133 @@
<template>
<div class="chat-box-message">
<div class="cl-chat-message" v-loading="loading" element-loading-text="消息加载中">
<div
class="chat-box-message__item"
v-for="item in flist"
:key="item.id || item.uid"
:class="[item.type == 0 ? `is-right` : `is-left`, `is-${item.mode}`]"
class="cl-chat-message__scroller scroller1"
ref="scroller"
:style="{
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 class="main">
<div class="avatar" @tap="toUserDetail(item)">
<img :src="item.avatarUrl" />
</div>
<!-- 消息列表 -->
<div class="cl-chat-message__list">
<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">
<span class="name">{{ item.nickName }}</span>
<div class="main">
<div class="avatar" @tap="toUserDetail(item)">
<img :src="item.avatarUrl" />
</div>
<div
class="content"
v-loading="item.loading"
:element-loading-text="item.progress"
@click="tapItem(item)"
>
<!-- 文本 -->
<template v-if="item.mode === 'text'">{{ item.content.text }}</template>
<div class="det">
<span class="name">{{ item.nickName }}</span>
<!-- 图片 -->
<template v-else-if="item.mode === 'image'">
<el-image
:key="item.uid"
:src="item.content.imageUrl"
:preview-src-list="[item.content.imageUrl]"
></el-image>
</template>
<div
class="content"
v-loading="item.loading"
:element-loading-text="item.progress"
@click="onTap(item)"
>
<!-- 文本 -->
<template v-if="item.mode === 'text'">{{
item.content.text
}}</template>
<!-- 表情 -->
<template v-else-if="item.mode === 'emoji'">
<img :src="item.content.imageUrl" />
</template>
<!-- 图片 -->
<template v-else-if="item.mode === 'image'">
<el-image
:key="item.uid"
:src="item.content.imageUrl"
:preview-src-list="[item.content.imageUrl]"
></el-image>
</template>
<!-- 语音 -->
<template v-else-if="item.mode === 'voice'">
<icon-voice :play="item.isPlay"></icon-voice>
<span class="duration">{{ item.content.duration | duration }}"</span>
</template>
<!-- 表情 -->
<template v-else-if="item.mode === 'emoji'">
<img :src="item.content.imageUrl" />
</template>
<!-- 视频 -->
<template v-else-if="item.mode === 'video'">
<div class="item">
<video
:poster="item.content.videoUrl | video_poster"
:src="item.content.videoUrl"
controls
></video>
<!-- 语音 -->
<template v-else-if="item.mode === 'voice'">
<icon-voice :play="item.isPlay"></icon-voice>
<span class="duration"
>{{ item.content.duration | duration }}"</span
>
</template>
<!-- 视频 -->
<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>
</template>
<!-- 未知 -->
<template v-else>
<span>待扩展消息类型</span>
<i class="el-icon-warning-outline"></i>
</template>
</div>
</div>
</div>
</div>
</div>
<!-- voice -->
<div class="voice">
<audio style="display: none" ref="voice" :src="voice.url" controls></audio>
<!-- voice -->
<div class="voice">
<audio style="display: none" ref="voice" :src="voice.url" controls></audio>
</div>
</div>
</div>
</div>
</template>
<script>
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";
//
const ModeList = ["text", "image", "emoji", "voice", "video"];
export default {
components: {
IconVoice
},
props: {
list: Array
},
data() {
return {
loading: false,
visible: false,
list: [],
pagination: {
page: 1,
size: 20,
total: 0
},
player: {},
voice: {
url: "",
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() {
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: {
tapItem(item) {
//
onTap(item) {
//
if (item.mode == "voice") {
this.list.map(e => {
this.$set(e, "isPlay", e.id == item.id ? e.isPlay : false);
@ -156,13 +227,114 @@ export default {
item.isPlay = false;
}, 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>
<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 {
margin-bottom: 20px;

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

View File

@ -1,4 +1,5 @@
import components from "./components";
import service from "./service";
import store from "./store";
export default { components, service };
export default { components, service, store };

View File

@ -0,0 +1,5 @@
import session from "./session";
export default {
session
};

View 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 = [];
}
}
};

View File

@ -0,0 +1,2 @@
import Vue from "vue";
export default new Vue();

View File

@ -48,6 +48,10 @@ Mock.mock("/im/session/unreadCount", "get", options => {
};
});
Mock.setup({
timeout: "500-1000"
});
Mock.mock("/im/message/page", "post", options => {
const data = Mock.mock({
"list|20": [