Commit ea4f460b by 杨铁龙

merage

parents 362709e0 19c37adb
<template>
<div class="chat-list-con">
<div class="chat-list h-100">
<div class="chat-list-scroll">
<el-scrollbar
ref="scrollbar"
class="list-scroll no-bottom-scrollbar"
>
<div
v-for="item in chatRooms"
:key="'room_' + item.id"
class="chat-item"
:class="{ selected: item.chat_id == activeId }"
@click="goToChatRoom(item)"
>
<div class="chat-info">
<div
class="checkbox-wrap"
@click.stop
v-if="showItemCheckbox"
>
<el-checkbox
v-model="item.checked"
></el-checkbox>
</div>
<div class="w-100">
<div
class="
chat-info-left
d-flex
justify-content-between
align-items-center
"
>
<div
class="
chat-name
flex-fill
text-dot-dot-dot
"
>
<span>{{ item.title }}</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">
{{
userNames[item.last_msg_sender]
? userNames[item.last_msg_sender] +
": "
: ""
}}{{ parseMesage(item) }}
</div>
</div>
</div>
</div>
<div
class="empty"
v-if="chatRooms && chatRooms.length <= 0"
>
无接待
</div>
</el-scrollbar>
<div class="d-flex align-items-center justify-content-center">
<el-pagination
class="page-comp"
@size-change="handleSizeChange"
:page-size="pageSize"
:total="total"
:current-page.sync="currentPage"
@current-change="getList"
:page-sizes="[10, 20, 50]"
:pager-count="5"
layout="total, prev, pager, next"
></el-pagination>
</div>
<div class="action-row">
<el-button v-if="showItemCheckbox" @click="toggle"
>全选</el-button
>
<el-button
v-if="!showItemCheckbox"
@click="showItemCheckbox = true"
>批量接待</el-button
>
<el-button
v-if="showItemCheckbox"
@click="batchStartReception"
type="primary"
>确定接待</el-button
>
<el-button v-if="showItemCheckbox" @click="unselectAll"
>取消</el-button
>
<i
title="刷新"
class="refresh-icon"
@click="getList"
:class="
refreshing ? 'el-icon-loading' : 'el-icon-refresh'
"
></i>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
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 {
formatTime,
parseString2TimeValue,
TimeFormatRule,
} from "@/customer-service/utils/time";
import { Chat as ChatType } from "@/customer-service/xim/models/chat";
import xim from "@/customer-service/xim";
interface SelectChatType extends ChatType {
checked?: boolean;
}
@Component({ components: {} })
export default class ModelChatList extends Vue {
@chatStore.Action(ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN)
private readonly _createChat!: ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN;
private chatList: SelectChatType[] = [];
@chatStore.Mutation(ChatStore.MUTATION_SAVE_MYSELF_ID)
private readonly saveMyId!: ChatStore.MUTATION_SAVE_MYSELF_ID;
@chatStore.Mutation(ChatStore.MUTATION_SET_CHAT_SOURCE)
private readonly setSource!: ChatStore.MUTATION_SET_CHAT_SOURCE;
@chatStore.State(ChatStore.STATE_CHAT_USERNAME)
private readonly userNames!: ChatStore.STATE_CHAT_USERNAME;
@chatStore.Mutation(ChatStore.MUTATION_SAVE_USERNAME)
private readonly updateUserName!: ChatStore.MUTATION_SAVE_USERNAME;
@chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA)
private readonly clearChatId!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA;
@Prop({ type: Number, default: -1 })
private selected!: number;
@Prop({ type: String })
private modelName!: string;
@Prop({ type: String })
private listName!: string;
@Ref("scrollbar")
private scrollbar: Vue & { update: () => void };
private activeId = "";
private pageSize = 10;
private total = 0;
private currentPage = 1;
private sseTs = 0;
private showItemCheckbox = false;
private refreshing = false;
private get chatRooms() {
return this.chatList.sort(
(x, y) =>
parseString2TimeValue((y.last_msg_ts || "") + "", 100) -
parseString2TimeValue((x.last_msg_ts || "") + "", 99)
);
}
public clearActiveId() {
this.clearChatId();
}
private async getList() {
this.refreshing = true;
let result = await this.sdk
.model(this.modelName)
.list(this.listName || undefined)
.query({
pageIndex: this.currentPage,
item_size: this.pageSize,
})
.finally(() => (this.refreshing = false));
if (result.pageData.rows.length === 0 && this.currentPage !== 1) {
this.currentPage = 1;
if (result.pageData.record_count > 0) {
result = await this.sdk
.model(this.modelName)
.list(this.listName || undefined)
.query({
pageIndex: this.currentPage,
item_size: this.pageSize,
});
}
}
this.chatList = result.pageData.rows.map((it) => {
return {
id: it.id.value,
chat_id: it.ImChatId.value,
model_name: it.ModelName.value,
obj_id: it.ObjId.value,
last_msg_sender: it.LastSpeakUid.value,
last_msg_content: it.LastMsgContent.value,
last_msg_ts: it.LastMsgTime.value,
last_msg_type: it.LastMsgType.value,
title: it.Title.value || `会话${it.id.value}`,
checked: false,
} as SelectChatType;
});
this.total = result.pageData.record_count;
this.$emit("list-count-update", this.total);
}
async created() {
await this.getList();
this.setSource(xim.getServiceType());
this.scrollbar && this.scrollbar.update();
await this.sdk
.model("UniplatChat")
.registerOnChange(this.onTransportMessage);
await this.sdk
.model("general_order")
.registerOnChange(this.onTransportMessage);
if (
this.listName === "group_receiving" ||
this.listName === "group_wait"
) {
xim.$on(EVENTS.ChatUpdate, this.refreshListDebounce);
}
}
onTransportMessage(e: any) {
const index = e.dataUpdates.findIndex(
(it) =>
it.action === "startChat" ||
it.action === "createChat" ||
it.action === "csExitChat" ||
it.action === "finishChat" ||
(it.action === "delete" && it.model === "general_order") ||
(it.action === "sendMsg" &&
this.listName === "group_before_handle" &&
this.chatList.findIndex(
(chat) => chat.id === it.selectedList[0]
) > -1)
);
if (index > -1) {
if (
this.listName === "group_before_handle" &&
e.dataUpdates.findIndex((it) => it.action === "sendMsg") > -1
) {
xim.$emit(EVENTS.ChatUpdate);
}
this.refreshListDebounce();
}
}
private refreshListDebounce() {
if (this.sseTs) {
return;
}
this.sseTs = new Date().getTime();
setTimeout(() => {
this.sseTs = 0;
this.getList();
}, 1000);
}
mounted() {
this.saveMyId();
}
private handleSizeChange(newPageSize) {
this.pageSize = newPageSize;
this.getList();
}
private async goToChatRoom(data: ChatType) {
await this._createChat({
modelName: data.model_name,
selectedListId: data.obj_id,
uids: [],
showByPage: true,
});
this.activeId = data.chat_id.toString();
}
private raiseChatIdChanged() {
this.$emit("change");
}
private parseMesage(data: ChatType) {
if (data.last_msg_sender && data.last_msg_sender !== "0") {
if (this.userNames[data.last_msg_sender] === undefined) {
this.updateUserName({ id: data.last_msg_sender, name: "" });
this.sdk
.model("user")
.detail(data.last_msg_sender)
.query()
.then((userInfo) => {
this.updateUserName({
id: data.last_msg_sender,
name: userInfo.row.first_name.display as string,
});
});
}
}
if (data.last_msg_content === "") return "[暂无消息]";
return parserMessage(data.last_msg_type, data.last_msg_content);
}
private formatTimestamp(v: number) {
return formatTime(v, { short: true, rule: TimeFormatRule.Hour12 });
}
private goToDetail(model_name: string, keyvalue: string) {
this.$router.push(
`/${this.$route.params.project}/${this.$route.params.entrance}/detail/${model_name}/key/${keyvalue}`
);
}
private batchStartReception() {
const chats = this.chatRooms.filter((chat) => chat.checked);
if (chats.length === 0) {
return this.$message.warning("请先勾选要接待的会话");
}
const length = chats.length;
let count = 0;
chats.forEach((chat) => {
this.sdk
.model(chat.model_name)
.chat(chat.obj_id, this.global.org.id.toString())
.startChat()
.finally(() => {
count++;
if (count >= length) {
this.getList();
this.$message.success(`批量接待完成`);
}
});
chat.checked = false;
});
this.showItemCheckbox = false;
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>
<style lang="less" scoped>
.w-100 {
width: 100%;
}
.chat-list-con {
display: inline-block;
position: relative;
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 {
padding-top: 100%;
}
.list-scroll {
height: calc(100% - 110px);
}
}
.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;
}
}
.chat-list {
.chat-item {
cursor: pointer;
padding: 4px 15px 10px;
border-bottom: 1px solid #eee;
&:hover {
background: #e4f0ff;
}
&.selected {
background: #f0f0f0;
}
.chat-avatar {
display: inline-block;
vertical-align: middle;
width: 0;
height: 0;
margin-right: 5px;
}
.chat-info {
display: flex;
vertical-align: middle;
width: calc(100% - 10px);
.chat-name {
line-height: 35px;
min-height: 35px;
font-size: 15px;
}
.checkbox-wrap {
padding-top: 18px;
padding-right: 8px;
}
}
.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: -13px;
min-height: 16px;
}
}
}
.chat-check-detail {
margin-left: 10px;
}
.need-update-tip {
background: #fff2e6;
color: #e87005;
font-size: 14px;
font-weight: 400;
line-height: 14px;
padding: 8px 0;
text-align: center;
cursor: pointer;
}
.page-comp {
padding: 15px 0;
}
.action-row {
position: relative;
.el-button {
padding: 8px 14px;
border-radius: 15px;
}
}
.refresh-icon {
margin: 0 5px;
cursor: pointer;
color: #409eff;
position: absolute;
right: 10px;
top: 8px;
}
</style>
<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 @@
import MessageInput from "@/customer-service/components/message-input.vue";
import messages from "@/customer-service/components/message-list.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model";
import Chat from "@/customer-service/xim";
type RoomInfoTab = "customer" | "order";
......@@ -125,7 +126,7 @@
}
private onError(msg: string) {
this.$message.error(msg);
Chat.error(msg);
}
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 {
@chatStore.Action(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();
protected parseMesage(data: ChatItem) {
......
......@@ -16,5 +16,14 @@ export function parserMessage(type: string, rawMsg: string) {
if (type === MessageType.Withdraw) {
return `[撤回了一条消息]`;
}
if (type === MessageType.MyPurchasePlan) {
return `[我的采购计划]`;
}
if (type === MessageType.MyWelfare) {
return `[我的福利]`;
}
if (type === MessageType.QuestionAnswer) {
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;
.my-plan,.my-welfare{
color: #E84929;
.my-plan,
.my-welfare {
color: #e84929;
margin-bottom: 10px;
}
.item-title{
cursor: pointer;
}
/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;
&: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;
cursor: pointer;
&:hover{
color: #666;
}
}
}
\ No newline at end of file
}
<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 @@
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private get getAttachment() {
protected get getAttachment() {
if (this.messageBody) {
return this.messageBody.msg.name;
}
return "文件下载";
}
private get fileIcon() {
protected get fileIcon() {
if (this.value) {
return getFileType(this.messageBody.msg.name);
}
......@@ -80,7 +80,7 @@
return FileType.Others;
}
private format(v: number) {
protected format(v: number) {
return formatSize(v);
}
......@@ -97,6 +97,7 @@
border: 1px solid #c5d4e5;
.file-message-name {
max-width: 130px;
word-break: break-all;
}
}
</style>
......@@ -2,7 +2,8 @@
<div
class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }"
@dblclick="openFile"
@dblclick="dbClick"
@click="open"
>
<img
v-if="messageRealUrl"
......@@ -20,11 +21,16 @@
import { FileType } from "./file-controller";
import BaseMessage from "./index";
import FileIcon from "./file-icon.vue";
import { UserAgentHelper } from "@/customer-service/third-party/user-agent";
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private readonly image404 = FileType.Image_404;
private readonly mobile = UserAgentHelper.isMobile(
window.navigator.userAgent
);
private onImageError() {
this.fileFailed2Load = true;
this.messageRealUrl = "";
......@@ -33,6 +39,14 @@
mounted() {
this.buildMessageUrl();
}
private dbClick() {
!this.mobile && this.openFile();
}
private open() {
this.mobile && this.openFile();
}
}
</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 @@
@Component({ components: {} })
export default class Index extends BaseMessage {
private readonly emptyText = " ";
protected readonly emptyText = " ";
private format2Link(text: string) {
protected format2Link(text: string) {
let t = replaceText2Link(text);
const keywords = xim.getMatchedTextKeywords();
for (const item of keywords) {
......
......@@ -2,7 +2,8 @@
<div
class="message-con d-flex align-items-center"
:class="{
'my-message flex-row-reverse': isMyMessage,
'my-message flex-row-reverse':
isMyMessage && !isQuestionAnswerMessage,
'justify-content-center': isWithdrawMessage,
'offset-bottom': matchKeywords,
}"
......@@ -13,23 +14,15 @@
:class="{ 'algin-left': !isMyMessage }"
v-if="!isWithdrawMessage"
>
{{ userName }}
{{ isQuestionAnswerMessage ? "" : userName }}
</div>
<div
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
:is="messageComponent"
:user-name="userName"
......@@ -40,7 +33,19 @@
v-model="data"
@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>
......@@ -133,8 +138,13 @@
import AudioMessage from "./message-item/audio-message.vue";
import VideoMessage from "./message-item/video-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 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 { ChatRole } from "@/customer-service/model";
const twoMinutes = 2 * 60 * 1000;
......@@ -146,6 +156,10 @@
[dto.MessageType.Text, "text-message"],
[dto.MessageType.Withdraw, "withdraw-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({
......@@ -158,6 +172,10 @@
VideoMessage,
TextMessage,
WithdrawMessage,
PurchasePlanMessage,
MyWelfareMessage,
QuestionAnswerMessage,
ActionMessage,
},
})
export default class Message extends Vue {
......@@ -216,6 +234,10 @@
return this.data.type === dto.MessageType.Withdraw;
}
private get isQuestionAnswerMessage() {
return this.data.type === dto.MessageType.QuestionAnswer;
}
private get isAllRead() {
return this.data.read_count >= this.data.total_read_count;
}
......@@ -271,13 +293,12 @@
private get avatar() {
const avatar = chat.getUserMapping();
if (Object.getOwnPropertyNames(avatar).length > 0) {
this.showHostAvatar = true;
} else {
this.showHostAvatar = false;
}
if (this.isSendingMessage) {
if (Object.getOwnPropertyNames(avatar).length > 0) {
this.showHostAvatar = true;
} else {
this.showHostAvatar = false;
}
if (avatar && this.chatMyId) {
const user = avatar[this.chatMyId];
if (user && user.avatar) {
......@@ -287,11 +308,6 @@
}
if (avatar && this.data) {
if (Object.getOwnPropertyNames(avatar).length > 0) {
this.showHostAvatar = true;
} else {
this.showHostAvatar = false;
}
const value = avatar[this.data.eid];
if (value && value.avatar) {
return value.avatar;
......@@ -301,6 +317,16 @@
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() {
const type = this.data?.type;
if (type === "file") {
......@@ -447,7 +473,6 @@
word-break: break-all;
}
}
&.offset-bottom {
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 {
* 用户信息(头像,别名)可选
*/
user?: { icon?: string; username?: string };
message?: ChatMessageController;
}
export interface ChatMessageController {
error: (msg: string) => void;
info: (msg: string) => void;
}
export interface ChatServiceLogger {
......@@ -91,6 +98,9 @@ export const enum MessageType {
Voice = "voice",
GeneralOrderMsg = "general_order_msg",
Withdraw = "withdraw",
MyPurchasePlan = "my_purchase_plan",
MyWelfare = "my_welfare",
QuestionAnswer = "question_answer",
Action = "action",
}
......
......@@ -83,10 +83,17 @@ async function preCacheImgs(msgs?: any[]) {
}
function buildChatItem(chat: RawChatItem) {
if (!chat.model_name && chat.business_data) {
if (chat.business_data) {
const b = JSON.parse(chat.business_data) as BaseChatItemBusinessData;
chat.model_name = b.model_name;
b.obj_id && (chat.obj_id = b.obj_id);
if (!chat.model_name) {
chat.model_name = b.model_name;
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;
}
......@@ -476,7 +483,7 @@ export default {
items.forEach((i) => (sum += i.unread_msg_count));
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];
if (chatId == null) return;
let data: Message[] = [];
......@@ -484,12 +491,25 @@ export default {
if (cache && cache.length) {
data = cache;
} else {
data = await xim.queryLastPageMsg(
chatType,
chatId,
20,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
);
const current = state[
ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER
] as boolean;
if (current) {
data = await xim.queryLastPageMsg(
chatType,
chatId,
20,
!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 {
commit(ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY, data);
......@@ -503,35 +523,69 @@ export default {
}
},
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]
) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return;
const data = await xim.queryPrevPageMsg(
chatType,
chatId,
msgId,
10,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
);
const current = state[
ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER
] as boolean;
let data: Message[] = [];
if (current) {
data = await xim.queryPrevPageMsg(
chatType,
chatId,
msgId,
10,
!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);
dbController.appendMessages(chatId, data);
return data;
},
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]
) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return;
const data = await xim.queryNextPageMsg(
chatType,
chatId,
msgId,
10,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
);
const current = state[
ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER
] as boolean;
let data: Message[] = [];
if (current) {
data = await xim.queryNextPageMsg(
chatType,
chatId,
msgId,
10,
!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);
dbController.appendMessages(chatId, data);
return data;
......@@ -546,7 +600,9 @@ export default {
}
}
try {
const chat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
const chat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
const data = await Chat.getSdk()
.model(chat.model_name)
.chat(chat.obj_id, orgId())
......@@ -809,7 +865,9 @@ export default {
);
},
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 (
!currentChat ||
!currentChat.model_name ||
......@@ -824,7 +882,9 @@ export default {
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
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 (
!currentChat ||
!currentChat.model_name ||
......@@ -839,7 +899,9 @@ export default {
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
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 (
!currentChat ||
!currentChat.model_name ||
......@@ -854,7 +916,9 @@ export default {
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
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 (
!currentChat ||
!currentChat.model_name ||
......@@ -872,7 +936,9 @@ export default {
{ getters, dispatch },
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 (
!currentChat ||
!currentChat.model_name ||
......@@ -883,14 +949,19 @@ export default {
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.addMember(uids.map((id) => +id))
.addMember(
uids.map((id) => +id),
-30
)
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
async [ChatStore.ACTION_CHAT_REMOVE_MEMBER](
{ getters, dispatch },
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 (
!currentChat ||
!currentChat.model_name ||
......@@ -908,7 +979,9 @@ export default {
{ getters, dispatch },
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 (
!currentChat ||
!currentChat.model_name ||
......@@ -926,7 +999,9 @@ export default {
{ getters, dispatch },
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 (
!currentChat ||
!currentChat.model_name ||
......
......@@ -7,13 +7,14 @@ import {
TokenStringGetter,
ServiceType,
CustomerServiceProduct,
ChatMessageController,
} from "./../model";
import { ChatLoggerService } from "./logger";
import tokenManager from "./token";
import xim from "./xim";
import { dbController } from "../database";
class Chat {
class Chat implements ChatMessageController {
private _sdk?: () => UniplatSdk;
private _orgId: () => string | number = () => "0";
private token!: TokenStringGetter;
......@@ -24,6 +25,7 @@ class Chat {
private ws = "";
private connectedActions: (() => void)[] = [];
private connected = false;
private messageController: ChatMessageController | null = null;
private userMapping: { [key: string]: { name: string; avatar: string } } =
{};
......@@ -59,7 +61,9 @@ class Chat {
};
}
this.setupIndexDb();
option.message && (this.messageController = option.message);
await this.setupIndexDb();
this.token = async () => option.sdk().global.jwtToken;
tokenManager.save(this.token);
......@@ -88,7 +92,7 @@ class Chat {
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) {
......@@ -140,7 +144,7 @@ class Chat {
if (xim.isConnected()) {
return Promise.resolve(uri);
}
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
xim.open(uri, this.token).finally(() => {
this.registerXimEvent();
if (xim.isConnected()) {
......@@ -172,7 +176,7 @@ class Chat {
}
private debug(message: string) {
ChatLoggerService.logger?.debug(message);
ChatLoggerService.logger && ChatLoggerService.logger.debug(message);
}
public $emit(event: string, ...args: any[]) {
......@@ -190,6 +194,14 @@ class Chat {
public getMatchedTextKeywords() {
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();
......@@ -7,6 +7,7 @@ import { Message, NotifyMessage } from "./models/chat";
import chat from "./index";
import { STATUS } from "xchat-client/dist/xchat";
import { AxiosInstance } from "axios";
wampDebug(true);
......@@ -37,6 +38,10 @@ const chatType = "group";
export class Xim {
private eventBus = new Vue();
private readonly messageNotifyActions: {
key: string;
action: (m: Message) => void;
}[] = [];
private client?: XChatClient;
......@@ -96,6 +101,10 @@ export class Xim {
return token.replace(/^Bearer\s/, "");
}
private buildError() {
return new Error(`Connect is not ready`);
}
/**
* token过期或者切换用户登录时,需要设置新的token
*/
......@@ -108,22 +117,22 @@ export class Xim {
}
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);
}
public fetchChatList() {
if (this.client == null) return Promise.reject();
if (this.client == null) return Promise.reject(this.buildError());
return this.client.fetchChatList({});
}
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 });
}
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);
}
......@@ -137,13 +146,13 @@ export class Xim {
msg: string
) {
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, "", {});
}
public inputing(chatId: number) {
this.checkConnected();
if (this.client == null) return Promise.reject();
if (this.client == null) return Promise.reject(this.buildError());
return this.client.userInput(chatType, chatId);
}
......@@ -152,7 +161,7 @@ export class Xim {
*/
public fetchChatMembers(chat_id: number) {
this.checkConnected();
if (this.client == null) return Promise.reject();
if (this.client == null) return Promise.reject(this.buildError());
return this.client.fetchChatMembers(chat_id);
}
......@@ -199,6 +208,55 @@ export class Xim {
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(
chatType: string,
......@@ -366,6 +424,17 @@ export class Xim {
private handleMsg(kind: any, msg: any) {
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) {
case "chat":
this.emit(`msg`, msg);
......@@ -384,7 +453,7 @@ export class Xim {
if (this.client == null) return;
if (!this.client.connected) {
try {
this.client?.open();
this.client && this.client.open();
} catch (e) {
// eslint-disable-next-line no-console
console.error("checkConnected", e);
......@@ -394,12 +463,20 @@ export class Xim {
}
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) {
this.on("msg", action);
vue.$once("hook:beforeDestroy", () => this.off("msg", action));
const key = `${new Date().valueOf()}-${Math.random()}`;
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