Commit 55070bdd by Sixong.Zhu

update

parent d9ff54d3
......@@ -7,6 +7,7 @@
<script lang="ts">
import { replaceText2Link } from "@/customer-service/utils";
import xim from "@/customer-service/xim";
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
......@@ -15,9 +16,21 @@ export default class Index extends BaseMessage {
private readonly emptyText = " ";
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>
<style lang="less" scoped></style>
<style lang="less" scoped>
.inline-text {
/deep/ .highlight {
color: #e87005;
}
}
</style>
......@@ -4,6 +4,7 @@
:class="{
'my-message flex-row-reverse': isMyMessage,
'justify-content-center': isWithdrawMessage,
'offset-bottom': matchKeywords,
}"
>
<div class="msg-content" :class="{ 'algin-left': !isMyMessage }">
......@@ -31,7 +32,7 @@
></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">
<span
@click="openReaderList"
......@@ -59,6 +60,36 @@
@click="withdraw"
>撤回此消息</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>
</template>
......@@ -90,6 +121,7 @@ import AudioMessage from "./message-item/audio-message.vue";
import VideoMessage from "./message-item/video-message.vue";
import TextMessage from "./message-item/text-message.vue";
import WithdrawMessage from "./message-item/withdraw-message.vue";
import xim from "./../xim";
const twoMinutes = 2 * 60 * 1000;
......@@ -130,8 +162,11 @@ export default class Message extends Mixins(Filters) {
@chatStore.Mutation(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) })
private data!: dto.Message;
private readonly data!: dto.Message;
@Prop()
private readonly isSendingMessage!: boolean;
......@@ -144,6 +179,8 @@ export default class Message extends Mixins(Filters) {
@Inject({ default: false }) readonly showReadSummary!: boolean;
private readonly backend = chat.isBackend();
private messageComponent = "";
private readListVisibility = false;
......@@ -151,9 +188,10 @@ export default class Message extends Mixins(Filters) {
private org = "";
private readerListOffset = false;
private defaultMessageHandledStatus = dto.MessageHandled.Default;
private get canWithdraw() {
if (this.data) {
if (this.backend && this.data) {
return new Date().valueOf() - this.data.ts * 1000 < twoMinutes;
}
return false;
......@@ -182,6 +220,13 @@ export default class Message extends Mixins(Filters) {
return { msg: { text: "" } };
}
private get handled() {
if (this.data) {
return this.defaultMessageHandledStatus || this.data.handled;
}
return dto.MessageHandled.Default;
}
private get isMyMessage() {
if (this.isSendingMessage) {
return true;
......@@ -196,10 +241,13 @@ export default class Message extends Mixins(Filters) {
}
private get userName() {
return (
this.chatMembers!.find((member) => member.eid === this.data.eid)
?.name ?? ""
);
if (this.chatMembers) {
const t = this.chatMembers.find((i) => i.eid === this.data.eid);
if (t) {
return t.name;
}
}
return "";
}
private get avatar() {
......@@ -250,6 +298,21 @@ export default class Message extends Mixins(Filters) {
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() {
return !this.showReadSummary;
}
......@@ -281,6 +344,28 @@ export default class Message extends Mixins(Filters) {
this.readerListOffset = e.x < 450;
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>
......@@ -363,6 +448,10 @@ export default class Message extends Mixins(Filters) {
}
}
&.offset-bottom {
margin-bottom: 30px;
}
> i {
height: 16px;
font-size: 16px;
......@@ -493,6 +582,7 @@ i.msg-avatar {
font-size: 14px;
}
}
.no-selection {
user-select: none;
}
......@@ -512,4 +602,53 @@ i.msg-avatar {
.all {
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>
import { Chat, Message } from "./../xim/models/chat";
import { MessageHandled } from "../model";
class ChatCacheDatabaseController {
private db!: IDBDatabase;
......@@ -212,6 +213,30 @@ class ChatCacheDatabaseController {
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();
......@@ -88,6 +88,12 @@ export const enum MessageType {
Withdraw = "withdraw",
}
export const enum MessageHandled {
Default,
Handled,
Ignored,
}
export interface Message {
at_id: string;
chat_id: number;
......@@ -108,6 +114,7 @@ export interface Message {
type: MessageType;
update_time: number;
url: string;
handled?: MessageHandled;
}
export type MessageRequestResult = readonly Message[];
......
......@@ -2,7 +2,7 @@ import Vue from "vue";
import { Module } from "vuex";
import { dbController } from "../database";
import { ChatMember, ServiceType } from "../model";
import { ChatMember, ServiceType, MessageHandled } from "../model";
import { isAccessibleUrl } from "../service/tools";
import { unique } from "../utils";
import { getChatModelInfo } from "../utils/chat-info";
......@@ -28,7 +28,7 @@ function uniqueMessages(
messages: NonNullable<ChatStore.STATE_CHAT_MSG_HISTORY>
) {
const arr = [...messages];
return unique(arr, function(item, all) {
return unique(arr, function (item, all) {
return all.findIndex((k) => k.id === item.id);
});
}
......@@ -301,7 +301,7 @@ export default {
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 } = {};
return (
state: ChatStoreState,
......@@ -640,16 +640,18 @@ export default {
wantedChatRoom.business_data.model_name,
wantedChatRoom.business_data.obj_id,
wantedChatRoom.business_data.detail_name
).then(info => {
commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_VERSION,
info.uniplat_version
);
commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_UNIPLAT_ID,
info.uniplatId
);
}).catch(console.error)
)
.then((info) => {
commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_VERSION,
info.uniplat_version
);
commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_UNIPLAT_ID,
info.uniplatId
);
})
.catch(console.error);
commit(ChatStore.MUTATION_INITING_CHAT);
removeRegisterChatEvents.forEach((k) => k());
......@@ -710,7 +712,7 @@ export default {
}
commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_MEMBERS,
unique(newChatMembers, function(item, all) {
unique(newChatMembers, function (item, all) {
return all.findIndex((k) => k.eid === item.eid);
})
);
......@@ -846,6 +848,24 @@ export default {
await new Promise((resolve) => setTimeout(resolve, 500));
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: {
[ChatStore.STATE_CHAT_MSG_HISTORY](state) {
......
......@@ -329,6 +329,12 @@ export namespace ChatStore {
export const ACTION_CHAT_CS_EXIT = "客服退出";
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 {
......
......@@ -20,6 +20,7 @@ class Chat {
private serviceType = ServiceType.Backend;
private product = CustomerServiceProduct.Default;
private eventHub: Vue | null = null;
private keywords: string[] = [];
private userMapping: { [key: string]: { name: string; avatar: string } } =
{};
......@@ -47,9 +48,13 @@ class Chat {
EmojiService.raiseOnReady(this.token);
option.sdk().events.addTokenChanged((token) => {
this.setToken(() => new Promise((resolve) => resolve(token)));
});
option
.sdk()
.events.addTokenChanged((token) =>
this.setToken(() => new Promise((resolve) => resolve(token)))
);
// this.keywords = ["社保"];
return this.initChatSdk(option.webSocketUri);
}
......@@ -59,7 +64,7 @@ class Chat {
}
public getSdk = () => {
if (this._sdk == null) {
if (!this._sdk) {
throw new Error("sdk shouldn't undefined");
}
return this._sdk();
......@@ -69,6 +74,10 @@ class Chat {
return this.serviceType;
}
public isBackend() {
return this.serviceType === ServiceType.Backend;
}
public getProduct() {
return this.product;
}
......@@ -133,6 +142,10 @@ class Chat {
this.eventHub.$on(event, callback);
}
}
public getMatchedTextKeywords() {
return this.keywords;
}
}
export default new Chat();
import { MessageHandled } from "@/customer-service/model";
export interface Chat {
id: number;
org_id: string;
......@@ -68,6 +70,7 @@ export interface Message {
status: number;
url: string;
is_open: boolean;
handled?: MessageHandled;
}
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