Commit ea4f460b by 杨铁龙

merage

parents 362709e0 19c37adb
<template>
<div class="chat-list-con">
<div class="chat-list h-100">
<slot />
<div class="chat-list-scroll">
<el-scrollbar ref="scrollbar" class="h-100 no-bottom-scrollbar">
<div
v-for="item in chatRooms"
:key="item.chat_id"
class="chat-item"
:class="{ selected: isSelected(item) }"
@click="goToChatRoom(item)"
>
<div class="red-dot" v-if="item.unread_msg_count > 0">
{{ item.unread_msg_count }}
</div>
<div class="chat-info">
<div
class="
chat-info-left
d-flex
justify-content-between
align-items-center
"
>
<div
:title="item.customer_name"
class="chat-name flex-fill text-dot-dot-dot"
>
<span>{{
item.title || item.chat_id
}}</span>
</div>
<div v-if="item.last_msg_ts" class="chat-time">
{{ formatTimestamp(item.last_msg_ts) }}
</div>
</div>
<div class="chat-msg text-dot-dot-dot">
{{ buildLastMessage(item) }}
</div>
</div>
</div>
<div
class="empty"
v-if="chatRooms && chatRooms.length <= 0"
>
{{ searchKeyword ? "无相关接待" : "无接待" }}
</div>
</el-scrollbar>
</div>
</div>
</div>
</template>
<script lang="ts">
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 { Chat as ChatType } from "@/customer-service/xim/models/chat";
import { ServiceType } from "../model";
import xim from "@/customer-service/xim";
@Component({ components: { avatar } })
export default class ChatList extends Controller {
private searchKeyword = "";
@Prop({ type: Number, default: -1 })
private selected!: number;
@Ref("scrollbar")
private readonly scrollbar!: Vue & { update: () => void };
private unReadMsgCount = 0;
private get chatRooms() {
if (this.chatList) {
const list = this.chatList.list
.filter((chat) => chat.title.indexOf(this.searchKeyword) > -1)
.sort((x, y) => y.last_msg_ts - x.last_msg_ts);
let unReadMsgCount = 0;
list.filter((chat) => chat.unread_msg_count > 0).forEach((chat) => {
unReadMsgCount += chat.unread_msg_count;
});
this.unReadMsgCount = unReadMsgCount;
this.$emit("list-count-update", this.unReadMsgCount);
xim.$emit(EVENTS.NewMsg, this.unReadMsgCount);
return list;
}
return [];
}
private isSelected(item: ChatType) {
if (this.chatId) {
return item.chat_id === this.chatId;
}
return this.selected === item.chat_id;
}
async created() {
await this.getMyChatList();
this.setSource(ServiceType.Backend);
this.scrollbar.update();
}
mounted() {
this.saveMyId();
}
public async search(searchKeyword: string) {
this.searchKeyword = searchKeyword.trim();
}
private async goToChatRoom(data: ChatType) {
if (this.chatId === data.chat_id) {
this.showChat();
return;
}
await this.saveChatId(data.chat_id).finally(this.raiseChatIdChanged);
this.showChat();
this.close();
if (data.unread_msg_count > 0) {
data.unread_msg_count = 0;
}
}
private raiseChatIdChanged() {
this.$emit("change");
}
private close() {
this.$emit("close");
}
private goToDetail(model_name: string, keyvalue: string) {
this.$router.push(
`/${this.$route.params.project}/${this.$route.params.entrance}/detail/${model_name}/key/${keyvalue}`
);
this.close();
}
}
</script>
<style lang="less" scoped>
.chat-list-con {
display: inline-block;
width: 25%;
box-sizing: border-box;
height: 100%;
border-right: 1px solid #ddd;
.title {
padding-left: 20px;
line-height: 59px;
font-size: 18px;
border-bottom: 1px solid #e1e1e1;
}
}
.chat-list {
text-align: center;
}
.chat-list-scroll {
height: 100%;
.empty {
margin-top: 100%;
}
}
.keyword-input {
width: 90%;
margin: 15px;
/deep/ .el-input__inner {
font-size: 13px;
height: 30px;
line-height: 30px;
border-radius: 15px;
padding-right: 15px;
}
/deep/ .el-icon-time {
background: transparent;
}
/deep/ .el-input__icon {
line-height: 32px;
}
}
.chat-list {
.chat-item {
position: relative;
cursor: pointer;
padding: 4px 15px 10px;
border-bottom: 1px solid #eee;
&:hover {
background: #e4f0ff;
}
&.selected {
background: #f0f0f0;
}
.red-dot {
position: absolute;
min-width: 14px;
height: 14px;
line-height: 14px;
padding: 0 2px;
background: #e87005;
border-radius: 7px;
z-index: 1;
right: 10px;
bottom: 10px;
font-size: 12px;
color: #fff;
}
.chat-info {
display: inline-block;
vertical-align: middle;
width: calc(100% - 10px);
.chat-name {
line-height: 35px;
font-size: 16px;
}
}
.chat-info-left {
text-align: start;
font-size: 14px;
line-height: 20px;
color: #333333;
margin-bottom: 10px;
.chat-time {
text-align: end;
flex: none;
color: #999999;
margin-left: 10px;
font-size: 12px;
line-height: 1;
}
}
.chat-msg {
color: #888;
text-align: start;
font-size: 12px;
margin-top: -15px;
}
}
}
.chat-check-detail {
margin-left: 10px;
}
</style>
<template>
<div class="chat-members">
<div
class="chat-member pos-rel"
v-for="item in chatMembers"
:key="item.id"
>
<span class="member-name ver-mid text-truncate">
{{ item.name || item.eid }}
</span>
<span class="member-phone ver-mid text-nowrap">
{{ item.phone }}
</span>
<span class="member-type ver-mid text-nowrap">
{{ memberTypeStr(item.type) }}
</span>
<el-button class="ver-mid get-out" type="text" @click="getout(item)"
>踢出</el-button
>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { ChatStore, chatStore } from "../store/model";
import avatar from "@/customer-service/components/avatar.vue";
import { ChatRole } from "../model";
@Component({ components: { avatar } })
export default class ChatMembers extends Vue {
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
@chatStore.Action(ChatStore.ACTION_CHAT_REMOVE_MEMBER)
private readonly _getout!: ChatStore.ACTION_CHAT_REMOVE_MEMBER;
@chatStore.Action(ChatStore.ACTION_CHAT_REMOVE_CS)
private readonly _getoutCs!: ChatStore.ACTION_CHAT_REMOVE_CS;
private async getout(
item: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS[number]
) {
await this.$confirm(`确定要移除${item.name}?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
// xim里的eid等于uniplat里的uid
if (item.type === ChatRole.Default) {
// 普通成员
this._getout([item.eid]);
} else if (item.type === ChatRole.CustomerService) {
// 可否
this._getoutCs([item.eid]);
}
}
private memberTypeStr(type: string | number) {
if (+type === ChatRole.CustomerService) {
return "客服";
}
if (+type === ChatRole.Admin) {
return "管理员";
}
return "";
}
}
</script>
<style lang="less" scoped>
.chat-members {
padding: 20px;
padding-bottom: 0;
background: #fff;
.chat-member {
display: flex;
vertical-align: top;
align-items: center;
margin: 10px 0;
padding: 10px;
&:hover {
background-color: #f5f7fa;
.get-out {
display: unset;
}
}
}
.member-name {
display: inline-block;
min-width: 7em;
margin-right: 10px;
}
.member-type {
margin-left: 20px;
}
.get-out {
display: none;
margin-left: auto;
padding: 0;
}
}
.ver-mid {
vertical-align: middle;
}
</style>
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
import MessageInput from "@/customer-service/components/message-input.vue"; import MessageInput from "@/customer-service/components/message-input.vue";
import messages from "@/customer-service/components/message-list.vue"; import messages from "@/customer-service/components/message-list.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model"; import { ChatStore, chatStore } from "@/customer-service/store/model";
import Chat from "@/customer-service/xim";
type RoomInfoTab = "customer" | "order"; type RoomInfoTab = "customer" | "order";
...@@ -125,7 +126,7 @@ ...@@ -125,7 +126,7 @@
} }
private onError(msg: string) { private onError(msg: string) {
this.$message.error(msg); Chat.error(msg);
} }
private dragControllerDiv(e: MouseEvent) { private dragControllerDiv(e: MouseEvent) {
......
<template>
<div class="room-title d-flex justify-content-between align-items-center">
<div class="title text-nowrap">
{{ chatTitle }}
<template v-if="chatMembers.length">
<span class="members-count"
>(成员{{ chatMembers.length }}人)</span
>
</template>
<template v-if="!notOnlyCheck">
<div v-if="currentChat.is_finish" class="chat-status chat-done">
已完成
</div>
<div v-else class="chat-status">接待中</div>
</template>
</div>
<div class="title-buttons d-flex align-items-center">
<el-button
class="button"
@click="startReception"
size="small"
v-if="!isChatMember"
type="primary"
:disabled="isChatError"
>我要接待</el-button
>
<el-button
class="button"
@click="showAddMember"
size="small"
:disabled="isChatError"
>添加客服</el-button
>
<el-button
class="button"
@click="finishReception"
size="small"
v-if="isChatMember && operatorType > 25"
type="warning"
:disabled="isChatError"
>结束接待</el-button
>
<el-button
class="button"
@click="exitChat"
size="small"
v-if="isChatMember"
type="danger"
:disabled="isChatError"
>退出会话</el-button
>
<i
v-if="close && isSingleChat"
@click="close"
class="title-close el-icon-close"
/>
</div>
<ChatCreator
v-if="visible"
:selected="chatMembersId"
@submit="addMember"
@hide="hideAddMember"
/>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import ChatCreator from "@/customer-service/components/create-chat.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model";
import { ChatRole } from "../model";
import {
ChatChangedEvent,
ChatEventHandler,
} from "./controller/chat-event-handler";
@Component({ components: { ChatCreator } })
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)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_TITLE)
private readonly chatTitle!: ChatStore.STATE_CURRENT_CHAT_TITLE;
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_IS_SINGLE)
private readonly isSingleChat: ChatStore.STATE_CHAT_DIALOG_IS_SINGLE;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR)
private readonly chatError: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR;
@chatStore.Action(ChatStore.ACTION_CHAT_ADD_CS)
private readonly _addCS!: ChatStore.ACTION_CHAT_ADD_CS;
@chatStore.Action(ChatStore.ACTION_CHAT_START_RECEPTION)
private readonly _startReception!: ChatStore.ACTION_CHAT_START_RECEPTION;
@chatStore.Action(ChatStore.ACTION_CHAT_FINISH_RECEPTION)
private readonly _finishReception!: ChatStore.ACTION_CHAT_FINISH_RECEPTION;
@chatStore.Action(ChatStore.ACTION_CHAT_USER_EXIT)
private readonly _userExitChat!: ChatStore.ACTION_CHAT_USER_EXIT;
@chatStore.Action(ChatStore.ACTION_CHAT_CS_EXIT)
private readonly _csExitChat!: ChatStore.ACTION_CHAT_CS_EXIT;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER)
private readonly isChatMember!: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_USER_UID)
private readonly operatorUid!: ChatStore.STATE_CHAT_CURRENT_USER_UID;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_USER_TYPE)
private readonly operatorType!: ChatStore.STATE_CHAT_CURRENT_USER_TYPE;
@chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT)
private readonly hideChat: ChatStore.MUTATION_HIDE_CHAT;
private get isChatError() {
return this.chatError === this.chatId;
}
private get chatMembersId() {
return this.chatMembers.map((k) => +k.eid);
}
private visible = false;
private get notOnlyCheck(): boolean {
return true;
}
@Prop({ type: Function })
private close?: () => void;
private showAddMember() {
this.visible = true;
}
private hideAddMember() {
this.visible = false;
}
private async addMember(users: string[], done: () => void) {
try {
await this._addCS(users);
this.hideAddMember();
} catch (error) {
console.error(error);
} finally {
done();
}
}
private noop() {
return 1;
}
private async exitChat() {
this.$confirm("确认要退出此会话?")
.then(async () => {
try {
if (+this.operatorType === ChatRole.Default) {
await this._userExitChat();
} else if (+this.operatorType > ChatRole.Default) {
await this._csExitChat();
}
this.hideChat();
} catch (error) {
console.error(error);
}
})
.catch(this.noop);
}
private async startReception() {
try {
await this._startReception().then(() =>
ChatEventHandler.raiseChatChanged(
ChatChangedEvent.Start,
this.chatId
)
);
this.$emit("updateActive", "my_receiving");
} catch (error) {
console.error(error);
}
}
private async finishReception() {
await this.$confirm(
"确定要结束接待吗?结束接待将会终止客服会话",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
);
await this._finishReception().then(() =>
ChatEventHandler.raiseChatChanged(ChatChangedEvent.End, this.chatId)
);
this.hideChat();
}
}
</script>
<style lang="less" scoped>
.room-title {
font-size: 16px;
padding: 0 20px;
height: 60px;
min-height: 60px;
border-bottom: 1px solid #e1e1e1;
.title {
cursor: pointer;
}
.members-count {
color: #666666;
}
.title-right-arrow {
font-size: 10px;
margin-left: 10px;
vertical-align: middle;
color: #666;
}
.title-close {
color: #8d959d;
cursor: pointer;
margin-left: 30px;
}
}
</style>
...@@ -49,6 +49,12 @@ export default class ChatList extends Vue { ...@@ -49,6 +49,12 @@ export default class ChatList extends Vue {
@chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA) @chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA)
protected readonly reset!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA; protected readonly reset!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA;
@chatStore.Getter(ChatStore.STATE_CHAT_MSG_HISTORY)
protected readonly historyMessage!: ChatStore.STATE_CHAT_MSG_HISTORY;
@chatStore.Getter(ChatStore.STATE_CHAT_SENDING_MESSAGES)
protected readonly sendingMessages!: ChatStore.STATE_CHAT_SENDING_MESSAGES;
private readonly invoker = Xim.getSdk(); private readonly invoker = Xim.getSdk();
protected parseMesage(data: ChatItem) { protected parseMesage(data: ChatItem) {
......
...@@ -16,5 +16,14 @@ export function parserMessage(type: string, rawMsg: string) { ...@@ -16,5 +16,14 @@ export function parserMessage(type: string, rawMsg: string) {
if (type === MessageType.Withdraw) { if (type === MessageType.Withdraw) {
return `[撤回了一条消息]`; return `[撤回了一条消息]`;
} }
if (type === MessageType.MyPurchasePlan) {
return `[我的采购计划]`;
}
if (type === MessageType.MyWelfare) {
return `[我的福利]`;
}
if (type === MessageType.QuestionAnswer) {
return `[问答]`;
}
return `[系统自动回复]`; return `[系统自动回复]`;
} }
<template>
<el-dialog
class="create-chat"
title="添加客服"
:visible="true"
@close="hide"
>
<div class="search-bar">
<div class="row input-row">
<span class="search-title">用户搜索: </span>
<el-input class="search-input" v-model="searchText"></el-input>
<el-button
style="margin-left: auto"
type="primary"
size="medium"
@click="search"
>筛选</el-button
>
</div>
<div class="row">
<GeneralTagSelectForFilter
ref="generalTagSelect"
:tagGroups="tagGroups"
class="tag-group"
></GeneralTagSelectForFilter>
</div>
</div>
<div class="users" v-loading="loading">
<div
v-for="user in userList"
:key="user.id"
class="user-con"
@click="onClickUser(user.id)"
:class="{ forbid: forbidden(user.id) }"
>
<avatar />
<span
class="user-name"
:class="{
isChoosed: isMember(user.id),
}"
>{{ user.name }}</span
>
</div>
</div>
<el-pagination
:current-page.sync="currentPage"
class="fs-pager"
layout="prev, pager, next"
:total="total"
:page-size="pageSize"
></el-pagination>
<span slot="footer" class="dialog-footer">
<el-button @click="hide">取 消</el-button>
<el-button type="primary" @click="createChat">确定</el-button>
</span>
</el-dialog>
</template>
<script lang="ts">
import { ListEasy, ListTypes, TagManagerTypes } from "uniplat-sdk";
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import buttonThrottle from "../utils/button-throttle";
import GeneralTagSelectForFilter from "@/components/statistic/GeneralTagSelectForFilter.vue";
import avatar from "@/customer-service/components/avatar.vue";
import chat from "@/customer-service/xim/index";
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
@Component({ components: { avatar, GeneralTagSelectForFilter } })
export default class ChatCreator extends Vue {
@Prop({ type: Array, default: () => [] })
private selected!: number[];
@Watch("currentPage")
private pageChange() {
this.nextPage();
}
private tagGroups: TagManagerTypes.TagGroup[] = [];
private searchText = "";
private currentPage = 1;
private total = 0;
private pageSize = 15;
private selectedUsers: number[] = [];
private userList: {
id: any;
name: any;
}[] = [];
private getList!: ThenArg<ReturnType<ListEasy["query"]>>["getList"];
public async created() {
await this.getUserList();
}
private getSelectedTags() {
if (this.$refs.generalTagSelect) {
return (
this.$refs.generalTagSelect as GeneralTagSelectForFilter
).getSelectedTags();
}
return [];
}
private async getUserList(searchText: string | null = null) {
this.loading = true;
const list = chat.getSdk().model("user").list();
if (searchText) {
list.addFilter({
property: "first_name",
value: searchText,
match: ListTypes.filterMatchType.fuzzy,
});
}
const { pageData, getList } = await list.query({
pageIndex: this.currentPage,
item_size: this.pageSize,
tagFilters: this.getSelectedTags(),
});
this.total = pageData.record_count;
this.getList = getList;
this.userList = this.exactUserList(pageData.rows);
this.loading = false;
this.tagGroups = pageData.tagGroups || [];
}
private exactUserList(rows: any[]) {
return rows.map((k) => {
return {
id: k.id.value,
name: k.first_name.value,
};
});
}
private hide() {
this.$emit("hide");
}
private search() {
this.currentPage = 1;
this.getUserList(this.searchText);
}
private loading = false;
@buttonThrottle()
private async nextPage() {
this.loading = true;
const data = await this.getList(this.currentPage);
this.loading = false;
this.userList = this.exactUserList(data.rows);
}
private forbidden(id: number) {
return this.selected.includes(id);
}
private isMember(id: number) {
return this.selectedUsers.includes(id) || this.selected.includes(id);
}
private isSelected(id: number) {
return this.selectedUsers.includes(id);
}
private onClickUser(id: number) {
if (this.isSelected(id)) {
this.removeUser(id);
} else {
this.addUser(id);
}
}
private addUser(id: number) {
this.selectedUsers.push(id);
}
private removeUser(id: number) {
this.selectedUsers = this.selectedUsers.filter((_id) => _id !== id);
}
@buttonThrottle()
private createChat() {
return new Promise((resolve) => {
this.$emit(
"submit",
this.selectedUsers.map((id) => String(id)),
resolve
);
});
}
}
</script>
<style lang="less" scoped>
.text-right {
text-align: right;
}
.create-chat {
/deep/ .el-dialog__body {
padding: 30px 40px;
}
}
.users {
white-space: pre-line;
}
.user-con {
display: inline-flex;
vertical-align: top;
width: 33.33%;
align-items: center;
margin-bottom: 40px;
cursor: pointer;
&.forbid {
cursor: not-allowed;
}
}
.user-name {
height: 15px;
font-size: 15px;
color: #000000;
line-height: 15px;
margin-left: 20px;
&.isChoosed {
color: #3285ff;
}
}
.search-bar {
align-items: center;
background: #f5f6fa;
padding: 12px 20px;
box-sizing: border-box;
margin-bottom: 30px;
.row + .row {
margin-top: 5px;
}
.tag-group /deep/ .checkbox-group {
padding: 0;
margin-bottom: 0;
}
.input-row {
display: flex;
line-height: 40px;
}
}
.search-input {
width: 160px;
margin-left: 10px;
/deep/ .el-input__inner {
border: 1px solid rgba(229, 230, 236, 1);
border-radius: 0;
padding-left: 21px;
}
}
</style>
/deep/.benefits-plan-message, /deep/.benefits-welfare-message{ /deep/.benefits-plan-message,
/deep/.benefits-welfare-message {
white-space: normal; white-space: normal;
.my-plan,.my-welfare{ .my-plan,
color: #E84929; .my-welfare {
color: #e84929;
margin-bottom: 10px; margin-bottom: 10px;
} }
.item-title{ }
/deep/.benefits-plan-message {
.plan-title-wrap {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
.total-price {
color: #e84929;
}
.item-title {
max-width: 185px;
color: #373737;
font-size: 14px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: underline;
margin-right: 8px;
cursor: pointer; cursor: pointer;
&:hover{
color: #666;
}
}
}
.product-item {
display: flex;
padding: 16px 0;
&:not(:last-child) {
border-bottom: 1px dashed #ededed;
}
img {
width: 50px;
height: 50px;
margin-right: 10px;
}
.product-detail {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
.product-name {
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price-nums{
display: flex;
justify-content: space-between;
}
}
}
}
/deep/.benefits-welfare-message {
.item-title {
font-weight: bold;
margin-bottom: 10px;
text-decoration: underline; text-decoration: underline;
cursor: pointer;
&:hover{
color: #666;
}
} }
} }
<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 {
protected readonly emptyText = " ";
protected 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;
padding: 20px 30px;
color: #409eff;
/deep/ .highlight {
color: #e87005;
}
}
</style>
\ No newline at end of file
...@@ -65,14 +65,14 @@ ...@@ -65,14 +65,14 @@
@Component({ components: { FileIcon } }) @Component({ components: { FileIcon } })
export default class Index extends BaseMessage { export default class Index extends BaseMessage {
private get getAttachment() { protected get getAttachment() {
if (this.messageBody) { if (this.messageBody) {
return this.messageBody.msg.name; return this.messageBody.msg.name;
} }
return "文件下载"; return "文件下载";
} }
private get fileIcon() { protected get fileIcon() {
if (this.value) { if (this.value) {
return getFileType(this.messageBody.msg.name); return getFileType(this.messageBody.msg.name);
} }
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
return FileType.Others; return FileType.Others;
} }
private format(v: number) { protected format(v: number) {
return formatSize(v); return formatSize(v);
} }
...@@ -97,6 +97,7 @@ ...@@ -97,6 +97,7 @@
border: 1px solid #c5d4e5; border: 1px solid #c5d4e5;
.file-message-name { .file-message-name {
max-width: 130px; max-width: 130px;
word-break: break-all;
} }
} }
</style> </style>
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<div <div
class="msg-detail image-message" class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }" :class="{ 'image-404': fileFailed2Load }"
@dblclick="openFile" @dblclick="dbClick"
@click="open"
> >
<img <img
v-if="messageRealUrl" v-if="messageRealUrl"
...@@ -20,11 +21,16 @@ ...@@ -20,11 +21,16 @@
import { FileType } from "./file-controller"; import { FileType } from "./file-controller";
import BaseMessage from "./index"; import BaseMessage from "./index";
import FileIcon from "./file-icon.vue"; import FileIcon from "./file-icon.vue";
import { UserAgentHelper } from "@/customer-service/third-party/user-agent";
@Component({ components: { FileIcon } }) @Component({ components: { FileIcon } })
export default class Index extends BaseMessage { export default class Index extends BaseMessage {
private readonly image404 = FileType.Image_404; private readonly image404 = FileType.Image_404;
private readonly mobile = UserAgentHelper.isMobile(
window.navigator.userAgent
);
private onImageError() { private onImageError() {
this.fileFailed2Load = true; this.fileFailed2Load = true;
this.messageRealUrl = ""; this.messageRealUrl = "";
...@@ -33,6 +39,14 @@ ...@@ -33,6 +39,14 @@
mounted() { mounted() {
this.buildMessageUrl(); this.buildMessageUrl();
} }
private dbClick() {
!this.mobile && this.openFile();
}
private open() {
this.mobile && this.openFile();
}
} }
</script> </script>
......
<template>
<div
class="msg-detail inline-text"
v-html="messageBody.msg.text || emptyText"
></div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {
protected readonly emptyText = " ";
}
</script>
<style lang="less" scoped>
.inline-text {
display: inline-block;
white-space: pre-wrap;
text-align: left;
}
</style>
\ No newline at end of file
<script lang="ts">
import { Component } from "vue-property-decorator";
import MyWelfareMessage from "@/customer-service/components/message-item/my-welfare-message.vue";
import Chat from "@/customer-service/xim/index";
@Component({ components: {} })
export default class Index extends MyWelfareMessage {
mounted() {
if (Chat.isBackend()) {
this.backEndPlan();
}
}
private backEndPlan() {
const productTitleDom = document.getElementsByClassName(
"plan-welfare-title"
) as any;
if (productTitleDom) {
// 遍历注册点击事件
productTitleDom.forEach((item: HTMLElement) => {
item.onclick = (e: any) => {
// 我的福利在后台没找到位置
if (e.target.attributes["data-type"].value === "plan") {
window.open(
`/福利宝.福利宝管理端.采购管理/detail/w_plan/key/${e.target.attributes["data-id"].value}`,
"_blank"
);
}
};
});
}
}
}
</script>
<style lang="less" scoped>
.inline-text {
display: inline-block;
white-space: pre-wrap;
text-align: left;
}
</style>
\ No newline at end of file
<script lang="ts">
import { Component } from "vue-property-decorator";
import TextMessage from "@/customer-service/components/message-item/text-message.vue";
@Component({ components: {} })
export default class Index extends TextMessage {}
</script>
<style lang="less" scoped>
.chat-room-con .message-template .my-message.msg-detail {
background-color: #f5f5f5 !important;
&::after {
border: 0;
}
}
.inline-text {
display: inline-block;
white-space: pre-wrap;
text-align: left;
}
</style>
\ No newline at end of file
...@@ -13,9 +13,9 @@ ...@@ -13,9 +13,9 @@
@Component({ components: {} }) @Component({ components: {} })
export default class Index extends BaseMessage { export default class Index extends BaseMessage {
private readonly emptyText = " "; protected readonly emptyText = " ";
private format2Link(text: string) { protected format2Link(text: string) {
let t = replaceText2Link(text); let t = replaceText2Link(text);
const keywords = xim.getMatchedTextKeywords(); const keywords = xim.getMatchedTextKeywords();
for (const item of keywords) { for (const item of keywords) {
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<div <div
class="message-con d-flex align-items-center" class="message-con d-flex align-items-center"
:class="{ :class="{
'my-message flex-row-reverse': isMyMessage, 'my-message flex-row-reverse':
isMyMessage && !isQuestionAnswerMessage,
'justify-content-center': isWithdrawMessage, 'justify-content-center': isWithdrawMessage,
'offset-bottom': matchKeywords, 'offset-bottom': matchKeywords,
}" }"
...@@ -13,23 +14,15 @@ ...@@ -13,23 +14,15 @@
:class="{ 'algin-left': !isMyMessage }" :class="{ 'algin-left': !isMyMessage }"
v-if="!isWithdrawMessage" v-if="!isWithdrawMessage"
> >
{{ userName }} {{ isQuestionAnswerMessage ? "" : userName }}
</div> </div>
<div <div
class="content-avatar d-flex align-items-start" class="content-avatar d-flex align-items-start"
:class="{ 'justify-content-end': isMyMessage }" :class="{
'justify-content-end': isMyMessage,
'cs-flex-direction': chatRole !== 'default',
}"
> >
<img
src="../imgs/default-host-avatar.svg"
style="width: 42px"
v-if="
!avatar &&
messageComponent &&
showHostAvatar &&
!isWithdrawMessage
"
class="host-avatar"
/>
<component <component
:is="messageComponent" :is="messageComponent"
:user-name="userName" :user-name="userName"
...@@ -40,7 +33,19 @@ ...@@ -40,7 +33,19 @@
v-model="data" v-model="data"
@open="openFile" @open="openFile"
/> />
<avatar v-if="avatar" :src="avatar" shape="circle" /> <avatar
v-if="
(avatar || showHostAvatar) &&
!isQuestionAnswerMessage &&
!isWithdrawMessage
"
:src="
chatRole === 'default'
? avatar
: require('../imgs/default-host-avatar.svg')
"
shape="circle"
/>
</div> </div>
</div> </div>
...@@ -133,8 +138,13 @@ ...@@ -133,8 +138,13 @@
import AudioMessage from "./message-item/audio-message.vue"; import AudioMessage from "./message-item/audio-message.vue";
import VideoMessage from "./message-item/video-message.vue"; import VideoMessage from "./message-item/video-message.vue";
import TextMessage from "./message-item/text-message.vue"; import TextMessage from "./message-item/text-message.vue";
import ActionMessage from "./message-item/action-message.vue";
import WithdrawMessage from "./message-item/withdraw-message.vue"; import WithdrawMessage from "./message-item/withdraw-message.vue";
import PurchasePlanMessage from "./message-item/purchase-plan-message.vue";
import MyWelfareMessage from "./message-item/my-welfare-message.vue";
import QuestionAnswerMessage from "./message-item/question-answer-message.vue";
import xim from "./../xim"; import xim from "./../xim";
import { ChatRole } from "@/customer-service/model";
const twoMinutes = 2 * 60 * 1000; const twoMinutes = 2 * 60 * 1000;
...@@ -146,6 +156,10 @@ ...@@ -146,6 +156,10 @@
[dto.MessageType.Text, "text-message"], [dto.MessageType.Text, "text-message"],
[dto.MessageType.Withdraw, "withdraw-message"], [dto.MessageType.Withdraw, "withdraw-message"],
[dto.MessageType.GeneralOrderMsg, "text-message"], [dto.MessageType.GeneralOrderMsg, "text-message"],
[dto.MessageType.MyPurchasePlan, "purchase-plan-message"],
[dto.MessageType.MyWelfare, "my-welfare-message"],
[dto.MessageType.QuestionAnswer, "question-answer-message"],
[dto.MessageType.Action, "action-message"],
]); ]);
@Component({ @Component({
...@@ -158,6 +172,10 @@ ...@@ -158,6 +172,10 @@
VideoMessage, VideoMessage,
TextMessage, TextMessage,
WithdrawMessage, WithdrawMessage,
PurchasePlanMessage,
MyWelfareMessage,
QuestionAnswerMessage,
ActionMessage,
}, },
}) })
export default class Message extends Vue { export default class Message extends Vue {
...@@ -216,6 +234,10 @@ ...@@ -216,6 +234,10 @@
return this.data.type === dto.MessageType.Withdraw; return this.data.type === dto.MessageType.Withdraw;
} }
private get isQuestionAnswerMessage() {
return this.data.type === dto.MessageType.QuestionAnswer;
}
private get isAllRead() { private get isAllRead() {
return this.data.read_count >= this.data.total_read_count; return this.data.read_count >= this.data.total_read_count;
} }
...@@ -271,13 +293,12 @@ ...@@ -271,13 +293,12 @@
private get avatar() { private get avatar() {
const avatar = chat.getUserMapping(); const avatar = chat.getUserMapping();
if (this.isSendingMessage) {
if (Object.getOwnPropertyNames(avatar).length > 0) { if (Object.getOwnPropertyNames(avatar).length > 0) {
this.showHostAvatar = true; this.showHostAvatar = true;
} else { } else {
this.showHostAvatar = false; this.showHostAvatar = false;
} }
if (this.isSendingMessage) {
if (avatar && this.chatMyId) { if (avatar && this.chatMyId) {
const user = avatar[this.chatMyId]; const user = avatar[this.chatMyId];
if (user && user.avatar) { if (user && user.avatar) {
...@@ -287,11 +308,6 @@ ...@@ -287,11 +308,6 @@
} }
if (avatar && this.data) { if (avatar && this.data) {
if (Object.getOwnPropertyNames(avatar).length > 0) {
this.showHostAvatar = true;
} else {
this.showHostAvatar = false;
}
const value = avatar[this.data.eid]; const value = avatar[this.data.eid];
if (value && value.avatar) { if (value && value.avatar) {
return value.avatar; return value.avatar;
...@@ -301,6 +317,16 @@ ...@@ -301,6 +317,16 @@
return ""; return "";
} }
private get chatRole() {
if (this.chatMembers) {
const t = this.chatMembers.find((i) => i.eid === this.data.eid);
if (t?.type === ChatRole.Default) {
return "default";
}
}
return "";
}
private get messageType() { private get messageType() {
const type = this.data?.type; const type = this.data?.type;
if (type === "file") { if (type === "file") {
...@@ -447,7 +473,6 @@ ...@@ -447,7 +473,6 @@
word-break: break-all; word-break: break-all;
} }
} }
&.offset-bottom { &.offset-bottom {
margin-bottom: 30px; margin-bottom: 30px;
} }
......
<template>
<div class="msg-short-cut-wrap">
<div class="btn-group top-btn-group">
<el-button
type="text"
@click="replyInputVisible = true"
v-if="!replyInputVisible"
>添加</el-button
>
<el-button
v-show="replyInputVisible"
@click="addReply"
size="small"
type="text"
>提交</el-button
>
<el-button
v-show="replyInputVisible"
@click="hideReplyInput"
size="small"
type="text"
>取消</el-button
>
<br />
<el-input
class="remark-input"
type="textarea"
v-show="replyInputVisible"
v-model="addReplyStr"
maxlength="200"
minlength="2"
:show-word-limit="true"
></el-input>
</div>
<div
class="shortcut pointer"
v-for="(reply, index) in replyList"
:key="reply.id"
@dblclick="goEdit(reply)"
>
<span class="rep-index">{{ index + 1 }}.</span>
<span class="rep-content" v-if="!editingItem[reply.id]">{{
reply.content
}}</span>
<el-input
v-if="editingItem[reply.id]"
class="remark-input"
type="textarea"
v-model="editingItemContent[reply.id]"
maxlength="200"
minlength="2"
:show-word-limit="true"
:ref="`input-item-${reply.id}`"
:rows="buildRows(editingItemContent[reply.id])"
></el-input>
<div class="btn-group">
<el-button
type="text"
@click="sendMsg(reply)"
v-if="!editingItem[reply.id]"
:disabled="!isChatMember"
>发送</el-button
>
<el-button
type="text"
v-if="uid === reply.created_by && !editingItem[reply.id]"
@click="editing(reply)"
>编辑</el-button
>
<el-button
type="text"
v-if="uid === reply.created_by && !editingItem[reply.id]"
@click="delReply(reply)"
>删除</el-button
>
<el-button
v-if="editingItem[reply.id]"
@click="editDone(reply)"
type="text"
>确定</el-button
>
<el-button
v-if="editingItem[reply.id]"
@click="editCancel(reply)"
type="text"
>取消</el-button
>
</div>
</div>
<div
v-if="!replyList.length && !replyInputVisible"
class="text-center text-hint"
>
暂无快捷回复,<el-button
type="text"
@click="replyInputVisible = true"
>添加</el-button
>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { MessageType } from "../model";
import { ChatStore, chatStore } from "../store/model";
import buttonThrottle from "../utils/button-throttle";
interface Reply {
sort: string;
id: string;
content: string;
uniplat_version: string;
created_by: string;
}
const ReplyModelName = "uniplat_chat_reply";
let cacheReply: Reply[] = [];
@Component({ components: {} })
export default class MsgShortCut extends Vue {
@chatStore.Action(ChatStore.ACTION_SEND_MESSAGE)
private readonly _sendMsg!: ChatStore.ACTION_SEND_MESSAGE;
@chatStore.Getter(ChatStore.STATE_CHAT_SOURCE)
private readonly source!: ChatStore.STATE_CHAT_SOURCE;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER)
private readonly isChatMember!: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER;
@chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
protected readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
private replyList: Reply[] = [];
private uid = this.sdk.global.uid;
private replyInputVisible = false;
private addReplyStr = "";
private editingItem: { [key: string]: boolean } = {};
private editingItemContent: { [key: string]: string } = {};
mounted() {
this.getReplyList();
}
private async getReplyList() {
if (cacheReply && cacheReply.length) {
return (this.replyList = cacheReply);
}
cacheReply = this.replyList = await this.sdk
.domainService("uniplat_base", "chat.chat", "reply")
.request("get");
}
@buttonThrottle()
private async sendMsg(reply: Reply) {
return this._sendMsg({
msgType: MessageType.Text,
msg: JSON.stringify({ text: reply.content, source: this.source }),
}).then(() => this.getMyChatList());
}
private delReply(reply: Reply) {
this.$confirm("确定要删除该回复吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
this.sdk
.model(ReplyModelName)
.action("delete")
.updateInitialParams({
selected_list: [
{ v: +reply.uniplat_version, id: +reply.id },
],
})
.execute()
.then(() => {
const index = this.replyList.findIndex(
(it) => it === reply
);
this.replyList.splice(index, 1);
});
});
}
private hideReplyInput() {
this.replyInputVisible = false;
this.addReplyStr = "";
}
private addReply() {
const addReplyStr = this.addReplyStr.trim();
if (addReplyStr.length < 2 || addReplyStr.length > 200) {
this.$message.warning("回复在2-200个字符之间");
return;
}
this.sdk
.model(ReplyModelName)
.action("insert_my")
.addInputs_parameter({
category: "毕节客服工作台",
org_id: this.sdk.global.initData.orgId,
model: ReplyModelName,
content: addReplyStr,
sort: 0,
})
.execute()
.then(() => {
this.getReplyList();
this.hideReplyInput();
});
}
private editing(reply: Reply) {
this.$set(this.editingItem, reply.id, true);
this.$set(this.editingItemContent, reply.id, reply.content);
}
private editCancel(reply: Reply) {
this.$set(this.editingItem, reply.id, false);
}
private editDone(reply: Reply) {
const addReplyStr = this.editingItemContent[reply.id].trim();
if (addReplyStr.length < 2 || addReplyStr.length > 200) {
this.$message.warning("回复在2-200个字符之间");
return;
}
this.sdk
.model(ReplyModelName)
.action("update")
.updateInitialParams({
selected_list: [{ v: +reply.uniplat_version, id: +reply.id }],
})
.addInputs_parameter({
category: "毕节客服工作台",
org_id: this.sdk.global.initData.orgId,
model: ReplyModelName,
content: addReplyStr,
sort: 0,
is_enabled: 1,
uniplat_uid: this.sdk.global.uid,
})
.execute()
.then(() => {
reply.content = addReplyStr;
reply.uniplat_version = (+reply.uniplat_version + 1).toString();
this.editCancel(reply);
});
}
private goEdit(reply: Reply) {
this.editing(reply);
this.$nextTick(() => {
const e = (this.$refs as any)[`input-item-${reply.id}`] as {
focus: () => void;
}[];
if (e && e.length) {
e[0].focus();
}
});
}
private buildRows(content: string) {
return Math.round(content.length / 18);
}
}
</script>
<style lang="less" scoped>
.shortcut {
position: relative;
padding: 10px 10px;
font-size: 14px;
white-space: normal;
color: #999;
.rep-content {
margin-left: 5px;
}
&:hover {
background: #e4f0ff;
}
}
.btn-group {
text-align: right;
.el-button {
padding: 4px 0;
}
&.top-btn-group {
padding: 0 10px;
}
}
.msg-short-cut-wrap {
height: 100%;
overflow: auto;
}
</style>
<template>
<div class="h-100 pos-rel workflows">
<div class="workflow-header" v-if="flowList.length">
<div class="cell">名称</div>
<div class="cell">备注</div>
<div class="cell">状态</div>
<div class="cell">操作</div>
</div>
<el-scrollbar class="workflow-scrollbar adjust-el-scroll-right-bar">
<div
class="workflow pos-rel table"
v-for="item in flowList"
:key="item.id"
>
<span class="cell workflow-name">
{{ item.processName }}
</span>
<span class="cell workflow-remark">
{{ item.state }}
</span>
<span class="cell status">
{{ getStatus(item.status) }}
</span>
<span class="cell">
<el-button
v-if="item.status === workFlowstatus.未启动"
class="get-out"
type="text"
@click="start(item)"
>启动</el-button
>
<el-button
v-else
class="get-out"
type="text"
@click="goToDetail(item)"
>查看详情</el-button
>
</span>
</div>
<div v-if="!flowList.length" class="text-center text-hint">
暂无数据
</div>
</el-scrollbar>
<div class="detal-btns">
<el-button>查看详情</el-button>
</div>
<start-process-dialog
@dialog-confirm="startDialogConfirm"
:modelName="model_name"
:ids="[]"
:processName="processName"
actionName="startProcess"
:selected_list="selectedProcess"
ref="startProcessDialog"
/>
</div>
</template>
<script lang="ts">
import { EVENTS } from "@/EventConsts";
import startProcessDialog from "@/views/workflow2/components/startProcessDialog.vue";
import { Component, Prop, Ref, Vue } from "vue-property-decorator";
import Chat from "../xim/index";
enum WorkFlowStatus {
"未启动" = 0,
"已启动" = 1,
"已完成" = 2,
}
@Component({ components: { startProcessDialog } })
export default class WorkFlow extends Vue {
@Ref("startProcessDialog")
private readonly startProcessIns!: startProcessDialog;
@Prop({ required: true })
private readonly model_name!: string;
@Prop({ required: true })
private readonly id!: string | number;
@Prop({ type: String, default: null })
private readonly name!: string;
private processName = "";
private selectedProcess = [];
private startDialogConfirm() {
console.log("startDialogConfirm");
}
private workFlowstatus = WorkFlowStatus;
private getStatus(status: WorkFlowStatus) {
return WorkFlowStatus[status];
}
private flowList = [];
public async created() {
this.flowList = await Chat.getSdk()
.model(this.model_name)
.workflow2()
.queryProcessByAssociateId(this.id as number);
}
public start(workflow: any) {
this.processName = workflow.processName;
this.selectedProcess = [{ id: workflow.id }];
this.startProcessIns.showDialog();
}
public goToDetail(workflow: any) {
Chat.$emit(EVENTS.ShowModalDialog, {
dialogName: "show_process_detail",
params: {
modelName: this.model_name,
selected: JSON.stringify([{ id: workflow.id }]),
},
});
}
}
</script>
<style lang="less" scoped>
.workflows {
padding: 10px 30px;
padding-bottom: 0;
background: #fff;
.workflow {
margin: 10px 0;
padding: 10px 0;
&:hover {
background-color: #f5f7fa;
}
}
.cell {
display: inline-block;
width: 25%;
}
.workflow-name {
word-break: break-word;
white-space: pre-line;
}
.get-out {
padding: 0;
}
}
.workflow-scrollbar {
height: calc(100% - 15px);
}
.text-hint {
margin: 20px 0;
}
</style>
...@@ -69,6 +69,13 @@ export interface ChatOption { ...@@ -69,6 +69,13 @@ export interface ChatOption {
* 用户信息(头像,别名)可选 * 用户信息(头像,别名)可选
*/ */
user?: { icon?: string; username?: string }; user?: { icon?: string; username?: string };
message?: ChatMessageController;
}
export interface ChatMessageController {
error: (msg: string) => void;
info: (msg: string) => void;
} }
export interface ChatServiceLogger { export interface ChatServiceLogger {
...@@ -91,6 +98,9 @@ export const enum MessageType { ...@@ -91,6 +98,9 @@ export const enum MessageType {
Voice = "voice", Voice = "voice",
GeneralOrderMsg = "general_order_msg", GeneralOrderMsg = "general_order_msg",
Withdraw = "withdraw", Withdraw = "withdraw",
MyPurchasePlan = "my_purchase_plan",
MyWelfare = "my_welfare",
QuestionAnswer = "question_answer",
Action = "action", Action = "action",
} }
......
...@@ -83,11 +83,18 @@ async function preCacheImgs(msgs?: any[]) { ...@@ -83,11 +83,18 @@ async function preCacheImgs(msgs?: any[]) {
} }
function buildChatItem(chat: RawChatItem) { function buildChatItem(chat: RawChatItem) {
if (!chat.model_name && chat.business_data) { if (chat.business_data) {
const b = JSON.parse(chat.business_data) as BaseChatItemBusinessData; const b = JSON.parse(chat.business_data) as BaseChatItemBusinessData;
if (!chat.model_name) {
chat.model_name = b.model_name; chat.model_name = b.model_name;
b.obj_id && (chat.obj_id = b.obj_id); b.obj_id && (chat.obj_id = b.obj_id);
} }
b.detail_name &&
!chat.detail_name &&
(chat.detail_name = b.detail_name);
}
return { ...chat, chat_id: chat.id } as ChatType; return { ...chat, chat_id: chat.id } as ChatType;
} }
...@@ -476,7 +483,7 @@ export default { ...@@ -476,7 +483,7 @@ export default {
items.forEach((i) => (sum += i.unread_msg_count)); items.forEach((i) => (sum += i.unread_msg_count));
state[ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT] = sum; state[ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT] = sum;
}, },
async [ChatStore.ACTION_GET_CHAT_MESSAGES]({ state, commit }) { async [ChatStore.ACTION_GET_CHAT_MESSAGES]({ state, commit, getters }) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]; const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return; if (chatId == null) return;
let data: Message[] = []; let data: Message[] = [];
...@@ -484,12 +491,25 @@ export default { ...@@ -484,12 +491,25 @@ export default {
if (cache && cache.length) { if (cache && cache.length) {
data = cache; data = cache;
} else { } else {
const current = state[
ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER
] as boolean;
if (current) {
data = await xim.queryLastPageMsg( data = await xim.queryLastPageMsg(
chatType, chatType,
chatId, chatId,
20, 20,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER] !state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
); );
} else {
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
data = await xim.queryLastPageMsgWhenNotMember({
model: currentChat.model_name,
obj: currentChat.obj_id,
});
}
} }
try { try {
commit(ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY, data); commit(ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY, data);
...@@ -503,35 +523,69 @@ export default { ...@@ -503,35 +523,69 @@ export default {
} }
}, },
async [ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID]( async [ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID](
{ state, commit }, { state, commit, getters },
msgId: Parameters<ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID>[0] msgId: Parameters<ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID>[0]
) { ) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]; const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return; if (chatId == null) return;
const data = await xim.queryPrevPageMsg( const current = state[
ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER
] as boolean;
let data: Message[] = [];
if (current) {
data = await xim.queryPrevPageMsg(
chatType, chatType,
chatId, chatId,
msgId, msgId,
10, 10,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER] !state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
); );
} else {
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
data = await xim.queryPreviousPageMsgWhenNotMember(
{
model: currentChat.model_name,
obj: currentChat.obj_id,
},
msgId
);
}
commit(ChatStore.MUTATION_UNSHIFT_CHAT_MSG_HISTORY, data); commit(ChatStore.MUTATION_UNSHIFT_CHAT_MSG_HISTORY, data);
dbController.appendMessages(chatId, data); dbController.appendMessages(chatId, data);
return data; return data;
}, },
async [ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID]( async [ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID](
{ state, commit }, { state, commit, getters },
msgId: Parameters<ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID>[0] msgId: Parameters<ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID>[0]
) { ) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]; const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return; if (chatId == null) return;
const data = await xim.queryNextPageMsg( const current = state[
ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER
] as boolean;
let data: Message[] = [];
if (current) {
data = await xim.queryNextPageMsg(
chatType, chatType,
chatId, chatId,
msgId, msgId,
10, 10,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER] !state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
); );
} else {
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
data = await xim.queryNextPageMsgWhenNotMember(
{
model: currentChat.model_name,
obj: currentChat.obj_id,
},
msgId
);
}
commit(ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY, data); commit(ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY, data);
dbController.appendMessages(chatId, data); dbController.appendMessages(chatId, data);
return data; return data;
...@@ -546,7 +600,9 @@ export default { ...@@ -546,7 +600,9 @@ export default {
} }
} }
try { try {
const chat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const chat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
const data = await Chat.getSdk() const data = await Chat.getSdk()
.model(chat.model_name) .model(chat.model_name)
.chat(chat.obj_id, orgId()) .chat(chat.obj_id, orgId())
...@@ -809,7 +865,9 @@ export default { ...@@ -809,7 +865,9 @@ export default {
); );
}, },
async [ChatStore.ACTION_CHAT_START_RECEPTION]({ getters, dispatch }) { async [ChatStore.ACTION_CHAT_START_RECEPTION]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
...@@ -824,7 +882,9 @@ export default { ...@@ -824,7 +882,9 @@ export default {
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS)); .finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
}, },
async [ChatStore.ACTION_CHAT_FINISH_RECEPTION]({ getters, dispatch }) { async [ChatStore.ACTION_CHAT_FINISH_RECEPTION]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
...@@ -839,7 +899,9 @@ export default { ...@@ -839,7 +899,9 @@ export default {
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS)); .finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
}, },
async [ChatStore.ACTION_CHAT_USER_EXIT]({ getters, dispatch }) { async [ChatStore.ACTION_CHAT_USER_EXIT]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
...@@ -854,7 +916,9 @@ export default { ...@@ -854,7 +916,9 @@ export default {
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS)); .finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
}, },
async [ChatStore.ACTION_CHAT_CS_EXIT]({ getters, dispatch }) { async [ChatStore.ACTION_CHAT_CS_EXIT]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
...@@ -872,7 +936,9 @@ export default { ...@@ -872,7 +936,9 @@ export default {
{ getters, dispatch }, { getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_ADD_MEMBERS>[0] uids: Parameters<ChatStore.ACTION_CHAT_ADD_MEMBERS>[0]
) { ) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
...@@ -883,14 +949,19 @@ export default { ...@@ -883,14 +949,19 @@ export default {
return await Chat.getSdk() return await Chat.getSdk()
.model(currentChat.model_name) .model(currentChat.model_name)
.chat(currentChat.obj_id, orgId()) .chat(currentChat.obj_id, orgId())
.addMember(uids.map((id) => +id)) .addMember(
uids.map((id) => +id),
-30
)
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS)); .finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
}, },
async [ChatStore.ACTION_CHAT_REMOVE_MEMBER]( async [ChatStore.ACTION_CHAT_REMOVE_MEMBER](
{ getters, dispatch }, { getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_REMOVE_MEMBER>[0] uids: Parameters<ChatStore.ACTION_CHAT_REMOVE_MEMBER>[0]
) { ) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
...@@ -908,7 +979,9 @@ export default { ...@@ -908,7 +979,9 @@ export default {
{ getters, dispatch }, { getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_ADD_CS>[0] uids: Parameters<ChatStore.ACTION_CHAT_ADD_CS>[0]
) { ) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
...@@ -926,7 +999,9 @@ export default { ...@@ -926,7 +999,9 @@ export default {
{ getters, dispatch }, { getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_REMOVE_CS>[0] uids: Parameters<ChatStore.ACTION_CHAT_REMOVE_CS>[0]
) { ) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT]; const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if ( if (
!currentChat || !currentChat ||
!currentChat.model_name || !currentChat.model_name ||
......
...@@ -7,13 +7,14 @@ import { ...@@ -7,13 +7,14 @@ import {
TokenStringGetter, TokenStringGetter,
ServiceType, ServiceType,
CustomerServiceProduct, CustomerServiceProduct,
ChatMessageController,
} from "./../model"; } 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"; import { dbController } from "../database";
class Chat { class Chat implements ChatMessageController {
private _sdk?: () => UniplatSdk; private _sdk?: () => UniplatSdk;
private _orgId: () => string | number = () => "0"; private _orgId: () => string | number = () => "0";
private token!: TokenStringGetter; private token!: TokenStringGetter;
...@@ -24,6 +25,7 @@ class Chat { ...@@ -24,6 +25,7 @@ class Chat {
private ws = ""; private ws = "";
private connectedActions: (() => void)[] = []; private connectedActions: (() => void)[] = [];
private connected = false; private connected = false;
private messageController: ChatMessageController | null = null;
private userMapping: { [key: string]: { name: string; avatar: string } } = private userMapping: { [key: string]: { name: string; avatar: string } } =
{}; {};
...@@ -59,7 +61,9 @@ class Chat { ...@@ -59,7 +61,9 @@ class Chat {
}; };
} }
this.setupIndexDb(); option.message && (this.messageController = option.message);
await this.setupIndexDb();
this.token = async () => option.sdk().global.jwtToken; this.token = async () => option.sdk().global.jwtToken;
tokenManager.save(this.token); tokenManager.save(this.token);
...@@ -88,7 +92,7 @@ class Chat { ...@@ -88,7 +92,7 @@ class Chat {
s.global.uid + "-" + (s.global.initData.orgId || 0) s.global.uid + "-" + (s.global.initData.orgId || 0)
); );
} }
return Promise.reject(); return Promise.reject(new Error("Sdk is not defined"));
} }
public resetup(org: () => string | number) { public resetup(org: () => string | number) {
...@@ -140,7 +144,7 @@ class Chat { ...@@ -140,7 +144,7 @@ class Chat {
if (xim.isConnected()) { if (xim.isConnected()) {
return Promise.resolve(uri); return Promise.resolve(uri);
} }
return new Promise((resolve) => { return new Promise<void>((resolve) => {
xim.open(uri, this.token).finally(() => { xim.open(uri, this.token).finally(() => {
this.registerXimEvent(); this.registerXimEvent();
if (xim.isConnected()) { if (xim.isConnected()) {
...@@ -172,7 +176,7 @@ class Chat { ...@@ -172,7 +176,7 @@ class Chat {
} }
private debug(message: string) { private debug(message: string) {
ChatLoggerService.logger?.debug(message); ChatLoggerService.logger && ChatLoggerService.logger.debug(message);
} }
public $emit(event: string, ...args: any[]) { public $emit(event: string, ...args: any[]) {
...@@ -190,6 +194,14 @@ class Chat { ...@@ -190,6 +194,14 @@ class Chat {
public getMatchedTextKeywords() { public getMatchedTextKeywords() {
return this.keywords; return this.keywords;
} }
public error(msg: string) {
this.messageController && this.messageController.error(msg);
}
public info(msg: string) {
this.messageController && this.messageController.info(msg);
}
} }
export default new Chat(); export default new Chat();
...@@ -7,6 +7,7 @@ import { Message, NotifyMessage } from "./models/chat"; ...@@ -7,6 +7,7 @@ import { Message, NotifyMessage } from "./models/chat";
import chat from "./index"; import chat from "./index";
import { STATUS } from "xchat-client/dist/xchat"; import { STATUS } from "xchat-client/dist/xchat";
import { AxiosInstance } from "axios";
wampDebug(true); wampDebug(true);
...@@ -37,6 +38,10 @@ const chatType = "group"; ...@@ -37,6 +38,10 @@ const chatType = "group";
export class Xim { export class Xim {
private eventBus = new Vue(); private eventBus = new Vue();
private readonly messageNotifyActions: {
key: string;
action: (m: Message) => void;
}[] = [];
private client?: XChatClient; private client?: XChatClient;
...@@ -96,6 +101,10 @@ export class Xim { ...@@ -96,6 +101,10 @@ export class Xim {
return token.replace(/^Bearer\s/, ""); return token.replace(/^Bearer\s/, "");
} }
private buildError() {
return new Error(`Connect is not ready`);
}
/** /**
* token过期或者切换用户登录时,需要设置新的token * token过期或者切换用户登录时,需要设置新的token
*/ */
...@@ -108,22 +117,22 @@ export class Xim { ...@@ -108,22 +117,22 @@ export class Xim {
} }
public fetchMsgInBox(chatId: number, msgId: number) { public fetchMsgInBox(chatId: number, msgId: number) {
if (this.client == null) return Promise.reject(); if (this.client == null) return Promise.reject(this.buildError());
return this.client.fetchMsgInBox(chatType, chatId, msgId); return this.client.fetchMsgInBox(chatType, chatId, msgId);
} }
public fetchChatList() { public fetchChatList() {
if (this.client == null) return Promise.reject(); if (this.client == null) return Promise.reject(this.buildError());
return this.client.fetchChatList({}); return this.client.fetchChatList({});
} }
public fetchChatListAfter(lastMsgTs: number) { public fetchChatListAfter(lastMsgTs: number) {
if (this.client == null) return Promise.reject(); if (this.client == null) return Promise.reject(this.buildError());
return this.client.fetchChatList({ last_msg_ts: lastMsgTs }); return this.client.fetchChatList({ last_msg_ts: lastMsgTs });
} }
public fetchChat(chat_id: number) { public fetchChat(chat_id: number) {
if (this.client == null) return Promise.reject(); if (this.client == null) return Promise.reject(this.buildError());
return this.client.fetchChat(chat_id); return this.client.fetchChat(chat_id);
} }
...@@ -137,13 +146,13 @@ export class Xim { ...@@ -137,13 +146,13 @@ export class Xim {
msg: string msg: string
) { ) {
this.checkConnected(); this.checkConnected();
if (this.client == null) return Promise.reject(); if (this.client == null) return Promise.reject(this.buildError());
return this.client.sendMsg(chatType, chatId, msgType, msg, "", {}); return this.client.sendMsg(chatType, chatId, msgType, msg, "", {});
} }
public inputing(chatId: number) { public inputing(chatId: number) {
this.checkConnected(); this.checkConnected();
if (this.client == null) return Promise.reject(); if (this.client == null) return Promise.reject(this.buildError());
return this.client.userInput(chatType, chatId); return this.client.userInput(chatType, chatId);
} }
...@@ -152,7 +161,7 @@ export class Xim { ...@@ -152,7 +161,7 @@ export class Xim {
*/ */
public fetchChatMembers(chat_id: number) { public fetchChatMembers(chat_id: number) {
this.checkConnected(); this.checkConnected();
if (this.client == null) return Promise.reject(); if (this.client == null) return Promise.reject(this.buildError());
return this.client.fetchChatMembers(chat_id); return this.client.fetchChatMembers(chat_id);
} }
...@@ -199,6 +208,55 @@ export class Xim { ...@@ -199,6 +208,55 @@ export class Xim {
return data; return data;
} }
public async queryLastPageMsgWhenNotMember(chatParameter: {
model: string;
obj: string;
}) {
return this.queryMessagesWhenNotMember(chatParameter, 0, 0, 20, 1);
}
public async queryPreviousPageMsgWhenNotMember(
chatParameter: {
model: string;
obj: string;
},
start: number
) {
return this.queryMessagesWhenNotMember(chatParameter, 0, start, 20, 1);
}
public async queryNextPageMsgWhenNotMember(
chatParameter: {
model: string;
obj: string;
},
end: number
) {
return this.queryMessagesWhenNotMember(chatParameter, end, 0, 20, 0);
}
private queryMessagesWhenNotMember(
chatParameter: { model: string; obj: string },
lid: number,
rid: number,
limit: number,
desc: number
) {
const sdk = chat.getSdk();
const query = sdk.getAxios() as AxiosInstance;
const q = [
`lid=${lid}`,
`rid=${rid}`,
`limit=${limit}`,
`desc=${desc}`,
];
return query.get<any, Message[]>(
`${sdk.global.baseUrl}general/xim/model/${chatParameter.model}/${
chatParameter.obj
}/msgs?${q.join("&")}`
);
}
/** 查询上一页消息 */ /** 查询上一页消息 */
public async queryPrevPageMsg( public async queryPrevPageMsg(
chatType: string, chatType: string,
...@@ -366,6 +424,17 @@ export class Xim { ...@@ -366,6 +424,17 @@ export class Xim {
private handleMsg(kind: any, msg: any) { private handleMsg(kind: any, msg: any) {
this.debug(`收到消息 ${new Date().getTime()}`, kind, msg); this.debug(`收到消息 ${new Date().getTime()}`, kind, msg);
if (
(kind === "chat" || kind === "chat_notify") &&
msg &&
msg.msg_type !== "read" &&
msg.msg_type !== "user.input"
) {
for (const item of this.messageNotifyActions) {
item.action(msg);
}
}
switch (kind) { switch (kind) {
case "chat": case "chat":
this.emit(`msg`, msg); this.emit(`msg`, msg);
...@@ -384,7 +453,7 @@ export class Xim { ...@@ -384,7 +453,7 @@ export class Xim {
if (this.client == null) return; if (this.client == null) return;
if (!this.client.connected) { if (!this.client.connected) {
try { try {
this.client?.open(); this.client && this.client.open();
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("checkConnected", e); console.error("checkConnected", e);
...@@ -394,12 +463,20 @@ export class Xim { ...@@ -394,12 +463,20 @@ export class Xim {
} }
private debug(message: any, ...params: any[]) { private debug(message: any, ...params: any[]) {
ChatLoggerService.logger?.debug(message, params); ChatLoggerService.logger &&
ChatLoggerService.logger.debug(message, params);
} }
public registerOnMessage(vue: Vue, action: (e: Message) => void) { public registerOnMessage(vue: Vue, action: (e: Message) => void) {
this.on("msg", action); const key = `${new Date().valueOf()}-${Math.random()}`;
vue.$once("hook:beforeDestroy", () => this.off("msg", action)); this.messageNotifyActions.push({ key, action });
vue.$once("hook:beforeDestroy", () => {
const t = this.messageNotifyActions.find((i) => i.key === key);
if (t) {
const index = this.messageNotifyActions.indexOf(t);
this.messageNotifyActions.splice(index, 1);
}
});
} }
} }
......
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