Commit a5f82bc4 by Sixong.Zhu

chat

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