Commit a5f82bc4 by Sixong.Zhu

chat

parent 49b18f62
...@@ -5,13 +5,13 @@ ...@@ -5,13 +5,13 @@
v-for="item in chatMembers" v-for="item in chatMembers"
:key="item.id" :key="item.id"
> >
<span class="member-name ver-mid"> <span class="member-name ver-mid text-truncate">
{{ item.name || item.eid }} {{ item.name || item.eid }}
</span> </span>
<span class="member-phone ver-mid"> <span class="member-phone ver-mid text-nowrap">
{{ item.phone }} {{ item.phone }}
</span> </span>
<span class="member-type ver-mid"> <span class="member-type ver-mid text-nowrap">
{{ memberTypeStr(item.type) }} {{ memberTypeStr(item.type) }}
</span> </span>
<el-button class="ver-mid get-out" type="text" @click="getout(item)" <el-button class="ver-mid get-out" type="text" @click="getout(item)"
...@@ -22,10 +22,10 @@ ...@@ -22,10 +22,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { ChatStore, chatStore } from "../store/model"; import { ChatStore, chatStore } from "../store/model";
import avatar from "@/customer-service/components/avatar.vue"; import avatar from "@/customer-service/components/avatar.vue";
import { ChatRole } from "../model";
@Component({ components: { avatar } }) @Component({ components: { avatar } })
export default class ChatMembers extends Vue { export default class ChatMembers extends Vue {
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS) @chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
...@@ -46,31 +46,29 @@ export default class ChatMembers extends Vue { ...@@ -46,31 +46,29 @@ export default class ChatMembers extends Vue {
type: "warning", type: "warning",
}); });
// xim里的eid等于uniplat里的uid // xim里的eid等于uniplat里的uid
if (item.type === 25) { if (item.type === ChatRole.Default) {
// 普通成员 // 普通成员
this._getout([item.eid]); this._getout([item.eid]);
} else if (item.type === 92) { } else if (item.type === ChatRole.CustomerService) {
// 可否 // 可否
this._getoutCs([item.eid]); this._getoutCs([item.eid]);
} }
} }
private memberTypeStr(type: string | number) { private memberTypeStr(type: string | number) {
if (type.toString() === "25") { if (+type === ChatRole.CustomerService) {
return "";
}
if (type.toString() === "92") {
return "客服"; return "客服";
} }
if (type.toString() === "85") { if (+type === ChatRole.Admin) {
return "管理员"; return "管理员";
} }
return "";
} }
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.chat-members { .chat-members {
padding: 30px; padding: 20px;
padding-bottom: 0; padding-bottom: 0;
background: #fff; background: #fff;
.chat-member { .chat-member {
...@@ -78,7 +76,7 @@ export default class ChatMembers extends Vue { ...@@ -78,7 +76,7 @@ export default class ChatMembers extends Vue {
vertical-align: top; vertical-align: top;
align-items: center; align-items: center;
margin: 10px 0; margin: 10px 0;
padding: 10px 0; padding: 10px;
&:hover { &:hover {
background-color: #f5f7fa; background-color: #f5f7fa;
.get-out { .get-out {
...@@ -88,9 +86,7 @@ export default class ChatMembers extends Vue { ...@@ -88,9 +86,7 @@ export default class ChatMembers extends Vue {
} }
.member-name { .member-name {
display: inline-block; display: inline-block;
width: 5em; min-width: 7em;
word-break: break-word;
white-space: pre-line;
margin-right: 10px; margin-right: 10px;
} }
.member-type { .member-type {
......
<template> <template>
<div class="room-title d-flex justify-content-between align-items-center"> <div class="room-title d-flex justify-content-between align-items-center">
<div class="title"> <div class="title text-nowrap">
{{ chatTitle }} {{ chatTitle }}
<template v-if="chatMembers.length"> <template v-if="chatMembers.length">
<span class="members-count" <span class="members-count"
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
class="button" class="button"
@click="startReception" @click="startReception"
round round
size="small"
v-if="!isChatMember" v-if="!isChatMember"
>我要接待</el-button >我要接待</el-button
> >
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
class="button" class="button"
@click="exitChat" @click="exitChat"
round round
size="small"
v-if="isChatMember" v-if="isChatMember"
>退出会话</el-button >退出会话</el-button
> >
...@@ -33,10 +35,11 @@ ...@@ -33,10 +35,11 @@
class="button" class="button"
@click="finishReception" @click="finishReception"
round round
size="small"
v-if="isChatMember && operatorType > 25" v-if="isChatMember && operatorType > 25"
>结束接待</el-button >结束接待</el-button
> >
<el-button class="button" @click="showAddMember" round <el-button class="button" @click="showAddMember" round size="small"
>添加客服</el-button >添加客服</el-button
> >
<i <i
...@@ -58,6 +61,7 @@ import { Component, Prop, Vue } from "vue-property-decorator"; ...@@ -58,6 +61,7 @@ import { Component, Prop, Vue } from "vue-property-decorator";
import ChatCreator from "@/customer-service/components/create-chat.vue"; import ChatCreator from "@/customer-service/components/create-chat.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model"; import { ChatStore, chatStore } from "@/customer-service/store/model";
import { ChatRole } from "../model";
@Component({ components: { ChatCreator } }) @Component({ components: { ChatCreator } })
export default class ChatTitle extends Vue { export default class ChatTitle extends Vue {
...@@ -131,9 +135,9 @@ export default class ChatTitle extends Vue { ...@@ -131,9 +135,9 @@ export default class ChatTitle extends Vue {
private async exitChat() { private async exitChat() {
try { try {
if (this.operatorType === "25") { if (+this.operatorType === ChatRole.Default) {
await this._userExitChat(); await this._userExitChat();
} else if (+this.operatorType > 25) { } else if (+this.operatorType > ChatRole.Default) {
await this._csExitChat(); await this._csExitChat();
} }
this.hideChat(); this.hideChat();
......
...@@ -67,15 +67,15 @@ export default class MessageInput extends Vue { ...@@ -67,15 +67,15 @@ export default class MessageInput extends Vue {
for (const item of msg) { for (const item of msg) {
if (isImageOrFile(item)) { if (isImageOrFile(item)) {
if ((item as Element).classList.contains(FILE_INFO_CLASS)) { if ((item as Element).classList.contains(FILE_INFO_CLASS)) {
this.sendFile(item, "file"); await this.sendFile(item, "file");
} else { } else {
this.sendFile(item, "image"); await this.sendFile(item, "image");
} }
continue; continue;
} }
if (item.textContent) { if (item.textContent) {
this.sendText(item.textContent); await this.sendText(item.textContent);
} }
} }
ChatLoggerService.logger?.debug("all messages sent"); ChatLoggerService.logger?.debug("all messages sent");
...@@ -88,13 +88,13 @@ export default class MessageInput extends Vue { ...@@ -88,13 +88,13 @@ export default class MessageInput extends Vue {
await xim.inputing(this.chatId); await xim.inputing(this.chatId);
} }
private sendText(text: string) { private async sendText(text: string) {
if (text && text.trim()) { if (text && text.trim()) {
const msg = { text: text.trim() }; const msg = { text: text.trim() };
if (this.source) { if (this.source) {
Object.assign(msg, { source: this.source }); Object.assign(msg, { source: this.source });
} }
this.sendMsg({ msgType: "text", msg: JSON.stringify(msg) }); return this.sendMsg({ msgType: "text", msg: JSON.stringify(msg) });
} }
} }
......
...@@ -618,6 +618,7 @@ i.msg-avatar { ...@@ -618,6 +618,7 @@ i.msg-avatar {
.no-selection { .no-selection {
user-select: none; user-select: none;
} }
.image-message { .image-message {
max-width: 300px; max-width: 300px;
box-sizing: content-box; box-sizing: content-box;
...@@ -626,6 +627,10 @@ i.msg-avatar { ...@@ -626,6 +627,10 @@ i.msg-avatar {
} }
} }
.pointer {
cursor: pointer;
}
.all { .all {
color: #4389f8; color: #4389f8;
} }
......
...@@ -8,8 +8,9 @@ ...@@ -8,8 +8,9 @@
:key="item.label" :key="item.label"
> >
<span class="data-key" <span class="data-key"
>{{ item.label }}{{ item.label ? ":" : "" }} </span >{{ item.label }}{{ item.label ? ":" : "" }}
><span class="data-value" v-html="item.template"></span> </span>
<span class="data-value" v-html="item.template"></span>
<span <span
class="operation_field" class="operation_field"
v-if="item.actions && item.actions.length > 0" v-if="item.actions && item.actions.length > 0"
...@@ -97,24 +98,21 @@ export default class ChatModelDetail extends Vue { ...@@ -97,24 +98,21 @@ export default class ChatModelDetail extends Vue {
private goTodetail() { private goTodetail() {
const path = `/${this.$route.params.project}.${this.$route.params.entrance}/detail/${this.model_name}/key/${this.id}`; const path = `/${this.$route.params.project}.${this.$route.params.entrance}/detail/${this.model_name}/key/${this.id}`;
this.openUrl(path);
this.hideChat();
}
private openUrl(path: string) {
if (this.drawer) { if (this.drawer) {
this.$emit("drawer", path); this.$emit("drawer", path);
} else { } else {
this.$router.push(path); this.$router.push(path);
} }
this.hideChat();
} }
private async execute_action(actionParams) { private async execute_action(actionParams) {
let { let { action_name, container, forward, confirm_caption, authed } =
action_name, actionParams;
container,
forward,
confirm_caption,
open_in_new_tab,
authed,
} = actionParams;
const x = this.detailRow; const x = this.detailRow;
const r: { v: number; id: number } = { v: 0, id: 0 }; const r: { v: number; id: number } = { v: 0, id: 0 };
r.id = x[this.keyField].value as number; r.id = x[this.keyField].value as number;
...@@ -217,17 +215,7 @@ export default class ChatModelDetail extends Vue { ...@@ -217,17 +215,7 @@ export default class ChatModelDetail extends Vue {
) { ) {
window.open(forward, "_blank"); window.open(forward, "_blank");
} else { } else {
if (this.global.$ssr && this.global.$vapperRootPath) { this.openUrl(forward);
forward =
"/" +
this.global.$vapperRootPath +
forward.replace(/^\//, "");
}
if (open_in_new_tab) {
window.open(`${forward}`, "_blank");
} else {
this.$router.push(forward);
}
} }
} }
} }
......
...@@ -3,6 +3,9 @@ import { Chat, Message } from "./../xim/models/chat"; ...@@ -3,6 +3,9 @@ import { Chat, Message } from "./../xim/models/chat";
class ChatCacheDatabaseController { class ChatCacheDatabaseController {
private db: IDBDatabase; private db: IDBDatabase;
private readonly messageDatabases = new Map<string, IDBDatabase>();
private uid = "";
private readonly version = 1; private readonly version = 1;
private readonly chatListKey = "chat-list"; private readonly chatListKey = "chat-list";
private readonly chatMessageKey = "chat-message"; private readonly chatMessageKey = "chat-message";
...@@ -10,16 +13,19 @@ class ChatCacheDatabaseController { ...@@ -10,16 +13,19 @@ class ChatCacheDatabaseController {
public setup(uid: string) { public setup(uid: string) {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (uid && indexedDB) { if (uid && indexedDB) {
const r = indexedDB.open(uid, this.version); const r = indexedDB.open((this.uid = uid), this.version);
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this; const that = this;
const setupDb = () => { const setupDb = () => {
if (that.db) { if (that.db) {
that.buildTables(that.db, that.chatListKey); try {
that.buildTables(that.db, that.chatMessageKey); that.buildTables(that.db, that.chatListKey);
console.log( console.log(
`build index database for chat completed, 100%` `build index database for chat completed, 100%`
); );
} catch (e) {
console.error(e);
}
} }
resolve(); resolve();
}; };
...@@ -42,6 +48,51 @@ class ChatCacheDatabaseController { ...@@ -42,6 +48,51 @@ class ChatCacheDatabaseController {
}); });
} }
private setupChatMessageDatabase(chat: number) {
const k = this.buildChatMessageKey(chat);
const t = this.messageDatabases.get(k);
if (!t) {
return new Promise<void>((resolve) => {
if (this.uid && indexedDB) {
const r = indexedDB.open(k, this.version);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
const setupDb = () => {
const db = that.messageDatabases.get(k);
try {
that.buildTables(db, this.chatMessageKey);
} catch (e) {
console.error(e);
}
setTimeout(() => resolve(), 200);
};
r.onsuccess = function (e) {
const db = (e.target as any).result;
that.messageDatabases.set(k, db);
setupDb();
};
r.onupgradeneeded = function (e) {
const db = (e.target as any).result;
that.messageDatabases.set(k, db);
setupDb();
};
r.onerror = function (e) {
console.log(
`chat message index database init failed, ${e}`
);
};
} else {
resolve();
}
});
}
return Promise.resolve();
}
private buildChatMessageKey(chat: number) {
return `u-${this.uid}-chat-${chat}`;
}
private buildTables(database: IDBDatabase, key: string, path?: string) { private buildTables(database: IDBDatabase, key: string, path?: string) {
if (!database.objectStoreNames.contains(key)) { if (!database.objectStoreNames.contains(key)) {
database.createObjectStore(key, { keyPath: path }); database.createObjectStore(key, { keyPath: path });
...@@ -73,17 +124,26 @@ class ChatCacheDatabaseController { ...@@ -73,17 +124,26 @@ class ChatCacheDatabaseController {
} }
const store = this.buildStore(this.chatListKey); const store = this.buildStore(this.chatListKey);
const r = store.getAll(); const r = store.getAll();
r.onsuccess = (o) => { r.onsuccess = (o) => resolve((o.target as any).result);
resolve((o.target as any).result);
};
r.onerror = () => resolve([]); r.onerror = () => resolve([]);
}); });
} }
public saveChatMessages(chat: number, items: Message[]) { private buildChatMessageStore(chat: number) {
const k = this.buildChatMessageKey(chat);
const db = this.messageDatabases.get(k) as IDBDatabase;
const transaction = db.transaction(this.chatMessageKey, "readwrite");
return transaction.objectStore(this.chatMessageKey);
}
public async saveChatMessages(chat: number, items: Message[]) {
if (this.db && items) { if (this.db && items) {
const store = this.buildStore(this.chatMessageKey); this.setupChatMessageDatabase(chat).finally(() => {
store.add(items, chat); const store = this.buildChatMessageStore(chat);
for (const item of items) {
store.add(item, item.id);
}
});
} }
} }
...@@ -92,12 +152,14 @@ class ChatCacheDatabaseController { ...@@ -92,12 +152,14 @@ class ChatCacheDatabaseController {
if (!this.db) { if (!this.db) {
return resolve([]); return resolve([]);
} }
const store = this.buildStore(this.chatMessageKey); this.setupChatMessageDatabase(chat).finally(() => {
const r = store.get(chat); const store = this.buildChatMessageStore(chat);
r.onsuccess = (o) => { const r = store.getAll();
resolve((o.target as any).result); r.onsuccess = (o) => {
}; resolve((o.target as any).result);
r.onerror = () => resolve([]); };
r.onerror = () => resolve([]);
});
}); });
} }
...@@ -106,20 +168,10 @@ class ChatCacheDatabaseController { ...@@ -106,20 +168,10 @@ class ChatCacheDatabaseController {
if (!this.db || !items.length) { if (!this.db || !items.length) {
return resolve(); return resolve();
} }
const store = this.buildStore(this.chatMessageKey); const store = this.buildChatMessageStore(chat);
const r = store.get(chat); for (const item of items) {
r.onsuccess = (o) => { store.add(item, item.id);
const cache = (o.target as any).result as Message[]; }
for (const item of items) {
if (!cache.find((i) => i.id === item.id)) {
cache.push(item);
}
}
const sort = cache.sort((i) => i.ts);
store.put(sort, chat);
resolve();
};
r.onerror = () => resolve();
}); });
} }
......
...@@ -97,7 +97,7 @@ export interface InputMessage { ...@@ -97,7 +97,7 @@ export interface InputMessage {
file?: File | null; file?: File | null;
} }
const chatStoreNamespace = namespace("chatStore"); const chatStore = namespace("chatStore");
const chatCache: { [key: number]: any } = {}; const chatCache: { [key: number]: any } = {};
...@@ -115,9 +115,12 @@ export function isImageOrFile(node: ChildNode) { ...@@ -115,9 +115,12 @@ export function isImageOrFile(node: ChildNode) {
@Component({ components: {} }) @Component({ components: {} })
export default class Input extends Vue { export default class Input extends Vue {
@chatStoreNamespace.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.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
protected readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
@Ref("input") @Ref("input")
private readonly messageInputBox!: HTMLDivElement; private readonly messageInputBox!: HTMLDivElement;
...@@ -306,12 +309,14 @@ export default class Input extends Vue { ...@@ -306,12 +309,14 @@ export default class Input extends Vue {
this.$emit("error", e); this.$emit("error", e);
reject(e); reject(e);
} }
}).then(() => { })
this.clearInput(); .then(() => {
if (this.chatId) { this.clearInput();
chatCache[this.chatId] = []; if (this.chatId) {
} chatCache[this.chatId] = [];
}); }
})
.finally(() => setTimeout(() => this.getMyChatList(), 120));
} }
/** /**
......
import type { UniplatSdk } from "uniplat-sdk"; import type { UniplatSdk } from "uniplat-sdk";
export const enum ChatRole {
Default = 25,
Admin = 85,
CustomerService = 92,
}
export interface Chat { export interface Chat {
chat_id: number; chat_id: number;
title: string; title: string;
......
...@@ -222,14 +222,12 @@ export default { ...@@ -222,14 +222,12 @@ export default {
[ChatStore.MUTATION_CLEAR_CHAT_TITLE](state) { [ChatStore.MUTATION_CLEAR_CHAT_TITLE](state) {
state[ChatStore.STATE_CURRENT_CHAT_TITLE] = ""; state[ChatStore.STATE_CURRENT_CHAT_TITLE] = "";
}, },
[ChatStore.MUTATION_SAVE_SINGLE_CHAT](state, v: ChatType) { [ChatStore.MUTATION_SAVE_SINGLE_CHAT](state, v: ChatType) {
state[ChatStore.STATE_SINGLE_CHAT] = v; state[ChatStore.STATE_SINGLE_CHAT] = v;
}, },
[ChatStore.MUTATION_CLEAR_SINGLE_CHAT](state) { [ChatStore.MUTATION_CLEAR_SINGLE_CHAT](state) {
state[ChatStore.STATE_SINGLE_CHAT] = null; state[ChatStore.STATE_SINGLE_CHAT] = null;
}, },
[ChatStore.MUTATION_SCROLL_TO_BOTTOM](state) { [ChatStore.MUTATION_SCROLL_TO_BOTTOM](state) {
state[ChatStore.STATE_FUNC_SCROLL_TO_BOTTOM](); state[ChatStore.STATE_FUNC_SCROLL_TO_BOTTOM]();
}, },
...@@ -266,9 +264,10 @@ export default { ...@@ -266,9 +264,10 @@ export default {
dbController.appendMessages(chat, [payload]); dbController.appendMessages(chat, [payload]);
} }
preCacheImgs([payload]).then(() => { preCacheImgs([payload]).then(() => {
setTimeout(() => { setTimeout(
state[ChatStore.STATE_FUNC_SCROLL_TO_BOTTOM](); () => state[ChatStore.STATE_FUNC_SCROLL_TO_BOTTOM](),
}, 100); 100
);
}); });
}, },
[ChatStore.MUTATION_REMOVE_SENDING_MESSAGE]: ( [ChatStore.MUTATION_REMOVE_SENDING_MESSAGE]: (
...@@ -350,9 +349,7 @@ export default { ...@@ -350,9 +349,7 @@ export default {
}, },
}, },
actions: { actions: {
async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit }) {
commit,
}) /* ...params: Parameters<ChatStore.ACTION_GET_MY_CHAT_LIST> */ {
let cache = await dbController.getChatList(); let cache = await dbController.getChatList();
if (cache && cache.length) { if (cache && cache.length) {
......
...@@ -312,7 +312,7 @@ export namespace ChatStore { ...@@ -312,7 +312,7 @@ export namespace ChatStore {
msgType: "text" | "image" | "file" | "voice" | "video"; msgType: "text" | "image" | "file" | "voice" | "video";
msg: string; msg: string;
ts?: number; ts?: number;
}) => void; }) => Promise<void>;
export const ACTION_TERINATE_CHAT = "结束会话"; export const ACTION_TERINATE_CHAT = "结束会话";
export type ACTION_TERINATE_CHAT = () => Promise<void>; export type ACTION_TERINATE_CHAT = () => Promise<void>;
export const ACTION_CHAT_ADD_MEMBERS = "添加成员"; export const ACTION_CHAT_ADD_MEMBERS = "添加成员";
......
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