Commit 55070bdd by Sixong.Zhu

update

parent d9ff54d3
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
<script lang="ts"> <script lang="ts">
import { replaceText2Link } from "@/customer-service/utils"; import { replaceText2Link } from "@/customer-service/utils";
import xim from "@/customer-service/xim";
import { Component } from "vue-property-decorator"; import { Component } from "vue-property-decorator";
import BaseMessage from "./index"; import BaseMessage from "./index";
...@@ -15,9 +16,21 @@ export default class Index extends BaseMessage { ...@@ -15,9 +16,21 @@ export default class Index extends BaseMessage {
private readonly emptyText = " "; private readonly emptyText = " ";
private format2Link(text: string) { private format2Link(text: string) {
return replaceText2Link(text); let t = replaceText2Link(text);
const keywords = xim.getMatchedTextKeywords();
for (const item of keywords) {
const r = new RegExp(item, "g");
t = t.replace(r, `<span class="highlight">${item}</span>`);
}
return t;
} }
} }
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
.inline-text {
/deep/ .highlight {
color: #e87005;
}
}
</style>
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
:class="{ :class="{
'my-message flex-row-reverse': isMyMessage, 'my-message flex-row-reverse': isMyMessage,
'justify-content-center': isWithdrawMessage, 'justify-content-center': isWithdrawMessage,
'offset-bottom': matchKeywords,
}" }"
> >
<div class="msg-content" :class="{ 'algin-left': !isMyMessage }"> <div class="msg-content" :class="{ 'algin-left': !isMyMessage }">
...@@ -31,7 +32,7 @@ ...@@ -31,7 +32,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 && !isWithdrawMessage"> <template v-if="backend && showReadSummary && !isWithdrawMessage">
<div v-if="isMyMessage" class="msg-read pos-rel"> <div v-if="isMyMessage" class="msg-read pos-rel">
<span <span
@click="openReaderList" @click="openReaderList"
...@@ -59,6 +60,36 @@ ...@@ -59,6 +60,36 @@
@click="withdraw" @click="withdraw"
>撤回此消息</span >撤回此消息</span
> >
<el-popover
:visible-arrow="false"
v-if="backend && matchKeywords"
placement="right"
popper-class="match-keyword-popover"
trigger="click"
:disabled="!!handled"
>
<ul class="keyword-action">
<li @click.stop="executeHandled">设为已处理</li>
<li @click.stop="ignoredKeyword">忽略</li>
<!-- <li>创建工作流</li> -->
<!-- <li>更多</li> -->
</ul>
<span
slot="reference"
class="match-keyword d-flex align-items-center text-nowrap"
:class="{ handled: handled === 1, ignored: handled === 2 }"
>
<i :class="handled ? 'el-icon-success' : 'el-icon-info'"></i>
<span>{{
handled
? handled === 1
? "已处理"
: "已忽略"
: "触发敏感词,请处理"
}}</span>
</span>
</el-popover>
</div> </div>
</template> </template>
...@@ -90,6 +121,7 @@ import AudioMessage from "./message-item/audio-message.vue"; ...@@ -90,6 +121,7 @@ import AudioMessage from "./message-item/audio-message.vue";
import VideoMessage from "./message-item/video-message.vue"; import VideoMessage from "./message-item/video-message.vue";
import TextMessage from "./message-item/text-message.vue"; import TextMessage from "./message-item/text-message.vue";
import WithdrawMessage from "./message-item/withdraw-message.vue"; import WithdrawMessage from "./message-item/withdraw-message.vue";
import xim from "./../xim";
const twoMinutes = 2 * 60 * 1000; const twoMinutes = 2 * 60 * 1000;
...@@ -130,8 +162,11 @@ export default class Message extends Mixins(Filters) { ...@@ -130,8 +162,11 @@ export default class Message extends Mixins(Filters) {
@chatStore.Mutation(ChatStore.MUTATION_WITHDRAW) @chatStore.Mutation(ChatStore.MUTATION_WITHDRAW)
private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW; private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW;
@chatStore.Action(ChatStore.ACTION_SET_HANDLED)
private readonly setHandled!: ChatStore.ACTION_SET_HANDLED;
@Prop({ type: Object, default: () => Object.create(null) }) @Prop({ type: Object, default: () => Object.create(null) })
private data!: dto.Message; private readonly data!: dto.Message;
@Prop() @Prop()
private readonly isSendingMessage!: boolean; private readonly isSendingMessage!: boolean;
...@@ -144,6 +179,8 @@ export default class Message extends Mixins(Filters) { ...@@ -144,6 +179,8 @@ export default class Message extends Mixins(Filters) {
@Inject({ default: false }) readonly showReadSummary!: boolean; @Inject({ default: false }) readonly showReadSummary!: boolean;
private readonly backend = chat.isBackend();
private messageComponent = ""; private messageComponent = "";
private readListVisibility = false; private readListVisibility = false;
...@@ -151,9 +188,10 @@ export default class Message extends Mixins(Filters) { ...@@ -151,9 +188,10 @@ export default class Message extends Mixins(Filters) {
private org = ""; private org = "";
private readerListOffset = false; private readerListOffset = false;
private defaultMessageHandledStatus = dto.MessageHandled.Default;
private get canWithdraw() { private get canWithdraw() {
if (this.data) { if (this.backend && this.data) {
return new Date().valueOf() - this.data.ts * 1000 < twoMinutes; return new Date().valueOf() - this.data.ts * 1000 < twoMinutes;
} }
return false; return false;
...@@ -182,6 +220,13 @@ export default class Message extends Mixins(Filters) { ...@@ -182,6 +220,13 @@ export default class Message extends Mixins(Filters) {
return { msg: { text: "" } }; return { msg: { text: "" } };
} }
private get handled() {
if (this.data) {
return this.defaultMessageHandledStatus || this.data.handled;
}
return dto.MessageHandled.Default;
}
private get isMyMessage() { private get isMyMessage() {
if (this.isSendingMessage) { if (this.isSendingMessage) {
return true; return true;
...@@ -196,10 +241,13 @@ export default class Message extends Mixins(Filters) { ...@@ -196,10 +241,13 @@ export default class Message extends Mixins(Filters) {
} }
private get userName() { private get userName() {
return ( if (this.chatMembers) {
this.chatMembers!.find((member) => member.eid === this.data.eid) const t = this.chatMembers.find((i) => i.eid === this.data.eid);
?.name ?? "" if (t) {
); return t.name;
}
}
return "";
} }
private get avatar() { private get avatar() {
...@@ -250,6 +298,21 @@ export default class Message extends Mixins(Filters) { ...@@ -250,6 +298,21 @@ export default class Message extends Mixins(Filters) {
return type; return type;
} }
private get isTextMessage() {
return this.messageType === dto.MessageType.Text;
}
private get matchKeywords() {
if (this.isTextMessage && !this.isMyMessage) {
const m = this.messageBody.msg as { text: string };
if (m && m.text) {
const keywords = xim.getMatchedTextKeywords();
return keywords.find((i) => m.text.includes(i));
}
}
return false;
}
private isCustomer() { private isCustomer() {
return !this.showReadSummary; return !this.showReadSummary;
} }
...@@ -281,6 +344,28 @@ export default class Message extends Mixins(Filters) { ...@@ -281,6 +344,28 @@ export default class Message extends Mixins(Filters) {
this.readerListOffset = e.x < 450; this.readerListOffset = e.x < 450;
this.readListVisibility = true; this.readListVisibility = true;
} }
private executeHandled() {
this.setHandled({
id: this.data.id,
value: (this.defaultMessageHandledStatus =
dto.MessageHandled.Handled),
});
this.closeKeywordPopover();
}
private ignoredKeyword() {
this.setHandled({
id: this.data.id,
value: (this.defaultMessageHandledStatus =
dto.MessageHandled.Ignored),
});
this.closeKeywordPopover();
}
private closeKeywordPopover() {
document.body.click();
}
} }
</script> </script>
...@@ -363,6 +448,10 @@ export default class Message extends Mixins(Filters) { ...@@ -363,6 +448,10 @@ export default class Message extends Mixins(Filters) {
} }
} }
&.offset-bottom {
margin-bottom: 30px;
}
> i { > i {
height: 16px; height: 16px;
font-size: 16px; font-size: 16px;
...@@ -493,6 +582,7 @@ i.msg-avatar { ...@@ -493,6 +582,7 @@ i.msg-avatar {
font-size: 14px; font-size: 14px;
} }
} }
.no-selection { .no-selection {
user-select: none; user-select: none;
} }
...@@ -512,4 +602,53 @@ i.msg-avatar { ...@@ -512,4 +602,53 @@ i.msg-avatar {
.all { .all {
color: #4389f8; color: #4389f8;
} }
.match-keyword {
background-color: #fff3e0;
border-radius: 13px;
padding: 3px 8px;
color: #e87005;
position: absolute;
bottom: -10px;
left: 0;
cursor: pointer;
font-size: 12px;
&.handled {
color: #59ba7b;
background-color: #f0f0f0;
cursor: default;
}
&.ignored {
color: #999;
background-color: #f0f0f0;
cursor: default;
}
i {
margin-right: 5px;
}
}
.keyword-action {
list-style: none;
li {
list-style: none;
padding: 8px 10px;
color: #077aec;
cursor: pointer;
&:hover {
background-color: #f5f6fa;
}
}
}
</style>
<style lang="less">
.match-keyword-popover {
padding: 0;
}
</style> </style>
import { Chat, Message } from "./../xim/models/chat"; import { Chat, Message } from "./../xim/models/chat";
import { MessageHandled } from "../model";
class ChatCacheDatabaseController { class ChatCacheDatabaseController {
private db!: IDBDatabase; private db!: IDBDatabase;
...@@ -212,6 +213,30 @@ class ChatCacheDatabaseController { ...@@ -212,6 +213,30 @@ class ChatCacheDatabaseController {
return source; return source;
} }
public updateMessageStatus(
chat: number,
msg: number,
status: MessageHandled
) {
return new Promise<void>((resolve) => {
if (!this.db) {
return resolve();
}
this.setupChatMessageDatabase(chat).finally(() => {
const store = this.buildChatMessageStore(chat);
const r = store.get(msg);
r.onsuccess = (o) => {
const p = (o.target as any).result as Message;
p.handled = status;
const u = store.put(p);
u.onsuccess = () => resolve();
u.onerror = () => resolve();
};
r.onerror = () => resolve();
});
});
}
} }
export const dbController = new ChatCacheDatabaseController(); export const dbController = new ChatCacheDatabaseController();
...@@ -88,6 +88,12 @@ export const enum MessageType { ...@@ -88,6 +88,12 @@ export const enum MessageType {
Withdraw = "withdraw", Withdraw = "withdraw",
} }
export const enum MessageHandled {
Default,
Handled,
Ignored,
}
export interface Message { export interface Message {
at_id: string; at_id: string;
chat_id: number; chat_id: number;
...@@ -108,6 +114,7 @@ export interface Message { ...@@ -108,6 +114,7 @@ export interface Message {
type: MessageType; type: MessageType;
update_time: number; update_time: number;
url: string; url: string;
handled?: MessageHandled;
} }
export type MessageRequestResult = readonly Message[]; export type MessageRequestResult = readonly Message[];
......
...@@ -2,7 +2,7 @@ import Vue from "vue"; ...@@ -2,7 +2,7 @@ import Vue from "vue";
import { Module } from "vuex"; import { Module } from "vuex";
import { dbController } from "../database"; import { dbController } from "../database";
import { ChatMember, ServiceType } from "../model"; import { ChatMember, ServiceType, MessageHandled } from "../model";
import { isAccessibleUrl } from "../service/tools"; import { isAccessibleUrl } from "../service/tools";
import { unique } from "../utils"; import { unique } from "../utils";
import { getChatModelInfo } from "../utils/chat-info"; import { getChatModelInfo } from "../utils/chat-info";
...@@ -28,7 +28,7 @@ function uniqueMessages( ...@@ -28,7 +28,7 @@ function uniqueMessages(
messages: NonNullable<ChatStore.STATE_CHAT_MSG_HISTORY> messages: NonNullable<ChatStore.STATE_CHAT_MSG_HISTORY>
) { ) {
const arr = [...messages]; const arr = [...messages];
return unique(arr, function(item, all) { return unique(arr, function (item, all) {
return all.findIndex((k) => k.id === item.id); return all.findIndex((k) => k.id === item.id);
}); });
} }
...@@ -301,7 +301,7 @@ export default { ...@@ -301,7 +301,7 @@ export default {
state[ChatStore.STATE_CHAT_SENDING_MESSAGES] = [...current]; state[ChatStore.STATE_CHAT_SENDING_MESSAGES] = [...current];
} }
}, },
[ChatStore.MUTATION_SAVE_CURRENT_CHAT_INPUTING]: (function() { [ChatStore.MUTATION_SAVE_CURRENT_CHAT_INPUTING]: (function () {
const setTimeoutId: { [key: string]: number } = {}; const setTimeoutId: { [key: string]: number } = {};
return ( return (
state: ChatStoreState, state: ChatStoreState,
...@@ -640,7 +640,8 @@ export default { ...@@ -640,7 +640,8 @@ export default {
wantedChatRoom.business_data.model_name, wantedChatRoom.business_data.model_name,
wantedChatRoom.business_data.obj_id, wantedChatRoom.business_data.obj_id,
wantedChatRoom.business_data.detail_name wantedChatRoom.business_data.detail_name
).then(info => { )
.then((info) => {
commit( commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_VERSION, ChatStore.MUTATION_SAVE_CURRENT_CHAT_VERSION,
info.uniplat_version info.uniplat_version
...@@ -649,7 +650,8 @@ export default { ...@@ -649,7 +650,8 @@ export default {
ChatStore.MUTATION_SAVE_CURRENT_CHAT_UNIPLAT_ID, ChatStore.MUTATION_SAVE_CURRENT_CHAT_UNIPLAT_ID,
info.uniplatId info.uniplatId
); );
}).catch(console.error) })
.catch(console.error);
commit(ChatStore.MUTATION_INITING_CHAT); commit(ChatStore.MUTATION_INITING_CHAT);
removeRegisterChatEvents.forEach((k) => k()); removeRegisterChatEvents.forEach((k) => k());
...@@ -710,7 +712,7 @@ export default { ...@@ -710,7 +712,7 @@ export default {
} }
commit( commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_MEMBERS, ChatStore.MUTATION_SAVE_CURRENT_CHAT_MEMBERS,
unique(newChatMembers, function(item, all) { unique(newChatMembers, function (item, all) {
return all.findIndex((k) => k.eid === item.eid); return all.findIndex((k) => k.eid === item.eid);
}) })
); );
...@@ -846,6 +848,24 @@ export default { ...@@ -846,6 +848,24 @@ export default {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS); await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
}, },
[ChatStore.ACTION_SET_HANDLED](
{ state },
p: {
id: number;
value: MessageHandled;
}
) {
const msgs = state[ChatStore.STATE_CHAT_MSG_HISTORY];
if (msgs) {
const t = msgs.find((i) => i.id === p.id);
if (t) {
t.handled = p.value;
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
chatId &&
dbController.updateMessageStatus(chatId, p.id, p.value);
}
}
},
}, },
getters: { getters: {
[ChatStore.STATE_CHAT_MSG_HISTORY](state) { [ChatStore.STATE_CHAT_MSG_HISTORY](state) {
......
...@@ -329,6 +329,12 @@ export namespace ChatStore { ...@@ -329,6 +329,12 @@ export namespace ChatStore {
export const ACTION_CHAT_CS_EXIT = "客服退出"; export const ACTION_CHAT_CS_EXIT = "客服退出";
export type ACTION_CHAT_CS_EXIT = () => Promise<void>; export type ACTION_CHAT_CS_EXIT = () => Promise<void>;
export const ACTION_SET_HANDLED = "设置敏感词已处理";
export type ACTION_SET_HANDLED = (p: {
id: number;
value: dto.MessageHandled;
}) => void;
} }
export interface ChatStoreState { export interface ChatStoreState {
......
...@@ -20,6 +20,7 @@ class Chat { ...@@ -20,6 +20,7 @@ class Chat {
private serviceType = ServiceType.Backend; private serviceType = ServiceType.Backend;
private product = CustomerServiceProduct.Default; private product = CustomerServiceProduct.Default;
private eventHub: Vue | null = null; private eventHub: Vue | null = null;
private keywords: string[] = [];
private userMapping: { [key: string]: { name: string; avatar: string } } = private userMapping: { [key: string]: { name: string; avatar: string } } =
{}; {};
...@@ -47,9 +48,13 @@ class Chat { ...@@ -47,9 +48,13 @@ class Chat {
EmojiService.raiseOnReady(this.token); EmojiService.raiseOnReady(this.token);
option.sdk().events.addTokenChanged((token) => { option
this.setToken(() => new Promise((resolve) => resolve(token))); .sdk()
}); .events.addTokenChanged((token) =>
this.setToken(() => new Promise((resolve) => resolve(token)))
);
// this.keywords = ["社保"];
return this.initChatSdk(option.webSocketUri); return this.initChatSdk(option.webSocketUri);
} }
...@@ -59,7 +64,7 @@ class Chat { ...@@ -59,7 +64,7 @@ class Chat {
} }
public getSdk = () => { public getSdk = () => {
if (this._sdk == null) { if (!this._sdk) {
throw new Error("sdk shouldn't undefined"); throw new Error("sdk shouldn't undefined");
} }
return this._sdk(); return this._sdk();
...@@ -69,6 +74,10 @@ class Chat { ...@@ -69,6 +74,10 @@ class Chat {
return this.serviceType; return this.serviceType;
} }
public isBackend() {
return this.serviceType === ServiceType.Backend;
}
public getProduct() { public getProduct() {
return this.product; return this.product;
} }
...@@ -133,6 +142,10 @@ class Chat { ...@@ -133,6 +142,10 @@ class Chat {
this.eventHub.$on(event, callback); this.eventHub.$on(event, callback);
} }
} }
public getMatchedTextKeywords() {
return this.keywords;
}
} }
export default new Chat(); export default new Chat();
import { MessageHandled } from "@/customer-service/model";
export interface Chat { export interface Chat {
id: number; id: number;
org_id: string; org_id: string;
...@@ -68,6 +70,7 @@ export interface Message { ...@@ -68,6 +70,7 @@ export interface Message {
status: number; status: number;
url: string; url: string;
is_open: boolean; is_open: boolean;
handled?: MessageHandled;
} }
export interface NotifyMessage { export interface NotifyMessage {
......
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