Commit 05c87287 by 杨铁龙

mr master to wx

parents 5254eade 9b069739
Showing with 2508 additions and 1758 deletions
<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="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 {
......@@ -50,21 +49,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 === "") {
......
import { MessageType } from "@/customer-service/model";
import { CardMessage } from '@/customer-service/model/card';
export function parserMessage(type: string, rawMsg: string) {
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 {
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;
}
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
<template>
<el-dialog
:modal="false"
:before-close="close"
:visible="value"
custom-class="hide-header show-close padding-0 width-auto"
>
<div class="d-flex flex-column">
<div class="preview-title text-center">图片预览</div>
<el-dialog
:modal="false"
:before-close="close"
:visible="value"
custom-class="hide-header show-close padding-0 width-auto"
>
<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>
<div class="d-flex justify-content-center" style="min-width: 300px">
<img v-if="file" :src="file.url" :style="style" />
</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)"
:download="getAttachment"
>
<i class="el-icon-download"></i>
</a>
</div>
</div>
</el-dialog>
<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"
:download="getAttachment"
>
<i class="el-icon-download"></i>
</a>
</div>
</div>
</el-dialog>
</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) {
@Model("update")
private value!: boolean;
export default class ImagePreview extends Vue {
@Model("update")
private value!: boolean;
@Prop()
private file!: { name: string; url: string };
@Prop()
private file!: { name: string; url: string };
private style: {
"max-height": number | string;
"max-width": number | string;
} = {
"max-height": "300px",
"max-width": "600px",
};
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 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 set2Default() {
this.style = { "max-height": "1600px", "max-width": "1600px" };
}
private get getAttachment() {
if (this.file) {
return this.file.name;
private get getAttachment() {
if (this.file) {
return this.file.name;
}
return "文件下载";
}
return "文件下载";
}
}
</script>
<style lang="less" scoped>
.preview-title {
font-size: 18px;
color: #333;
margin-bottom: 15px;
font-size: 18px;
color: #333;
margin-bottom: 15px;
}
.actions {
margin: 15px 0;
margin: 15px 0;
> span,
a {
width: 30px;
height: 30px;
background-color: #7a7b7d;
color: #fff;
border-radius: 50%;
cursor: pointer;
> span,
a {
width: 30px;
height: 30px;
background-color: #7a7b7d;
color: #fff;
border-radius: 50%;
cursor: pointer;
i {
color: #fff;
font-size: 20px;
}
i {
color: #fff;
font-size: 20px;
}
& + span {
margin-left: 15px;
& + span {
margin-left: 15px;
}
}
}
> a {
margin-left: 15px;
}
> a {
margin-left: 15px;
}
}
</style>
<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
......@@ -2,7 +2,6 @@
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
......@@ -21,6 +20,8 @@
v-else-if="fileFailed2Load"
title="[语音加载失败]"
></i>
<text-message v-model="value" v-if="backend" />
</div>
</template>
......@@ -28,12 +29,16 @@
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,7 +91,6 @@
<style lang="less" scoped>
.voice-message {
height: 40px;
width: 200px;
&.can-play {
......@@ -97,15 +101,22 @@
font-size: 16px;
}
}
.inline-text {
position: absolute;
bottom: 0;
left: 40px;
}
.my-message {
.voice-message {
> div {
flex-flow: row-reverse;
}
svg {
transform: rotateY(180deg);
}
.voice-message {
> div {
flex-flow: row-reverse;
}
svg {
transform: rotateY(180deg);
}
}
}
</style>
import { MessageType } from "@/customer-service/model";
const SVG_AUDIO = `<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595484227561" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6614" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M132.267 0h518.4l288 288v689.067A46.933 46.933 0 0 1 891.733 1024H132.267a46.933 46.933 0 0 1-46.934-46.933V46.933A46.933 46.933 0 0 1 132.267 0z" fill="#E54C94" p-id="6615"></path><path d="M607.744 434.39c-31.488 19.86-73.472-1.43-73.472-1.43s40.47 178.795 37.483 193.003c0 0 0-1.43 0 0 0 42.56-37.483 78.037-83.968 78.037-46.464 0-82.454-35.477-82.454-78.037 0-42.582 37.483-78.059 82.454-78.059 16.49 0 34.496 5.675 43.477 15.616l-38.976-143.317s-16.49-65.28 41.984-56.768c41.984 7.082 29.995 34.048 104.96 11.349 1.493-1.408 4.48 36.907-31.488 59.605zM650.667 0v241.067A46.933 46.933 0 0 0 697.6 288h241.067l-288-288z" fill="#FFFFFF" p-id="6616"></path></svg>`;
const SVG_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="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="text-nowrap text-truncate file-message-name"
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 {
return { ...this.value, msg: JSON.parse(this.value.msg) };
if (this.value.msg.startsWith("{")) {
return { ...this.value, msg: JSON.parse(this.value.msg) };
}
return { ...this.value, msg: this.value.msg };
} catch {
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">
<span class="title">{{ 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() {
return this.positionData.salary;
}
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,
];
}
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 {
mounted() {
this.buildMessageUrl();
@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,68 +2,70 @@
<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 {
@Prop({ default: 25 })
private size!: number;
@Component({ components: {} })
export default class VoiceIcon extends Vue {
@Prop({ default: 25 })
private size!: number;
@Prop()
private loading!: boolean;
@Prop()
private loading!: boolean;
private status = 0;
private interval = 0;
@Prop()
private readonly white!: boolean;
@Watch("loading")
private onLoadingChanged() {
if (this.loading) {
this.interval = window.setInterval(() => {
const v = this.status + 1;
if (v > 3) {
this.status = 0;
} else {
this.status = v;
}
}, 500);
} else {
clearInterval(this.interval);
this.status = 0;
private status = 0;
private interval = 0;
private get fill() {
return this.white ? "#fff" : "#8D959D";
}
}
beforeDestroy() {
clearInterval(this.interval);
@Watch("loading")
private onLoadingChanged() {
if (this.loading) {
this.interval = window.setInterval(() => {
const v = this.status + 1;
if (v > 3) {
this.status = 0;
} else {
this.status = v;
}
}, 500);
} else {
clearInterval(this.interval);
this.status = 0;
}
}
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;
......
<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>
<template>
<el-dialog
:modal="false"
:before-close="close"
:visible="value"
custom-class="hide-header show-close padding-0 width-auto"
>
<div class="d-flex flex-column">
<div class="preview-title text-center">视频预览</div>
<div class="d-flex justify-content-center" style="min-width: 300px">
<video
ref="video"
v-if="file"
:src="file.url"
controls
:style="style"
></video>
</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)"
:download="getAttachment"
>
<i class="el-icon-download"></i>
</a>
</div>
</div>
</el-dialog>
<el-dialog
:modal="false"
:before-close="close"
:visible="value"
custom-class="hide-header show-close padding-0 width-auto"
>
<div class="d-flex flex-column">
<div class="preview-title text-center">视频预览</div>
<div class="d-flex justify-content-center" style="min-width: 300px">
<video
ref="video"
v-if="file"
:src="file.url"
controls
:style="style"
></video>
</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"
:download="getAttachment"
>
<i class="el-icon-download"></i>
</a>
</div>
</div>
</el-dialog>
</template>
<script lang="ts">
import {
Component,
Mixins,
Model,
Prop,
Ref,
Watch
Component,
Model,
Prop,
Ref,
Vue,
Watch,
} from "vue-property-decorator";
import { Filters } from '../mixin/filter';
@Component({ components: {} })
export default class VideoPreview extends Mixins(Filters) {
@Model("update")
private value!: boolean;
@Prop()
private file!: { name: string; url: string };
@Ref("video")
private video!: HTMLVideoElement;
private style: {
"max-height": number | string;
"max-width": number | string;
} = {
"max-height": "800px",
"max-width": "800px",
};
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;
export default class VideoPreview extends Vue {
@Model("update")
private value!: boolean;
@Prop()
private file!: { name: string; url: string };
@Ref("video")
private video!: HTMLVideoElement;
private style: {
"max-height": number | string;
"max-width": number | string;
} = {
"max-height": "800px",
"max-width": "800px",
};
private close() {
setTimeout(
() =>
(this.style = { "max-height": "300px", "max-width": "600px" }),
300
);
this.$emit("update", false);
}
return "视频下载";
}
@Watch("value")
private onOpen() {
if (this.value) {
this.video?.load();
setTimeout(() => this.video?.play(), 100);
} else {
this.video?.pause();
private set2Default() {
this.style = { "max-height": "1600px", "max-width": "1600px" };
}
private get getAttachment() {
if (this.file) {
return this.file.name;
}
return "视频下载";
}
@Watch("value")
private onOpen() {
if (this.value) {
this.video?.load();
setTimeout(() => this.video?.play(), 100);
} else {
this.video?.pause();
}
}
}
}
</script>
<style lang="less" scoped>
.preview-title {
font-size: 18px;
color: #333;
margin-bottom: 15px;
font-size: 18px;
color: #333;
margin-bottom: 15px;
}
.actions {
margin: 15px 0;
> span,
a {
width: 30px;
height: 30px;
background-color: #7a7b7d;
color: #fff;
border-radius: 50%;
cursor: pointer;
i {
color: #fff;
font-size: 20px;
margin: 15px 0;
> span,
a {
width: 30px;
height: 30px;
background-color: #7a7b7d;
color: #fff;
border-radius: 50%;
cursor: pointer;
i {
color: #fff;
font-size: 20px;
}
& + span {
margin-left: 15px;
}
}
& + span {
margin-left: 15px;
> a {
margin-left: 15px;
}
}
> a {
margin-left: 15px;
}
}
</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);
});
});
}
}
export const devAppTools = new DevAppTools();
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);
}
}
<?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;
}
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,14 @@ export interface ChatOption {
eventHub?: Vue;
/**
* 用户信息(头像,别名)可选
*/
user?: { icon?: string; username?: string };
message?: ChatMessageController;
avatar?: string;
}
export interface ChatMessageController {
error: (msg: string) => void;
info: (msg: string) => void;
}
export interface ChatServiceLogger {
......@@ -91,8 +109,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 +159,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 +196,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 +216,7 @@ export interface TransferedChatItem extends BaseChatItem {
export type ChatMemberExtraInfo = {
name?: string;
phone?: string;
alias_name?: string;
};
export interface ChatMember {
......@@ -195,7 +226,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 +243,7 @@ export interface ChatMember {
update_time: number;
name: string;
phone: string;
alias_name?: string;
}
export type ChatMembers = readonly ChatMember[];
......@@ -265,3 +297,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;
}
}
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 } 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;
}
class WebMonitor {
private key = "";
private envir = ImEnvironment.Dev;
private product = Product.Default;
private readonly url = "https://pre-hrs-monitor.hrs100.com";
public updateKey(key: string) {
this.key = key;
return this;
}
private buildHeaders() {
return {
headers: { authorization: "cdd0a34e-f537-4e5b-808e-2ba06af21845" },
};
}
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) {
this.envir = options.envir;
this.product = options.product;
sdk.events.addUniversalErrorResponseCallback((r: AxiosResponse<any>) => {
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.data && 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
);
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].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
}
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;
/**
* 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)
})
}
/**
*
* @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,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;
......@@ -168,7 +169,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";
......@@ -246,6 +247,12 @@ export namespace ChatStore {
keyword?: string
) => Promise<ChatType[]>;
export const ACTION_SYNC_MY_CHAT_LIST = "同步我的会话列表";
export type ACTION_SYNC_MY_CHAT_LIST = () => void;
export const ACTION_FORCE_RELOAD_CHAT_LIST = "重新获取我的会话列表";
export type ACTION_FORCE_RELOAD_CHAT_LIST = () => Promise<ChatType[]>;
export const ACTION_REBUILD_UNREAD_MESSAGE_COUNT = "重新计算未读消息数";
export type ACTION_REBUILD_UNREAD_MESSAGE_COUNT = () => void;
......@@ -278,14 +285,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 +325,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 +356,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 +417,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);
}
return Promise.resolve({
uniplatId: 0,
chat_id: 0,
uniplat_version: 0,
data: d,
});
}
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 {
uniplatId: info.row.UniplatChatId.value,
chat_id: Number(info.row.UniplatImChatId.value),
uniplat_version: 0,
};
chatInfo[key] = data;
loadingKeys.delete(key);
const o = (
info.row && info.row.UniplatChatId
? {
uniplatId: info.row.UniplatChatId.value,
chat_id: +(info.row.UniplatImChatId.value as string),
uniplat_version: 0,
data,
}
: {
uniplatId: 0,
chat_id: 0,
uniplat_version: 0,
data,
}
) as ChatModelInfoData;
let 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)
})
}
......@@ -62,10 +62,20 @@ const URL_REGEX =
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]: {
name: string;
phone: string;
icon: string;
alias_name: string;
};
};
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 data = {
name: info.row.first_name.value as string,
phone: info.row.last_name.value as string,
};
userMapping[eid] = data;
return data;
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: "" };
}
const info = await (sdk || Chat.getSdk())
.domainService(
"passport",
"anonymous",
`oidc.account/user_info?id=${eid}`
)
.request<any, any, UserInfo>("get");
const data = {
name: info.realname || info.username || info.mobile || info.mobile,
phone: info.mobile,
icon: info.avatar_url,
alias_name: info.alias_name,
};
userMapping[eid] = data;
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,41 @@ class Chat {
private product = CustomerServiceProduct.Default;
private eventHub: Vue | null = null;
private keywords: string[] = [];
private userMapping: { [key: string]: { name: string; avatar: string } } =
{};
private ws = "";
private connectedActions: (() => void)[] = [];
private connected = false;
private messageController: ChatMessageController | null = null;
private defaultAvatar = "";
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);
await this.setupIndexDb(option.orgId());
this.token = async () => option.sdk().global.jwtToken;
tokenManager.save(this.token);
......@@ -62,7 +72,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 +135,27 @@ class Chat {
}
public async getToken() {
return this.trimToken(await this.token());
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((resolve: (p?: unknown) => void) => {
xim.open(uri, this.token);
this.registerXimEvent(resolve);
return new Promise<void>((resolve, reject) => {
xim.open(uri, this.token)
.catch(reject)
.finally(() => {
this.registerXimEvent();
if (xim.isConnected()) {
resolve();
} else {
reject(new Error(`xim is not connected`));
}
});
});
}
......@@ -129,10 +174,6 @@ class Chat {
this.debug(`client status ${e}`);
}
public getUserMapping() {
return this.userMapping;
}
private debug(message: string) {
ChatLoggerService.logger?.debug(message);
}
......@@ -152,6 +193,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,9 @@ export interface Chat {
chat_id: number;
catalog: string;
biz_type_id: number;
biz_type_code: string;
business_data?: string;
detail_name?: string;
}
export interface Message {
......@@ -194,6 +202,36 @@ 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;
}
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,16 @@ export class Xim {
lid = 0,
rid = 0,
limit = DefaultMsgPageSize,
desc = true
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 +184,25 @@ 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 ? 0 : 1}`
)
}
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 +211,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 +234,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 +242,10 @@ export class Xim {
0,
msgId,
limit,
true
true,
p
);
if (!notToRead) {
if (p && p.isMember) {
this.setMessagesRead(chatId, data);
}
return data;
......@@ -227,7 +257,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 +265,10 @@ export class Xim {
msgId,
0,
limit,
false
false,
p
);
if (!notToRead) {
if (p && p.isMember) {
this.setMessagesRead(chatId, data);
}
return data;
......@@ -256,7 +287,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 +307,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 +351,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 +363,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 +430,11 @@ export class Xim {
this.on("msg", action);
vue.$once("hook:beforeDestroy", () => this.off("msg", action));
}
public withDrawMsgHandle(e: Message) {
const ids = e.ref_id ? [e.ref_id] : e.msg.startsWith("[") ? JSON.parse(e.msg) : [+e.msg];
return ids;
}
}
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