Commit 59e595ef by 杨铁龙

Merge commit '178de50f' into wx

parents 01ea14ef 178de50f
.h-100 {
height: 100%;
}
// 此部分是参考常见栅格样式,提供常用分割布局的全局class
.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-10,
.col-11,
.col-12,
.col,
.col-auto,
.col-sm-1,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-sm,
.col-sm-auto,
.col-md-1,
.col-md-2,
.col-md-3,
.col-md-4,
.col-md-5,
.col-md-6,
.col-md-7,
.col-md-8,
.col-md-9,
.col-md-10,
.col-md-11,
.col-md-12,
.col-md,
.col-md-auto,
.col-lg-1,
.col-lg-2,
.col-lg-3,
.col-lg-4,
.col-lg-5,
.col-lg-6,
.col-lg-7,
.col-lg-8,
.col-lg-9,
.col-lg-10,
.col-lg-11,
.col-lg-12,
.col-lg,
.col-lg-auto,
.col-xl-1,
.col-xl-2,
.col-xl-3,
.col-xl-4,
.col-xl-5,
.col-xl-6,
.col-xl-7,
.col-xl-8,
.col-xl-9,
.col-xl-10,
.col-xl-11,
.col-xl-12,
.col-xl,
.col-xl-auto {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
.col {
-ms-flex-preferred-size: 0;
flex-basis: 0;
-ms-flex-positive: 1;
flex-grow: 1;
max-width: 100%;
}
.row-cols-1 > * {
-ms-flex: 0 0 100%;
flex: 0 0 100%;
max-width: 100%;
}
.row-cols-2 > * {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
.row-cols-3 > * {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.row-cols-4 > * {
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
.row-cols-5 > * {
-ms-flex: 0 0 20%;
flex: 0 0 20%;
max-width: 20%;
}
.row-cols-6 > * {
-ms-flex: 0 0 16.666667%;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-auto {
-ms-flex: 0 0 auto;
flex: 0 0 auto;
width: auto;
max-width: 100%;
}
.col-1 {
-ms-flex: 0 0 8.333333%;
flex: 0 0 8.333333%;
max-width: 8.333333%;
}
.col-2 {
-ms-flex: 0 0 16.666667%;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-3 {
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
.col-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.col-5 {
-ms-flex: 0 0 41.666667%;
flex: 0 0 41.666667%;
max-width: 41.666667%;
}
.col-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
.col-7 {
-ms-flex: 0 0 58.333333%;
flex: 0 0 58.333333%;
max-width: 58.333333%;
}
.col-8 {
-ms-flex: 0 0 66.666667%;
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
.col-9 {
-ms-flex: 0 0 75%;
flex: 0 0 75%;
max-width: 75%;
}
.col-10 {
-ms-flex: 0 0 83.333333%;
flex: 0 0 83.333333%;
max-width: 83.333333%;
}
.col-11 {
-ms-flex: 0 0 91.666667%;
flex: 0 0 91.666667%;
max-width: 91.666667%;
}
.col-12 {
-ms-flex: 0 0 100%;
flex: 0 0 100%;
max-width: 100%;
}
.d-flex {
display: -ms-flexbox;
display: flex;
}
.d-inline-flex {
display: -ms-inline-flexbox;
display: inline-flex;
}
.flex-row {
-ms-flex-direction: row;
flex-direction: row;
}
.flex-column {
-ms-flex-direction: column;
flex-direction: column;
}
.flex-row-reverse {
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
}
.flex-column-reverse {
-ms-flex-direction: column-reverse;
flex-direction: column-reverse;
}
.flex-wrap {
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.flex-nowrap {
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
}
.flex-wrap-reverse {
-ms-flex-wrap: wrap-reverse;
flex-wrap: wrap-reverse;
}
.flex-fill {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
}
.flex-none {
flex: none;
}
.flex-grow-0 {
-ms-flex-positive: 0;
flex-grow: 0;
}
.flex-grow-1 {
-ms-flex-positive: 1;
flex-grow: 1;
}
.flex-shrink-0 {
-ms-flex-negative: 0;
flex-shrink: 0;
}
.flex-shrink-1 {
-ms-flex-negative: 1;
flex-shrink: 1;
}
.justify-content-start {
-ms-flex-pack: start;
justify-content: flex-start;
}
.justify-content-end {
-ms-flex-pack: end;
justify-content: flex-end;
}
.justify-content-center {
-ms-flex-pack: center;
justify-content: center;
}
.justify-content-between {
-ms-flex-pack: justify;
justify-content: space-between;
}
.justify-content-around {
-ms-flex-pack: distribute;
justify-content: space-around;
}
.align-items-start {
-ms-flex-align: start;
align-items: flex-start;
}
.align-items-end {
-ms-flex-align: end;
align-items: flex-end;
}
.align-items-center {
-ms-flex-align: center;
align-items: center;
}
.align-items-baseline {
-ms-flex-align: baseline;
align-items: baseline;
}
.align-items-stretch {
-ms-flex-align: stretch;
align-items: stretch;
}
.align-content-start {
-ms-flex-line-pack: start;
align-content: flex-start;
}
.align-content-end {
-ms-flex-line-pack: end;
align-content: flex-end;
}
.align-content-center {
-ms-flex-line-pack: center;
align-content: center;
}
.align-content-between {
-ms-flex-line-pack: justify;
align-content: space-between;
}
.align-content-around {
-ms-flex-line-pack: distribute;
align-content: space-around;
}
.align-content-stretch {
-ms-flex-line-pack: stretch;
align-content: stretch;
}
.align-self-auto {
-ms-flex-item-align: auto;
align-self: auto;
}
.align-self-start {
-ms-flex-item-align: start;
align-self: flex-start;
}
.align-self-end {
-ms-flex-item-align: end;
align-self: flex-end;
}
.align-self-center {
-ms-flex-item-align: center;
align-self: center;
}
.align-self-baseline {
-ms-flex-item-align: baseline;
align-self: baseline;
}
.align-self-stretch {
-ms-flex-item-align: stretch;
align-self: stretch;
}
.no-bottom-scrollbar { .no-bottom-scrollbar {
> .el-scrollbar__wrap { > .el-scrollbar__wrap {
overflow-x: hidden; overflow-x: hidden;
......
<template>
<div class="chat-con" :class="{ userMode, isSingle: isSingleChat }" v-if="chatId != null">
<ChatTitle :close="hide" class="chat-title" @updateActive="$emit('updateActive', $event)" />
<div class="chat-area-con" :class="{isSingle: isSingleChat, needSearch: !modelName}">
<div v-if="chatId != null" class="h-100 chat-area">
<chat-room />
</div>
<div class="chat-panel pos-rel" v-if="!userMode">
<el-tabs class="chat-panel-tabs h-100" v-model="currentTab" v-if="currentChat && !refreshFlag">
<el-tab-pane label="数据" name="one" class="h-100">
<ModelDetail
:model_name="currentChat.business_data.model_name"
:id="currentChat.business_data.obj_id"
:name="currentChat.business_data.detail_name"
/>
</el-tab-pane>
<el-tab-pane
class="h-100"
:label="`成员${chatMembers.length}人`"
name="two"
>
<ChatMembers />
</el-tab-pane>
<el-tab-pane class="h-100" label="工作流" name="three">
<workflow
:model_name="currentChat.business_data.model_name"
:id="currentChat.business_data.obj_id"
:name="currentChat.business_data.detail_name"
/>
</el-tab-pane>
<el-tab-pane class="h-100" label="备注" name="four">
<remarkList
:isInSmallPage="true"
:modelName="currentChat.business_data.model_name"
:associateId="currentChat.business_data.obj_id"
/>
</el-tab-pane>
<el-tab-pane label="回复" name="five" class="h-100">
<MsgShortCut />
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch, Prop } from "vue-property-decorator";
import remarkList from "../components/common/remarkList.vue";
import ChatMembers from "./components/chat-members.vue";
import ChatRoom from "./components/chat-room.vue";
import ChatTitle from "./components/chat-title.vue";
import MessageList from "./components/message-list.vue";
import ModelDetail from "./components/model-detail.vue";
import MsgShortCut from "./components/msg-shortcut.vue";
import workflow from "./components/workflow.vue";
import buttonThrottle from "./utils/button-throttle";
import { ChatStore, chatStore } from "@/customer-service/store/model";
@Component({
components: {
MsgShortCut,
MessageList,
ChatRoom,
ChatMembers,
remarkList,
ChatTitle,
ModelDetail,
workflow,
},
})
export default class Chat extends Vue {
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CURRENT_CHAT)
private readonly currentChat!: ChatStore.GETTER_CURRENT_CURRENT_CHAT;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_IS_SINGLE)
private readonly isSingleChat: ChatStore.STATE_CHAT_DIALOG_IS_SINGLE;
@chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT)
private readonly hide!: ChatStore.MUTATION_HIDE_CHAT;
@chatStore.Action(ChatStore.ACTION_TERINATE_CHAT)
private readonly _terminate!: ChatStore.ACTION_TERINATE_CHAT;
@chatStore.Action(ChatStore.ACTION_CHAT_ADD_MEMBERS)
private readonly _addMember!: ChatStore.ACTION_CHAT_ADD_MEMBERS;
private userMode = false;
private refreshFlag = false;
private currentTab = "one";
private get chatMembersId() {
return this.chatMembers.map((k) => +k.eid);
}
@buttonThrottle()
private terminate() {
this._terminate();
}
@Watch("chatId") chatIdUpdate() {
this.refreshFlag = true;
this.$nextTick(() => {
this.refreshFlag = false;
})
}
@Prop(String) modelName: string;
}
</script>
<style lang="less" scoped>
.chat-area-con {
height: 100%;
&.isSingle {
height: 70vh;
}
&.needSearch {
height: calc(100% - 60px);
}
}
.chat-con {
height: 100%;
--chat-panel-width: 350px;
font-size: 13px;
color: black;
&.isSingle {
height: 70vh;
}
&.userMode {
--chat-panel-width: 0px;
.chat-title {
/deep/ .title-buttons {
.button {
display: none;
}
}
}
}
}
.chat-area,
.chat-panel {
display: inline-block;
vertical-align: top;
}
.chat-area {
width: calc(100% - var(--chat-panel-width) - 2px);
}
.chat-panel {
position: relative;
width: var(--chat-panel-width);
border-left: 1px solid #e1e1e1;
height: calc(100% - 2px);
}
.buttons {
position: absolute;
left: 0;
right: 0;
bottom: 20px;
text-align: center;
}
.chat-panel {
/deep/ .el-tabs__item {
padding: 0 18px;
}
}
.chat-panel-tabs {
/deep/ .el-tabs__content {
height: calc(100% - 54px);
}
/deep/ .el-tabs__nav-wrap {
padding-left: 15px;
}
}
</style>
<template> <template>
<el-avatar class="tm-avatar" :src="hasAvatar ? src : ''" :size="size" :shape="shape" :alt="alt" :fit="fit"> <el-avatar
<img class="tm-load-img" v-if="src" :src="src" @load="hasAvatar = true" /> class="tm-avatar"
:src="hasAvatar ? src : ''"
:size="size"
:shape="shape"
:alt="alt"
:fit="fit"
>
<img
class="tm-load-img"
v-if="src"
:src="src"
@load="hasAvatar = true"
/>
<slot> <slot>
<img src="../imgs/default-avatar.png" :style="`width:${size}px`" /> <img src="../imgs/default-avatar.png" :style="`width:${size}px`" />
</slot> </slot>
...@@ -8,26 +20,26 @@ ...@@ -8,26 +20,26 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
@Component @Component
export default class Avatar extends Vue { export default class Avatar extends Vue {
@Prop({ @Prop()
type: String,
})
private readonly src?: string; private readonly src?: string;
@Prop({ @Prop({
type: [String, Number], type: [String, Number],
default: 30, default: 30,
validator: value => ["large", "medium", "small"].indexOf(value) >= 0 || typeof value === "number", validator: (value) =>
["large", "medium", "small"].indexOf(value) >= 0 ||
typeof value === "number",
}) })
private readonly size!: number | "large" | "medium" | "small"; private readonly size!: number | "large" | "medium" | "small";
@Prop({ @Prop({
type: String, type: String,
default: "square", default: "square",
validator: value => ["circle", "square"].indexOf(value) >= 0, validator: (value) => ["circle", "square"].indexOf(value) >= 0,
}) })
private readonly shape!: "circle" | "square"; private readonly shape!: "circle" | "square";
...@@ -37,31 +49,33 @@ ...@@ -37,31 +49,33 @@
@Prop({ @Prop({
type: String, type: String,
default: "cover", default: "cover",
validator: value => ["fill", "contain", "cover", "none", "scale-down"].indexOf(value) >= 0, validator: (value) =>
["fill", "contain", "cover", "none", "scale-down"].indexOf(value) >=
0,
}) })
private readonly fit!: "fill" | "contain" | "cover" | "none" | "scale-down"; private readonly fit!: "fill" | "contain" | "cover" | "none" | "scale-down";
private hasAvatar = false; private hasAvatar = false;
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.tm-avatar { .tm-avatar {
background: none; background: none;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
} }
.tm-avatar .tm-svg-icon { .tm-avatar .tm-svg-icon {
background: #d8d8d8; background: #d8d8d8;
} }
.tm-avatar .tm-load-img { .tm-avatar .tm-load-img {
display: none; display: none;
} }
.tm-avatar /deep/img { .tm-avatar /deep/img {
width: 100%; width: 100%;
} }
.default-avatar { .default-avatar {
display: inline-block; display: inline-block;
color: #333; color: #333;
} }
</style> </style>
<template>
<div class="chat-container" :class="{ 'is-in-page': isInPage }">
<div class="search-wrap" v-if="!modelName">
<el-input
class="keyword-input"
placeholder="会话标题"
prefix-icon="el-icon-search"
v-model="searchKeyword"
v-on:keyup.enter.native="search"
clearable
@clear="search"
></el-input>
<i
v-if="!isInPage"
class="close-btn el-icon-close"
@click="$emit('close')"
></i>
</div>
<chat-list
v-if="!modelName"
ref="chatListComp"
@list-count-update="$emit('list-count-update', $event)"
/>
<chat-list-model
v-if="modelName"
@list-count-update="$emit('list-count-update', $event)"
ref="chatListModel"
:modelName="modelName"
:listName="listName"
/>
<div class="chat-content-wrap" v-if="chatVisible && onShow">
<chat
:modelName="modelName"
@updateActive="$emit('updateActive', $event)"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Ref, Vue, Watch } from "vue-property-decorator";
import ChatList from "@/customer-service/components/chat-list.vue";
import ChatListModel from "@/customer-service/components/chat-list-model.vue";
import chat from "@/customer-service/chat.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model";
@Component({
name: "ChatContainer",
components: {
chat,
ChatList,
ChatListModel,
},
})
export default class ChatContainer extends Vue {
@Prop(Boolean) isInPage: boolean;
@Prop(String) modelName: string;
@Prop(String) listName: string;
@Prop(String) activeName: string;
@Prop(Boolean) isActive: boolean;
private onShow = false;
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE)
private readonly chatVisible!: ChatStore.STATE_CHAT_DIALOG_VISIBLE;
@chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT)
private readonly hideChat: ChatStore.MUTATION_HIDE_CHAT;
@Ref("chatListComp") chatListComp: ChatList;
private searchKeyword = "";
@Ref("chatListModel") chatListModel: ChatListModel;
private search() {
this.chatListComp.search(this.searchKeyword);
}
@Watch("isActive", { immediate: true }) isActiveUpdate() {
this.onShow = this.isActive;
if (
(!this.onShow && this.activeName !== "my_receiving") ||
(this.onShow && this.listName)
) {
this.chatListModel && this.chatListModel.clearActiveId();
}
}
private onChatDrawerClose() {
this.hideChat();
}
}
</script>
<style lang="less">
.chat-container {
height: 70vh;
&.is-in-page {
height: 100%;
}
}
.keyword-input {
width: 300px;
margin: 15px 0 14px 20px;
/deep/ .el-input__inner {
font-size: 13px;
height: 30px;
line-height: 28px;
border-radius: 15px;
padding-right: 15px;
}
/deep/ .el-icon-time {
background: transparent;
}
/deep/ .el-input__icon {
line-height: 32px;
}
}
.search-wrap {
height: 59px;
border-bottom: 1px solid #ddd;
}
.close-btn {
float: right;
font-size: 20px;
color: #aaa;
padding: 5px;
cursor: pointer;
margin: 15px 10px 0;
}
.chat-content-wrap {
display: inline-block;
width: 75%;
height: calc(100% - 60px);
box-sizing: border-box;
vertical-align: top;
}
</style>
...@@ -66,10 +66,9 @@ ...@@ -66,10 +66,9 @@
无接待 无接待
</div> </div>
</el-scrollbar> </el-scrollbar>
<div class="d-flex align-items-center justify-content-center">
<el-pagination <el-pagination
class="page-comp" class="page-comp"
background
small
@size-change="handleSizeChange" @size-change="handleSizeChange"
:page-size="pageSize" :page-size="pageSize"
:total="total" :total="total"
...@@ -79,8 +78,12 @@ ...@@ -79,8 +78,12 @@
:pager-count="5" :pager-count="5"
layout="total, prev, pager, next" layout="total, prev, pager, next"
></el-pagination> ></el-pagination>
</div>
<div class="action-row"> <div class="action-row">
<el-button @click="getList">刷新</el-button> <el-button v-if="showItemCheckbox" @click="toggle"
>全选</el-button
>
<el-button <el-button
v-if="!showItemCheckbox" v-if="!showItemCheckbox"
@click="showItemCheckbox = true" @click="showItemCheckbox = true"
...@@ -89,13 +92,21 @@ ...@@ -89,13 +92,21 @@
<el-button <el-button
v-if="showItemCheckbox" v-if="showItemCheckbox"
@click="batchStartReception" @click="batchStartReception"
type="primary"
>确定接待</el-button >确定接待</el-button
> >
<el-button <el-button v-if="showItemCheckbox" @click="unselectAll"
v-if="showItemCheckbox"
@click="showItemCheckbox = false"
>取消</el-button >取消</el-button
> >
<i
title="刷新"
class="refresh-icon"
@click="getList"
:class="
refreshing ? 'el-icon-loading' : 'el-icon-refresh'
"
></i>
</div> </div>
</div> </div>
</div> </div>
...@@ -104,11 +115,18 @@ ...@@ -104,11 +115,18 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Ref, Vue } from "vue-property-decorator"; import { Component, Prop, Ref, Vue } from "vue-property-decorator";
import { parserMessage } from "./controller";
import { EVENTS } from "@/EventConsts";
import { chatStore, ChatStore } from "@/customer-service/store/model"; import { chatStore, ChatStore } from "@/customer-service/store/model";
import { formatTime, TimeFormatRule } from "@/customer-service/utils/time"; import {
formatTime,
parseString2TimeValue,
TimeFormatRule,
} from "@/customer-service/utils/time";
import { Chat as ChatType } from "@/customer-service/xim/models/chat"; import { Chat as ChatType } from "@/customer-service/xim/models/chat";
import { EVENTS } from "@/EventConsts"; import xim from "@/customer-service/xim";
import { parserMessage } from "./controller";
interface SelectChatType extends ChatType { interface SelectChatType extends ChatType {
checked?: boolean; checked?: boolean;
...@@ -154,9 +172,14 @@ export default class ModelChatList extends Vue { ...@@ -154,9 +172,14 @@ export default class ModelChatList extends Vue {
private currentPage = 1; private currentPage = 1;
private sseTs = 0; private sseTs = 0;
private showItemCheckbox = false; private showItemCheckbox = false;
private refreshing = false;
private get chatRooms() { private get chatRooms() {
return this.chatList || []; return this.chatList.sort(
(x, y) =>
parseString2TimeValue((y.last_msg_ts || "") + "", 100) -
parseString2TimeValue((x.last_msg_ts || "") + "", 99)
);
} }
public clearActiveId() { public clearActiveId() {
...@@ -164,13 +187,15 @@ export default class ModelChatList extends Vue { ...@@ -164,13 +187,15 @@ export default class ModelChatList extends Vue {
} }
private async getList() { private async getList() {
this.refreshing = true;
let result = await this.sdk let result = await this.sdk
.model(this.modelName) .model(this.modelName)
.list(this.listName || undefined) .list(this.listName || undefined)
.query({ .query({
pageIndex: this.currentPage, pageIndex: this.currentPage,
item_size: this.pageSize, item_size: this.pageSize,
}); })
.finally(() => (this.refreshing = false));
if (result.pageData.rows.length === 0 && this.currentPage !== 1) { if (result.pageData.rows.length === 0 && this.currentPage !== 1) {
this.currentPage = 1; this.currentPage = 1;
if (result.pageData.record_count > 0) { if (result.pageData.record_count > 0) {
...@@ -187,17 +212,15 @@ export default class ModelChatList extends Vue { ...@@ -187,17 +212,15 @@ export default class ModelChatList extends Vue {
return { return {
id: it.id.value, id: it.id.value,
chat_id: it.ImChatId.value, chat_id: it.ImChatId.value,
business_data: {
model_name: it.ModelName.value, model_name: it.ModelName.value,
obj_id: it.ObjId.value, obj_id: it.ObjId.value,
detail_name: "",
},
last_msg_sender: it.LastSpeakUid.value, last_msg_sender: it.LastSpeakUid.value,
last_msg_content: it.LastMsgContent.value, last_msg_content: it.LastMsgContent.value,
last_msg_ts: it.LastMsgTime.value, last_msg_ts: it.LastMsgTime.value,
last_msg_type: it.LastMsgType.value, last_msg_type: it.LastMsgType.value,
title: it.Title.value, title: it.Title.value || `会话${it.id.value}`,
} as ChatType; checked: false,
} as SelectChatType;
}); });
this.total = result.pageData.record_count; this.total = result.pageData.record_count;
this.$emit("list-count-update", this.total); this.$emit("list-count-update", this.total);
...@@ -205,8 +228,8 @@ export default class ModelChatList extends Vue { ...@@ -205,8 +228,8 @@ export default class ModelChatList extends Vue {
async created() { async created() {
await this.getList(); await this.getList();
this.setSource(ChatStore.StateChatSourceDirection.Server); this.setSource(xim.getServiceType());
this.scrollbar.update(); this.scrollbar && this.scrollbar.update();
await this.sdk await this.sdk
.model("UniplatChat") .model("UniplatChat")
.registerOnChange(this.onTransportMessage); .registerOnChange(this.onTransportMessage);
...@@ -217,12 +240,12 @@ export default class ModelChatList extends Vue { ...@@ -217,12 +240,12 @@ export default class ModelChatList extends Vue {
this.listName === "group_receiving" || this.listName === "group_receiving" ||
this.listName === "group_wait" this.listName === "group_wait"
) { ) {
this.$eventHub.$on(EVENTS.ChatUpdate, this.refreshListDebounce); xim.$on(EVENTS.ChatUpdate, this.refreshListDebounce);
} }
} }
onTransportMessage(e: any) { onTransportMessage(e: any) {
let index = e.dataUpdates.findIndex( const index = e.dataUpdates.findIndex(
(it) => (it) =>
it.action === "startChat" || it.action === "startChat" ||
it.action === "createChat" || it.action === "createChat" ||
...@@ -232,7 +255,7 @@ export default class ModelChatList extends Vue { ...@@ -232,7 +255,7 @@ export default class ModelChatList extends Vue {
(it.action === "sendMsg" && (it.action === "sendMsg" &&
this.listName === "group_before_handle" && this.listName === "group_before_handle" &&
this.chatList.findIndex( this.chatList.findIndex(
(chat) => chat.id == it.selectedList[0] (chat) => chat.id === it.selectedList[0]
) > -1) ) > -1)
); );
if (index > -1) { if (index > -1) {
...@@ -240,7 +263,7 @@ export default class ModelChatList extends Vue { ...@@ -240,7 +263,7 @@ export default class ModelChatList extends Vue {
this.listName === "group_before_handle" && this.listName === "group_before_handle" &&
e.dataUpdates.findIndex((it) => it.action === "sendMsg") > -1 e.dataUpdates.findIndex((it) => it.action === "sendMsg") > -1
) { ) {
this.$eventHub.$emit(EVENTS.ChatUpdate); xim.$emit(EVENTS.ChatUpdate);
} }
this.refreshListDebounce(); this.refreshListDebounce();
} }
...@@ -267,9 +290,9 @@ export default class ModelChatList extends Vue { ...@@ -267,9 +290,9 @@ export default class ModelChatList extends Vue {
} }
private async goToChatRoom(data: ChatType) { private async goToChatRoom(data: ChatType) {
const chatInfo = await this._createChat({ await this._createChat({
modelName: data.business_data.model_name, modelName: data.model_name,
selectedListId: data.business_data.obj_id, selectedListId: data.obj_id,
uids: [], uids: [],
showByPage: true, showByPage: true,
}); });
...@@ -281,7 +304,7 @@ export default class ModelChatList extends Vue { ...@@ -281,7 +304,7 @@ export default class ModelChatList extends Vue {
} }
private parseMesage(data: ChatType) { private parseMesage(data: ChatType) {
if (data.last_msg_sender && data.last_msg_sender != "0") { if (data.last_msg_sender && data.last_msg_sender !== "0") {
if (this.userNames[data.last_msg_sender] === undefined) { if (this.userNames[data.last_msg_sender] === undefined) {
this.updateUserName({ id: data.last_msg_sender, name: "" }); this.updateUserName({ id: data.last_msg_sender, name: "" });
this.sdk this.sdk
...@@ -311,22 +334,42 @@ export default class ModelChatList extends Vue { ...@@ -311,22 +334,42 @@ export default class ModelChatList extends Vue {
} }
private batchStartReception() { private batchStartReception() {
let chats = this.chatRooms.filter((chat) => chat.checked); const chats = this.chatRooms.filter((chat) => chat.checked);
if (chats.length === 0) { if (chats.length === 0) {
this.$message.warning("请先勾选要接待的会话"); return this.$message.warning("请先勾选要接待的会话");
return;
} }
const length = chats.length;
let count = 0;
chats.forEach((chat) => { chats.forEach((chat) => {
const { model_name, obj_id } = chat.business_data;
this.sdk this.sdk
.model(model_name) .model(chat.model_name)
.chat(+obj_id, this.global.org.id.toString()) .chat(+chat.obj_id, this.global.org.id.toString())
.startChat(); .startChat()
.finally(() => {
count++;
if (count >= length) {
this.getList();
this.$message.success(`批量接待完成`);
}
});
chat.checked = false; chat.checked = false;
}); });
this.showItemCheckbox = false; this.showItemCheckbox = false;
this.clearActiveId(); this.clearActiveId();
} }
private toggle() {
for (const item of this.chatList) {
item.checked = true;
}
}
private unselectAll() {
for (const item of this.chatList) {
item.checked = false;
}
this.showItemCheckbox = false;
}
} }
</script> </script>
...@@ -361,8 +404,8 @@ export default class ModelChatList extends Vue { ...@@ -361,8 +404,8 @@ export default class ModelChatList extends Vue {
} }
} }
.keyword-input { .keyword-input {
width: 200px; width: 90%;
margin: 15px 0; margin: 15px;
/deep/ .el-input__inner { /deep/ .el-input__inner {
font-size: 13px; font-size: 13px;
height: 30px; height: 30px;
...@@ -450,9 +493,20 @@ export default class ModelChatList extends Vue { ...@@ -450,9 +493,20 @@ export default class ModelChatList extends Vue {
padding: 15px 0; padding: 15px 0;
} }
.action-row { .action-row {
position: relative;
.el-button { .el-button {
padding: 8px 14px; padding: 8px 14px;
border-radius: 15px; border-radius: 15px;
} }
} }
.refresh-icon {
margin: 0 5px;
cursor: pointer;
color: #409eff;
position: absolute;
right: 10px;
top: 8px;
}
</style> </style>
<template> <template>
<div class="chat-list-con"> <div class="chat-list-con">
<div class="chat-list h-100"> <div class="chat-list h-100">
<slot />
<div class="chat-list-scroll"> <div class="chat-list-scroll">
<el-scrollbar ref="scrollbar" class="h-100 no-bottom-scrollbar"> <el-scrollbar ref="scrollbar" class="h-100 no-bottom-scrollbar">
<div <div
...@@ -53,11 +54,14 @@ ...@@ -53,11 +54,14 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Ref, Vue } from "vue-property-decorator"; import { Component, Prop, Ref, Vue } from "vue-property-decorator";
import Controller from "./controller/chat-list";
import { EVENTS } from "@/EventConsts";
import avatar from "@/customer-service/components/avatar.vue"; import avatar from "@/customer-service/components/avatar.vue";
import { ChatStore } from "@/customer-service/store/model";
import { Chat as ChatType } from "@/customer-service/xim/models/chat"; import { Chat as ChatType } from "@/customer-service/xim/models/chat";
import { EVENTS } from "@/EventConsts"; import { ServiceType } from "../model";
import Controller from "./controller/chat-list"; import xim from "@/customer-service/xim";
@Component({ components: { avatar } }) @Component({ components: { avatar } })
export default class ChatList extends Controller { export default class ChatList extends Controller {
...@@ -72,19 +76,21 @@ export default class ChatList extends Controller { ...@@ -72,19 +76,21 @@ export default class ChatList extends Controller {
private unReadMsgCount = 0; private unReadMsgCount = 0;
private get chatRooms() { private get chatRooms() {
const list = if (this.chatList) {
this.chatList?.list.filter( const list = this.chatList.list
(chat) => chat.title.indexOf(this.searchKeyword) > -1 .filter((chat) => chat.title.indexOf(this.searchKeyword) > -1)
) || []; .sort((x, y) => y.last_msg_ts - x.last_msg_ts);
let unReadMsgCount = 0; let unReadMsgCount = 0;
list.filter((chat) => chat.unread_msg_count > 0).forEach((chat) => { list.filter((chat) => chat.unread_msg_count > 0).forEach((chat) => {
unReadMsgCount += chat.unread_msg_count; unReadMsgCount += chat.unread_msg_count;
}); });
this.unReadMsgCount = unReadMsgCount; this.unReadMsgCount = unReadMsgCount;
this.$emit("list-count-update", this.unReadMsgCount); this.$emit("list-count-update", this.unReadMsgCount);
this.$eventHub.$emit(EVENTS.NewMsg, this.unReadMsgCount); xim.$emit(EVENTS.NewMsg, this.unReadMsgCount);
return list; return list;
} }
return [];
}
private isSelected(item: ChatType) { private isSelected(item: ChatType) {
if (this.chatId) { if (this.chatId) {
...@@ -95,7 +101,7 @@ export default class ChatList extends Controller { ...@@ -95,7 +101,7 @@ export default class ChatList extends Controller {
async created() { async created() {
await this.getMyChatList(); await this.getMyChatList();
this.setSource(ChatStore.StateChatSourceDirection.Server); this.setSource(ServiceType.Backend);
this.scrollbar.update(); this.scrollbar.update();
} }
...@@ -144,7 +150,7 @@ export default class ChatList extends Controller { ...@@ -144,7 +150,7 @@ export default class ChatList extends Controller {
display: inline-block; display: inline-block;
width: 25%; width: 25%;
box-sizing: border-box; box-sizing: border-box;
height: calc(100% - 59px); height: 100%;
border-right: 1px solid #ddd; border-right: 1px solid #ddd;
.title { .title {
padding-left: 20px; padding-left: 20px;
...@@ -164,8 +170,8 @@ export default class ChatList extends Controller { ...@@ -164,8 +170,8 @@ export default class ChatList extends Controller {
} }
} }
.keyword-input { .keyword-input {
width: 200px; width: 90%;
margin: 15px 0; margin: 15px;
/deep/ .el-input__inner { /deep/ .el-input__inner {
font-size: 13px; font-size: 13px;
height: 30px; height: 30px;
...@@ -176,6 +182,9 @@ export default class ChatList extends Controller { ...@@ -176,6 +182,9 @@ export default class ChatList extends Controller {
/deep/ .el-icon-time { /deep/ .el-icon-time {
background: transparent; background: transparent;
} }
/deep/ .el-input__icon {
line-height: 32px;
}
} }
.chat-list { .chat-list {
.chat-item { .chat-item {
......
...@@ -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)
...@@ -37,33 +37,38 @@ export default class ChatMembers extends Vue { ...@@ -37,33 +37,38 @@ export default class ChatMembers extends Vue {
@chatStore.Action(ChatStore.ACTION_CHAT_REMOVE_CS) @chatStore.Action(ChatStore.ACTION_CHAT_REMOVE_CS)
private readonly _getoutCs!: ChatStore.ACTION_CHAT_REMOVE_CS; private readonly _getoutCs!: ChatStore.ACTION_CHAT_REMOVE_CS;
private async getout(item: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS[number]) { private async getout(
item: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS[number]
) {
await this.$confirm(`确定要移除${item.name}?`, "提示", { await this.$confirm(`确定要移除${item.name}?`, "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
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]) // 普通成员
} else if (item.type == 92) { // 可否 this._getout([item.eid]);
this._getoutCs([item.eid]) } else if (item.type === ChatRole.CustomerService) {
// 可否
this._getoutCs([item.eid]);
} }
} }
private memberTypeStr(type: string | number) { private memberTypeStr(type: string | number) {
if (type.toString() === "25") { if (+type === ChatRole.CustomerService) {
return "" return "客服";
} else if (type.toString() === "92") { }
return "客服" if (+type === ChatRole.Admin) {
} else if (type.toString() === "85") { 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 {
...@@ -71,7 +76,7 @@ export default class ChatMembers extends Vue { ...@@ -71,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 {
...@@ -81,9 +86,7 @@ export default class ChatMembers extends Vue { ...@@ -81,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 {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<div class="chat-area h-100 d-flex flex-column" ref="chatBox"> <div class="chat-area h-100 d-flex flex-column" ref="chatBox">
<div <div
ref="top" ref="top"
class="chat-messages pos-rel flex-fill" class="chat-messages pos-rel flex-fill d-flex"
:class="{ 'is-not-chat-member': !isChatMember }" :class="{ 'is-not-chat-member': !isChatMember }"
> >
<div <div
...@@ -13,7 +13,8 @@ ...@@ -13,7 +13,8 @@
> >
{{ getCurrentInputingPeople }}正在输入 {{ getCurrentInputingPeople }}正在输入
</div> </div>
<messages /> <messages class="flex-fill" />
<slot name="chat-right-panel"></slot>
</div> </div>
<div <div
class="resize" class="resize"
...@@ -24,7 +25,7 @@ ...@@ -24,7 +25,7 @@
></div> ></div>
<div <div
ref="bottom" ref="bottom"
class="chat-input flex-none overflow-hidden" class="chat-input flex-none h-100"
v-if="isChatMember" v-if="isChatMember"
> >
<message-input @error="onError" /> <message-input @error="onError" />
...@@ -37,10 +38,10 @@ ...@@ -37,10 +38,10 @@
import { import {
Component, Component,
Prop, Prop,
Provide,
Ref, Ref,
Watch,
Vue, Vue,
Provide, Watch,
} from "vue-property-decorator"; } from "vue-property-decorator";
import MessageInput from "@/customer-service/components/message-input.vue"; import MessageInput from "@/customer-service/components/message-input.vue";
...@@ -56,10 +57,10 @@ type RoomInfoTab = "customer" | "order"; ...@@ -56,10 +57,10 @@ type RoomInfoTab = "customer" | "order";
}, },
}) })
export default class ChatRoom extends Vue { export default class ChatRoom extends Vue {
@Ref("chatBox") chatBox: Element; @Ref("chatBox") chatBox!: Element;
@Ref("top") refTop: Element; @Ref("top") refTop!: Element;
@Ref("bottom") refBottom: Element; @Ref("bottom") refBottom!: Element;
@Ref("resize") refResize: Element; @Ref("resize") refResize!: Element;
@chatStore.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;
...@@ -156,8 +157,9 @@ export default class ChatRoom extends Vue { ...@@ -156,8 +157,9 @@ export default class ChatRoom extends Vue {
} }
mounted() { mounted() {
(this.refBottom as HTMLElement).style.height = this.refBottom &&
this.chatBox.clientHeight - this.refTop.clientHeight + "px"; ((this.refBottom as HTMLElement).style.height =
this.chatBox.clientHeight - this.refTop.clientHeight + "px");
} }
} }
</script> </script>
...@@ -218,7 +220,7 @@ export default class ChatRoom extends Vue { ...@@ -218,7 +220,7 @@ export default class ChatRoom extends Vue {
height: calc(100% - 130px + 1px); height: calc(100% - 130px + 1px);
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
&.is-not-chat-member { &.is-not-chat-member {
height: 100%; height: calc(100% - 50px);
border-bottom: none; border-bottom: none;
} }
} }
......
<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"
...@@ -18,26 +18,29 @@ ...@@ -18,26 +18,29 @@
<el-button <el-button
class="button" class="button"
@click="startReception" @click="startReception"
round size="small"
v-if="!isChatMember" v-if="!isChatMember"
type="primary"
>我要接待</el-button >我要接待</el-button
> >
<el-button <el-button class="button" @click="showAddMember" size="small"
class="button" >添加客服</el-button
@click="exitChat"
round
v-if="isChatMember"
>退出会话</el-button
> >
<el-button <el-button
class="button" class="button"
@click="finishReception" @click="finishReception"
round size="small"
v-if="isChatMember && operatorType > 25" v-if="isChatMember && operatorType > 25"
type="warning"
>结束接待</el-button >结束接待</el-button
> >
<el-button class="button" @click="showAddMember" round <el-button
>添加客服</el-button class="button"
@click="exitChat"
size="small"
v-if="isChatMember"
type="danger"
>退出会话</el-button
> >
<i <i
v-if="close && isSingleChat" v-if="close && isSingleChat"
...@@ -55,11 +58,20 @@ ...@@ -55,11 +58,20 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; 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";
import {
ChatChangedEvent,
ChatEventHandler,
} from "./controller/chat-event-handler";
@Component({ components: { ChatCreator } }) @Component({ components: { ChatCreator } })
export default class ChatTitle extends Vue { export default class ChatTitle extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS) @chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS; private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
...@@ -128,22 +140,35 @@ export default class ChatTitle extends Vue { ...@@ -128,22 +140,35 @@ export default class ChatTitle extends Vue {
} }
} }
private noop() {
return 1;
}
private async exitChat() { private async exitChat() {
this.$confirm("确认要退出此会话?")
.then(async () => {
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();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
})
.catch(this.noop);
} }
private async startReception() { private async startReception() {
try { try {
await this._startReception(); await this._startReception().then(() =>
ChatEventHandler.raiseChatChanged(
ChatChangedEvent.Start,
this.chatId
)
);
this.$emit("updateActive", "my_receiving"); this.$emit("updateActive", "my_receiving");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
...@@ -160,7 +185,9 @@ export default class ChatTitle extends Vue { ...@@ -160,7 +185,9 @@ export default class ChatTitle extends Vue {
type: "warning", type: "warning",
} }
); );
await this._finishReception(); await this._finishReception().then(() =>
ChatEventHandler.raiseChatChanged(ChatChangedEvent.End, this.chatId)
);
this.hideChat(); this.hideChat();
} }
} }
...@@ -170,6 +197,7 @@ export default class ChatTitle extends Vue { ...@@ -170,6 +197,7 @@ export default class ChatTitle extends Vue {
font-size: 16px; font-size: 16px;
padding: 0 20px; padding: 0 20px;
height: 60px; height: 60px;
min-height: 60px;
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
.title { .title {
cursor: pointer; cursor: pointer;
......
export const enum ChatChangedEvent {
Start = 1,
End,
}
export class ChatEventHandler {
private static actions: {
key: string;
action: (e: ChatChangedEvent, chat: number) => void;
}[] = [];
public static registerOnChatChanged(
vue: Vue,
action: (e: ChatChangedEvent, chat: number) => void
) {
const key = `${new Date().valueOf()}-${Math.random()}`;
this.actions.push({ key, action });
vue.$once("hook:beforeDestroy", () => {
const t = this.actions.find((i) => i.key === key);
if (t) {
this.actions = this.actions.filter((i) => i !== t);
}
});
}
public static raiseChatChanged(e: ChatChangedEvent, chat: number) {
for (const item of this.actions) {
item.action(e, chat);
}
}
}
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { chatStore, ChatStore } from "@/customer-service/store/model";
import { parserMessage } from "."; import { parserMessage } from ".";
import { Chat as ChatItem } from "@/customer-service/xim/models/chat";
import { chatStore, ChatStore } from "@/customer-service/store/model";
import { formatTime, TimeFormatRule } from "@/customer-service/utils/time"; import { formatTime, TimeFormatRule } from "@/customer-service/utils/time";
import { Chat as ChatItem } from "@/customer-service/xim/models/chat";
@Component({ components: {} }) @Component({ components: {} })
export default class ChatList extends Vue { export default class ChatList extends Vue {
...@@ -34,25 +36,35 @@ export default class ChatList extends Vue { ...@@ -34,25 +36,35 @@ export default class ChatList extends Vue {
protected readonly saveChatTitle!: ChatStore.MUTATION_SAVE_CHAT_TITLE; protected readonly saveChatTitle!: ChatStore.MUTATION_SAVE_CHAT_TITLE;
@chatStore.Mutation(ChatStore.MUTATION_SHOW_CHAT) @chatStore.Mutation(ChatStore.MUTATION_SHOW_CHAT)
protected readonly showChat: ChatStore.MUTATION_SHOW_CHAT; protected readonly showChat!: ChatStore.MUTATION_SHOW_CHAT;
@chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT) @chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT)
protected readonly hideChat: ChatStore.MUTATION_HIDE_CHAT; protected readonly hideChat!: ChatStore.MUTATION_HIDE_CHAT;
@chatStore.Action(ChatStore.ACTION_UPDATE_CHAT)
protected readonly updateChat!: ChatStore.ACTION_UPDATE_CHAT;
@chatStore.Action(ChatStore.ACTION_REBUILD_UNREAD_MESSAGE_COUNT)
protected readonly updateUnreadMessageCount!: ChatStore.ACTION_REBUILD_UNREAD_MESSAGE_COUNT;
@chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA)
protected readonly reset!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA;
protected parseMesage(data: ChatItem) { protected parseMesage(data: ChatItem) {
if (data.last_msg_sender && data.last_msg_sender != "0") { if (data.last_msg_sender && data.last_msg_sender !== "0") {
if (this.userNames[data.last_msg_sender] === undefined) { if (!this.userNames[data.last_msg_sender]) {
this.updateUserName({ id: data.last_msg_sender, name: "" }); this.updateUserName({ id: data.last_msg_sender, name: "" });
this.sdk this.sdk
.model("user") .model("user")
.detail(data.last_msg_sender) .detail(data.last_msg_sender)
.query() .query()
.then((userInfo) => { .then((userInfo: any) => {
this.updateUserName({ this.updateUserName({
id: data.last_msg_sender, id: data.last_msg_sender,
name: userInfo.row.first_name.display as string, name: userInfo.row.first_name.display as string,
}); });
}); })
.catch(() => {});
} }
} }
if (data.last_msg_content === "") { if (data.last_msg_content === "") {
......
import { MessageType } from "@/customer-service/model";
export function parserMessage(type: string, rawMsg: string) { export function parserMessage(type: string, rawMsg: string) {
if (!type) return ""; if (!type) return "";
if (!rawMsg) return ""; if (!rawMsg) return "";
if (type === MessageType.Text) {
const msg = JSON.parse(rawMsg); const msg = JSON.parse(rawMsg);
if (type === "text") {
return msg.text; return msg.text;
} else if (type === "image") { } else if (type === MessageType.Image) {
return `[图片]`; return `[图片]`;
} else if (type === "file") { } else if (type === MessageType.File) {
return `[文件]`; return `[文件]`;
} else if (type === MessageType.Withdraw) {
return `[撤回了一条消息]`;
} else { } else {
return `[系统自动回复]`; return `[系统自动回复]`;
} }
......
...@@ -57,12 +57,13 @@ ...@@ -57,12 +57,13 @@
</el-dialog> </el-dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ListEasy, ListTypes } from "uniplat-sdk"; import { ListEasy, ListTypes, TagManagerTypes } from "uniplat-sdk";
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import GeneralTagSelectForFilter from "@/components/statistic/GeneralTagSelectForFilter.vue"
import buttonThrottle from "../utils/button-throttle"; import buttonThrottle from "../utils/button-throttle";
import GeneralTagSelectForFilter from "@/components/statistic/GeneralTagSelectForFilter.vue";
import avatar from "@/customer-service/components/avatar.vue"; import avatar from "@/customer-service/components/avatar.vue";
import { TagManagerTypes } from "uniplat-sdk"
import chat from "@/customer-service/xim/index"; import chat from "@/customer-service/xim/index";
type User = { type User = {
id: string; id: string;
...@@ -103,9 +104,9 @@ export default class ChatCreator extends Vue { ...@@ -103,9 +104,9 @@ export default class ChatCreator extends Vue {
if (this.$refs.generalTagSelect) { if (this.$refs.generalTagSelect) {
return ( return (
this.$refs.generalTagSelect as GeneralTagSelectForFilter this.$refs.generalTagSelect as GeneralTagSelectForFilter
).getSelectedTags() ).getSelectedTags();
} }
return [] return [];
} }
private async getUserList(searchText: string | null = null) { private async getUserList(searchText: string | null = null) {
...@@ -127,7 +128,7 @@ export default class ChatCreator extends Vue { ...@@ -127,7 +128,7 @@ export default class ChatCreator extends Vue {
this.getList = getList; this.getList = getList;
this.userList = this.exactUserList(pageData.rows); this.userList = this.exactUserList(pageData.rows);
this.loading = false; this.loading = false;
this.tagGroups = pageData.tagGroups || [] this.tagGroups = pageData.tagGroups || [];
} }
private exactUserList(rows: any[]) { private exactUserList(rows: any[]) {
......
...@@ -13,7 +13,7 @@ import ChatInput, { ...@@ -13,7 +13,7 @@ import ChatInput, {
FILE_INFO_CLASS, FILE_INFO_CLASS,
isImageOrFile, isImageOrFile,
} from "../hybrid-input/index.vue"; } from "../hybrid-input/index.vue";
import { Message } from "../model"; import { Message, MessageType } from "../model";
import { uploadFile } from "../service/upload"; import { uploadFile } from "../service/upload";
import { ChatLoggerService } from "../xim/logger"; import { ChatLoggerService } from "../xim/logger";
import xim from "../xim/xim"; import xim from "../xim/xim";
...@@ -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, MessageType.File);
} else { } else {
this.sendFile(item, "image"); await this.sendFile(item, MessageType.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,17 +88,17 @@ export default class MessageInput extends Vue { ...@@ -88,17 +88,17 @@ 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: MessageType.Text, msg: JSON.stringify(msg) });
} }
} }
private async sendFile(file: any, type: "image" | "file") { private async sendFile(file: any, type: MessageType.Image | MessageType.File) {
const src = JSON.parse( const src = JSON.parse(
file.attributes[`data-${type}`]?.value || "" file.attributes[`data-${type}`]?.value || ""
) as { ) as {
...@@ -112,7 +112,7 @@ export default class MessageInput extends Vue { ...@@ -112,7 +112,7 @@ export default class MessageInput extends Vue {
if (file) { if (file) {
let w = 0; let w = 0;
let h = 0; let h = 0;
if (type === "image") { if (type === MessageType.Image) {
const img = new Image(); const img = new Image();
img.src = src.url; img.src = src.url;
img.onload = function () { img.onload = function () {
......
<template>
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-if="messageType === 'voice'"
@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>
</template>
<script lang="ts">
import { Component, Ref } from "vue-property-decorator";
import BaseMessage from "./index";
import VoiceIcon from "./voice.vue";
@Component({ components: { VoiceIcon } })
export default class Index extends BaseMessage {
@Ref("audio")
private readonly audioRef!: HTMLAudioElement;
private playing = false;
private get duration() {
const v = this.messageBody.msg.duration as number;
return v || 0;
}
private get durationInSecond() {
return Math.round(this.duration / 1000);
}
private get getVoiceMessageWidth() {
if (this.fileFailed2Load) {
return 35;
}
const d = this.duration / 1000;
if (d <= 3) {
return 60;
}
if (d >= 60) {
return 200;
}
return 60 + d;
}
private play() {
if (this.audioRef?.paused) {
this.audioRef?.load();
this.audioRef?.play();
} else {
this.audioRef?.pause();
}
}
private onPlay() {
this.playing = true;
}
private onPause() {
this.playing = false;
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped>
.voice-message {
height: 40px;
width: 200px;
&.can-play {
cursor: pointer;
}
i {
font-size: 16px;
}
}
.my-message {
.voice-message {
> div {
flex-flow: row-reverse;
}
svg {
transform: rotateY(180deg);
}
}
}
</style>
...@@ -70,7 +70,7 @@ export default class FileIcon extends Vue { ...@@ -70,7 +70,7 @@ export default class FileIcon extends Vue {
.file-icon { .file-icon {
margin-left: 10px; margin-left: 10px;
svg { /deep/ svg {
max-width: 36px; max-width: 36px;
max-height: 36px; max-height: 36px;
} }
......
<template>
<div class="msg-detail file-message d-flex" @dblclick="openFile">
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
:title="messageBody.msg.name"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
{{ format(messageBody.msg.size) }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
<a
class="
d-flex
align-items-center
justify-content-center
download-icon
"
:href="messageRealUrl"
:download="getAttachment"
title="下载文件"
>
<img src="~@/customer-service/imgs/download.png" alt="Download" />
</a>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import FileIcon from "./file-icon.vue";
import { FileType, getFileType } from "./file-controller";
const k = 1024,
m = 1024 * k,
g = 1024 * m,
t = 1024 * g;
function formatSize(size: number) {
if (size === undefined || size === null) {
return "";
}
if (size < k) {
return size + " B";
}
if (size < m) {
return Number((size / k).toFixed(2)) + " KB";
}
if (size < g) {
return Number((size / m).toFixed(2)) + " MB";
}
if (size < t) {
return Number((size / g).toFixed(2)) + " GB";
}
return Number((size / t).toFixed(2)) + " TB";
}
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private get getAttachment() {
if (this.messageBody) {
return this.messageBody.msg.name;
}
return "文件下载";
}
private get fileIcon() {
if (this.value) {
return getFileType(this.messageBody.msg.name);
}
return FileType.Others;
}
private format(v: number) {
return formatSize(v);
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped>
.file-message {
background-color: transparent !important;
border-radius: 4px !important;
border: 1px solid #c5d4e5;
.file-message-name {
max-width: 130px;
}
}
</style>
<template>
<div
class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }"
@dblclick="openFile"
>
<img
v-if="messageRealUrl"
:src="messageRealUrl"
:title="messageBody.msg.name"
:alt="messageBody.msg.name"
@error="onImageError"
/>
<file-icon v-else-if="fileFailed2Load" :value="image404"></file-icon>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import { FileType } from "./file-controller";
import BaseMessage from "./index";
import FileIcon from "./file-icon.vue";
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private readonly image404 = FileType.Image_404;
private onImageError() {
this.fileFailed2Load = true;
this.messageRealUrl = "";
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped>
.image-message {
background-color: transparent !important;
border-radius: 4px !important;
border: 1px solid #c5d4e5;
line-height: 1;
max-width: 300px;
box-sizing: content-box;
img {
width: 100%;
}
&.image-404 {
background: #f7f8fa;
display: flex;
align-items: center;
justify-content: center;
.file-icon {
margin: 0;
}
}
/deep/ .file-icon {
margin-left: 0;
}
}
.my-message {
&.image-message:not(.image-404) {
background-color: transparent !important;
border-radius: 4px !important;
border: 1px solid #c5d4e5;
}
}
</style>
import { Component, Vue, Model, Prop } from "vue-property-decorator";
import { Message } from "@/customer-service/model";
import { isAccessibleUrl } from "@/customer-service/service/tools";
@Component({ components: {} })
export default class BaseMessage extends Vue {
@Model()
protected readonly value!: Message;
@Prop()
protected readonly userName!: string;
protected messageRealUrl = "";
protected fileFailed2Load = false;
protected loadingRealUrl = false;
protected get messageBody(): { eid?: string; oid?: string; msg: any } {
if (this.value) {
try {
return { ...this.value, msg: JSON.parse(this.value.msg) };
} catch {
return {
...this.value,
msg: JSON.parse(this.value.msg.replace(/\n/g, "\\n")),
};
}
}
return { msg: { text: "" } };
}
protected openFile() {
this.$emit("open", this.messageRealUrl);
}
protected buildMessageUrl() {
if (this.messageRealUrl || this.loadingRealUrl) {
return;
}
const url = this.messageBody.msg.url as string;
if (url) {
if (isAccessibleUrl(url)) {
return (this.messageRealUrl = url);
}
this.loadingRealUrl = true;
} else {
this.fileFailed2Load = true;
}
}
}
<template>
<div
class="msg-detail inline-text"
v-html="format2Link(messageBody.msg.text || emptyText)"
></div>
</template>
<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";
@Component({ components: {} })
export default class Index extends BaseMessage {
private readonly emptyText = " ";
private format2Link(text: string) {
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>
.inline-text {
display: inline-block;
white-space: pre-wrap;
text-align: left;
/deep/ .highlight {
color: #e87005;
}
}
</style>
\ No newline at end of file
<template>
<div
class="
msg-detail
video-message
d-flex
align-items-center
justify-content-center
"
>
<video-player-icon @click.native="openFile"></video-player-icon>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import VideoPlayerIcon from "./video-player-icon.vue";
@Component({ components: { VideoPlayerIcon } })
export default class Index extends BaseMessage {
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped>
.video-message {
height: 160px;
width: 200px;
background-color: #000 !important;
border-radius: 0 !important;
svg {
cursor: pointer;
}
}
</style>
...@@ -32,20 +32,18 @@ ...@@ -32,20 +32,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator" import { Component, Prop, Vue, Watch } from "vue-property-decorator";
;
@Component({ components: {} }) @Component({ components: {} })
export default class VoiceIcon extends Vue { export default class VoiceIcon extends Vue {
@Prop({ default: 25 }) @Prop({ default: 25 })
private size!: number private size!: number;
@Prop() @Prop()
private loading!: boolean private loading!: boolean;
private status = 0 private status = 0;
private interval = 0 private interval = 0;
@Watch("loading") @Watch("loading")
private onLoadingChanged() { private onLoadingChanged() {
......
<template>
<div class="msg-detail withdraw-message">{{ userName }}撤回了一条消息</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {}
</script>
<style lang="less" scoped>
.my-message {
.withdraw-message {
background-color: transparent !important;
padding: 0 4px;
font-size: 12px;
color: #999;
}
}
</style>
...@@ -2,12 +2,7 @@ ...@@ -2,12 +2,7 @@
<div v-loading="chatIniting" class="message-list h-100"> <div v-loading="chatIniting" class="message-list h-100">
<el-scrollbar <el-scrollbar
ref="message-scrollbar" ref="message-scrollbar"
class=" class="message-list-scrollbar no-bottom-scrollbar h-100"
message-list-scrollbar
no-bottom-scrollbar
adjust-el-scroll-right-bar
h-100
"
> >
<template v-for="item in messages"> <template v-for="item in messages">
<div :key="item.id" class="message-template"> <div :key="item.id" class="message-template">
...@@ -28,6 +23,7 @@ ...@@ -28,6 +23,7 @@
:data="item" :data="item"
:shape="shape" :shape="shape"
@open="open" @open="open"
@withdraw="refresh"
/> />
</div> </div>
</template> </template>
...@@ -44,7 +40,7 @@ ...@@ -44,7 +40,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Ref, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Ref, Vue, Watch } from "vue-property-decorator";
import { Message } from "../model"; import { Message, MessageType } from "../model";
import { throttle } from "../utils"; import { throttle } from "../utils";
import { formatTime } from "../utils/time"; import { formatTime } from "../utils/time";
...@@ -53,6 +49,7 @@ import message from "./message.vue"; ...@@ -53,6 +49,7 @@ import message from "./message.vue";
import VideoPreview from "./video-preview.vue"; import VideoPreview from "./video-preview.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model"; import { ChatStore, chatStore } from "@/customer-service/store/model";
import { dbController } from "../database";
@Component({ components: { message, ImagePreview, VideoPreview } }) @Component({ components: { message, ImagePreview, VideoPreview } })
export default class MessageList extends Vue { export default class MessageList extends Vue {
...@@ -83,6 +80,15 @@ export default class MessageList extends Vue { ...@@ -83,6 +80,15 @@ export default class MessageList extends Vue {
@chatStore.Mutation(ChatStore.MUTATION_CLEAR_FUNC_SCROLL_TO_BOTTOM) @chatStore.Mutation(ChatStore.MUTATION_CLEAR_FUNC_SCROLL_TO_BOTTOM)
private readonly clearScrollToBottomFunc!: ChatStore.MUTATION_CLEAR_FUNC_SCROLL_TO_BOTTOM; private readonly clearScrollToBottomFunc!: ChatStore.MUTATION_CLEAR_FUNC_SCROLL_TO_BOTTOM;
@chatStore.Mutation(ChatStore.MUTATION_SAVE_FUNC_ON_NEW_MSG)
private readonly onNewMessage!: ChatStore.MUTATION_SAVE_FUNC_ON_NEW_MSG;
@chatStore.Mutation(ChatStore.MUTATION_CLEAR_FUNC_ON_NEW_MSG)
private readonly clearNewMessage!: ChatStore.MUTATION_CLEAR_FUNC_ON_NEW_MSG;
@chatStore.Mutation(ChatStore.MUTATION_WITHDRAW)
private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW;
@Prop({ default: "circle" }) @Prop({ default: "circle" })
private shape!: string; private shape!: string;
...@@ -176,6 +182,14 @@ export default class MessageList extends Vue { ...@@ -176,6 +182,14 @@ export default class MessageList extends Vue {
public created() { public created() {
this.handleScrollWrapper(); this.handleScrollWrapper();
this.onNewMessage((e) => {
if (e.type === MessageType.Withdraw) {
this.executeWithDraw(e.ref_id);
dbController
.removeMessage(e.chat_id, e.ref_id)
.finally(() => this.refresh());
}
});
} }
public mounted() { public mounted() {
...@@ -183,12 +197,15 @@ export default class MessageList extends Vue { ...@@ -183,12 +197,15 @@ export default class MessageList extends Vue {
this.scollWrapper.addEventListener("scroll", this.handleScroll); this.scollWrapper.addEventListener("scroll", this.handleScroll);
this.saveScrollToBottomFunc(this.scrollToNewMsg); this.saveScrollToBottomFunc(this.scrollToNewMsg);
this.scrollToNewMsg(); this.scrollToNewMsg();
setTimeout(() => this.scroll2End(200));
setTimeout(() => this.scroll2End(1000));
} }
public beforeDestroy() { public beforeDestroy() {
this.scollWrapper && this.scollWrapper &&
this.scollWrapper.removeEventListener("scroll", this.handleScroll); this.scollWrapper.removeEventListener("scroll", this.handleScroll);
this.clearScrollToBottomFunc(); this.clearScrollToBottomFunc();
this.clearNewMessage();
// this.clearChatId(); // this.clearChatId();
} }
...@@ -199,12 +216,16 @@ export default class MessageList extends Vue { ...@@ -199,12 +216,16 @@ export default class MessageList extends Vue {
) as HTMLElement; ) as HTMLElement;
if (wrap) { if (wrap) {
if (delay) { if (delay) {
setTimeout(() => { return setTimeout(
wrap.scrollTop = 100000; () =>
}, delay); (wrap.scrollTop = Math.max(
return; wrap.scrollHeight + 100,
10000
)),
delay
);
} }
wrap.scrollTop = 100000; wrap.scrollTop = Math.max(wrap.scrollHeight + 100, 10000);
} }
}); });
} }
...@@ -353,12 +374,17 @@ export default class MessageList extends Vue { ...@@ -353,12 +374,17 @@ export default class MessageList extends Vue {
} }
return v; return v;
} }
private refresh() {
this.fetchNewMsg();
}
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.message-list { .message-list {
padding: 0 20px; padding: 0 20px;
padding-right: 0;
} }
.loading-mask { .loading-mask {
......
<template>
<div class="h-100 pos-rel">
<div class="scroll-wrap">
<el-scrollbar class="h-100">
<div
class="data-row"
v-for="item in detailData"
:key="item.label"
>
<span class="data-key"
>{{ 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"
>
<el-button
v-for="action in item.actions"
:key="action.name"
@click="execute_action(action)"
type="text"
size="small"
>{{ action.label }}</el-button
>
</span>
</div>
<div class="top-actions">
<el-button
v-for="action in actions"
:key="action.name"
@click="execute_action(action)"
type="text"
size="small"
>{{ action.label }}</el-button
>
</div>
</el-scrollbar>
</div>
<div class="detal-btns">
<el-button @click="goTodetail">查看详情</el-button>
</div>
</div>
</template>
<script lang="ts">
import { DetailTypes } from "uniplat-sdk";
import { Component, Prop, Vue } from "vue-property-decorator";
import { ChatStore, chatStore } from "../store/model";
import Chat from "../xim";
import { EVENTS } from "@/EventConsts";
import { goForward } from "@/utils/go-forward";
@Component({ components: {} })
export default class ChatModelDetail extends Vue {
@chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT)
private readonly hideChat: ChatStore.MUTATION_HIDE_CHAT;
@Prop({ type: String, required: true })
private readonly model_name!: string;
@Prop({ type: String, required: true })
private readonly id!: string;
@Prop({ type: String, default: null })
private readonly name!: string;
private sseMessageRefreshData = false;
private detailData:
| DetailTypes.getDetailRequestResult["meta"]["header"]["field_groups"]
| null = null;
private detailRow: DetailTypes.getDetailRequestResult["row"] | null = null;
private keyField = "";
private actions:
| DetailTypes.getDetailRequestResult["meta"]["actions"]
| null = null;
public async created() {
await this.init();
}
private async init() {
const data = await Chat.getSdk()
.model(this.model_name)
.detail(this.id, this.name)
.query();
this.detailData = data.meta.header.field_groups;
this.detailRow = data.row;
this.keyField = data.meta.key_field;
this.actions = data.meta.actions;
}
private goTodetail() {
this.$router.push(
`/${this.$route.params.project}/${this.$route.params.entrance}/detail/${this.model_name}/key/${this.id}`
);
this.hideChat();
}
private async execute_action(actionParams) {
let {
action_name,
container,
forward,
confirm_caption,
open_in_new_tab,
authed,
} = actionParams;
const x = this.detailRow;
const r: { v: number; id: number } = { v: 0, id: 0 };
r.id = x[this.keyField].value as number;
if (x.uniplat_version) {
r.v = x.uniplat_version.value as number;
}
if (!authed) {
this.$eventHub.$emit(EVENTS.ShowModalDialog, {
dialogName: "authors_list",
params: {
actionName: actionParams.action_name,
modelName: this.model_name,
},
});
return;
}
if (container === "page") {
this.$message.warning("该类型操作暂不支持");
} else if (container === "dialog" || container === "none") {
this.$eventHub.$emit(EVENTS.ShowModalDialog, {
dialogName: "general_executor_dialog",
params: {
autoSubmit: container === "none",
modelName: this.model_name,
actionName: action_name,
selected: JSON.stringify([r]),
prefilters: JSON.stringify([]),
options: { confirm_caption },
callWhenSuccess: (data) => {
const forward = data.forward;
this.init();
if (!!forward && forward !== "") {
goForward.call(this, forward, this.init);
}
},
},
});
} else if (container === "batch") {
this.$eventHub.$emit(EVENTS.ShowModalDialog, {
dialogName: "general_execute_batch",
params: {
model_name: this.model_name,
action_name,
selected_list: JSON.stringify([r]),
prefilters: [],
onSuccessed: this.init,
},
});
} else if (container === "detail_dialog") {
this.$eventHub.$emit(EVENTS.ShowModalDialog, {
dialogName: "general_detail_dialog",
params: {
forward: forward,
},
});
} else if (container === "iframe") {
if (
forward.indexOf("http://") !== 0 &&
forward.indexOf("https://") !== 0
) {
if (this.global.$ssr && this.global.$vapperRootPath) {
forward =
"/" +
this.global.$vapperRootPath +
forward.replace(/^\//, "");
}
}
this.$eventHub.$emit(EVENTS.ShowModalDialog, {
dialogName: "general_iframe_dialog",
params: {
title: "",
href: forward,
width: "70%",
height: 500,
},
});
} else if (container === "startProcess") {
this.$eventHub.$emit(EVENTS.ShowModalDialog, {
dialogName: "start_process_dialog",
params: {
autoSubmit: container === "none",
modelName: this.model_name,
actionName: action_name,
selected: JSON.stringify([r]),
prefilters: {},
filters: {},
options: { confirm_caption },
callWhenSuccess: () => {
this.$emit("datachange");
},
},
});
} else {
if (
forward.indexOf("http://") === 0 ||
forward.indexOf("https://") === 0
) {
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);
}
}
}
}
}
</script>
<style lang="less" scoped>
.data-row {
color: #666;
font-size: 14px;
padding: 5px 20px;
}
.data-key {
width: 5em;
}
.data-value {
display: inline-block;
vertical-align: top;
}
.detal-btns {
position: absolute;
bottom: 20px;
right: 0;
left: 0;
text-align: center;
}
.scroll-wrap {
height: calc(100% - 70px);
}
.operation_field {
margin-left: 2px;
/deep/ .el-button {
padding: 0;
}
}
.top-actions {
margin-top: 10px;
padding: 0 20px;
white-space: normal;
.el-button + .el-button {
margin-left: 0;
}
.el-button {
margin-right: 10px;
}
}
</style>
...@@ -176,6 +176,11 @@ export default class WhoReadList extends Vue { ...@@ -176,6 +176,11 @@ export default class WhoReadList extends Vue {
z-index: 2; z-index: 2;
margin-left: -100px; margin-left: -100px;
width: 125px; width: 125px;
&.offset {
margin-left: 0;
}
.number-count { .number-count {
font-size: 14px; font-size: 14px;
color: #333333; color: #333333;
......
...@@ -76,16 +76,12 @@ export default class WorkFlow extends Vue { ...@@ -76,16 +76,12 @@ export default class WorkFlow extends Vue {
private readonly startProcessIns!: startProcessDialog; private readonly startProcessIns!: startProcessDialog;
@Prop({ @Prop({
type: String,
required: true, required: true,
}) })
private readonly model_name!: string; private readonly model_name!: string;
@Prop({ @Prop({ required: true })
type: String, private readonly id!: string | number;
required: true,
})
private readonly id!: string;
@Prop({ @Prop({
type: String, type: String,
...@@ -93,8 +89,8 @@ export default class WorkFlow extends Vue { ...@@ -93,8 +89,8 @@ export default class WorkFlow extends Vue {
}) })
private readonly name!: string; private readonly name!: string;
private processName = "" private processName = "";
private selectedProcess = [] private selectedProcess = [];
private startDialogConfirm() { private startDialogConfirm() {
console.log("startDialogConfirm"); console.log("startDialogConfirm");
...@@ -108,7 +104,10 @@ export default class WorkFlow extends Vue { ...@@ -108,7 +104,10 @@ export default class WorkFlow extends Vue {
private flowList = []; private flowList = [];
public async created() { public async created() {
this.flowList = await sdk().model(this.model_name).workflow2().queryProcessByAssociateId(+this.id); this.flowList = await sdk()
.model(this.model_name)
.workflow2()
.queryProcessByAssociateId(+this.id);
} }
public start(workflow: any) { public start(workflow: any) {
...@@ -118,7 +117,7 @@ export default class WorkFlow extends Vue { ...@@ -118,7 +117,7 @@ export default class WorkFlow extends Vue {
} }
public goToDetail(workflow: any) { public goToDetail(workflow: any) {
this.$eventHub.$emit(EVENTS.ShowModalDialog, { Chat.$emit(EVENTS.ShowModalDialog, {
dialogName: "show_process_detail", dialogName: "show_process_detail",
params: { params: {
modelName: this.model_name, modelName: this.model_name,
......
<template> <template>
<div class="input-wrap h-100 overflow-hidden"> <div class="input-wrap h-100">
<div class="tool-bar"> <div class="tool-bar">
<img <img
class="tool-bar-icon" class="tool-bar-icon"
...@@ -73,7 +73,7 @@ import { ...@@ -73,7 +73,7 @@ import {
MESSAGE_FILE_EMPTY, MESSAGE_FILE_EMPTY,
MESSAGE_FILE_TOO_LARGE, MESSAGE_FILE_TOO_LARGE,
MESSAGE_IMAGE_TOO_LARGE, MESSAGE_IMAGE_TOO_LARGE,
} from "../components/file-controller"; } from "../components/message-item/file-controller";
import { EmojiService } from "../service/emoji"; import { EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model"; import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils"; import { formatFileSize } from "../utils";
...@@ -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;
...@@ -276,7 +279,7 @@ export default class Input extends Vue { ...@@ -276,7 +279,7 @@ export default class Input extends Vue {
const el = this.messageInputBox; const el = this.messageInputBox;
const range = document.createRange(); const range = document.createRange();
const sel = window.getSelection(); const sel = window.getSelection();
const offset = sel.focusOffset; const offset = sel!.focusOffset;
const content = el.innerHTML; const content = el.innerHTML;
el.innerHTML = el.innerHTML =
content.slice(0, offset) + content.slice(0, offset) +
...@@ -284,8 +287,8 @@ export default class Input extends Vue { ...@@ -284,8 +287,8 @@ export default class Input extends Vue {
(content.slice(offset) || "\n"); (content.slice(offset) || "\n");
range.setStart(el.childNodes[0], offset + 1); range.setStart(el.childNodes[0], offset + 1);
range.collapse(true); range.collapse(true);
sel.removeAllRanges(); sel!.removeAllRanges();
sel.addRange(range); sel!.addRange(range);
return; return;
} }
if (e.shiftKey) { if (e.shiftKey) {
...@@ -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(() => { })
.then(() => {
this.clearInput(); this.clearInput();
if (this.chatId) { if (this.chatId) {
chatCache[this.chatId] = []; chatCache[this.chatId] = [];
} }
}); })
.finally(() => setTimeout(() => this.getMyChatList(), 120));
} }
/** /**
...@@ -635,7 +640,7 @@ export default class Input extends Vue { ...@@ -635,7 +640,7 @@ export default class Input extends Vue {
.emoji-picker { .emoji-picker {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
bottom: 99px; top: -232px;
left: -1px; left: -1px;
background-color: #fff; background-color: #fff;
padding: 20px; padding: 20px;
......
<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" viewBox="0 0 42 42"><defs><style>.a{fill:#e84929;}.b{fill:#fff;}</style></defs><path class="a" d="M21,0A21,21,0,1,0,42,21,21,21,0,0,0,21,0Z"/><g transform="translate(8.066 8.066)"><g transform="translate(0 0)"><path class="b" d="M3462.491,2574.047a12.934,12.934,0,0,0,12.476,13.375q.231.008.461.008a12.933,12.933,0,1,0-12.937-13.383Zm25.367.882a12.449,12.449,0,1,1-12.447-12.882c.145,0,.292,0,.438.007A12.463,12.463,0,0,1,3487.858,2574.929Z" transform="translate(-3462.483 -2561.563)"/><path class="b" d="M3573.4,2676.362a.238.238,0,0,0-.238.238v3.916l1.784-.026-.021-4.128Z" transform="translate(-3559.739 -2662.417)"/><path class="b" d="M3573.158,2723.018v3.94a10.489,10.489,0,0,0,1.81-.237l-.019-3.73Z" transform="translate(-3559.739 -2703.343)"/><path class="b" d="M3504.454,2582.427h7.627a10.326,10.326,0,0,0-12.393,0h3.1a.241.241,0,0,1,.238.243v.972a.241.241,0,0,1-.238.243H3498.1a10.567,10.567,0,0,0-.8,1.017h5.427a.24.24,0,0,1,.238.243v2.872a1.444,1.444,0,0,1-1.43,1.458h-1.736a.241.241,0,0,0-.239.243v9.874a10.439,10.439,0,0,0,1.249.827v-9.648a.241.241,0,0,1,.238-.243h1.668a.241.241,0,0,1,.239.243v10.556a10.138,10.138,0,0,0,1.24.284v-10.8a.241.241,0,0,1,.238-.243h9.535a.241.241,0,0,1,.238.243v6.743a10.854,10.854,0,0,0-.547-13.67h-9.212a.241.241,0,0,1-.239-.243v-.972A.241.241,0,0,1,3504.454,2582.427Zm0,2.491h1.669a.241.241,0,0,1,.238.243v2.613a.241.241,0,0,0,.238.243h5.3a.241.241,0,0,0,.239-.243v-1.154a.241.241,0,0,0-.239-.244h-4.589a.24.24,0,0,1-.238-.243v-.972a.241.241,0,0,1,.238-.243h6.734a.241.241,0,0,1,.239.243v2.856a1.444,1.444,0,0,1-1.431,1.458h-8.4a.241.241,0,0,1-.239-.243v-4.071A.241.241,0,0,1,3504.454,2584.919Z" transform="translate(-3492.747 -2578.121)"/><path class="b" d="M3607.532,2676.362h-1.574l.021,4.1,1.791-.026V2676.6A.238.238,0,0,0,3607.532,2676.362Z" transform="translate(-3588.562 -2662.417)"/><path class="b" d="M3606.2,2725.573a10.5,10.5,0,0,0,1.77-1.044v-2.019l-1.785.026Z" transform="translate(-3588.764 -2702.919)"/><path class="b" d="M3486.713,2631.115v-1.148a.239.239,0,0,0-.239-.239h-4.129a10.5,10.5,0,0,0,.963,10.8v-8.938a.239.239,0,0,1,.239-.239h2.927A.238.238,0,0,0,3486.713,2631.115Z" transform="translate(-3479.042 -2621.378)"/></g></g></svg>
\ No newline at end of file
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;
...@@ -27,6 +33,17 @@ export interface Chat { ...@@ -27,6 +33,17 @@ export interface Chat {
unread_msg_count: number; unread_msg_count: number;
} }
export const enum CustomerServiceProduct {
Default,
Fulibao,
Hrs,
}
export const enum ServiceType {
Frontend,
Backend,
}
export type TokenStringGetter = () => Promise<string>; export type TokenStringGetter = () => Promise<string>;
export interface ChatOption { export interface ChatOption {
...@@ -38,6 +55,20 @@ export interface ChatOption { ...@@ -38,6 +55,20 @@ export interface ChatOption {
sdk: () => UniplatSdk; sdk: () => UniplatSdk;
orgId: () => string | number; orgId: () => string | number;
product?: CustomerServiceProduct;
/**
* 用于标记会话启动是在客户端(用户)还是服务端(后端)
*/
serviceType?: ServiceType;
eventHub?: Vue;
/**
* 用户信息(头像,别名)可选
*/
user?: { icon?: string; username?: string };
} }
export interface ChatServiceLogger { export interface ChatServiceLogger {
...@@ -62,6 +93,12 @@ export const enum MessageType { ...@@ -62,6 +93,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;
...@@ -82,11 +119,17 @@ export interface Message { ...@@ -82,11 +119,17 @@ 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[];
export interface CreateChatByServicemanRequestResult { export interface BaseChatItemBusinessData {
model_name: string;
obj_id: string;
}
export interface BaseChatItem extends BaseChatItemBusinessData {
id: number; id: number;
org_id: string; org_id: string;
uid: string; uid: string;
...@@ -97,7 +140,6 @@ export interface CreateChatByServicemanRequestResult { ...@@ -97,7 +140,6 @@ export interface CreateChatByServicemanRequestResult {
app_id: string; app_id: string;
tag: string; tag: string;
msg_id: number; msg_id: number;
ext: string;
exit_msg_id: number; exit_msg_id: number;
is_exited: boolean; is_exited: boolean;
dnd: number; dnd: number;
...@@ -106,7 +148,6 @@ export interface CreateChatByServicemanRequestResult { ...@@ -106,7 +148,6 @@ export interface CreateChatByServicemanRequestResult {
join_msg_id: number; join_msg_id: number;
last_read_msg_id: number; last_read_msg_id: number;
biz_id: string; biz_id: string;
business_data: string;
is_finish: boolean; is_finish: boolean;
is_deleted: boolean; is_deleted: boolean;
is_remove: boolean; is_remove: boolean;
...@@ -125,10 +166,24 @@ export interface CreateChatByServicemanRequestResult { ...@@ -125,10 +166,24 @@ export interface CreateChatByServicemanRequestResult {
members_updated: number; members_updated: number;
user_updated: number; user_updated: number;
} }
export interface RawChatItem extends BaseChatItem {
ext: string;
last_msg_content: string;
business_data?: string;
}
export interface TransferedChatItem extends BaseChatItem {
chat_id: number;
ext: string;
last_msg_content: { text?: string };
}
export type ChatMemberExtraInfo = { export type ChatMemberExtraInfo = {
name?: string; name?: string;
phone?: string; phone?: string;
}; };
export interface ChatMember { export interface ChatMember {
id: number; id: number;
org_id: string; org_id: string;
......
...@@ -48,7 +48,7 @@ export namespace ChatStore { ...@@ -48,7 +48,7 @@ export namespace ChatStore {
export type STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID = string | null; export type STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID = string | null;
export const STATE_CHAT_USERNAME = "会话用户id-name"; export const STATE_CHAT_USERNAME = "会话用户id-name";
export type STATE_CHAT_USERNAME = { [key: string]: string } | {}; export type STATE_CHAT_USERNAME = { [key: string]: string };
export const STATE_CHAT_MY_ID = "聊天窗口显示在右边那个人的id"; export const STATE_CHAT_MY_ID = "聊天窗口显示在右边那个人的id";
export type STATE_CHAT_MY_ID = string | null; export type STATE_CHAT_MY_ID = string | null;
...@@ -56,7 +56,7 @@ export namespace ChatStore { ...@@ -56,7 +56,7 @@ export namespace ChatStore {
export type STATE_CHAT_MY_UID = string | null; export type STATE_CHAT_MY_UID = string | null;
export const STATE_CHAT_SOURCE = "stateChatSource"; export const STATE_CHAT_SOURCE = "stateChatSource";
export type STATE_CHAT_SOURCE = StateChatSourceDirection; export type STATE_CHAT_SOURCE = dto.ServiceType;
export const STATE_CURRENT_CHAT_INPUTING = "当前会话正在输入的人"; export const STATE_CURRENT_CHAT_INPUTING = "当前会话正在输入的人";
export type STATE_CURRENT_CHAT_INPUTING = string[]; export type STATE_CURRENT_CHAT_INPUTING = string[];
...@@ -67,14 +67,6 @@ export namespace ChatStore { ...@@ -67,14 +67,6 @@ export namespace ChatStore {
export const STATE_CHAT_SEND_FAIL_MESSAGE = "最新一条发送失败消息"; export const STATE_CHAT_SEND_FAIL_MESSAGE = "最新一条发送失败消息";
export type STATE_CHAT_SEND_FAIL_MESSAGE = string | null; export type STATE_CHAT_SEND_FAIL_MESSAGE = string | null;
/**
* 消息来源,是来自客服端(Server),还是来自于顾客端(Client)
*/
export const enum StateChatSourceDirection {
Server = 1,
Client = 0,
}
export const STATE_CURRENT_CHAT_MEMBERS = "当前会话参与者"; export const STATE_CURRENT_CHAT_MEMBERS = "当前会话参与者";
export type STATE_CURRENT_CHAT_MEMBERS = export type STATE_CURRENT_CHAT_MEMBERS =
| readonly (dto.ChatMember & dto.ChatMemberExtraInfo)[] | readonly (dto.ChatMember & dto.ChatMemberExtraInfo)[]
...@@ -89,6 +81,9 @@ export namespace ChatStore { ...@@ -89,6 +81,9 @@ export namespace ChatStore {
export const STATE_FUNC_ON_NEW_MSG = "收到消息回调方法"; export const STATE_FUNC_ON_NEW_MSG = "收到消息回调方法";
export type STATE_FUNC_ON_NEW_MSG = (e: chatDto.Message) => void; export type STATE_FUNC_ON_NEW_MSG = (e: chatDto.Message) => void;
export const STATE_CURRENT_UNREAD_MESSAGE_COUNT = "当前未读消息数";
export type STATE_CURRENT_UNREAD_MESSAGE_COUNT = number;
/* getter */ /* getter */
export const GETTER_CURRENT_CHAT_PRESENT_MEMBERS = "当前会话未退出的参与者"; export const GETTER_CURRENT_CHAT_PRESENT_MEMBERS = "当前会话未退出的参与者";
export type GETTER_CURRENT_CHAT_PRESENT_MEMBERS = dto.ChatMembers | null; export type GETTER_CURRENT_CHAT_PRESENT_MEMBERS = dto.ChatMembers | null;
...@@ -98,8 +93,10 @@ export namespace ChatStore { ...@@ -98,8 +93,10 @@ export namespace ChatStore {
/* mutation */ /* mutation */
export const MUTATION_SHOW_CHAT = "打开会话弹窗"; export const MUTATION_SHOW_CHAT = "打开会话弹窗";
export type MUTATION_SHOW_CHAT = () => void; export type MUTATION_SHOW_CHAT = () => void;
export const MUTATION_HIDE_CHAT = "关闭会话弹窗"; export const MUTATION_HIDE_CHAT = "关闭会话弹窗";
export type MUTATION_HIDE_CHAT = () => void; export type MUTATION_HIDE_CHAT = () => void;
export const MUTATION_SAVE_CHAT_LIST = "保存我的会话列表"; export const MUTATION_SAVE_CHAT_LIST = "保存我的会话列表";
export type MUTATION_SAVE_CHAT_LIST = ( export type MUTATION_SAVE_CHAT_LIST = (
list: STATE_MY_CHAT_ROOM_LIST list: STATE_MY_CHAT_ROOM_LIST
...@@ -170,6 +167,9 @@ export namespace ChatStore { ...@@ -170,6 +167,9 @@ export namespace ChatStore {
export const MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = "清空chat uniplat id"; export const MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = "清空chat uniplat id";
export type MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = () => void; export type MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = () => void;
export const MUTATION_WITHDRAW = "撤回";
export type MUTATION_WITHDRAW = (id: number) => void;
export const MUTATION_SAVE_MYSELF_ID = export const MUTATION_SAVE_MYSELF_ID =
"保存我的id:聊天窗口显示在右边那个人的id"; "保存我的id:聊天窗口显示在右边那个人的id";
export type MUTATION_SAVE_MYSELF_ID = () => void; export type MUTATION_SAVE_MYSELF_ID = () => void;
...@@ -179,9 +179,7 @@ export namespace ChatStore { ...@@ -179,9 +179,7 @@ export namespace ChatStore {
export type MUTATION_CLEAR_MYSELF_ID = () => void; export type MUTATION_CLEAR_MYSELF_ID = () => void;
export const MUTATION_SET_CHAT_SOURCE = "setChatSource"; export const MUTATION_SET_CHAT_SOURCE = "setChatSource";
export type MUTATION_SET_CHAT_SOURCE = ( export type MUTATION_SET_CHAT_SOURCE = (payload: dto.ServiceType) => void;
payload: StateChatSourceDirection
) => void;
export const MUTATION_SAVE_CURRENT_CHAT_MEMBERS = "保存当前会话参与者"; export const MUTATION_SAVE_CURRENT_CHAT_MEMBERS = "保存当前会话参与者";
export type MUTATION_SAVE_CURRENT_CHAT_MEMBERS = ( export type MUTATION_SAVE_CURRENT_CHAT_MEMBERS = (
...@@ -246,7 +244,10 @@ export namespace ChatStore { ...@@ -246,7 +244,10 @@ export namespace ChatStore {
export const ACTION_GET_MY_CHAT_LIST = "获取我的会话列表"; export const ACTION_GET_MY_CHAT_LIST = "获取我的会话列表";
export type ACTION_GET_MY_CHAT_LIST = ( export type ACTION_GET_MY_CHAT_LIST = (
keyword?: string keyword?: string
) => Promise<ChatStore.STATE_MY_CHAT_ROOM_LIST>; ) => Promise<ChatType[]>;
export const ACTION_REBUILD_UNREAD_MESSAGE_COUNT = "重新计算未读消息数";
export type ACTION_REBUILD_UNREAD_MESSAGE_COUNT = () => void;
export const ACTION_JOIN_CHAT = "加入某个会话"; export const ACTION_JOIN_CHAT = "加入某个会话";
export type ACTION_JOIN_CHAT = (chatId: number) => void; export type ACTION_JOIN_CHAT = (chatId: number) => void;
...@@ -309,10 +310,10 @@ export namespace ChatStore { ...@@ -309,10 +310,10 @@ export namespace ChatStore {
export const ACTION_SEND_MESSAGE = "发送消息"; export const ACTION_SEND_MESSAGE = "发送消息";
export type ACTION_SEND_MESSAGE = (params: { export type ACTION_SEND_MESSAGE = (params: {
msgType: "text" | "image" | "file" | "voice" | "video"; msgType: dto.MessageType;
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 = "添加成员";
...@@ -336,6 +337,24 @@ export namespace ChatStore { ...@@ -336,6 +337,24 @@ 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 ChatUpdateParameter {
chat: number;
type?: dto.MessageType;
msg?: string;
ts?: number;
eid?: string;
unread?: number;
}
export const ACTION_UPDATE_CHAT = "更新会话信息";
export type ACTION_UPDATE_CHAT = (p: ChatUpdateParameter) => void;
} }
export interface ChatStoreState { export interface ChatStoreState {
...@@ -362,6 +381,7 @@ export interface ChatStoreState { ...@@ -362,6 +381,7 @@ export interface ChatStoreState {
[ChatStore.STATE_CHAT_CURRENT_USER_TYPE]: ChatStore.STATE_CHAT_CURRENT_USER_TYPE; [ChatStore.STATE_CHAT_CURRENT_USER_TYPE]: ChatStore.STATE_CHAT_CURRENT_USER_TYPE;
[ChatStore.STATE_CHAT_SEND_FAIL_MESSAGE]: ChatStore.STATE_CHAT_SEND_FAIL_MESSAGE; [ChatStore.STATE_CHAT_SEND_FAIL_MESSAGE]: ChatStore.STATE_CHAT_SEND_FAIL_MESSAGE;
[ChatStore.STATE_CHAT_USERNAME]: ChatStore.STATE_CHAT_USERNAME; [ChatStore.STATE_CHAT_USERNAME]: ChatStore.STATE_CHAT_USERNAME;
[ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT] : ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT;
} }
export const chatStore = namespace(ChatStore.ns); export const chatStore = namespace(ChatStore.ns);
import { UserAgentHelper } from "../user-agent";
export enum TransformTarget {
Mobile,
Desktop,
}
export enum TransformDirection {
Home,
Search,
Favorite,
Followed,
HomeCategory,
Orders,
Order,
StationIndex,
StationSearch,
Articles,
Article,
Services,
Service,
Questions,
Question,
Documents,
Document,
Website,
}
export interface PcMobileTransformerOption {
/**
* 手机端根host,如 https://www.teammix.com/ms
*/
mobileHost?: string;
/**
* 桌面端根host,如 https://www.teammix.com/s
*/
desktopHost?: string;
}
/**
* 提供桌面端和手机端URL转换的方法
*/
export class PcMobileTransformer {
private static target = TransformTarget.Mobile
private static host = ""
private static readonly desktop2MobileMapping = new Map<
TransformDirection,
string
>([
[TransformDirection.Home, "/"],
[TransformDirection.Search, "/search"],
[TransformDirection.Favorite, "/mine/favorite"],
[TransformDirection.Followed, "/mine/followed"],
])
private static readonly rebuildMapping = new Map<
TransformDirection,
string
>([
[TransformDirection.StationIndex, "/s/{0}"],
[TransformDirection.Articles, "/s/{0}/articles/{1}"],
[TransformDirection.Article, "/s/{0}/article/{1}"],
[TransformDirection.Services, "/s/{0}/services/{1}"],
[TransformDirection.Service, "/s/{0}/service/{1}"],
[TransformDirection.Questions, "/s/{0}/questions/{1}"],
[TransformDirection.Question, "/s/{0}/question/{1}"],
[TransformDirection.Documents, "/s/{0}/documents/{1}"],
[TransformDirection.Document, "/s/{0}/document/{1}"],
[TransformDirection.Website, "/s/{0}/website/{1}"],
])
public static setup(option: PcMobileTransformerOption) {
if (option.desktopHost) {
PcMobileTransformer.target = TransformTarget.Desktop;
PcMobileTransformer.host = PcMobileTransformer.trimEnd(
option.desktopHost
);
}
if (option.mobileHost) {
PcMobileTransformer.target = TransformTarget.Mobile;
PcMobileTransformer.host = PcMobileTransformer.trimEnd(
option.mobileHost
);
}
}
public static isNeed2Redirect() {
if (PcMobileTransformer.target === TransformTarget.Mobile) {
return PcMobileTransformer.isMobileDevice();
}
return false;
}
private static trimEnd(input: string) {
if (input && input.endsWith("/")) {
return input.substring(0, input.length - 1);
}
return input;
}
private static isMobileDevice() {
const ua = window.navigator.userAgent;
return UserAgentHelper.isMobile(ua);
}
public static transform(
parameters: (string | number)[],
target: TransformDirection
) {
let v = "";
if (PcMobileTransformer.target === TransformTarget.Desktop) {
v = PcMobileTransformer.transform2Desktop(parameters, target);
}
if (PcMobileTransformer.target === TransformTarget.Mobile) {
v = PcMobileTransformer.transform2Mobile(parameters, target);
}
return v.replace(/\.html/gi, "");
}
private static transform2Mobile(
parameters: (string | number)[],
target: TransformDirection
) {
const url = PcMobileTransformer.desktop2MobileMapping.get(target);
if (url) {
let targetUrl = `${PcMobileTransformer.host}${url}`;
for (let i = 0; i < parameters.length; i++) {
targetUrl = targetUrl.replace(`{${i}}`, parameters[i] + "");
}
return targetUrl;
}
const base = PcMobileTransformer.rebuildMapping.get(target);
if (base) {
let rebuildUrl = base;
for (let i = 0; i < parameters.length; i++) {
rebuildUrl = rebuildUrl.replace(`{${i}}`, parameters[i] + "");
}
return `${PcMobileTransformer.host}${rebuildUrl}`;
}
return PcMobileTransformer.host;
}
private static transform2Desktop(
parameters: (string | number)[],
target: TransformDirection
) {
return target + "";
}
}
export class SeoHelper {
public static formatTitle(title?: string) {
if (
title &&
(title.indexOf("TeamMix") > -1 || title.indexOf("亲亲小站") > -1)
) {
return title;
}
return `${title || ""}-亲亲小站`;
}
public static updateFavicon(path: string) {
const link = document.querySelector(
"link[rel*='icon']"
) as HTMLLinkElement;
if (link) {
link.href = path;
} else {
const l = document.createElement("link");
l.type = "image/x-icon";
l.rel = "shortcut icon";
l.href = path;
document.getElementsByTagName("head")[0].appendChild(l);
}
}
}
...@@ -162,7 +162,7 @@ export function formatTime( ...@@ -162,7 +162,7 @@ export function formatTime(
if (String(time).indexOf("-")) { if (String(time).indexOf("-")) {
time = String(time).replace(/-/g, "/"); time = String(time).replace(/-/g, "/");
} }
if (/^\d+$/.test(time + '') && +time < STANDARD) { if (/^\d+$/.test(time + "") && +time < STANDARD) {
time = +time * 1000; time = +time * 1000;
} }
const t = new Date(time); const t = new Date(time);
...@@ -222,3 +222,13 @@ export function formatTime( ...@@ -222,3 +222,13 @@ export function formatTime(
format2DetailTime(hour, t, option.rule) format2DetailTime(hour, t, option.rule)
); );
} }
export function parseString2TimeValue(time: string, defaultValue = 0) {
if (!time) {
return defaultValue;
}
if (String(time).indexOf("-")) {
time = String(time).replace(/-/g, "/");
}
return new Date(time).valueOf();
}
export default "group";
...@@ -2,21 +2,29 @@ import type { UniplatSdk } from "uniplat-sdk"; ...@@ -2,21 +2,29 @@ import type { UniplatSdk } from "uniplat-sdk";
import { EmojiService } from "../service/emoji"; import { EmojiService } from "../service/emoji";
import { ChatOption, TokenStringGetter } from "./../model"; import {
ChatOption,
TokenStringGetter,
ServiceType,
CustomerServiceProduct,
} from "./../model";
import { ChatLoggerService } from "./logger"; import { ChatLoggerService } from "./logger";
import tokenManager from "./token"; import tokenManager from "./token";
import xim from "./xim"; import xim from "./xim";
import { dbController } from "../database";
class Chat { class Chat {
private _sdk?: () => UniplatSdk; private _sdk?: () => UniplatSdk;
private _orgId: () => string | number = () => "0"; private _orgId: () => string | number = () => "0";
private token!: TokenStringGetter; private token!: TokenStringGetter;
private serviceType = ServiceType.Backend;
private product = CustomerServiceProduct.Default;
private eventHub: Vue | null = null;
private keywords: string[] = [];
private userMapping: { [key: string]: { name: string; avatar: string } } = private userMapping: { [key: string]: { name: string; avatar: string } } =
{}; {};
private webHost = false;
public async setup(option: ChatOption) { public async setup(option: ChatOption) {
if (!option) { if (!option) {
throw new Error(`You must specify a chat option for chat service`); throw new Error(`You must specify a chat option for chat service`);
...@@ -29,15 +37,30 @@ class Chat { ...@@ -29,15 +37,30 @@ class Chat {
} }
this._sdk = option.sdk; this._sdk = option.sdk;
this._orgId = option.orgId; this._orgId = option.orgId;
option.serviceType !== undefined && (this.serviceType = option.serviceType);
option.product && (this.product = option.product);
this.eventHub = option.eventHub || null;
if (option.user) {
this.userMapping[this._sdk().global.uid] = {
name: option.user.username || "",
avatar: option.user.icon || "",
};
}
dbController.setup(this._sdk().global.uid);
this.token = async () => option.sdk().global.jwtToken; this.token = async () => option.sdk().global.jwtToken;
tokenManager.save(this.token); tokenManager.save(this.token);
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);
} }
...@@ -47,20 +70,28 @@ class Chat { ...@@ -47,20 +70,28 @@ 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();
}; };
public getServiceType() {
return this.serviceType;
}
public isBackend() {
return this.serviceType === ServiceType.Backend;
}
public getProduct() {
return this.product;
}
public getOrgId = () => { public getOrgId = () => {
return this._orgId(); return this._orgId();
}; };
public isWebHost() {
return this.webHost;
}
public setToken(token: TokenStringGetter) { public setToken(token: TokenStringGetter) {
return xim.setToken(token); return xim.setToken(token);
} }
...@@ -99,12 +130,28 @@ class Chat { ...@@ -99,12 +130,28 @@ class Chat {
} }
public getUserMapping() { public getUserMapping() {
return {} as any; return this.userMapping;
} }
private debug(message: string) { private debug(message: string) {
ChatLoggerService.logger?.debug(message); ChatLoggerService.logger?.debug(message);
} }
public $emit(event: string, ...args: any[]) {
if (this.eventHub) {
this.eventHub.$emit(event, ...args);
}
}
public $on(event: string, callback: Function) {
if (this.eventHub) {
this.eventHub.$on(event, callback);
}
}
public getMatchedTextKeywords() {
return this.keywords;
}
} }
export default new Chat(); export default new Chat();
import { MessageType } from "@/customer-service/model"; import { MessageHandled, MessageType } from "@/customer-service/model";
export interface Chat { export interface Chat {
id: number; id: number;
...@@ -22,12 +22,9 @@ export interface Chat { ...@@ -22,12 +22,9 @@ export interface Chat {
biz_id: string; biz_id: string;
last_msg_sender: string; last_msg_sender: string;
last_msg_content: string; last_msg_content: string;
last_msg_type: MessageType; last_msg_type: string;
business_data: {
model_name: string; model_name: string;
obj_id: string; obj_id: string;
detail_name: string;
};
is_finish: boolean; is_finish: boolean;
is_deleted: boolean; is_deleted: boolean;
is_remove: boolean; is_remove: boolean;
...@@ -48,6 +45,8 @@ export interface Chat { ...@@ -48,6 +45,8 @@ export interface Chat {
members_updated: number; members_updated: number;
user_updated: number; user_updated: number;
chat_id: number; chat_id: number;
catalog: string;
biz_type_id: number;
} }
export interface Message { export interface Message {
...@@ -56,7 +55,7 @@ export interface Message { ...@@ -56,7 +55,7 @@ export interface Message {
eid: string; eid: string;
id: number; id: number;
ts: number; ts: number;
type: string; type: MessageType;
msg: string; msg: string;
total_read_count: number; total_read_count: number;
read_count: number; read_count: number;
...@@ -70,6 +69,7 @@ export interface Message { ...@@ -70,6 +69,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 {
...@@ -95,44 +95,6 @@ export interface Member { ...@@ -95,44 +95,6 @@ export interface Member {
nick_name: string; nick_name: string;
} }
/**
* 消息类型
* @param text 文本
* @param file 文件
* @param image 图片
* @param voice 语音
* @param notify 通知类型
* @param text.notice 文本消息:公告文本
* @param video 视频
* @param url 卡片消息
* @param forward 转发消息
* @param quote 引用消息
* @param comment.forward 转发评论消息
* @param time 时间行
*/
export type ChatMessageType =
| "text"
| "file"
| "image"
| "voice"
| "notify"
| "text.notice"
| "video"
| "url"
| "forward"
| "quote"
| "comment.forward"
| "time";
export type ChatInputBoxData = { key: string } & (
| { at_id: string; type: "text"; body: TextMessageBody }
| { type: "image"; body: ImageMessageBody }
| { type: "file"; body: FileMessageBody }
| { type: "video"; body: VideoMessageBody }
| { at_id: string; type: "quote"; body: QuoteMessageBody }
| { at_id: string; type: "tm-at-member"; body: TextMessageBody }
);
export type TextMessageBody = { export type TextMessageBody = {
text: string; text: string;
}; };
...@@ -232,17 +194,6 @@ export type CommentForwardMessageBody = { ...@@ -232,17 +194,6 @@ export type CommentForwardMessageBody = {
comment_ids: number[]; // 评论id集合 comment_ids: number[]; // 评论id集合
}; };
// export type SpecifiedChatRecordMsg = SpecifiedChatRecord & {
// message?: Message | undefined | null,
// messageBody?: {
// "name"?: string, // "高达.txt",
// "url"?: string, // "9a5bd43db73681f6a90b9e717d8698c2",
// "size"?: number, // 4,
// "remark"?: string, // "C:\\Users\\Administrator\\Desktop\\高达.txt"
// }
// };
// 客服
export interface CsUser { export interface CsUser {
id: number; id: number;
oid: string; oid: string;
......
import Vue from "vue"; import Vue from "vue";
import { wampDebug, XChatClient } from "xchat-client"; import { wampDebug, XChatClient } from "xchat-client";
import chatType from "../xim/chat-type";
import { TokenStringGetter } from "./../model"; import { TokenStringGetter } from "./../model";
import { ChatLoggerService } from "./logger"; import { ChatLoggerService } from "./logger";
import { Message, NotifyMessage } from "./models/chat"; import { Message, NotifyMessage } from "./models/chat";
import chat from "./index"; import chat from "./index";
import { STATUS } from "xchat-client/dist/xchat";
wampDebug(true); wampDebug(true);
...@@ -16,11 +16,11 @@ function emptyFunc() { ...@@ -16,11 +16,11 @@ function emptyFunc() {
return null; return null;
} }
export type MsgListener = (msg: Message) => void export type MsgListener = (msg: Message) => void;
export type ChatNotifyListener = (msg: NotifyMessage) => void export type ChatNotifyListener = (msg: NotifyMessage) => void;
export type StatusChangeListener = (status: any, details: any) => void export type StatusChangeListener = (status: any, details: any) => void;
export enum Events { export enum Events {
Msg = "msg", Msg = "msg",
...@@ -33,15 +33,17 @@ export enum Kind { ...@@ -33,15 +33,17 @@ export enum Kind {
UserNotify = "user_notify", UserNotify = "user_notify",
} }
const chatType = "group";
export class Xim { export class Xim {
private eventBus = new Vue() private eventBus = new Vue();
private client?: XChatClient private client?: XChatClient;
private paramsForReconnection?: { private paramsForReconnection?: {
url: string; url: string;
token: TokenStringGetter; token: TokenStringGetter;
} };
public close() { public close() {
if (this.client) { if (this.client) {
...@@ -56,7 +58,7 @@ export class Xim { ...@@ -56,7 +58,7 @@ export class Xim {
} }
} }
private connectionPending = false private connectionPending = false;
public async open(url: string, token: TokenStringGetter) { public async open(url: string, token: TokenStringGetter) {
this.connectionPending = true; this.connectionPending = true;
...@@ -71,7 +73,10 @@ export class Xim { ...@@ -71,7 +73,10 @@ export class Xim {
client.onstatuschange = (status: any, details: any) => { client.onstatuschange = (status: any, details: any) => {
this.onStatusChange(status, details); this.onStatusChange(status, details);
if (status === "DISCONNECTED" || status === "CLOSED") { if (
status === STATUS.DISCONNECTED ||
status === STATUS.CLOSED
) {
reject(status); reject(status);
} }
}; };
...@@ -112,6 +117,11 @@ export class Xim { ...@@ -112,6 +117,11 @@ export class Xim {
return this.client.fetchChatList({}); return this.client.fetchChatList({});
} }
public fetchChatListAfter(lastMsgTs: number) {
if (this.client == null) return;
return this.client.fetchChatList({ last_msg_ts: lastMsgTs });
}
public fetchChat(chat_id: number) { public fetchChat(chat_id: number) {
if (this.client == null) return; if (this.client == null) return;
return this.client.fetchChat(chat_id); return this.client.fetchChat(chat_id);
...@@ -160,7 +170,7 @@ export class Xim { ...@@ -160,7 +170,7 @@ export class Xim {
this.checkConnected(); this.checkConnected();
if (this.client == null) { if (this.client == null) {
throw new Error("client shouldn't undefined"); throw new Error("client shouldn't undefined");
}; }
const res = await this.client.fetchChatMsgs(chatType, chatId, { const res = await this.client.fetchChatMsgs(chatType, chatId, {
lid, lid,
rid, rid,
...@@ -233,43 +243,43 @@ export class Xim { ...@@ -233,43 +243,43 @@ export class Xim {
return data; return data;
} }
public on(event: "msg", chatId: number, listener: MsgListener): this public on(event: "msg", chatId: number, listener: MsgListener): this;
public on(event: "msg", listener: MsgListener): this public on(event: "msg", listener: MsgListener): this;
public on( public on(
event: "chat_notify", event: "chat_notify",
kind: "chat_change", kind: "chat_change",
listener: ChatNotifyListener listener: ChatNotifyListener
): this ): this;
public on( public on(
event: "chat_notify", event: "chat_notify",
kind: string, kind: string,
listener: ChatNotifyListener listener: ChatNotifyListener
): this ): this;
public on(event: "chat_notify", listener: ChatNotifyListener): this public on(event: "chat_notify", listener: ChatNotifyListener): this;
public on(event: "status", listener: StatusChangeListener): this public on(event: "status", listener: StatusChangeListener): this;
public on(...args: any[]): this { public on(...args: any[]): this {
this.eventBus.$on(...this.parseEventListener(...args)); this.eventBus.$on(...this.parseEventListener(...args));
return this; return this;
} }
public off(event: "msg", chatId: number, listener: MsgListener): this public off(event: "msg", chatId: number, listener: MsgListener): this;
public off(event: "msg", listener: MsgListener): this public off(event: "msg", listener: MsgListener): this;
public off( public off(
event: "chat_notify", event: "chat_notify",
kind: "chat_change", kind: "chat_change",
listener: ChatNotifyListener listener: ChatNotifyListener
): this ): this;
public off( public off(
event: "chat_notify", event: "chat_notify",
kind: string, kind: string,
listener: ChatNotifyListener listener: ChatNotifyListener
): this ): this;
public off(event: "chat_notify", listener: ChatNotifyListener): this public off(event: "chat_notify", listener: ChatNotifyListener): this;
public off(event: "status", listener: StatusChangeListener): this public off(event: "status", listener: StatusChangeListener): this;
public off(...args: any[]): this { public off(...args: any[]): this {
this.eventBus.$off(...this.parseEventListener(...args)); this.eventBus.$off(...this.parseEventListener(...args));
return this; return this;
...@@ -310,6 +320,7 @@ export class Xim { ...@@ -310,6 +320,7 @@ export class Xim {
return this.client?.syncReadMsg(chatId, start_msg_id, end_msg_id); return this.client?.syncReadMsg(chatId, start_msg_id, end_msg_id);
} }
// xchat-client 2.2.2新增
public setUnRead(chatId: number) { public setUnRead(chatId: number) {
return this.client?.unreadChat(chatId); return this.client?.unreadChat(chatId);
} }
...@@ -330,16 +341,10 @@ export class Xim { ...@@ -330,16 +341,10 @@ export class Xim {
this.debug("xim connected"); this.debug("xim connected");
} }
/*
DISCONNECTED: "DISCONNECTED",
CONNECTING: "CONNECTING",
CONNECTED: "CONNECTED",
CLOSED: "CLOSED",
*/
private onStatusChange(status: any, details: any) { private onStatusChange(status: any, details: any) {
this.debug("onstatuschange", status, details); this.debug("onstatuschange", status, details);
this.emit(Events.Status, status, details); this.emit(Events.Status, status, details);
if (status === "DISCONNECTED" || status === "CLOSED") { if (status === STATUS.DISCONNECTED || status === STATUS.CLOSED) {
this.hanldeOffline(); this.hanldeOffline();
} }
} }
...@@ -391,6 +396,11 @@ export class Xim { ...@@ -391,6 +396,11 @@ export class Xim {
private debug(message: any, ...params: any[]) { private debug(message: any, ...params: any[]) {
ChatLoggerService.logger?.debug(message, params); ChatLoggerService.logger?.debug(message, params);
} }
public registerOnMessage(vue: Vue, action: (e: Message) => void) {
this.on("msg", action);
vue.$once("hook:beforeDestroy", () => this.off("msg", action));
}
} }
const ximInstance = new Xim(); const ximInstance = new Xim();
......
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