Commit b779691b by 杨铁龙

Merge branch 'master' into wx_master

parents 142ecbe7 fbedbee7
<template>
<div class="chat-room-con h-100 pos-rel">
<div class="chat-room-con pos-rel">
<div class="chat-panel">
<div class="chat-area h-100 d-flex flex-column" ref="chatBox">
<div
......@@ -43,25 +43,22 @@
import Chat from "@/customer-service/xim";
import { CustomerServiceEvent } from "../event";
const chatResizeKey1 = "chat-resize-1";
const chatResizeKey2 = "chat-resize-2";
@Component({ components: { MessageInput, messages } })
export default class ChatRoom extends Vue {
@Ref("chatBox") private readonly chatBox!: Element;
@Ref("top") private readonly refTop!: Element;
@Ref("bottom") private readonly refBottom!: Element;
@Ref("resize") private readonly refResize!: Element;
@Ref("chatBox") private readonly chatBox!: HTMLElement;
@Ref("top") private readonly refTop!: HTMLElement;
@Ref("bottom") private readonly refBottom!: HTMLElement;
@Ref("resize") private readonly refResize!: HTMLElement;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.Mutation(ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS)
private readonly clearChatMembers!: ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_INPUTING)
private readonly currentInputPeople!: ChatStore.STATE_CURRENT_CHAT_INPUTING;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID)
private readonly currentChatUniplatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER)
private readonly isChatMember!: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER;
......@@ -79,12 +76,6 @@
@Provide() showReadSummary = true;
@Watch("currentChatUniplatId")
private whenCurrentChatIdChanged(newValue: string, oldValue: string) {
if (Number(oldValue) === Number(newValue)) return;
this.clearChatMembers();
}
private get getCurrentInputingPeople() {
return this.currentInputPeople
.map(() => "" /* this.userInfo[k].name */)
......@@ -97,9 +88,9 @@
private dragControllerDiv(e: MouseEvent) {
const resize = this.refResize as any;
const top = this.refTop as HTMLElement;
const bottom = this.refBottom as HTMLElement;
const box = this.chatBox as HTMLElement;
const top = this.refTop;
const bottom = this.refBottom;
const box = this.chatBox;
const startY = e.clientY;
const originTop = resize.offsetTop;
......@@ -115,6 +106,9 @@
resize.style.top = moveLen + "px"; // 设置左侧区域的宽度
top.style.height = moveLen + "px";
bottom.style.height = bottomHeight + "px";
localStorage.setItem(chatResizeKey1, moveLen);
localStorage.setItem(chatResizeKey2, bottomHeight + "");
};
document.onmouseup = function () {
document.onmousemove = null;
......@@ -126,9 +120,29 @@
}
mounted() {
this.refBottom &&
((this.refBottom as HTMLElement).style.height =
this.chatBox.clientHeight - this.refTop.clientHeight + "px");
this.adjust();
}
private adjust() {
setTimeout(() => {
if (
localStorage.getItem(chatResizeKey1) &&
localStorage.getItem(chatResizeKey2) &&
this.hasInput
) {
const s1 = localStorage.getItem(chatResizeKey1) + "px";
const s2 = localStorage.getItem(chatResizeKey2) + "px";
this.refTop && (this.refTop.style.height = s1);
this.refResize && (this.refResize.style.top = s1);
this.refBottom && (this.refBottom.style.height = s2);
} else {
this.refBottom &&
((this.refBottom as HTMLElement).style.height =
this.chatBox.clientHeight -
this.refTop.clientHeight +
"px");
}
}, 800);
}
private openMessage(o: any) {
......@@ -148,6 +162,7 @@
),
300
);
this.$emit("send");
}
}
</script>
......@@ -158,7 +173,7 @@
width: 46px;
height: 20px;
line-height: 20px;
background: #22bd7a;
background-color: #22bd7a;
font-size: 13px;
border-radius: 2px;
color: #ffffff;
......@@ -223,6 +238,10 @@
top: calc(100% - 130px + 1px);
height: 6px;
width: 100%;
&:hover {
background-color: #eee;
}
}
}
.order-info-con {
......
......@@ -7,6 +7,8 @@ import { ChatUserInfoService } from "@/customer-service/utils/user-info";
@Component({ components: {} })
export default class ChatList extends Vue {
private nextTimer = 0;
@chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
protected readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
......@@ -91,4 +93,18 @@ export default class ChatList extends Vue {
}
return this.parseMesage(item);
}
/**
* 一分钟更新一次会话列表
*/
protected enableAutoRefresh() {
this.nextTimer = setTimeout(
() => this.getMyChatList().finally(() => this.enableAutoRefresh()),
60 * 1000
);
}
beforeDestroy() {
clearTimeout(this.nextTimer);
}
}
......@@ -3,21 +3,17 @@
:modal="false"
:before-close="close"
:visible="value"
custom-class="hide-header show-close padding-0 width-auto"
:show-close="false"
custom-class="transparent"
width="90%"
>
<div class="d-flex flex-column">
<div class="preview-title text-center">图片预览</div>
<div class="d-flex justify-content-center" style="min-width: 300px">
<img v-if="file" :src="file.url" :style="style" />
<div class="d-flex justify-content-center align-items-start">
<img v-if="file" :src="file.url" />
<i class="el-icon-close" @click="close"></i>
</div>
<div class="d-flex justify-content-center actions">
<span
class="d-flex align-items-center justify-content-center"
@click="set2Default"
>1:1</span
>
<a
class="d-flex align-items-center justify-content-center"
:href="file.url"
......@@ -31,77 +27,68 @@
</template>
<script lang="ts">
import { Component, Model, Prop, Vue } from "vue-property-decorator";
@Component({ components: {} })
export default class ImagePreview extends Vue {
@Model("update")
private value!: boolean;
import { Component, Model, Prop, Vue } from "vue-property-decorator";
@Prop()
private file!: { name: string; url: string };
@Component({ components: {} })
export default class ImagePreview extends Vue {
@Model("update")
private value!: boolean;
private style: {
"max-height": number | string;
"max-width": number | string;
} = {
"max-height": "300px",
"max-width": "600px",
};
@Prop()
private file!: { name: string; url: string };
private close() {
setTimeout(
() =>
(this.style = { "max-height": "300px", "max-width": "600px" }),
300
);
this.$emit("update", false);
}
private set2Default() {
this.style = { "max-height": "1600px", "max-width": "1600px" };
}
private close() {
this.$emit("update", false);
}
private get getAttachment() {
if (this.file) {
return this.file.name;
private get getAttachment() {
if (this.file) {
return this.file.name;
}
return "文件下载";
}
return "文件下载";
}
}
</script>
<style lang="less" scoped>
.preview-title {
font-size: 18px;
color: #333;
margin-bottom: 15px;
}
img {
max-width: 100%;
max-height: 100%;
.actions {
margin: 15px 0;
& + i {
top: -25px;
right: 20px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
padding: 5px;
font-size: 40px;
color: #fff;
display: table;
position: relative;
z-index: 1;
cursor: pointer;
}
}
> span,
a {
width: 30px;
height: 30px;
background-color: #7a7b7d;
color: #fff;
border-radius: 50%;
cursor: pointer;
.actions {
margin: 15px 0;
i {
a {
width: 50px;
height: 50px;
background-color: #7a7b7d;
color: #fff;
font-size: 20px;
}
border-radius: 50%;
cursor: pointer;
& + span {
margin-left: 15px;
}
}
i {
color: #fff;
font-size: 30px;
}
> a {
margin-left: 15px;
& + span {
margin-left: 15px;
}
}
}
}
</style>
......@@ -9,7 +9,7 @@
<script lang="ts">
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import ChatInput, {
FILE_INFO_CLASS,
isFileElement,
isImageOrFile,
} from "../hybrid-input/index.vue";
import { Message, MessageType } from "../model";
......@@ -51,6 +51,8 @@
@Ref("chat-input")
private readonly chatInput!: ChatInput;
private sending = false;
@Watch("chatRoomVisible")
private whenChatRoomShow() {
if (!this.chatRoomVisible) return;
......@@ -58,27 +60,39 @@
}
private async sendMessage(msg: ChildNode[]) {
if (this.chatIniting) {
if (this.chatIniting || this.sending) {
return;
}
const count = msg.length;
let finished = 0;
this.sending = true;
const onFinishedChanged = () => {
finished++;
if (finished === count) {
this.sending = false;
}
};
setTimeout(() => (this.sending = false), 3000);
for (const item of msg) {
if (isImageOrFile(item)) {
if ((item as Element).classList.contains(FILE_INFO_CLASS)) {
await this.sendFile(item, MessageType.File).catch((e) =>
this.onError(e)
);
if (isFileElement(item)) {
await this.sendFile(item, MessageType.File)
.catch((e) => this.onError(e))
.finally(onFinishedChanged);
} else {
await this.sendFile(item, MessageType.Image).catch((e) =>
this.onError(e)
);
await this.sendFile(item, MessageType.Image)
.catch((e) => this.onError(e))
.finally(onFinishedChanged);
}
continue;
}
if (item.textContent) {
await this.sendText(item.textContent).catch((e) =>
this.onError(e)
);
await this.sendText(item.textContent)
.catch((e) => this.onError(e))
.finally(onFinishedChanged);
} else {
onFinishedChanged();
}
}
this.$emit("sent");
......@@ -166,7 +180,8 @@
console.error(e);
this.setMsg2Failed(index);
this.chatInput && this.chatInput.updateUploadProgress(0);
this.chatInput &&
this.chatInput.updateUploadProgress(0);
});
}
}
......
<template>
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
<div>
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
</div>
<i
class="el-icon-warning-outline"
v-else-if="fileFailed2Load"
title="[语音加载失败]"
></i>
</div>
<i
class="el-icon-warning-outline"
v-else-if="fileFailed2Load"
title="[语音加载失败]"
></i>
<text-message v-model="value" v-if="backend" />
</div>
</template>
......@@ -92,6 +93,9 @@
<style lang="less" scoped>
.voice-message {
width: 200px;
background-color: #eee;
border-radius: 6px;
padding: 8px 10px;
&.can-play {
cursor: pointer;
......@@ -102,12 +106,6 @@
}
}
.inline-text {
position: absolute;
bottom: 0;
left: 40px;
}
.my-message {
.voice-message {
> div {
......
<template>
<div class="position-message" @click="openPosition">
<div class="d-flex justify-content-between align-items-center">
<span class="d-flex align-items-center">
<span class="title">{{ title }}</span>
<span class="d-flex align-items-center flex-fill">
<span class="title flex-fill">{{ title }}</span>
<span
v-for="item in tags"
:key="item.title"
......@@ -46,7 +46,43 @@
}
private get salary() {
return this.positionData.salary;
const max = this.positionData.max_salary;
const min = this.positionData.min_salary;
const formatSalary = (v: number, type?: "Y" | "K") => {
if (type === "K") {
return {
v: parseFloat((v / 1000).toFixed(2)),
unit: "K",
};
}
if (+v < 1000) {
return {
v,
unit: "",
};
} else {
return {
v: parseFloat((v / 1000).toFixed(2)),
unit: "K",
};
}
};
if (!max && !min) {
return "面议";
}
if (+min >= +max) {
const v = formatSalary(min);
return `${v.v}${v.unit}`;
}
const formatMin = formatSalary(min);
const formatMax = formatSalary(max);
if (formatMin.unit === formatMax.unit) {
return `${formatMin.v}-${formatMax.v} ${formatMax.unit}`;
} else {
const formatMin = formatSalary(min, "K");
const formatMax = formatSalary(max, "K");
return `${formatMin.v}-${formatMax.v} K`;
}
}
private get positionBody() {
......@@ -64,7 +100,9 @@
private get tail() {
return [
this.positionData.company_name,
this.positionData.business_scope,
this.positionData.business_scope !== '0'
? this.positionData.business_scope
: "",
];
}
......
......@@ -50,7 +50,6 @@
import { dbController } from "../database";
import { getLastMessageId } from "../store";
import { CustomerServiceEvent } from "../event";
import xim from "../xim/xim";
@Component({ components: { message, ImagePreview, VideoPreview } })
export default class MessageList extends Vue {
......@@ -95,7 +94,7 @@
private get messages() {
if (this.historyMessage) {
if (this.sendingMessages) {
if (this.sendingMessages && this.sendingMessages.length) {
return [...this.historyMessage, ...this.sendingMessages].filter(
(i) => i.chat_id === this.chatId && i.id > 0
);
......@@ -103,7 +102,7 @@
return this.historyMessage;
}
if (this.sendingMessages) {
if (this.sendingMessages && this.sendingMessages.length) {
return this.sendingMessages.filter(
(i) => i.chat_id === this.chatId && i.id > 0
);
......@@ -190,15 +189,6 @@
public created() {
this.handleScrollWrapper();
this.onNewMessage((e) => {
if (e.type === MessageType.Withdraw) {
const ids = xim.withDrawMsgHandle(e);
this.executeWithDraw(ids);
dbController
.removeMessage(e.chat_id, ids)
.finally(() => this.refresh());
}
});
}
public mounted() {
......
......@@ -33,10 +33,17 @@
<div v-if="isMyMessage" class="msg-read pos-rel">
<span
@click="openReaderList"
class="pointer"
:class="{ all: isAllRead }"
:class="[
isAllRead ? 'all' : 'not-all',
{ pointer: isChatMember },
]"
>
<template v-if="isAllRead">全部已读</template>
<template v-if="isAllRead">
<i
class="el-icon-circle-check"
title="全部已读"
></i>
</template>
<template
v-else-if="manualReaded || data.read_count"
>{{
......@@ -73,12 +80,7 @@
/>
<avatar
v-if="!isQuestionAnswerMessage && !isWithdrawMessage"
:src="
chatRole === 'admin' ||
chatRole === 'customer-service'
? defaultAvatar
: avatar
"
:src="avatar || defaultAvatar"
shape="circle"
/>
</div>
......@@ -153,7 +155,6 @@
import avatar from "@/customer-service/components/avatar.vue";
import { chatStore, ChatStore } from "@/customer-service/store/model";
import ximInstance from "../xim/xim";
import { dbController } from "../database";
import ImageMessage from "./message-item/image-message.vue";
import FileMessage from "./message-item/file-message.vue";
import AudioMessage from "./message-item/audio-message.vue";
......@@ -168,12 +169,12 @@
import PayMessage from "./message-item/pay-message.vue";
import NotifyMessage from "./message-item/notify-message.vue";
import { ChatRole } from "@/customer-service/model";
import { getUserMapping } from "../utils/user-info";
import { ChatUserInfoService, getUserMapping } from "../utils/user-info";
import Xim from "@/customer-service/xim";
import { CustomerServiceEvent, MessageEvent } from "../event";
import { PayMessageBody } from "../xim/models/chat";
const twoMinutes = 2 * 60 * 1000;
const oneDay = 24 * 60 * 60 * 1000;
const messageMapping = new Map<dto.MessageType, string>([
[dto.MessageType.Image, "image-message"],
......@@ -219,8 +220,8 @@
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_USER_UID)
private readonly chatMyId!: ChatStore.STATE_CHAT_CURRENT_USER_UID;
@chatStore.Getter(ChatStore.STATE_CURRENT_CHAT_MEMBERS)
private readonly allChatMembers!: ChatStore.STATE_CURRENT_CHAT_MEMBERS;
@chatStore.State(ChatStore.STATE_ALL_HISTORY_CHAT_MEMBERS)
private readonly allChatMembers!: ChatStore.STATE_ALL_HISTORY_CHAT_MEMBERS;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
......@@ -228,9 +229,6 @@
@chatStore.State(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;
@chatStore.Action(ChatStore.ACTION_SET_HANDLED)
private readonly setHandled!: ChatStore.ACTION_SET_HANDLED;
......@@ -266,12 +264,22 @@
private org = "";
private manualAllRead = false;
private manualReaded = 0;
private refetchUsername = "";
private refetchUserIcon = "";
private readerListOffset = false;
private defaultMessageHandledStatus = dto.MessageHandled.Default;
private isWithdraw = true;
private get isSystemMessage() {
return (
this.messageBody &&
this.messageBody.eid &&
+this.messageBody.eid === 0
);
}
private get isPayMessage() {
return dto.MessageTypeController.isPayMessage(this.data.type);
}
......@@ -281,14 +289,17 @@
if (this.isPayMessage) {
return true;
}
if (this.needReadTip) {
return new Date().valueOf() - this.data.ts * 1000 < twoMinutes;
if (this.needReadTip && this.isMyMessage) {
return new Date().valueOf() - this.data.ts * 1000 < oneDay;
}
}
return false;
}
private get needReadTip() {
if (this.isSystemMessage) {
return false;
}
return (
this.data.type !== dto.MessageType.Pay &&
this.data.type !== dto.MessageType.Refund &&
......@@ -344,6 +355,15 @@
return true;
}
// 系统推送的消息或老用户(一般是客服,eid为负数),默认为客服发送
if (
this.messageBody &&
this.messageBody.eid &&
+this.messageBody.eid <= 0
) {
return true;
}
if (
this.backend &&
this.messageBody &&
......@@ -370,24 +390,37 @@
}
private get userName() {
if (this.refetchUsername) {
return this.refetchUsername;
}
if (this.chatMembers) {
const t = this.chatMembers.find((i) => i.eid === this.data.eid);
if (t) {
return this.getFilterUsername(t.alias_name || t.name);
const name = this.getFilterUsername(
t.alias_name as string,
t.name
);
if (name) {
return name;
}
}
}
return "";
this.refetchUsername4Message();
return this.refetchUsername;
}
private getFilterUsername(name: string) {
private getFilterUsername(name1: string, name2: string) {
const backend = Xim.isBackend();
if (
this.currentChat &&
this.currentChat.catalog === "福利宝" &&
this.chatRole === "customer-service"
) {
return `采购顾问 ${name}`;
return backend && name1
? `采购顾问 ${name1}(${name2})`
: `采购顾问 ${name1 || name2}`;
}
return name;
return backend && name1 ? `${name1}(${name2})` : name1 || name2;
}
private get avatar() {
......@@ -400,7 +433,7 @@
}
}
return "";
return this.refetchUserIcon;
}
private get defaultAvatar() {
......@@ -525,14 +558,7 @@
},
});
}
ximInstance.withdraw(this.chatId, this.data.id).finally(() => {
dbController
.removeMessage(this.chatId, [this.data.id])
.finally(() => {
this.executeWithDraw([this.data.id]);
this.$emit("withdraw", this.data.id);
});
});
ximInstance.withdraw(this.chatId, this.data.id);
}
private hoverWithdraw() {
......@@ -543,12 +569,14 @@
return false;
}
return (this.isWithdraw =
new Date().valueOf() - this.data.ts * 1000 < twoMinutes);
new Date().valueOf() - this.data.ts * 1000 < oneDay);
}
private openReaderList(e: MouseEvent) {
this.readerListOffset = e.x < 450;
this.readListVisibility = true;
if (this.isChatMember) {
this.readerListOffset = e.x < 450;
this.readListVisibility = true;
}
}
private executeHandled() {
......@@ -576,6 +604,24 @@
private openMessage(o: any) {
CustomerServiceEvent.emit(this, o);
}
private refetchUsername4Message() {
if (this.data && this.data.eid) {
ChatUserInfoService.getUserInfo(this.data.eid).then((r) => {
if (r) {
if (Xim.isBackend() && r.alias_name) {
this.refetchUsername = `${r.alias_name}(${
r.name || r.phone
})`;
} else {
this.refetchUsername =
r.alias_name || r.name || r.phone;
}
r.icon && (this.refetchUserIcon = r.icon);
}
});
}
}
}
</script>
......@@ -694,7 +740,12 @@
}
.all {
color: #4389f8;
color: #ccc;
opacity: 0.7;
}
.not-all {
color: #077aec;
}
.match-keyword {
......
......@@ -79,6 +79,61 @@ class DevAppTools {
});
});
}
public getDataByKey(key: string) {
return new Promise<any>((resolve) => {
this.onReady().finally(() => {
if (!this.db) {
return resolve(false);
}
setTimeout(() => {
const store = this.buildStore(this.table);
const r = store.get(key);
r.onsuccess = ((o) => {
const result = (o.target as any).result;
if (result) {
resolve(result.content)
} else {
resolve(result)
}
});
r.onerror = () => resolve(false);
}, 300);
});
});
}
public addData(data: any, key: string) {
return new Promise<boolean>((resolve) => {
this.onReady().finally(() => {
if (!this.db) {
return resolve(false);
}
setTimeout(() => {
const store = this.buildStore(this.table);
const r = store.add({ value: key, content: data });
r.onsuccess = () => resolve(true);
r.onerror = () => resolve(false);
}, 300);
});
});
}
public deleteData(key: string) {
return new Promise<boolean>((resolve) => {
this.onReady().finally(() => {
if (!this.db) {
return resolve(false);
}
setTimeout(() => {
const store = this.buildStore(this.table);
const r = store.delete(key);
r.onsuccess = () => resolve(true);
r.onerror = () => resolve(false);
}, 300);
});
});
}
}
export const devAppTools = new DevAppTools();
......@@ -152,8 +152,12 @@ class ChatCacheDatabaseController {
public saveChatList(items: Chat[]) {
if (this.db) {
const store = this.buildStore(this.chatListKey);
for (const item of items) {
store.add(item, item.id);
if (items && items.length) {
for (const item of items) {
store.add(item, item.id);
}
} else {
store.clear();
}
}
}
......@@ -375,7 +379,7 @@ class ChatCacheDatabaseController {
public appendMessages(chat: number, items: Message[]) {
return new Promise<void>(resolve => {
if (!this.db || !items.length) {
if (!this.db || !items || !items.length) {
return resolve();
}
const store = this.buildChatMessageStore(chat);
......@@ -459,49 +463,6 @@ class ChatCacheDatabaseController {
});
});
}
public syncChats(chats: number[]) {
return new Promise<void>(resolve => {
if (this.db) {
this.getChatList().then(r => {
const set = new Set<number>(chats);
let finished = 0;
let removing = [];
for (const item of r) {
if (!set.has(item.id)) {
removing.push(item.id);
}
}
for (const item of removing) {
this.removeChatAndMessages(item).finally(() => {
finished++;
if (finished === removing.length) {
resolve();
}
});
}
!removing.length && resolve();
});
} else {
resolve();
}
});
}
private removeChatAndMessages(chat: number) {
this.setupChatMessageDatabase(chat).finally(() => {
const store = this.buildChatMessageStore(chat);
const r = store.clear();
r.onsuccess = () => {
indexedDB.deleteDatabase(this.buildChatMessageKey(chat));
};
});
return this.removeChatFromList(chat);
}
}
export const dbController = new ChatCacheDatabaseController();
......@@ -125,6 +125,10 @@
);
}
export function isFileElement(node: ChildNode) {
return (node as Element).classList.contains(FILE_INFO_CLASS);
}
const limitedFileExtension = [
"ppt",
"pptx",
......@@ -149,9 +153,6 @@
@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;
......@@ -339,16 +340,18 @@
if (e.shiftKey || e.ctrlKey || e.altKey) {
return;
}
const data = this.getNodeListFromInputBox();
this.$emit("send", data);
this.clearInput();
if (this.chatId) {
chatCache[this.chatId] = [];
}
// 避免按一下enter键多次触发发送
if (this.reloadTimer) {
clearTimeout(this.reloadTimer);
}
this.reloadTimer = setTimeout(() => this.getMyChatList(), 120);
this.reloadTimer = setTimeout(() => {
const data = this.getNodeListFromInputBox();
this.$emit("send", data);
this.clearInput();
if (this.chatId) {
chatCache[this.chatId] = [];
}
}, 120);
}
/**
......@@ -366,30 +369,17 @@
*/
private combine(nodes: ChildNode[]) {
const sendingNodes: ChildNode[] = [];
let needCreateNewNode = false;
let text = "";
for (const item of nodes) {
if (!isImageOrFile(item) && item.textContent) {
if (needCreateNewNode) {
text = "";
needCreateNewNode = false;
}
text += item.textContent;
} else {
needCreateNewNode = true;
if (text) {
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
if (isImageOrFile(item)) {
sendingNodes.push(item);
continue;
}
if (item.textContent) {
const text = item.textContent;
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
}
if (text) {
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
return sendingNodes;
......
......@@ -174,15 +174,15 @@ export const imItems = [
// 亲亲小保
{
type: IMDomainType.社保客服,
title: "社保客服",
title: "在线咨询",
},
{
type: IMDomainType.pc网站咨询,
title: "pc网站咨询",
title: "在线咨询",
},
{
type: IMDomainType.手机官网咨询,
title: "手机官网咨询",
title: "在线咨询",
},
{
type: IMDomainType.问答动态提醒,
......
......@@ -115,8 +115,21 @@ class WebMonitor {
options.userAgent &&
msg.push(`UserAgent: ${window.navigator.userAgent}`);
r.config &&
r.config.data &&
msg.push(`Payload: ${JSON.stringify(r.config.data)}`);
r.config.params &&
msg.push(`Params: ${JSON.stringify(r.config.params)}`);
if (r.config && r.config.data) {
const form = r.config.data as FormData;
if (form.getAll) {
const p = form.getAll("parameters");
for (const item of p) {
msg.push(`Payload: ${item}`);
}
} else {
msg.push(
`Payload: ${JSON.stringify(r.config.data)}`
);
}
}
msg.push(
`Exception: ${(
......
......@@ -138,16 +138,19 @@ class OrderService {
r.pageData.rows,
orderPayItemPredict
);
items = items.filter(
(i) =>
i.status !== PayStatus.Deleted &&
i.status !== PayStatus.Cancel
);
if (!withActions) {
items = items.filter(
(i) =>
i.status !== PayStatus.Deleted &&
i.status !== PayStatus.Cancel
);
}
if (withActions) {
for (let i = 0; i < r.pageData.rows.length; i++) {
r.pageData &&
r.pageData.rows &&
r.pageData.rows[i] &&
items[i] &&
(items[i].actions = r.pageData.rows[i].actions);
}
}
......
......@@ -73,6 +73,11 @@ export namespace ChatStore {
| readonly (dto.ChatMember & dto.ChatMemberExtraInfo)[]
| null;
export const STATE_ALL_HISTORY_CHAT_MEMBERS = "当前会话历史所有参与者";
export type STATE_ALL_HISTORY_CHAT_MEMBERS =
| readonly (dto.ChatMember & dto.ChatMemberExtraInfo)[]
| null;
export const STATE_CURRENT_CHAT_TITLE = "会话标题";
export type STATE_CURRENT_CHAT_TITLE = string;
......@@ -247,12 +252,6 @@ export namespace ChatStore {
keyword?: string
) => Promise<ChatType[]>;
export const ACTION_SYNC_MY_CHAT_LIST = "同步我的会话列表";
export type ACTION_SYNC_MY_CHAT_LIST = () => void;
export const ACTION_FORCE_RELOAD_CHAT_LIST = "重新获取我的会话列表";
export type ACTION_FORCE_RELOAD_CHAT_LIST = () => Promise<ChatType[]>;
export const ACTION_REBUILD_UNREAD_MESSAGE_COUNT = "重新计算未读消息数";
export type ACTION_REBUILD_UNREAD_MESSAGE_COUNT = () => void;
......
import Chat from "../xim";
import { orderService } from '../service/order';
import { orderService } from "../service/order";
export type ChatInfo = {
[eid: string]: any;
......@@ -10,7 +10,8 @@ const chatInfo: ChatInfo = {};
export const getChatModel = () => chatInfo;
const loadingKeys = new Set<string>();
let waitingAction: { key: string; resolve: (d: ChatModelInfoData) => void }[] = [];
let waitingAction: { key: string; resolve: (d: ChatModelInfoData) => void }[] =
[];
export interface ChatModelInfoData {
uniplatId: string | number;
......@@ -25,7 +26,10 @@ function buildCache() {
if (!model2DetailNameMapping.size) {
// 用户端默认不使用chat内置的detailName(这个专属于服务端),所以这里加一层内置转换
if (!Chat.isBackend()) {
model2DetailNameMapping.set(orderService.generalOrder, orderService.generalOrderDefaultDetailName);
model2DetailNameMapping.set(
orderService.generalOrder,
orderService.generalOrderDefaultDetailName
);
}
}
}
......@@ -57,12 +61,6 @@ export async function getChatModelInfo(
data: d,
} as ChatModelInfoData);
}
return Promise.resolve({
uniplatId: 0,
chat_id: 0,
uniplat_version: 0,
data: d,
});
}
loadingKeys.add(key);
......@@ -71,26 +69,26 @@ export async function getChatModelInfo(
.detail(id + "", detail)
.query();
const data = info;
chatInfo[key] = data;
info && info.row.UniplatChatId && (chatInfo[key] = data);
loadingKeys.delete(key);
const o = (
info.row && info.row.UniplatChatId
? {
uniplatId: info.row.UniplatChatId.value,
chat_id: +(info.row.UniplatImChatId.value as string),
uniplat_version: 0,
data,
}
uniplatId: info.row.UniplatChatId.value,
chat_id: +(info.row.UniplatImChatId.value as string),
uniplat_version: 0,
data,
}
: {
uniplatId: 0,
chat_id: 0,
uniplat_version: 0,
data,
}
uniplatId: 0,
chat_id: 0,
uniplat_version: 0,
data,
}
) as ChatModelInfoData;
let removing = [];
const removing = [];
for (const item of waitingAction) {
if (item.key === key) {
item.resolve(o);
......
......@@ -56,6 +56,17 @@ export function uuid() {
return s.join("")
}
export function copyTextToClipboard(text: string) {
const input = document.createElement("input");
input.setAttribute("readonly", "readonly");
input.setAttribute("value", text);
document.body.appendChild(input);
input.select();
const ret = document.execCommand("copy");
document.body.removeChild(input);
return ret;
}
const URL_REGEX =
/((?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$]))/gim
......
import { UniplatSdk } from "uniplat-sdk";
import Chat from "../xim";
export type UserMapping = {
[eid: string]: {
name: string;
phone: string;
icon: string;
alias_name: string;
};
};
export interface ChatUserSummaryInfo {
name: string;
phone: string;
icon: string;
alias_name: string;
}
export type UserMapping = { [eid: string]: ChatUserSummaryInfo };
const userMapping: UserMapping = {};
......@@ -25,6 +25,12 @@ interface UserInfo {
export const getUserMapping = () => userMapping;
const loadingKeys = new Set<string>();
let waitingAction: {
key: string;
resolve: (d: ChatUserSummaryInfo) => void;
}[] = [];
export class ChatUserInfoService {
public static async getUserInfo(eid: string, sdk?: UniplatSdk) {
if (userMapping[eid]) {
......@@ -33,6 +39,15 @@ export class ChatUserInfoService {
if (!+eid || +eid < 0) {
return { name: "", phone: "", icon: "", alias_name: "" };
}
if (loadingKeys.has(eid)) {
return new Promise<ChatUserSummaryInfo>((resolve) =>
waitingAction.push({ key: eid, resolve })
);
}
loadingKeys.add(eid);
const info = await (sdk || Chat.getSdk())
.domainService(
"passport",
......@@ -47,6 +62,21 @@ export class ChatUserInfoService {
alias_name: info.alias_name,
};
userMapping[eid] = data;
const removing = [];
for (const item of waitingAction) {
if (item.key === eid) {
item.resolve(data);
removing.push(item.key);
}
}
for (const item of removing) {
waitingAction = waitingAction.filter((i) => i.key !== item);
}
loadingKeys.delete(eid);
return data;
}
......
......@@ -154,7 +154,7 @@ class Chat {
.finally(() => {
this.registerXimEvent();
if (xim.isConnected()) {
resolve();
setTimeout(resolve, 200);
} else {
reject(new Error(`xim is not connected`));
}
......
......@@ -55,6 +55,7 @@ export interface Chat {
biz_type_code: string;
business_data?: string;
detail_name?: string;
keyword?: string;
}
export interface Message {
......@@ -230,6 +231,8 @@ export interface PositionMessage {
company_name: string;
business_scope: string;
post_id: number;
max_salary: number;
min_salary: number;
}
export interface CsUser {
......
......@@ -165,6 +165,7 @@ export class Xim {
lid = 0,
rid = 0,
limit = DefaultMsgPageSize,
// = 0 正序(最新的消息在最下面),=1 倒序(最新的消息在最上面)
desc: boolean,
p?: { isMember: boolean; model: string; obj: string }
): Promise<Message[]> {
......@@ -195,8 +196,12 @@ export class Xim {
.getSdk()
.getAxios()
.get<any, Message[]>(
`/general/xim/model/${p.model}/${p.obj}/msgs?lid=${lid}&rid=${rid}&limit=${limit}&desc=${desc ? 0 : 1}`
)
`/general/xim/model/${p.model}/${
p.obj
}/msgs?lid=${lid}&rid=${rid}&limit=${limit}&desc=${
desc ? 1 : 0
}`
);
}
private setMessagesRead(chatId: number, msg: Message[]) {
......@@ -431,9 +436,12 @@ export class Xim {
vue.$once("hook:beforeDestroy", () => this.off("msg", action));
}
public withDrawMsgHandle(e: Message) {
const ids = e.ref_id ? [e.ref_id] : e.msg.startsWith("[") ? JSON.parse(e.msg) : [+e.msg];
return ids;
public withDrawMsgHandle(e: Message): number[] {
return e.ref_id
? [e.ref_id]
: e.msg.startsWith("[")
? JSON.parse(e.msg)
: [+e.msg];
}
}
......
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