123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807 |
- <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: (_, 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>
|