Commit 0f1e57c5 by Sixong.Zhu

merge from master

parents cfc2cd85 ae5dedf7
<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>
......@@ -111,8 +111,7 @@
private get currentChat() {
const chatId = this.chatId;
if (this.myChatList == null) return;
const result = this.myChatList.list.find((k) => k.chat_id === chatId);
const result = this.myChatList.find((k) => k.chat_id === chatId);
return result ?? {};
}
......
<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>
......@@ -3,7 +3,7 @@ import { parserMessage } from ".";
import { chatStore, ChatStore } from "@/customer-service/store/model";
import { formatTime, TimeFormatRule } from "@/customer-service/utils/time";
import { Chat as ChatItem } from "@/customer-service/xim/models/chat";
import Xim from "@/customer-service/xim";
import { getUserInfo } from "@/customer-service/utils/user-info";
@Component({ components: {} })
export default class ChatList extends Vue {
......@@ -49,23 +49,24 @@ export default class ChatList extends Vue {
@chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA)
protected readonly reset!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA;
private readonly invoker = Xim.getSdk();
@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;
@chatStore.Mutation(ChatStore.MUTATION_CLEAR_CURRENT_CHAT_ID)
protected readonly clearCurrentChatId!: ChatStore.MUTATION_CLEAR_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT)
protected readonly unread!: ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT;
protected parseMesage(data: ChatItem) {
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.invoker
.model("user")
.detail(data.last_msg_sender)
.query()
.then((userInfo: any) => {
this.updateUserName({
id: data.last_msg_sender,
name: userInfo.row.first_name.display as string,
});
})
.catch(() => {});
const id = data.last_msg_sender
this.updateUserName({ id, name: "" });
getUserInfo(id).then(d => this.updateUserName({ id, name: d.name }))
}
}
if (data.last_msg_content === "") {
......
import { MessageType } from "@/customer-service/model";
export function parserMessage(type: string, rawMsg: string) {
const mapping = new Map<MessageType, string>([
[MessageType.Image, '图片'],
[MessageType.Video, '视频'],
[MessageType.Voice, '语音'],
[MessageType.File, '文件'],
[MessageType.Withdraw, '撤回了一条消息'],
[MessageType.MyPurchasePlan, '我的采购计划'],
[MessageType.MyWelfare, '我的福利'],
[MessageType.QuestionAnswer, '问答'],
])
export function parserMessage(type: MessageType, rawMsg: string) {
if (!type) return "";
if (!rawMsg) return "";
if (type === MessageType.Text) {
......@@ -11,14 +22,12 @@ export function parserMessage(type: string, rawMsg: string) {
const msg = JSON.parse(rawMsg);
return msg.text;
}
if (type === MessageType.Image) {
return `[图片]`;
}
if (type === MessageType.File) {
return `[文件]`;
if (type === MessageType.Notify) {
return rawMsg;
}
if (type === MessageType.Withdraw) {
return `[撤回了一条消息]`;
const t = mapping.get(type)
if (t) {
return `[${t}]`;
}
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>
......@@ -32,7 +32,6 @@
display: inline-block;
white-space: pre-wrap;
text-align: left;
padding: 20px 30px;
color: #409eff;
/deep/ .highlight {
color: #e87005;
......
......@@ -2,7 +2,6 @@
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
......@@ -86,7 +85,6 @@
<style lang="less" scoped>
.voice-message {
height: 40px;
width: 200px;
&.can-play {
......
import { MessageType } from "@/customer-service/model";
const SVG_AUDIO = `<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595484227561" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6614" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M132.267 0h518.4l288 288v689.067A46.933 46.933 0 0 1 891.733 1024H132.267a46.933 46.933 0 0 1-46.934-46.933V46.933A46.933 46.933 0 0 1 132.267 0z" fill="#E54C94" p-id="6615"></path><path d="M607.744 434.39c-31.488 19.86-73.472-1.43-73.472-1.43s40.47 178.795 37.483 193.003c0 0 0-1.43 0 0 0 42.56-37.483 78.037-83.968 78.037-46.464 0-82.454-35.477-82.454-78.037 0-42.582 37.483-78.059 82.454-78.059 16.49 0 34.496 5.675 43.477 15.616l-38.976-143.317s-16.49-65.28 41.984-56.768c41.984 7.082 29.995 34.048 104.96 11.349 1.493-1.408 4.48 36.907-31.488 59.605zM650.667 0v241.067A46.933 46.933 0 0 0 697.6 288h241.067l-288-288z" fill="#FFFFFF" p-id="6616"></path></svg>`;
const SVG_EXCEL = `<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595484180583" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5396" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M132.267 0h518.4l288 288v689.067A46.933 46.933 0 0 1 891.733 1024H132.267a46.933 46.933 0 0 1-46.934-46.933V46.933A46.933 46.933 0 0 1 132.267 0z" fill="#3AC877" p-id="5397"></path><path d="M369.707 704L474.09 544.64l-94.592-146.048h72.085l61.248 98.133 60.01-98.133h71.467l-95.018 148.33L653.675 704h-74.39l-67.712-105.621L443.67 704zM650.667 0v241.067A46.933 46.933 0 0 0 697.6 288h241.067l-288-288z" fill="#FFFFFF" p-id="5398"></path></svg>`;
const SVG_IMAGE = `<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595484188280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5531" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M132.267 0h518.4l288 288v689.067A46.933 46.933 0 0 1 891.733 1024H132.267a46.933 46.933 0 0 1-46.934-46.933V46.933A46.933 46.933 0 0 1 132.267 0z" fill="#F6AD00" p-id="5532"></path><path d="M294.485 684.95l78.379-115.2a12.181 12.181 0 0 1 18.624-1.835l51.797 50.752 113.643-178.134a12.181 12.181 0 0 1 20.843 0.47l141.205 244.714A12.181 12.181 0 0 1 708.416 704h-403.84a12.181 12.181 0 0 1-10.09-19.05z" fill="#FFF7F7" p-id="5533"></path><path d="M443.307 423.616c0 32.512-29.014 60.95-62.187 60.95-33.152 0-62.165-28.438-62.165-60.95-2.07-32.512 26.944-60.95 62.165-60.95 33.152 0 62.165 28.438 62.165 60.95zM650.667 0v241.067A46.933 46.933 0 0 0 697.6 288h241.067l-288-288z" fill="#FFFFFF" p-id="5534"></path></svg>`;
......@@ -189,6 +191,22 @@ export function isImage(name: string) {
return name && FILE_EXTENSION_IMAGE.some((i) => name.endsWith(i));
}
export function getMessageType(name: string) {
if (name) {
if (isImage(name)) {
return MessageType.Image;
}
if (isAudio(name)) {
return MessageType.Voice;
}
if (isVideo(name)) {
return MessageType.Video;
}
return MessageType.File;
}
return MessageType.Text;
}
/**
* 最大图片文件大小
*/
......
......@@ -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>
......
......@@ -17,7 +17,10 @@ export default class BaseMessage extends Vue {
protected get messageBody(): { eid?: string; oid?: string; msg: any } {
if (this.value) {
try {
if (this.value.msg.startsWith("{")) {
return { ...this.value, msg: JSON.parse(this.value.msg) };
}
return { ...this.value, msg: this.value.msg };
} catch {
return {
...this.value,
......@@ -30,7 +33,7 @@ export default class BaseMessage extends Vue {
}
protected openFile() {
this.$emit("open", this.messageRealUrl);
this.$emit("open", this.messageRealUrl, this.messageBody.msg.name);
}
protected buildMessageUrl() {
......
<script lang="ts">
import { Component } from "vue-property-decorator";
import PurchasePlanMessage from "@/customer-service/components/message-item/purchase-plan-message.vue";
import Chat from "@/customer-service/xim/index";
@Component({ components: {} })
export default class Index extends PurchasePlanMessage {
protected openPlanUrl(e: any) {
if (Chat.isBackend()) {
return;
}
this.planTargetUrl = "/my-benefit-detail/";
window.open(
`${this.planTargetUrl}${e.target.attributes["plan-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
<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";
import Chat from "@/customer-service/xim/index";
@Component({ components: {} })
export default class Index extends BaseMessage {
protected readonly emptyText = " ";
protected planDom: HTMLElement | null = null;
protected planTargetUrl = "";
public mounted() {
this.planDom = this.$el?.querySelector(".plan-welfare-title");
this.listenPlan();
}
public beforeDestroy() {
this.planDom?.removeEventListener("click", this.openPlanUrl);
}
protected listenPlan() {
this.planDom?.addEventListener("click", this.openPlanUrl);
}
protected openPlanUrl(e: any) {
if (Chat.isBackend()) {
this.planTargetUrl =
"/福利宝.福利宝管理端.采购管理/detail/w_plan/key/";
} else {
this.planTargetUrl = "/my-plan-walfare-detail/";
}
window.open(
`${this.planTargetUrl}${e.target.attributes["plan-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) {
......
......@@ -11,7 +11,7 @@
</script>
<style lang="less" scoped>
.my-message {
.msg-content {
.withdraw-message {
background-color: transparent !important;
padding: 0 4px;
......
......@@ -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': !isMyMessage,
}"
>
<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,15 @@
v-model="data"
@open="openFile"
/>
<avatar v-if="avatar" :src="avatar" shape="circle" />
<avatar
v-if="!isQuestionAnswerMessage && !isWithdrawMessage"
:src="
chatRole === 'admin' || chatRole === 'customer-service'
? defaultAvatar
: avatar
"
shape="circle"
/>
</div>
</div>
......@@ -75,7 +76,7 @@
<span
class="withdraw"
v-if="isMyMessage && canWithdraw && !isWithdrawMessage"
v-if="isMyMessage && canWithdraw && !isWithdrawMessage && !isQuestionAnswerMessage"
@click="withdraw"
>撤回此消息</span
>
......@@ -135,7 +136,12 @@
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";
import { getUserMapping } from "../utils/user-info";
const twoMinutes = 2 * 60 * 1000;
......@@ -147,6 +153,9 @@
[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"],
]);
......@@ -160,6 +169,9 @@
VideoMessage,
TextMessage,
WithdrawMessage,
PurchasePlanMessage,
MyWelfareMessage,
QuestionAnswerMessage,
ActionMessage,
},
})
......@@ -206,7 +218,6 @@
private readerListOffset = false;
private defaultMessageHandledStatus = dto.MessageHandled.Default;
private showHostAvatar: boolean = false;
private get canWithdraw() {
if (this.backend && this.data) {
......@@ -219,6 +230,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;
}
......@@ -273,34 +288,37 @@
}
private get avatar() {
const avatar = chat.getUserMapping();
const mapping = getUserMapping();
if (this.isSendingMessage) {
if (Object.getOwnPropertyNames(avatar).length > 0) {
this.showHostAvatar = true;
} else {
this.showHostAvatar = false;
if (this.data) {
const value = mapping[this.data.eid];
if (value && value.icon) {
return value.icon;
}
if (avatar && this.chatMyId) {
const user = avatar[this.chatMyId];
if (user && user.avatar) {
return user.avatar;
}
return "";
}
private get defaultAvatar() {
return xim.getAvatar();
}
if (avatar && this.data) {
if (Object.getOwnPropertyNames(avatar).length > 0) {
this.showHostAvatar = true;
} else {
this.showHostAvatar = false;
private get chatRole() {
if (this.chatMembers) {
const t = this.chatMembers.find((i) => i.eid === this.data.eid);
if (t) {
if (t.type === ChatRole.Default) {
return "default";
}
if (t.type === ChatRole.Admin) {
return "admin";
}
if (t.type === ChatRole.CustomerService) {
return "customer-service";
}
const value = avatar[this.data.eid];
if (value && value.avatar) {
return value.avatar;
}
}
return "";
}
......@@ -450,7 +468,6 @@
word-break: break-all;
}
}
&.offset-bottom {
margin-bottom: 30px;
}
......@@ -478,6 +495,9 @@
&.algin-left {
text-align: left;
}
.cs-flex-direction {
flex-direction: row-reverse;
}
}
.msg-detail {
......
<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>
......@@ -6,13 +6,7 @@
class="who-read-list pos-rel"
>
<template>
<div
class="d-flex tabs"
:class="{
'justify-content-center':
!readlist.length || !unreadlist.length,
}"
>
<div class="d-flex tabs justify-content-center">
<div
class="tab text-nowrap"
:class="{ selected: tab === 1 }"
......@@ -63,8 +57,8 @@
import { unique } from "../utils";
import avatar from "@/customer-service/components/avatar.vue";
import { ChatStore } from "@/customer-service/store/model";
import chat from "@/customer-service/xim/index";
import xim from "@/customer-service/xim/xim";
import { getUserInfo } from "../utils/user-info";
const chatStoreNamespace = namespace("chatStore");
......@@ -116,8 +110,8 @@
}
private async getUserNameByid(eid: string) {
const data = await chat.getSdk().model("user").detail(eid).query();
return data.row.first_name.value as string;
const data = await getUserInfo(eid);
return data.name;
}
private async getReader() {
......@@ -177,7 +171,7 @@
color: #000;
z-index: 2;
margin-left: -100px;
width: 125px;
width: 200px;
&.offset {
margin-left: 0;
......
<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>
......@@ -13,9 +13,28 @@ class ChatCacheDatabaseController {
private readonly chatListKey = "chat-list";
private readonly chatMessageKey = "chat-message";
private setuping = false;
private waitSetupCompleted() {
return new Promise<void>((resolve) => {
const checker = () => {
if (!this.setuping) {
resolve();
} else {
setTimeout(() => checker(), 200);
}
};
checker();
});
}
public setup(uid: string) {
if (this.setuping) {
return this.waitSetupCompleted();
}
return new Promise<void>((resolve) => {
if (uid && indexedDB) {
this.setuping = true;
const r = indexedDB.open(
"u-" + (this.uid = uid),
this.listVersion
......@@ -23,6 +42,7 @@ class ChatCacheDatabaseController {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
const setupDb = () => {
if (this.setuping) {
if (that.db) {
try {
that.buildTables(that.db, that.chatListKey);
......@@ -33,16 +53,18 @@ class ChatCacheDatabaseController {
console.error(e);
}
}
this.setuping = false;
}
resolve();
};
r.onsuccess = function (e) {
that.db = (e.target as any).result;
console.log(`index database init comepleted, 33%`);
console.log(`index database init comepleted`);
setupDb();
};
r.onupgradeneeded = function (e) {
that.db = (e.target as any).result;
console.log(`index database init comepleted, 66%`);
console.log(`upgrade database comepleted`);
setupDb();
};
r.onerror = function (e) {
......@@ -136,10 +158,30 @@ class ChatCacheDatabaseController {
p.eid && (chat.last_msg_sender = p.eid);
p.type && (chat.last_msg_type = p.type);
p.msg && (chat.last_msg_content = p.msg);
p.unread && chat.unread_msg_count++;
if (p.unread === 0) {
chat.unread_msg_count = 0;
p.set2Read && (chat.unread_msg_count = 0);
const u = store.put(chat, chat.id);
u.onsuccess = () => resolve();
u.onerror = () => resolve();
} else {
resolve();
}
};
t.onerror = () => resolve();
} else {
resolve();
}
});
}
public setRead(chat: number) {
return new Promise<void>((resolve) => {
if (this.db) {
const store = this.buildStore(this.chatListKey);
const t = store.get(chat);
t.onsuccess = (r) => {
const chat = (r.target as any).result as Chat;
if (chat) {
chat.unread_msg_count = 0;
const u = store.put(chat, chat.id);
u.onsuccess = () => resolve();
u.onerror = () => resolve();
......
......@@ -22,7 +22,6 @@
for="chat-upload-file"
:title="tip4File"
@click="allowLoadFile"
v-if="enableFileSelection"
>
<img
class="tool-bar-icon"
......@@ -36,7 +35,6 @@
id="chat-upload-file"
type="file"
:accept="acceptType"
multiple
/>
</div>
......@@ -121,6 +119,16 @@
);
}
const limitedFileExtension = [
"ppt",
"pptx",
"doc",
"docx",
"xls",
"xlsx",
"pdf",
];
@Component({ components: {} })
export default class Input extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
......@@ -139,8 +147,6 @@
private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})`;
private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})`;
private enableFileSelection = false;
private emoji: EmojiItem[] = [];
@Watch("chatId")
......@@ -201,7 +207,8 @@
}
private allowLoadFile() {
this.acceptType = "*";
this.acceptType =
limitedFileExtension.map((i) => `.${i}`).join(",") + ",image/*";
}
private async handlePasteEvent(event: ClipboardEvent) {
......@@ -221,19 +228,22 @@
const file = items[i].getAsFile();
if (file) {
if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY);
return;
return this.$emit("error", MESSAGE_FILE_EMPTY);
}
if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
return;
return this.$emit(
"error",
MESSAGE_IMAGE_TOO_LARGE
);
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
return this.$emit(
"error",
MESSAGE_FILE_TOO_LARGE
);
}
html += this.buildFileHtml(file);
}
......@@ -497,30 +507,23 @@
for (let index = 0; index < files.length; index++) {
const file = files[index];
if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY);
return;
return this.$emit("error", MESSAGE_FILE_EMPTY);
}
if (
this.enableFileSelection &&
file.size >= MAX_FILE_SIZE
) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
if (file.size >= MAX_FILE_SIZE) {
return this.$emit("error", MESSAGE_FILE_TOO_LARGE);
}
if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
return;
return this.$emit(
"error",
MESSAGE_IMAGE_TOO_LARGE
);
}
html += this.buildImageHtml(file);
} else {
if (this.enableFileSelection) {
html += this.buildFileHtml(file);
} else {
return this.$emit("error", ERROR_IMAGE);
}
}
}
......@@ -619,6 +622,11 @@
.file-size {
margin-top: 10px;
}
svg {
max-width: 70px;
max-height: 50px;
}
}
}
}
......
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 59 (86127) - https://sketch.com -->
<title>-mockplus-</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#57A4F1" offset="0%"></stop>
<stop stop-color="#2893F7" offset="100%"></stop>
</linearGradient>
</defs>
<g id="服务中心" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="客服中心-工单咨询" transform="translate(-294.000000, -242.000000)">
<g id="middle" transform="translate(264.000000, 142.000000)">
<g id="右" transform="translate(30.000000, 80.000000)">
<g id="-mockplus-">
<g id="other/table/content-copy-3">
<g id="Group-2" transform="translate(0.000000, 16.000000)">
<g id="-mockplus-" transform="translate(0.000000, 4.000000)">
<g id="icon/36/业务">
<g id="icon/news">
<rect id="Rectangle" fill="url(#linearGradient-1)" x="0" y="0" width="40" height="40" rx="4"></rect>
<g id="-mockplus-" transform="translate(8.000000, 8.000000)" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round">
<g id="icon/40/business/sb" transform="translate(0.000000, 1.000000)">
<path d="M0.75,3.5 L23.25,3.5 L23.25,21.5 L0.75,21.5 L0.75,3.5 Z M4.125,3.5001125 L7.875375,0.5001125 L16.124625,0.5001125 L19.875,3.5001125 M15,10.625 L18.75,10.625 M15,14.375 L18.75,14.375" id="Combined-Shape" stroke-width="1.5"></path>
<path d="M7.875,8 C8.70342712,8 9.375,8.67157288 9.375,9.5 L9.375,10.25 C9.375,11.0784271 8.70342712,11.75 7.875,11.75 C7.04657288,11.75 6.375,11.0784271 6.375,10.25 L6.375,9.5 C6.375,8.67157288 7.04657288,8 7.875,8 Z M4.5,14.375 C5.85835351,13.875 6.98335351,13.625 7.875,13.625 C8.76664649,13.625 9.89164649,13.875 11.25,14.375 L11.25,17 L4.5,17 L4.5,14.375 Z" id="Rectangle-2" stroke-width="0.75" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
......@@ -44,13 +44,25 @@ export const enum ServiceType {
Backend,
}
export const enum ImEnvironment {
Dev = 1,
Stage,
Pro
}
export const socketMapping = new Map<ImEnvironment, string>([
[ImEnvironment.Dev, 'ws://hro.channel.jinsehuaqin.com:8080/ws'],
[ImEnvironment.Stage, 'wss://pre-channel.qinqinxiaobao.com/ws'],
[ImEnvironment.Pro, 'wss://channel.qinqinxiaobao.com/ws'],
]);
export type TokenStringGetter = () => Promise<string>;
export interface ChatOption {
/**
* 长链接chat sdk地址
* IM 链接,如果使用系统配置,只需要传入环境标识即可,如果默认配置不满足,可以传入指定地址
*/
webSocketUri: string;
connection: ImEnvironment | string;
sdk: () => UniplatSdk;
......@@ -65,10 +77,14 @@ export interface ChatOption {
eventHub?: Vue;
/**
* 用户信息(头像,别名)可选
*/
user?: { icon?: string; username?: string };
message?: ChatMessageController;
avatar?: string;
}
export interface ChatMessageController {
error: (msg: string) => void;
info: (msg: string) => void;
}
export interface ChatServiceLogger {
......@@ -91,7 +107,12 @@ export const enum MessageType {
Voice = "voice",
GeneralOrderMsg = "general_order_msg",
Withdraw = "withdraw",
MyPurchasePlan = "my_purchase_plan",
MyWelfare = "my_welfare",
QuestionAnswer = "question_answer",
Action = "action",
Notify = 'notify',
MpNavigate = "mp-navigate",
}
export const enum MessageHandled {
......
......@@ -11,11 +11,11 @@ import {
import { isAccessibleUrl } from "../service/tools";
import { unique } from "../utils";
import { getChatModelInfo } from "../utils/chat-info";
import { decode } from "../utils/jwt";
import { getUserInfo } from "../utils/user-info";
import Chat from "../xim";
import { Chat as ChatType, Message } from "../xim/models/chat";
import xim, { ChatNotifyListener } from "../xim/xim";
import { decodeJwt } from "uniplat-sdk";
import { ChatStatus, ChatStore, ChatStoreState } from "./model";
......@@ -93,24 +93,29 @@ function buildChatItem(chat: RawChatItem) {
const chatType = "group";
const filterActiveChats = (items: RawChatItem[]) => {
const allowedChatTypes = [chatType, "notify"];
export const filterActiveChats = (items: RawChatItem[]) => {
return items.filter(
(i) =>
!i.is_finish &&
!i.is_exited &&
!i.is_remove &&
!i.is_deleted &&
i.type === chatType
allowedChatTypes.includes(i.type)
);
};
export function getLastMessageId(msgs: Message[] | any) {
if (msgs && msgs.length) {
const last = msgs[msgs.length - 1];
let id = last.id;
if (id < 0) {
id = Math.max(...msgs.map((i: any) => i.id));
}
return id;
}
return 0;
}
export default {
......@@ -124,7 +129,7 @@ export default {
[ChatStore.STATE_CHAT_CURRENT_USER_UID]: null,
[ChatStore.STATE_CHAT_MSG_HISTORY]: null,
[ChatStore.STATE_CHAT_SENDING_MESSAGES]: [],
[ChatStore.STATE_MY_CHAT_ROOM_LIST]: null,
[ChatStore.STATE_MY_CHAT_ROOM_LIST]: [],
[ChatStore.STATE_SINGLE_CHAT]: null,
[ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION]: null,
[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]: null,
......@@ -201,7 +206,7 @@ export default {
},
[ChatStore.MUTATION_SAVE_MYSELF_ID](state) {
Chat.getToken().then((token) => {
const eid = decode(token);
const eid = decodeJwt<{ user_id: string; sub: string }>(token);
state[ChatStore.STATE_CHAT_MY_ID] = String(eid.user_id);
state[ChatStore.STATE_CHAT_MY_UID] = eid.sub;
});
......@@ -386,6 +391,8 @@ export default {
async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit, state }) {
let cache = await dbController.getChatList();
cache.sort((x, y) => (x.last_msg_ts < y.last_msg_ts ? 1 : -1));
for (const item of cache) {
if (item.business_data && !item.detail_name) {
const d = JSON.parse(
......@@ -415,10 +422,7 @@ export default {
};
if (cache && cache.length) {
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, {
list: cache,
total: 9999,
});
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, cache);
const ts = cache
.map((i) => Math.max(i.last_msg_ts, i.update_time))
.sort();
......@@ -459,10 +463,7 @@ export default {
buildChatItem(chat)
);
dbController.saveChatList(items);
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, {
list: items,
total: 9999,
});
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, items);
resolve(buildUnreadMessage(items));
});
});
......@@ -606,7 +607,7 @@ export default {
const { imChatId } = await Chat.getSdk()
.model(params.modelName)
.chat(params.selectedListId, orgId())
.createChat(true);
.createChat(true, params.title);
const chatId = +imChatId;
await commit(ChatStore.MUTATION_SHOW_CHAT, true);
await dispatch(
......@@ -676,8 +677,9 @@ export default {
if (!chatId) {
return;
}
const chatList =
state[ChatStore.STATE_MY_CHAT_ROOM_LIST]?.list ?? [];
const chatList = state[
ChatStore.STATE_MY_CHAT_ROOM_LIST
] as ChatType[];
let wantedChatRoom = chatList.find((k) => k.chat_id === chatId);
if (!wantedChatRoom) {
......@@ -801,7 +803,7 @@ export default {
})
.execute();
await dispatch(ChatStore.ACTION_GET_MY_CHAT_LIST);
const firstChat = state[ChatStore.STATE_MY_CHAT_ROOM_LIST]?.list[0];
const firstChat = state[ChatStore.STATE_MY_CHAT_ROOM_LIST][0];
if (firstChat == null) return;
await getChatModelInfo(
firstChat.model_name,
......@@ -905,7 +907,7 @@ export default {
}
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.chat(currentChat.obj_id, currentChat.org_id)
.removeMember(uids.map((id) => +id))
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
......@@ -974,6 +976,25 @@ export default {
[ChatStore.ACTION_SET_CHAT_ERROR]: ({ state }, chat: number) => {
(<any>state)[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR] = chat;
},
[ChatStore.ACTION_GET_USERINFO]: ({ state }, id: string) => {
const cache = state[ChatStore.STATE_CHAT_USERNAME] || {};
if (cache[id]) {
return Promise.resolve({ id, name: cache[id] });
}
return new Promise<{ id: string; name: string }>(
(resolve, reject) =>
getUserInfo(id)
.then((d) => {
Vue.set(
state[ChatStore.STATE_CHAT_USERNAME],
id,
d.name
);
resolve({ id, name: d.name });
})
.catch(reject)
);
},
},
getters: {
[ChatStore.STATE_CHAT_MSG_HISTORY](state) {
......@@ -1002,8 +1023,7 @@ export default {
if (singleChat && singleChat.chat_id === chatId) {
return singleChat;
}
const chatList =
state[ChatStore.STATE_MY_CHAT_ROOM_LIST]?.list ?? [];
const chatList = state[ChatStore.STATE_MY_CHAT_ROOM_LIST];
return chatList.find((chat) => chat.chat_id === chatId);
},
},
......
......@@ -3,6 +3,7 @@ import { namespace } from "vuex-class";
import * as dto from "../model";
import { Chat as ChatType } from "../xim/models/chat";
import * as chatDto from "../xim/models/chat";
export enum ChatStatus {
opening = 0,
terminated = 1,
......@@ -19,10 +20,7 @@ export namespace ChatStore {
export const STATE_CHAT_DIALOG_IS_SINGLE = "会话模块是否是单个弹窗";
export type STATE_CHAT_DIALOG_IS_SINGLE = boolean;
export const STATE_MY_CHAT_ROOM_LIST = "我的会话列表";
export type STATE_MY_CHAT_ROOM_LIST = {
list: ChatType[];
total: number;
} | null;
export type STATE_MY_CHAT_ROOM_LIST = ChatType[];
export const STATE_SINGLE_CHAT = "单独的会话";
export type STATE_SINGLE_CHAT = ChatType | null;
export const STATE_CHAT_MSG_HISTORY = "某个会话聊天记录";
......@@ -288,6 +286,7 @@ export namespace ChatStore {
modelName: string;
selectedListId: string;
uids: string[];
title?: string;
}) => Promise<number>;
export const ACTION_CREATE_NEW_CHAT_BY_CLIENT_SIDE =
......@@ -320,9 +319,13 @@ export namespace ChatStore {
export const ACTION_TERINATE_CHAT = "结束会话";
export type ACTION_TERINATE_CHAT = () => Promise<void>;
export const ACTION_CHAT_ADD_MEMBERS = "添加成员";
export type ACTION_CHAT_ADD_MEMBERS = (uids: (string | number)[]) => Promise<void>;
export type ACTION_CHAT_ADD_MEMBERS = (
uids: (string | number)[]
) => Promise<void>;
export const ACTION_CHAT_REMOVE_MEMBER = "移除成员";
export type ACTION_CHAT_REMOVE_MEMBER = (uids: (string | number)[]) => Promise<void>;
export type ACTION_CHAT_REMOVE_MEMBER = (
uids: (string | number)[]
) => Promise<void>;
export const ACTION_CHAT_ADD_CS = "添加客服";
export type ACTION_CHAT_ADD_CS = (uids: string[]) => Promise<void>;
......@@ -350,13 +353,18 @@ export namespace ChatStore {
export const ACTION_SET_CHAT_ERROR = "标记会话关键性错误";
export type ACTION_SET_CHAT_ERROR = (chat: number) => void;
export const ACTION_GET_USERINFO = "获取用户个人信息";
export type ACTION_GET_USERINFO = (
id: string
) => Promise<{ id: string; name: string }>;
export interface ChatUpdateParameter {
chat: number;
type?: dto.MessageType;
msg?: string;
ts?: number;
eid?: string;
unread?: number;
set2Read?: boolean;
}
export const ACTION_UPDATE_CHAT = "更新会话信息";
......
function b64DecodeUnicode(str: string) {
return decodeURIComponent(
atob(str).replace(/(.)/g, function (m, p) {
let code = p.charCodeAt(0).toString(16).toUpperCase();
if (code.length < 2) {
code = "0" + code;
}
return "%" + code;
})
);
}
function base64_url_decode(str: string) {
let output = str.replace(/-/g, "+").replace(/_/g, "/");
switch (output.length % 4) {
case 0:
break;
case 2:
output += "==";
break;
case 3:
output += "=";
break;
default:
throw new Error("Illegal base64url string!");
}
try {
return b64DecodeUnicode(output);
} catch {
return atob(output);
}
}
export function decode(token: string) {
return JSON.parse(base64_url_decode(token.split(".")[1]));
}
import { UniplatSdk } from "uniplat-sdk";
import Chat from "../xim";
export type UserMapping = {
[eid: string]: {
name: string;
phone: string;
icon: string;
};
};
const userMapping: UserMapping = {};
export const getUserMapping = () => userMapping;
export async function getUserInfo(eid: string) {
export async function getUserInfo(eid: string, sdk?: UniplatSdk) {
if (userMapping[eid]) {
return userMapping[eid];
}
const info = await Chat.getSdk().model("user").detail(eid).query();
if (!+eid) {
return { name: '', phone: '', icon: '' };
}
const info = await (sdk || Chat.getSdk())
.domainService('passport', 'anonymous', `oidc.account/user_info?id=${eid}`)
.request<any, any, {
avatar_url: string;
email: string;
id: string;
mobile: string;
realname: string;
uniplatId: string;
username: string;
}>('get');
const data = {
name: info.row.first_name.value as string,
phone: info.row.last_name.value as string,
name: info.username || info.realname || info.mobile || info.mobile,
phone: info.mobile,
icon: info.avatar_url
};
userMapping[eid] = data;
return data;
......
......@@ -7,6 +7,9 @@ import {
TokenStringGetter,
ServiceType,
CustomerServiceProduct,
ChatMessageController,
socketMapping,
ImEnvironment,
} from "./../model";
import { ChatLoggerService } from "./logger";
import tokenManager from "./token";
......@@ -24,9 +27,8 @@ class Chat {
private ws = "";
private connectedActions: (() => void)[] = [];
private connected = false;
private userMapping: { [key: string]: { name: string; avatar: string } } =
{};
private messageController: ChatMessageController | null = null;
private defaultAvatar = "";
public onReady(action: () => void) {
if (this.connected) {
......@@ -41,7 +43,7 @@ class Chat {
throw new Error(`You must specify a chat option for chat service`);
}
if (!option.webSocketUri) {
if (!option.connection) {
throw new Error(
`You must specify a web socket address for chat service`
);
......@@ -52,14 +54,12 @@ class Chat {
(this.serviceType = option.serviceType);
option.product && (this.product = option.product);
this.eventHub = option.eventHub || null;
if (option.user) {
this.userMapping[this._sdk().global.uid] = {
name: option.user.username || "",
avatar: option.user.icon || "",
};
}
this.setupIndexDb();
option.message && (this.messageController = option.message);
option.avatar !== undefined &&
(this.defaultAvatar = option.avatar);
await this.setupIndexDb();
this.token = async () => option.sdk().global.jwtToken;
tokenManager.save(this.token);
......@@ -73,7 +73,9 @@ class Chat {
// this.keywords = ["社保"];
return this.initChatSdk((this.ws = option.webSocketUri)).finally(() => {
const path = socketMapping.get(option.connection as ImEnvironment) as string || option.connection as string;
return this.initChatSdk((this.ws = path)).finally(() => {
this.connected = true;
for (const item of this.connectedActions) {
item();
......@@ -167,10 +169,6 @@ class Chat {
this.debug(`client status ${e}`);
}
public getUserMapping() {
return this.userMapping;
}
private debug(message: string) {
ChatLoggerService.logger?.debug(message);
}
......@@ -190,6 +188,18 @@ 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);
}
public getAvatar() {
return this.defaultAvatar;
}
}
export default new Chat();
......@@ -22,7 +22,7 @@ export interface Chat {
biz_id: string;
last_msg_sender: string;
last_msg_content: string;
last_msg_type: string;
last_msg_type: MessageType;
model_name: string;
obj_id: string;
is_finish: boolean;
......@@ -47,6 +47,7 @@ export interface Chat {
chat_id: number;
catalog: string;
biz_type_id: number;
biz_type_code: string;
business_data?: string;
detail_name?: string;
}
......
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