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 @@ ...@@ -111,8 +111,7 @@
private get currentChat() { private get currentChat() {
const chatId = this.chatId; const chatId = this.chatId;
if (this.myChatList == null) return; const result = this.myChatList.find((k) => k.chat_id === chatId);
const result = this.myChatList.list.find((k) => k.chat_id === chatId);
return result ?? {}; 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 "."; ...@@ -3,7 +3,7 @@ import { parserMessage } from ".";
import { chatStore, ChatStore } from "@/customer-service/store/model"; import { chatStore, ChatStore } from "@/customer-service/store/model";
import { formatTime, TimeFormatRule } from "@/customer-service/utils/time"; import { formatTime, TimeFormatRule } from "@/customer-service/utils/time";
import { Chat as ChatItem } from "@/customer-service/xim/models/chat"; 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: {} }) @Component({ components: {} })
export default class ChatList extends Vue { export default class ChatList extends Vue {
...@@ -49,23 +49,24 @@ export default class ChatList extends Vue { ...@@ -49,23 +49,24 @@ export default class ChatList extends Vue {
@chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA) @chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA)
protected readonly reset!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA; protected readonly reset!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA;
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) { protected parseMesage(data: ChatItem) {
if (data.last_msg_sender && data.last_msg_sender !== "0") { if (data.last_msg_sender && data.last_msg_sender !== "0") {
if (this.userNames[data.last_msg_sender] === undefined) { if (this.userNames[data.last_msg_sender] === undefined) {
this.updateUserName({ id: data.last_msg_sender, name: "" }); const id = data.last_msg_sender
this.invoker this.updateUserName({ id, name: "" });
.model("user") getUserInfo(id).then(d => this.updateUserName({ id, name: d.name }))
.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(() => {});
} }
} }
if (data.last_msg_content === "") { if (data.last_msg_content === "") {
......
import { MessageType } from "@/customer-service/model"; 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 (!type) return "";
if (!rawMsg) return ""; if (!rawMsg) return "";
if (type === MessageType.Text) { if (type === MessageType.Text) {
...@@ -11,14 +22,12 @@ export function parserMessage(type: string, rawMsg: string) { ...@@ -11,14 +22,12 @@ export function parserMessage(type: string, rawMsg: string) {
const msg = JSON.parse(rawMsg); const msg = JSON.parse(rawMsg);
return msg.text; return msg.text;
} }
if (type === MessageType.Image) { if (type === MessageType.Notify) {
return `[图片]`; return rawMsg;
}
if (type === MessageType.File) {
return `[文件]`;
} }
if (type === MessageType.Withdraw) { const t = mapping.get(type)
return `[撤回了一条消息]`; if (t) {
return `[${t}]`;
} }
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>
...@@ -32,7 +32,6 @@ ...@@ -32,7 +32,6 @@
display: inline-block; display: inline-block;
white-space: pre-wrap; white-space: pre-wrap;
text-align: left; text-align: left;
padding: 20px 30px;
color: #409eff; color: #409eff;
/deep/ .highlight { /deep/ .highlight {
color: #e87005; color: #e87005;
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
<div <div
class="msg-detail voice-message d-flex align-items-center" class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }" :class="{ playing: playing, 'can-play': messageRealUrl }"
v-if="messageType === 'voice'"
@click.stop="play" @click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }" :style="{ width: getVoiceMessageWidth + 'px' }"
> >
...@@ -86,7 +85,6 @@ ...@@ -86,7 +85,6 @@
<style lang="less" scoped> <style lang="less" scoped>
.voice-message { .voice-message {
height: 40px;
width: 200px; width: 200px;
&.can-play { &.can-play {
...@@ -98,14 +96,14 @@ ...@@ -98,14 +96,14 @@
} }
} }
.my-message { .my-message {
.voice-message { .voice-message {
> div { > div {
flex-flow: row-reverse; flex-flow: row-reverse;
}
svg {
transform: rotateY(180deg);
}
} }
svg {
transform: rotateY(180deg);
}
}
} }
</style> </style>
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_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_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>`; 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) { ...@@ -189,6 +191,22 @@ export function isImage(name: string) {
return name && FILE_EXTENSION_IMAGE.some((i) => name.endsWith(i)); 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 @@ ...@@ -65,14 +65,14 @@
@Component({ components: { FileIcon } }) @Component({ components: { FileIcon } })
export default class Index extends BaseMessage { export default class Index extends BaseMessage {
private get getAttachment() { protected get getAttachment() {
if (this.messageBody) { if (this.messageBody) {
return this.messageBody.msg.name; return this.messageBody.msg.name;
} }
return "文件下载"; return "文件下载";
} }
private get fileIcon() { protected get fileIcon() {
if (this.value) { if (this.value) {
return getFileType(this.messageBody.msg.name); return getFileType(this.messageBody.msg.name);
} }
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
return FileType.Others; return FileType.Others;
} }
private format(v: number) { protected format(v: number) {
return formatSize(v); return formatSize(v);
} }
...@@ -97,6 +97,7 @@ ...@@ -97,6 +97,7 @@
border: 1px solid #c5d4e5; border: 1px solid #c5d4e5;
.file-message-name { .file-message-name {
max-width: 130px; max-width: 130px;
word-break: break-all;
} }
} }
</style> </style>
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<div <div
class="msg-detail image-message" class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }" :class="{ 'image-404': fileFailed2Load }"
@dblclick="openFile" @dblclick="dbClick"
@click="open"
> >
<img <img
v-if="messageRealUrl" v-if="messageRealUrl"
...@@ -20,11 +21,16 @@ ...@@ -20,11 +21,16 @@
import { FileType } from "./file-controller"; import { FileType } from "./file-controller";
import BaseMessage from "./index"; import BaseMessage from "./index";
import FileIcon from "./file-icon.vue"; import FileIcon from "./file-icon.vue";
import { UserAgentHelper } from "@/customer-service/third-party/user-agent";
@Component({ components: { FileIcon } }) @Component({ components: { FileIcon } })
export default class Index extends BaseMessage { export default class Index extends BaseMessage {
private readonly image404 = FileType.Image_404; private readonly image404 = FileType.Image_404;
private readonly mobile = UserAgentHelper.isMobile(
window.navigator.userAgent
);
private onImageError() { private onImageError() {
this.fileFailed2Load = true; this.fileFailed2Load = true;
this.messageRealUrl = ""; this.messageRealUrl = "";
...@@ -33,6 +39,14 @@ ...@@ -33,6 +39,14 @@
mounted() { mounted() {
this.buildMessageUrl(); this.buildMessageUrl();
} }
private dbClick() {
!this.mobile && this.openFile();
}
private open() {
this.mobile && this.openFile();
}
} }
</script> </script>
......
...@@ -17,7 +17,10 @@ export default class BaseMessage extends Vue { ...@@ -17,7 +17,10 @@ export default class BaseMessage extends Vue {
protected get messageBody(): { eid?: string; oid?: string; msg: any } { protected get messageBody(): { eid?: string; oid?: string; msg: any } {
if (this.value) { if (this.value) {
try { try {
return { ...this.value, msg: JSON.parse(this.value.msg) }; if (this.value.msg.startsWith("{")) {
return { ...this.value, msg: JSON.parse(this.value.msg) };
}
return { ...this.value, msg: this.value.msg };
} catch { } catch {
return { return {
...this.value, ...this.value,
...@@ -30,7 +33,7 @@ export default class BaseMessage extends Vue { ...@@ -30,7 +33,7 @@ export default class BaseMessage extends Vue {
} }
protected openFile() { protected openFile() {
this.$emit("open", this.messageRealUrl); this.$emit("open", this.messageRealUrl, this.messageBody.msg.name);
} }
protected buildMessageUrl() { 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 @@ ...@@ -13,9 +13,9 @@
@Component({ components: {} }) @Component({ components: {} })
export default class Index extends BaseMessage { export default class Index extends BaseMessage {
private readonly emptyText = " "; protected readonly emptyText = " ";
private format2Link(text: string) { protected format2Link(text: string) {
let t = replaceText2Link(text); let t = replaceText2Link(text);
const keywords = xim.getMatchedTextKeywords(); const keywords = xim.getMatchedTextKeywords();
for (const item of keywords) { for (const item of keywords) {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.my-message { .msg-content {
.withdraw-message { .withdraw-message {
background-color: transparent !important; background-color: transparent !important;
padding: 0 4px; padding: 0 4px;
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<div <div
class="message-con d-flex align-items-center" class="message-con d-flex align-items-center"
:class="{ :class="{
'my-message flex-row-reverse': isMyMessage, 'my-message flex-row-reverse':
isMyMessage && !isQuestionAnswerMessage,
'justify-content-center': isWithdrawMessage, 'justify-content-center': isWithdrawMessage,
'offset-bottom': matchKeywords, 'offset-bottom': matchKeywords,
}" }"
...@@ -13,23 +14,15 @@ ...@@ -13,23 +14,15 @@
:class="{ 'algin-left': !isMyMessage }" :class="{ 'algin-left': !isMyMessage }"
v-if="!isWithdrawMessage" v-if="!isWithdrawMessage"
> >
{{ userName }} {{ isQuestionAnswerMessage ? "" : userName }}
</div> </div>
<div <div
class="content-avatar d-flex align-items-start" class="content-avatar d-flex align-items-start"
:class="{ 'justify-content-end': isMyMessage }" :class="{
'justify-content-end': isMyMessage,
'cs-flex-direction': !isMyMessage,
}"
> >
<img
src="../imgs/default-host-avatar.svg"
style="width: 42px"
v-if="
!avatar &&
messageComponent &&
showHostAvatar &&
!isWithdrawMessage
"
class="host-avatar"
/>
<component <component
:is="messageComponent" :is="messageComponent"
:user-name="userName" :user-name="userName"
...@@ -40,7 +33,15 @@ ...@@ -40,7 +33,15 @@
v-model="data" v-model="data"
@open="openFile" @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>
</div> </div>
...@@ -75,7 +76,7 @@ ...@@ -75,7 +76,7 @@
<span <span
class="withdraw" class="withdraw"
v-if="isMyMessage && canWithdraw && !isWithdrawMessage" v-if="isMyMessage && canWithdraw && !isWithdrawMessage && !isQuestionAnswerMessage"
@click="withdraw" @click="withdraw"
>撤回此消息</span >撤回此消息</span
> >
...@@ -135,7 +136,12 @@ ...@@ -135,7 +136,12 @@
import TextMessage from "./message-item/text-message.vue"; import TextMessage from "./message-item/text-message.vue";
import ActionMessage from "./message-item/action-message.vue"; import ActionMessage from "./message-item/action-message.vue";
import WithdrawMessage from "./message-item/withdraw-message.vue"; import WithdrawMessage from "./message-item/withdraw-message.vue";
import PurchasePlanMessage from "./message-item/purchase-plan-message.vue";
import MyWelfareMessage from "./message-item/my-welfare-message.vue";
import QuestionAnswerMessage from "./message-item/question-answer-message.vue";
import xim from "./../xim"; import xim from "./../xim";
import { ChatRole } from "@/customer-service/model";
import { getUserMapping } from "../utils/user-info";
const twoMinutes = 2 * 60 * 1000; const twoMinutes = 2 * 60 * 1000;
...@@ -147,6 +153,9 @@ ...@@ -147,6 +153,9 @@
[dto.MessageType.Text, "text-message"], [dto.MessageType.Text, "text-message"],
[dto.MessageType.Withdraw, "withdraw-message"], [dto.MessageType.Withdraw, "withdraw-message"],
[dto.MessageType.GeneralOrderMsg, "text-message"], [dto.MessageType.GeneralOrderMsg, "text-message"],
[dto.MessageType.MyPurchasePlan, "purchase-plan-message"],
[dto.MessageType.MyWelfare, "my-welfare-message"],
[dto.MessageType.QuestionAnswer, "question-answer-message"],
[dto.MessageType.Action, "action-message"], [dto.MessageType.Action, "action-message"],
]); ]);
...@@ -160,6 +169,9 @@ ...@@ -160,6 +169,9 @@
VideoMessage, VideoMessage,
TextMessage, TextMessage,
WithdrawMessage, WithdrawMessage,
PurchasePlanMessage,
MyWelfareMessage,
QuestionAnswerMessage,
ActionMessage, ActionMessage,
}, },
}) })
...@@ -206,7 +218,6 @@ ...@@ -206,7 +218,6 @@
private readerListOffset = false; private readerListOffset = false;
private defaultMessageHandledStatus = dto.MessageHandled.Default; private defaultMessageHandledStatus = dto.MessageHandled.Default;
private showHostAvatar: boolean = false;
private get canWithdraw() { private get canWithdraw() {
if (this.backend && this.data) { if (this.backend && this.data) {
...@@ -219,6 +230,10 @@ ...@@ -219,6 +230,10 @@
return this.data.type === dto.MessageType.Withdraw; return this.data.type === dto.MessageType.Withdraw;
} }
private get isQuestionAnswerMessage() {
return this.data.type === dto.MessageType.QuestionAnswer;
}
private get isAllRead() { private get isAllRead() {
return this.data.read_count >= this.data.total_read_count; return this.data.read_count >= this.data.total_read_count;
} }
...@@ -273,34 +288,37 @@ ...@@ -273,34 +288,37 @@
} }
private get avatar() { private get avatar() {
const avatar = chat.getUserMapping(); const mapping = getUserMapping();
if (this.isSendingMessage) { if (this.data) {
if (Object.getOwnPropertyNames(avatar).length > 0) { const value = mapping[this.data.eid];
this.showHostAvatar = true; if (value && value.icon) {
} else { return value.icon;
this.showHostAvatar = false;
}
if (avatar && this.chatMyId) {
const user = avatar[this.chatMyId];
if (user && user.avatar) {
return user.avatar;
}
} }
} }
if (avatar && this.data) { return "";
if (Object.getOwnPropertyNames(avatar).length > 0) { }
this.showHostAvatar = true;
} else { private get defaultAvatar() {
this.showHostAvatar = false; return xim.getAvatar();
} }
const value = avatar[this.data.eid];
if (value && value.avatar) { private get chatRole() {
return value.avatar; 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";
}
} }
} }
return ""; return "";
} }
...@@ -450,7 +468,6 @@ ...@@ -450,7 +468,6 @@
word-break: break-all; word-break: break-all;
} }
} }
&.offset-bottom { &.offset-bottom {
margin-bottom: 30px; margin-bottom: 30px;
} }
...@@ -478,6 +495,9 @@ ...@@ -478,6 +495,9 @@
&.algin-left { &.algin-left {
text-align: left; text-align: left;
} }
.cs-flex-direction {
flex-direction: row-reverse;
}
} }
.msg-detail { .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 @@ ...@@ -6,13 +6,7 @@
class="who-read-list pos-rel" class="who-read-list pos-rel"
> >
<template> <template>
<div <div class="d-flex tabs justify-content-center">
class="d-flex tabs"
:class="{
'justify-content-center':
!readlist.length || !unreadlist.length,
}"
>
<div <div
class="tab text-nowrap" class="tab text-nowrap"
:class="{ selected: tab === 1 }" :class="{ selected: tab === 1 }"
...@@ -63,8 +57,8 @@ ...@@ -63,8 +57,8 @@
import { unique } from "../utils"; import { unique } from "../utils";
import avatar from "@/customer-service/components/avatar.vue"; import avatar from "@/customer-service/components/avatar.vue";
import { ChatStore } from "@/customer-service/store/model"; import { ChatStore } from "@/customer-service/store/model";
import chat from "@/customer-service/xim/index";
import xim from "@/customer-service/xim/xim"; import xim from "@/customer-service/xim/xim";
import { getUserInfo } from "../utils/user-info";
const chatStoreNamespace = namespace("chatStore"); const chatStoreNamespace = namespace("chatStore");
...@@ -116,8 +110,8 @@ ...@@ -116,8 +110,8 @@
} }
private async getUserNameByid(eid: string) { private async getUserNameByid(eid: string) {
const data = await chat.getSdk().model("user").detail(eid).query(); const data = await getUserInfo(eid);
return data.row.first_name.value as string; return data.name;
} }
private async getReader() { private async getReader() {
...@@ -177,7 +171,7 @@ ...@@ -177,7 +171,7 @@
color: #000; color: #000;
z-index: 2; z-index: 2;
margin-left: -100px; margin-left: -100px;
width: 125px; width: 200px;
&.offset { &.offset {
margin-left: 0; 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 { ...@@ -13,9 +13,28 @@ class ChatCacheDatabaseController {
private readonly chatListKey = "chat-list"; private readonly chatListKey = "chat-list";
private readonly chatMessageKey = "chat-message"; 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) { public setup(uid: string) {
if (this.setuping) {
return this.waitSetupCompleted();
}
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (uid && indexedDB) { if (uid && indexedDB) {
this.setuping = true;
const r = indexedDB.open( const r = indexedDB.open(
"u-" + (this.uid = uid), "u-" + (this.uid = uid),
this.listVersion this.listVersion
...@@ -23,26 +42,29 @@ class ChatCacheDatabaseController { ...@@ -23,26 +42,29 @@ class ChatCacheDatabaseController {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this; const that = this;
const setupDb = () => { const setupDb = () => {
if (that.db) { if (this.setuping) {
try { if (that.db) {
that.buildTables(that.db, that.chatListKey); try {
console.log( that.buildTables(that.db, that.chatListKey);
`build index database for chat completed, 100%` console.log(
); `build index database for chat completed, 100%`
} catch (e) { );
console.error(e); } catch (e) {
console.error(e);
}
} }
this.setuping = false;
} }
resolve(); resolve();
}; };
r.onsuccess = function (e) { r.onsuccess = function (e) {
that.db = (e.target as any).result; that.db = (e.target as any).result;
console.log(`index database init comepleted, 33%`); console.log(`index database init comepleted`);
setupDb(); setupDb();
}; };
r.onupgradeneeded = function (e) { r.onupgradeneeded = function (e) {
that.db = (e.target as any).result; that.db = (e.target as any).result;
console.log(`index database init comepleted, 66%`); console.log(`upgrade database comepleted`);
setupDb(); setupDb();
}; };
r.onerror = function (e) { r.onerror = function (e) {
...@@ -136,10 +158,30 @@ class ChatCacheDatabaseController { ...@@ -136,10 +158,30 @@ class ChatCacheDatabaseController {
p.eid && (chat.last_msg_sender = p.eid); p.eid && (chat.last_msg_sender = p.eid);
p.type && (chat.last_msg_type = p.type); p.type && (chat.last_msg_type = p.type);
p.msg && (chat.last_msg_content = p.msg); p.msg && (chat.last_msg_content = p.msg);
p.unread && chat.unread_msg_count++; p.set2Read && (chat.unread_msg_count = 0);
if (p.unread === 0) { const u = store.put(chat, chat.id);
chat.unread_msg_count = 0; 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); const u = store.put(chat, chat.id);
u.onsuccess = () => resolve(); u.onsuccess = () => resolve();
u.onerror = () => resolve(); u.onerror = () => resolve();
......
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
for="chat-upload-file" for="chat-upload-file"
:title="tip4File" :title="tip4File"
@click="allowLoadFile" @click="allowLoadFile"
v-if="enableFileSelection"
> >
<img <img
class="tool-bar-icon" class="tool-bar-icon"
...@@ -36,7 +35,6 @@ ...@@ -36,7 +35,6 @@
id="chat-upload-file" id="chat-upload-file"
type="file" type="file"
:accept="acceptType" :accept="acceptType"
multiple
/> />
</div> </div>
...@@ -121,6 +119,16 @@ ...@@ -121,6 +119,16 @@
); );
} }
const limitedFileExtension = [
"ppt",
"pptx",
"doc",
"docx",
"xls",
"xlsx",
"pdf",
];
@Component({ components: {} }) @Component({ components: {} })
export default class Input extends Vue { export default class Input extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
...@@ -139,8 +147,6 @@ ...@@ -139,8 +147,6 @@
private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})`; private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})`;
private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})`; private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})`;
private enableFileSelection = false;
private emoji: EmojiItem[] = []; private emoji: EmojiItem[] = [];
@Watch("chatId") @Watch("chatId")
...@@ -201,7 +207,8 @@ ...@@ -201,7 +207,8 @@
} }
private allowLoadFile() { private allowLoadFile() {
this.acceptType = "*"; this.acceptType =
limitedFileExtension.map((i) => `.${i}`).join(",") + ",image/*";
} }
private async handlePasteEvent(event: ClipboardEvent) { private async handlePasteEvent(event: ClipboardEvent) {
...@@ -221,19 +228,22 @@ ...@@ -221,19 +228,22 @@
const file = items[i].getAsFile(); const file = items[i].getAsFile();
if (file) { if (file) {
if (file.size <= 0) { if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY); return this.$emit("error", MESSAGE_FILE_EMPTY);
return;
} }
if (this.isImage(file)) { if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) { if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE); return this.$emit(
return; "error",
MESSAGE_IMAGE_TOO_LARGE
);
} }
html += this.buildImageHtml(file); html += this.buildImageHtml(file);
} else { } else {
if (file.size >= MAX_FILE_SIZE) { if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE); return this.$emit(
return; "error",
MESSAGE_FILE_TOO_LARGE
);
} }
html += this.buildFileHtml(file); html += this.buildFileHtml(file);
} }
...@@ -497,30 +507,23 @@ ...@@ -497,30 +507,23 @@
for (let index = 0; index < files.length; index++) { for (let index = 0; index < files.length; index++) {
const file = files[index]; const file = files[index];
if (file.size <= 0) { if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY); return this.$emit("error", MESSAGE_FILE_EMPTY);
return;
} }
if ( if (file.size >= MAX_FILE_SIZE) {
this.enableFileSelection && return this.$emit("error", MESSAGE_FILE_TOO_LARGE);
file.size >= MAX_FILE_SIZE
) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
} }
if (this.isImage(file)) { if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) { if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE); return this.$emit(
return; "error",
MESSAGE_IMAGE_TOO_LARGE
);
} }
html += this.buildImageHtml(file); html += this.buildImageHtml(file);
} else { } else {
if (this.enableFileSelection) { html += this.buildFileHtml(file);
html += this.buildFileHtml(file);
} else {
return this.$emit("error", ERROR_IMAGE);
}
} }
} }
...@@ -619,6 +622,11 @@ ...@@ -619,6 +622,11 @@
.file-size { .file-size {
margin-top: 10px; 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 { ...@@ -44,13 +44,25 @@ export const enum ServiceType {
Backend, 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 type TokenStringGetter = () => Promise<string>;
export interface ChatOption { export interface ChatOption {
/** /**
* 长链接chat sdk地址 * IM 链接,如果使用系统配置,只需要传入环境标识即可,如果默认配置不满足,可以传入指定地址
*/ */
webSocketUri: string; connection: ImEnvironment | string;
sdk: () => UniplatSdk; sdk: () => UniplatSdk;
...@@ -65,10 +77,14 @@ export interface ChatOption { ...@@ -65,10 +77,14 @@ export interface ChatOption {
eventHub?: Vue; eventHub?: Vue;
/** message?: ChatMessageController;
* 用户信息(头像,别名)可选
*/ avatar?: string;
user?: { icon?: string; username?: string }; }
export interface ChatMessageController {
error: (msg: string) => void;
info: (msg: string) => void;
} }
export interface ChatServiceLogger { export interface ChatServiceLogger {
...@@ -91,7 +107,12 @@ export const enum MessageType { ...@@ -91,7 +107,12 @@ export const enum MessageType {
Voice = "voice", Voice = "voice",
GeneralOrderMsg = "general_order_msg", GeneralOrderMsg = "general_order_msg",
Withdraw = "withdraw", Withdraw = "withdraw",
MyPurchasePlan = "my_purchase_plan",
MyWelfare = "my_welfare",
QuestionAnswer = "question_answer",
Action = "action", Action = "action",
Notify = 'notify',
MpNavigate = "mp-navigate",
} }
export const enum MessageHandled { export const enum MessageHandled {
......
...@@ -11,11 +11,11 @@ import { ...@@ -11,11 +11,11 @@ import {
import { isAccessibleUrl } from "../service/tools"; import { isAccessibleUrl } from "../service/tools";
import { unique } from "../utils"; import { unique } from "../utils";
import { getChatModelInfo } from "../utils/chat-info"; import { getChatModelInfo } from "../utils/chat-info";
import { decode } from "../utils/jwt";
import { getUserInfo } from "../utils/user-info"; import { getUserInfo } from "../utils/user-info";
import Chat from "../xim"; import Chat from "../xim";
import { Chat as ChatType, Message } from "../xim/models/chat"; import { Chat as ChatType, Message } from "../xim/models/chat";
import xim, { ChatNotifyListener } from "../xim/xim"; import xim, { ChatNotifyListener } from "../xim/xim";
import { decodeJwt } from "uniplat-sdk";
import { ChatStatus, ChatStore, ChatStoreState } from "./model"; import { ChatStatus, ChatStore, ChatStoreState } from "./model";
...@@ -93,24 +93,29 @@ function buildChatItem(chat: RawChatItem) { ...@@ -93,24 +93,29 @@ function buildChatItem(chat: RawChatItem) {
const chatType = "group"; const chatType = "group";
const filterActiveChats = (items: RawChatItem[]) => { const allowedChatTypes = [chatType, "notify"];
export const filterActiveChats = (items: RawChatItem[]) => {
return items.filter( return items.filter(
(i) => (i) =>
!i.is_finish && !i.is_finish &&
!i.is_exited && !i.is_exited &&
!i.is_remove && !i.is_remove &&
!i.is_deleted && !i.is_deleted &&
i.type === chatType allowedChatTypes.includes(i.type)
); );
}; };
export function getLastMessageId(msgs: Message[] | any) { export function getLastMessageId(msgs: Message[] | any) {
const last = msgs[msgs.length - 1]; if (msgs && msgs.length) {
let id = last.id; const last = msgs[msgs.length - 1];
if (id < 0) { let id = last.id;
id = Math.max(...msgs.map((i: any) => i.id)); if (id < 0) {
id = Math.max(...msgs.map((i: any) => i.id));
}
return id;
} }
return id; return 0;
} }
export default { export default {
...@@ -124,7 +129,7 @@ export default { ...@@ -124,7 +129,7 @@ export default {
[ChatStore.STATE_CHAT_CURRENT_USER_UID]: null, [ChatStore.STATE_CHAT_CURRENT_USER_UID]: null,
[ChatStore.STATE_CHAT_MSG_HISTORY]: null, [ChatStore.STATE_CHAT_MSG_HISTORY]: null,
[ChatStore.STATE_CHAT_SENDING_MESSAGES]: [], [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_SINGLE_CHAT]: null,
[ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION]: null, [ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION]: null,
[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]: null, [ChatStore.STATE_CHAT_CURRENT_CHAT_ID]: null,
...@@ -201,7 +206,7 @@ export default { ...@@ -201,7 +206,7 @@ export default {
}, },
[ChatStore.MUTATION_SAVE_MYSELF_ID](state) { [ChatStore.MUTATION_SAVE_MYSELF_ID](state) {
Chat.getToken().then((token) => { 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_ID] = String(eid.user_id);
state[ChatStore.STATE_CHAT_MY_UID] = eid.sub; state[ChatStore.STATE_CHAT_MY_UID] = eid.sub;
}); });
...@@ -386,6 +391,8 @@ export default { ...@@ -386,6 +391,8 @@ export default {
async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit, state }) { async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit, state }) {
let cache = await dbController.getChatList(); let cache = await dbController.getChatList();
cache.sort((x, y) => (x.last_msg_ts < y.last_msg_ts ? 1 : -1));
for (const item of cache) { for (const item of cache) {
if (item.business_data && !item.detail_name) { if (item.business_data && !item.detail_name) {
const d = JSON.parse( const d = JSON.parse(
...@@ -415,10 +422,7 @@ export default { ...@@ -415,10 +422,7 @@ export default {
}; };
if (cache && cache.length) { if (cache && cache.length) {
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, { commit(ChatStore.MUTATION_SAVE_CHAT_LIST, cache);
list: cache,
total: 9999,
});
const ts = cache const ts = cache
.map((i) => Math.max(i.last_msg_ts, i.update_time)) .map((i) => Math.max(i.last_msg_ts, i.update_time))
.sort(); .sort();
...@@ -459,10 +463,7 @@ export default { ...@@ -459,10 +463,7 @@ export default {
buildChatItem(chat) buildChatItem(chat)
); );
dbController.saveChatList(items); dbController.saveChatList(items);
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, { commit(ChatStore.MUTATION_SAVE_CHAT_LIST, items);
list: items,
total: 9999,
});
resolve(buildUnreadMessage(items)); resolve(buildUnreadMessage(items));
}); });
}); });
...@@ -606,7 +607,7 @@ export default { ...@@ -606,7 +607,7 @@ export default {
const { imChatId } = await Chat.getSdk() const { imChatId } = await Chat.getSdk()
.model(params.modelName) .model(params.modelName)
.chat(params.selectedListId, orgId()) .chat(params.selectedListId, orgId())
.createChat(true); .createChat(true, params.title);
const chatId = +imChatId; const chatId = +imChatId;
await commit(ChatStore.MUTATION_SHOW_CHAT, true); await commit(ChatStore.MUTATION_SHOW_CHAT, true);
await dispatch( await dispatch(
...@@ -676,8 +677,9 @@ export default { ...@@ -676,8 +677,9 @@ export default {
if (!chatId) { if (!chatId) {
return; return;
} }
const chatList = const chatList = state[
state[ChatStore.STATE_MY_CHAT_ROOM_LIST]?.list ?? []; ChatStore.STATE_MY_CHAT_ROOM_LIST
] as ChatType[];
let wantedChatRoom = chatList.find((k) => k.chat_id === chatId); let wantedChatRoom = chatList.find((k) => k.chat_id === chatId);
if (!wantedChatRoom) { if (!wantedChatRoom) {
...@@ -801,7 +803,7 @@ export default { ...@@ -801,7 +803,7 @@ export default {
}) })
.execute(); .execute();
await dispatch(ChatStore.ACTION_GET_MY_CHAT_LIST); 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; if (firstChat == null) return;
await getChatModelInfo( await getChatModelInfo(
firstChat.model_name, firstChat.model_name,
...@@ -905,7 +907,7 @@ export default { ...@@ -905,7 +907,7 @@ export default {
} }
return await Chat.getSdk() return await Chat.getSdk()
.model(currentChat.model_name) .model(currentChat.model_name)
.chat(currentChat.obj_id, orgId()) .chat(currentChat.obj_id, currentChat.org_id)
.removeMember(uids.map((id) => +id)) .removeMember(uids.map((id) => +id))
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS)); .finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
}, },
...@@ -974,6 +976,25 @@ export default { ...@@ -974,6 +976,25 @@ export default {
[ChatStore.ACTION_SET_CHAT_ERROR]: ({ state }, chat: number) => { [ChatStore.ACTION_SET_CHAT_ERROR]: ({ state }, chat: number) => {
(<any>state)[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR] = chat; (<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: { getters: {
[ChatStore.STATE_CHAT_MSG_HISTORY](state) { [ChatStore.STATE_CHAT_MSG_HISTORY](state) {
...@@ -1002,8 +1023,7 @@ export default { ...@@ -1002,8 +1023,7 @@ export default {
if (singleChat && singleChat.chat_id === chatId) { if (singleChat && singleChat.chat_id === chatId) {
return singleChat; return singleChat;
} }
const chatList = const chatList = state[ChatStore.STATE_MY_CHAT_ROOM_LIST];
state[ChatStore.STATE_MY_CHAT_ROOM_LIST]?.list ?? [];
return chatList.find((chat) => chat.chat_id === chatId); return chatList.find((chat) => chat.chat_id === chatId);
}, },
}, },
......
...@@ -3,6 +3,7 @@ import { namespace } from "vuex-class"; ...@@ -3,6 +3,7 @@ import { namespace } from "vuex-class";
import * as dto from "../model"; import * as dto from "../model";
import { Chat as ChatType } from "../xim/models/chat"; import { Chat as ChatType } from "../xim/models/chat";
import * as chatDto from "../xim/models/chat"; import * as chatDto from "../xim/models/chat";
export enum ChatStatus { export enum ChatStatus {
opening = 0, opening = 0,
terminated = 1, terminated = 1,
...@@ -19,10 +20,7 @@ export namespace ChatStore { ...@@ -19,10 +20,7 @@ export namespace ChatStore {
export const STATE_CHAT_DIALOG_IS_SINGLE = "会话模块是否是单个弹窗"; export const STATE_CHAT_DIALOG_IS_SINGLE = "会话模块是否是单个弹窗";
export type STATE_CHAT_DIALOG_IS_SINGLE = boolean; export type STATE_CHAT_DIALOG_IS_SINGLE = boolean;
export const STATE_MY_CHAT_ROOM_LIST = "我的会话列表"; export const STATE_MY_CHAT_ROOM_LIST = "我的会话列表";
export type STATE_MY_CHAT_ROOM_LIST = { export type STATE_MY_CHAT_ROOM_LIST = ChatType[];
list: ChatType[];
total: number;
} | null;
export const STATE_SINGLE_CHAT = "单独的会话"; export const STATE_SINGLE_CHAT = "单独的会话";
export type STATE_SINGLE_CHAT = ChatType | null; export type STATE_SINGLE_CHAT = ChatType | null;
export const STATE_CHAT_MSG_HISTORY = "某个会话聊天记录"; export const STATE_CHAT_MSG_HISTORY = "某个会话聊天记录";
...@@ -288,6 +286,7 @@ export namespace ChatStore { ...@@ -288,6 +286,7 @@ export namespace ChatStore {
modelName: string; modelName: string;
selectedListId: string; selectedListId: string;
uids: string[]; uids: string[];
title?: string;
}) => Promise<number>; }) => Promise<number>;
export const ACTION_CREATE_NEW_CHAT_BY_CLIENT_SIDE = export const ACTION_CREATE_NEW_CHAT_BY_CLIENT_SIDE =
...@@ -320,9 +319,13 @@ export namespace ChatStore { ...@@ -320,9 +319,13 @@ export namespace ChatStore {
export const ACTION_TERINATE_CHAT = "结束会话"; export const ACTION_TERINATE_CHAT = "结束会话";
export type ACTION_TERINATE_CHAT = () => Promise<void>; export type ACTION_TERINATE_CHAT = () => Promise<void>;
export const ACTION_CHAT_ADD_MEMBERS = "添加成员"; export const ACTION_CHAT_ADD_MEMBERS = "添加成员";
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 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 const ACTION_CHAT_ADD_CS = "添加客服";
export type ACTION_CHAT_ADD_CS = (uids: string[]) => Promise<void>; export type ACTION_CHAT_ADD_CS = (uids: string[]) => Promise<void>;
...@@ -350,13 +353,18 @@ export namespace ChatStore { ...@@ -350,13 +353,18 @@ export namespace ChatStore {
export const ACTION_SET_CHAT_ERROR = "标记会话关键性错误"; export const ACTION_SET_CHAT_ERROR = "标记会话关键性错误";
export type ACTION_SET_CHAT_ERROR = (chat: number) => void; 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 { export interface ChatUpdateParameter {
chat: number; chat: number;
type?: dto.MessageType; type?: dto.MessageType;
msg?: string; msg?: string;
ts?: number; ts?: number;
eid?: string; eid?: string;
unread?: number; set2Read?: boolean;
} }
export const ACTION_UPDATE_CHAT = "更新会话信息"; 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"; import Chat from "../xim";
export type UserMapping = { export type UserMapping = {
[eid: string]: { [eid: string]: {
name: string; name: string;
phone: string; phone: string;
icon: string;
}; };
}; };
const userMapping: UserMapping = {}; const userMapping: UserMapping = {};
export const getUserMapping = () => userMapping; export const getUserMapping = () => userMapping;
export async function getUserInfo(eid: string) { export async function getUserInfo(eid: string, sdk?: UniplatSdk) {
if (userMapping[eid]) { if (userMapping[eid]) {
return 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 = { const data = {
name: info.row.first_name.value as string, name: info.username || info.realname || info.mobile || info.mobile,
phone: info.row.last_name.value as string, phone: info.mobile,
icon: info.avatar_url
}; };
userMapping[eid] = data; userMapping[eid] = data;
return data; return data;
......
...@@ -7,6 +7,9 @@ import { ...@@ -7,6 +7,9 @@ import {
TokenStringGetter, TokenStringGetter,
ServiceType, ServiceType,
CustomerServiceProduct, CustomerServiceProduct,
ChatMessageController,
socketMapping,
ImEnvironment,
} from "./../model"; } from "./../model";
import { ChatLoggerService } from "./logger"; import { ChatLoggerService } from "./logger";
import tokenManager from "./token"; import tokenManager from "./token";
...@@ -24,9 +27,8 @@ class Chat { ...@@ -24,9 +27,8 @@ class Chat {
private ws = ""; private ws = "";
private connectedActions: (() => void)[] = []; private connectedActions: (() => void)[] = [];
private connected = false; private connected = false;
private messageController: ChatMessageController | null = null;
private userMapping: { [key: string]: { name: string; avatar: string } } = private defaultAvatar = "";
{};
public onReady(action: () => void) { public onReady(action: () => void) {
if (this.connected) { if (this.connected) {
...@@ -41,7 +43,7 @@ class Chat { ...@@ -41,7 +43,7 @@ class Chat {
throw new Error(`You must specify a chat option for chat service`); throw new Error(`You must specify a chat option for chat service`);
} }
if (!option.webSocketUri) { if (!option.connection) {
throw new Error( throw new Error(
`You must specify a web socket address for chat service` `You must specify a web socket address for chat service`
); );
...@@ -52,14 +54,12 @@ class Chat { ...@@ -52,14 +54,12 @@ class Chat {
(this.serviceType = option.serviceType); (this.serviceType = option.serviceType);
option.product && (this.product = option.product); option.product && (this.product = option.product);
this.eventHub = option.eventHub || null; 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; this.token = async () => option.sdk().global.jwtToken;
tokenManager.save(this.token); tokenManager.save(this.token);
...@@ -73,7 +73,9 @@ class Chat { ...@@ -73,7 +73,9 @@ class Chat {
// this.keywords = ["社保"]; // 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; this.connected = true;
for (const item of this.connectedActions) { for (const item of this.connectedActions) {
item(); item();
...@@ -167,10 +169,6 @@ class Chat { ...@@ -167,10 +169,6 @@ class Chat {
this.debug(`client status ${e}`); this.debug(`client status ${e}`);
} }
public getUserMapping() {
return this.userMapping;
}
private debug(message: string) { private debug(message: string) {
ChatLoggerService.logger?.debug(message); ChatLoggerService.logger?.debug(message);
} }
...@@ -190,6 +188,18 @@ class Chat { ...@@ -190,6 +188,18 @@ class Chat {
public getMatchedTextKeywords() { public getMatchedTextKeywords() {
return this.keywords; return this.keywords;
} }
public error(msg: string) {
this.messageController && this.messageController.error(msg);
}
public info(msg: string) {
this.messageController && this.messageController.info(msg);
}
public getAvatar() {
return this.defaultAvatar;
}
} }
export default new Chat(); export default new Chat();
...@@ -22,7 +22,7 @@ export interface Chat { ...@@ -22,7 +22,7 @@ export interface Chat {
biz_id: string; biz_id: string;
last_msg_sender: string; last_msg_sender: string;
last_msg_content: string; last_msg_content: string;
last_msg_type: string; last_msg_type: MessageType;
model_name: string; model_name: string;
obj_id: string; obj_id: string;
is_finish: boolean; is_finish: boolean;
...@@ -47,6 +47,7 @@ export interface Chat { ...@@ -47,6 +47,7 @@ export interface Chat {
chat_id: number; chat_id: number;
catalog: string; catalog: string;
biz_type_id: number; biz_type_id: number;
biz_type_code: string;
business_data?: string; business_data?: string;
detail_name?: 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