Commit 42b99d0f by 杨铁龙

Merge commit '2634302f' into wx

parents 5254eade 2634302f
Showing with 5146 additions and 2738 deletions
<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 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>
<template>
<div class="chat-room-con h-100 pos-rel">
<div class="chat-room-con pos-rel">
<div class="chat-panel">
<div class="chat-area h-100 d-flex flex-column" ref="chatBox">
<div
ref="top"
class="chat-messages pos-rel flex-fill d-flex"
:class="{ 'is-not-chat-member': !isChatMember }"
:class="{ 'is-not-chat-member': !hasInput }"
>
<div
v-if="getCurrentInputingPeople.length"
......@@ -13,7 +13,7 @@
>
{{ getCurrentInputingPeople }}正在输入
</div>
<messages class="flex-fill" />
<messages class="flex-fill" @open-message="openMessage" />
<slot name="chat-right-panel"></slot>
</div>
<div
......@@ -21,85 +21,60 @@
title="收缩侧边栏"
ref="resize"
@mousedown="dragControllerDiv"
v-if="isChatMember"
v-if="hasInput"
></div>
<div
ref="bottom"
class="chat-input flex-none h-100"
v-if="isChatMember"
v-if="hasInput"
>
<message-input @error="onError" />
<message-input @error="onError" @sent="onMessageSent" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
Component,
Prop,
Provide,
Ref,
Vue,
Watch,
} from "vue-property-decorator";
import MessageInput from "@/customer-service/components/message-input.vue";
import messages from "@/customer-service/components/message-list.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model";
type RoomInfoTab = "customer" | "order";
@Component({
components: {
MessageInput,
messages,
},
})
export default class ChatRoom extends Vue {
@Ref("chatBox") chatBox!: Element;
@Ref("top") refTop!: Element;
@Ref("bottom") refBottom!: Element;
@Ref("resize") refResize!: Element;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
<script lang="ts">
import { Component, Provide, Ref, Vue, Watch } from "vue-property-decorator";
import MessageInput from "@/customer-service/components/message-input.vue";
import messages from "@/customer-service/components/message-list.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model";
import Chat from "@/customer-service/xim";
import { CustomerServiceEvent } from "../event";
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
const chatResizeKey1 = "chat-resize-1";
const chatResizeKey2 = "chat-resize-2";
@chatStore.Mutation(ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS)
private readonly clearChatMembers!: ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS;
@Component({ components: { MessageInput, messages } })
export default class ChatRoom extends Vue {
@Ref("chatBox") private readonly chatBox!: HTMLElement;
@Ref("top") private readonly refTop!: HTMLElement;
@Ref("bottom") private readonly refBottom!: HTMLElement;
@Ref("resize") private readonly refResize!: HTMLElement;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_TITLE)
private readonly chatTitle!: ChatStore.STATE_CURRENT_CHAT_TITLE;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_INPUTING)
private readonly currentInputPeople!: ChatStore.STATE_CURRENT_CHAT_INPUTING;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID)
private readonly currentChatUniplatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID;
@chatStore.State(ChatStore.STATE_MY_CHAT_ROOM_LIST)
private readonly myChatList!: ChatStore.STATE_MY_CHAT_ROOM_LIST;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER)
private readonly isChatMember!: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER;
private allChatList = { list: [] };
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR)
private readonly chatError!: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR;
@Prop({ type: Function })
private close?: () => void;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CURRENT_CHAT)
private readonly currentChat!: ChatStore.GETTER_CURRENT_CURRENT_CHAT;
@Provide() showReadSummary = true;
private setting = 0;
@Watch("currentChatUniplatId")
private whenCurrentChatIdChanged(newValue: string, oldValue: string) {
if (Number(oldValue) === Number(newValue)) return;
this.clearChatMembers();
private get hasInput() {
return this.isChatMember && this.chatError !== this.chatId;
}
private activeTab: RoomInfoTab = "customer";
@Provide() showReadSummary = true;
private get getCurrentInputingPeople() {
return this.currentInputPeople
......@@ -107,30 +82,15 @@ export default class ChatRoom extends Vue {
.join("、");
}
private get currentChat() {
const chatId = this.chatId;
if (this.myChatList == null) return;
const result = this.myChatList.list.find((k) => k.chat_id === chatId);
return result ?? {};
}
private get customerInfoTabShow() {
return this.activeTab === "customer";
}
private get orderInfoTabShow() {
return this.activeTab === "order";
}
private onError(msg: string) {
this.$message.error(msg);
Chat.error(msg);
}
private dragControllerDiv(e: MouseEvent) {
const resize = this.refResize as any;
const top = this.refTop as HTMLElement;
const bottom = this.refBottom as HTMLElement;
const box = this.chatBox as HTMLElement;
const top = this.refTop;
const bottom = this.refBottom;
const box = this.chatBox;
const startY = e.clientY;
const originTop = resize.offsetTop;
......@@ -146,6 +106,9 @@ export default class ChatRoom extends Vue {
resize.style.top = moveLen + "px"; // 设置左侧区域的宽度
top.style.height = moveLen + "px";
bottom.style.height = bottomHeight + "px";
localStorage.setItem(chatResizeKey1, moveLen);
localStorage.setItem(chatResizeKey2, bottomHeight + "");
};
document.onmouseup = function () {
document.onmousemove = null;
......@@ -157,20 +120,60 @@ export default class ChatRoom extends Vue {
}
mounted() {
this.adjust();
}
private adjust() {
setTimeout(() => {
if (
localStorage.getItem(chatResizeKey1) &&
localStorage.getItem(chatResizeKey2) &&
this.hasInput
) {
const s1 = localStorage.getItem(chatResizeKey1) + "px";
const s2 = localStorage.getItem(chatResizeKey2) + "px";
this.refTop && (this.refTop.style.height = s1);
this.refResize && (this.refResize.style.top = s1);
this.refBottom && (this.refBottom.style.height = s2);
} else {
this.refBottom &&
((this.refBottom as HTMLElement).style.height =
this.chatBox.clientHeight - this.refTop.clientHeight + "px");
this.chatBox.clientHeight -
this.refTop.clientHeight +
"px");
}
}, 800);
}
private openMessage(o: any) {
CustomerServiceEvent.emit(this, o);
}
private onMessageSent() {
if (this.setting) {
clearTimeout(this.setting);
}
this.setting = setTimeout(
() =>
this.currentChat &&
Chat.setRead(
this.currentChat.model_name,
this.currentChat.obj_id
),
300
);
this.$emit("send");
}
}
}
</script>
<style lang="less" scoped>
.chat-status {
.chat-status {
display: inline-block;
width: 46px;
height: 20px;
line-height: 20px;
background: #22bd7a;
background-color: #22bd7a;
font-size: 13px;
border-radius: 2px;
color: #ffffff;
......@@ -179,9 +182,13 @@ export default class ChatRoom extends Vue {
&.chat-done {
background: #c5d4e5;
}
}
}
.chat-panel {
.chat-room-con {
min-width: 400px;
}
.chat-panel {
height: 100%;
.chat-area,
.chat-info {
......@@ -192,8 +199,8 @@ export default class ChatRoom extends Vue {
width: 349px;
border-left: 1px solid #e1e1e1;
}
}
.info-tabs {
}
.info-tabs {
height: 40px;
line-height: 40px;
border-bottom: 1px solid #f0f0f0;
......@@ -211,8 +218,8 @@ export default class ChatRoom extends Vue {
font-weight: 600;
}
}
}
.chat-area {
}
.chat-area {
position: relative;
width: 100%;
overflow: hidden;
......@@ -231,16 +238,20 @@ export default class ChatRoom extends Vue {
top: calc(100% - 130px + 1px);
height: 6px;
width: 100%;
&:hover {
background-color: #eee;
}
}
.order-info-con {
}
}
.order-info-con {
height: calc(100% - 40px);
}
.someone-inputing {
}
.someone-inputing {
position: absolute;
left: 20px;
bottom: 20px;
z-index: 1;
color: #c2c2c2;
}
}
</style>
<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"
>我要接待</el-button
>
<el-button class="button" @click="showAddMember" size="small"
>添加客服</el-button
>
<el-button
class="button"
@click="finishReception"
size="small"
v-if="isChatMember && operatorType > 25"
type="warning"
>结束接待</el-button
>
<el-button
class="button"
@click="exitChat"
size="small"
v-if="isChatMember"
type="danger"
>退出会话</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.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 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>
import { Component, Vue } from "vue-property-decorator";
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 { ChatUserInfoService } from "@/customer-service/utils/user-info";
@Component({ components: {} })
export default class ChatList extends Vue {
private nextTimer = 0;
@chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
protected readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
......@@ -50,21 +51,29 @@ export default class ChatList extends Vue {
@chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA)
protected readonly reset!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA;
@chatStore.Getter(ChatStore.STATE_CHAT_MSG_HISTORY)
protected readonly historyMessage!: ChatStore.STATE_CHAT_MSG_HISTORY;
@chatStore.Getter(ChatStore.STATE_CHAT_SENDING_MESSAGES)
protected readonly sendingMessages!: ChatStore.STATE_CHAT_SENDING_MESSAGES;
@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;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CURRENT_CHAT)
protected readonly currentChat!: ChatStore.GETTER_CURRENT_CURRENT_CHAT;
protected parseMesage(data: ChatItem) {
if (data.last_msg_sender && data.last_msg_sender !== "0") {
if (!this.userNames[data.last_msg_sender]) {
this.updateUserName({ id: data.last_msg_sender, name: "" });
this.sdk
.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(() => {});
if (this.userNames[data.last_msg_sender] === undefined) {
const id = data.last_msg_sender;
this.updateUserName({ id, name: "" });
ChatUserInfoService.getUserInfo(id).then((d) =>
this.updateUserName({ id, name: d.alias_name || d.name })
);
}
}
if (data.last_msg_content === "") {
......@@ -84,4 +93,18 @@ export default class ChatList extends Vue {
}
return this.parseMesage(item);
}
/**
* 一分钟更新一次会话列表
*/
protected enableAutoRefresh() {
this.nextTimer = setTimeout(
() => this.getMyChatList().finally(() => this.enableAutoRefresh()),
60 * 1000
);
}
beforeDestroy() {
clearTimeout(this.nextTimer);
}
}
import { MessageType } from "@/customer-service/model";
import { CardMessage } from '@/customer-service/model/card';
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, '问答'],
[MessageType.Pay, '付款通知'],
[MessageType.PayV1, '付款通知'],
[MessageType.Refund, '退款通知'],
[MessageType.RefundV1, '退款通知'],
[MessageType.Notify, '通知'],
])
export function parserMessage(type: MessageType, rawMsg: string) {
try {
if (!type) return "";
if (!rawMsg) return "";
if (type === MessageType.Text) {
const msg = JSON.parse(rawMsg);
return msg.text;
} else if (type === MessageType.Image) {
return `[图片]`;
} else if (type === MessageType.File) {
return `[文件]`;
} else if (type === MessageType.Withdraw) {
return `[撤回了一条消息]`;
} else {
}
if (type === MessageType.Action) {
const msg = JSON.parse(rawMsg);
return msg.text;
}
if (type === MessageType.Notify) {
return rawMsg;
}
if (type === MessageType.Card) {
const p = JSON.parse(rawMsg) as CardMessage;
if (p && p.title) {
return p.title || '通知';
}
}
const t = mapping.get(type)
if (t) {
return `[${t}]`;
}
return `[系统自动回复]`;
} catch {
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 User = {
id: string;
name: string;
};
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
@Component({ components: { avatar, GeneralTagSelectForFilter } })
export default class ChatCreator extends Vue {
@Prop({
type: Array,
default: () => [],
})
private selected!: number[];
@Watch("currentPage")
private pageChange() {
this.nextPage();
}
private tagGroups: TagManagerTypes.TagGroup[] = []
private searchText = "";
private currentPage = 1;
private total = 0;
private pageSize = 15;
private selectedUsers: number[] = [];
private userList: {
id: any;
name: any;
}[] = [];
private getList!: ThenArg<ReturnType<ListEasy["query"]>>["getList"];
public async created() {
await this.getUserList();
}
private getSelectedTags() {
if (this.$refs.generalTagSelect) {
return (
this.$refs.generalTagSelect as GeneralTagSelectForFilter
).getSelectedTags();
}
return [];
}
private async getUserList(searchText: string | null = null) {
this.loading = true;
const list = chat.getSdk().model("user").list();
if (searchText) {
list.addFilter({
property: "first_name",
value: searchText,
match: ListTypes.filterMatchType.fuzzy,
});
}
const { pageData, getList } = await list.query({
pageIndex: this.currentPage,
item_size: this.pageSize,
tagFilters: this.getSelectedTags()
});
this.total = pageData.record_count;
this.getList = getList;
this.userList = this.exactUserList(pageData.rows);
this.loading = false;
this.tagGroups = pageData.tagGroups || [];
}
private exactUserList(rows: any[]) {
return rows.map((k) => {
return {
id: k.id.value,
name: k.first_name.value,
};
});
}
private hide() {
this.$emit("hide");
}
private search() {
this.currentPage = 1;
this.getUserList(this.searchText);
}
private loading = false;
@buttonThrottle()
private async nextPage() {
this.loading = true;
const data = await this.getList(this.currentPage);
this.loading = false;
this.userList = this.exactUserList(data.rows);
}
private forbidden(id: number) {
return this.selected.includes(id);
}
private isMember(id: number) {
return this.selectedUsers.includes(id) || this.selected.includes(id);
}
private isSelected(id: number) {
return this.selectedUsers.includes(id);
}
private onClickUser(id: number) {
if (this.isSelected(id)) {
this.removeUser(id);
} else {
this.addUser(id);
}
}
private addUser(id: number) {
this.selectedUsers.push(id);
}
private removeUser(id: number) {
this.selectedUsers = this.selectedUsers.filter((_id) => _id !== id);
}
@buttonThrottle()
private createChat() {
return new Promise((resolve) => {
this.$emit(
"submit",
this.selectedUsers.map((id) => String(id)),
resolve
);
});
}
}
</script>
<style lang="less" scoped>
.text-right {
text-align: right;
}
.create-chat {
/deep/ .el-dialog__body {
padding: 30px 40px;
}
}
.users {
white-space: pre-line;
}
.user-con {
display: inline-flex;
vertical-align: top;
width: 33.33%;
align-items: center;
margin-bottom: 40px;
cursor: pointer;
&.forbid {
cursor: not-allowed;
}
}
.user-name {
height: 15px;
font-size: 15px;
color: #000000;
line-height: 15px;
margin-left: 20px;
&.isChoosed {
color: #3285ff;
}
}
.search-bar {
align-items: center;
background: #f5f6fa;
padding: 12px 20px;
box-sizing: border-box;
margin-bottom: 30px;
.row + .row {
margin-top: 5px;
}
.tag-group /deep/ .checkbox-group {
padding: 0;
margin-bottom: 0;
}
.input-row {
display: flex;
line-height: 40px;
}
}
.search-input {
width: 160px;
margin-left: 10px;
/deep/ .el-input__inner {
border: 1px solid rgba(229, 230, 236, 1);
border-radius: 0;
padding-left: 21px;
}
}
</style>
/deep/.benefits-plan-message, /deep/.benefits-welfare-message{
white-space: normal;
.my-plan,.my-welfare{
color: #E84929;
margin-bottom: 10px;
}
}
/deep/.benefits-plan-message {
.plan-title-wrap {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
.total-price {
color: #e84929;
white-space: nowrap;
}
.paid-money{
color: #e84929;
margin: 0 10px;
white-space: nowrap;
}
.plan-status{
white-space: nowrap;
}
.item-title {
max-width: 185px;
color: #373737;
font-size: 14px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: underline;
margin-right: 8px;
cursor: pointer;
&:hover{
color: #666;
}
}
}
.product-item {
display: flex;
padding: 16px 0;
&:not(:last-child) {
border-bottom: 1px dashed #ededed;
}
img {
width: 50px;
height: 50px;
margin-right: 10px;
}
.product-detail {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
.product-name {
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price-nums{
display: flex;
justify-content: space-between;
}
}
}
}
/deep/.benefits-welfare-message {
.item-title {
font-weight: bold;
margin-bottom: 10px;
}
}
\ No newline at end of file
......@@ -3,24 +3,20 @@
:modal="false"
:before-close="close"
:visible="value"
custom-class="hide-header show-close padding-0 width-auto"
:show-close="false"
custom-class="transparent"
width="90%"
>
<div class="d-flex flex-column">
<div class="preview-title text-center">图片预览</div>
<div class="d-flex justify-content-center" style="min-width: 300px">
<img v-if="file" :src="file.url" :style="style" />
<div class="d-flex justify-content-center align-items-start">
<img v-if="file" :src="file.url" />
<i class="el-icon-close" @click="close"></i>
</div>
<div class="d-flex justify-content-center actions">
<span
class="d-flex align-items-center justify-content-center"
@click="set2Default"
>1:1</span
>
<a
class="d-flex align-items-center justify-content-center"
:href="file.url | downloadUrl(getAttachment)"
:href="file.url"
:download="getAttachment"
>
<i class="el-icon-download"></i>
......@@ -31,60 +27,55 @@
</template>
<script lang="ts">
import { Component, Mixins, Model, Prop } from "vue-property-decorator";
import { Component, Model, Prop, Vue } from "vue-property-decorator";
import { Filters } from '../mixin/filter';
@Component({ components: {} })
export default class ImagePreview extends Mixins(Filters) {
@Component({ components: {} })
export default class ImagePreview extends Vue {
@Model("update")
private value!: boolean;
@Prop()
private file!: { name: string; url: string };
private style: {
"max-height": number | string;
"max-width": number | string;
} = {
"max-height": "300px",
"max-width": "600px",
};
private close() {
setTimeout(
() => (this.style = { "max-height": "300px", "max-width": "600px" }),
300
);
this.$emit("update", false);
}
private set2Default() {
this.style = { "max-height": "1600px", "max-width": "1600px" };
}
private get getAttachment() {
if (this.file) {
return this.file.name;
}
return "文件下载";
}
}
}
</script>
<style lang="less" scoped>
.preview-title {
font-size: 18px;
color: #333;
margin-bottom: 15px;
}
img {
max-width: 100%;
max-height: 100%;
.actions {
& + i {
top: -25px;
right: 20px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
padding: 5px;
font-size: 40px;
color: #fff;
display: table;
position: relative;
z-index: 1;
cursor: pointer;
}
}
.actions {
margin: 15px 0;
> span,
a {
width: 30px;
height: 30px;
width: 50px;
height: 50px;
background-color: #7a7b7d;
color: #fff;
border-radius: 50%;
......@@ -92,16 +83,12 @@ export default class ImagePreview extends Mixins(Filters) {
i {
color: #fff;
font-size: 20px;
font-size: 30px;
}
& + span {
margin-left: 15px;
}
}
> a {
margin-left: 15px;
}
}
</style>
......@@ -7,23 +7,20 @@
/>
</template>
<script lang="ts">
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import ChatInput, {
FILE_INFO_CLASS,
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import ChatInput, {
isFileElement,
isImageOrFile,
} from "../hybrid-input/index.vue";
import { Message, MessageType } from "../model";
import { uploadFile } from "../service/upload";
import { ChatLoggerService } from "../xim/logger";
import xim from "../xim/xim";
import { ChatStore, chatStore } from "@/customer-service/store/model";
} from "../hybrid-input/index.vue";
import { Message, MessageType } from "../model";
import { uploadFile } from "../service/upload";
import xim from "../xim/xim";
import { ChatStore, chatStore } from "@/customer-service/store/model";
let sendingMessageIndex = 1;
let sendingMessageIndex = 1;
@Component({ components: { ChatInput } })
export default class MessageInput extends Vue {
@Component({ components: { ChatInput } })
export default class MessageInput extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE)
private readonly chatRoomVisible!: ChatStore.STATE_CHAT_DIALOG_VISIBLE;
......@@ -54,32 +51,50 @@ export default class MessageInput extends Vue {
@Ref("chat-input")
private readonly chatInput!: ChatInput;
private sending = false;
@Watch("chatRoomVisible")
private whenChatRoomShow() {
if (!this.chatRoomVisible) return;
this.chatInput.focus();
}
private async sendMessage(msg: ChildNode[], done: () => void) {
if (this.chatIniting) {
private async sendMessage(msg: ChildNode[]) {
if (this.chatIniting || this.sending) {
return;
}
const count = msg.length;
let finished = 0;
this.sending = true;
const onFinishedChanged = () => {
finished++;
if (finished === count) {
this.sending = false;
}
};
setTimeout(() => (this.sending = false), 3000);
for (const item of msg) {
if (isImageOrFile(item)) {
if ((item as Element).classList.contains(FILE_INFO_CLASS)) {
await this.sendFile(item, MessageType.File);
if (isFileElement(item)) {
await this.sendFile(item, MessageType.File)
.catch((e) => this.onError(e))
.finally(onFinishedChanged);
} else {
await this.sendFile(item, MessageType.Image);
await this.sendFile(item, MessageType.Image)
.catch((e) => this.onError(e))
.finally(onFinishedChanged);
}
continue;
}
if (item.textContent) {
await this.sendText(item.textContent);
await this.sendText(item.textContent)
.catch((e) => this.onError(e))
.finally(onFinishedChanged);
} else {
onFinishedChanged();
}
}
ChatLoggerService.logger?.debug("all messages sent");
done();
this.$emit("sent");
}
......@@ -94,11 +109,17 @@ export default class MessageInput extends Vue {
if (this.source) {
Object.assign(msg, { source: this.source });
}
return this.sendMsg({ msgType: MessageType.Text, msg: JSON.stringify(msg) });
return this.sendMsg({
msgType: MessageType.Text,
msg: JSON.stringify(msg),
});
}
}
private async sendFile(file: any, type: MessageType.Image | MessageType.File) {
private async sendFile(
file: any,
type: MessageType.Image | MessageType.File
) {
const src = JSON.parse(
file.attributes[`data-${type}`]?.value || ""
) as {
......@@ -121,7 +142,13 @@ export default class MessageInput extends Vue {
};
img.remove();
}
uploadFile(file)
return uploadFile(
file,
(p: number) =>
type === MessageType.File &&
this.chatInput &&
this.chatInput.updateUploadProgress(p)
)
.then((r) => {
if (r) {
const msg = {
......@@ -143,6 +170,7 @@ export default class MessageInput extends Vue {
});
this.removeSendingMessages(index);
URL.revokeObjectURL(src.url);
return index;
} else {
this.setMsg2Failed(index);
}
......@@ -152,6 +180,8 @@ export default class MessageInput extends Vue {
console.error(e);
this.setMsg2Failed(index);
this.chatInput &&
this.chatInput.updateUploadProgress(0);
});
}
}
......@@ -188,5 +218,5 @@ export default class MessageInput extends Vue {
private onError(e: any) {
this.$emit("error", e.message || e);
}
}
}
</script>
<template>
<div
class="msg-detail inline-text"
v-html="format2Link(messageBody.msg.text || emptyText)"
></div>
</template>
<script lang="ts">
import { replaceText2Link } from "@/customer-service/utils";
import xim from "@/customer-service/xim";
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {
protected readonly emptyText = " ";
protected format2Link(text: string) {
let t = replaceText2Link(text);
const keywords = xim.getMatchedTextKeywords();
for (const item of keywords) {
const r = new RegExp(item, "g");
t = t.replace(r, `<span class="highlight">${item}</span>`);
}
return t;
}
}
</script>
<style lang="less" scoped>
.inline-text {
display: inline-block;
white-space: pre-wrap;
text-align: left;
color: #409eff;
/deep/ .highlight {
color: #e87005;
}
}
</style>
\ No newline at end of file
<template>
<div>
<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' }"
>
......@@ -22,18 +22,24 @@
title="[语音加载失败]"
></i>
</div>
<text-message v-model="value" v-if="backend" />
</div>
</template>
<script lang="ts">
import { Component, Ref } from "vue-property-decorator";
import BaseMessage from "./index";
import VoiceIcon from "./voice.vue";
import TextMessage from "./text-message.vue";
import Chat from "@/customer-service/xim";
@Component({ components: { VoiceIcon } })
@Component({ components: { VoiceIcon, TextMessage } })
export default class Index extends BaseMessage {
@Ref("audio")
private readonly audioRef!: HTMLAudioElement;
private readonly backend = Chat.isBackend();
private playing = false;
private get duration() {
......@@ -86,8 +92,10 @@
<style lang="less" scoped>
.voice-message {
height: 40px;
width: 200px;
background-color: #eee;
border-radius: 6px;
padding: 8px 10px;
&.can-play {
cursor: pointer;
......@@ -97,6 +105,7 @@
font-size: 16px;
}
}
.my-message {
.voice-message {
> div {
......
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;
}
/**
* 最大图片文件大小
*/
......@@ -196,6 +214,7 @@ export const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
export const MAX_IMAGE_SIZE_STRING = "5MB";
export const MESSAGE_IMAGE_TOO_LARGE = `您发送的图片大小超过 ${MAX_IMAGE_SIZE_STRING}。`;
export const MESSAGE_FILE_EMPTY = "不能发送空文件。";
export const ERROR_IMAGE = "发送的不是图片";
/**
* 最大文件大小
......
<template>
<div class="msg-detail file-message d-flex" @dblclick="openFile">
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
class="msg-detail file-message d-flex"
@dblclick="openFile"
@click="download"
>
<div
class="file-message-info"
:class="{ 'd-flex align-items-center': !messageBody.msg.size }"
>
<div
class="file-message-name"
:title="messageBody.msg.name"
:class="{ 'text-truncate': messageBody.msg.size }"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
<div class="text-hint" v-if="messageBody.msg.size">
{{ format(messageBody.msg.size) }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
<a
class="
d-flex
align-items-center
justify-content-center
download-icon
"
:href="messageRealUrl"
:download="getAttachment"
title="下载文件"
>
<img src="~@/customer-service/imgs/download.png" alt="Download" />
</a>
<file-icon :value="fileIcon"></file-icon>
</div>
</template>
......@@ -61,14 +56,7 @@
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private 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);
}
......@@ -76,7 +64,11 @@
return FileType.Others;
}
private format(v: number) {
private download() {
window.open(this.messageRealUrl);
}
protected format(v: number) {
return formatSize(v);
}
......@@ -88,11 +80,17 @@
<style lang="less" scoped>
.file-message {
background-color: transparent !important;
border-radius: 4px !important;
border: 1px solid #c5d4e5;
background-color: #f5f6fa;
border-radius: 6px !important;
cursor: pointer;
&.my-message {
background-color: #dbf2ff;
}
.file-message-name {
max-width: 130px;
word-break: break-all;
}
}
</style>
<template>
<div
class="msg-detail image-message"
class="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,17 +39,24 @@
mounted() {
this.buildMessageUrl();
}
private dbClick() {
!this.mobile && this.openFile();
}
private open() {
this.mobile && this.openFile();
}
}
</script>
<style lang="less" scoped>
.image-message {
background-color: transparent !important;
border-radius: 4px !important;
border: 1px solid #c5d4e5;
line-height: 1;
max-width: 300px;
box-sizing: content-box;
background-color: unset;
img {
width: 100%;
}
......@@ -62,11 +75,4 @@
margin-left: 0;
}
}
.my-message {
&.image-message:not(.image-404) {
background-color: transparent !important;
border-radius: 4px !important;
border: 1px solid #c5d4e5;
}
}
</style>
......@@ -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="notifyMessage"></div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {
private get notifyData() {
return this.messageBody.msg as {
text: string;
remark: string;
resultId: string;
};
}
private get notifyMessage() {
return this.notifyData
? this.notifyData.text ||
`${this.notifyData.remark}, 工单ID ${
this.notifyData.resultId || "无"
}`
: "[通知消息]";
}
}
</script>
<style lang="less" scoped>
.inline-text {
display: inline-block;
white-space: pre-wrap;
text-align: left;
/deep/ .highlight {
color: #e87005;
}
}
</style>
\ No newline at end of file
<?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">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
<stop stop-color="#F95B3C" offset="0%"></stop>
<stop stop-color="#F94623" offset="100%"></stop>
</linearGradient>
</defs>
<g id="专项订单" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="专项订单-结束订单的咨询-通知" transform="translate(-88.000000, -204.000000)">
<g id="交易卡片类型" transform="translate(12.000000, 20.000000)">
<g id="扣费类型-copy-3" transform="translate(0.000000, 166.000491)">
<g id="客服-待扣费" transform="translate(56.000000, 0.000000)">
<g id="对话内容">
<g id="2" transform="translate(124.000000, 53.000491) scale(-1, 1) translate(-124.000000, -53.000491) ">
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
<g id="1" transform="translate(124.000000, 38.000246) scale(-1, 1) translate(-124.000000, -38.000246) ">
<use fill="url(#linearGradient-3)" xlink:href="#path-4"></use>
<use fill-opacity="0.3" fill="#FFFFFF" xlink:href="#path-4"></use>
</g>
</g>
<g id="Group-2" transform="translate(20.000000, 18.000000)" stroke="#FFFFFF" stroke-linejoin="round" stroke-width="2">
<path d="M20,39 C30.4934102,39 39,30.4934102 39,20 C39,9.50658975 30.4934102,1 20,1 C9.50658975,1 1,9.50658975 1,20 C1,30.4934102 9.50658975,39 20,39 Z M10,20 L16.22774,28.0070942 C16.56681,28.4430414 17.1950856,28.5215759 17.6310328,28.1825058 C17.6909399,28.1359114 17.745348,28.0826486 17.7932062,28.0237462 L30,13 L30,13" id="Oval-3"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<template>
<div
class="pay-message d-flex flex-column"
:class="[
messageClass,
{ 'user-side': !backend || !isChatMember, click: isChatMember },
]"
@click="view"
>
<div class="d-flex align-items-center flex-fill pay-msg-body">
<span :class="icon" class="icon flex-none"></span>
<div class="texts flex-fill overflow-hidden">
<div class="text-left text-nowrap text-truncate">
{{ title }}
</div>
<div class="d-flex justify-content-between">
<span>{{ amount | currency }}</span>
<span>{{ status }}</span>
</div>
</div>
</div>
<div class="pay-method text-left">{{ method }}</div>
</div>
</template>
<script lang="ts">
import {
PayMethod,
payMethodMapping,
PayStatus,
payStatusMapping,
cardPayStatusMapping,
} from "@/customer-service/model";
import { PayMessageBody } from "@/customer-service/xim/models/chat";
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import Chat from "@/customer-service/xim";
import { ChatStore, chatStore } from "@/customer-service/store/model";
import { CustomerServiceEvent, MessageEvent } from "@/customer-service/event";
@Component({ components: {} })
export default class Index extends BaseMessage {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER)
private readonly isChatMember!: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER;
protected backend = Chat.isBackend();
private get payData() {
return this.messageBody.msg as PayMessageBody;
}
private get title() {
return this.payData.itemName || this.payData.totalName;
}
private get amount() {
return +this.payData.amount || this.payData.totalMoney;
}
private get status() {
// 银行卡转账
if (+this.payData.paymentFunction === PayMethod.CardTransfer) {
return cardPayStatusMapping.get(+this.payData.status);
}
return payStatusMapping.get(+this.payData.status);
}
private get method() {
return payMethodMapping.get(+this.payData.paymentFunction);
}
private get icon() {
const m = +this.payData.paymentFunction;
const s = +this.payData.status;
if (m === PayMethod.Balance && s === PayStatus.UnPay) {
return "icon-1";
}
if (m === PayMethod.CardTransfer && s === PayStatus.UnPay) {
return "icon-2";
}
if (s === PayStatus.WaitRefund) {
return "icon-3";
}
return "completed";
}
private get messageClass() {
const m = +this.payData.paymentFunction;
const s = +this.payData.status;
if (m === PayMethod.Balance) {
if (s === PayStatus.UnPay) {
return "balance-unpay";
}
if (s === PayStatus.Paied) {
return "balance-paied";
}
}
if (m === PayMethod.CardTransfer) {
if (s === PayStatus.UnPay) {
return "card-unpay";
}
if (s === PayStatus.Paied) {
return "card-paied";
}
}
if (s === PayStatus.WaitRefund) {
return "refund-unpay";
}
if (s === PayStatus.Refund) {
return "refund-paied";
}
return "default";
}
private view() {
this.isChatMember &&
CustomerServiceEvent.open(
this,
MessageEvent.PayMessage,
+this.payData.paymentId
);
}
}
</script>
<style lang="less" scoped>
.pay-message {
border-radius: 10px 0 10px 10px;
width: 248px;
height: 106px;
color: #fff;
&.click {
cursor: pointer;
}
&.user-side {
border-radius: 0 10px 10px 10px;
}
&.default,
&.balance-unpay {
background: linear-gradient(180deg, #f95b3c 0%, #f94623 100%);
}
&.balance-paied {
background: linear-gradient(180deg, #f95b3c 0%, #f94623 100%)
rgba(255, 255, 255, 0.3);
}
&.card-unpay {
background: linear-gradient(180deg, #ff884d 0%, #ff7a38 100%);
}
&.card-paied {
background: linear-gradient(180deg, #ff884d 0%, #ff7a38 100%)
rgba(255, 255, 255, 0.3);
}
&.refund-unpay {
background: linear-gradient(180deg, #faad14 0%, #faa40f 100%);
}
&.refund-paied {
background: linear-gradient(180deg, #faad14 0%, #faa40f 100%)
rgba(255, 255, 255, 0.3);
}
.pay-msg-body {
padding: 20px;
}
.pay-method {
background-color: #fff;
color: #8e8d99;
padding: 5px 10px;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.06);
}
.icon {
width: 40px;
height: 40px;
display: inline-block;
margin-right: 20px;
background-repeat: no-repeat;
&.icon-1 {
background-image: url("./pay-status-1.svg");
}
&.icon-2 {
background-image: url("./pay-status-2.svg");
}
&.icon-3 {
background-image: url("./pay-status-3.svg");
}
&.completed {
background-image: url("./pay-completed.svg");
}
}
.texts {
line-height: 1.5;
div:last-child {
margin-left: -2px;
}
}
}
</style>
\ No newline at end of file
<?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">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-88.000000, -320.000000)">
<g transform="translate(12.000000, 20.000000)">
<g transform="translate(0.000000, 282.000982)">
<g transform="translate(56.000000, 0.000000)">
<g>
<path d="M10,0 L248,0 L248,0 L248,76.0004911 L0,76.0004911 L0,10 C-2.45271059e-15,4.4771525 4.4771525,1.01453063e-15 10,0 Z" id="1" fill="url(#linearGradient-3)" transform="translate(124.000000, 38.000246) scale(-1, 1) translate(-124.000000, -38.000246) "></path>
</g>
<g transform="translate(20.000000, 18.000000)">
<path d="M38.0401212,26.4966147 C34.4139232,36.354512 23.3977849,41.4372833 13.4348876,37.8493021 C8.06509634,35.9154559 4.09724597,31.8574701 2.14778094,26.9833815 L2.16352869,27.0357731 C1.61303033,25.6706127 1.21797399,24.227429 1,22.7276344 L4.6900288,24.0565404 M1.96127879,13.5033853 C5.58747682,3.64548801 16.6036151,-1.43728327 26.5665124,2.15069793 C31.3994686,3.89121133 35.1565883,7.49572098 37.8378713,12.9642269 C38.3871931,14.3264697 38.7817367,15.7664026 39,17.2627503 L35.3172901,15.9364801" id="Path" stroke="#FFFFFF" stroke-width="2" stroke-linecap="square"></path>
<text id="转" font-family="PingFang-SC-Medium, PingFang SC" font-size="18" font-weight="400" line-spacing="18" fill="#FFFFFF">
<tspan x="11" y="26"></tspan>
</text>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?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">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-88.000000, -320.000000)">
<g transform="translate(12.000000, 20.000000)">
<g transform="translate(0.000000, 282.000982)">
<g transform="translate(56.000000, 0.000000)">
<g>
<path d="M10,0 L248,0 L248,0 L248,76.0004911 L0,76.0004911 L0,10 C-2.45271059e-15,4.4771525 4.4771525,1.01453063e-15 10,0 Z" id="1" fill="url(#linearGradient-3)" transform="translate(124.000000, 38.000246) scale(-1, 1) translate(-124.000000, -38.000246) "></path>
</g>
<g transform="translate(20.000000, 18.000000)">
<path d="M38.0401212,26.4966147 C34.4139232,36.354512 23.3977849,41.4372833 13.4348876,37.8493021 C8.06509634,35.9154559 4.09724597,31.8574701 2.14778094,26.9833815 L2.16352869,27.0357731 C1.61303033,25.6706127 1.21797399,24.227429 1,22.7276344 L4.6900288,24.0565404 M1.96127879,13.5033853 C5.58747682,3.64548801 16.6036151,-1.43728327 26.5665124,2.15069793 C31.3994686,3.89121133 35.1565883,7.49572098 37.8378713,12.9642269 C38.3871931,14.3264697 38.7817367,15.7664026 39,17.2627503 L35.3172901,15.9364801" id="Path" stroke="#FFFFFF" stroke-width="2" stroke-linecap="square"></path>
<text id="转" font-family="PingFang-SC-Medium, PingFang SC" font-size="18" font-weight="400" line-spacing="18" fill="#FFFFFF">
<tspan x="11" y="26"></tspan>
</text>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?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">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-88.000000, -320.000000)">
<g transform="translate(12.000000, 20.000000)">
<g transform="translate(0.000000, 282.000982)">
<g transform="translate(56.000000, 0.000000)">
<g>
<path d="M10,0 L248,0 L248,0 L248,76.0004911 L0,76.0004911 L0,10 C-2.45271059e-15,4.4771525 4.4771525,1.01453063e-15 10,0 Z" id="1" fill="url(#linearGradient-3)" transform="translate(124.000000, 38.000246) scale(-1, 1) translate(-124.000000, -38.000246) "></path>
</g>
<g transform="translate(20.000000, 18.000000)">
<path d="M38.0401212,26.4966147 C34.4139232,36.354512 23.3977849,41.4372833 13.4348876,37.8493021 C8.06509634,35.9154559 4.09724597,31.8574701 2.14778094,26.9833815 L2.16352869,27.0357731 C1.61303033,25.6706127 1.21797399,24.227429 1,22.7276344 L4.6900288,24.0565404 M1.96127879,13.5033853 C5.58747682,3.64548801 16.6036151,-1.43728327 26.5665124,2.15069793 C31.3994686,3.89121133 35.1565883,7.49572098 37.8378713,12.9642269 C38.3871931,14.3264697 38.7817367,15.7664026 39,17.2627503 L35.3172901,15.9364801" id="Path" stroke="#FFFFFF" stroke-width="2" stroke-linecap="square"></path>
<text id="转" font-family="PingFang-SC-Medium, PingFang SC" font-size="18" font-weight="400" line-spacing="18" fill="#FFFFFF">
<tspan x="11" y="26">退</tspan>
</text>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>bg_head</title>
<defs>
<circle id="path-1" cx="20" cy="20" r="20"></circle>
<filter x="-17.5%" y="-12.5%" width="135.0%" height="135.0%" filterUnits="objectBoundingBox" id="filter-3">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0.846 0 0 0 0 0.8712 0 0 0 0 0.9 0 0 0 1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-4">
<stop stop-color="#FF884D" offset="0%"></stop>
<stop stop-color="#FF7A38" offset="100%"></stop>
</linearGradient>
</defs>
<g id="专项订单" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="专项订单-结束订单的咨询-通知" transform="translate(-12.000000, -302.000000)">
<g id="交易卡片类型" transform="translate(12.000000, 20.000000)">
<g id="扣费类型-copy" transform="translate(0.000000, 282.000982)">
<g id="deal_payment" transform="translate(0.000000, 0.000491)">
<g id="bg_head" transform="translate(4.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Oval-Copy-3">
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-1"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
<g id="Group-2" mask="url(#mask-2)" stroke="url(#linearGradient-4)" stroke-linecap="square" stroke-width="1.5">
<g transform="translate(10.000000, 10.000000)" id="Path">
<path d="M18.7826906,13.1628256 C17.0173047,17.962065 11.6541847,20.4365721 6.80382687,18.6897918 C4.18958638,17.7483141 2.25786975,15.7727157 1.30878809,13.3998042 L1.31645476,13.4253106 C1.04844898,12.760693 0.856118915,12.0580905 0.75,11.3279273 L2.54646139,11.9748947 M1.21799099,6.83717444 C2.98337687,2.03793495 8.34649684,-0.436572119 13.1968547,1.3102082 C15.5497413,2.15756341 17.3788653,3.91239048 18.6842268,6.57468939 C18.9516598,7.23788655 19.1437402,7.93890651 19.25,8.66739159 L17.4571018,8.0217074 M6.91666667,8.76666667 L13.0833333,8.76666667 M6.91666667,11.2333333 L13.0833333,11.2333333 M10,13.7 L10,8.76666667 M10,8.76666667 L12.4666667,6.3 M10,8.76666667 L7.53333333,6.3"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<template>
<div class="position-message" @click="openPosition">
<div class="d-flex justify-content-between align-items-center">
<span class="d-flex align-items-center flex-fill">
<span class="title flex-fill">{{ title }}</span>
<span
v-for="item in tags"
:key="item.title"
:style="{ 'background-color': item.color }"
class="tag"
>{{ item.title }}</span
>
</span>
<span class="salary">{{ salary }}</span>
</div>
<div class="summary">
<span v-for="item in summary" :key="item">{{ item }}</span>
</div>
<div class="msg-content" v-html="positionBody"></div>
<div class="msg-tail d-flex justify-content-between">
<span v-for="item in tail" :key="item" class="text-truncate">{{
item
}}</span>
</div>
</div>
</template>
<script lang="ts">
import { CustomerServiceEvent, MessageEvent } from "@/customer-service/event";
import { PositionMessage } from "@/customer-service/xim/models/chat";
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {
private get positionData() {
return this.messageBody.msg as PositionMessage;
}
private get title() {
return this.positionData.title;
}
private get tags() {
return this.positionData.tags;
}
private get salary() {
const max = this.positionData.max_salary;
const min = this.positionData.min_salary;
const formatSalary = (v: number, type?: "Y" | "K") => {
if (type === "K") {
return {
v: parseFloat((v / 1000).toFixed(2)),
unit: "K",
};
}
if (+v < 1000) {
return {
v,
unit: "",
};
} else {
return {
v: parseFloat((v / 1000).toFixed(2)),
unit: "K",
};
}
};
if (!max && !min) {
return "面议";
}
if (+min >= +max) {
const v = formatSalary(min);
return `${v.v}${v.unit}`;
}
const formatMin = formatSalary(min);
const formatMax = formatSalary(max);
if (formatMin.unit === formatMax.unit) {
return `${formatMin.v}-${formatMax.v} ${formatMax.unit}`;
} else {
const formatMin = formatSalary(min, "K");
const formatMax = formatSalary(max, "K");
return `${formatMin.v}-${formatMax.v} K`;
}
}
private get positionBody() {
return this.positionData.post_require;
}
private get summary() {
return [
this.positionData.address,
this.positionData.education_require,
this.positionData.recruit_count,
];
}
private get tail() {
return [
this.positionData.company_name,
this.positionData.business_scope !== '0'
? this.positionData.business_scope
: "",
];
}
private openPosition() {
CustomerServiceEvent.open(
this,
MessageEvent.PositionMessage,
this.positionData.post_id
);
}
}
</script>
<style lang="less" scoped>
.position-message {
border: 1px solid #ccc;
border-radius: 10px;
padding: 10px;
max-width: 300px;
cursor: pointer;
}
.title {
font-size: 16px;
font-weight: 500;
}
.tag {
border-radius: 4px;
padding: 0 2px;
margin-left: 10px;
color: #fff;
font-size: 12px;
}
.salary {
color: #e87005;
margin-left: 50px;
}
.summary {
border-bottom: 1px solid #f0f0f0;
margin: 6px -10px 10px -10px;
font-size: 12px;
color: #999;
padding: 0 10px 8px 10px;
span + span {
margin-left: 20px;
}
}
.msg-content {
line-height: 1.4;
}
.msg-tail {
margin: 15px -10px 0 -10px;
padding: 10px 10px 0 10px;
border-top: 1px solid #f0f0f0;
span {
max-width: 200px;
}
}
</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) {
......
......@@ -13,20 +13,20 @@
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import VideoPlayerIcon from "./video-player-icon.vue";
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import VideoPlayerIcon from "./video-player-icon.vue";
@Component({ components: { VideoPlayerIcon } })
export default class Index extends BaseMessage {
@Component({ components: { VideoPlayerIcon } })
export default class Index extends BaseMessage {
mounted() {
this.buildMessageUrl();
}
}
}
</script>
<style lang="less" scoped>
.video-message {
.video-message {
height: 160px;
width: 200px;
background-color: #000 !important;
......
......@@ -2,49 +2,51 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
t="1595840909244"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
p-id="11656"
:width="size"
:height="size"
>
<path
d="M56.888889 512a105.016889 102.4 90 1 0 204.8 0 105.016889 102.4 90 1 0-204.8 0Z"
fill="#8D959D"
p-id="11657"
:fill="fill"
v-if="!status || status >= 1"
/>
<path
d="M425.415111 782.449778a68.266667 68.266667 0 0 1-97.792-95.288889A249.912889 249.912889 0 0 0 398.222222 512c0-66.787556-25.713778-129.137778-70.542222-175.160889a68.266667 68.266667 0 0 1 97.735111-95.288889A386.389333 386.389333 0 0 1 534.755556 512c0 102.627556-39.822222 199.111111-109.340445 270.449778z"
fill="#8D959D"
p-id="11658"
:fill="fill"
v-if="!status || status >= 2"
/>
<path
d="M618.496 980.48a68.266667 68.266667 0 0 1-97.792-95.288889A532.707556 532.707556 0 0 0 671.288889 512a532.707556 532.707556 0 0 0-150.584889-373.191111A68.266667 68.266667 0 0 1 618.496 43.52 669.184 669.184 0 0 1 807.822222 512c0 177.891556-68.835556 344.917333-189.326222 468.48z"
fill="#8D959D"
p-id="11659"
:fill="fill"
v-if="!status || status >= 3"
/>
</svg>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
@Component({ components: {} })
export default class VoiceIcon extends Vue {
@Component({ components: {} })
export default class VoiceIcon extends Vue {
@Prop({ default: 25 })
private size!: number;
@Prop()
private loading!: boolean;
@Prop()
private readonly white!: boolean;
private status = 0;
private interval = 0;
private get fill() {
return this.white ? "#fff" : "#8D959D";
}
@Watch("loading")
private onLoadingChanged() {
if (this.loading) {
......@@ -65,5 +67,5 @@ export default class VoiceIcon extends Vue {
beforeDestroy() {
clearInterval(this.interval);
}
}
}
</script>
......@@ -11,7 +11,7 @@
</script>
<style lang="less" scoped>
.my-message {
.msg-content {
.withdraw-message {
background-color: transparent !important;
padding: 0 4px;
......
......@@ -24,6 +24,7 @@
:shape="shape"
@open="open"
@withdraw="refresh"
@open-message="openMessage"
/>
</div>
</template>
......@@ -38,21 +39,20 @@
</template>
<script lang="ts">
import { Component, Prop, Ref, Vue, Watch } from "vue-property-decorator";
import { Message, MessageType } from "../model";
import { throttle } from "../utils";
import { formatTime } from "../utils/time";
import ImagePreview from "./image-preview.vue";
import message from "./message.vue";
import VideoPreview from "./video-preview.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model";
import { dbController } from "../database";
@Component({ components: { message, ImagePreview, VideoPreview } })
export default class MessageList extends Vue {
import { Component, Prop, Ref, Vue, Watch } from "vue-property-decorator";
import { Message, MessageType } from "../model";
import { throttle } from "../utils";
import { formatTime } from "../utils/time";
import ImagePreview from "./image-preview.vue";
import message from "./message.vue";
import VideoPreview from "./video-preview.vue";
import { ChatStore, chatStore } from "@/customer-service/store/model";
import { dbController } from "../database";
import { getLastMessageId } from "../store";
import { CustomerServiceEvent } from "../event";
@Component({ components: { message, ImagePreview, VideoPreview } })
export default class MessageList extends Vue {
@chatStore.Getter(ChatStore.STATE_CHAT_MSG_HISTORY)
private readonly historyMessage!: ChatStore.STATE_CHAT_MSG_HISTORY;
......@@ -71,9 +71,6 @@ export default class MessageList extends Vue {
@chatStore.Action(ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID)
private readonly getNextPageMsg!: ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID;
@chatStore.Action(ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA)
private readonly clearChatId!: ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA;
@chatStore.Mutation(ChatStore.MUTATION_SAVE_FUNC_SCROLL_TO_BOTTOM)
private readonly saveScrollToBottomFunc!: ChatStore.MUTATION_SAVE_FUNC_SCROLL_TO_BOTTOM;
......@@ -89,22 +86,25 @@ export default class MessageList extends Vue {
@chatStore.Mutation(ChatStore.MUTATION_WITHDRAW)
private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW;
@chatStore.Action(ChatStore.ACTION_SET_CHAT_ERROR)
private readonly setError!: ChatStore.ACTION_SET_CHAT_ERROR;
@Prop({ default: "circle" })
private shape!: string;
private get messages() {
if (this.historyMessage) {
if (this.sendingMessages) {
if (this.sendingMessages && this.sendingMessages.length) {
return [...this.historyMessage, ...this.sendingMessages].filter(
(i) => i.chat_id === this.chatId
(i) => i.chat_id === this.chatId && i.id > 0
);
}
return this.historyMessage;
}
if (this.sendingMessages) {
if (this.sendingMessages && this.sendingMessages.length) {
return this.sendingMessages.filter(
(i) => i.chat_id === this.chatId
(i) => i.chat_id === this.chatId && i.id > 0
);
}
......@@ -150,6 +150,13 @@ export default class MessageList extends Vue {
}
}
@Watch("chatId")
private onChatChanged(o: number, n: number) {
o && n && this.messages.length
? this.fetchNewMsg()
: setTimeout(() => this.fetchNewMsg(), 300);
}
private raiseFileOpen(value: boolean) {
this.$emit("file-open", value);
}
......@@ -182,14 +189,6 @@ export default class MessageList extends Vue {
public created() {
this.handleScrollWrapper();
this.onNewMessage((e) => {
if (e.type === MessageType.Withdraw) {
this.executeWithDraw(e.ref_id);
dbController
.removeMessage(e.chat_id, e.ref_id)
.finally(() => this.refresh());
}
});
}
public mounted() {
......@@ -198,7 +197,7 @@ export default class MessageList extends Vue {
this.saveScrollToBottomFunc(this.scrollToNewMsg);
this.scrollToNewMsg();
setTimeout(() => this.scroll2End(200));
setTimeout(() => this.scroll2End(1000));
setTimeout(() => this.scroll2End(1000), 200);
}
public beforeDestroy() {
......@@ -206,7 +205,6 @@ export default class MessageList extends Vue {
this.scollWrapper.removeEventListener("scroll", this.handleScroll);
this.clearScrollToBottomFunc();
this.clearNewMessage();
// this.clearChatId();
}
public scroll2End(delay?: number) {
......@@ -326,14 +324,46 @@ export default class MessageList extends Vue {
if (msg == null) return;
if (msg.length === 0) return;
this.startLoadingNew();
const msgId = msg[msg.length - 1].id;
const data = await this.getNextPageMsg(msgId);
const msgId = getLastMessageId(msg);
return this.getNextPageMsg(msgId)
.then((data) => {
if (data.length === 0) {
// eslint-disable-next-line no-console
console.log("没有更多新消息了");
this.scroll2End();
} else {
setTimeout(() => this.fetchNewMsg(), 200);
}
const removingIds: number[] = [];
for (const item of data) {
if (item.type === MessageType.Withdraw) {
item.msg && removingIds.push(...JSON.parse(item.msg));
}
}
if (removingIds.length) {
dbController
.removeMessage(this.chatId, removingIds)
.then(() => {
this.refresh();
this.executeWithDraw(removingIds);
});
}
this.$emit("next-page", msgId);
this.endLoadingNew();
})
.catch((e) => {
if (
e &&
e.message &&
(e.message as string).includes("sql: no rows in result set")
) {
this.setError(this.chatId as number);
}
})
.finally(() => this.endLoadingNew());
}
private format2Time(time: number) {
......@@ -378,23 +408,29 @@ export default class MessageList extends Vue {
private refresh() {
this.fetchNewMsg();
}
}
private openMessage(o: any) {
CustomerServiceEvent.emit(this, o);
}
}
</script>
<style lang="less" scoped>
.message-list {
.message-list {
padding: 0 20px;
padding-right: 0;
}
}
.loading-mask {
.loading-mask {
height: 50px;
line-height: 50px;
text-align: center;
}
}
.message-template {
.message-template {
&:first-child {
padding-top: 10px;
.timestamp {
margin-top: 20px;
}
......@@ -408,5 +444,5 @@ export default class MessageList extends Vue {
font-size: 12px;
user-select: none;
}
}
}
</style>
......@@ -2,29 +2,71 @@
<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,
}"
@mouseenter="hoverWithdraw"
>
<div class="msg-content" :class="{ 'algin-left': !isMyMessage }">
<div
class="msg-name no-selection"
:class="{ 'algin-left': !isMyMessage }"
v-if="!isWithdrawMessage"
v-if="
!isWithdrawMessage &&
!isQuestionAnswerMessage &&
needReadTip
"
>
{{ userName }}
</div>
<div class="d-flex" :class="{ 'justify-content-end': isMyMessage }">
<template
v-if="
backend &&
showReadSummary &&
!isWithdrawMessage &&
needReadTip
"
>
<div v-if="isMyMessage" class="msg-read pos-rel">
<span
@click="openReaderList"
:class="[
isAllRead ? 'all' : 'not-all',
{ pointer: isChatMember },
]"
>
<template v-if="isAllRead">
<i
class="el-icon-circle-check"
title="全部已读"
></i>
</template>
<template
v-else-if="manualReaded || data.read_count"
>{{
manualReaded || data.read_count
}}人已读</template
>
<template v-else>未读</template>
</span>
<who-read-list
v-if="readListVisibility"
@blur="readListVisibility = false"
:msgId="data.id"
:class="{ offset: readerListOffset }"
/>
</div>
</template>
<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"
class="host-avatar"
/>
<component
:is="messageComponent"
:user-name="userName"
......@@ -34,8 +76,14 @@
v-if="messageComponent"
v-model="data"
@open="openFile"
@open-message="openMessage"
/>
<avatar
v-if="!isQuestionAnswerMessage && !isWithdrawMessage"
:src="avatar || defaultAvatar"
shape="circle"
/>
<avatar v-if="avatar" :src="avatar" shape="circle" />
</div>
</div>
</div>
......@@ -46,31 +94,16 @@
></i>
<i class="el-icon-loading" v-else-if="isSendingMessage"></i>
<template v-if="backend && showReadSummary && !isWithdrawMessage">
<div v-if="isMyMessage" class="msg-read pos-rel">
<span
@click="openReaderList"
class="pointer"
:class="{ all: isAllRead }"
>
<template v-if="isAllRead">全部已读</template>
<template v-else-if="data.read_count > 0"
>{{ data.read_count }}人已读</template
>
<template v-else>未读</template>
</span>
<who-read-list
v-if="readListVisibility"
@blur="readListVisibility = false"
:msgId="data.id"
:class="{ offset: readerListOffset }"
/>
</div>
</template>
<span
class="withdraw"
v-if="isMyMessage && canWithdraw && !isWithdrawMessage"
v-if="
isMyMessage &&
canWithdraw &&
isWithdraw &&
!isWithdrawMessage &&
!isQuestionAnswerMessage &&
isChatMember
"
@click="withdraw"
>撤回此消息</span
>
......@@ -108,11 +141,8 @@
</template>
<script lang="ts">
import { Component, Inject, Mixins, Prop } from "vue-property-decorator";
import { Filters } from "../mixin/filter";
import { Component, Inject, Prop, Vue } from "vue-property-decorator";
import * as dto from "../model";
import chat from "./../xim";
import {
isAudio,
......@@ -121,23 +151,30 @@
MAX_FILE_SIZE,
MAX_IMAGE_SIZE,
} from "./message-item/file-controller";
import WhoReadList from "./who-read-list.vue";
import avatar from "@/customer-service/components/avatar.vue";
import { chatStore, ChatStore } from "@/customer-service/store/model";
import ximInstance from "../xim/xim";
import { dbController } from "../database";
import ImageMessage from "./message-item/image-message.vue";
import FileMessage from "./message-item/file-message.vue";
import AudioMessage from "./message-item/audio-message.vue";
import VideoMessage from "./message-item/video-message.vue";
import TextMessage from "./message-item/text-message.vue";
import ActionMessage from "./message-item/action-message.vue";
import WithdrawMessage from "./message-item/withdraw-message.vue";
import xim from "./../xim";
const twoMinutes = 2 * 60 * 1000;
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 PositionMessage from "./message-item/position-message.vue";
import PayMessage from "./message-item/pay-message.vue";
import NotifyMessage from "./message-item/notify-message.vue";
import { ChatRole } from "@/customer-service/model";
import { ChatUserInfoService, getUserMapping } from "../utils/user-info";
import Xim from "@/customer-service/xim";
import { CustomerServiceEvent, MessageEvent } from "../event";
import { PayMessageBody } from "../xim/models/chat";
const oneDay = 24 * 60 * 60 * 1000;
const messageMapping = new Map<dto.MessageType, string>([
[dto.MessageType.Image, "image-message"],
......@@ -147,6 +184,17 @@
[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"],
[dto.MessageType.Pay, "pay-message"],
[dto.MessageType.PayV1, "pay-message"],
[dto.MessageType.Refund, "pay-message"],
[dto.MessageType.RefundV1, "pay-message"],
[dto.MessageType.PayResult, "notify-message"],
[dto.MessageType.Notify, "notify-message"],
[dto.MessageType.Position, "position-message"],
]);
@Component({
......@@ -159,27 +207,40 @@
VideoMessage,
TextMessage,
WithdrawMessage,
PurchasePlanMessage,
MyWelfareMessage,
QuestionAnswerMessage,
ActionMessage,
PayMessage,
NotifyMessage,
PositionMessage,
},
})
export default class Message extends Mixins(Filters) {
export default class Message extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_USER_UID)
private readonly chatMyId!: ChatStore.STATE_CHAT_CURRENT_USER_UID;
@chatStore.State(ChatStore.STATE_ALL_HISTORY_CHAT_MEMBERS)
private readonly allChatMembers!: ChatStore.STATE_ALL_HISTORY_CHAT_MEMBERS;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
@chatStore.State(ChatStore.STATE_CHAT_SOURCE)
private readonly chatSource!: ChatStore.STATE_CHAT_SOURCE;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.Mutation(ChatStore.MUTATION_WITHDRAW)
private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW;
@chatStore.Action(ChatStore.ACTION_SET_HANDLED)
private readonly setHandled!: ChatStore.ACTION_SET_HANDLED;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CURRENT_CHAT)
private readonly currentChat!: ChatStore.GETTER_CURRENT_CURRENT_CHAT;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER)
private readonly isChatMember!: ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER;
@chatStore.Action(ChatStore.ACTION_UPDATE_MESSAGE_READ_STATUS)
private readonly updateMessage!: ChatStore.ACTION_UPDATE_MESSAGE_READ_STATUS;
@Prop({ type: Object, default: () => Object.create(null) })
private readonly data!: dto.Message;
......@@ -201,30 +262,76 @@
private readListVisibility = false;
private org = "";
private manualAllRead = false;
private manualReaded = 0;
private refetchUsername = "";
private refetchUserIcon = "";
private readerListOffset = false;
private defaultMessageHandledStatus = dto.MessageHandled.Default;
private showHostAvatar: boolean = false;
private isWithdraw = true;
private get isSystemMessage() {
return (
this.messageBody &&
this.messageBody.eid &&
+this.messageBody.eid === 0
);
}
private get isPayMessage() {
return dto.MessageTypeController.isPayMessage(this.data.type);
}
private get canWithdraw() {
if (this.backend && this.data) {
return new Date().valueOf() - this.data.ts * 1000 < twoMinutes;
if (this.isPayMessage) {
return true;
}
if (this.needReadTip && this.isMyMessage) {
return new Date().valueOf() - this.data.ts * 1000 < oneDay;
}
}
return false;
}
private get needReadTip() {
if (this.isSystemMessage) {
return false;
}
return (
this.data.type !== dto.MessageType.Pay &&
this.data.type !== dto.MessageType.Refund &&
this.data.type !== dto.MessageType.PayV1 &&
this.data.type !== dto.MessageType.RefundV1 &&
this.data.type !== dto.MessageType.PayResult
);
}
private get isWithdrawMessage() {
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;
return (
this.manualAllRead ||
this.data.read_count >= this.data.total_read_count
);
}
private get messageBody(): { eid?: string; oid?: string; msg: any } {
if (this.data) {
const msg = this.data.msg;
try {
if (msg.startsWith("{")) {
return { ...this.data, msg: JSON.parse(this.data.msg) };
}
return { ...this.data, msg: this.data.msg };
} catch {
return {
...this.data,
......@@ -248,53 +355,106 @@
return true;
}
const senderEid = this.messageBody.eid;
return senderEid!.toString() === this.chatMyId!.toString();
// 系统推送的消息或老用户(一般是客服,eid为负数),默认为客服发送
if (
this.messageBody &&
this.messageBody.eid &&
+this.messageBody.eid <= 0
) {
return true;
}
created() {
this.messageComponent = messageMapping.get(this.messageType) as string;
if (
this.backend &&
this.messageBody &&
this.messageBody.eid &&
this.allChatMembers
) {
const t = this.allChatMembers.find(
(i) => +i.eid === +(this.messageBody.eid as string)
);
if (t && t.type !== ChatRole.Default) {
return true;
}
}
if (this.messageBody.eid && this.chatMyId) {
const o = +this.messageBody.eid;
const m1 = o === +this.chatMyId;
if (m1 && Xim.isBackend() && this.messageBody.oid) {
return +this.messageBody.oid === +Xim.getOrgId();
}
return m1;
}
return false;
}
private get userName() {
if (this.refetchUsername) {
return this.refetchUsername;
}
if (this.chatMembers) {
const t = this.chatMembers.find((i) => i.eid === this.data.eid);
if (t) {
return t.name;
const name = this.getFilterUsername(
t.alias_name as string,
t.name
);
if (name) {
return name;
}
}
return "";
}
this.refetchUsername4Message();
return this.refetchUsername;
}
private getFilterUsername(name1: string, name2: string) {
const backend = Xim.isBackend();
if (
this.currentChat &&
this.currentChat.catalog === "福利宝" &&
this.chatRole === "customer-service"
) {
return backend && name1
? `采购顾问 ${name1}(${name2})`
: `采购顾问 ${name1 || name2}`;
}
return backend && name1 ? `${name1}(${name2})` : name1 || name2;
}
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 this.refetchUserIcon;
}
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 "";
}
......@@ -332,13 +492,43 @@
if (this.isTextMessage && !this.isMyMessage) {
const m = this.messageBody.msg as { text: string };
if (m && m.text) {
const keywords = xim.getMatchedTextKeywords();
const keywords = Xim.getMatchedTextKeywords();
return keywords.find((i) => m.text.includes(i));
}
}
return false;
}
created() {
this.messageComponent = messageMapping.get(this.messageType) as string;
if (Xim.isBackend()) {
this.data &&
!this.isAllRead &&
ximInstance
.queryNextPageMsg(
"group",
this.chatId as number,
this.data.id - 1,
1
)
.then((m) => {
if (m && m.length) {
const t = m[0];
t.read_count &&
t.read_count !== this.data.read_count &&
this.updateMessage({
chat: this.chatId as number,
start: this.data.id,
all: (this.manualAllRead =
t.read_count === t.total_read_count),
readed: (this.manualReaded = t.read_count),
});
}
});
}
}
private isCustomer() {
return !this.showReadSummary;
}
......@@ -356,20 +546,38 @@
}
private withdraw() {
ximInstance.withdraw(this.chatId!, this.data.id).finally(() => {
dbController
.removeMessage(this.chatId!, this.data.id)
.finally(() => {
this.executeWithDraw(this.data.id);
this.$emit("withdraw", this.data.id);
});
if (!this.hoverWithdraw()) {
return Xim.error("不能撤回");
}
if (dto.MessageTypeController.isChargeMessage(this.data.type)) {
return this.openMessage({
type: MessageEvent.WithdrawCharge,
model: {
payment: (this.messageBody.msg as PayMessageBody).paymentId,
msg: this.data.id,
},
});
}
ximInstance.withdraw(this.chatId, this.data.id);
}
private hoverWithdraw() {
if (this.isPayMessage) {
return true;
}
if (!this.isWithdraw || !this.isMyMessage) {
return false;
}
return (this.isWithdraw =
new Date().valueOf() - this.data.ts * 1000 < oneDay);
}
private openReaderList(e: MouseEvent) {
if (this.isChatMember) {
this.readerListOffset = e.x < 450;
this.readListVisibility = true;
}
}
private executeHandled() {
this.setHandled({
......@@ -392,10 +600,33 @@
private closeKeywordPopover() {
document.body.click();
}
private openMessage(o: any) {
CustomerServiceEvent.emit(this, o);
}
private refetchUsername4Message() {
if (this.data && this.data.eid) {
ChatUserInfoService.getUserInfo(this.data.eid).then((r) => {
if (r) {
if (Xim.isBackend() && r.alias_name) {
this.refetchUsername = `${r.alias_name}(${
r.name || r.phone
})`;
} else {
this.refetchUsername =
r.alias_name || r.name || r.phone;
}
r.icon && (this.refetchUserIcon = r.icon);
}
});
}
}
}
</script>
<style lang="less" scoped>
@import "./css/benefits-plan.less";
.message-con {
padding-bottom: 20px;
margin-right: 15px;
......@@ -437,8 +668,12 @@
display: inline-block;
}
}
}
/deep/ .file-message-info {
text-align: initial;
word-break: break-all;
}
}
&.offset-bottom {
margin-bottom: 30px;
}
......@@ -466,13 +701,16 @@
&.algin-left {
text-align: left;
}
.cs-flex-direction {
flex-direction: row-reverse;
}
}
.msg-detail {
margin-top: 10px;
font-size: 14px;
line-height: 20px;
background: #f5f6fa;
background-color: #f5f6fa;
border-radius: 0px 8px 8px;
padding: 10px;
word-break: break-word;
......@@ -502,7 +740,12 @@
}
.all {
color: #4389f8;
color: #ccc;
opacity: 0.7;
}
.not-all {
color: #077aec;
}
.match-keyword {
......
<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"
>
<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"
></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>
</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";
@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;
private replyList: Reply[] = [];
private uid = this.sdk.global.uid;
private replyInputVisible = false;
private addReplyStr = "";
private editingItem: { [key: string]: boolean } = {};
private editingItemContent: { [key: string]: string } = {};
async mounted() {
await this.getReplyList();
}
private async getReplyList() {
this.replyList = await this.sdk
.domainService("uniplat_base", "chat.chat", "reply")
.request("get");
}
@buttonThrottle()
private sendMsg(reply: Reply) {
return this._sendMsg({
msgType: MessageType.Text,
msg: JSON.stringify({ text: reply.content, source: this.source }),
});
}
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);
});
}
}
</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>
......@@ -26,7 +26,7 @@
>
<a
class="d-flex align-items-center justify-content-center"
:href="file.url | downloadUrl(getAttachment)"
:href="file.url"
:download="getAttachment"
>
<i class="el-icon-download"></i>
......@@ -39,16 +39,15 @@
<script lang="ts">
import {
Component,
Mixins,
Model,
Prop,
Ref,
Watch
Vue,
Watch,
} from "vue-property-decorator";
import { Filters } from '../mixin/filter';
@Component({ components: {} })
export default class VideoPreview extends Mixins(Filters) {
export default class VideoPreview extends Vue {
@Model("update")
private value!: boolean;
......@@ -68,7 +67,8 @@ export default class VideoPreview extends Mixins(Filters) {
private close() {
setTimeout(
() => (this.style = { "max-height": "300px", "max-width": "600px" }),
() =>
(this.style = { "max-height": "300px", "max-width": "600px" }),
300
);
this.$emit("update", false);
......
......@@ -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 }"
......@@ -35,7 +29,7 @@
<div class="items">
<el-scrollbar>
<div
class="member-item"
class="member-item d-flex align-items-center"
v-for="item in items"
:key="`${item.eid}-${tab}`"
>
......@@ -44,7 +38,11 @@
:src="item.avatar"
:size="30"
/>
<span class="member-name">{{ item.name }}</span>
<span
class="member-name text-truncate"
:title="item.name"
>{{ item.name }}</span
>
</div>
</el-scrollbar>
</div>
......@@ -53,27 +51,28 @@
</template>
<script lang="ts">
import { Component, Prop, Ref, Vue } from "vue-property-decorator";
import { namespace } from "vuex-class";
import * as dto from "../model";
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";
const chatStoreNamespace = namespace("chatStore");
@Component({ components: { avatar } })
export default class WhoReadList extends Vue {
import { Component, Prop, Ref, Vue } from "vue-property-decorator";
import { namespace } from "vuex-class";
import * as dto from "../model";
import { unique } from "../utils";
import avatar from "@/customer-service/components/avatar.vue";
import { ChatStore } from "@/customer-service/store/model";
import xim from "@/customer-service/xim/xim";
import { ChatUserInfoService } from "../utils/user-info";
const chatStoreNamespace = namespace("chatStore");
@Component({ components: { avatar } })
export default class WhoReadList extends Vue {
@chatStoreNamespace.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStoreNamespace.State(ChatStore.STATE_CHAT_MY_ID)
private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID;
@chatStoreNamespace.Action(ChatStore.ACTION_UPDATE_MESSAGE_READ_STATUS)
private readonly updateMessage!: ChatStore.ACTION_UPDATE_MESSAGE_READ_STATUS;
@Prop({ type: Number })
private msgId!: number;
......@@ -114,8 +113,8 @@ export default class WhoReadList extends Vue {
}
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 ChatUserInfoService.getUserInfo(eid);
return data.name;
}
private async getReader() {
......@@ -140,6 +139,9 @@ export default class WhoReadList extends Vue {
};
})
);
if (!this.readlist.length) {
this.tab = 2;
}
this.unreadlist = await Promise.all(
readerlist
.filter((k) => !k.is_read)
......@@ -154,6 +156,14 @@ export default class WhoReadList extends Vue {
};
})
);
if (!this.unreadlist.length) {
this.updateMessage({
chat: this.chatId,
start: this.msgId,
all: true,
});
}
}
private uniqueReaderList(data: dto.OneWhoReadMessage[]) {
......@@ -161,11 +171,11 @@ export default class WhoReadList extends Vue {
return all.findIndex((k) => k.eid === item.eid);
});
}
}
}
</script>
<style lang="less" scoped>
.who-read-list {
.who-read-list {
background-color: #fff;
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.2);
border-radius: 4px;
......@@ -175,7 +185,7 @@ export default class WhoReadList extends Vue {
color: #000;
z-index: 2;
margin-left: -100px;
width: 125px;
width: 200px;
&.offset {
margin-left: 0;
......@@ -186,9 +196,9 @@ export default class WhoReadList extends Vue {
color: #333333;
margin-bottom: 15px;
}
}
}
.tabs {
.tabs {
border-bottom: 1px solid #f0f0f0;
padding: 10px;
......@@ -211,15 +221,15 @@ export default class WhoReadList extends Vue {
margin-left: 30px;
}
}
}
}
.items {
.items {
padding: 10px;
padding-top: 0;
padding-left: 20px;
}
}
.member-item {
.member-item {
margin-top: 10px;
.member-avatar,
.member-name {
......@@ -229,5 +239,5 @@ export default class WhoReadList extends Vue {
.member-avatar {
margin-right: 10px;
}
}
}
</style>
<template>
<div class="h-100 pos-rel workflows">
<div class="workflow-header">
<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>
</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,
}
const sdk = Chat.getSdk;
@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 sdk()
.model(this.model_name)
.workflow2()
.queryProcessByAssociateId(+this.id);
}
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);
}
</style>
class DevAppTools {
private db!: IDBDatabase;
private readonly key = 'dev-tools';
private readonly valueKey = 'value';
private readonly table = 'dev-table';
private onReady() {
if (this.db) {
return Promise.resolve();
}
return new Promise<void>((resolve => {
if (indexedDB) {
const r = indexedDB.open(this.key, 1);
const setupDb = () => {
if (this.db && !this.db.objectStoreNames.contains(this.table)) {
this.db.createObjectStore(this.table, { keyPath: this.valueKey });
}
resolve();
};
r.onsuccess = (e) => {
this.db = (e.target as any).result;
setupDb();
};
r.onupgradeneeded = (e) => {
this.db = (e.target as any).result;
setupDb();
};
r.onerror = () => setupDb();
} else {
resolve();
}
}));
}
private buildTransaction(key: string) {
return this.db.transaction(key, "readwrite");
}
private buildStore(key: string) {
const transaction = this.buildTransaction(key);
return transaction.objectStore(key);
}
public isOpen() {
return new Promise<boolean>((resolve) => {
this.onReady().finally(() => {
if (!this.db) {
return resolve(false);
}
setTimeout(() => {
const store = this.buildStore(this.table);
const r = store.getKey(1);
r.onsuccess = (o) => resolve((o.target as any).result);
r.onerror = () => resolve(false);
}, 300);
});
});
}
public toggle() {
return new Promise<boolean>((resolve) => {
this.onReady().finally(() => {
setTimeout(() => {
this.isOpen().then(r => {
if (r) {
const store = this.buildStore(this.table);
const d = store.delete(1);
d.onsuccess = () => resolve(false);
d.onerror = () => resolve(false);
} else {
const store = this.buildStore(this.table);
const d = store.add({ value: 1 });
d.onsuccess = () => resolve(true);
d.onerror = () => resolve(true);
}
});
}, 300);
});
});
}
public getDataByKey(key: string) {
return new Promise<any>((resolve) => {
this.onReady().finally(() => {
if (!this.db) {
return resolve(false);
}
setTimeout(() => {
const store = this.buildStore(this.table);
const r = store.get(key);
r.onsuccess = ((o) => {
const result = (o.target as any).result;
if (result) {
resolve(result.content)
} else {
resolve(result)
}
});
r.onerror = () => resolve(false);
}, 300);
});
});
}
public addData(data: any, key: string) {
return new Promise<boolean>((resolve) => {
this.onReady().finally(() => {
if (!this.db) {
return resolve(false);
}
setTimeout(() => {
const store = this.buildStore(this.table);
const r = store.add({ value: key, content: data });
r.onsuccess = () => resolve(true);
r.onerror = () => resolve(false);
}, 300);
});
});
}
public deleteData(key: string) {
return new Promise<boolean>((resolve) => {
this.onReady().finally(() => {
if (!this.db) {
return resolve(false);
}
setTimeout(() => {
const store = this.buildStore(this.table);
const r = store.delete(key);
r.onsuccess = () => resolve(true);
r.onerror = () => resolve(false);
}, 300);
});
});
}
}
export const devAppTools = new DevAppTools();
......@@ -13,40 +13,70 @@ class ChatCacheDatabaseController {
private readonly chatListKey = "chat-list";
private readonly chatMessageKey = "chat-message";
private setuping = false;
private setupError = false;
public readonly historyOrderModelName = "order_info";
private waitSetupCompleted() {
return new Promise<void>((resolve, reject) => {
const checker = () => {
if (!this.setuping) {
this.setupError
? reject(new Error(`IM index database setup failed`))
: resolve();
} else {
setTimeout(() => checker(), 200);
}
};
checker();
});
}
public setup(uid: string) {
return new Promise<void>((resolve) => {
if (this.db) {
return Promise.resolve();
}
if (this.setuping) {
return this.waitSetupCompleted();
}
return new Promise<void>((resolve, reject) => {
if (uid && indexedDB) {
const r = indexedDB.open(
"u-" + (this.uid = uid),
this.listVersion
);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
this.setuping = true;
const key = "u-" + (this.uid = uid);
const r = indexedDB.open(key, this.listVersion);
const setupDb = () => {
if (that.db) {
if (this.setuping) {
if (this.db) {
try {
that.buildTables(that.db, that.chatListKey);
this.buildTables(this.db, this.chatListKey);
console.log(
`build index database for chat completed, 100%`
);
} catch (e) {
this.setupError = true;
console.error(e);
reject();
}
}
this.setuping = false;
}
resolve();
};
r.onsuccess = function (e) {
that.db = (e.target as any).result;
console.log(`index database init comepleted, 33%`);
r.onsuccess = e => {
this.db = (e.target as any).result;
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%`);
r.onupgradeneeded = e => {
this.db = (e.target as any).result;
console.log(`upgrade database comepleted`);
setupDb();
};
r.onerror = function (e) {
r.onerror = e => {
console.log(`index database init failed, ${e}`);
this.setupError = true;
reject();
};
} else {
resolve();
......@@ -58,7 +88,7 @@ class ChatCacheDatabaseController {
const k = this.buildChatMessageKey(chat);
const t = this.messageDatabases.get(k);
if (!t) {
return new Promise<void>((resolve) => {
return new Promise<void>(resolve => {
if (this.uid && indexedDB) {
const r = indexedDB.open(k, this.msgVersion);
// eslint-disable-next-line @typescript-eslint/no-this-alias
......@@ -72,17 +102,17 @@ class ChatCacheDatabaseController {
}
setTimeout(() => resolve(), 200);
};
r.onsuccess = function (e) {
r.onsuccess = function(e) {
const db = (e.target as any).result;
that.messageDatabases.set(k, db);
setupDb();
};
r.onupgradeneeded = function (e) {
r.onupgradeneeded = function(e) {
const db = (e.target as any).result;
that.messageDatabases.set(k, db);
setupDb();
};
r.onerror = function (e) {
r.onerror = function(e) {
console.log(
`chat message index database init failed, ${e}`
);
......@@ -106,7 +136,12 @@ class ChatCacheDatabaseController {
}
private buildTransaction(key: string) {
try {
return this.db.transaction(key, "readwrite");
} catch {
window.location.reload();
throw new Error(`transition failed`);
}
}
private buildStore(key: string) {
......@@ -117,18 +152,22 @@ class ChatCacheDatabaseController {
public saveChatList(items: Chat[]) {
if (this.db) {
const store = this.buildStore(this.chatListKey);
if (items && items.length) {
for (const item of items) {
store.add(item, item.id);
}
} else {
store.clear();
}
}
}
public updateChat(p: ChatStore.ChatUpdateParameter) {
return new Promise<void>((resolve) => {
return new Promise<void>(resolve => {
if (this.db) {
const store = this.buildStore(this.chatListKey);
const t = store.get(p.chat);
t.onsuccess = (r) => {
t.onsuccess = r => {
const chat = (r.target as any).result as Chat;
if (chat) {
chat.eid = p.eid as string;
......@@ -136,10 +175,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 updateChat4UnreadCount(chat: number, unread: 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 = unread;
const u = store.put(chat, chat.id);
u.onsuccess = () => resolve();
u.onerror = () => resolve();
......@@ -154,8 +213,100 @@ class ChatCacheDatabaseController {
});
}
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();
} else {
resolve();
}
};
t.onerror = () => resolve();
} else {
resolve();
}
});
}
public setMessageReaded(
chat: number,
option: {
/**
* 要更新已读状态的消息Id
*/
start: number;
/**
* 如果此值存在且大于 start ,则表示需要更新从 [start ~ end] 这个区间内所有消息的已读状态
*/
end?: number;
/**
* 如果此值为true,则标记此条消息被所有人已读,否则只会在现有已读数上+1
*/
allRead?: boolean;
}
) {
return new Promise<void>(resolve => {
if (this.db) {
const store = this.buildChatMessageStore(chat);
if (option.end && option.end > option.start) {
const total = option.end - option.start;
let finished = 0;
const onCompleted = () => {
finished++;
if (finished >= total) {
resolve();
}
};
for (let i = option.start; i <= option.end; i++) {
const r = store.get(i);
r.onsuccess = m => {
const p = (m.target as any).result as Message;
if (p) {
if (option.allRead) {
p.read_count = p.total_read_count;
} else {
p.read_count++;
}
store.put(p).onsuccess = () => onCompleted();
} else {
onCompleted();
}
};
r.onerror = () => onCompleted();
}
} else {
const r = store.get(option.start);
r.onsuccess = m => {
const p = (m.target as any).result as Message;
if (p) {
if (option.allRead) {
p.read_count = p.total_read_count;
} else {
p.read_count++;
}
store.put(p).onsuccess = () => resolve();
} else {
resolve();
}
};
r.onerror = () => resolve();
}
} else {
resolve();
}
});
}
public removeChatFromList(chat: number) {
return new Promise<void>((resolve) => {
return new Promise<void>(resolve => {
if (this.db) {
const store = this.buildStore(this.chatListKey);
const t = store.delete(chat);
......@@ -168,17 +319,32 @@ class ChatCacheDatabaseController {
}
public getChatList() {
return new Promise<Chat[]>((resolve) => {
return new Promise<Chat[]>(resolve => {
if (!this.db) {
return resolve([]);
}
const store = this.buildStore(this.chatListKey);
const r = store.getAll();
r.onsuccess = (o) => resolve((o.target as any).result);
r.onsuccess = o => resolve((o.target as any).result);
r.onerror = () => resolve([]);
});
}
public getChatByCode(code: string) {
return new Promise<Chat | null>(resolve => {
if (!this.db) {
return resolve(null);
}
const store = this.buildStore(this.chatListKey);
const r = store.getAll();
r.onsuccess = o => {
const items = (o.target as any).result as Chat[];
resolve(items.find(i => i.biz_type_code === code) as Chat);
};
r.onerror = () => resolve(null);
});
}
private buildChatMessageStore(chat: number) {
const k = this.buildChatMessageKey(chat);
const db = this.messageDatabases.get(k) as IDBDatabase;
......@@ -198,22 +364,22 @@ class ChatCacheDatabaseController {
}
public getChatMessages(chat: number) {
return new Promise<Message[]>((resolve) => {
return new Promise<Message[]>(resolve => {
if (!this.db) {
return resolve([]);
}
this.setupChatMessageDatabase(chat).finally(() => {
const store = this.buildChatMessageStore(chat);
const r = store.getAll();
r.onsuccess = (o) => resolve((o.target as any).result);
r.onsuccess = o => resolve((o.target as any).result);
r.onerror = () => resolve([]);
});
});
}
public appendMessages(chat: number, items: Message[]) {
return new Promise<void>((resolve) => {
if (!this.db || !items.length) {
return new Promise<void>(resolve => {
if (!this.db || !items || !items.length) {
return resolve();
}
const store = this.buildChatMessageStore(chat);
......@@ -223,13 +389,22 @@ class ChatCacheDatabaseController {
});
}
public removeMessage(chat: number, msg: number) {
public removeMessage(chat: number, msgs: number[]) {
return new Promise<void>((resolve, reject) => {
if (this.db) {
const store = this.buildChatMessageStore(chat);
const d = store.delete(msg);
d.onsuccess = () => setTimeout(() => resolve(), 100);
d.onerror = () => reject();
let count = 0;
const action = () => {
count++;
if (count === msgs.length) {
resolve();
}
};
for (const item of msgs) {
const d = store.delete(item);
d.onsuccess = () => action();
d.onerror = () => action();
}
} else {
resolve();
}
......@@ -238,8 +413,12 @@ class ChatCacheDatabaseController {
public mergeChatList(source1: Chat[], source2: Chat[]) {
for (const item of source2) {
const t = source1.find((i) => i.id === item.id);
const t = source1.find(i => i.id === item.id);
if (t) {
item.unread_msg_count = Math.max(
item.unread_msg_count,
t.unread_msg_count
);
const index = source1.indexOf(t);
source1[index] = item;
} else {
......@@ -266,14 +445,14 @@ class ChatCacheDatabaseController {
msg: number,
status: MessageHandled
) {
return new Promise<void>((resolve) => {
return new Promise<void>(resolve => {
if (!this.db) {
return resolve();
}
this.setupChatMessageDatabase(chat).finally(() => {
const store = this.buildChatMessageStore(chat);
const r = store.get(msg);
r.onsuccess = (o) => {
r.onsuccess = o => {
const p = (o.target as any).result as Message;
p.handled = status;
const u = store.put(p);
......
export const enum MessageEvent {
Default = "open-message",
PayMessage = "pay-message",
PositionMessage = "position-message",
WithdrawCharge = "widthdraw-charge",
}
export class CustomerServiceEvent {
public static open(vue: Vue, type: MessageEvent, model: any) {
return this.emit(vue, { type, model });
}
public static emit(vue: Vue, o: { type: MessageEvent; model: any }) {
return vue.$emit(MessageEvent.Default, o);
}
}
......@@ -18,9 +18,23 @@
src="@/customer-service/imgs/pic.png"
/>
</label>
<!-- <label for="chat-upload-file" :title="tip4File" @click="allowLoadFile">
<img class="tool-bar-icon" src="@/customer-service/imgs/file.png" />
</label> -->
<label
for="chat-upload-file"
:title="tip4File"
@click="allowLoadFile"
>
<img
class="tool-bar-icon"
src="@/customer-service/imgs/file.png"
/>
</label>
<el-progress
:percentage="percentage"
:show-text="false"
:format="formatPercentage"
v-if="percentage"
></el-progress>
<input
@change="onChange"
......@@ -28,7 +42,6 @@
id="chat-upload-file"
type="file"
:accept="acceptType"
multiple
/>
</div>
......@@ -60,10 +73,9 @@
</template>
<script lang="ts">
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import { namespace } from "vuex-class";
import {
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import { namespace } from "vuex-class";
import {
getFileType,
getSvg,
MAX_FILE_SIZE,
......@@ -73,54 +85,74 @@ import {
MESSAGE_FILE_EMPTY,
MESSAGE_FILE_TOO_LARGE,
MESSAGE_IMAGE_TOO_LARGE,
} from "../components/message-item/file-controller";
import { EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils";
} from "../components/message-item/file-controller";
import { EmojiItem, EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils";
export const enum InputMessageType {
export const enum InputMessageType {
Text = "text",
Image = "image",
File = "file",
}
}
export interface InputMessageBody {
export interface InputMessageBody {
text?: string;
url?: string;
name?: string;
size?: number;
}
}
export interface InputMessage {
export interface InputMessage {
type: InputMessageType;
body: InputMessageBody;
file?: File | null;
}
}
const chatStore = namespace("chatStore");
const chatStore = namespace("chatStore");
const chatCache: { [key: number]: any } = {};
const chatCache: { [key: number]: any } = {};
export const IMAGE_INFO_CLASS = "img-info";
export const FILE_INFO_CLASS = "file-info";
export const IMAGE_INFO_CLASS = "img-info";
export const FILE_INFO_CLASS = "file-info";
export function isImageOrFile(node: ChildNode) {
export function isImageOrFile(node: ChildNode) {
const e = node as HTMLElement;
return (
e.classList &&
(e.classList.contains(IMAGE_INFO_CLASS) ||
e.classList.contains(FILE_INFO_CLASS))
);
}
}
@Component({ components: {} })
export default class Input extends Vue {
export function isFileElement(node: ChildNode) {
return (node as Element).classList.contains(FILE_INFO_CLASS);
}
const limitedFileExtension = [
"ppt",
"pptx",
"doc",
"docx",
"xls",
"xlsx",
"pdf",
"zip",
"rar",
"rars",
"7z",
"iso",
"tar",
"cab",
"jar",
"uue",
];
@Component({ components: {} })
export default class Input extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
protected readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
@Ref("input")
private readonly messageInputBox!: HTMLDivElement;
......@@ -131,7 +163,9 @@ export default class Input extends Vue {
private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})`;
private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})`;
private emoji: { name: string; emoji_chars: string; code: string }[] = [];
private emoji: EmojiItem[] = [];
private percentage = 0;
private reloadTimer = 0;
@Watch("chatId")
private onChatIdChanged(v: number, old: number) {
......@@ -191,7 +225,9 @@ export default class Input extends Vue {
}
private allowLoadFile() {
this.acceptType = "*";
this.acceptType =
limitedFileExtension.map((i) => `.${i}`).join(",") +
",image/*,video/*";
}
private async handlePasteEvent(event: ClipboardEvent) {
......@@ -211,19 +247,22 @@ export default class Input extends Vue {
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);
}
......@@ -301,22 +340,18 @@ export default class Input extends Vue {
if (e.shiftKey || e.ctrlKey || e.altKey) {
return;
}
return new Promise((resolve, reject) => {
try {
const data = this.getNodeListFromInputBox();
this.$emit("send", data, resolve);
} catch (e) {
this.$emit("error", e);
reject(e);
// 避免按一下enter键多次触发发送
if (this.reloadTimer) {
clearTimeout(this.reloadTimer);
}
})
.then(() => {
this.reloadTimer = setTimeout(() => {
const data = this.getNodeListFromInputBox();
this.$emit("send", data);
this.clearInput();
if (this.chatId) {
chatCache[this.chatId] = [];
}
})
.finally(() => setTimeout(() => this.getMyChatList(), 120));
}, 120);
}
/**
......@@ -334,31 +369,18 @@ export default class Input extends Vue {
*/
private combine(nodes: ChildNode[]) {
const sendingNodes: ChildNode[] = [];
let needCreateNewNode = false;
let text = "";
for (const item of nodes) {
if (!isImageOrFile(item) && item.textContent) {
if (needCreateNewNode) {
text = "";
needCreateNewNode = false;
}
text += item.textContent;
} else {
needCreateNewNode = true;
if (text) {
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
if (isImageOrFile(item)) {
sendingNodes.push(item);
continue;
}
}
if (text) {
if (item.textContent) {
const text = item.textContent;
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
}
return sendingNodes;
}
......@@ -496,20 +518,22 @@ export default class Input extends Vue {
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 (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 (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
}
html += this.buildFileHtml(file);
}
}
......@@ -544,11 +568,22 @@ export default class Input extends Vue {
});
});
}
}
private formatPercentage(p: number) {
return `文件处理中 ${p}%`;
}
public updateUploadProgress(v: number) {
this.percentage = v;
if (v <= 0 || v >= 100) {
this.percentage = 0;
}
}
}
</script>
<style lang="less" scoped>
.input-wrap {
.input-wrap {
position: relative;
padding-left: 20px;
......@@ -609,6 +644,11 @@ export default class Input extends Vue {
.file-size {
margin-top: 10px;
}
svg {
max-width: 70px;
max-height: 50px;
}
}
}
}
......@@ -620,9 +660,9 @@ export default class Input extends Vue {
top: -225px;
outline: 0;
}
}
}
.tool-bar {
.tool-bar {
padding-top: 10px;
user-select: none;
......@@ -635,9 +675,17 @@ export default class Input extends Vue {
.offset {
margin: 0 22px;
}
}
.emoji-picker {
.el-progress {
display: inline-block;
margin-left: 20px;
width: 150px;
top: -4px;
position: relative;
}
}
.emoji-picker {
position: absolute;
z-index: 2;
top: -232px;
......@@ -665,10 +713,10 @@ export default class Input extends Vue {
justify-content: center;
align-items: center;
}
}
}
#chat-upload-file {
#chat-upload-file {
visibility: hidden;
position: absolute;
}
}
</style>
<?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
import moment from "moment";
import Vue from "vue";
import Component from "vue-class-component";
// Define a super class component
@Component({
filters: {
downloadUrl(time: number) {
const format = {
sameDay: 'HH:mm',
nextDay: 'MM/DD',
nextWeek: 'MM/DD',
lastDay: '昨天HH:mm',
lastWeek: 'MM/DD',
sameElse: 'YYYY.MM.DD',
};
return moment(time).calendar(null, format);
}
},
})
export class Filters extends Vue {
}
export interface CardMessage {
title: string;
date: string;
desc?: string;
desc1?: string;
desc2?: string;
values?: { key: string; value: string }[];
params?: any;
path?: string;
}
export enum IMDomainType {
// 亲亲小保
"社保客服" = "con_sbkf",
"pc网站咨询" = "con_pcweb",
"手机官网咨询" = "con_iphoneweb",
"问答动态提醒" = "que_wddttx",
"活动通知c" = "con_campaign",
"节日祝福" = "con_blessing",
"社保资讯" = "con_information",
"系统消息" = "con_xtxxx",
"账户提醒" = "acc_zhtx",
// HRS100
"灵活用工h" = "con_lhyg_h",
"其他企业服务咨询h" = "con_qtqyfwconsult_h",
"人力外包h" = "con_ssfw_h",
"人事代理h" = "con_sbkf_h",
"活动通知h" = "con_campaign_h",
"节日祝福h" = "con_blessing_h",
"社保资讯h" = "con_information_h",
"系统消息h" = "con_xtxxx_h",
// 专项订单
"雇主责任险" = "biz_gzzrx",
"保准牛雇主责任保险" = "biz_bzngzzrbx",
"老年人重大疾病" = "biz_lnrzdjb_h",
"少儿百种疾病意外医疗保险" = "biz_srbzjbywylbx_h",
"补充医疗" = "biz_bcylxx_h",
"团体意外险" = "biz_ttywx",
"重疾险" = "biz_zjxxxx_h",
"高端体检" = "biz_crgdtj",
"入职体检套餐" = "biz_rztjtc",
"体检卡套餐" = "biz_tjktc",
"员工体检套餐" = "biz_ygtjtc",
"补充医疗h" = "biz_bcylll_h",
"雇主责任险h" = "biz_gzzrxxx_h",
"团体意外保险h" = "biz_ttywxxx_h",
"高端体检h" = "biz_crgdtj_h",
"补充保险" = "biz_bcbxsa_h",
"劳务外包h" = "biz_lwwbgl_h",
"薪酬个税" = "biz_xcgsqs_h",
"业务外包h" = "biz_ywwbgl_h",
"代理记账" = "biz_dljzhd",
"工商财税" = "biz_gscs",
"中小微企业财税代理" = "biz_zxwqycsdl",
"中小微企业工商代理" = "biz_zxwqygsdl",
"高新技术企业认定" = "biz_gxjsqyrd",
"劳务派遣经营许可证" = "biz_lwpqjyxkz",
"人力资源服务许可证" = "biz_rlzifwxkzz",
"食品流通许可证" = "biz_spltxkz",
"项目申报" = "biz_xmssb",
"资质认证" = "biz_zzrzq",
"劳务外包" = "biz_lwwbgl",
"全风险岗位外包" = "biz_qfxgwwb",
"自然人代开" = "biz_zrrdk",
"版权服务" = "biz_bqfw",
"商标注册" = "biz_sbzc",
"专利布局" = "biz_zlbj",
"专利服务" = "biz_zlfwhycs",
"中小微企业财税代理h" = "biz_zxwqycsdl_h",
"中小微企业工商代理h" = "biz_zxwqygsdl_h",
"出版物批发零售许可" = "biz_cbwpfls_h",
"高新技术企业认定h" = "biz_gxjsqyrd_h",
"广播电视节目制作许可" = "biz_gbdsjmzz_h",
"进出口备案许可" = "biz_jckbaxk_h",
"劳务派遣经营许可证h" = "biz_lwpqjyxkz_h",
"人力资源服务许可证h" = "biz_rlzyfwxkz_h",
"食品流通许可证h" = "biz_spltxkz_h",
"项目申报h" = "biz_xmsb_h",
"医疗器械经营许可" = "biz_ylqiqxk_h",
"资质认证h" = "biz_zzrz_h",
"劳务派遣h" = "biz_lwpxsq_h",
"自然人代开h" = "biz_zrrdk_h",
"审计报告" = "biz_sjbgs",
"验资报告" = "biz_yzbgs",
"专项审计" = "biz_zxsjs",
"岗位外包管理服务h" = "biz_gwwbglfw_h",
"人力外包" = "biz_rlwb_h",
"人事外包管理服务" = "biz_rswbglfw",
"版权服务h" = "biz_nqfw_h",
"商标注册h" = "biz_sbzc_h",
"专利布局h" = "biz_zlbj_h",
"专利服务h" = "biz_zlfw_h",
"社保业务" = "biz_sbyww_h",
"公积金业务" = "biz_gjjywuj_h",
"公积金跨省转移" = "biz_gjjkszy",
"公积金提取" = "biz_gjjtq",
"其他公积金服务" = "biz_qtgjjfw",
"参保人信息变更" = "biz_cbrxxbg",
"定点医院变更" = "biz_ddyybg",
"补缴咨询" = "biz_sbbj",
"其他社保服务" = "biz_qtsbrw",
"社保卡办理补办" = "biz_sbkbl",
"社保跨省转移" = "biz_sbkszy",
"生育险报销" = "biz_syxbx",
"医疗费用报销" = "biz_zlfybx",
"工资代发" = "biz_gzdfgr",
"劳务费代发" = "biz_lwfdf",
"灵活就业薪酬个税" = "biz_xcgsfu",
"人力外包年服务资格" = "biz_rlwbnffzg_h",
"高级会员申请" = "biz_gjhysqlk_h",
"公积金柜台增减员h" = "biz_gjjgtzjy_qy_h",
"公积金开户h" = "biz_gjjkh_qy_h",
"公积金跨省转移h" = "biz_gjjkszy_qy_h",
"公积金提取h" = "biz_gjjtq_h",
"公积金账户合并h" = "biz_gjjzhhb_qy_h",
"公积金还欠款h" = "biz_gjjhqk_qy_h",
"公积金数字证书办理续费h" = "biz_szzh_gjj_qy_h",
"参保人信息变更h" = "biz_cbrxxbg_h",
"档案管理" = "biz_dagl_qy_h",
"定点医院变更h" = "biz_ddyybg_h",
"定制参保缴费明细h" = "biz_dzcbjfmx_qy_h",
"工伤申报" = "biz_sbgssb_qy_h",
"领取医保存折h" = "biz_lqybcz_qy_h",
"社保柜台增减员" = "biz_sbgtzjy_qy_h",
"社保卡办理/变更" = "biz_sbkbl_h",
"社保开户" = "biz_sbkh_sb_qy_h",
"社保跨省转移h" = "biz_sbkszy_h",
"社保清算h" = "biz_sbqs_sb_qy_h",
"社保最后一个人减员" = "biz_sbzhygrjy_qy_h",
"生育险报销h" = "biz_syxbx_h",
"社保数字证书办理续费" = "biz_szzh_sb_qy_h",
"医疗费用报销h" = "biz_ylfybx_h",
"异地安置h" = "biz_ydaz_sb_qy_h",
"工资代发h" = "biz_xcgsmn_h",
"公积金托管申请" = "biz_gjjtgsh_qy_h",
"社保托管申请" = "biz_sbtgsh_qy_h",
"公积金托管补缴" = "biz_gjjtgbj_h",
"公积金托管调整员工基数" = "biz_gjjtgtzygjs_h",
"公积金托管减员" = "biz_jjtgjy_h",
"公积金托管其他" = "biz_gjjtgqt_h",
"公积金托管增员" = "biz_gjjtgzy_h",
"托管客户信息修改" = "biz_gjjtgyhxxxg_h",
"社保托管补缴" = "biz_sbtgbj_h",
"社保托管调整员工基数" = "biz_sbtgtzygjs_h",
"社保托管减员" = "biz_sbtgjy_h",
"社保托管其他" = "biz_sbtgqt_h",
"社保托管增员" = "biz_sbtgzy_h",
"托管客户信息修改h" = "biz_sbtgkhxxxg_h",
"北京集体户口迁移咨询" = "biz_bjjtlhqyzx",
"北京工作居住证咨询服务" = "biz_bjgzjzzjfsqzxfw",
"北京应届毕业生落户咨询" = "biz_bjyjbysl_h",
"简历优化" = "biz_jlyhs",
"劳动权益保护" = "biz_ldqybh",
"面试指导" = "biz_mszds",
"上海居住证办理咨询" = "biz_shhzzblzx",
"上海居住证积分申请咨询服务" = "biz_shjzzjfsqzxfw",
"上海居转户咨询" = "biz_shjzhzx",
"上海留学生落户咨询" = "biz_shlxslh",
"上海应届毕业生落户咨询" = "biz_shyjbyslhzx",
"天津落户咨询服务" = "biz_tjlhzxfw",
"幼升小政策咨询" = "biz_ysxzczx",
"职业测评" = "biz_zypcs",
"职业规划咨询" = "biz_zyghzx",
"北京积分落户咨询服务h" = "biz_bjjflhzxfw_h",
"北京工作居住证咨询服务h" = "biz_bjgzjzzzxfw_h",
"北京应届毕业生落户咨询h" = "biz_bjyjbysl_h1",
"劳动争议" = "biz_ldzyqy_h",
"上海居住证积分申请咨询服务h" = "biz_shjzzjfsqzxfw_h",
"上海居转户咨询h" = "biz_shjzhzx_h",
"上海留学生落户咨询h" = "biz_shlxslh_h",
"上海应届毕业生落户咨询h" = "biz_shyjbyslhzx_h",
"天津落户咨询服务h" = "biz_tjlhzxfw_h",
"外国人工作许可h" = "biz_wgrgzxk_h",
"成都购房社保合规咨询" = "biz_cdgfsbhgzx",
"成都落户咨询服务" = "biz_cdlhzxfw",
// 表格以外,亲亲小保项目中有使用
"企业社保薪酬咨询" = "con_qysbxcconsult",
"知识产权相关咨询" = "con_zscqxgconsult",
"公司资质咨询" = "con_gszzzx",
"保险及员工福利业务咨询" = "con_bxjygflywconsult",
"工商财税业务咨询" = "con_gscsywconsult",
}
export const imItems = [
// 亲亲小保
{
type: IMDomainType.社保客服,
title: "在线咨询",
},
{
type: IMDomainType.pc网站咨询,
title: "在线咨询",
},
{
type: IMDomainType.手机官网咨询,
title: "在线咨询",
},
{
type: IMDomainType.问答动态提醒,
title: "问答动态提醒",
},
{
type: IMDomainType.活动通知c,
title: "活动通知",
},
{
type: IMDomainType.节日祝福,
title: "节日祝福",
},
{
type: IMDomainType.社保资讯,
title: "社保资讯",
},
{
type: IMDomainType.系统消息,
title: "系统消息",
},
{
type: IMDomainType.账户提醒,
title: "账户提醒",
},
// HRS100
{
type: IMDomainType.灵活用工h,
title: "灵活用工",
},
{
type: IMDomainType.其他企业服务咨询h,
title: "其他企业服务咨询",
},
{
type: IMDomainType.人力外包h,
title: "人力外包",
},
{
type: IMDomainType.人事代理h,
title: "人事代理",
},
{
type: IMDomainType.活动通知h,
title: "活动通知",
},
{
type: IMDomainType.节日祝福h,
title: "节日祝福",
},
{
type: IMDomainType.社保资讯h,
title: "社保资讯",
},
{
type: IMDomainType.系统消息h,
title: "系统消息",
},
// 专项订单
{
type: IMDomainType.雇主责任险,
title: "雇主责任险",
},
{
type: IMDomainType.保准牛雇主责任保险,
title: "雇主责任险-创业团队必备",
},
{
type: IMDomainType.老年人重大疾病,
title: "老年人重大疾病",
},
{
type: IMDomainType.少儿百种疾病意外医疗保险,
title: "少儿百种疾病意外医疗保险",
},
{
type: IMDomainType.补充医疗,
title: "团队补充医疗",
},
{
type: IMDomainType.团体意外险,
title: "团体意外险",
},
{
type: IMDomainType.重疾险,
title: "重疾险",
},
{
type: IMDomainType.高端体检,
title: "高端体检",
},
{
type: IMDomainType.入职体检套餐,
title: "入职体检套餐",
},
{
type: IMDomainType.体检卡套餐,
title: "体检卡套餐",
},
{
type: IMDomainType.员工体检套餐,
title: "员工体检套餐",
},
{
type: IMDomainType.补充医疗h,
title: "补充医疗",
},
{
type: IMDomainType.雇主责任险h,
title: "雇主责任险",
},
{
type: IMDomainType.团体意外保险h,
title: "团体意外险",
},
{
type: IMDomainType.高端体检h,
title: "高端体检",
},
{
type: IMDomainType.补充保险,
title: "补充保险",
},
{
type: IMDomainType.劳务外包h,
title: "劳务外包管理",
},
{
type: IMDomainType.薪酬个税,
title: "薪酬个税",
},
{
type: IMDomainType.业务外包h,
title: "业务外包管理",
},
{
type: IMDomainType.代理记账,
title: "业务外包管理",
},
{
type: IMDomainType.工商财税,
title: "工商财税",
},
{
type: IMDomainType.中小微企业财税代理,
title: "工商财税",
},
{
type: IMDomainType.中小微企业工商代理,
title: "中小微企业工商代理",
},
{
type: IMDomainType.高新技术企业认定,
title: "高新技术企业认定",
},
{
type: IMDomainType.劳务派遣经营许可证,
title: "劳务派遣经营许可证",
},
{
type: IMDomainType.人力资源服务许可证,
title: "人力资源服务许可证",
},
{
type: IMDomainType.食品流通许可证,
title: "食品流通许可证",
},
{
type: IMDomainType.项目申报,
title: "项目申报",
},
{
type: IMDomainType.资质认证,
title: "资质认证",
},
{
type: IMDomainType.劳务外包,
title: "劳务外包管理",
},
{
type: IMDomainType.全风险岗位外包,
title: "全风险岗位外包",
},
{
type: IMDomainType.自然人代开,
title: "自然人代开",
},
{
type: IMDomainType.版权服务,
title: "版权服务",
},
{
type: IMDomainType.商标注册,
title: "商标注册",
},
{
type: IMDomainType.专利布局,
title: "专利布局",
},
{
type: IMDomainType.专利服务,
title: "专利服务",
},
{
type: IMDomainType.中小微企业财税代理h,
title: "中小微企业财税代理",
},
{
type: IMDomainType.中小微企业工商代理h,
title: "中小微企业工商代理",
},
{
type: IMDomainType.出版物批发零售许可,
title: "出版物批发零售许可",
},
{
type: IMDomainType.高新技术企业认定h,
title: "高新技术企业认定",
},
{
type: IMDomainType.广播电视节目制作许可,
title: "广播电视节目制作许可",
},
{
type: IMDomainType.进出口备案许可,
title: "进出口备案许可",
},
{
type: IMDomainType.劳务派遣经营许可证h,
title: "劳务派遣经营许可证",
},
{
type: IMDomainType.人力资源服务许可证h,
title: "人力资源服务许可证",
},
{
type: IMDomainType.食品流通许可证h,
title: "食品流通许可证",
},
{
type: IMDomainType.项目申报h,
title: "项目申报",
},
{
type: IMDomainType.医疗器械经营许可,
title: "医疗器械经营许可",
},
{
type: IMDomainType.资质认证h,
title: "资质认证",
},
{
type: IMDomainType.劳务派遣h,
title: "劳务派遣服务",
},
{
type: IMDomainType.自然人代开h,
title: "自然人代开",
},
{
type: IMDomainType.审计报告,
title: "审计报告",
},
{
type: IMDomainType.验资报告,
title: "验资报告",
},
{
type: IMDomainType.专项审计,
title: "专项审计",
},
{
type: IMDomainType.岗位外包管理服务h,
title: "岗位外包管理服务",
},
{
type: IMDomainType.人力外包,
title: "人才派驻",
},
{
type: IMDomainType.人事外包管理服务,
title: "人事外包管理服务",
},
{
type: IMDomainType.版权服务h,
title: "版权服务",
},
{
type: IMDomainType.商标注册h,
title: "商标注册",
},
{
type: IMDomainType.专利布局h,
title: "专利布局",
},
{
type: IMDomainType.专利服务h,
title: "专利服务",
},
{
type: IMDomainType.社保业务,
title: "社保业务",
},
{
type: IMDomainType.公积金业务,
title: "公积金业务",
},
{
type: IMDomainType.公积金跨省转移,
title: "公积金跨省转移",
},
{
type: IMDomainType.公积金提取,
title: "公积金提取",
},
{
type: IMDomainType.其他公积金服务,
title: "其他公积金服务",
},
{
type: IMDomainType.参保人信息变更,
title: "参保人信息变更",
},
{
type: IMDomainType.定点医院变更,
title: "定点医院变更",
},
{
type: IMDomainType.补缴咨询,
title: "跨年补缴",
},
{
type: IMDomainType.其他社保服务,
title: "其他社保服务",
},
{
type: IMDomainType.社保卡办理补办,
title: "社保卡办理/补办",
},
{
type: IMDomainType.社保跨省转移,
title: "社保跨省转移",
},
{
type: IMDomainType.生育险报销,
title: "生育险报销",
},
{
type: IMDomainType.医疗费用报销,
title: "医疗费用报销",
},
{
type: IMDomainType.工资代发,
title: "工资代发",
},
{
type: IMDomainType.劳务费代发,
title: "劳务费代发",
},
{
type: IMDomainType.灵活就业薪酬个税,
title: "灵活就业工资发放",
},
{
type: IMDomainType.人力外包年服务资格,
title: "人力外包-年服务资格",
},
{
type: IMDomainType.高级会员申请,
title: "人事代理-年服务资格",
},
{
type: IMDomainType.公积金柜台增减员h,
title: "公积金柜台增减员",
},
{
type: IMDomainType.公积金开户h,
title: "公积金开户",
},
{
type: IMDomainType.公积金跨省转移h,
title: "公积金跨省转移",
},
{
type: IMDomainType.公积金提取h,
title: "公积金提取",
},
{
type: IMDomainType.公积金账户合并h,
title: "公积金账户合并",
},
{
type: IMDomainType.公积金还欠款h,
title: "还欠款",
},
{
type: IMDomainType.公积金数字证书办理续费h,
title: "数字证书办理/续费",
},
{
type: IMDomainType.参保人信息变更h,
title: "参保人信息变更",
},
{
type: IMDomainType.档案管理,
title: "档案管理",
},
{
type: IMDomainType.定点医院变更h,
title: "定点医院变更",
},
{
type: IMDomainType.定制参保缴费明细h,
title: "定制参保缴费明细",
},
{
type: IMDomainType.工伤申报,
title: "工伤申报",
},
{
type: IMDomainType.领取医保存折h,
title: "领取医保存折",
},
{
type: IMDomainType.社保柜台增减员,
title: "社保柜台增减员",
},
{
type: IMDomainType["社保卡办理/变更"],
title: "社保卡办理/补办",
},
{
type: IMDomainType.社保开户,
title: "社保开户",
},
{
type: IMDomainType.社保跨省转移h,
title: "社保跨省转移",
},
{
type: IMDomainType.社保清算h,
title: "社保清算",
},
{
type: IMDomainType.社保最后一个人减员,
title: "社保最后一个人减员",
},
{
type: IMDomainType.生育险报销h,
title: "生育费用报销",
},
{
type: IMDomainType.社保数字证书办理续费,
title: "数字证书办理/续费",
},
{
type: IMDomainType.医疗费用报销h,
title: "医疗费用报销",
},
{
type: IMDomainType.异地安置h,
title: "异地安置",
},
{
type: IMDomainType.工资代发h,
title: "工资代发",
},
{
type: IMDomainType.公积金托管申请,
title: "公积金托管申请",
},
{
type: IMDomainType.社保托管申请,
title: "社保托管申请",
},
{
type: IMDomainType.公积金托管补缴,
title: "公积金托管补缴",
},
{
type: IMDomainType.公积金托管调整员工基数,
title: "公积金托管调整员工基数",
},
{
type: IMDomainType.公积金托管减员,
title: "公积金托管减员",
},
{
type: IMDomainType.公积金托管其他,
title: "公积金托管其他",
},
{
type: IMDomainType.公积金托管增员,
title: "公积金托管增员",
},
{
type: IMDomainType.托管客户信息修改,
title: "托管客户信息修改",
},
{
type: IMDomainType.社保托管补缴,
title: "社保托管补缴",
},
{
type: IMDomainType.社保托管调整员工基数,
title: "社保托管调整员工基数",
},
{
type: IMDomainType.社保托管减员,
title: "社保托管减员",
},
{
type: IMDomainType.社保托管其他,
title: "社保托管其他",
},
{
type: IMDomainType.社保托管增员,
title: "社保托管增员",
},
{
type: IMDomainType.托管客户信息修改h,
title: "托管客户信息修改",
},
{
type: IMDomainType.北京集体户口迁移咨询,
title: "北京集体户口迁移咨询办理",
},
{
type: IMDomainType.北京工作居住证咨询服务,
title: "北京市工作居住证咨询",
},
{
type: IMDomainType.北京应届毕业生落户咨询,
title: "北京应届毕业生落户咨询",
},
{
type: IMDomainType.简历优化,
title: "简历优化",
},
{
type: IMDomainType.劳动权益保护,
title: "劳动权益保护",
},
{
type: IMDomainType.面试指导,
title: "面试指导",
},
{
type: IMDomainType.上海居住证办理咨询,
title: "上海居住证办理咨询",
},
{
type: IMDomainType.上海居住证积分申请咨询服务,
title: "上海居住证积分申请咨询服务",
},
{
type: IMDomainType.上海居转户咨询,
title: "上海居转户咨询",
},
{
type: IMDomainType.上海留学生落户咨询,
title: "上海留学生落户咨询",
},
{
type: IMDomainType.上海应届毕业生落户咨询,
title: "上海应届毕业生落户咨询",
},
{
type: IMDomainType.天津落户咨询服务,
title: "天津落户咨询服务",
},
{
type: IMDomainType.幼升小政策咨询,
title: "幼升小政策咨询",
},
{
type: IMDomainType.职业测评,
title: "职业测评",
},
{
type: IMDomainType.职业规划咨询,
title: "职业咨询",
},
{
type: IMDomainType.北京积分落户咨询服务h,
title: "北京积分落户咨询服务",
},
{
type: IMDomainType.北京工作居住证咨询服务h,
title: "北京企业工作居住证费用",
},
{
type: IMDomainType.北京应届毕业生落户咨询h,
title: "北京应届毕业生落户咨询",
},
{
type: IMDomainType.劳动争议,
title: "劳动纠纷咨询",
},
{
type: IMDomainType.上海居住证积分申请咨询服务h,
title: "上海居住证积分申请咨询服务",
},
{
type: IMDomainType.上海居转户咨询h,
title: "上海居转户咨询",
},
{
type: IMDomainType.上海留学生落户咨询h,
title: "上海留学生落户咨询",
},
{
type: IMDomainType.上海应届毕业生落户咨询h,
title: "上海应届毕业生落户咨询",
},
{
type: IMDomainType.天津落户咨询服务h,
title: "天津落户咨询服务",
},
{
type: IMDomainType.外国人工作许可h,
title: "外国人工作许可",
},
{
type: IMDomainType.成都购房社保合规咨询,
title: "成都购房社保合规咨询",
},
{
type: IMDomainType.成都落户咨询服务,
title: "成都落户咨询服务",
},
// 表格以外,亲亲小保项目中有使用
{
type: IMDomainType.企业社保薪酬咨询,
title: "企业社保薪酬咨询",
},
{
type: IMDomainType.知识产权相关咨询,
title: "知识产权相关咨询",
},
{
type: IMDomainType.公司资质咨询,
title: "公司资质咨询",
},
{
type: IMDomainType.保险及员工福利业务咨询,
title: "保险及员工福利业务咨询",
},
{
type: IMDomainType.工商财税业务咨询,
title: "工商财税业务咨询",
},
];
export function getOrderProductName(code: string) {
const t = imItems.find(i => i.type === code);
if (t) {
return { ...t, icon: `/im/icons1/${t.type}.png` };
}
return null;
}
import type { UniplatSdk } from "uniplat-sdk";
export * from "./order";
export * from "./order-product";
export const enum ChatRole {
Default = 25,
......@@ -44,13 +46,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 +79,16 @@ export interface ChatOption {
eventHub?: Vue;
/**
* 用户信息(头像,别名)可选
*/
user?: { icon?: string; username?: string };
message?: ChatMessageController;
avatar?: string;
disabledDbIndex?:boolean;
}
export interface ChatMessageController {
error: (msg: string) => void;
info: (msg: string) => void;
}
export interface ChatServiceLogger {
......@@ -91,8 +111,18 @@ 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",
PayV1 = "gpay",
Pay = "gpay2",
PayResult = "gresult",
RefundV1 = "grefund",
Refund = "grefund2",
Card = "card",
Position = "position",
}
......@@ -131,6 +161,7 @@ export type MessageRequestResult = readonly Message[];
export interface BaseChatItemBusinessData {
model_name: string;
obj_id: string;
detail_name?: string;
}
export interface BaseChatItem extends BaseChatItemBusinessData {
......@@ -167,6 +198,7 @@ export interface BaseChatItem extends BaseChatItemBusinessData {
create_time: number;
update_time: number;
last_msg_ts: number;
last_msg_sender: string;
members_updated: number;
user_updated: number;
}
......@@ -186,6 +218,7 @@ export interface TransferedChatItem extends BaseChatItem {
export type ChatMemberExtraInfo = {
name?: string;
phone?: string;
alias_name?: string;
};
export interface ChatMember {
......@@ -195,7 +228,7 @@ export interface ChatMember {
oid: string;
eid: string;
chat_id: number;
type: number;
type: ChatRole;
join_msg_id: number;
exit_msg_id: number;
is_exited: boolean;
......@@ -212,6 +245,7 @@ export interface ChatMember {
update_time: number;
name: string;
phone: string;
alias_name?: string;
}
export type ChatMembers = readonly ChatMember[];
......@@ -265,3 +299,59 @@ export interface GetAllChatListParams {
page_size?: number;
last_cs_eid?: string | number;
}
export const enum IMCatalog {
BiJie = "大爱毕节",
Qqxb = "亲亲小保",
Hrs100 = "HRS100",
Flb = "福利宝",
Order = "专项业务订单",
}
export class MessageTypeController {
private static readonly pays = new Set<MessageType>([
MessageType.Pay,
MessageType.PayResult,
MessageType.PayV1,
MessageType.Refund,
MessageType.RefundV1,
MessageType.Withdraw,
]);
private static readonly charges = new Set<MessageType>([
MessageType.Pay,
MessageType.PayV1,
MessageType.Refund,
MessageType.RefundV1,
]);
public static isPayMessage(type: MessageType) {
return this.pays.has(type);
}
public static isChargeMessage(type: MessageType) {
return this.charges.has(type);
}
public static isOrderOpenedMessage(e: Message) {
return (
e &&
!+e.eid &&
e.msg &&
e.msg.includes("订单详情") &&
e.msg.includes("查看订单")
);
}
public static isOrderClosedMessage(e: Message) {
if (e && e.msg && !+e.eid) {
const msg = e.msg;
return (
msg.includes("办理完成") ||
msg.includes("办理失败") ||
msg.includes("订单已取消")
);
}
return false;
}
}
export const enum GeneralOrderDirection {
"社保卡办理/补办1" = "PRODUCT1",
"参保人信息变更1" = "PRODUCT2",
"中小微企业工商代理1" = "PRODUCT3",
"中小微企业财税代理2" = "PRODUCT4",
"定点医院变更1" = "PRODUCT5",
"医疗费用报销1" = "PRODUCT6",
"生育费用报销2" = "PRODUCT7",
"公积金提取1" = "PRODUCT8",
"雇主责任险-创业团队必备" = "PRODUCT9",
"北京市工作居住证咨询" = "PRODUCT16",
"其他服务1" = "PRODUCT24",
"社保卡办理/补办2" = "PRODUCT28",
"参保人信息变更2" = "PRODUCT29",
"中小微企业工商代理" = "PRODUCT30",
"中小微企业财税代理" = "PRODUCT31",
"定点医院变更2" = "PRODUCT32",
"医疗费用报销" = "PRODUCT33",
"生育费用报销" = "PRODUCT34",
"公积金提取2" = "PRODUCT35",
"北京工作居住证咨询服务" = "PRODUCT39",
"社保业务" = "PRODUCT67",
"公积金业务" = "PRODUCT68",
"薪酬个税" = "PRODUCT69",
"社保跨省转移1" = "PRODUCT76",
"社保跨省转移2" = "PRODUCT77",
"公积金跨省转移1" = "PRODUCT79",
"公积金跨省转移2" = "PRODUCT80",
"专利服务1" = "PRODUCT82",
"专利服务2" = "PRODUCT83",
"商标注册1" = "PRODUCT84",
"商标注册2" = "PRODUCT85",
"版权服务1" = "PRODUCT86",
"版权服务2" = "PRODUCT87",
"高新技术企业认定1" = "PRODUCT96",
"高新技术企业认定2" = "PRODUCT97",
"专利布局" = "PRODUCT99",
"项目申报" = "PRODUCT101",
"资质认证" = "PRODUCT103",
"劳务派遣经营许可证1" = "PRODUCT104",
"劳务派遣经营许可证2" = "PRODUCT105",
"人力资源服务许可证1" = "PRODUCT106",
"人力资源服务许可证2" = "PRODUCT107",
"食品流通许可证1" = "PRODUCT108",
"食品流通许可证2" = "PRODUCT109",
"工资代发1" = "PRODUCT111",
"劳务外包管理1" = "PRODUCT114",
"业务外包管理" = "PRODUCT115",
"岗位外包管理服务" = "PRODUCT116",
"劳务费代发" = "PRODUCT117",
"劳务外包管理2" = "PRODUCT118",
"全风险岗位外包" = "PRODUCT120",
"社保托管增员" = "PRODUCT121",
"社保托管减员" = "PRODUCT122",
"社保托管补缴" = "PRODUCT123",
"社保托管调整员工基数" = "PRODUCT125",
"公积金托管增员" = "PRODUCT126",
"公积金托管减员" = "PRODUCT127",
"公积金托管补缴" = "PRODUCT128",
"公积金托管调整员工基数" = "PRODUCT129",
"社保托管其他" = "PRODUCT130",
"公积金托管其他" = "PRODUCT131",
"社保托管申请" = "PRODUCT132",
"公积金托管申请" = "PRODUCT133",
"上海居住证积分申请咨询服务1" = "PRODUCT136",
"上海居住证积分申请咨询服务2" = "PRODUCT137",
"天津落户咨询服务" = "PRODUCT141",
"社保开户" = "PRODUCT143",
"社保最后一个人减员" = "PRODUCT144",
"社保清算1" = "PRODUCT145",
"异地安置" = "PRODUCT146",
"定制参保缴费明细" = "PRODUCT147",
"领取医保存折" = "PRODUCT149",
"工伤申报" = "PRODUCT150",
"社保柜台增减员" = "PRODUCT151",
"数字证书办理/续费1" = "PRODUCT152",
"公积金账户合并" = "PRODUCT153",
"公积金开户" = "PRODUCT154",
"还欠款" = "PRODUCT155",
"公积金柜台增减员" = "PRODUCT156",
"数字证书办理/续费2" = "PRODUCT157",
"托管客户信息修改1" = "PRODUCT158",
"托管客户信息修改2" = "PRODUCT159",
"档案管理" = "PRODUCT163",
"其他服务2" = "PRODUCT164",
"北京积分落户咨询服务" = "PRODUCT170",
"劳动权益保护" = "PRODUCT172",
"劳动纠纷咨询" = "PRODUCT174",
"幼升小政策咨询" = "PRODUCT177",
"老年人重大疾病" = "PRODUCT179",
"重疾险" = "PRODUCT180",
"少儿百种疾病意外医疗保险" = "PRODUCT181",
"团队补充医疗" = "PRODUCT182",
"补充医疗" = "PRODUCT183",
"雇主责任险1" = "PRODUCT184",
"团体意外险1" = "PRODUCT185",
"工资代发2" = "PRODUCT192",
"劳务派遣服务" = "PRODUCT193",
"北京企业工作居住证费用" = "PRODUCT194",
"雇主责任险2" = "PRODUCT198",
"团体意外险2" = "PRODUCT199",
"高端体检1" = "PRODUCT201",
"高端体检2" = "PRODUCT202",
"上海留学生落户咨询1" = "PRODUCT205",
"上海留学生落户咨询2" = "PRODUCT206",
"外国人工作许可" = "PRODUCT208",
"社保清算2" = "PRODUCT211",
"存档" = "PRODUCT212",
"证明开具" = "PRODUCT213",
"北京市工作居住证费用" = "PRODUCT214",
"人才派驻" = "PRODUCT215",
"医疗器械经营许可" = "PRODUCT217",
"进出口备案许可" = "PRODUCT218",
"出版物批发零售许可" = "PRODUCT219",
"广播电视节目制作许可" = "PRODUCT220",
"上海应届毕业生落户咨询1" = "PRODUCT222",
"上海应届毕业生落户咨询2" = "PRODUCT223",
"上海居转户咨询1" = "PRODUCT224",
"上海居转户咨询2" = "PRODUCT225",
"工商财税" = "PRODUCT226",
"灵活就业工资发放" = "PRODUCT227",
"职业咨询" = "PRODUCT229",
"人事代理-年服务资格" = "PRODUCT232",
"简历优化" = "PRODUCT234",
"面试指导" = "PRODUCT235",
"职业测评" = "PRODUCT236",
"人力外包-年服务资格" = "PRODUCT237",
"自然人代开1" = "PRODUCT238",
"自然人代开2" = "PRODUCT241",
"入职体检套餐" = "PRODUCT242",
"员工体检套餐" = "PRODUCT243",
"体检卡套餐" = "PRODUCT244",
"审计报告" = "PRODUCT245",
"验资报告" = "PRODUCT246",
"专项审计" = "PRODUCT247",
"跨年补缴" = "PRODUCT248",
"上海居住证办理咨询" = "PRODUCT249",
"人事外包管理服务" = "PRODUCT250",
"代理记账" = "PRODUCT251",
"成都购房社保合规咨询" = 'PRODUCT168',
"成都落户" = 'PRODUCT233',
}
export enum GeneralOrderTitle {
PRODUCT1 = "社保卡办理/补办",
PRODUCT2 = "参保人信息变更",
PRODUCT3 = "中小微企业工商代理",
PRODUCT4 = "中小微企业财税代理",
PRODUCT5 = "定点医院变更",
PRODUCT6 = "医疗费用报销",
PRODUCT7 = "生育费用报销",
PRODUCT8 = "公积金提取",
PRODUCT9 = "雇主责任险-创业团队必备",
PRODUCT16 = "北京市工作居住证咨询",
PRODUCT24 = "其他服务",
PRODUCT28 = "社保卡办理/补办",
PRODUCT29 = "参保人信息变更",
PRODUCT30 = "中小微企业工商代理",
PRODUCT31 = "中小微企业财税代理",
PRODUCT32 = "定点医院变更",
PRODUCT33 = "医疗费用报销",
PRODUCT34 = "生育费用报销",
PRODUCT35 = "公积金提取",
PRODUCT39 = "北京工作居住证咨询服务",
PRODUCT67 = "社保业务",
PRODUCT68 = "公积金业务",
PRODUCT69 = "薪酬个税",
PRODUCT76 = "社保跨省转移",
PRODUCT77 = "社保跨省转移",
PRODUCT79 = "公积金跨省转移",
PRODUCT80 = "公积金跨省转移",
PRODUCT82 = "专利服务",
PRODUCT83 = "专利服务",
PRODUCT84 = "商标注册",
PRODUCT85 = "商标注册",
PRODUCT86 = "版权服务",
PRODUCT87 = "版权服务",
PRODUCT96 = "高新技术企业认定",
PRODUCT97 = "高新技术企业认定",
PRODUCT99 = "专利布局",
PRODUCT101 = "项目申报",
PRODUCT103 = "资质认证",
PRODUCT104 = "劳务派遣经营许可证",
PRODUCT105 = "劳务派遣经营许可证",
PRODUCT106 = "人力资源服务许可证",
PRODUCT107 = "人力资源服务许可证",
PRODUCT108 = "食品流通许可证",
PRODUCT109 = "食品流通许可证",
PRODUCT111 = "工资代发",
PRODUCT114 = "劳务外包管理",
PRODUCT115 = "业务外包管理",
PRODUCT116 = "岗位外包管理服务",
PRODUCT117 = "劳务费代发",
PRODUCT118 = "劳务外包管理",
PRODUCT120 = "全风险岗位外包",
PRODUCT121 = "社保托管增员",
PRODUCT122 = "社保托管减员",
PRODUCT123 = "社保托管补缴",
PRODUCT125 = "社保托管调整员工基数",
PRODUCT126 = "公积金托管增员",
PRODUCT127 = "公积金托管减员",
PRODUCT128 = "公积金托管补缴",
PRODUCT129 = "公积金托管调整员工基数",
PRODUCT130 = "社保托管其他",
PRODUCT131 = "公积金托管其他",
PRODUCT132 = "社保托管申请",
PRODUCT133 = "公积金托管申请",
PRODUCT136 = "上海居住证积分申请咨询服务",
PRODUCT137 = "上海居住证积分申请咨询服务",
PRODUCT141 = "天津落户咨询服务",
PRODUCT143 = "社保开户",
PRODUCT144 = "社保最后一个人减员",
PRODUCT145 = "社保清算",
PRODUCT146 = "异地安置",
PRODUCT147 = "定制参保缴费明细",
PRODUCT149 = "领取医保存折",
PRODUCT150 = "工伤申报",
PRODUCT151 = "社保柜台增减员",
PRODUCT152 = "数字证书办理/续费",
PRODUCT153 = "公积金账户合并",
PRODUCT154 = "公积金开户",
PRODUCT155 = "还欠款",
PRODUCT156 = "公积金柜台增减员",
PRODUCT157 = "数字证书办理/续费",
PRODUCT158 = "托管客户信息修改",
PRODUCT159 = "托管客户信息修改",
PRODUCT163 = "档案管理",
PRODUCT164 = "其他服务",
PRODUCT170 = "北京积分落户咨询服务",
PRODUCT172 = "劳动权益保护",
PRODUCT174 = "劳动纠纷咨询",
PRODUCT177 = "幼升小政策咨询",
PRODUCT179 = "老年人重大疾病",
PRODUCT180 = "重疾险",
PRODUCT181 = "少儿百种疾病意外医疗保险",
PRODUCT182 = "团队补充医疗",
PRODUCT183 = "补充医疗",
PRODUCT184 = "雇主责任险",
PRODUCT185 = "团体意外险",
PRODUCT192 = "工资代发",
PRODUCT193 = "劳务派遣服务",
PRODUCT194 = "北京企业工作居住证费用",
PRODUCT198 = "雇主责任险",
PRODUCT199 = "团体意外险",
PRODUCT201 = "高端体检",
PRODUCT202 = "高端体检",
PRODUCT205 = "上海留学生落户咨询",
PRODUCT206 = "上海留学生落户咨询",
PRODUCT208 = "外国人工作许可",
PRODUCT211 = "社保清算",
PRODUCT212 = "存档",
PRODUCT213 = "证明开具",
PRODUCT214 = "北京市工作居住证费用",
PRODUCT215 = "人才派驻",
PRODUCT217 = "医疗器械经营许可",
PRODUCT218 = "进出口备案许可",
PRODUCT219 = "出版物批发零售许可",
PRODUCT220 = "广播电视节目制作许可",
PRODUCT222 = "上海应届毕业生落户咨询",
PRODUCT223 = "上海应届毕业生落户咨询",
PRODUCT224 = "上海居转户咨询",
PRODUCT225 = "上海居转户咨询",
PRODUCT226 = "工商财税",
PRODUCT227 = "灵活就业工资发放",
PRODUCT229 = "职业咨询",
PRODUCT232 = "人事代理-年服务资格",
PRODUCT234 = "简历优化",
PRODUCT235 = "面试指导",
PRODUCT236 = "职业测评",
PRODUCT237 = "人力外包-年服务资格",
PRODUCT238 = "自然人代开",
PRODUCT241 = "自然人代开",
PRODUCT242 = "入职体检套餐",
PRODUCT243 = "员工体检套餐",
PRODUCT244 = "体检卡套餐",
PRODUCT245 = "审计报告",
PRODUCT246 = "验资报告",
PRODUCT247 = "专项审计",
PRODUCT248 = "跨年补缴",
PRODUCT249 = "上海居住证办理咨询",
PRODUCT250 = "人事外包管理服务",
PRODUCT251 = "代理记账",
PRODUCT168 = "成都购房社保合规咨询",
PRODUCT233 = "成都落户",
}
import { Chat } from "../xim/models/chat";
import { GeneralOrderDirection } from './order-product';
import { action } from 'uniplat-sdk';
export interface ChatGroup extends Chat {
children?: ChatGroup[];
}
export const enum OrderStatus {
None,
Waiting,
Progressing,
Finished,
Failed,
UserCancelled,
AdminCancelled,
Deleted,
}
export const statusMapping = new Map<OrderStatus, string>([
[OrderStatus.None, ""],
[OrderStatus.Waiting, "等待处理"],
[OrderStatus.Progressing, "处理中"],
[OrderStatus.Finished, "已完成"],
[OrderStatus.Failed, "处理失败"],
[OrderStatus.UserCancelled, "已取消"],
[OrderStatus.AdminCancelled, "已取消"],
[OrderStatus.Deleted, "已删除"],
]);
export interface OrderTableListItem {
id: string;
v: number;
no: string;
title: string;
time: string;
chat: number;
status: OrderStatus;
status_label: string;
editTitle: string;
remark: string;
/**
* 待支付金额
*/
PayAmount: number;
/**
* 已支付金额
*/
PaidAmount: number;
/**
* 待退款金额
*/
RefundAmount: number;
/**
* 已退款金额
*/
RefundedAmount: number;
chatTypeCode: string;
lastMsgContent: string;
lastMsgTime: string;
unreadCount: number;
logo?: string;
orderId?: string;
/**
* 付款笔数
*/
PayPaymentNum?: string | number;
product: GeneralOrderDirection;
}
export const orderPredict = {
id: "ID",
v: "uniplat_version",
no: "OrderDocNo",
title: "ProductId#product.OuterName",
editTitle: "Title",
status: "Status_label",
remark: "Remark",
time: "CreatedDate",
PayAmount: "PayAmount",
PaidAmount: "PaidAmount",
RefundAmount: "RefundAmount",
RefundedAmount: "RefundedAmount",
chat: "UniplatImChatId",
chatTypeCode: "UniplatChatTypeCode",
lastMsgContent: "UniplatLastMsgContent",
lastMsgTime: "UniplatLastMsgTime",
unreadCount: 0,
PayPaymentNum: "PayPaymentNum",
product: 'ProductId#product.Code'
};
export const enum PayStatus {
UnPay = 1,
Paied,
Cancel,
Deleted,
WaitRefund,
Refund,
}
export const payStatusMapping = new Map<PayStatus, string>([
[PayStatus.UnPay, "待支付"],
[PayStatus.Paied, "已支付"],
[PayStatus.Cancel, "已取消"],
[PayStatus.Deleted, "已删除"],
[PayStatus.WaitRefund, "待退费"],
[PayStatus.Refund, "已退费"],
]);
export const cardPayStatusMapping = new Map<PayStatus, string>([
[PayStatus.UnPay, "待转账"],
[PayStatus.Paied, "已转账"],
]);
export const enum PayMethod {
CardTransfer = 1,
Balance,
Refund2Card,
Refund2Balance,
}
export const payMethodMapping = new Map<PayMethod, string>([
[PayMethod.CardTransfer, "银行卡转账"],
[PayMethod.Balance, "余额扣费"],
[PayMethod.Refund2Card, "退款至银行卡"],
[PayMethod.Refund2Balance, "退款至余额"],
]);
export interface OrderPayItem {
id: number;
v: number;
title: string;
time: string;
dueTime: string;
value: number;
status: PayStatus;
method: PayMethod;
method_label: string;
type: string;
desc: string;
agent: string;
createdTime: string;
actions?: action[];
bankAccountName?: string;
OpenningBankName?: string;
bankAccountNo?: string;
paymentItemId?: number;
paymentDate: string;
}
export interface OrderComment {
id: number;
content: string;
user: string;
attachment: string;
attachment_label: string;
time: string;
}
export const orderPayItemPredict = {
status: "Status",
time: "PaymentDate",
dueTime: "PaymentDueDate",
method: "PaymentFunction_label",
type: "ItemName",
desc: "PaymentDesc",
agent: "ProviderCollectId#provider_all_agent.AgentTrueName",
id: "ID",
v: "uniplat_version",
title: "ItemName",
value: "Amount",
createdTime: "CreatedDate",
bankAccountName: "ProviderHandleId#ServiceProviderBank.AccountName",
OpenningBankName: "ProviderHandleId#ServiceProviderBank.OpenningBankName",
bankAccountNo: "ProviderHandleId#ServiceProviderBank.AccountNo",
paymentItemId: "PaymentItemId",
paymentDate: "PaymentDate",
};
export const enum ChatOpenDirection {
/**
* 通知外壳,打开一个新的Webview容器打开会话
*/
NewWebview,
/**
* 直接会用当前Webview,跳转到聊天页面
*/
Current,
}
export const enum SrcPlatform {
Backend = 1,
Website,
H5,
OtherApp,
QqxbApp,
}
export const enum OperationType {
User = 1,
Backend,
}
export interface UploadImageItem {
id: number;
v: number;
url: string;
fileName: string;
time: string;
fileSize: number;
}
......@@ -49,11 +49,6 @@
"debug": "=3.1.0"
}
},
"moment": {
"version": "2.29.1",
"resolved": "http://npm.job.qinqinxiaobao.com/moment/-/moment-2.29.1.tgz",
"integrity": "sha1-sr52n6MZQL6e7qZGnAdeNQBvo9M="
},
"ms": {
"version": "2.0.0",
"resolved": "http://npm.job.qinqinxiaobao.com/ms/-/ms-2.0.0.tgz",
......@@ -98,9 +93,9 @@
"integrity": "sha1-i0vEr1GM+r0Ec65PmRRCh7M+uIE="
},
"xchat-client": {
"version": "2.2.2",
"resolved": "http://npm.job.qinqinxiaobao.com/xchat-client/-/xchat-client-2.2.2.tgz",
"integrity": "sha512-cGuq/BqVNMkdiAI7LXRw8DfGJkVGkE6zc9O7wjbSMTcEw+5FnVYQlb+z1kpfrRNsM5BGoD8u6FiGAmR43GEmsw==",
"version": "2.2.4",
"resolved": "http://npm.job.qinqinxiaobao.com/xchat-client/-/xchat-client-2.2.4.tgz",
"integrity": "sha512-czqfNdgcohEXlzc4xZlTEoQgluDTCHKTFokmwfbJbivBUUBBeJ2UD8bJAJ8ObMso55UjwCDrP2BIrvwUnztGfQ==",
"requires": {
"wamp.js": "0.3.2"
}
......
......@@ -2,10 +2,9 @@
"dependencies": {
"@types/sha1": "^1.1.2",
"axios": "^0.19.2",
"moment": "^2.29.1",
"qs": "~6.9.3",
"sha1": "^1.1.1",
"vuex-class": "^0.3.2",
"xchat-client": "2.2.2"
"xchat-client": "^2.2.4"
}
}
import { TokenStringGetter } from "../model";
import { invokeGet } from "./request";
export interface EmojiItem {
code: string;
name: string;
emoji_chars: string;
}
export interface EmojiResult {
type: string;
list: EmojiItem[];
}
let cacheEmoji: EmojiResult | null = null;
export class EmojiService {
private static ready = false
private static token: TokenStringGetter
private static beforeReadyCacheAction: Function[] = []
private url = ""
public constructor() {
this.url =
process.env.NODE_ENV === "production"
? "https://file.teammix.com"
: "";
}
private static ready = false;
private static token: TokenStringGetter;
private static beforeReadyCacheAction: Function[] = [];
private readonly url = "https://file.teammix.com";
public async getEmoji() {
if (cacheEmoji) {
return Promise.resolve(cacheEmoji);
}
const token = await EmojiService.token();
return invokeGet<{
type: string;
list: {
code: string;
name: string;
emoji_chars: string;
}[];
}>(`${this.url}/v1/emoji/list?type=chat`, token);
return invokeGet<EmojiResult>(
`${this.url}/v1/emoji/list?type=chat`,
token
).then((o) => (cacheEmoji = o));
}
public static onReady(callback: () => void) {
......
export * from "./upload"
\ No newline at end of file
import Axios, { AxiosResponse, AxiosAdapter } from "axios";
import { UniplatSdk } from "uniplat-sdk";
import { ImEnvironment } from "../model";
export const enum Product {
Default = "default",
QqxbWeixin = "qqxb-weixin",
QqxbApp = "qqxb-app",
Fulibao = "fulibao",
HrManager = "hr-manager",
Hrs100 = "hrs100",
HrsApp = "hrs-app",
BiJie = "bi-jie",
Cashier = "cashier",
Uniplat = "uniplat",
DeShengJiuYeBao = "de-sheng-jiu-ye-bao",
}
const enum ProductTable {
Default = "",
Login = "login",
Error = "error",
}
export interface SdkMonitorOption {
userAgent?: boolean;
envir: ImEnvironment;
product: Product;
call?: (r: any) => void;
}
class WebMonitor {
private key = "";
private envir = ImEnvironment.Dev;
private product = Product.Default;
private readonly url = "https://pre-hrs-monitor.hrs100.com";
private adapter: AxiosAdapter | undefined;
public updateKey(key: string) {
this.key = key;
return this;
}
private buildHeaders() {
return {
headers: { authorization: "cdd0a34e-f537-4e5b-808e-2ba06af21845" },
adapter: this.adapter,
};
}
private enable() {
return this.envir !== ImEnvironment.Dev;
}
private envirString() {
if (this.envir === ImEnvironment.Dev) {
return "[Dev]";
}
if (this.envir === ImEnvironment.Stage) {
return "[Stage]";
}
return "";
}
public log(msg: any) {
return Axios.post(
this.url,
{
type: ProductTable.Login,
product: this.product,
msg: `${this.envirString()} ${msg} `,
key: this.key,
},
this.buildHeaders()
);
}
public error(msg: any) {
return Axios.post(
this.url,
{
type: ProductTable.Error,
product: this.product,
msg: `${this.envirString()} ${msg} `,
key: this.key,
},
this.buildHeaders()
);
}
public useSdk(
sdk: UniplatSdk,
options: SdkMonitorOption,
adapter?: AxiosAdapter
) {
this.envir = options.envir;
this.product = options.product;
this.adapter = adapter;
sdk.events.addUniversalErrorResponseCallback(
(r: AxiosResponse<any>) => {
options.call && options.call(r);
if (this.enable()) {
const msg: string[] = [];
msg.push(
`URL: ${decodeURIComponent(r.config.url as string)}`
);
msg.push(`Token: ${sdk.global.jwtToken}`);
const header = r.config.headers;
if (header) {
msg.push(`CurrentOrg: ${header.CurrentOrg}`);
msg.push(`Scenes: ${header.Scenes}`);
}
options &&
options.userAgent &&
msg.push(`UserAgent: ${window.navigator.userAgent}`);
r.config &&
r.config.params &&
msg.push(`Params: ${JSON.stringify(r.config.params)}`);
if (r.config && r.config.data) {
const form = r.config.data as FormData;
if (form.getAll) {
const p = form.getAll("parameters");
for (const item of p) {
msg.push(`Payload: ${item}`);
}
} else {
msg.push(
`Payload: ${JSON.stringify(r.config.data)}`
);
}
}
msg.push(
`Exception: ${(
(r.data.error as string) || ""
).substring(0, 500)}`
);
r && r.config && this.error(msg.join("\n"));
}
}
);
}
}
export const monitor = new WebMonitor();
import Chat from "@/customer-service/xim";
import { metaRow, UniplatSdkExtender, UniplatSdk } from "uniplat-sdk";
import {
OperationType,
OrderStatus,
OrderTableListItem,
orderPredict,
SrcPlatform,
OrderPayItem,
UploadImageItem,
orderPayItemPredict,
OrderComment,
PayStatus,
} from "../model";
import { GeneralOrderDirection } from "../model/order-product";
class OrderService {
public readonly generalOrder = "general_order_info";
public readonly generalOrderPaymentModel = "general_order_payment_info";
public readonly generalOrderDefaultDetailName = "userOrderDetail";
private readonly commentModel = "uniplat_model_remark@general_order";
private readonly fileModel = "general_order_payment_file_info";
private readonly handler = new UniplatSdkExtender();
private innerSdk: UniplatSdk | null = null;
public injectSdk(sdk: UniplatSdk) {
this.innerSdk = sdk;
return this;
}
private getSdk() {
return this.innerSdk || Chat.getSdk();
}
public openOrder(params: {
productCode: string;
srcPlatform: SrcPlatform;
createdType: OperationType;
/**
* 是否强制开启新订单,默认如果有已存在处理中的订单时会直接返回
*/
forceNewOrder?: boolean;
}) {
return this.getSdk()
.model(this.generalOrder)
.action(params.forceNewOrder ? "addOrder" : "getOrAddOrder")
.addInputs_parameter({
ProductCode: params.productCode,
SrcPlatform: params.srcPlatform,
CreatedType: params.createdType,
})
.execute();
}
public updateOrderStatus(
id: number | string,
v: number,
status: OrderStatus
) {
return this.getSdk()
.model(this.generalOrder)
.action("editStatus")
.addInputs_parameter({
Status: status,
userType: OperationType.User,
})
.updateInitialParams({ selected_list: [{ v, id: +id }] })
.execute();
}
/** 专项工单 */
public getOrders(
params: {
productCode?: GeneralOrderDirection;
index: number;
size?: number;
},
isHrs = false
) {
const list = this.getSdk()
.model(this.generalOrder)
.list(isHrs ? "hroOrgOrderList" : "userOrderList");
if (params.productCode) {
list.addPrefilter({ "ProductId#product.Code": params.productCode });
}
return list
.query({ item_size: params.size || 100, pageIndex: params.index })
.then((r) => {
return {
total: r.pageData.record_count,
list: this.handler.buildRows<OrderTableListItem>(
r.pageData.rows,
orderPredict
),
};
});
}
public getProcessOrders(params: {
productCode?: GeneralOrderDirection;
index: number;
size?: number;
}) {
const list = this.getSdk()
.model(this.generalOrder)
.list("hroOrgDoingOrderList");
if (params.productCode) {
list.addPrefilter({ "ProductId#product.Code": params.productCode });
}
return list
.query({ item_size: params.size || 100, pageIndex: params.index })
.then((r) => {
return {
total: r.pageData.record_count,
list: this.handler.buildRows<OrderTableListItem>(
r.pageData.rows,
orderPredict
),
};
});
}
/**
* 获取订单支付记录
* @param id 订单id
* @param withActions 是否获取行数据中action条目
*/
public getPayments(id: number, withActions = false) {
return this.getSdk()
.model(orderService.generalOrderPaymentModel)
.list(withActions ? "" : "userOrderPaymentList")
.addPrefilter({ OrderId: id })
.query({ pageIndex: 1, item_size: 100 })
.then((r) => {
if (r && r.pageData && r.pageData.rows) {
let items = this.handler.buildRows<OrderPayItem>(
r.pageData.rows,
orderPayItemPredict
);
if (!withActions) {
items = items.filter(
(i) =>
i.status !== PayStatus.Deleted &&
i.status !== PayStatus.Cancel
);
}
if (withActions) {
for (let i = 0; i < r.pageData.rows.length; i++) {
r.pageData &&
r.pageData.rows &&
r.pageData.rows[i] &&
items[i] &&
(items[i].actions = r.pageData.rows[i].actions);
}
}
return items;
}
return [];
});
}
public getPayment(id: string | number) {
return this.getSdk()
.model(orderService.generalOrderPaymentModel)
.detail(id as string, "userOrderPaymentDetail")
.query()
.then((r) =>
this.handler.buildRow<OrderPayItem>(r.row, orderPayItemPredict)
);
}
public addImage4Payment(payment: number | string, v: number, path: string) {
return this.getSdk()
.model(orderService.generalOrderPaymentModel)
.action("addImages")
.updateInitialParams({
selected_list: [{ v, id: payment as number }],
})
.addInputs_parameter({ images: path })
.execute();
}
public getImages4Payment(payment: number) {
return this.getSdk()
.model(this.fileModel)
.list()
.addPrefilter({ PaymentId: payment })
.query({ item_size: 10, pageIndex: 1 })
.then((r) =>
this.handler.buildRows<UploadImageItem>(r.pageData.rows, {
time: "CreatedDate",
fileSize: "FileSize",
fileName: "FileName",
url: "Url",
id: "ID",
v: "uniplat_version",
})
);
}
public deleteImage(payment: number, image: number, v: number) {
return this.getSdk()
.model(this.fileModel)
.action("delete")
.updateInitialParams({
selected_list: [{ v, id: image }],
prefilters: [{ property: "PaymentId", value: payment }],
})
.execute();
}
public buildOrder(o: metaRow) {
return this.handler.buildRow<OrderTableListItem>(o, orderPredict);
}
/** 设置备注 */
public setRemark(
id: string,
v: number,
data: { Title: string; Remark: string }
) {
return this.getSdk()
.model(this.generalOrder)
.action("editTitle")
.updateInitialParams({ selected_list: [{ v, id: +id }] })
.addInputs_parameter(data)
.execute();
}
public getOrderDetail(id: number | string) {
return this.getSdk()
.model(this.generalOrder)
.detail(id as string)
.query();
}
public sendPayAccountInfo(params: { send: string; type?: number }) {
return this.getSdk()
.domainService("hro_spview", "OrderSetting", "sendPayAccountInfo")
.request("get", { params });
}
public sendPayAccountInfoForAgent(params: {
send: string;
userOrderPaymentId: number;
}) {
return this.getSdk()
.domainService(
"hro_spview",
"OrderSetting",
"sendPayAccountInfoForAgent"
)
.request("get", { params });
}
public getComments(order: number | string) {
return this.getSdk()
.model(orderService.commentModel)
.list("system_remark")
.addPrefilter({ associate_id: order })
.query({ pageIndex: 1, item_size: 100 })
.then((r) => {
if (r && r.pageData && r.pageData.rows) {
const items = this.handler.buildRows<OrderComment>(
r.pageData.rows,
{
id: "",
time: "create_time",
attachment: "attachments_label",
user: "dealer_name",
content: "",
}
);
return items;
}
return [];
});
}
}
export const orderService = new OrderService();
import Axios from "axios";
import qs from "qs";
import chat from "../xim/index";
export function buildConfig(token: string, url: string) {
......@@ -9,6 +8,7 @@ export function buildConfig(token: string, url: string) {
}
return { headers: { Authorization: token } };
}
export function invokeGet<T>(url: string, token: string) {
return new Promise<T>((resolve, reject) => {
Axios.get(url, buildConfig(token, url))
......
import Axios from 'axios';
const enum Status {
Default,
Upgrading
}
export class UpgradeController {
public static getUpgradingRange() {
return Axios
.get<{ code: Status; start: string; end: string }>(
`https://serverstatus.qinqinxiaobao.com/v0/servicestatus/apiservice/status/?t=${new Date().valueOf()}`
).then(r => {
if (r && r.data && r.data.code === Status.Upgrading) {
return { start: r.data.start, end: r.data.end };
}
return { start: '', end: '' };
});
}
}
import Vue from "vue";
import { UniplatSdk } from "uniplat-sdk"
import Chat from "@/customer-service/xim";
const orgId = () => Vue.prototype.global.org?.id ?? "0";
export async function uploadFile(file: File, uploading?: (p: number) => void) {
const sdk = Chat.getSdk();
const { url } = await sdk.uploadFileV2(file, {
onUploadProgress: (e) => uploading && uploading(e.percent),
});
return `${sdk.global.baseUrl}${url}`;
}
export const enum UploadType {
Default,
Image,
Camera
}
/**
* x
* @param type UploadType 默认值 UploadType.Default
* @param size 可不传 单位M
* @returns
*/
export function chooseFileAndUpload(type = UploadType.Default, size?: number) {
return chooseFile(type).then(r => {
const sdk = Chat.getSdk();
return sdk.uploadFileV2(r)
})
}
export async function uploadFile(file: File) {
let { url } = await (Vue.prototype.sdk as UniplatSdk).uploadFileV2(file)
const realUrl = `${Vue.prototype.sdk.global.baseUrl}${url}`;
return realUrl;
/**
*
* @param type UploadType
* @param size 可不传 单位M
* @returns
*/
export function chooseFile(type = UploadType.Default, size?: number) {
return new Promise<File>((resolve, reject) => {
const target = document.createElement('input');
target.setAttribute('type', 'file');
// 添加这个属性,就可以唤起相机的功能
(type === UploadType.Camera) && target.setAttribute('capture', 'camera');
// 这里如果不加属性 accept 是 "image/*" 或者 "video/*",就默认打开摄像头,既可以拍照也可以录像
(type === UploadType.Image) && target.setAttribute('accept', 'image/*');
target.setAttribute('style', 'display:none');
// 监听改变事件
target.addEventListener('change', (e: Event) => {
// 拿到文件对象
if (e && e.target) {
const t = e.target as HTMLInputElement;
const { files } = t;
if (files) {
// 返回的是一个文件对象
if (size && files[0].size >= size * 1024 * 1024) {
reject(`上传的${type === UploadType.Default ? "文件" : "图片"}太大了~`);
return;
}
resolve(files[0]);
setTimeout(() => target.remove(), 200);
return;
}
}
reject(new Error('系统不支持'));
setTimeout(() => target.remove(), 200);
});
document.body.appendChild(target);
// 这里是模拟点击了input控件
target.click();
});
}
\ No newline at end of file
......@@ -3,20 +3,20 @@ import { Module } from "vuex";
import { dbController } from "../database";
import {
ChatMember,
MessageType,
ServiceType,
MessageHandled,
RawChatItem,
BaseChatItemBusinessData,
MessageType,
} from "../model";
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 { ChatUserInfoService } 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";
......@@ -24,27 +24,28 @@ import { RootStoreState } from "@/store/model";
export const ns = ChatStore.ns;
const sdk = Chat.getSdk;
const UniplatChatModelName = "UniplatChat";
const model = () => sdk().model(UniplatChatModelName);
const model = () => Chat.getSdk().model(UniplatChatModelName);
const orgId = () => Chat.getOrgId() as string;
function uniqueMessages(
messages: NonNullable<ChatStore.STATE_CHAT_MSG_HISTORY>
) {
const arr = [...messages];
return unique(arr, function(item, all) {
return all.findIndex((k) => k.id === item.id);
});
}
function filterMessages(
messages: NonNullable<ChatStore.STATE_CHAT_MSG_HISTORY>,
chatid: number
function combineMessages(
items1: ChatStore.STATE_CHAT_MSG_HISTORY,
items2: ChatStore.STATE_CHAT_MSG_HISTORY,
chat: number
) {
return uniqueMessages(Array.from(messages)).filter(
(k) => k.chat_id === chatid
);
const set = new Set<number>();
const items: Message[] = [];
for (const item of [...(items1 || []), ...(items2 || [])]) {
if (item.chat_id !== chat) {
continue;
}
const id = item.id;
if (!set.has(id)) {
set.add(id);
items.push(item);
}
}
return items.sort((x, y) => (x.id - y.id ? 1 : -1));
}
let removeRegisterChatEvents: (() => void)[] = [];
......@@ -54,13 +55,15 @@ async function preCacheImgs(msgs?: any[]) {
return Promise.resolve();
}
await Promise.all(
msgs.map((k) => {
msgs
.filter(i => i.id > 0)
.map(k => {
return new Promise((resolve: (p: void) => void) => {
if (k.type === "image") {
const msg = JSON.parse(k.msg);
const url = msg.url;
if (!isAccessibleUrl(url)) {
resolve();
return resolve();
}
if (
url &&
......@@ -83,47 +86,82 @@ async function preCacheImgs(msgs?: any[]) {
}
function buildChatItem(chat: RawChatItem) {
if (!chat.model_name && chat.business_data) {
if ((!chat.model_name || !chat.obj_id) && chat.business_data) {
const b = JSON.parse(chat.business_data) as BaseChatItemBusinessData;
chat.model_name = b.model_name;
b.obj_id && (chat.obj_id = b.obj_id);
}
if (!chat.detail_name && chat.business_data) {
const b = JSON.parse(chat.business_data) as BaseChatItemBusinessData;
b.detail_name && (chat.detail_name = b.detail_name);
}
// 系统发的自动消息,重新校准未读消息数为0
if (!+chat.last_msg_sender && chat.msg_id === 1) {
chat.unread_msg_count = 0;
}
return { ...chat, chat_id: chat.id } as ChatType;
}
const chatType = "group";
const allowedChatTypes = [chatType, "notify"]
const allowedChatTypes = [chatType, "notify"];
const filterActiveChats = (items: RawChatItem[]) => {
return items.filter(
(i) =>
export const filterActiveChats = (items: RawChatItem[]) => {
return items
.filter(
i =>
!i.is_finish &&
!i.is_exited &&
!i.is_remove &&
!i.is_deleted &&
allowedChatTypes.includes(i.type)
);
)
.sort((x, y) => y.last_msg_ts - x.last_msg_ts);
};
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;
}
let loadingChatList = false;
let cachedLoadingChatListAction: ((value: ChatType[]) => void)[] = [];
function clearAction(value: ChatType[]) {
for (const item of cachedLoadingChatListAction) {
item(value);
}
cachedLoadingChatListAction = [];
return value;
}
export default {
namespaced: true,
state: () => ({
[ChatStore.STATE_CHAT_DIALOG_VISIBLE]: false,
[ChatStore.STATE_CHAT_DIALOG_IS_SINGLE]: false,
[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]: false,
[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR]: null,
[ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID]: null,
[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,
[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]: 0,
[ChatStore.STATE_CHAT_MY_ID]: null,
[ChatStore.STATE_CHAT_MY_UID]: null,
[ChatStore.STATE_CHAT_SOURCE]: ServiceType.Frontend,
[ChatStore.STATE_CURRENT_CHAT_MEMBERS]: null,
[ChatStore.STATE_ALL_HISTORY_CHAT_MEMBERS]: null,
[ChatStore.STATE_CURRENT_CHAT_TITLE]: "",
[ChatStore.STATE_FUNC_SCROLL_TO_BOTTOM]: () => true,
[ChatStore.STATE_FUNC_ON_NEW_MSG]: () => true,
......@@ -154,13 +192,13 @@ export default {
state[ChatStore.STATE_CHAT_MSG_HISTORY] = null;
},
[ChatStore.MUTATION_CLEAR_CURRENT_CHAT_ID](state) {
state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID] = null;
state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID] = 0;
},
[ChatStore.MUTATION_SAVE_CURRENT_CHAT_ID](
state,
id: ChatStore.STATE_CHAT_CURRENT_CHAT_ID
) {
state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID] = Number(id);
state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID] = id;
},
[ChatStore.MUTATION_CLEAR_CURRENT_CHAT_VERSION](state) {
state[ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION] = null;
......@@ -186,17 +224,37 @@ export default {
) {
const old = state[ChatStore.STATE_CHAT_MSG_HISTORY] || [];
const chatid = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatid == null) return;
if (chatid) {
// 移除撤回的消息
const newItems = data || [];
const hasWithdraw = newItems.find(
i => i.type === MessageType.Withdraw
);
const withdraw = hasWithdraw
? newItems
.filter(i => i.type === MessageType.Withdraw)
.map(i => +i.msg)
: [];
const filterout = withdraw.length
? newItems.filter(i => !withdraw.includes(i.id))
: newItems;
state[ChatStore.STATE_CHAT_MSG_HISTORY] = Object.freeze(
filterMessages([...old, ...(data || [])], chatid)
combineMessages(old, filterout, chatid)
);
}
},
[ChatStore.MUTATION_SAVE_MYSELF_ID](state) {
Chat.getToken().then((token) => {
const eid = decode(token);
if (!state[ChatStore.STATE_CHAT_MY_ID]) {
Chat.getToken().then(token => {
if (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;
}
});
}
},
[ChatStore.MUTATION_CLEAR_MYSELF_ID](state) {
state[ChatStore.STATE_CHAT_MY_ID] = null;
......@@ -216,10 +274,10 @@ export default {
data: ChatStore.STATE_CHAT_MSG_HISTORY
) {
const old = state[ChatStore.STATE_CHAT_MSG_HISTORY] || [];
const chatid = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatid == null) return;
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (!chatId) return;
state[ChatStore.STATE_CHAT_MSG_HISTORY] = Object.freeze(
filterMessages([...(data || []), ...old], chatid)
combineMessages(data || [], old, chatId)
);
},
[ChatStore.MUTATION_SAVE_CURRENT_CHAT_MEMBERS](
......@@ -296,7 +354,7 @@ export default {
ChatStore.STATE_CHAT_SENDING_MESSAGES
] as ChatStore.STATE_CHAT_SENDING_MESSAGE[];
if (current) {
const target = current.find((i) => i.id === payload);
const target = current.find(i => i.id === payload);
if (target) {
const index = current.indexOf(target);
current.splice(index, 1);
......@@ -311,7 +369,7 @@ export default {
ChatStore.STATE_CHAT_SENDING_MESSAGES
] as ChatStore.STATE_CHAT_SENDING_MESSAGE[];
if (current) {
const target = current.find((i) => i.id === payload);
const target = current.find(i => i.id === payload);
if (target) {
target.status = -1;
}
......@@ -323,10 +381,12 @@ export default {
const setTimeoutId: { [key: string]: number } = {};
return (
state: ChatStoreState,
payload: Parameters<ChatStore.MUTATION_SAVE_CURRENT_CHAT_INPUTING>[0]
payload: Parameters<
ChatStore.MUTATION_SAVE_CURRENT_CHAT_INPUTING
>[0]
) => {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return;
if (!chatId) return;
if (payload.chat_id !== chatId) return;
const arr = state[ChatStore.STATE_CURRENT_CHAT_INPUTING];
const eid = payload.eid;
......@@ -336,18 +396,19 @@ export default {
} else {
arr.push(eid);
}
setTimeoutId[eid] = setTimeout(() => {
arr.splice(arr.indexOf(eid), 1);
}, 4000);
setTimeoutId[eid] = setTimeout(
() => arr.splice(arr.indexOf(eid), 1),
4000
);
};
})(),
[ChatStore.MUTATION_CLEAR_CURRENT_CHAT_INPUTING]: (state) => {
[ChatStore.MUTATION_CLEAR_CURRENT_CHAT_INPUTING]: state => {
state[ChatStore.STATE_CURRENT_CHAT_INPUTING] = [];
},
[ChatStore.MUTATION_INITING_CHAT]: (state) => {
[ChatStore.MUTATION_INITING_CHAT]: state => {
state[ChatStore.STATE_CURRENT_CHAT_INITING] = true;
},
[ChatStore.MUTATION_INITING_CHAT_DONE]: (state) => {
[ChatStore.MUTATION_INITING_CHAT_DONE]: state => {
state[ChatStore.STATE_CURRENT_CHAT_INITING] = false;
},
[ChatStore.MUTATION_CHAT_UPDATE_IS_MEMBER]: (state, v: boolean) => {
......@@ -365,123 +426,171 @@ export default {
) => {
Vue.set(state[ChatStore.STATE_CHAT_USERNAME], param.id, param.name);
},
[ChatStore.MUTATION_WITHDRAW]: (state, id: number) => {
[ChatStore.MUTATION_WITHDRAW]: (state, ids: number[]) => {
const old = state[ChatStore.STATE_CHAT_MSG_HISTORY] || [];
const chatid = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatid == null) return;
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (!chatId) return;
state[ChatStore.STATE_CHAT_MSG_HISTORY] = old.filter(
(i) => i.id !== id
i => !ids.includes(i.id)
);
},
},
actions: {
async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit, state }) {
let cache = await dbController.getChatList();
async [ChatStore.ACTION_GET_MY_CHAT_LIST]({ commit, dispatch, state }) {
commit(ChatStore.MUTATION_SAVE_MYSELF_ID);
const buildUnreadMessage = (items: ChatType[]) => {
let sum = 0;
items.forEach((i) => (sum += i.unread_msg_count));
state[ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT] = sum;
return items;
};
if (cache && cache.length) {
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, {
list: cache,
total: 9999,
});
const ts = cache
.map((i) => Math.max(i.last_msg_ts, i.update_time))
.sort();
const last = ts[ts.length - 1];
await xim.fetchChatListAfter(last)!.then((r) => {
const list = filterActiveChats(r.args[0] as RawChatItem[]);
const items = list.map((i) => buildChatItem(i));
if (items && items.length) {
cache = dbController.mergeChatList(cache, items);
}
});
return buildUnreadMessage(cache);
if (loadingChatList) {
return new Promise<ChatType[]>(resolve =>
cachedLoadingChatListAction.push(resolve)
);
}
const data = await xim.fetchChatList();
if (data == null) return;
const chatList = filterActiveChats(data.args[0] as RawChatItem[]);
const items = chatList.map((chat) => buildChatItem(chat));
loadingChatList = true;
const execute = () =>
new Promise<ChatType[]>((resolve, reject) => {
Chat.onReady(() => {
xim.fetchChatList()
.then(data => {
if (!data) {
return resolve([]);
}
const chatList = filterActiveChats(
data.args[0] as RawChatItem[]
);
const items = chatList.map(chat =>
buildChatItem(chat)
);
dbController.saveChatList(items);
commit(ChatStore.MUTATION_SAVE_CHAT_LIST, {
list: items,
total: 9999,
commit(
ChatStore.MUTATION_SAVE_CHAT_LIST,
items
);
dispatch(
ChatStore.ACTION_REBUILD_UNREAD_MESSAGE_COUNT
);
resolve(items);
})
.catch(reject);
});
return buildUnreadMessage(items);
}).finally(() => (loadingChatList = false));
return await execute().then(d => clearAction(d));
},
async [ChatStore.ACTION_REBUILD_UNREAD_MESSAGE_COUNT]({ state }) {
let items = await dbController.getChatList();
[ChatStore.ACTION_REBUILD_UNREAD_MESSAGE_COUNT]({ state }) {
const items = state[ChatStore.STATE_MY_CHAT_ROOM_LIST];
if (items) {
let sum = 0;
items.forEach((i) => (sum += i.unread_msg_count));
items.forEach(i => (sum += i.unread_msg_count));
state[ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT] = sum;
}
},
// async [ChatStore.ACTION_JOIN_CHAT](
// { commit },
// chatId: Parameters<ChatStore.ACTION_JOIN_CHAT>[0]
// ) {
// // return await XimService.getInstance().joinChat(chatId)
// },
async [ChatStore.ACTION_GET_CHAT_MESSAGES]({ state, commit }) {
async [ChatStore.ACTION_UPDATE_CHAT_UNREAD_MESSAGE_COUNT](
{ dispatch, state },
p: { chat: number; unread: number }
) {
const list = state[ChatStore.STATE_MY_CHAT_ROOM_LIST] as ChatType[];
const t = list.find(i => i.id === p.chat);
t && (t.unread_msg_count = p.unread);
return dbController
.updateChat4UnreadCount(p.chat, p.unread)
.then(() =>
dispatch(ChatStore.ACTION_REBUILD_UNREAD_MESSAGE_COUNT)
)
.finally(() => {
if (!p.unread) {
const messages =
state[ChatStore.STATE_CHAT_MSG_HISTORY];
if (messages && messages.length) {
xim.setRead(
p.chat,
messages[0].id,
messages[messages.length - 1].id
);
}
}
});
},
async [ChatStore.ACTION_GET_CHAT_MESSAGES]({ state, commit, getters }) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return;
const chat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
const isMember = state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER];
if (!chatId) return;
let data: Message[] = [];
const getMessages = async () =>
await xim.queryLastPageMsg(chatType, chatId, 20, {
model: chat.model_name,
obj: chat.obj_id,
isMember,
});
if (isMember) {
const cache = await dbController.getChatMessages(chatId);
if (cache && cache.length) {
data = cache;
} else {
data = await xim.queryLastPageMsg(
chatType,
chatId,
20,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
);
data = await getMessages();
}
try {
} else {
data = await getMessages();
}
commit(ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY, data);
dbController.saveChatMessages(chatId, data);
await preCacheImgs(data);
commit(ChatStore.MUTATION_SCROLL_TO_BOTTOM);
return data;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
},
async [ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID](
{ state, commit },
msgId: Parameters<ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID>[0]
{ state, commit, getters },
msgId: Parameters<
ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID
>[0]
) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return;
if (!chatId) return;
const chat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
const o = {
model: chat.model_name,
obj: chat.obj_id,
isMember: state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER],
};
const data = await xim.queryPrevPageMsg(
chatType,
chatId,
msgId,
10,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
o
);
commit(ChatStore.MUTATION_UNSHIFT_CHAT_MSG_HISTORY, data);
dbController.appendMessages(chatId, data);
return data;
},
async [ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID](
{ state, commit },
msgId: Parameters<ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID>[0]
{ state, commit, getters },
msgId: Parameters<
ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID
>[0]
) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return;
if (!chatId) return;
const chat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
const o = {
model: chat.model_name,
obj: chat.obj_id,
isMember: state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER],
};
const data = await xim.queryNextPageMsg(
chatType,
chatId,
msgId,
10,
!state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER]
o
);
commit(ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY, data);
dbController.appendMessages(chatId, data);
......@@ -491,14 +600,18 @@ export default {
{ state, dispatch, getters, commit },
params: Parameters<ChatStore.ACTION_SEND_MESSAGE>[0]
) {
const uniplatId =
state[ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID];
if (uniplatId == null) return;
if (Chat.getServiceType() === ServiceType.Backend) {
if (!state[ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID]) {
return Promise.reject(`No Uniplat Id Found`);
}
}
try {
const chat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
const data = await sdk()
const chat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
const data = await Chat.getSdk()
.model(chat.model_name)
.chat(Number(chat.obj_id), orgId())
.chat(chat.obj_id, orgId())
.sendMsg(params.msgType, params.msg);
await dispatch(ChatStore.ACTION_GET_FRESH_MESSAGE);
return data;
......@@ -508,7 +621,7 @@ export default {
ChatStore.MUTATION_SAVE_SEND_FAIL_MESSAGE,
JSON.stringify(params)
);
console.error("testing 信息发送失败", error);
return Promise.reject(error);
}
},
async [ChatStore.ACTION_GET_FRESH_MESSAGE]({
......@@ -521,9 +634,10 @@ export default {
if (msgs == null || msgs.length === 0) {
newMsgsArr = await dispatch(ChatStore.ACTION_GET_CHAT_MESSAGES);
} else {
const id = getLastMessageId(msgs);
newMsgsArr = await dispatch(
ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID,
msgs[msgs.length - 1].id
id
);
}
// const lastMsg = newMsgsArr[newMsgsArr.length - 1];
......@@ -532,29 +646,32 @@ export default {
},
async [ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN](
{ commit, dispatch },
params: Parameters<ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN>[0]
params: Parameters<
ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN
>[0]
) {
const { imChatId } = await sdk()
const { imChatId, catalog } = await Chat.getSdk()
.model(params.modelName)
.chat(+params.selectedListId, orgId())
.chat(params.selectedListId, orgId())
.createChat();
const chatId = Number(imChatId);
const chatId = +imChatId;
await dispatch(ChatStore.ACTION_GET_MY_CHAT_LIST);
commit(ChatStore.MUTATION_SHOW_CHAT, !params.showByPage);
await dispatch(
ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION,
chatId
);
return { chatId, catalog };
},
async [ChatStore.ACTION_CREATE_NEW_CHAT_BY_CLIENT](
{ commit, dispatch },
params: Parameters<ChatStore.ACTION_CREATE_NEW_CHAT_BY_CLIENT>[0]
) {
const { imChatId } = await sdk()
const { imChatId } = await Chat.getSdk()
.model(params.modelName)
.chat(+params.selectedListId, orgId())
.createChat(true);
const chatId = Number(imChatId);
.chat(params.selectedListId, orgId())
.createChat(true, params.title);
const chatId = +imChatId;
await commit(ChatStore.MUTATION_SHOW_CHAT, true);
await dispatch(
ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION,
......@@ -562,39 +679,87 @@ export default {
);
// 打开会话后获取一下会话列表,刷新未读消息
dispatch(ChatStore.ACTION_GET_MY_CHAT_LIST);
return chatId;
},
async [ChatStore.ACTION_REGISTER_EVENT]({ dispatch, commit, state }) {
async [ChatStore.ACTION_REGISTER_EVENT]({
dispatch,
commit,
state,
getters,
}) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
const onNewMsg = (e: Message) => {
if (e.chat_id === chatId) {
dispatch(ChatStore.ACTION_GET_FRESH_MESSAGE);
const thenAction = () => {
if (
e.type === MessageType.Withdraw &&
// 这里再取一次当前chatId避免数据和当前不一致
e.chat_id ===
state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]
) {
commit(
ChatStore.MUTATION_WITHDRAW,
xim.withDrawMsgHandle(e)
);
}
const scroll = () =>
state[ChatStore.STATE_FUNC_ON_NEW_MSG](e);
if (e.chat_id === chatId) {
dispatch(
ChatStore.ACTION_GET_FRESH_MESSAGE
).finally(() => scroll());
} else {
scroll();
}
};
if (e.type === MessageType.Withdraw) {
dbController
.removeMessage(e.chat_id, xim.withDrawMsgHandle(e))
.finally(() => thenAction());
} else {
thenAction();
}
};
if (!chatId) {
xim.off("msg", onNewMsg);
xim.on("msg", onNewMsg);
return;
}
const onMsgRead: ChatNotifyListener = async (e) => {
const onMsgRead: ChatNotifyListener = async e => {
if (chatId !== e.chat_id) return;
const msgs = state[ChatStore.STATE_CHAT_MSG_HISTORY];
if (msgs == null) return;
const oldestMsgId = msgs[0].id - 1;
const lastMsgId = msgs[msgs.length - 1].id + 1;
const chat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
const o = {
model: chat.model_name,
obj: chat.obj_id,
isMember:
state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER],
};
const start = oldestMsgId < 1 ? 1 : oldestMsgId;
const freshMsgs = await xim.queryMsgs(
chatType,
chatId,
oldestMsgId < 1 ? 1 : oldestMsgId,
lastMsgId
start,
lastMsgId,
20,
true,
o
);
dbController.setMessageReaded(chatId, {
start,
end: lastMsgId,
});
commit(ChatStore.MUTATION_CLEAR_CHAT_MSG_HISTORY);
commit(
ChatStore.MUTATION_PUSH_CHAT_MSG_HISTORY,
msgs.map((msg) => {
msgs.map(msg => {
msg = { ...msg };
const newMsg = freshMsgs.find(
(freshMsg) => freshMsg.id === msg.id
freshMsg => freshMsg.id === msg.id
);
if (newMsg != null) {
msg.read_count = newMsg.read_count;
......@@ -603,7 +768,7 @@ export default {
})
);
};
const onInputing: ChatNotifyListener = (e) => {
const onInputing: ChatNotifyListener = e => {
commit(ChatStore.MUTATION_SAVE_CURRENT_CHAT_INPUTING, e);
};
removeRegisterChatEvents.push(() => {
......@@ -622,9 +787,8 @@ export default {
if (!chatId) {
return;
}
const chatList =
state[ChatStore.STATE_MY_CHAT_ROOM_LIST]?.list ?? [];
let wantedChatRoom = chatList.find((k) => k.chat_id === chatId);
const chatList = state[ChatStore.STATE_MY_CHAT_ROOM_LIST];
let wantedChatRoom = chatList.find(k => k.chat_id === chatId);
if (!wantedChatRoom) {
const data = await xim.fetchChat(chatId);
......@@ -641,18 +805,19 @@ export default {
}
if (!state[ChatStore.STATE_CHAT_CURRENT_USER_UID]) {
const userInfo = await sdk().getUserInfo();
const userInfo = await Chat.getSdk().getUserInfo();
commit(ChatStore.MUTATION_SET_CURRENT_USER_UID, userInfo.id);
}
commit(ChatStore.MUTATION_CLEAR_CHAT_MSG_HISTORY);
commit(ChatStore.MUTATION_SAVE_CURRENT_CHAT_ID, chatId);
await getChatModelInfo(
getChatModelInfo(
wantedChatRoom.model_name,
wantedChatRoom.obj_id
wantedChatRoom.obj_id,
wantedChatRoom.detail_name
)
.then((info) => {
.then(info => {
commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_VERSION,
info.uniplat_version
......@@ -664,26 +829,38 @@ export default {
})
.catch(console.error);
commit(ChatStore.MUTATION_INITING_CHAT);
removeRegisterChatEvents.forEach((k) => k());
removeRegisterChatEvents.forEach(k => k());
removeRegisterChatEvents = [];
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
try {
await Promise.all([
dispatch(ChatStore.ACTION_REGISTER_EVENT),
dispatch(ChatStore.ACTION_GET_CHAT_MESSAGES),
]);
} catch (e) {
Chat.error(e);
} finally {
commit(
ChatStore.MUTATION_SAVE_CHAT_TITLE,
wantedChatRoom.title || `会话${chatId}`
wantedChatRoom.title || `在线咨询-${chatId}`
);
commit(ChatStore.MUTATION_INITING_CHAT_DONE);
commit(ChatStore.MUTATION_SCROLL_TO_BOTTOM);
(<any>state)[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR] = null;
state[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_MEMBER] &&
Chat.setRead(
wantedChatRoom.model_name,
wantedChatRoom.obj_id
);
}
},
async [ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA]({ commit }) {
async [ChatStore.ACTION_CLEAR_CURRENT_CHAT_DATA]({ commit, state }) {
commit(ChatStore.MUTATION_CLEAR_CURRENT_CHAT_ID);
commit(ChatStore.MUTATION_CLEAR_MYSELF_ID);
commit(ChatStore.MUTATION_CLEAR_CHAT_MSG_HISTORY);
commit(ChatStore.MUTATION_CLEAR_CHAT_TITLE);
commit(ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS);
(<any>state)[ChatStore.STATE_CHAT_CURRENT_IS_CHAT_ERROR] = null;
},
async [ChatStore.ACTION_GET_CHAT_MEMBERS]({ commit, state }) {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
......@@ -691,11 +868,15 @@ export default {
const getChatMembersResult = await xim.fetchChatMembers(chatId);
if (!getChatMembersResult) return;
const chatMembers = getChatMembersResult.args[0] as ChatMember[];
let newChatMembers = await Promise.all(
chatMembers.map(async (member) => {
let result: NonNullable<ChatStore.STATE_CURRENT_CHAT_MEMBERS>[number];
const all = await Promise.all(
chatMembers.map(async member => {
let result: NonNullable<
ChatStore.STATE_CURRENT_CHAT_MEMBERS
>[number];
try {
const data = await getUserInfo(member.eid);
const data = await ChatUserInfoService.getUserInfo(
member.eid
);
result = {
...member,
...data,
......@@ -708,9 +889,10 @@ export default {
return result;
})
);
newChatMembers = newChatMembers.filter((it) => !it.is_exited);
(<any>state)[ChatStore.STATE_ALL_HISTORY_CHAT_MEMBERS] = all;
const newChatMembers = all.filter(it => !it.is_exited);
const member = newChatMembers.find(
(it) =>
it =>
it.eid ===
String(state[ChatStore.STATE_CHAT_CURRENT_USER_UID])
);
......@@ -721,10 +903,11 @@ export default {
commit(ChatStore.MUTATION_CHAT_UPDATE_IS_MEMBER, false);
commit(ChatStore.MUTATION_CHAT_UPDATE_USER_TYPE, "0");
}
commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_MEMBERS,
unique(newChatMembers, function(item, all) {
return all.findIndex((k) => k.eid === item.eid);
return all.findIndex(k => k.eid === item.eid);
})
);
},
......@@ -744,117 +927,174 @@ 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, firstChat.obj_id);
await getChatModelInfo(
firstChat.model_name,
firstChat.obj_id,
firstChat.detail_name
);
await dispatch(
ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION,
firstChat.chat_id
);
},
async [ChatStore.ACTION_CHAT_START_RECEPTION]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk().model(model_name).chat(obj_id, orgId()).startChat();
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
return new Promise<void>((resolve, reject) => {
Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.startChat()
.catch(reject)
.finally(() =>
dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS)
.then(resolve)
.catch(reject)
);
});
},
async [ChatStore.ACTION_CHAT_FINISH_RECEPTION]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk().model(model_name).chat(obj_id, orgId()).finishChat();
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.finishChat()
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
async [ChatStore.ACTION_CHAT_USER_EXIT]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk().model(model_name).chat(obj_id, orgId()).userExitChat();
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
await dbController.removeChatFromList(currentChat.id);
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.userExitChat()
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
async [ChatStore.ACTION_CHAT_CS_EXIT]({ getters, dispatch }) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk().model(model_name).chat(obj_id, orgId()).csExitChat();
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
await dbController.removeChatFromList(currentChat.id);
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.csExitChat()
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
async [ChatStore.ACTION_CHAT_ADD_MEMBERS](
{ getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_ADD_MEMBERS>[0]
) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk()
.model(model_name)
.chat(obj_id, orgId())
.addMember(uids.map((id) => Number(id)));
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.addMember(uids.map(id => +id))
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
async [ChatStore.ACTION_CHAT_REMOVE_MEMBER](
{ getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_REMOVE_MEMBER>[0]
) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk()
.model(model_name)
.chat(obj_id, orgId())
.removeMember(uids.map((id) => Number(id)));
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, currentChat.org_id)
.removeMember(uids.map(id => +id))
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
async [ChatStore.ACTION_CHAT_ADD_CS](
{ getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_ADD_CS>[0]
) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk()
.model(model_name)
.chat(obj_id, orgId())
.addCs(uids.map((id) => Number(id)));
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.addCs(uids.map(id => Number(id)))
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
async [ChatStore.ACTION_CHAT_REMOVE_CS](
{ getters, dispatch },
uids: Parameters<ChatStore.ACTION_CHAT_REMOVE_CS>[0]
) {
const currentChat = getters[ChatStore.GETTER_CURRENT_CURRENT_CHAT];
if (currentChat == null) return;
const { model_name, obj_id } = currentChat;
if (model_name == null) return;
if (obj_id == null) return;
await sdk()
.model(model_name)
.chat(obj_id, orgId())
.removeCs(uids.map((id) => Number(id)));
await new Promise((resolve) => setTimeout(resolve, 500));
await dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS);
const currentChat = getters[
ChatStore.GETTER_CURRENT_CURRENT_CHAT
] as ChatType;
if (
!currentChat ||
!currentChat.model_name ||
!currentChat.obj_id
) {
return Promise.reject();
}
return await Chat.getSdk()
.model(currentChat.model_name)
.chat(currentChat.obj_id, orgId())
.removeCs(uids.map(id => Number(id)))
.finally(() => dispatch(ChatStore.ACTION_GET_CHAT_MEMBERS));
},
[ChatStore.ACTION_SET_HANDLED](
{ state },
......@@ -865,7 +1105,7 @@ export default {
) {
const msgs = state[ChatStore.STATE_CHAT_MSG_HISTORY];
if (msgs) {
const t = msgs.find((i) => i.id === p.id);
const t = msgs.find(i => i.id === p.id);
if (t) {
t.handled = p.value;
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
......@@ -882,6 +1122,69 @@ export default {
.updateChat(p)
.finally(() => dispatch(ChatStore.ACTION_GET_MY_CHAT_LIST));
},
[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) =>
ChatUserInfoService.getUserInfo(id)
.then(d => {
Vue.set(
state[ChatStore.STATE_CHAT_USERNAME],
id,
d.name
);
resolve({ id, name: d.name });
})
.catch(reject)
);
},
[ChatStore.ACTION_UPDATE_MESSAGE_READ_STATUS]: (
{ state },
option: {
chat: number;
start: number;
end?: number;
all?: boolean;
readed?: number;
}
) => {
const items = state[ChatStore.STATE_CHAT_MSG_HISTORY] as Message[];
if (items) {
if (option.end && option.end > option.start) {
for (let i = option.start; i <= option.end; i++) {
const p = items.find(m => m.id === i);
if (p) {
p.read_count = option.all
? p.total_read_count
: option.readed
? option.readed
: p.read_count + 1;
}
}
} else {
const p = items.find(i => i.id === option.start);
if (p) {
p.read_count = option.all
? p.total_read_count
: option.readed
? option.readed
: p.read_count + 1;
}
}
}
return dbController.setMessageReaded(option.chat, {
start: option.start,
end: option.end,
allRead: option.all,
});
},
},
getters: {
[ChatStore.STATE_CHAT_MSG_HISTORY](state) {
......@@ -895,7 +1198,7 @@ export default {
},
[ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS](state) {
return (state[ChatStore.STATE_CURRENT_CHAT_MEMBERS] ?? []).filter(
(member) => !member.is_exited
member => !member.is_exited
);
},
[ChatStore.STATE_CURRENT_CHAT_TITLE](state) {
......@@ -910,9 +1213,8 @@ export default {
if (singleChat && singleChat.chat_id === chatId) {
return singleChat;
}
const chatList =
state[ChatStore.STATE_MY_CHAT_ROOM_LIST]?.list ?? [];
return chatList.find((chat) => chat.chat_id === chatId);
const chatList = state[ChatStore.STATE_MY_CHAT_ROOM_LIST];
return chatList.find(chat => chat.chat_id === chatId);
},
},
} as Module<ChatStoreState, RootStoreState>;
......@@ -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 = "某个会话聊天记录";
......@@ -33,7 +31,7 @@ export namespace ChatStore {
export type STATE_CHAT_SENDING_MESSAGE = dto.Message;
export const STATE_CHAT_CURRENT_CHAT_ID = "当前chat-id";
export type STATE_CHAT_CURRENT_CHAT_ID = number | null;
export type STATE_CHAT_CURRENT_CHAT_ID = number;
export const STATE_CHAT_CURRENT_CHAT_VERSION = "当前chat的Uniplat version";
export type STATE_CHAT_CURRENT_CHAT_VERSION = number | null;
export const STATE_CHAT_CURRENT_IS_CHAT_MEMBER = "是否是当前chat的成员";
......@@ -44,6 +42,9 @@ export namespace ChatStore {
"当前用户类型状态,25-普通用户,92-客服";
export type STATE_CHAT_CURRENT_USER_TYPE = string | null;
export const STATE_CHAT_CURRENT_IS_CHAT_ERROR = "当前会话是否错误";
export type STATE_CHAT_CURRENT_IS_CHAT_ERROR = number | null;
export const STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID = "当前chat的Uniplat id";
export type STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID = string | null;
......@@ -72,6 +73,11 @@ export namespace ChatStore {
| readonly (dto.ChatMember & dto.ChatMemberExtraInfo)[]
| null;
export const STATE_ALL_HISTORY_CHAT_MEMBERS = "当前会话历史所有参与者";
export type STATE_ALL_HISTORY_CHAT_MEMBERS =
| readonly (dto.ChatMember & dto.ChatMemberExtraInfo)[]
| null;
export const STATE_CURRENT_CHAT_TITLE = "会话标题";
export type STATE_CURRENT_CHAT_TITLE = string;
......@@ -168,7 +174,7 @@ export namespace ChatStore {
export type MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID = () => void;
export const MUTATION_WITHDRAW = "撤回";
export type MUTATION_WITHDRAW = (id: number) => void;
export type MUTATION_WITHDRAW = (ids: number[]) => void;
export const MUTATION_SAVE_MYSELF_ID =
"保存我的id:聊天窗口显示在右边那个人的id";
......@@ -278,14 +284,15 @@ export namespace ChatStore {
selectedListId: string;
uids: string[];
showByPage?: boolean;
}) => Promise<void>;
}) => Promise<{ chat: number; catalog: string }>;
export const ACTION_CREATE_NEW_CHAT_BY_CLIENT = "顾客向客服发起新会话";
export type ACTION_CREATE_NEW_CHAT_BY_CLIENT = (params: {
modelName: string;
selectedListId: string;
uids: string[];
}) => Promise<void>;
title?: string;
}) => Promise<number>;
export const ACTION_CREATE_NEW_CHAT_BY_CLIENT_SIDE =
"startNewConversationByCustomerSide";
......@@ -317,9 +324,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[]) => 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[]) => 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>;
......@@ -344,17 +355,41 @@ export namespace ChatStore {
value: dto.MessageHandled;
}) => void;
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 const ACTION_UPDATE_MESSAGE_READ_STATUS = "更新消息已读状态";
export type ACTION_UPDATE_MESSAGE_READ_STATUS = (option: {
chat: number;
start: number;
end?: number;
all?: boolean;
readed?: number;
}) => Promise<void>;
export interface ChatUpdateParameter {
chat: number;
type?: dto.MessageType;
msg?: string;
ts?: number;
eid?: string;
unread?: number;
set2Read?: boolean;
}
export const ACTION_UPDATE_CHAT = "更新会话信息";
export type ACTION_UPDATE_CHAT = (p: ChatUpdateParameter) => void;
export const ACTION_UPDATE_CHAT_UNREAD_MESSAGE_COUNT =
"更新Chat会话未读消息并重新计数总数";
export type ACTION_UPDATE_CHAT_UNREAD_MESSAGE_COUNT = (p: {
chat: number;
unread: number;
}) => Promise<void>;
}
export interface ChatStoreState {
......@@ -381,7 +416,7 @@ export interface ChatStoreState {
[ChatStore.STATE_CHAT_CURRENT_USER_TYPE]: ChatStore.STATE_CHAT_CURRENT_USER_TYPE;
[ChatStore.STATE_CHAT_SEND_FAIL_MESSAGE]: ChatStore.STATE_CHAT_SEND_FAIL_MESSAGE;
[ChatStore.STATE_CHAT_USERNAME]: ChatStore.STATE_CHAT_USERNAME;
[ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT] : ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT;
[ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT]: ChatStore.STATE_CURRENT_UNREAD_MESSAGE_COUNT;
}
export const chatStore = namespace(ChatStore.ns);
import Chat from "../xim";
import { orderService } from "../service/order";
export type ChatInfo = {
[eid: string]: any;
};
const chatInfo: ChatInfo = {};
export const getChatModel = () => chatInfo;
export async function getChatModelInfo(modelName: string, id: string, detailname?: string) {
if (chatInfo[id] != null) {
return {
uniplatId: chatInfo[id].row.UniplatChatId.value,
chat_id: +chatInfo[id].row.UniplatImChatId.value,
const loadingKeys = new Set<string>();
let waitingAction: { key: string; resolve: (d: ChatModelInfoData) => void }[] =
[];
export interface ChatModelInfoData {
uniplatId: string | number;
chat_id: number | string;
uniplat_version: number;
data: any;
}
const model2DetailNameMapping = new Map<string, string>();
function buildCache() {
if (!model2DetailNameMapping.size) {
// 用户端默认不使用chat内置的detailName(这个专属于服务端),所以这里加一层内置转换
if (!Chat.isBackend()) {
model2DetailNameMapping.set(
orderService.generalOrder,
orderService.generalOrderDefaultDetailName
);
}
}
}
export async function getChatModelInfo(
modelName: string,
id: string | number,
detailname?: string,
forceReload = false
) {
buildCache();
const detail = model2DetailNameMapping.get(modelName) || detailname;
const key = `${modelName}-${id}-${detail}`;
if (loadingKeys.has(key)) {
return new Promise<ChatModelInfoData>((resolve) =>
waitingAction.push({ key, resolve })
);
}
if (chatInfo[key] && !forceReload) {
const d = chatInfo[key];
if (d.row && d.row.UniplatChatId) {
return Promise.resolve({
uniplatId: d.row.UniplatChatId.value,
chat_id: +d.row.UniplatImChatId.value,
uniplat_version: 0,
};
data: d,
} as ChatModelInfoData);
}
const info = await Chat.getSdk().model(modelName).detail(id, detailname).query();
}
loadingKeys.add(key);
const info = await Chat.getSdk()
.model(modelName)
.detail(id + "", detail)
.query();
const data = info;
chatInfo[id] = data;
return {
info && info.row.UniplatChatId && (chatInfo[key] = data);
loadingKeys.delete(key);
const o = (
info.row && info.row.UniplatChatId
? {
uniplatId: info.row.UniplatChatId.value,
chat_id: Number(info.row.UniplatImChatId.value),
chat_id: +(info.row.UniplatImChatId.value as string),
uniplat_version: 0,
};
data,
}
: {
uniplatId: 0,
chat_id: 0,
uniplat_version: 0,
data,
}
) as ChatModelInfoData;
const removing = [];
for (const item of waitingAction) {
if (item.key === key) {
item.resolve(o);
removing.push(item.key);
}
}
for (const item of removing) {
waitingAction = waitingAction.filter((i) => i.key !== item);
}
return o;
}
/* eslint-disable */
export function throttle(time: number = 100) {
let pending = false
return function (target: any, name: string): any {
return function(target: any, name: string): any {
const originFunc = target[name]
const newFunc = function (this: Vue, ...params: any[]) {
const newFunc = function(this: Vue, ...params: any[]) {
if (pending) {
return
}
......@@ -22,7 +22,7 @@ export function throttle(time: number = 100) {
}
export function unique<T>(arr: T[], existed: (item: T, all: T[]) => number) {
return arr.filter(function (item, index, arr) {
return arr.filter(function(item, index, arr) {
return index === existed(item, arr)
})
}
......@@ -56,16 +56,37 @@ export function uuid() {
return s.join("")
}
export function copyTextToClipboard(text: string) {
const input = document.createElement("input");
input.setAttribute("readonly", "readonly");
input.setAttribute("value", text);
document.body.appendChild(input);
input.select();
const ret = document.execCommand("copy");
document.body.removeChild(input);
return ret;
}
const URL_REGEX =
/((?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$]))/gim
const LINE_URL_REGEX =
/((?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$]))/i
const mobile_reg = /(1[0-9_]{8,14})/ig
export function replaceText2Link(text: string) {
return text.replace(URL_REGEX, '<a href="$1" target="_blank">$1</a>')
}
export function replaceText2Dom(text: string, className: string) {
return text.replace(URL_REGEX, `<span class="${className}">$1</span>`)
}
export function replacePhone2Dom(text: string, className: string) {
return text.replace(mobile_reg, `<a href="tel:$1" class="${className}">$1</a>`)
}
export function isUrl(text: string) {
return LINE_URL_REGEX.test(text)
}
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]: {
export interface ChatUserSummaryInfo {
name: string;
phone: string;
};
};
icon: string;
alias_name: string;
}
export type UserMapping = { [eid: string]: ChatUserSummaryInfo };
const userMapping: UserMapping = {};
interface UserInfo {
avatar_url: string;
email: string;
id: string;
mobile: string;
realname: string;
uniplatId: string;
username: string;
alias_name: string;
}
export const getUserMapping = () => userMapping;
export async function getUserInfo(eid: string) {
if (userMapping[eid] != null) return userMapping[eid];
const info = await Chat.getSdk().model("user").detail(eid).query();
const loadingKeys = new Set<string>();
let waitingAction: {
key: string;
resolve: (d: ChatUserSummaryInfo) => void;
}[] = [];
export class ChatUserInfoService {
public static async getUserInfo(eid: string, sdk?: UniplatSdk) {
if (userMapping[eid]) {
return userMapping[eid];
}
if (!+eid || +eid < 0) {
return { name: "", phone: "", icon: "", alias_name: "" };
}
if (loadingKeys.has(eid)) {
return new Promise<ChatUserSummaryInfo>((resolve) =>
waitingAction.push({ key: eid, resolve })
);
}
loadingKeys.add(eid);
const info = await (sdk || Chat.getSdk())
.domainService(
"passport",
"anonymous",
`oidc.account/user_info?id=${eid}`
)
.request<any, any, UserInfo>("get");
const data = {
name: info.row.first_name.value as string,
phone: info.row.last_name.value as string,
name: info.realname || info.username || info.mobile || info.mobile,
phone: info.mobile,
icon: info.avatar_url,
alias_name: info.alias_name,
};
userMapping[eid] = data;
const removing = [];
for (const item of waitingAction) {
if (item.key === eid) {
item.resolve(data);
removing.push(item.key);
}
}
for (const item of removing) {
waitingAction = waitingAction.filter((i) => i.key !== item);
}
loadingKeys.delete(eid);
return data;
}
public static updateCache(uid: string, alias_name: string) {
const t = userMapping[uid];
t && (t.alias_name = alias_name);
}
public static setAliasName(
uid: string | number,
name: string,
sdk?: UniplatSdk
) {
return (sdk || Chat.getSdk())
.domainService("passport", "anonymous", `oidc.account/set_alias`)
.request<any, any, {}>("post", {
data: { id: uid, alias_name: name },
});
}
}
......@@ -7,6 +7,9 @@ import {
TokenStringGetter,
ServiceType,
CustomerServiceProduct,
ChatMessageController,
socketMapping,
ImEnvironment,
} from "./../model";
import { ChatLoggerService } from "./logger";
import tokenManager from "./token";
......@@ -21,34 +24,46 @@ class Chat {
private product = CustomerServiceProduct.Default;
private eventHub: Vue | null = null;
private keywords: string[] = [];
private ws = "";
private connectedActions: (() => void)[] = [];
private connected = false;
private messageController: ChatMessageController | null = null;
private defaultAvatar = "";
private userMapping: { [key: string]: { name: string; avatar: string } } =
{};
public onReady(action: () => void) {
if (this.connected) {
action();
} else {
this.connectedActions.push(action);
}
}
public async setup(option: ChatOption) {
if (!option) {
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`
);
}
this._sdk = option.sdk;
this._orgId = option.orgId;
option.serviceType !== undefined && (this.serviceType = option.serviceType);
option.serviceType !== undefined &&
(this.serviceType = option.serviceType);
option.product && (this.product = option.product);
this.eventHub = option.eventHub || null;
if (option.user) {
this.userMapping[this._sdk().global.uid] = {
name: option.user.username || "",
avatar: option.user.icon || "",
};
}
dbController.setup(this._sdk().global.uid);
option.message && (this.messageController = option.message);
option.avatar !== undefined && (this.defaultAvatar = option.avatar);
if(!option.disabledDbIndex){
await this.setupIndexDb(option.orgId()).catch(err => {
// 必须catch error不然小程序不会向后运行
console.error("setupIndexDb Error")
});
}
this.token = async () => option.sdk().global.jwtToken;
tokenManager.save(this.token);
......@@ -62,7 +77,31 @@ class Chat {
// this.keywords = ["社保"];
return this.initChatSdk(option.webSocketUri);
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();
}
});
}
private setupIndexDb(orgId: number | string) {
if (this._sdk) {
const s = this._sdk();
const key = `${s.global.uid}-${orgId || 0}`;
return dbController.setup(key);
}
return Promise.reject();
}
public resetup(org: () => string | number) {
this._orgId = org;
xim.onConnected();
return this.setupIndexDb(org());
}
public unSetup() {
......@@ -101,16 +140,27 @@ class Chat {
}
public async getToken() {
if (this.token) {
return this.trimToken(await this.token());
}
return Promise.resolve("");
}
private async initChatSdk(uri: string) {
if (xim.isConnected()) {
return uri;
return Promise.resolve(uri);
}
return new Promise<void>((resolve, reject) => {
xim.open(uri, this.token)
.catch(reject)
.finally(() => {
this.registerXimEvent();
if (xim.isConnected()) {
setTimeout(resolve, 200);
} else {
reject(new Error(`xim is not connected`));
}
return new Promise((resolve: (p?: unknown) => void) => {
xim.open(uri, this.token);
this.registerXimEvent(resolve);
});
});
}
......@@ -129,10 +179,6 @@ class Chat {
this.debug(`client status ${e}`);
}
public getUserMapping() {
return this.userMapping;
}
private debug(message: string) {
ChatLoggerService.logger?.debug(message);
}
......@@ -152,6 +198,27 @@ 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;
}
public setRead(model: string, obj: string) {
if (this.isBackend()) {
const sdk = this.getSdk();
if (sdk) {
sdk.getAxios().post(`/general/xim/model/${model}/${obj}/read`);
}
}
}
}
export default new Chat();
import { MessageHandled, MessageType } from "@/customer-service/model";
import {
MessageHandled,
MessageType,
PayMethod,
PayStatus,
} from "@/customer-service/model";
export interface Chat {
id: number;
......@@ -22,7 +27,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 +52,10 @@ export interface Chat {
chat_id: number;
catalog: string;
biz_type_id: number;
biz_type_code: string;
business_data?: string;
detail_name?: string;
keyword?: string;
}
export interface Message {
......@@ -194,6 +203,38 @@ export type CommentForwardMessageBody = {
comment_ids: number[]; // 评论id集合
};
export interface PayMessageBody {
status: PayStatus;
paymentFunction: PayMethod;
itemName: string;
amount: string;
paymentId: string;
/**
* 对应v1版本itemName
*/
totalName?: string;
/**
* 对应v1版本amount
*/
totalMoney?: string;
}
export interface PositionMessage {
title: string;
tags: { title: string; color: string }[];
salary: string;
address: string;
education_require: string;
recruit_count: string;
post_require: string;
company_name: string;
business_scope: string;
post_id: number;
max_salary: number;
min_salary: number;
}
export interface CsUser {
id: number;
oid: string;
......
......@@ -108,22 +108,22 @@ export class Xim {
}
public fetchMsgInBox(chatId: number, msgId: number) {
if (this.client == null) return;
if (this.client == null) return Promise.reject();
return this.client.fetchMsgInBox(chatType, chatId, msgId);
}
public fetchChatList() {
if (this.client == null) return;
if (this.client == null) return Promise.reject();
return this.client.fetchChatList({});
}
public fetchChatListAfter(lastMsgTs: number) {
if (this.client == null) return;
if (this.client == null) return Promise.reject();
return this.client.fetchChatList({ last_msg_ts: lastMsgTs });
}
public fetchChat(chat_id: number) {
if (this.client == null) return;
if (this.client == null) return Promise.reject();
return this.client.fetchChat(chat_id);
}
......@@ -137,13 +137,13 @@ export class Xim {
msg: string
) {
this.checkConnected();
if (this.client == null) return;
if (this.client == null) return Promise.reject();
return this.client.sendMsg(chatType, chatId, msgType, msg, "", {});
}
public inputing(chatId: number) {
this.checkConnected();
if (this.client == null) return;
if (this.client == null) return Promise.reject();
return this.client.userInput(chatType, chatId);
}
......@@ -152,7 +152,7 @@ export class Xim {
*/
public fetchChatMembers(chat_id: number) {
this.checkConnected();
if (this.client == null) return;
if (this.client == null) return Promise.reject();
return this.client.fetchChatMembers(chat_id);
}
......@@ -165,12 +165,17 @@ export class Xim {
lid = 0,
rid = 0,
limit = DefaultMsgPageSize,
desc = true
// = 0 正序(最新的消息在最下面),=1 倒序(最新的消息在最上面)
desc: boolean,
p?: { isMember: boolean; model: string; obj: string }
): Promise<Message[]> {
this.checkConnected();
if (this.client == null) {
throw new Error("client shouldn't undefined");
}
if (p && !p.isMember && p.model && p.obj) {
return this.queryMessageWhenIsNotMember(p, limit, lid, rid, desc);
}
const res = await this.client.fetchChatMsgs(chatType, chatId, {
lid,
rid,
......@@ -180,8 +185,29 @@ export class Xim {
return res.args[0];
}
private queryMessageWhenIsNotMember(
p: { isMember: boolean; model: string; obj: string },
limit: number,
lid: number,
rid: number,
desc: boolean
) {
return chat
.getSdk()
.getAxios()
.get<any, Message[]>(
`/general/xim/model/${p.model}/${
p.obj
}/msgs?lid=${lid}&rid=${rid}&limit=${limit}&desc=${
desc ? 1 : 0
}`
);
}
private setMessagesRead(chatId: number, msg: Message[]) {
if (msg.length === 0) return;
if (!msg.length) {
return this.setRead(chatId, 1, 1);
}
return this.setRead(chatId, msg[0].id, msg[msg.length - 1].id);
}
......@@ -190,10 +216,18 @@ export class Xim {
chatType: string,
chatId: number,
limit: number,
notToRead?: boolean
p: { isMember: boolean; model: string; obj: string }
) {
const data = await this.queryMsgs(chatType, chatId, 0, 0, limit, true);
if (!notToRead) {
const data = await this.queryMsgs(
chatType,
chatId,
0,
0,
limit,
true,
p
);
if (p && p.isMember) {
this.setMessagesRead(chatId, data);
}
return data;
......@@ -205,7 +239,7 @@ export class Xim {
chatId: number,
msgId: number,
limit: number,
notToRead?: boolean
p: { isMember: boolean; model: string; obj: string }
) {
const data = await this.queryMsgs(
chatType,
......@@ -213,9 +247,10 @@ export class Xim {
0,
msgId,
limit,
true
true,
p
);
if (!notToRead) {
if (p && p.isMember) {
this.setMessagesRead(chatId, data);
}
return data;
......@@ -227,7 +262,7 @@ export class Xim {
chatId: number,
msgId: number,
limit: number,
notToRead?: boolean
p?: { isMember: boolean; model: string; obj: string }
) {
const data = await this.queryMsgs(
chatType,
......@@ -235,9 +270,10 @@ export class Xim {
msgId,
0,
limit,
false
false,
p
);
if (!notToRead) {
if (p && p.isMember) {
this.setMessagesRead(chatId, data);
}
return data;
......@@ -256,7 +292,6 @@ export class Xim {
kind: string,
listener: ChatNotifyListener
): this;
public on(event: "chat_notify", listener: ChatNotifyListener): this;
public on(event: "status", listener: StatusChangeListener): this;
public on(...args: any[]): this {
......@@ -277,7 +312,6 @@ export class Xim {
kind: string,
listener: ChatNotifyListener
): this;
public off(event: "chat_notify", listener: ChatNotifyListener): this;
public off(event: "status", listener: StatusChangeListener): this;
public off(...args: any[]): this {
......@@ -322,7 +356,7 @@ export class Xim {
// xchat-client 2.2.2新增
public setUnRead(chatId: number) {
return this.client?.unreadChat(chatId);
return (this.client as any).unreadChat(chatId);
}
private parseEventListener(...args: any[]): [string, Function] {
......@@ -334,7 +368,7 @@ export class Xim {
return [args.slice(0, -1).join("."), listener];
}
private onConnected() {
public onConnected() {
if (this.client == null) return;
// 连接成功后,需要调用pubUserInfo, 否则服务端会认为此连接无效
this.client.pubUserInfo(JSON.stringify({ org_id: chat.getOrgId() }));
......@@ -401,6 +435,14 @@ export class Xim {
this.on("msg", action);
vue.$once("hook:beforeDestroy", () => this.off("msg", action));
}
public withDrawMsgHandle(e: Message): number[] {
return e.ref_id
? [e.ref_id]
: e.msg.startsWith("[")
? JSON.parse(e.msg)
: [+e.msg];
}
}
const ximInstance = new Xim();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment