Commit cf20b3e5 by Sixong.Zhu

客服支持批量接待,支持2min内撤回功能

parent ef8432c0
...@@ -131,7 +131,7 @@ export default class ChatContainer extends Vue { ...@@ -131,7 +131,7 @@ export default class ChatContainer extends Vue {
.chat-content-wrap { .chat-content-wrap {
display: inline-block; display: inline-block;
width: 75%; width: 75%;
height: calc(100% - 60px); height: 100%;
box-sizing: border-box; box-sizing: border-box;
vertical-align: top; vertical-align: top;
} }
......
...@@ -21,15 +21,11 @@ ...@@ -21,15 +21,11 @@
round round
size="small" size="small"
v-if="!isChatMember" v-if="!isChatMember"
type="primary"
>我要接待</el-button >我要接待</el-button
> >
<el-button <el-button class="button" @click="showAddMember" round size="small"
class="button" >添加客服</el-button
@click="exitChat"
round
size="small"
v-if="isChatMember"
>退出会话</el-button
> >
<el-button <el-button
class="button" class="button"
...@@ -37,10 +33,17 @@ ...@@ -37,10 +33,17 @@
round round
size="small" size="small"
v-if="isChatMember && operatorType > 25" v-if="isChatMember && operatorType > 25"
type="warning"
>结束接待</el-button >结束接待</el-button
> >
<el-button class="button" @click="showAddMember" round size="small" <el-button
>添加客服</el-button class="button"
@click="exitChat"
round
size="small"
v-if="isChatMember"
type="danger"
>退出会话</el-button
> >
<i <i
v-if="close && isSingleChat" v-if="close && isSingleChat"
...@@ -133,17 +136,25 @@ export default class ChatTitle extends Vue { ...@@ -133,17 +136,25 @@ export default class ChatTitle extends Vue {
} }
} }
private noop() {
return 1;
}
private async exitChat() { private async exitChat() {
try { this.$confirm("确认要退出此会话?")
if (+this.operatorType === ChatRole.Default) { .then(async () => {
await this._userExitChat(); try {
} else if (+this.operatorType > ChatRole.Default) { if (+this.operatorType === ChatRole.Default) {
await this._csExitChat(); await this._userExitChat();
} } else if (+this.operatorType > ChatRole.Default) {
this.hideChat(); await this._csExitChat();
} catch (error) { }
console.error(error); this.hideChat();
} } catch (error) {
console.error(error);
}
})
.catch(this.noop);
} }
private async startReception() { private async startReception() {
......
import { MessageType } from "@/customer-service/model";
export function parserMessage(type: string, rawMsg: string) { export function parserMessage(type: string, rawMsg: string) {
if (!type) return ""; if (!type) return "";
if (!rawMsg) return ""; if (!rawMsg) return "";
const msg = JSON.parse(rawMsg); const msg = JSON.parse(rawMsg);
if (type === "text") { if (type === MessageType.Text) {
return msg.text; return msg.text;
} else if (type === "image") { } else if (type === MessageType.Image) {
return `[图片]`; return `[图片]`;
} else if (type === "file") { } else if (type === MessageType.File) {
return `[文件]`; return `[文件]`;
} else if (type === MessageType.Withdraw) {
return `[您撤回了一条消息]`;
} else { } else {
return `[系统自动回复]`; return `[系统自动回复]`;
} }
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
:data="item" :data="item"
:shape="shape" :shape="shape"
@open="open" @open="open"
@withdraw="refresh"
/> />
</div> </div>
</template> </template>
...@@ -353,6 +354,10 @@ export default class MessageList extends Vue { ...@@ -353,6 +354,10 @@ export default class MessageList extends Vue {
} }
return v; return v;
} }
private refresh(msg: number) {
this.fetchNewMsg();
}
} }
</script> </script>
......
<template> <template>
<div <div
class="message-con d-flex align-items-center" class="message-con d-flex align-items-center"
:class="isMyMessage ? 'my-message flex-row-reverse' : ''" :class="{
'my-message flex-row-reverse': isMyMessage,
'justify-content-center': isWithdrawMessage,
}"
> >
<div class="msg-content" :class="{ 'algin-left': !isMyMessage }"> <div class="msg-content" :class="{ 'algin-left': !isMyMessage }">
<div <div
class="msg-name no-selection" class="msg-name no-selection"
:class="{ 'algin-left': !isMyMessage }" :class="{ 'algin-left': !isMyMessage }"
v-if="!isWithdrawMessage"
> >
{{ userName }} {{ userName }}
</div> </div>
...@@ -85,6 +89,12 @@ ...@@ -85,6 +89,12 @@
> >
<video-player-icon @click.native="openFile"></video-player-icon> <video-player-icon @click.native="openFile"></video-player-icon>
</div> </div>
<div
class="msg-detail withdraw-message"
v-else-if="messageType === 'withdraw'"
>
您撤回了一条消息
</div>
<!-- Text --> <!-- Text -->
<div <div
class="msg-detail inline-text" class="msg-detail inline-text"
...@@ -115,7 +125,7 @@ ...@@ -115,7 +125,7 @@
></i> ></i>
<i class="el-icon-loading" v-else-if="isSendingMessage"></i> <i class="el-icon-loading" v-else-if="isSendingMessage"></i>
<template v-if="showReadSummary"> <template v-if="showReadSummary && !isWithdrawMessage">
<div v-if="isMyMessage" class="msg-read pos-rel"> <div v-if="isMyMessage" class="msg-read pos-rel">
<span <span
@click="readListVisibility = true" @click="readListVisibility = true"
...@@ -135,6 +145,13 @@ ...@@ -135,6 +145,13 @@
/> />
</div> </div>
</template> </template>
<span
class="withdraw"
v-if="isMyMessage && canWithdraw && !isWithdrawMessage"
@click="withdraw"
>撤回此消息</span
>
</div> </div>
</template> </template>
...@@ -163,6 +180,10 @@ import WhoReadList from "./who-read-list.vue"; ...@@ -163,6 +180,10 @@ import WhoReadList from "./who-read-list.vue";
import avatar from "@/customer-service/components/avatar.vue"; import avatar from "@/customer-service/components/avatar.vue";
import { chatStore, ChatStore } from "@/customer-service/store/model"; import { chatStore, ChatStore } from "@/customer-service/store/model";
import ximInstance from "../xim/xim";
import { dbController } from "../database";
const twoMinutes = 2 * 60 * 1000;
@Component({ @Component({
components: { FileIcon, VoiceIcon, WhoReadList, VideoPlayerIcon, avatar }, components: { FileIcon, VoiceIcon, WhoReadList, VideoPlayerIcon, avatar },
...@@ -180,6 +201,9 @@ export default class Message extends Mixins(Filters) { ...@@ -180,6 +201,9 @@ export default class Message extends Mixins(Filters) {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID; private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.Mutation(ChatStore.MUTATION_WITHDRAW)
private readonly executeWithDraw: ChatStore.MUTATION_WITHDRAW;
/** /**
* tbd: 文件消息所在的域名的url,逻辑需要补充 * tbd: 文件消息所在的域名的url,逻辑需要补充
*/ */
...@@ -213,6 +237,17 @@ export default class Message extends Mixins(Filters) { ...@@ -213,6 +237,17 @@ export default class Message extends Mixins(Filters) {
private org = ""; private org = "";
private get canWithdraw() {
if (this.data) {
return new Date().valueOf() - this.data.ts * 1000 < twoMinutes;
}
return false;
}
private get isWithdrawMessage() {
return this.data.type === dto.MessageType.Withdraw;
}
private get isAllRead() { private get isAllRead() {
return this.data.read_count >= this.data.total_read_count; return this.data.read_count >= this.data.total_read_count;
} }
...@@ -431,6 +466,16 @@ export default class Message extends Mixins(Filters) { ...@@ -431,6 +466,16 @@ export default class Message extends Mixins(Filters) {
this.fileFailed2Load = true; this.fileFailed2Load = true;
this.messageRealUrl = ""; this.messageRealUrl = "";
} }
private withdraw() {
ximInstance
.withdraw(this.chatId, this.data.id)
.then(() => {
this.executeWithDraw(this.data.id);
dbController.removeMessage(this.chatId, this.data.id);
})
.finally(() => this.$emit("withdraw", this.data.id));
}
} }
</script> </script>
...@@ -438,6 +483,7 @@ export default class Message extends Mixins(Filters) { ...@@ -438,6 +483,7 @@ export default class Message extends Mixins(Filters) {
.message-con { .message-con {
margin: 20px 0; margin: 20px 0;
margin-right: 15px; margin-right: 15px;
position: relative;
&.my-message { &.my-message {
.msg-avatar { .msg-avatar {
...@@ -471,6 +517,13 @@ export default class Message extends Mixins(Filters) { ...@@ -471,6 +517,13 @@ export default class Message extends Mixins(Filters) {
background-color: #000; background-color: #000;
border-radius: 0; border-radius: 0;
} }
&.withdraw-message {
background-color: transparent;
padding: 0 4px;
font-size: 12px;
color: #999;
}
} }
.msg-read { .msg-read {
...@@ -487,6 +540,22 @@ export default class Message extends Mixins(Filters) { ...@@ -487,6 +540,22 @@ export default class Message extends Mixins(Filters) {
margin-left: 0; margin-left: 0;
margin-top: 0; margin-top: 0;
} }
.withdraw {
color: #999;
position: absolute;
bottom: -18px;
right: 0;
font-size: 12px;
display: none;
cursor: pointer;
}
&:hover {
.withdraw {
display: inline-block;
}
}
} }
> i { > i {
......
...@@ -175,6 +175,11 @@ class ChatCacheDatabaseController { ...@@ -175,6 +175,11 @@ class ChatCacheDatabaseController {
}); });
} }
public removeMessage(chat: number, msg: number) {
const store = this.buildChatMessageStore(chat);
store.delete(msg);
}
public mergeChatList(source1: Chat[], source2: Chat[]) { public mergeChatList(source1: Chat[], source2: Chat[]) {
for (const item of source2) { for (const item of source2) {
const t = source1.find((i) => i.id === item.id); const t = source1.find((i) => i.id === item.id);
......
...@@ -58,6 +58,16 @@ export type ChatListRequestList = { ...@@ -58,6 +58,16 @@ export type ChatListRequestList = {
total: number; total: number;
}; };
export const enum MessageType {
Text = "text",
Image = "image",
File = "file",
Video = "video",
Voice = "voice",
GeneralOrderMsg = "general_order_msg",
Withdraw = "withdraw",
}
export interface Message { export interface Message {
at_id: string; at_id: string;
chat_id: number; chat_id: number;
...@@ -75,7 +85,7 @@ export interface Message { ...@@ -75,7 +85,7 @@ export interface Message {
status: number; status: number;
total_read_count: number; total_read_count: number;
ts: number; ts: number;
type: "text" | "image" | "file" | "video" | "voice" | "general_order_msg"; type: MessageType;
update_time: number; update_time: number;
url: string; url: string;
} }
......
...@@ -347,6 +347,14 @@ export default { ...@@ -347,6 +347,14 @@ export default {
) => { ) => {
Vue.set(state[ChatStore.STATE_CHAT_USERNAME], param.id, param.name); Vue.set(state[ChatStore.STATE_CHAT_USERNAME], param.id, param.name);
}, },
[ChatStore.MUTATION_WITHDRAW]: (state, id: number) => {
const old = state[ChatStore.STATE_CHAT_MSG_HISTORY] || [];
const chatid = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatid == null) return;
state[ChatStore.STATE_CHAT_MSG_HISTORY] = old.filter(
(i) => i.id !== id
);
},
}, },
actions: { actions: {
async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit }) { async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit }) {
......
...@@ -170,6 +170,9 @@ export namespace ChatStore { ...@@ -170,6 +170,9 @@ export namespace ChatStore {
export const MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = "清空chat uniplat id"; export const MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = "清空chat uniplat id";
export type MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = () => void; export type MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = () => void;
export const MUTATION_WITHDRAW = "撤回";
export type MUTATION_WITHDRAW = (id: number) => void;
export const MUTATION_SAVE_MYSELF_ID = export const MUTATION_SAVE_MYSELF_ID =
"保存我的id:聊天窗口显示在右边那个人的id"; "保存我的id:聊天窗口显示在右边那个人的id";
export type MUTATION_SAVE_MYSELF_ID = () => void; export type MUTATION_SAVE_MYSELF_ID = () => void;
......
...@@ -291,6 +291,16 @@ export class Xim { ...@@ -291,6 +291,16 @@ export class Xim {
return this; return this;
} }
public async withdraw(chat: number, msg: number) {
this.checkConnected();
if (this.client == null) {
throw new Error("client shouldn't undefined");
}
return this.client
.withdrawMsg(chatType, chat, msg)
.then((r) => r.args[0]);
}
/** /**
* 移除会话(用户端/移动端使用) * 移除会话(用户端/移动端使用)
*/ */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment