Commit 86932012 by panjiangyi

格式化代码

parent 03c9c8d6
<template> <template>
<div class="chat-list-con h-100"> <div class="chat-list-con h-100">
<div class="chat-list h-100"> <div class="chat-list h-100">
<el-input <el-input
class="keyword-input" class="keyword-input"
placeholder="昵称、手机、Email、备注" placeholder="昵称、手机、Email、备注"
prefix-icon="el-icon-search" prefix-icon="el-icon-search"
v-model="searchKeyword" v-model="searchKeyword"
v-on:keyup.enter.native="search" v-on:keyup.enter.native="search"
clearable clearable
@clear="search" @clear="search"
></el-input> ></el-input>
<div class="chat-list-scroll"> <div class="chat-list-scroll">
<el-scrollbar class="h-100 no-bottom-scrollbar"> <el-scrollbar class="h-100 no-bottom-scrollbar">
<div <div
v-for="item in chatRooms" v-for="item in chatRooms"
:key="item.chat_id" :key="item.chat_id"
class="chat-item" class="chat-item"
:class="{ selected: isSelected(item) }" :class="{ selected: isSelected(item) }"
@click="goToChatRoom(item)" @click="goToChatRoom(item)"
> >
<div <div
class="chat-avatar pos-rel" class="chat-avatar pos-rel"
:class="{ 'red-dot': item.unread_msg_count > 0 }" :class="{ 'red-dot': item.unread_msg_count > 0 }"
> >
<fs-avatar <fs-avatar
shape="circle" shape="circle"
:size="36" :size="36"
:src="item.customer_avatar_url" :src="item.customer_avatar_url"
/> />
</div> </div>
<div class="chat-info"> <div class="chat-info">
<div <div
class=" class="
chat-info-left chat-info-left
d-flex d-flex
justify-content-between justify-content-between
align-items-center align-items-center
" "
> >
<div <div
:title="item.customer_name" :title="item.customer_name"
class="chat-name flex-fill text-dot-dot-dot" class="chat-name flex-fill text-dot-dot-dot"
> >
<!-- <span>{{ <!-- <span>{{
item.customer_name || item.customer_name ||
item.customer_mobile || item.customer_mobile ||
item.customer_eid item.customer_eid
}}</span> --> }}</span> -->
<span>{{ item.chat_id }}</span> <span>{{ item.chat_id }}</span>
</div> </div>
<div v-if="item.last_msg_ts" class="chat-time"> <div v-if="item.last_msg_ts" class="chat-time">
{{ formatTimestamp(item.last_msg_ts) }} {{ formatTimestamp(item.last_msg_ts) }}
</div> </div>
</div> </div>
<div class="chat-msg text-dot-dot-dot"> <div class="chat-msg text-dot-dot-dot">
{{ parseMesage(item) }} {{ parseMesage(item) }}
</div> </div>
</div>
</div>
<div
class="empty"
v-if="chatRooms && chatRooms.length <= 0"
>
{{ searchKeyword ? "无相关接待" : "无接待" }}
</div>
</el-scrollbar>
</div> </div>
</div> </div>
<div class="empty" v-if="chatRooms && chatRooms.length <= 0">
{{ searchKeyword ? "无相关接待" : "无接待" }}
</div>
</el-scrollbar>
</div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import buttonThrottle from "@/utils/button-throttle"; // import buttonThrottle from "@/utils/button-throttle";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { chatStore, ChatStore } from "@/customer-service/store/model"; import { chatStore, ChatStore } from "@/customer-service/store/model";
...@@ -80,225 +77,227 @@ import { formatTime, TimeFormatRule } from "@/customer-service/utils/time"; ...@@ -80,225 +77,227 @@ import { formatTime, TimeFormatRule } from "@/customer-service/utils/time";
import Chat from "@/customer-service/xim"; import Chat from "@/customer-service/xim";
export function parserMessage(type: string, rawMsg: string) { export function parserMessage(type: string, rawMsg: string) {
if (!type) return ""; if (!type) return "";
if (!rawMsg) return ""; if (!rawMsg) return "";
const msg = JSON.parse(rawMsg); const msg = JSON.parse(rawMsg);
if (type === "text") { if (type === "text") {
return msg.text; return msg.text;
} else if (type === "image") { } else if (type === "image") {
return `[图片]`; return `[图片]`;
} else if (type === "file") { } else if (type === "file") {
return `[文件]`; return `[文件]`;
} else { } else {
return `[不支持的消息格式]`; return `[不支持的消息格式]`;
} }
} }
type Chat = ChatStore.STATE_MY_CHAT_ROOM_LIST["list"][number] type Chat = NonNullable<ChatStore.STATE_MY_CHAT_ROOM_LIST>["list"][number];
@Component({ components: {} }) @Component({ components: {} })
export default class ChatList extends Vue { export default class ChatList extends Vue {
@chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST) @chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
private readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST private readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID)
private readonly currentChatUniplatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly uniplatVersion!: ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_MY_CHAT_ROOM_LIST) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID)
private readonly chatList!: ChatStore.STATE_MY_CHAT_ROOM_LIST private readonly currentChatUniplatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID;
@chatStore.Action(ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION)
private readonly saveChatId!: ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION private readonly uniplatVersion!: ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION;
@chatStore.Mutation(ChatStore.MUTATION_SAVE_MYSELF_ID) @chatStore.State(ChatStore.STATE_MY_CHAT_ROOM_LIST)
private readonly saveMyId!: ChatStore.MUTATION_SAVE_MYSELF_ID private readonly chatList!: ChatStore.STATE_MY_CHAT_ROOM_LIST;
@chatStore.Mutation(ChatStore.MUTATION_SET_CHAT_SOURCE) @chatStore.Action(ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION)
private readonly setSource!: ChatStore.MUTATION_SET_CHAT_SOURCE private readonly saveChatId!: ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION;
@chatStore.Mutation(ChatStore.MUTATION_SAVE_CHAT_TITLE) @chatStore.Mutation(ChatStore.MUTATION_SAVE_MYSELF_ID)
private readonly saveChatTitle!: ChatStore.MUTATION_SAVE_CHAT_TITLE private readonly saveMyId!: ChatStore.MUTATION_SAVE_MYSELF_ID;
@Prop({ type: String, default: "-1" })
private selected!: string
private searchKeyword = ""
private get chatRooms() {
return this.chatList?.list || [];
}
private isSelected(item: Chat) { @chatStore.Mutation(ChatStore.MUTATION_SET_CHAT_SOURCE)
if (this.currentChatUniplatId) { private readonly setSource!: ChatStore.MUTATION_SET_CHAT_SOURCE;
return item.uniplatId === this.currentChatUniplatId;
}
return this.selected === item.uniplatId;
}
async created() { @chatStore.Mutation(ChatStore.MUTATION_SAVE_CHAT_TITLE)
await this.getMyChatList(); private readonly saveChatTitle!: ChatStore.MUTATION_SAVE_CHAT_TITLE;
this.setSource(ChatStore.StateChatSourceDirection.Server);
this.selectFirstChat();
}
mounted() { @Prop({ type: String, default: "-1" })
this.saveMyId(); private selected!: string;
this.goToOnlyRoom();
}
private goToOnlyRoom() { private searchKeyword = "";
if (this.chatRooms.length === 1) {
const wantedChat = this.chatRooms[0];
this.goToChatRoom(wantedChat);
}
}
private selectFirstChat() { private get chatRooms() {
if (this.chatId != null) return; return this.chatList?.list || [];
if (!this.chatRooms.length) return; }
const { chat_id, uniplat_version, uniplatId } = this.chatRooms[0];
this.saveChatId({
chatId: chat_id,
v: uniplat_version,
uniplatId,
});
}
@buttonThrottle() private isSelected(item: Chat) {
private async search() { if (this.currentChatUniplatId) {
this.searchKeyword = this.searchKeyword.trim(); return item.uniplatId === this.currentChatUniplatId;
if (!this.searchKeyword) {
await this.getMyChatList();
} else {
await this.getMyChatList(this.searchKeyword);
}
} }
return this.selected === item.uniplatId;
private goToChatRoom(data: Chat) { }
if (this.currentChatUniplatId === data.uniplatId) {
return; async created() {
} await this.getMyChatList();
this.setSource(ChatStore.StateChatSourceDirection.Server);
this.saveChatId({ this.selectFirstChat();
chatId: this.chatRooms.find((k) => k.uniplatId === data.uniplatId) }
.chat_id,
v: data.uniplat_version, mounted() {
uniplatId: data.uniplatId, this.saveMyId();
}).finally(this.raiseChatIdChanged); this.goToOnlyRoom();
}
this.saveChatTitle(data.uniplatId);
private goToOnlyRoom() {
if (this.chatRooms.length === 1) {
const wantedChat = this.chatRooms[0];
this.goToChatRoom(wantedChat);
} }
}
private raiseChatIdChanged() {
this.$emit("change"); private selectFirstChat() {
} if (this.chatId != null) return;
if (!this.chatRooms.length) return;
private parseMesage(data: Chat) { const { chat_id, uniplat_version, uniplatId } = this.chatRooms[0];
return parserMessage(data.msg_type, data.msg); this.saveChatId({
chatId: chat_id,
v: uniplat_version,
uniplatId,
});
}
// @buttonThrottle()
private async search() {
this.searchKeyword = this.searchKeyword.trim();
if (!this.searchKeyword) {
await this.getMyChatList();
} else {
await this.getMyChatList(this.searchKeyword);
} }
}
private formatTimestamp(v: number) { private goToChatRoom(data: Chat) {
return formatTime(v, { short: true, rule: TimeFormatRule.Hour12 }); if (this.currentChatUniplatId === data.uniplatId) {
return;
} }
const wantedChatRoom = this.chatRooms.find(
(k) => k.uniplatId === data.uniplatId
);
if (wantedChatRoom == null) return;
this.saveChatId({
chatId: wantedChatRoom.chat_id,
v: data.uniplat_version,
uniplatId: data.uniplatId,
}).finally(this.raiseChatIdChanged);
this.saveChatTitle(data.uniplatId);
}
private raiseChatIdChanged() {
this.$emit("change");
}
private parseMesage(data: Chat) {
return parserMessage(data.msg_type, data.msg);
}
private formatTimestamp(v: number) {
return formatTime(v, { short: true, rule: TimeFormatRule.Hour12 });
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-list-con { .chat-list-con {
.title { .title {
padding-left: 20px; padding-left: 20px;
line-height: 59px; line-height: 59px;
font-size: 18px; font-size: 18px;
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
} }
} }
.chat-list { .chat-list {
text-align: center; text-align: center;
} }
.chat-list-scroll { .chat-list-scroll {
height: calc(100% - 50px); height: calc(100% - 50px);
.empty { .empty {
margin-top: 100%; margin-top: 100%;
} }
} }
.keyword-input { .keyword-input {
width: 200px; width: 200px;
margin: 15px 0; margin: 15px 0;
/deep/ .el-input__inner { /deep/ .el-input__inner {
font-size: 13px; font-size: 13px;
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
border-radius: 15px; border-radius: 15px;
padding-right: 15px; padding-right: 15px;
} }
/deep/ .el-icon-time { /deep/ .el-icon-time {
background: transparent; background: transparent;
} }
} }
.chat-list { .chat-list {
.chat-item { .chat-item {
cursor: pointer; cursor: pointer;
padding: 10px 15px; padding: 10px 15px;
&:hover { &:hover {
background: #e4f0ff; background: #e4f0ff;
} }
&.selected { &.selected {
background: #f0f0f0; background: #f0f0f0;
} }
.chat-avatar { .chat-avatar {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
width: 36px; width: 36px;
height: 36px; height: 36px;
margin-right: 15px; margin-right: 15px;
&.red-dot::before { &.red-dot::before {
content: ""; content: "";
position: absolute; position: absolute;
width: 8px; width: 8px;
height: 8px; height: 8px;
background: #e87005; background: #e87005;
border-radius: 50%; border-radius: 50%;
z-index: 1; z-index: 1;
right: -4px; right: -4px;
top: -4px; top: -4px;
} }
} }
.chat-info { .chat-info {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
width: calc(100% - 51px); width: calc(100% - 51px);
} }
.chat-info-left { .chat-info-left {
text-align: start; text-align: start;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
color: #333333; color: #333333;
margin-bottom: 10px; margin-bottom: 10px;
.chat-time { .chat-time {
text-align: end; text-align: end;
flex: none; flex: none;
color: #999999; color: #999999;
margin-left: 10px; margin-left: 10px;
font-size: 13px; font-size: 13px;
line-height: 1; line-height: 1;
} }
} }
.chat-msg { .chat-msg {
color: #888; color: #888;
text-align: start; text-align: start;
height: 16px; height: 16px;
margin-top: -3px; margin-top: -3px;
}
} }
}
} }
</style> </style>
<template> <template>
<div class="chat-room-con h-100"> <div class="chat-room-con h-100">
<div <div class="room-title d-flex justify-content-between align-items-center">
class="room-title d-flex justify-content-between align-items-center" <div class="title" @click="showMembers">
> {{ chatTitle }}
<div class="title" @click="showMembers"> <template v-if="chatMembers.length">
{{ chatTitle }} <span class="members-count">(成员{{ chatMembers.length }}人)</span>
<template v-if="chatMembers.length"> <i
<span class="members-count" v-if="membersPanelVisibility"
>(成员{{ chatMembers.length }}人)</span class="title-right-arrow fast-service-icon-arrow-up"
> />
<i <i v-else class="title-right-arrow fast-service-icon-arrow-down" />
v-if="membersPanelVisibility" </template>
class="title-right-arrow fast-service-icon-arrow-up" <template v-if="!notOnlyCheck">
/> <div v-if="currentChat.is_finish" class="chat-status chat-done">
<i 已完成
v-else </div>
class="title-right-arrow fast-service-icon-arrow-down" <div v-else class="chat-status">接待中</div>
/> </template>
</template> </div>
<template v-if="!notOnlyCheck"> <i v-if="close" @click="close" class="title-close el-icon-close" />
<div </div>
v-if="currentChat.is_finish" <div v-if="membersPanelVisibility" class="chat-members">
class="chat-status chat-done" <div class="chat-member" v-for="item in chatMembers" :key="item.id">
> <fs-avatar shape="circle" :src="item.avatar" :size="40" />
已完成 <div class="member-name">
</div> {{ item.name || item.eid }}
<div v-else class="chat-status">接待中</div>
</template>
</div>
<i v-if="close" @click="close" class="title-close el-icon-close" />
</div>
<div v-if="membersPanelVisibility" class="chat-members">
<div class="chat-member" v-for="item in chatMembers" :key="item.id">
<fs-avatar shape="circle" :src="item.avatar" :size="40" />
<div class="member-name">
{{ item.name || item.eid }}
</div>
</div>
</div> </div>
<div class="chat-panel"> </div>
<div class="chat-area h-100"> </div>
<template v-if="notOnlyCheck"> <div class="chat-panel">
<div class="chat-messages pos-rel"> <div class="chat-area h-100">
<div <template v-if="notOnlyCheck">
v-if="getCurrentInputingPeople.length" <div class="chat-messages pos-rel">
class="someone-inputing" <div
> v-if="getCurrentInputingPeople.length"
{{ getCurrentInputingPeople }}正在输入 class="someone-inputing"
</div> >
<messages /> {{ getCurrentInputingPeople }}正在输入
</div>
<div class="chat-input">
<message-input @error="onError" />
</div>
</template>
<template v-else>
<messages />
</template>
</div> </div>
<!-- <div class="chat-info h-100 pos-rel"> <messages />
</div>
<div class="chat-input">
<message-input @error="onError" />
</div>
</template>
<template v-else>
<messages />
</template>
</div>
<!-- <div class="chat-info h-100 pos-rel">
<div class="info-tabs"> <div class="info-tabs">
<div <div
@click="activeTab = 'customer'" @click="activeTab = 'customer'"
...@@ -75,22 +65,22 @@ ...@@ -75,22 +65,22 @@
订单 订单
</div> </div>
</div> --> </div> -->
<!-- <cusomter-info v-if="customerInfoTabShow" /> --> <!-- <cusomter-info v-if="customerInfoTabShow" /> -->
<!-- <div v-else class="order-info-con"> <!-- <div v-else class="order-info-con">
<order-info /> <order-info />
</div> --> </div> -->
<!-- </div> --> <!-- </div> -->
</div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { import {
Component, Component,
Prop, Prop,
Provide, Provide,
Ref, Ref,
Vue, Vue,
Watch, Watch,
} from "vue-property-decorator"; } from "vue-property-decorator";
import MessageInput from "@/customer-service/message-input.vue"; import MessageInput from "@/customer-service/message-input.vue";
...@@ -99,205 +89,206 @@ import messages from "@/customer-service/message-list.vue"; ...@@ -99,205 +89,206 @@ import messages from "@/customer-service/message-list.vue";
// import OrderInfo from "./order-info.vue" // import OrderInfo from "./order-info.vue"
import { ChatStore, chatStore } from "@/customer-service/store/model"; import { ChatStore, chatStore } from "@/customer-service/store/model";
type RoomInfoTab = "customer" | "order" type RoomInfoTab = "customer" | "order";
@Component({ @Component({
components: { components: {
MessageInput, MessageInput,
messages, messages,
// CusomterInfo, // CusomterInfo,
// OrderInfo, // OrderInfo,
}, },
}) })
export default class ChatRoom extends Vue { export default class ChatRoom extends Vue {
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS) @chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
@chatStore.Mutation(ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS) @chatStore.Mutation(ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS)
private readonly clearChatMembers!: ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS private readonly clearChatMembers!: ChatStore.MUTATION_CLEAR_CURRENT_CHAT_MEMBERS;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_TITLE) @chatStore.State(ChatStore.STATE_CURRENT_CHAT_TITLE)
private readonly chatTitle!: ChatStore.STATE_CURRENT_CHAT_TITLE private readonly chatTitle!: ChatStore.STATE_CURRENT_CHAT_TITLE;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_INPUTING) @chatStore.State(ChatStore.STATE_CURRENT_CHAT_INPUTING)
private readonly currentInputPeople!: ChatStore.STATE_CURRENT_CHAT_INPUTING private readonly currentInputPeople!: ChatStore.STATE_CURRENT_CHAT_INPUTING;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID)
private readonly currentChatUniplatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID private readonly currentChatUniplatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID;
@chatStore.State(ChatStore.STATE_MY_CHAT_ROOM_LIST) @chatStore.State(ChatStore.STATE_MY_CHAT_ROOM_LIST)
private readonly myChatList!: ChatStore.STATE_MY_CHAT_ROOM_LIST private readonly myChatList!: ChatStore.STATE_MY_CHAT_ROOM_LIST;
private allChatList = { list: [] } private allChatList = { list: [] };
@Prop({ type: Function }) @Prop({ type: Function })
private close?: () => void private close?: () => void;
@Provide() showReadSummary = true @Provide() showReadSummary = true;
@Watch("currentChatUniplatId") @Watch("currentChatUniplatId")
private whenCurrentChatIdChanged(newValue: string, oldValue: string) { private whenCurrentChatIdChanged(newValue: string, oldValue: string) {
if (Number(oldValue) === Number(newValue)) return; if (Number(oldValue) === Number(newValue)) return;
this.hideMembers(); this.hideMembers();
this.clearChatMembers(); this.clearChatMembers();
} }
private activeTab: RoomInfoTab = "customer" private activeTab: RoomInfoTab = "customer";
private membersPanelVisibility = false private membersPanelVisibility = false;
private get getCurrentInputingPeople() { private get getCurrentInputingPeople() {
return this.currentInputPeople return this.currentInputPeople
.map((k) => "" /* this.userInfo[k].name */) .map((k) => "" /* this.userInfo[k].name */)
.join("、"); .join("、");
} }
private get currentChat() { private get currentChat() {
const chatId = this.currentChatUniplatId; const chatId = this.currentChatUniplatId;
let result = this.myChatList.list.find((k) => k.uniplatId === chatId); if (this.myChatList == null) return;
if (result) return result; let result = this.myChatList.list.find((k) => k.uniplatId === chatId);
result = this.allChatList.list.find((k) => k.uniplatId === chatId); // if (result) return result;
return result ?? {}; // result = this.allChatList.list.find((k) => k.uniplatId === chatId);
} return result ?? {};
}
private get notOnlyCheck(): boolean { private get notOnlyCheck(): boolean {
return true; return true;
} }
private get customerInfoTabShow() { private get customerInfoTabShow() {
return this.activeTab === "customer"; return this.activeTab === "customer";
} }
private get orderInfoTabShow() { private get orderInfoTabShow() {
return this.activeTab === "order"; return this.activeTab === "order";
} }
private showMembers() { private showMembers() {
this.membersPanelVisibility = !this.membersPanelVisibility; this.membersPanelVisibility = !this.membersPanelVisibility;
} }
private hideMembers() { private hideMembers() {
this.membersPanelVisibility = false; this.membersPanelVisibility = false;
} }
private onError(msg: string) { private onError(msg: string) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(msg); console.error(msg);
this.$message.error(msg); this.$message.error(msg);
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.room-title { .room-title {
font-size: 16px; font-size: 16px;
padding: 0 20px; padding: 0 20px;
height: 60px; height: 60px;
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
.title { .title {
cursor: pointer; cursor: pointer;
} }
.members-count { .members-count {
color: #666666; color: #666666;
} }
.title-right-arrow { .title-right-arrow {
font-size: 10px; font-size: 10px;
margin-left: 10px; margin-left: 10px;
vertical-align: middle; vertical-align: middle;
color: #666; color: #666;
} }
.title-close { .title-close {
color: #8d959d; color: #8d959d;
cursor: pointer; cursor: pointer;
} }
} }
.chat-status { .chat-status {
display: inline-block; display: inline-block;
width: 46px; width: 46px;
height: 20px; height: 20px;
line-height: 20px; line-height: 20px;
background: #22bd7a; background: #22bd7a;
font-size: 13px; font-size: 13px;
border-radius: 2px; border-radius: 2px;
color: #ffffff; color: #ffffff;
text-align: center; text-align: center;
margin-left: 10px; margin-left: 10px;
&.chat-done { &.chat-done {
background: #c5d4e5; background: #c5d4e5;
} }
} }
.chat-members { .chat-members {
position: absolute; position: absolute;
width: calc(100% - 350px); width: calc(100% - 350px);
padding: 30px; padding: 30px;
padding-bottom: 0; padding-bottom: 0;
background: #fff; background: #fff;
z-index: 1; z-index: 1;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
.chat-member { .chat-member {
text-align: center; text-align: center;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-bottom: 30px; margin-bottom: 30px;
margin-right: 30px; margin-right: 30px;
} }
.member-name { .member-name {
margin-top: 10px; margin-top: 10px;
} }
} }
.chat-panel { .chat-panel {
height: calc(100% - 60px); height: calc(100% - 60px);
.chat-area, .chat-area,
.chat-info { .chat-info {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
} }
.chat-area { .chat-area {
width: 100%; width: 100%;
} }
.chat-info { .chat-info {
width: 349px; width: 349px;
border-left: 1px solid #e1e1e1; border-left: 1px solid #e1e1e1;
} }
} }
.info-tabs { .info-tabs {
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
.info-tab { .info-tab {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: 50%; width: 50%;
text-align: center; text-align: center;
font-size: 15px; font-size: 15px;
color: #333; color: #333;
cursor: pointer; cursor: pointer;
&.active { &.active {
font-weight: 600; font-weight: 600;
}
} }
}
} }
.chat-area { .chat-area {
$input-height: 130px; $input-height: 130px;
.chat-messages { .chat-messages {
height: calc(100% - 130px + 1px); height: calc(100% - 130px + 1px);
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
} }
.chat-input { .chat-input {
height: $input-height; height: $input-height;
} }
} }
.order-info-con { .order-info-con {
height: calc(100% - 40px); height: calc(100% - 40px);
} }
.someone-inputing { .someone-inputing {
position: absolute; position: absolute;
left: 20px; left: 20px;
bottom: 20px; bottom: 20px;
z-index: 1; z-index: 1;
color: #c2c2c2; color: #c2c2c2;
} }
</style> </style>
<template> <template>
<el-dialog <el-dialog
class="chat-dialog-con" class="chat-dialog-con"
:close-on-click-modal="false" :close-on-click-modal="false"
:visible="visible" :visible="visible"
@close="hide" @close="hide"
> >
<div class="chat-con h-100"> <div class="chat-con h-100">
<div class="h-100 chat-list"> <div class="h-100 chat-list">
<chat-list /> <chat-list />
</div> </div>
<div v-if="chatId != null" class="h-100 chat-area"> <div v-if="chatId != null" class="h-100 chat-area">
<chat-room :close="hide" /> <chat-room :close="hide" />
</div> </div>
<div class="chat-panel h-100"> <div class="chat-panel h-100">
<el-button @click="terminate">结束</el-button> <el-button @click="terminate">结束</el-button>
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
...@@ -29,52 +29,52 @@ import { ChatStore, chatStore } from "@/customer-service/store/model"; ...@@ -29,52 +29,52 @@ import { ChatStore, chatStore } from "@/customer-service/store/model";
@Component({ components: { MessageList, ChatRoom, ChatList } }) @Component({ components: { MessageList, ChatRoom, ChatList } })
export default class Chat extends Vue { export default class Chat extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE) @chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE)
private readonly visible: ChatStore.STATE_CHAT_DIALOG_VISIBLE private readonly visible!: ChatStore.STATE_CHAT_DIALOG_VISIBLE;
@chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT) @chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT)
private readonly hide: ChatStore.MUTATION_HIDE_CHAT private readonly hide!: ChatStore.MUTATION_HIDE_CHAT;
@chatStore.Action(ChatStore.ACTION_TERINATE_CHAT) @chatStore.Action(ChatStore.ACTION_TERINATE_CHAT)
private readonly _terminate: ChatStore.ACTION_TERINATE_CHAT private readonly _terminate!: ChatStore.ACTION_TERINATE_CHAT;
private terminate() { private terminate() {
this._terminate(); this._terminate();
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-dialog-con { .chat-dialog-con {
/deep/ .el-dialog__header { /deep/ .el-dialog__header {
display: none; display: none;
} }
/deep/ .el-dialog { /deep/ .el-dialog {
width: 80%; width: 80%;
max-width: 1200px; max-width: 1200px;
} }
--chat-side-width: 200px; --chat-side-width: 200px;
} }
.chat-con { .chat-con {
height: 500px; height: 500px;
} }
.chat-list, .chat-list,
.chat-area, .chat-area,
.chat-panel { .chat-panel {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
} }
.chat-list { .chat-list {
width: var(--chat-side-width); width: var(--chat-side-width);
border-right: 1px solid #e1e1e1; border-right: 1px solid #e1e1e1;
} }
.chat-area { .chat-area {
width: calc(100% - 2 * var(--chat-side-width) - 2px); width: calc(100% - 2 * var(--chat-side-width) - 2px);
} }
.chat-panel { .chat-panel {
width: var(--chat-side-width); width: var(--chat-side-width);
border-left: 1px solid #e1e1e1; border-left: 1px solid #e1e1e1;
} }
</style> </style>
<template> <template>
<div <div
class="message-con d-flex" class="message-con d-flex"
:class=" :class="
isMyMessage isMyMessage
? 'my-message align-items-start flex-row-reverse' ? 'my-message align-items-start flex-row-reverse'
: 'align-items-start' : 'align-items-start'
" "
> >
<span class="no-selection"> <span class="no-selection">
<fs-avatar <fs-avatar :size="40" class="msg-avatar" :shape="shape" :src="avatar" />
:size="40" </span>
class="msg-avatar"
:shape="shape" <div class="msg-content">
:src="avatar" <div class="msg-name no-selection">{{ username }}</div>
/> <!-- Image -->
</span> <div
class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }"
v-if="messageType === 'image'"
@dblclick="openFile"
>
<img
v-if="messageRealUrl"
:src="messageRealUrl"
:title="messageBody.msg.name"
:alt="messageBody.msg.name"
/>
<file-icon v-else-if="fileFailed2Load" :value="image404"></file-icon>
</div>
<!-- File -->
<div
class="msg-detail file-message d-flex"
v-else-if="messageType === 'file'"
@dblclick="openFile"
>
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
:title="messageBody.msg.name"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
{{ messageBody.msg.size | formatSize }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
</div>
<!-- Audio -->
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-else-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
</div>
<div class="msg-content"> <i
<div class="msg-name no-selection">{{ username }}</div> class="el-icon-warning-outline"
<!-- Image --> v-else-if="fileFailed2Load"
<div title="[语音加载失败]"
class="msg-detail image-message" ></i>
:class="{ 'image-404': fileFailed2Load }" </div>
v-if="messageType === 'image'" <!-- Video -->
@dblclick="openFile" <div
> class="
<img
v-if="messageRealUrl"
:src="messageRealUrl"
:title="messageBody.msg.name"
:alt="messageBody.msg.name"
/>
<file-icon
v-else-if="fileFailed2Load"
:value="image404"
></file-icon>
</div>
<!-- File -->
<div
class="msg-detail file-message d-flex"
v-else-if="messageType === 'file'"
@dblclick="openFile"
>
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
:title="messageBody.msg.name"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
{{ messageBody.msg.size | formatSize }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
</div>
<!-- Audio -->
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-else-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
</div>
<i
class="el-icon-warning-outline"
v-else-if="fileFailed2Load"
title="[语音加载失败]"
></i>
</div>
<!-- Video -->
<div
class="
msg-detail msg-detail
video-message video-message
d-flex d-flex
align-items-center align-items-center
justify-content-center justify-content-center
" "
v-else-if="messageType === 'video'" v-else-if="messageType === 'video'"
> >
<video-player-icon @click.native="openFile"></video-player-icon> <video-player-icon @click.native="openFile"></video-player-icon>
</div> </div>
<!-- Text --> <!-- Text -->
<div <div
class="msg-detail inline-text" class="msg-detail inline-text"
v-else v-else
v-html="format2Link(messageBody.msg.text || emptyText)" v-html="format2Link(messageBody.msg.text || emptyText)"
></div> ></div>
</div> </div>
<a <a
v-if="!isSendingMessage && messageType === 'file'" v-if="!isSendingMessage && messageType === 'file'"
class=" class="
d-flex d-flex
align-items-center align-items-center
justify-content-center justify-content-center
download-icon download-icon
" "
:href="messageRealUrl | downloadUrl(getAttachment)" :href="messageRealUrl | downloadUrl(getAttachment)"
:download="getAttachment" :download="getAttachment"
title="下载文件" title="下载文件"
> >
<img src="~@/customer-service/imgs/download.png" alt="Download" /> <img src="~@/customer-service/imgs/download.png" alt="Download" />
</a> </a>
<i <i class="el-icon-warning text-danger" v-if="failed" title="发送失败"></i>
class="el-icon-warning text-danger" <i class="el-icon-loading" v-else-if="isSendingMessage"></i>
v-if="failed"
title="发送失败" <template v-if="showReadSummary">
></i> <div v-if="isMyMessage" class="msg-read pos-rel">
<i class="el-icon-loading" v-else-if="isSendingMessage"></i> <span @click="readListVisibility = true" class="pointer">
<template v-if="isAllRead">全部已读</template>
<template v-if="showReadSummary"> <template v-else-if="data.read_count > 0"
<div v-if="isMyMessage" class="msg-read pos-rel"> >{{ data.read_count }}人已读</template
<span @click="readListVisibility = true" class="pointer"> >
<template v-if="isAllRead">全部已读</template> <template v-else>未读</template>
<template v-else-if="data.read_count > 0" </span>
>{{ data.read_count }}人已读</template <who-read-list
> v-if="readListVisibility"
<template v-else>未读</template> @blur="readListVisibility = false"
</span> :msgId="data.id"
<who-read-list />
v-if="readListVisibility" </div>
@blur="readListVisibility = false" </template>
:msgId="data.id" </div>
/>
</div>
</template>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
...@@ -150,13 +138,13 @@ import { replaceText2Link } from "../utils"; ...@@ -150,13 +138,13 @@ import { replaceText2Link } from "../utils";
import chat from "./../xim"; import chat from "./../xim";
import { import {
FileType, FileType,
getFileType, getFileType,
isAudio, isAudio,
isImage, isImage,
isVideo, isVideo,
MAX_FILE_SIZE, MAX_FILE_SIZE,
MAX_IMAGE_SIZE, MAX_IMAGE_SIZE,
} from "./file-controller"; } from "./file-controller";
import FileIcon from "./file-icon.vue"; import FileIcon from "./file-icon.vue";
import VideoPlayerIcon from "./video-player-icon.vue"; import VideoPlayerIcon from "./video-player-icon.vue";
...@@ -166,459 +154,458 @@ import WhoReadList from "./who-read-list.vue"; ...@@ -166,459 +154,458 @@ import WhoReadList from "./who-read-list.vue";
import { chatStore, ChatStore } from "@/customer-service/store/model"; import { chatStore, ChatStore } from "@/customer-service/store/model";
@Component({ @Component({
components: { FileIcon, VoiceIcon, WhoReadList, VideoPlayerIcon }, components: { FileIcon, VoiceIcon, WhoReadList, VideoPlayerIcon },
}) })
export default class Message extends Vue { export default class Message extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_MY_ID) @chatStore.State(ChatStore.STATE_CHAT_MY_ID)
private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS;
@chatStore.Getter(ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS) @chatStore.State(ChatStore.STATE_CHAT_SOURCE)
private readonly chatMembers!: ChatStore.GETTER_CURRENT_CHAT_PRESENT_MEMBERS private readonly chatSource!: ChatStore.STATE_CHAT_SOURCE;
@chatStore.State(ChatStore.STATE_CHAT_SOURCE) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatSource!: ChatStore.STATE_CHAT_SOURCE private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) /**
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID * tbd: 文件消息所在的域名的url,逻辑需要补充
*/
private messageRealUrl = "";
private fileFailed2Load = false;
private image404 = FileType.Image_404;
private loadingRealUrl = false;
/** @Prop({ type: Object, default: () => Object.create(null) })
* tbd: 文件消息所在的域名的url,逻辑需要补充 private data!: dto.Message;
*/
private messageRealUrl = ""
private fileFailed2Load = false
private image404 = FileType.Image_404
private loadingRealUrl = false
@Prop({ type: Object, default: () => Object.create(null) }) @Prop()
private data!: dto.Message private isSendingMessage!: boolean;
@Prop() @Prop()
private isSendingMessage!: boolean private failed!: boolean;
@Prop() @Prop({ default: "circle" })
private failed!: boolean private shape!: string;
@Prop({ default: "circle" }) @Ref("audio")
private shape!: string private readonly audioRef!: HTMLAudioElement;
@Ref("audio") private playing = false;
private readonly audioRef!: HTMLAudioElement
private playing = false @Inject({ default: false }) readonly showReadSummary!: boolean;
@Inject({ default: false }) readonly showReadSummary!: boolean private emptyText = " ";
private emptyText = " " private readListVisibility = false;
private readListVisibility = false private org = "";
private org = "" private get isAllRead() {
return this.data.read_count >= this.data.total_read_count;
}
private get isAllRead() { private get messageBody(): { eid?: string; oid?: string; msg: any } {
return this.data.read_count >= this.data.total_read_count; if (this.data) {
try {
return { ...this.data, msg: JSON.parse(this.data.msg) };
} catch {
return {
...this.data,
msg: JSON.parse(this.data.msg.replace(/\n/g, "\\n")),
};
}
} }
private get messageBody(): { eid?: string; oid?: string; msg: any } { return { msg: { text: "" } };
if (this.data) { }
try {
return { ...this.data, msg: JSON.parse(this.data.msg) };
} catch {
return {
...this.data,
msg: JSON.parse(this.data.msg.replace(/\n/g, "\\n")),
};
}
}
return { msg: { text: "" } }; private get getAttachment() {
if (this.messageBody) {
return this.messageBody.msg.name;
} }
return "文件下载";
}
private get getAttachment() { private get isMyMessage() {
if (this.messageBody) { if (this.isSendingMessage) {
return this.messageBody.msg.name; return true;
}
return "文件下载";
} }
private get isMyMessage() { if (this.messageBody) {
if (this.isSendingMessage) { const msg = this.messageBody;
return true; if (this.chatSource) {
const source = msg.msg.source;
if (source) {
return (
source === this.chatSource && this.messageBody.eid === this.chatMyId
);
} }
if (this.messageBody) { if (this.org && this.messageBody.oid) {
const msg = this.messageBody; return (
if (this.chatSource) { this.messageBody.oid === this.org &&
const source = msg.msg.source; this.messageBody.eid === this.chatMyId
if (source) { );
return (
source === this.chatSource &&
this.messageBody.eid === this.chatMyId
);
}
if (this.org && this.messageBody.oid) {
return (
this.messageBody.oid === this.org &&
this.messageBody.eid === this.chatMyId
);
}
return false;
}
return this.messageBody.eid === this.chatMyId;
} }
return false; return false;
} }
private get username() { return this.messageBody.eid === this.chatMyId;
const avatar = chat.getUserMapping();
if (this.data == null) return "";
if (avatar == null) return this.data.id;
const value = avatar[this.data.eid];
if (value == null) return "";
if (value.name == null) return "";
return value.name;
} }
private get avatar() { return false;
const avatar = chat.getUserMapping(); }
if (this.isSendingMessage) { private get username() {
if (avatar && this.chatMyId) { const avatar: any = chat.getUserMapping();
const user = avatar[this.chatMyId]; if (this.data == null) return "";
if (user && user.avatar) { if (avatar == null) return this.data.id;
return user.avatar; const value = avatar[this.data.eid];
} if (value == null) return "";
} if (value.name == null) return "";
} return value.name;
}
if (avatar && this.data) {
const value = avatar[this.data.eid]; private get avatar() {
if (value && value.avatar) { const avatar = chat.getUserMapping();
return value.avatar;
} if (this.isSendingMessage) {
if (avatar && this.chatMyId) {
const user = avatar[this.chatMyId];
if (user && user.avatar) {
return user.avatar;
} }
}
}
return ""; if (avatar && this.data) {
const value = avatar[this.data.eid];
if (value && value.avatar) {
return value.avatar;
}
} }
private get fileIcon() { return "";
if (this.data) { }
return getFileType(this.messageBody.msg.name);
}
return FileType.Others; private get fileIcon() {
if (this.data) {
return getFileType(this.messageBody.msg.name);
} }
private get messageType() { return FileType.Others;
const type = this.data?.type; }
if (type === "file") {
const name = this.messageBody?.msg.name; private get messageType() {
if (name) { const type = this.data?.type;
const size = this.messageBody?.msg.size; if (type === "file") {
if (size) { const name = this.messageBody?.msg.name;
const outImageSize = size > MAX_IMAGE_SIZE; if (name) {
if (!outImageSize && isImage(name)) { const size = this.messageBody?.msg.size;
return "image"; if (size) {
} const outImageSize = size > MAX_IMAGE_SIZE;
const outSize = size > MAX_FILE_SIZE; if (!outImageSize && isImage(name)) {
if (!outSize) { return "image";
if (isAudio(name)) { }
return "voice"; const outSize = size > MAX_FILE_SIZE;
} if (!outSize) {
if (isVideo(name)) { if (isAudio(name)) {
return "video"; return "voice";
} }
} if (isVideo(name)) {
} return "video";
} }
}
} }
return this.data?.type; }
} }
return this.data?.type;
// 图片的样式设置,通过js偶尔会有高度计算不准确, 直接通过css的处理目前看可以达到对应效果 }
// private get imageStyle() {
// if (this.messageBody == null) return {}; // 图片的样式设置,通过js偶尔会有高度计算不准确, 直接通过css的处理目前看可以达到对应效果
// if (this.messageBody.msg.w == null) return {}; // private get imageStyle() {
// if (this.messageBody.msg.h == null) return {}; // if (this.messageBody == null) return {};
// const w = this.messageBody.msg.w; // if (this.messageBody.msg.w == null) return {};
// const h = this.messageBody.msg.h; // if (this.messageBody.msg.h == null) return {};
// const maxWidth = 300; // const w = this.messageBody.msg.w;
// const maxHeight = maxWidth * (h / w); // const h = this.messageBody.msg.h;
// return { width: `${w}px`, height: `${h}px`, maxHeight: `${maxHeight}px` }; // const maxWidth = 300;
// } // const maxHeight = maxWidth * (h / w);
// return { width: `${w}px`, height: `${h}px`, maxHeight: `${maxHeight}px` };
private get duration() { // }
const v = this.messageBody.msg.duration as number;
return v || 0; private get duration() {
const v = this.messageBody.msg.duration as number;
return v || 0;
}
private get durationInSecond() {
return Math.round(this.duration / 1000);
}
private get getVoiceMessageWidth() {
if (this.fileFailed2Load) {
return 35;
}
const d = this.duration / 1000;
if (d <= 3) {
return 60;
} }
private get durationInSecond() { if (d >= 60) {
return Math.round(this.duration / 1000); return 200;
} }
private get getVoiceMessageWidth() { return 60 + d;
if (this.fileFailed2Load) { }
return 35;
}
const d = this.duration / 1000;
if (d <= 3) {
return 60;
}
if (d >= 60) { private isCustomer() {
return 200; return !this.showReadSummary;
} }
return 60 + d; private buildMessageUrl() {
if (this.messageRealUrl || this.loadingRealUrl) {
return;
} }
const url = this.messageBody.msg.url as string;
private isCustomer() { if (url) {
return !this.showReadSummary; // const service = XimService.getInstance()
if (isAccessibleUrl(url)) {
return (this.messageRealUrl = url);
}
this.loadingRealUrl = true;
// service
// .getFileUrlById(url, this.chatId || 0, this.isCustomer())
// .then((data) => {
// if (data && data.itemList) {
// this.messageRealUrl = data.itemList[0].url
// } else {
// this.fileFailed2Load = true
// }
// })
// .catch(() => (this.fileFailed2Load = true))
// .finally(() => (this.loadingRealUrl = false))
} else {
this.fileFailed2Load = true;
} }
}
private buildMessageUrl() { private openFile() {
if (this.messageRealUrl || this.loadingRealUrl) { if (this.isSendingMessage) {
return; return;
}
const url = this.messageBody.msg.url as string;
if (url) {
// const service = XimService.getInstance()
if (isAccessibleUrl(url)) {
return (this.messageRealUrl = url);
}
this.loadingRealUrl = true;
// service
// .getFileUrlById(url, this.chatId || 0, this.isCustomer())
// .then((data) => {
// if (data && data.itemList) {
// this.messageRealUrl = data.itemList[0].url
// } else {
// this.fileFailed2Load = true
// }
// })
// .catch(() => (this.fileFailed2Load = true))
// .finally(() => (this.loadingRealUrl = false))
} else {
this.fileFailed2Load = true;
}
} }
if (this.failed || this.fileFailed2Load) {
private openFile() { return;
if (this.isSendingMessage) {
return;
}
if (this.failed || this.fileFailed2Load) {
return;
}
const copy = { ...this.messageBody.msg };
if (this.messageRealUrl) {
copy.url = this.messageRealUrl;
}
this.$emit("open", { type: this.messageType, msg: copy });
} }
const copy = { ...this.messageBody.msg };
private play() { if (this.messageRealUrl) {
if (this.audioRef?.paused) { copy.url = this.messageRealUrl;
this.audioRef?.load();
this.audioRef?.play();
} else {
this.audioRef?.pause();
}
} }
this.$emit("open", { type: this.messageType, msg: copy });
private onPlay() { }
this.playing = true;
private play() {
if (this.audioRef?.paused) {
this.audioRef?.load();
this.audioRef?.play();
} else {
this.audioRef?.pause();
} }
}
private onPause() { private onPlay() {
this.playing = false; this.playing = true;
} }
private format2Link(text: string) { private onPause() {
return replaceText2Link(text); this.playing = false;
} }
private format2Link(text: string) {
return replaceText2Link(text);
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.message-con { .message-con {
margin: 20px 0; margin: 20px 0;
&.my-message {
.msg-avatar {
margin-right: 0;
margin-left: 10px;
}
.msg-name { &.my-message {
display: none; .msg-avatar {
} margin-right: 0;
margin-left: 10px;
.msg-detail { }
margin-top: 0;
background-color: #dbf2ff;
border-radius: 8px 0 8px 8px;
&.image-message:not(.image-404), .msg-name {
&.file-message { display: none;
background-color: transparent; }
border-radius: 4px;
border: 1px solid #c5d4e5;
}
&.voice-message { .msg-detail {
> div { margin-top: 0;
flex-flow: row-reverse; background-color: #dbf2ff;
} border-radius: 8px 0 8px 8px;
svg { &.image-message:not(.image-404),
transform: rotateY(180deg); &.file-message {
} background-color: transparent;
} border-radius: 4px;
border: 1px solid #c5d4e5;
}
&.video-message { &.voice-message {
background-color: #000; > div {
border-radius: 0; flex-flow: row-reverse;
}
} }
.msg-read { svg {
display: inline-block; transform: rotateY(180deg);
color: #bfe1ff;
margin-right: 15px;
user-select: none;
flex: none;
} }
}
.download-icon { &.video-message {
margin-right: 15px; background-color: #000;
margin-left: 0; border-radius: 0;
margin-top: 0; }
} }
.msg-read {
display: inline-block;
color: #bfe1ff;
margin-right: 15px;
user-select: none;
flex: none;
} }
> i { .download-icon {
height: 16px; margin-right: 15px;
font-size: 16px; margin-left: 0;
margin-right: 10px; margin-top: 0;
} }
}
> i {
height: 16px;
font-size: 16px;
margin-right: 10px;
}
} }
.msg-avatar { .msg-avatar {
margin-right: 10px; margin-right: 10px;
flex: 40px 0 0; flex: 40px 0 0;
} }
i.msg-avatar { i.msg-avatar {
font-size: 30px; font-size: 30px;
background-color: #c0c4cc; background-color: #c0c4cc;
border-radius: 4px; border-radius: 4px;
width: 40px; width: 40px;
height: 40px; height: 40px;
&:before { &:before {
position: relative; position: relative;
left: 5px; left: 5px;
top: 5px; top: 5px;
color: #fff; color: #fff;
} }
} }
.msg-name { .msg-name {
font-size: 12px; font-size: 12px;
color: #888; color: #888;
} }
.msg-detail { .msg-detail {
margin-top: 10px; margin-top: 10px;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
background: #f5f6fa; background: #f5f6fa;
border-radius: 0px 8px 8px; border-radius: 0px 8px 8px;
padding: 10px; padding: 10px;
word-break: break-word; word-break: break-word;
&.image-message, &.image-message,
&.file-message { &.file-message {
background-color: transparent; background-color: transparent;
border-radius: 4px; border-radius: 4px;
border: 1px solid #c5d4e5; border: 1px solid #c5d4e5;
} }
&.image-message { &.image-message {
line-height: 1; line-height: 1;
&.image-404 { &.image-404 {
background: #f7f8fa; background: #f7f8fa;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.file-icon { .file-icon {
margin: 0; margin: 0;
} }
}
} }
}
&.voice-message { &.voice-message {
height: 40px; height: 40px;
width: 200px; width: 200px;
&.can-play { &.can-play {
cursor: pointer; cursor: pointer;
} }
i { i {
font-size: 16px; font-size: 16px;
}
} }
}
&.video-message { &.video-message {
height: 160px; height: 160px;
width: 200px; width: 200px;
background-color: #000; background-color: #000;
border-radius: 0; border-radius: 0;
svg { svg {
cursor: pointer; cursor: pointer;
}
} }
}
&.inline-text { &.inline-text {
display: inline-block; display: inline-block;
} }
.file-message-name { .file-message-name {
max-width: 130px; max-width: 130px;
} }
img { img {
max-width: 300px; max-width: 300px;
} }
} }
.download-icon { .download-icon {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
margin-left: 15px; margin-left: 15px;
margin-top: 42px; margin-top: 42px;
i { i {
color: #fff; color: #fff;
font-size: 14px; font-size: 14px;
} }
} }
.no-selection { .no-selection {
user-select: none; user-select: none;
} }
.image-message { .image-message {
max-width: 300px; max-width: 300px;
box-sizing: content-box; box-sizing: content-box;
img { img {
width: 100%; width: 100%;
} }
} }
</style> </style>
<template> <template>
<div <div
v-loading="loading" v-loading="loading"
ref="list-con" ref="list-con"
@blur="$emit('blur')" @blur="$emit('blur')"
class="who-read-list pos-rel" class="who-read-list pos-rel"
:style="`left:${left}px;top:${top}px`" :style="`left:${left}px;top:${top}px`"
> >
<template v-if="!loading"> <template v-if="!loading">
<div class="list-left"> <div class="list-left">
<div class="number-count">已读 {{ readlist.length }}</div> <div class="number-count">已读 {{ readlist.length }}</div>
<div <div class="member-item" v-for="item in readlist" :key="item.eid">
class="member-item" <fs-avatar class="member-avatar" :src="item.avatar" :size="30" />
v-for="item in readlist" <span class="member-name">{{ item.name }}</span>
:key="item.eid" </div>
> </div>
<fs-avatar <div class="list-right">
class="member-avatar" <div class="number-count">未读 {{ unreadlist.length }}</div>
:src="item.avatar" <div class="member-item" v-for="item in unreadlist" :key="item.eid">
:size="30" <fs-avatar class="member-avatar" :src="item.avatar" :size="30" />
/> <span class="member-name">{{ item.name }}</span>
<span class="member-name">{{ item.name }}</span> </div>
</div> </div>
</div> </template>
<div class="list-right"> </div>
<div class="number-count">未读 {{ unreadlist.length }}</div>
<div
class="member-item"
v-for="item in unreadlist"
:key="item.eid"
>
<fs-avatar
class="member-avatar"
:src="item.avatar"
:size="30"
/>
<span class="member-name">{{ item.name }}</span>
</div>
</div>
</template>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Ref, Vue } from "vue-property-decorator"; import { Component, Prop, Ref, Vue } from "vue-property-decorator";
...@@ -49,161 +33,166 @@ import { unique } from "../utils"; ...@@ -49,161 +33,166 @@ import { unique } from "../utils";
import { ChatStore } from "@/customer-service/store/model"; import { ChatStore } from "@/customer-service/store/model";
import xim from "@/customer-service/xim/xim"; import xim from "@/customer-service/xim/xim";
import chat from "@/customer-service/xim/index";
const chatStoreNamespace = namespace("chatStore"); const chatStoreNamespace = namespace("chatStore");
@Component({ components: {} }) @Component({ components: {} })
export default class WhoReadList extends Vue { export default class WhoReadList extends Vue {
@chatStoreNamespace.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) @chatStoreNamespace.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStoreNamespace.State(ChatStore.STATE_CHAT_MY_ID) @chatStoreNamespace.State(ChatStore.STATE_CHAT_MY_ID)
private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID;
@Prop({ @Prop({
type: Number, type: Number,
}) })
private msgId!: number private msgId!: number;
@Ref("list-con") @Ref("list-con")
private listCon!: HTMLElement private listCon!: HTMLElement;
private top = 0 private top = 0;
private left = 0 private left = 0;
private readlist: { name: string; avatar: string }[] = [] private readlist: { name: string; avatar: string }[] = [];
private unreadlist: { name: string; avatar: string }[] = [] private unreadlist: { name: string; avatar: string }[] = [];
private loading = false private loading = false;
private startLoading() { private startLoading() {
this.loading = true; this.loading = true;
} }
private endLoading() { private endLoading() {
this.loading = false; this.loading = false;
} }
public async created() { public async created() {
this.startLoading(); this.startLoading();
await this.getReader(); await this.getReader();
this.endLoading(); this.endLoading();
} }
public mounted() { public mounted() {
this.enableBlur(); this.enableBlur();
const { top, left } = ( const { top, left } = (this.listCon
this.listCon.parentNode as HTMLElement .parentNode as HTMLElement).getBoundingClientRect();
).getBoundingClientRect(); this.top = top;
this.top = top; this.left = left;
this.left = left; }
}
private enableBlur() { private enableBlur() {
this.listCon.setAttribute("tabindex", "-1"); this.listCon.setAttribute("tabindex", "-1");
this.listCon.focus(); this.listCon.focus();
} }
private async getUserNameByid(eid: string) { private async getUserNameByid(eid: string) {
const data = await this.sdk.model("user").detail(eid).query(); const data = await chat
return data.row.first_name.value as string; .getSdk()
} .model("user")
.detail(eid)
.query();
return data.row.first_name.value as string;
}
private async getReader() { private async getReader() {
if (this.chatId == null) return; if (this.chatId == null) return;
if (this.msgId == null) return; if (this.msgId == null) return;
const data = await xim.fetchMsgInBox(this.chatId, this.msgId); const data = await xim.fetchMsgInBox(this.chatId, this.msgId);
const readerlist = this.uniqueReaderList( if (data == null) return;
data.args[0] as dto.OneWhoReadMessage[] const readerlist = this.uniqueReaderList(
); data.args[0] as dto.OneWhoReadMessage[]
this.readlist = await Promise.all( );
readerlist this.readlist = await Promise.all(
.filter((k) => k.is_read) readerlist
.filter((k) => k.eid !== this.chatMyId) .filter((k) => k.is_read)
.map(async (k) => { .filter((k) => k.eid !== this.chatMyId)
const eid = k.eid; .map(async (k) => {
const name = await this.getUserNameByid(eid); const eid = k.eid;
return { const name = await this.getUserNameByid(eid);
eid, return {
name, eid,
avatar: "", name,
}; avatar: "",
}) };
); })
this.unreadlist = await Promise.all( );
readerlist this.unreadlist = await Promise.all(
.filter((k) => !k.is_read) readerlist
.filter((k) => k.eid !== this.chatMyId) .filter((k) => !k.is_read)
.map(async (k) => { .filter((k) => k.eid !== this.chatMyId)
const eid = k.eid; .map(async (k) => {
const name = await this.getUserNameByid(eid); const eid = k.eid;
return { const name = await this.getUserNameByid(eid);
eid, return {
name, eid,
avatar: "", name,
}; avatar: "",
}) };
); })
} );
}
private uniqueReaderList(data: dto.OneWhoReadMessage[]) { private uniqueReaderList(data: dto.OneWhoReadMessage[]) {
return unique(data, function (item, all) { return unique(data, function(item, all) {
return all.findIndex((k) => k.eid === item.eid); return all.findIndex((k) => k.eid === item.eid);
}); });
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.who-read-list { .who-read-list {
::before { ::before {
content: ""; content: "";
position: absolute; position: absolute;
width: 1px; width: 1px;
top: 15px; top: 15px;
bottom: 15px; bottom: 15px;
left: 0; left: 0;
right: 0; right: 0;
margin: auto; margin: auto;
background: #e1e2e2; background: #e1e2e2;
} }
&:focus { &:focus {
outline: unset; outline: unset;
} }
padding: 15px 30px; padding: 15px 30px;
background: rgba(249, 249, 249, 1); background: rgba(249, 249, 249, 1);
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.2); box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.2);
border-radius: 4px; border-radius: 4px;
border: 1px solid rgba(183, 191, 199, 1); border: 1px solid rgba(183, 191, 199, 1);
width: 455px; width: 455px;
min-height: 100px; min-height: 100px;
position: fixed; position: fixed;
margin-left: -200px; margin-left: -200px;
margin-top: 20px; margin-top: 20px;
color: #000; color: #000;
z-index: 2; z-index: 2;
.list-left, .list-left,
.list-right { .list-right {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: calc(50% - 30px); width: calc(50% - 30px);
} }
.list-left { .list-left {
padding-right: 30px; padding-right: 30px;
} }
.list-right { .list-right {
padding-left: 30px; padding-left: 30px;
} }
.number-count { .number-count {
font-size: 14px; font-size: 14px;
color: #333333; color: #333333;
margin-bottom: 15px; margin-bottom: 15px;
} }
} }
.member-item { .member-item {
margin-top: 10px; margin-top: 10px;
.member-avatar, .member-avatar,
.member-name { .member-name {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
.member-avatar { .member-avatar {
margin-right: 10px; margin-right: 10px;
} }
} }
</style> </style>
<template> <template>
<el-dialog title="创建会话" :visible="visible" @close="hide"> <el-dialog title="创建会话" :visible="visible" @close="hide">
选择聊天对象 选择聊天对象
{{ userList.length }} {{ userList.length }}
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="userList" :data="userList"
style="width: 100%" style="width: 100%"
height="250" height="250"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
<el-table-column type="selection" width="55"> </el-table-column> <el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="id" label="id" width="150"> <el-table-column prop="id" label="id" width="150"> </el-table-column>
</el-table-column> <el-table-column prop="name" label="name" width="150"> </el-table-column>
<el-table-column prop="name" label="name" width="150"> </el-table>
</el-table-column> <el-button @click="nextPage">加载下一页</el-button>
</el-table> <span slot="footer" class="dialog-footer">
<el-button @click="nextPage">加载下一页</el-button> <el-button @click="hide">取 消</el-button>
<span slot="footer" class="dialog-footer"> <el-button type="primary" @click="createChat">确 定</el-button>
<el-button @click="hide">取 消</el-button> </span>
<el-button type="primary" @click="createChat">确 定</el-button> </el-dialog>
</span>
</el-dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator" import { Component, Vue } from "vue-property-decorator";
import { ChatStore, chatStore } from "@/customer-service/store/model" import { ChatStore, chatStore } from "@/customer-service/store/model";
import { List, ListEasy, ListTypes } from "uniplat-sdk" import type { List, ListEasy, ListTypes } from "uniplat-sdk";
import chat from "@/customer-service/xim/index"
type User = { type User = {
id: string id: string;
name: string name: string;
} };
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
@Component({ components: {} }) @Component({ components: {} })
export default class ChatCreator extends Vue { export default class ChatCreator extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CREATOR_VISIBLE) @chatStore.State(ChatStore.STATE_CHAT_CREATOR_VISIBLE)
private readonly visible: ChatStore.STATE_CHAT_CREATOR_VISIBLE private readonly visible!: ChatStore.STATE_CHAT_CREATOR_VISIBLE;
@chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT_CREATOR) @chatStore.Mutation(ChatStore.MUTATION_HIDE_CHAT_CREATOR)
private readonly hide: ChatStore.MUTATION_HIDE_CHAT_CREATOR private readonly hide!: ChatStore.MUTATION_HIDE_CHAT_CREATOR;
@chatStore.Action(ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN) @chatStore.Action(ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN)
private readonly _createChat: ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN private readonly _createChat!: ChatStore.ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN;
private currentPage = 1 private currentPage = 1;
private userList = [] private userList: {
private getList: ThenArg<ReturnType<ListEasy["query"]>>["getList"] id: any;
name: any;
}[] = [];
private getList!: ThenArg<ReturnType<ListEasy["query"]>>["getList"];
public async created() { public async created() {
const list = this.sdk.model("user").list() const list = chat.getSdk().model("user").list();
const { pageData, getList } = await list.query({ const { pageData, getList } = await list.query({
pageIndex: this.currentPage, pageIndex: this.currentPage,
item_size: 50, item_size: 50,
}) });
this.getList = getList this.getList = getList;
this.userList = this.exactUserList(pageData.rows) this.userList = this.exactUserList(pageData.rows);
} }
private exactUserList(rows: any[]) { private exactUserList(rows: any[]) {
return rows.map((k) => { return rows.map((k) => {
return { return {
id: k.id.value, id: k.id.value,
name: k.first_name.value, name: k.first_name.value,
} };
}) });
} }
private loading = false private loading = false;
private async nextPage() { private async nextPage() {
this.loading = true this.loading = true;
this.currentPage++ this.currentPage++;
const data = await this.getList(this.currentPage) const data = await this.getList(this.currentPage);
this.loading = false this.loading = false;
this.userList = [...this.userList, ...this.exactUserList(data.rows)] this.userList = [...this.userList, ...this.exactUserList(data.rows)];
} }
private selectedRows: string[] = [] private selectedRows: string[] = [];
private handleSelectionChange(selectedRows: User[]) { private handleSelectionChange(selectedRows: User[]) {
this.selectedRows = selectedRows.map((k) => String(k.id)) this.selectedRows = selectedRows.map((k) => String(k.id));
} }
private createChat() { private createChat() {
const { keyvalue, model_name } = this.$route.params const { keyvalue, model_name } = this.$route.params;
this._createChat({ this._createChat({
modelName: model_name, modelName: model_name,
selectedListId: keyvalue, selectedListId: keyvalue,
uids: this.selectedRows, uids: this.selectedRows,
}) });
} }
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
<template> <template>
<div class="input-wrap h-100"> <div class="input-wrap h-100">
<div class="tool-bar"> <div class="tool-bar">
<img <img
class="tool-bar-icon" class="tool-bar-icon"
title="发送表情" title="发送表情"
@click.stop="toggleEmoji" @click.stop="toggleEmoji"
src="@/customer-service/imgs/emoji.png" src="@/customer-service/imgs/emoji.png"
/> />
<label <label
for="chat-upload-file" for="chat-upload-file"
:title="tip4Image" :title="tip4Image"
@click="allowLoadImg" @click="allowLoadImg"
class="offset" class="offset"
> >
<img <img class="tool-bar-icon" src="@/customer-service/imgs/pic.png" />
class="tool-bar-icon" </label>
src="@/customer-service/imgs/pic.png" <label for="chat-upload-file" :title="tip4File" @click="allowLoadFile">
/> <img class="tool-bar-icon" src="@/customer-service/imgs/file.png" />
</label> </label>
<label
for="chat-upload-file" <input
:title="tip4File" @change="onChange"
@click="allowLoadFile" :value="file"
> id="chat-upload-file"
<img type="file"
class="tool-bar-icon" :accept="acceptType"
src="@/customer-service/imgs/file.png" multiple
/> />
</label>
<input
@change="onChange"
:value="file"
id="chat-upload-file"
type="file"
:accept="acceptType"
multiple
/>
</div>
<el-scrollbar class="input-el-scrollbar">
<!-- contenteditable 只能设置为true,需要考虑浏览器兼容性问题,不能只考虑chrome -->
<div
class="input-container"
ref="input"
id="chat-input-box"
contenteditable="true"
@input="$emit('input')"
@keypress.enter="handleSendMsg"
></div>
</el-scrollbar>
<div class="emoji-picker" v-show="emojiPanelVisibility">
<el-scrollbar class="overflow-x-hidden">
<span
class="emoji-item"
v-for="item in emoji"
:key="item.code"
:title="item.name"
@click.stop="selectEmoji(item)"
>{{ item.emoji_chars }}</span
>
</el-scrollbar>
</div>
</div> </div>
<el-scrollbar class="input-el-scrollbar">
<!-- contenteditable 只能设置为true,需要考虑浏览器兼容性问题,不能只考虑chrome -->
<div
class="input-container"
ref="input"
id="chat-input-box"
contenteditable="true"
@input="$emit('input')"
@keypress.enter="handleSendMsg"
></div>
</el-scrollbar>
<div class="emoji-picker" v-show="emojiPanelVisibility">
<el-scrollbar class="overflow-x-hidden">
<span
class="emoji-item"
v-for="item in emoji"
:key="item.code"
:title="item.name"
@click.stop="selectEmoji(item)"
>{{ item.emoji_chars }}</span
>
</el-scrollbar>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
...@@ -71,37 +61,37 @@ import { Component, Ref, Vue, Watch } from "vue-property-decorator"; ...@@ -71,37 +61,37 @@ import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import { namespace } from "vuex-class"; import { namespace } from "vuex-class";
import { import {
getFileType, getFileType,
getSvg, getSvg,
MAX_FILE_SIZE, MAX_FILE_SIZE,
MAX_FILE_SIZE_STRING, MAX_FILE_SIZE_STRING,
MAX_IMAGE_SIZE, MAX_IMAGE_SIZE,
MAX_IMAGE_SIZE_STRING, MAX_IMAGE_SIZE_STRING,
MESSAGE_FILE_EMPTY, MESSAGE_FILE_EMPTY,
MESSAGE_FILE_TOO_LARGE, MESSAGE_FILE_TOO_LARGE,
MESSAGE_IMAGE_TOO_LARGE, MESSAGE_IMAGE_TOO_LARGE,
} from "../components/file-controller"; } from "../components/file-controller";
import { EmojiService } from "../service/emoji"; import { EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model"; import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils"; import { formatFileSize } from "../utils";
export const enum InputMessageType { export const enum InputMessageType {
Text = "text", Text = "text",
Image = "image", Image = "image",
File = "file", File = "file",
} }
export interface InputMessageBody { export interface InputMessageBody {
text?: string; text?: string;
url?: string; url?: string;
name?: string; name?: string;
size?: number; size?: number;
} }
export interface InputMessage { export interface InputMessage {
type: InputMessageType; type: InputMessageType;
body: InputMessageBody; body: InputMessageBody;
file?: File | null; file?: File | null;
} }
const chatStoreNamespace = namespace("chatStore"); const chatStoreNamespace = namespace("chatStore");
...@@ -112,544 +102,539 @@ export const IMAGE_INFO_CLASS = "img-info"; ...@@ -112,544 +102,539 @@ export const IMAGE_INFO_CLASS = "img-info";
export const FILE_INFO_CLASS = "file-info"; export const FILE_INFO_CLASS = "file-info";
export function isImageOrFile(node: ChildNode) { export function isImageOrFile(node: ChildNode) {
const e = node as HTMLElement; const e = node as HTMLElement;
return ( return (
e.classList && e.classList &&
(e.classList.contains(IMAGE_INFO_CLASS) || (e.classList.contains(IMAGE_INFO_CLASS) ||
e.classList.contains(FILE_INFO_CLASS)) e.classList.contains(FILE_INFO_CLASS))
); );
} }
@Component({ components: {} }) @Component({ components: {} })
export default class Input extends Vue { export default class Input extends Vue {
@chatStoreNamespace.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) @chatStoreNamespace.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@Ref("input") @Ref("input")
private readonly messageInputBox!: HTMLDivElement private readonly messageInputBox!: HTMLDivElement;
private file = "" private file = "";
private acceptType = "image/*" private acceptType = "image/*";
private emojiPanelVisibility = false private emojiPanelVisibility = false;
private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})` private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})`;
private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})` private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})`;
private emoji: { name: string; emoji_chars: string; code: string }[] = [] private emoji: { name: string; emoji_chars: string; code: string }[] = [];
@Watch("chatId") @Watch("chatId")
private onChatIdChanged(v: number, old: number) { private onChatIdChanged(v: number, old: number) {
if (old) { if (old) {
const current = this.getNodeListFromInputBox(); const current = this.getNodeListFromInputBox();
if (current && current.length) { if (current && current.length) {
chatCache[old] = current; chatCache[old] = current;
} }
}
this.clearInput();
if (v) {
const cache = chatCache[v];
if (cache) {
const e = document.querySelector(
"#chat-input-box"
) as HTMLElement;
if (e) {
for (const node of cache as ChildNode[]) {
e.appendChild(node);
}
}
}
}
}
public mounted() {
this.messageInputBox.addEventListener("paste", this.handlePasteEvent);
document.addEventListener("click", this.hideEmoji);
document.addEventListener("selectionchange", this.handleSaveRange);
this.$onBeforeDestroy(() => {
document.removeEventListener("click", this.hideEmoji);
document.removeEventListener(
"selectionchange",
this.handleSaveRange
);
});
this.setupEmoji();
this.focus();
} }
public focus() { this.clearInput();
this.messageInputBox.focus();
}
private clearInput() { if (v) {
this.messageInputBox.innerHTML = ""; const cache = chatCache[v];
const input = document.getElementById( if (cache) {
"chat-upload-file" const e = document.querySelector("#chat-input-box") as HTMLElement;
) as HTMLInputElement; if (e) {
if (input) { for (const node of cache as ChildNode[]) {
input.value = ""; e.appendChild(node);
}
} }
}
} }
}
private allowLoadImg() {
this.acceptType = "image/*"; public mounted() {
} this.messageInputBox.addEventListener("paste", this.handlePasteEvent);
document.addEventListener("click", this.hideEmoji);
private allowLoadFile() { document.addEventListener("selectionchange", this.handleSaveRange);
this.acceptType = "*"; this.setupEmoji();
this.focus();
}
public beforeDestroy() {
document.removeEventListener("click", this.hideEmoji);
document.removeEventListener("selectionchange", this.handleSaveRange);
}
public focus() {
this.messageInputBox.focus();
}
private clearInput() {
this.messageInputBox.innerHTML = "";
const input = document.getElementById(
"chat-upload-file"
) as HTMLInputElement;
if (input) {
input.value = "";
} }
}
private async handlePasteEvent(event: ClipboardEvent) {
/* private allowLoadImg() {
* 组织默认行为原因 this.acceptType = "image/*";
* 1、浏览器自带复制粘贴图片到输入框的功能,与js加工后的图片重复了, }
* 2、默认复制粘贴功能会粘贴dom结构
* */ private allowLoadFile() {
event.preventDefault(); this.acceptType = "*";
const items = event.clipboardData && event.clipboardData.items; }
let html = "";
const promiseArr = []; private async handlePasteEvent(event: ClipboardEvent) {
if (items && items.length) { /*
// 检索剪切板items中类型带有image的 * 组织默认行为原因
for (let i = 0; i < items.length; i++) { * 1、浏览器自带复制粘贴图片到输入框的功能,与js加工后的图片重复了,
if (items[i].kind === "file") { * 2、默认复制粘贴功能会粘贴dom结构
const file = items[i].getAsFile(); * */
if (file) { event.preventDefault();
if (file.size <= 0) { const items = event.clipboardData && event.clipboardData.items;
this.$emit("error", MESSAGE_FILE_EMPTY); let html = "";
return; const promiseArr = [];
} if (items && items.length) {
if (this.isImage(file)) { // 检索剪切板items中类型带有image的
if (file.size >= MAX_IMAGE_SIZE) { for (let i = 0; i < items.length; i++) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE); if (items[i].kind === "file") {
return; const file = items[i].getAsFile();
} if (file) {
html += this.buildImageHtml(file); if (file.size <= 0) {
} else { this.$emit("error", MESSAGE_FILE_EMPTY);
if (file.size >= MAX_FILE_SIZE) { return;
this.$emit("error", MESSAGE_FILE_TOO_LARGE); }
return; if (this.isImage(file)) {
} if (file.size >= MAX_IMAGE_SIZE) {
html += this.buildFileHtml(file); this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
return;
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
}
html += this.buildFileHtml(file);
}
break;
}
} else {
promiseArr.push(
new Promise<void>((resolve) => {
const contentType = items[i].type;
items[i].getAsString((k) => {
/*
* items第一项是文本
* 第二项是带有dom结构的文本(包含格式)
* 第三项写明了数据是从哪里复制来的
*/
if (i === 0) {
if (contentType === "text/plain") {
html += k;
}
} else if (i === 1) {
const srcRegex = /<img[^>]+src="([^">]+)"\s+title="([^">]+)"/g;
let result;
do {
result = srcRegex.exec(k);
if (result) {
const [, src, name] = result;
html += `<img tabindex="-1" src="${src}" data-image='${JSON.stringify(
{
url: src,
name,
} }
break; )}'>`;
html = html.replace(name, "");
} }
} else { } while (result);
promiseArr.push(
new Promise<void>((resolve) => {
const contentType = items[i].type;
items[i].getAsString((k) => {
/*
* items第一项是文本
* 第二项是带有dom结构的文本(包含格式)
* 第三项写明了数据是从哪里复制来的
*/
if (i === 0) {
if (contentType === "text/plain") {
html += k;
}
} else if (i === 1) {
const srcRegex =
/<img[^>]+src="([^">]+)"\s+title="([^">]+)"/g;
let result;
do {
result = srcRegex.exec(k);
if (result) {
const [, src, name] = result;
html += `<img tabindex="-1" src="${src}" data-image='${JSON.stringify(
{
url: src,
name,
}
)}'>`;
html = html.replace(name, "");
}
} while (result);
}
resolve();
});
})
);
} }
} resolve();
} });
await Promise.all(promiseArr); })
if (html) { );
this.insertHtmlAtCaret(html);
} }
}
} }
await Promise.all(promiseArr);
private async handleSendMsg(e: Event) { if (html) {
// 防止换行 this.insertHtmlAtCaret(html);
e.preventDefault();
return new Promise((resolve, reject) => {
try {
const data = this.getNodeListFromInputBox();
this.$emit("send", data, resolve);
} catch (e) {
this.$emit("error", e);
reject(e);
}
}).then(() => {
this.clearInput();
if (this.chatId) {
chatCache[this.chatId] = [];
}
});
}
/**
* 获取输入框中的内容
* @returns 返回的是节点数组
*/
private getNodeListFromInputBox() {
this.messageInputBox.normalize();
const nodes = Array.from(this.messageInputBox.childNodes);
return this.combine(nodes);
} }
}
/**
* 文本,链接等需要合并成纯文本发送 private async handleSendMsg(e: Event) {
*/ // 防止换行
private combine(nodes: ChildNode[]) { e.preventDefault();
const sendingNodes: ChildNode[] = []; return new Promise((resolve, reject) => {
let needCreateNewNode = false; try {
let text = ""; const data = this.getNodeListFromInputBox();
for (const item of nodes) { this.$emit("send", data, resolve);
if (!isImageOrFile(item) && item.textContent) { } catch (e) {
if (needCreateNewNode) { this.$emit("error", e);
text = ""; reject(e);
needCreateNewNode = false; }
} }).then(() => {
text += item.textContent; this.clearInput();
} else { if (this.chatId) {
needCreateNewNode = true; chatCache[this.chatId] = [];
if (text) { }
this.checkTextLength(text); });
const node = document.createTextNode(text); }
sendingNodes.push(node);
} /**
sendingNodes.push(item); * 获取输入框中的内容
} * @returns 返回的是节点数组
*/
private getNodeListFromInputBox() {
this.messageInputBox.normalize();
const nodes = Array.from(this.messageInputBox.childNodes);
return this.combine(nodes);
}
/**
* 文本,链接等需要合并成纯文本发送
*/
private combine(nodes: ChildNode[]) {
const sendingNodes: ChildNode[] = [];
let needCreateNewNode = false;
let text = "";
for (const item of nodes) {
if (!isImageOrFile(item) && item.textContent) {
if (needCreateNewNode) {
text = "";
needCreateNewNode = false;
} }
text += item.textContent;
} else {
needCreateNewNode = true;
if (text) { if (text) {
this.checkTextLength(text); this.checkTextLength(text);
const node = document.createTextNode(text); const node = document.createTextNode(text);
sendingNodes.push(node); sendingNodes.push(node);
}
return sendingNodes;
}
private checkTextLength(text: string) {
if (text.length >= 4000) {
throw new Error("消息不能超过4000个字");
} }
sendingNodes.push(item);
}
} }
private handleSaveRange() { if (text) {
const sel = window.getSelection(); this.checkTextLength(text);
const node = document.createTextNode(text);
if (sel && sel.rangeCount) { sendingNodes.push(node);
const range = sel.getRangeAt(0);
const oldRange = this.getRange && this.getRange();
if (
this.messageInputBox &&
this.messageInputBox.contains(range.endContainer)
) {
if (
oldRange &&
range.collapsed &&
range.endContainer === oldRange.endContainer &&
range.endOffset === oldRange.endOffset
) {
return;
}
this.saveRange && this.saveRange(range);
}
}
}
private saveRange?: (param: Range) => void
private getRange?: () => Range | null
/**
* 在光标处插入元素
*/
public insertHtmlAtCaret = (function () {
let range: Range | null = null;
return function (this: Input, html: string) {
if (this.saveRange == null) {
this.saveRange = (alternate) => (range = alternate);
}
if (this.getRange == null) {
this.getRange = () => range;
}
const sel = window.getSelection();
if (sel) {
if (range) {
range.deleteContents();
sel.removeAllRanges();
sel.addRange(range);
} else {
this.focus();
range = sel.getRangeAt(0);
document.execCommand("selectAll");
window.getSelection()?.collapseToEnd();
}
}
const el = document.createElement("div");
el.innerHTML = html;
const frag = document.createDocumentFragment();
let node = null;
let lastNode = null;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range?.insertNode(frag);
// Preserve the selection
if (lastNode) {
if (range) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
}
}
};
})()
private selectEmoji(emoji: any) {
this.insertHtmlAtCaret(emoji.emoji_chars);
this.hideEmoji();
}
private isImage(file: File) {
return file.type.startsWith("image");
} }
private buildImageHtml(file: File) { return sendingNodes;
const url = URL.createObjectURL(file); }
return `<img class="${IMAGE_INFO_CLASS}" tabindex="-1" src="${url}" data-image='${JSON.stringify(
{
url,
name: file.name,
size: file.size,
}
)}'>`;
}
private buildFileHtml(file: File) { private checkTextLength(text: string) {
const extension = file.name.split("."); if (text.length >= 4000) {
const type = getFileType(file.name); throw new Error("消息不能超过4000个字");
return `<div class="${FILE_INFO_CLASS}" tabindex="-1" title="${
extension[extension.length - 1]
}" data-file='${JSON.stringify({
url: URL.createObjectURL(file),
name: file.name,
size: file.size,
})}'><div style="display: inline-block"><div class="file-name text-truncate text-nowrap">${
file.name
}</div><div class="file-size">${formatFileSize(
file.size
)}</div></div><span class="file-icon" title="${type}">${getSvg(
type
)}</span></div> `; // 注意</div>最后面有个空
} }
}
private onChange(e: Event) {
const target = e.target as HTMLElement; private handleSaveRange() {
if (target) { const sel = window.getSelection();
const input = document.getElementById(
target.id as string if (sel && sel.rangeCount) {
) as HTMLInputElement; const range = sel.getRangeAt(0);
if (input) { const oldRange = this.getRange && this.getRange();
const files = input.files; if (
if (files && files.length) { this.messageInputBox &&
let html = ""; this.messageInputBox.contains(range.endContainer)
for (let index = 0; index < files.length; index++) { ) {
const file = files[index]; if (
if (file.size <= 0) { oldRange &&
this.$emit("error", MESSAGE_FILE_EMPTY); range.collapsed &&
return; range.endContainer === oldRange.endContainer &&
} range.endOffset === oldRange.endOffset
if (this.isImage(file)) { ) {
if (file.size >= MAX_IMAGE_SIZE) { return;
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
return;
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
}
html += this.buildFileHtml(file);
}
}
this.insertHtmlAtCaret(html);
}
}
} }
}
private toggleEmoji() { this.saveRange && this.saveRange(range);
this.emojiPanelVisibility = !this.emojiPanelVisibility; }
} }
}
private hideEmoji(e?: Event) {
if (e && e.target) { private saveRange?: (param: Range) => void;
const target = e.target as HTMLElement; private getRange?: () => Range | null;
if (target.closest(".emoji-picker")) {
/**
* 在光标处插入元素
*/
public insertHtmlAtCaret = (function() {
let range: Range | null = null;
return function(this: Input, html: string) {
if (this.saveRange == null) {
this.saveRange = (alternate) => (range = alternate);
}
if (this.getRange == null) {
this.getRange = () => range;
}
const sel = window.getSelection();
if (sel) {
if (range) {
range.deleteContents();
sel.removeAllRanges();
sel.addRange(range);
} else {
this.focus();
range = sel.getRangeAt(0);
document.execCommand("selectAll");
window.getSelection()?.collapseToEnd();
}
}
const el = document.createElement("div");
el.innerHTML = html;
const frag = document.createDocumentFragment();
let node = null;
let lastNode = null;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range?.insertNode(frag);
// Preserve the selection
if (lastNode) {
if (range) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
}
}
};
})();
private selectEmoji(emoji: any) {
this.insertHtmlAtCaret(emoji.emoji_chars);
this.hideEmoji();
}
private isImage(file: File) {
return file.type.startsWith("image");
}
private buildImageHtml(file: File) {
const url = URL.createObjectURL(file);
return `<img class="${IMAGE_INFO_CLASS}" tabindex="-1" src="${url}" data-image='${JSON.stringify(
{
url,
name: file.name,
size: file.size,
}
)}'>`;
}
private buildFileHtml(file: File) {
const extension = file.name.split(".");
const type = getFileType(file.name);
return `<div class="${FILE_INFO_CLASS}" tabindex="-1" title="${
extension[extension.length - 1]
}" data-file='${JSON.stringify({
url: URL.createObjectURL(file),
name: file.name,
size: file.size,
})}'><div style="display: inline-block"><div class="file-name text-truncate text-nowrap">${
file.name
}</div><div class="file-size">${formatFileSize(
file.size
)}</div></div><span class="file-icon" title="${type}">${getSvg(
type
)}</span></div> `; // 注意</div>最后面有个空
}
private onChange(e: Event) {
const target = e.target as HTMLElement;
if (target) {
const input = document.getElementById(
target.id as string
) as HTMLInputElement;
if (input) {
const files = input.files;
if (files && files.length) {
let html = "";
for (let index = 0; index < files.length; index++) {
const file = files[index];
if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY);
return;
}
if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
return; return;
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
}
html += this.buildFileHtml(file);
} }
}
this.insertHtmlAtCaret(html);
} }
this.emojiPanelVisibility = false; }
} }
}
private setupEmoji() {
EmojiService.onReady(() => { private toggleEmoji() {
const service = new EmojiService(); this.emojiPanelVisibility = !this.emojiPanelVisibility;
service.getEmoji().then((r) => { }
if (r) {
this.emoji = r.list; private hideEmoji(e?: Event) {
} if (e && e.target) {
}); const target = e.target as HTMLElement;
}); if (target.closest(".emoji-picker")) {
return;
}
} }
this.emojiPanelVisibility = false;
}
private setupEmoji() {
EmojiService.onReady(() => {
const service = new EmojiService();
service.getEmoji().then((r) => {
if (r) {
this.emoji = r.list;
}
});
});
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.input-wrap { .input-wrap {
position: relative; position: relative;
padding-left: 20px; padding-left: 20px;
/deep/.input-el-scrollbar.el-scrollbar { /deep/.input-el-scrollbar.el-scrollbar {
// 28px : tool-bar的高度 // 28px : tool-bar的高度
height: calc(100% - 28px); height: calc(100% - 28px);
> .el-scrollbar__wrap { > .el-scrollbar__wrap {
overflow-x: hidden; overflow-x: hidden;
} }
> .el-scrollbar__wrap > .el-scrollbar__view { > .el-scrollbar__wrap > .el-scrollbar__view {
min-height: 100%; min-height: 100%;
display: flex; display: flex;
// 输入框
> .input-container {
width: 100%;
font-size: 14px;
padding: 10px 20px 10px 0;
outline: 0;
white-space: pre-wrap;
user-select: text;
&::selection {
background-color: #cce6fc;
}
img { // 输入框
max-width: 300px; > .input-container {
font-size: 50px; width: 100%;
vertical-align: bottom; font-size: 14px;
border: 1px solid transparent; padding: 10px 20px 10px 0;
outline: 0;
white-space: pre-wrap;
user-select: text;
&:focus { &::selection {
border-color: var(--main-color); background-color: #cce6fc;
} }
&::selection { img {
background-color: #cce6fc; max-width: 300px;
} font-size: 50px;
} vertical-align: bottom;
border: 1px solid transparent;
.file-info { &:focus {
padding: 10px; border-color: var(--main-color);
border: 1px solid #c5d4e5; }
border-radius: 4px;
display: inline-block;
margin-right: 10px;
-webkit-user-modify: read-only;
user-select: none;
.file-name {
color: #000;
font-size: 14px;
max-width: 130px;
}
.file-size { &::selection {
margin-top: 10px; background-color: #cce6fc;
} }
}
}
} }
}
.input-emoji { .file-info {
position: absolute; padding: 10px;
left: 0; border: 1px solid #c5d4e5;
top: -225px; border-radius: 4px;
outline: 0; display: inline-block;
margin-right: 10px;
-webkit-user-modify: read-only;
user-select: none;
.file-name {
color: #000;
font-size: 14px;
max-width: 130px;
}
.file-size {
margin-top: 10px;
}
}
}
} }
}
.input-emoji {
position: absolute;
left: 0;
top: -225px;
outline: 0;
}
} }
.tool-bar { .tool-bar {
padding-top: 10px; padding-top: 10px;
user-select: none; user-select: none;
.tool-bar-icon { .tool-bar-icon {
height: 16px; height: 16px;
width: 16px; width: 16px;
cursor: pointer; cursor: pointer;
} }
.offset { .offset {
margin: 0 22px; margin: 0 22px;
} }
} }
.emoji-picker { .emoji-picker {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
bottom: 99px; bottom: 99px;
left: -1px; left: -1px;
background-color: #fff; background-color: #fff;
padding: 20px; padding: 20px;
padding-right: 0; padding-right: 0;
padding-bottom: 10px; padding-bottom: 10px;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
right: 45px; right: 45px;
overflow: hidden; overflow: hidden;
.el-scrollbar { .el-scrollbar {
height: 200px; height: 200px;
} }
.emoji-item { .emoji-item {
display: inline-flex; display: inline-flex;
cursor: pointer; cursor: pointer;
min-width: 35px; min-width: 35px;
min-height: 25px; min-height: 25px;
font-size: 20px; font-size: 20px;
vertical-align: top; vertical-align: top;
margin-top: 5px; margin-top: 5px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
} }
#chat-upload-file { #chat-upload-file {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
} }
</style> </style>
<template> <template>
<div class="h-100"> <div class="h-100">
<chat-input <chat-input
ref="chat-input" ref="chat-input"
@input="onInput" @input="onInput"
@send="sendMessage" @send="sendMessage"
@error="onError" @error="onError"
/> />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Ref, Vue, Watch } from "vue-property-decorator"; import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import ChatInput, { import ChatInput, {
FILE_INFO_CLASS, FILE_INFO_CLASS,
isImageOrFile, isImageOrFile,
} from "./hybrid-input/index.vue"; } from "./hybrid-input/index.vue";
import { Message } from "./model"; import { Message } from "./model";
import { uploadFile } from "./service/upload"; import { uploadFile } from "./service/upload";
...@@ -26,169 +26,167 @@ let sendingMessageIndex = 1; ...@@ -26,169 +26,167 @@ let sendingMessageIndex = 1;
@Component({ components: { ChatInput } }) @Component({ components: { ChatInput } })
export default class MessageInput extends Vue { export default class MessageInput extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE) @chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE)
private readonly chatRoomVisible: ChatStore.STATE_CHAT_DIALOG_VISIBLE private readonly chatRoomVisible!: ChatStore.STATE_CHAT_DIALOG_VISIBLE;
@chatStore.Action(ChatStore.ACTION_SEND_MESSAGE) @chatStore.Action(ChatStore.ACTION_SEND_MESSAGE)
private readonly sendMsg!: ChatStore.ACTION_SEND_MESSAGE private readonly sendMsg!: ChatStore.ACTION_SEND_MESSAGE;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CHAT_MY_ID) @chatStore.State(ChatStore.STATE_CHAT_MY_ID)
private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_INITING) @chatStore.State(ChatStore.STATE_CURRENT_CHAT_INITING)
private readonly chatIniting!: ChatStore.STATE_CURRENT_CHAT_INITING private readonly chatIniting!: ChatStore.STATE_CURRENT_CHAT_INITING;
@chatStore.Getter(ChatStore.STATE_CHAT_SOURCE) @chatStore.Getter(ChatStore.STATE_CHAT_SOURCE)
private readonly source!: ChatStore.STATE_CHAT_SOURCE private readonly source!: ChatStore.STATE_CHAT_SOURCE;
@chatStore.Mutation(ChatStore.MUTATION_APPEND_SENDING_MESSAGE) @chatStore.Mutation(ChatStore.MUTATION_APPEND_SENDING_MESSAGE)
private readonly appendSendingMessages!: ChatStore.MUTATION_APPEND_SENDING_MESSAGE private readonly appendSendingMessages!: ChatStore.MUTATION_APPEND_SENDING_MESSAGE;
@chatStore.Mutation(ChatStore.MUTATION_FAILED_SENDING_MESSAGE) @chatStore.Mutation(ChatStore.MUTATION_FAILED_SENDING_MESSAGE)
private readonly failedSendingMessage!: ChatStore.MUTATION_FAILED_SENDING_MESSAGE private readonly failedSendingMessage!: ChatStore.MUTATION_FAILED_SENDING_MESSAGE;
@chatStore.Mutation(ChatStore.MUTATION_REMOVE_SENDING_MESSAGE) @chatStore.Mutation(ChatStore.MUTATION_REMOVE_SENDING_MESSAGE)
private readonly removeSendingMessages!: ChatStore.MUTATION_REMOVE_SENDING_MESSAGE private readonly removeSendingMessages!: ChatStore.MUTATION_REMOVE_SENDING_MESSAGE;
@Ref("chat-input") @Ref("chat-input")
private readonly chatInput!: ChatInput private readonly chatInput!: ChatInput;
@Watch("chatRoomVisible") @Watch("chatRoomVisible")
private whenChatRoomShow() { private whenChatRoomShow() {
if (!this.chatRoomVisible) return; if (!this.chatRoomVisible) return;
this.chatInput.focus(); this.chatInput.focus();
} }
private async sendMessage(msg: ChildNode[], done: () => void) { private async sendMessage(msg: ChildNode[], done: () => void) {
if (this.chatIniting) { if (this.chatIniting) {
return; return;
}
for (const item of msg) {
if (isImageOrFile(item)) {
if ((item as Element).classList.contains(FILE_INFO_CLASS)) {
this.sendFile(item, "file");
} else {
this.sendFile(item, "image");
} }
for (const item of msg) { continue;
if (isImageOrFile(item)) { }
if ((item as Element).classList.contains(FILE_INFO_CLASS)) {
this.sendFile(item, "file");
} else {
this.sendFile(item, "image");
}
continue;
}
if (item.textContent) { if (item.textContent) {
this.sendText(item.textContent); this.sendText(item.textContent);
} }
}
ChatLoggerService.logger?.debug("all messages sent");
done();
this.$emit("sent");
} }
ChatLoggerService.logger?.debug("all messages sent");
private async onInput() { done();
if (this.chatId == null) return; this.$emit("sent");
await xim.inputing(this.chatId); }
private async onInput() {
if (this.chatId == null) return;
await xim.inputing(this.chatId);
}
private sendText(text: string) {
if (text && text.trim()) {
const msg = { text: text.trim() };
if (this.source) {
Object.assign(msg, { source: this.source });
}
this.sendMsg({ msgType: "text", msg: JSON.stringify(msg) });
} }
}
private sendText(text: string) {
if (text && text.trim()) { private async sendFile(file: any, type: "image" | "file") {
const msg = { text: text.trim() }; const src = JSON.parse(file.attributes[`data-${type}`]?.value || "") as {
if (this.source) { url: string;
Object.assign(msg, { source: this.source }); name: string;
} size: number;
this.sendMsg({ msgType: "text", msg: JSON.stringify(msg) }); };
if (src) {
const index = this.sendSendingMessage(type, src);
const file = await this.readBlobUrl2Base64(src.url, src.name);
if (file) {
let w = 0;
let h = 0;
if (type === "image") {
const img = new Image();
img.src = src.url;
img.onload = function() {
w = img.naturalWidth;
h = img.naturalHeight;
};
img.remove();
} }
} uploadFile(file, this.chatId || 0, w, h)
.then((r) => {
private async sendFile(file: any, type: "image" | "file") { if (r) {
const src = JSON.parse( const msg = {
file.attributes[`data-${type}`]?.value || "" url: r,
) as { name: file.name,
url: string; size: file.size,
name: string; };
size: number; if (this.source) {
}; Object.assign(msg, { source: this.source });
if (src) { }
const index = this.sendSendingMessage(type, src);
const file = await this.readBlobUrl2Base64(src.url, src.name);
if (file) {
let w = 0;
let h = 0;
if (type === "image") {
const img = new Image();
img.src = src.url;
img.onload = function () {
w = img.naturalWidth;
h = img.naturalHeight;
};
img.remove();
}
uploadFile(file, this.chatId || 0, w, h)
.then((r) => {
if (r) {
const msg = {
url: r,
name: file.name,
size: file.size,
};
if (this.source) {
Object.assign(msg, { source: this.source });
}
if (w && h) {
Object.assign(msg, { w, h });
}
this.sendMsg({
msgType: type,
msg: JSON.stringify(msg),
});
this.removeSendingMessages(index);
URL.revokeObjectURL(src.url);
} else {
this.setMsg2Failed(index);
}
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.setMsg2Failed(index);
});
}
}
}
private setMsg2Failed(index: number) { if (w && h) {
this.failedSendingMessage(index); Object.assign(msg, { w, h });
} }
private sendSendingMessage(type: string, msg: any) { this.sendMsg({
const index = sendingMessageIndex++; msgType: type,
if (this.source) {
Object.assign(msg, { source: this.source, eid: this.chatMyId });
}
if (this.chatId) {
this.appendSendingMessages({
id: -index,
chat_id: this.chatId,
ts: Date.now(),
type,
msg: JSON.stringify(msg), msg: JSON.stringify(msg),
} as Message); });
return -index; this.removeSendingMessages(index);
} URL.revokeObjectURL(src.url);
return 0; } else {
this.setMsg2Failed(index);
}
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.setMsg2Failed(index);
});
}
} }
}
private readBlobUrl2Base64(url: string, name: string) { private setMsg2Failed(index: number) {
return fetch(url) this.failedSendingMessage(index);
.then((r) => r.blob()) }
.then((blob) => new File([blob], name));
}
private onError(e: any) { private sendSendingMessage(type: string, msg: any) {
this.$emit("error", e.message || e); const index = sendingMessageIndex++;
if (this.source) {
Object.assign(msg, { source: this.source, eid: this.chatMyId });
}
if (this.chatId) {
this.appendSendingMessages({
id: -index,
chat_id: this.chatId,
ts: Date.now(),
type,
msg: JSON.stringify(msg),
} as Message);
return -index;
} }
return 0;
}
private readBlobUrl2Base64(url: string, name: string) {
return fetch(url)
.then((r) => r.blob())
.then((blob) => new File([blob], name));
}
private onError(e: any) {
this.$emit("error", e.message || e);
}
} }
</script> </script>
import Axios from "axios"; import Axios from "axios";
import qs from "qs"; import qs from "qs";
import chat from "../xim/index";
export function buildConfig(token: string, url: string) { export function buildConfig(token: string, url: string) {
if (url && url.includes("/general")) { if (url && url.includes("/general")) {
return { headers: { Authorization: token, CurrentOrg: this.station } }; return { headers: { Authorization: token, CurrentOrg: chat.getOrgId() } };
} }
return { headers: { Authorization: token } }; return { headers: { Authorization: token } };
} }
......
...@@ -29,7 +29,7 @@ function uniqueMessages( ...@@ -29,7 +29,7 @@ function uniqueMessages(
messages: NonNullable<ChatStore.STATE_CHAT_MSG_HISTORY> messages: NonNullable<ChatStore.STATE_CHAT_MSG_HISTORY>
) { ) {
const arr = [...messages]; const arr = [...messages];
return unique(arr, function (item, all) { return unique(arr, function(item, all) {
return all.findIndex((k) => k.id === item.id); return all.findIndex((k) => k.id === item.id);
}); });
} }
...@@ -254,7 +254,7 @@ export default { ...@@ -254,7 +254,7 @@ export default {
state[ChatStore.STATE_CHAT_SENDING_MESSAGES] = [...current]; state[ChatStore.STATE_CHAT_SENDING_MESSAGES] = [...current];
} }
}, },
[ChatStore.MUTATION_SAVE_CURRENT_CHAT_INPUTING]: (function () { [ChatStore.MUTATION_SAVE_CURRENT_CHAT_INPUTING]: (function() {
const setTimeoutId: { [key: string]: number } = {}; const setTimeoutId: { [key: string]: number } = {};
return ( return (
state: ChatStoreState, state: ChatStoreState,
...@@ -432,7 +432,7 @@ export default { ...@@ -432,7 +432,7 @@ export default {
detailManager.done(); detailManager.done();
const { id } = await action.dryExecute(); const { id } = await action.dryExecute();
// 无法得到chat id // 无法得到chat id
await sdk() await sdk()
.model(UniplatChatModelName) .model(UniplatChatModelName)
.action("createXimChat") .action("createXimChat")
.updateInitialParams({ .updateInitialParams({
...@@ -441,9 +441,12 @@ export default { ...@@ -441,9 +441,12 @@ export default {
.dryExecute(); .dryExecute();
commit(ChatStore.MUTATION_HIDE_CHAT_CREATOR); commit(ChatStore.MUTATION_HIDE_CHAT_CREATOR);
await dispatch(ChatStore.ACTION_GET_MY_CHAT_LIST); await dispatch(ChatStore.ACTION_GET_MY_CHAT_LIST);
const newChat = state[ChatStore.STATE_MY_CHAT_ROOM_LIST].list.find( const roomList = state[ChatStore.STATE_MY_CHAT_ROOM_LIST];
if (roomList == null) return
const newChat = roomList.list.find(
(k) => k.uniplatId === id (k) => k.uniplatId === id
); );
if (newChat == null) return
commit(ChatStore.MUTATION_SHOW_CHAT); commit(ChatStore.MUTATION_SHOW_CHAT);
await dispatch(ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION, { await dispatch(ChatStore.ACTION_SAVE_CURRENT_CHAT_ID_VERSION, {
chatId: newChat.chat_id, chatId: newChat.chat_id,
...@@ -538,10 +541,11 @@ export default { ...@@ -538,10 +541,11 @@ export default {
const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID]; const chatId = state[ChatStore.STATE_CHAT_CURRENT_CHAT_ID];
if (chatId == null) return; if (chatId == null) return;
const getChatMembersResult = await xim.fetchChatMembers(chatId); const getChatMembersResult = await xim.fetchChatMembers(chatId);
if (getChatMembersResult == null) return
const chatMembers = getChatMembersResult.args[0] as ChatMember[]; const chatMembers = getChatMembersResult.args[0] as ChatMember[];
const newChatMembers = await Promise.all( const newChatMembers = await Promise.all(
chatMembers.map(async (member) => { chatMembers.map(async (member) => {
let result: ChatStore.STATE_CURRENT_CHAT_MEMBERS[number]; let result: NonNullable<ChatStore.STATE_CURRENT_CHAT_MEMBERS>[number];
try { try {
const info = await sdk() const info = await sdk()
.model("user") .model("user")
...@@ -562,13 +566,14 @@ export default { ...@@ -562,13 +566,14 @@ export default {
); );
commit( commit(
ChatStore.MUTATION_SAVE_CURRENT_CHAT_MEMBERS, ChatStore.MUTATION_SAVE_CURRENT_CHAT_MEMBERS,
unique(newChatMembers, function (item, all) { unique(newChatMembers, function(item, all) {
return all.findIndex((k) => k.eid === item.eid); return all.findIndex((k) => k.eid === item.eid);
}) })
); );
}, },
async [ChatStore.ACTION_TERINATE_CHAT]({ state, dispatch }) { async [ChatStore.ACTION_TERINATE_CHAT]({ state, dispatch }) {
const v = state[ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION]; const v = state[ChatStore.STATE_CHAT_CURRENT_CHAT_VERSION];
if (v == null) return
const id = Number( const id = Number(
state[ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID] state[ChatStore.STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID]
); );
......
...@@ -30,7 +30,7 @@ export namespace ChatStore { ...@@ -30,7 +30,7 @@ export namespace ChatStore {
is_finish: 0 | 1; is_finish: 0 | 1;
}[]; }[];
total: number; total: number;
} } | null
export const STATE_CHAT_MSG_HISTORY = "某个会话聊天记录"; export const STATE_CHAT_MSG_HISTORY = "某个会话聊天记录";
export type STATE_CHAT_MSG_HISTORY = dto.MessageRequestResult | null export type STATE_CHAT_MSG_HISTORY = dto.MessageRequestResult | null
......
...@@ -44,7 +44,9 @@ class Chat { ...@@ -44,7 +44,9 @@ class Chat {
} }
public getSdk = () => { public getSdk = () => {
if (this._sdk == null) return; if (this._sdk == null) {
throw new Error("sdk shouldn't undefined")
};
return this._sdk(); return this._sdk();
}; };
...@@ -90,7 +92,7 @@ class Chat { ...@@ -90,7 +92,7 @@ class Chat {
} }
public getUserMapping() { public getUserMapping() {
return {}; return {} as any;
} }
private debug(message: string) { private debug(message: string) {
......
...@@ -145,7 +145,9 @@ export class Xim { ...@@ -145,7 +145,9 @@ export class Xim {
desc = true desc = true
): Promise<Message[]> { ): Promise<Message[]> {
this.checkConnected(); this.checkConnected();
if (this.client == null) return; if (this.client == null) {
throw new Error("client shouldn't undefined")
};
const res = await this.client.fetchChatMsgs(chatType, chatId, { const res = await this.client.fetchChatMsgs(chatType, chatId, {
lid, lid,
rid, rid,
......
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