Commit 98c80ac9 authored by chenqikuai's avatar chenqikuai

Merge branch 'chat' into dev

parents 722d2573 9ea0283f
<template>
<div
class="item flex items-center justify-center ml-4"
:class="{ 'select-item': selected }"
>{{ value }}</div>
</template>
<script setup lang="ts">
import { PropType } from "@vue/runtime-core";
const props = defineProps({
selected: {
required: true,
type: Boolean,
},
value: {
required: true,
type: String as PropType<string>,
}
})
</script>
<style>
.item {
width: 100px;
height: 35px;
background: #f5f6f9;
border-radius: 20px;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #1b1f37;
}
.select-item {
background: #3e4faf;
color: #ffffff;
}
</style>
\ No newline at end of file
<template>
<div class="ChatOption bg-white flex items-center">
<div
v-for="item in props.list"
:key="item.name"
class="item flex items-center justify-center ml-4"
:class="{ 'select-item': props.selected === item.id }"
@click="props.setSelected && props.setSelected(item.id)"
>{{ item.name }}</div>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { PropType } from "vue";
const props = defineProps({
list: {
type: Array as PropType<{ name: string, id: number }[]>,
},
selected: {
type: Number,
},
setSelected: {
type: Function,
requied: true
},
});
</script>
<style lang="less" scoped>
.ChatOption {
......
<template>
<div>
<van-popup v-model:show="show" round teleport="body" :style="{ width: '90%', top: '80%' }" @click-overlay="hide">
<div class="text-center py-4" @click="handleClickCall">
<icon
name="icon-a-dianhua"
color="#3E4FAF"
size="17"
class="inline-block pr-3 align-text-bottom"
/>
<span class="text-app-blue text-sm align-middle font-semibold">呼叫{{ phone }}</span>
</div>
</van-popup>
<van-popup
<van-popup
v-model:show="show"
round
teleport="body"
:style="{ width: '90%', top: '80%' }"
@click-overlay="hide"
>
<div class="text-center py-4" @click="handleClickCall">
<icon
name="icon-a-dianhua"
color="#3E4FAF"
size="17"
class="inline-block pr-3 align-text-bottom"
/>
<span class="text-app-blue text-sm align-middle font-semibold">呼叫{{ phone }}</span>
</div>
</van-popup>
<van-popup
v-model:show="show"
round
:style="{ width: '90%', margin: '60px auto 0px auto', top: '80%' }"
......@@ -28,7 +34,7 @@
</template>
<script lang="ts" setup>
import Vue, { ComponentInternalInstance, getCurrentInstance } from 'vue'
import Vue, { getCurrentInstance } from 'vue'
import jsBridge from "@/utils/jsBridge2"
const { ctx } = getCurrentInstance() as any
......
export const TRANSFER_HUMAN_1 = '客服经理已接入会话,我们预计3分钟内联系您'
export const TRANSFER_HUMAN_2 = '客户已接入会话,请尽快联系客户'
export const CONST_START_CHAT = {
user: { id: '1', value: '客服经理已接入会话,我们预计3分钟内联系您' },
staff: { id: '2', value: '客户已接入会话,请尽快联系客户' },
}
export const CONST_END_CHAT = {
user: { id: '3', value: '会话已结束' },
staff: { id: '4', value: '会话已结束' },
}
export const CHAT_CONST_LIST = [CONST_START_CHAT, CONST_END_CHAT]
import { DisplayMessage } from '@/store/messagesStore'
import { ChatMessageTypes } from '@/types/chatMessageTypes'
import { eRole } from '@/types/roleType'
import ChatDataService from '@/utils/ChatDataService'
import {
getMasterIdFromDisplayMsg,
getTargetIdFromDisplayMsg,
} from '@/utils/chatutils'
import { getUserMsg } from '@/utils/userMsg'
import { iChatListCard, MyAppDatabase } from './index'
export default class ChatListCardDB extends MyAppDatabase {
......@@ -72,7 +76,17 @@ export default class ChatListCardDB extends MyAppDatabase {
})
.first()
const content = msg.content?.content || '[新消息]'
let content: string
if (msg.type === ChatMessageTypes.Card) {
content =
ChatDataService.getInstance().extractCommonMsgContentFromMsg(
msg,
getUserMsg()?.role as eRole,
) || ''
} else {
content = msg.content?.content || '[新消息]'
}
const unreadMsgCount = cardItem?.unreadMsgCount || 0
this.updateCard(
......@@ -90,13 +104,14 @@ export default class ChatListCardDB extends MyAppDatabase {
const content = data.msg.content?.content || '[新消息]'
const masterId = getMasterIdFromDisplayMsg(data.msg)
const targetId = getTargetIdFromDisplayMsg(data.msg)
console.log(data.msg, "in addNewCard");
console.log(data.msg, 'in addNewCard')
this.saveCard({
masterId,
targetId: targetId,
unreadMsgCount: data.isChattingWithTargetId ? 0 : 1,
content,
inChat: false,
})
}
......@@ -109,4 +124,24 @@ export default class ChatListCardDB extends MyAppDatabase {
item.unreadMsgCount = 0
})
}
setChatStatus(masterId: string, targetId: string, isChat: boolean) {
return this.chatListCard
.filter((item) => {
return item.targetId === targetId && item.masterId === masterId
})
.modify((item) => {
item.inChat = isChat
})
}
async getChatStatus(masterId: string, targetId: string) {
const ret = await this.chatListCard
.filter((item) => {
return item.targetId === targetId && item.masterId === masterId
})
.first()
return ret?.inChat
}
}
......@@ -11,6 +11,7 @@ export interface iChatListCard {
targetId: string
unreadMsgCount: number
content: string
inChat: boolean // 会话状态,会话中?
}
export class MyAppDatabase extends Dexie {
......@@ -25,12 +26,11 @@ export class MyAppDatabase extends Dexie {
this.version(1.2).stores({
chatMessage:
'++id, content, from, uuid, state, uploadProgress, type, datetime, hideDatetime, logid, masterId, readed',
chatListCard: '++id, masterId, targetId, unreadMsgCount, content',
chatListCard: '++id, masterId, targetId, unreadMsgCount, content, inChat',
contactPerson: '++id, addr, bank, phone, user_name',
userInfo: '++id, created_at, phone, remark, user_name, uuid, addr',
})
this.chatMessage = this.table('chatMessage')
this.chatListCard = this.table('chatListCard')
this.contactPerson = this.table('contactPerson')
......
......@@ -4,10 +4,7 @@
import { MessageContent } from '@/types/chat-message'
import { reactive, Ref, ref } from '@vue/reactivity'
import {
target as __target,
getFromId
} from '@/store/appCallerStore'
import { target as __target, getFromId } from '@/store/appCallerStore'
import { ChatMessageTypes } from '@/types/chatMessageTypes'
import encodeChatMessage from '@/utils/fzm-message-protocol-chat/encodeChatMessage'
import { v4 as uuidv4 } from 'uuid'
......@@ -26,6 +23,8 @@ import {
getTargetIdFromDisplayMsg,
} from '@/utils/chatutils'
import { useRoute } from 'vue-router'
import ChatListCardDB from '@/db/ChatListCardDB'
import { CONST_END_CHAT, CONST_START_CHAT } from '@/config/chat'
/** 多媒体消息的上传进度 */
export interface UploadProgress {
......@@ -116,7 +115,6 @@ class MessageStore {
target: string
uuid?: string
}) {
const _uuid = uuid || uuidv4()
/** 聊天界面显示的消息 */
......@@ -189,17 +187,17 @@ class MessageStore {
}
// 文本类消息,不需要上传 OSS,直接发送
else {
ChatDBService.getInstance().handleEveryReceive({
msg: message,
masterId: getMasterIdFromDisplayMsg(message),
isChattingWithTargetId: isChattingWith(
getMasterIdFromDisplayMsg(message),
getTargetIdFromDisplayMsg(message),
target
target,
),
}).then(()=>{
this.send(type, content, _uuid, message, target as string)
})
this.send(type, content, _uuid, message, target as string)
}
}
......@@ -228,11 +226,8 @@ class MessageStore {
// hideDatetime: false,
// logid: record.log_id,
// }
// if (this.isMessageDuplicated(message)) return
// this.messages.unshift(message)
// // 新插入的消息和下面那条消息比较时间,小于两分钟就隐藏下面那条消息的时间
// const underMessage = this.messages[1]
// if (underMessage) {
......@@ -307,6 +302,23 @@ class MessageStore {
},
{ state: message.state },
)
if (type === ChatMessageTypes.Card) {
if (content.bank === CONST_START_CHAT.user.id)
ChatListCardDB.getInstance().setChatStatus(
getFromId() as string,
target,
true,
)
else if (content.bank === CONST_END_CHAT.user.id) {
ChatListCardDB.getInstance().setChatStatus(
getFromId() as string,
target,
false,
)
}
}
/* 存数据库...... */
})
.catch(() => {
......
export interface iNotifyMsg {
staffMsg: string
userMsg: string
}
......@@ -7,5 +7,6 @@ export enum ChatMessageTypes {
Video = 4,
File = 5,
Card = 6,
Alert = 7,
robot = 100,
}
import { CHAT_CONST_LIST } from '@/config/chat'
import { DisplayMessage } from '@/store/messagesStore'
import { eRole } from '@/types/roleType'
export default class ChatDataService {
static instance: ChatDataService
static getInstance() {
if (!ChatDataService.instance)
ChatDataService.instance = new ChatDataService()
return ChatDataService.instance
}
extractCommonMsgContentFromMsg(msg: DisplayMessage, role: eRole) {
const userId = msg.content.bank
const CONST = CHAT_CONST_LIST.find((i) => i.user.id === userId)
return eRole.user === role ? CONST?.user.value : CONST?.staff.value
}
}
......@@ -53,3 +53,52 @@ export const getDisplayNamesFromAddress = async (
return msg?.user_name || msg?.phone
})
}
export const getMsgFromAddress = async (
addressList: string[],
): Promise<any[]> => {
/* 数据库查 有结果拿 没结果网上查且存 */
const user = getUserMsg()
let foundList = [] as any[]
let notFoundList = [] as any[]
if (user?.role === eRole.user) {
const ret = await ContactPersonService.getInstance().findByList(addressList)
foundList = ret.foundList
notFoundList = ret.notFoundList
} else if (user?.role === eRole.staff) {
const ret = await UserInfoDBService.getInstance().findByList(addressList)
foundList = ret.foundList
notFoundList = ret.notFoundList
}
const fullList = (foundList as unknown) as any
if (notFoundList.length !== 0) {
if (user?.role === eRole.user) {
const ret = await UserService.getInstance().staffInfo({
addrs: notFoundList,
})
if (ret.code === 200) {
const theoseNotFoundList = ret.data.item
ContactPersonService.getInstance().save(theoseNotFoundList)
fullList.push(...theoseNotFoundList)
}
} else if (user?.role === eRole.staff) {
const ret = await UserService.getInstance().userInfo({
addrs: notFoundList,
})
if (ret.code === 200) {
const theoseNotFoundList = ret.data
UserInfoDBService.getInstance().save(theoseNotFoundList)
fullList.push(...theoseNotFoundList)
}
}
}
return addressList.map((item) => {
const msg = fullList.find((i: any) => i?.addr === item)
return msg;
})
}
......@@ -132,6 +132,7 @@ enum AlertType {
UpdateGroupMutedAlert = 5;
UpdateGroupMemberMutedAlert = 6;
UpdateGroupOwnerAlert = 7;
CommonMsg = 8;
}
message AlertUpdateGroupName {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -10,12 +10,22 @@
<div class="flex flex-col flex-grow overflow-hidden" style="flex-basis: 0px">
<ChatContentVue />
<ServiceRating :setSelectedRate="handleSelect" :selected="selected" v-if="showServiceRating" />
<ChatOption
:selected="selectedChatOption"
:setSelected="handleSelectChatOption"
:list="optionList"
/>
<ChatInputVue />
<ChatOption>
<ChatOptionItemVue
v-if="isUser"
:selected="questionSelected"
@click="handleClickQuestionOption"
value="常用问题"
/>
<ChatOptionItemVue
v-if="isUser"
:selected="serviceSelected"
:value="serviceShowValue"
@click="handleClickService"
/>
<ChatOptionItemVue :selected="false" value="电话联系" @click="handleClickCall" />
</ChatOption>
<ChatInputVue :serviceShowValue="serviceShowValue"/>
<CommonUseSentence
class="transition-all h-0"
:class="{ 'h-40': showShortSentences }"
......@@ -24,6 +34,7 @@
:list="sentenceList"
/>
</div>
<ShowCall :show="showCall" :phone="callPhone" @hidden="showCall = false" />
</div>
</template>
......@@ -41,7 +52,13 @@ import { v4 as uuidv4 } from 'uuid'
import { ChatMessageTypes } from "@/types/chatMessageTypes";
import { useRoute } from "vue-router";
import { queryFaqAnswer, queryFaqList } from "@/service/FaqService";
import { getDisplayNamesFromAddress } from "@/utils/displayName";
import { getDisplayNamesFromAddress, getMsgFromAddress } from "@/utils/displayName";
import { MessageContent } from "@/types/chat-message";
import { CONST_END_CHAT, CONST_START_CHAT } from "@/config/chat";
import ChatOptionItemVue from "@/components/ChatOptions/ChatOptionItem.vue";
import ShowCall from "@/components/showCall/index.vue"
import { getUserMsg } from "@/utils/userMsg";
import { eRole } from "@/types/roleType";
export default defineComponent({
......@@ -51,14 +68,69 @@ export default defineComponent({
NavBar,
ServiceRating: defineAsyncComponent(() => import(/* webpackChunkName: 'serviceRating' */ "@/components/ServiceRating/index.vue")),
ChatOption: defineAsyncComponent(() => import(/* webpackChunkName: 'ChatOption' */"@/components/ChatOptions/index.vue")),
CommonUseSentence: defineAsyncComponent(() => import(/* webpackChunkName: 'CommonUseSentence' */"@/components/CommonUseSentence/index.vue"))
CommonUseSentence: defineAsyncComponent(() => import(/* webpackChunkName: 'CommonUseSentence' */"@/components/CommonUseSentence/index.vue")),
ChatOptionItemVue,
ShowCall
},
setup() {
const questionSelected = ref(false)
const handleClickQuestionOption = () => {
questionSelected.value = !questionSelected.value
}
watch(questionSelected, () => {
setShowSentences(questionSelected.value)
sentensesLoading.value = true;
questionSelected.value && queryFaqList().then(ret => {
if (ret.code === 200) {
sentenceList.value = ret.data.question;
}
sentensesLoading.value = false;
})
})
const serviceSelected = ref(false);
const serviceShowValue = ref('人工服务')
const handleClickService = () => {
const sendChatMessage = (payload: {
type: ChatMessageTypes;
content: MessageContent;
}) => {
messageStore.sendMessage({ type: payload.type, content: payload.content, target: route.query.targetId as string });
};
if (serviceShowValue.value === '人工服务') {
sendChatMessage({
type: ChatMessageTypes.Card,
content: {
bank: CONST_START_CHAT.user.id,
name: CONST_START_CHAT.staff.id,
account: '',
} as MessageContent
})
serviceShowValue.value = '结束服务'
} else if (serviceShowValue.value === '结束服务') {
serviceShowValue.value = '人工服务'
sendChatMessage({
type: ChatMessageTypes.Card,
content: {
bank: CONST_END_CHAT.user.id,
name: CONST_END_CHAT.staff.id,
account: '',
} as MessageContent
})
// showServiceRating.value = true;
}
}
onMounted(async () => {
const inChat = await ChatListCardDB.getInstance().getChatStatus(getFromId() as string, target)
serviceShowValue.value = inChat ? '结束服务' : '人工服务'
})
const initError = ref(false);
const showServiceRating = ref(false);
const selected = ref("");
const selectedChatOption = ref(NaN);
const showShortSentences = ref(false);
const route = useRoute()
......@@ -67,38 +139,16 @@ export default defineComponent({
const sentenceList = ref<any[]>([])
const sentensesLoading = ref(false)
const optionList = [
{ name: '常用问题', id: 1 },
{ name: '人工服务', id: 2 },
{ name: '电话咨询', id: 3 },
];
const setShowSentences = (show: boolean) =>
(showShortSentences.value = show);
const handleSelect = (select: string) => (selected.value = select);
const handleSelectChatOption = (select: number) => {
selectedChatOption.value = select
};
watch(selectedChatOption, () => {
if (selectedChatOption.value === 1) {
setShowSentences(true)
sentensesLoading.value = true;
queryFaqList().then(ret => {
if (ret.code === 200) {
sentenceList.value = ret.data.question;
}
sentensesLoading.value = false;
})
setShowSentences(true)
} else {
setShowSentences(false)
}
if (selectedChatOption.value === 3) {
}
})
const handleClickCall = () => {
showCall.value = true;
}
const useSentence = (content: string) => {
/* 问 */
......@@ -153,6 +203,15 @@ export default defineComponent({
onMounted(async () => {
const list = await getDisplayNamesFromAddress([target])
title.value = list[0];
const ret = await getMsgFromAddress([target])
callPhone.value = ret[0].phone;
})
const showCall = ref(false)
const callPhone = ref('')
const isUser = computed(() => {
return getUserMsg()?.role === eRole.user
})
......@@ -162,13 +221,19 @@ export default defineComponent({
handleSelect,
showServiceRating,
showShortSentences,
selectedChatOption,
handleSelectChatOption,
useSentence,
sentenceList,
optionList,
sentensesLoading,
title
title,
questionSelected,
handleClickQuestionOption,
serviceSelected,
serviceShowValue,
handleClickService,
handleClickCall,
showCall,
callPhone,
isUser
};
},
});
......
<template>
<div>
<div v-if="!hideDatetime" class="text-xs text-gray-400 text-center pb-2 pt-4">{{ time }}</div>
<!-- 卡片消息 -->
<ChatContentMessageCardVue
v-if="type === 6"
:from-myself="fromMyself"
:content="content"
></ChatContentMessageCardVue>
<div
class="flex items-start flex-nowrap w-screen py-1.5"
:class="{ 'flex-row-reverse': fromMyself }"
>
<!-- 头像 -->
<q-avatar class="mx-4 min-w-chat-msg-avatar w-chat-msg-avatar h-chat-msg-avatar !rounded-md">
<q-avatar class="mx-4 min-w-chat-msg-avatar w-chat-msg-avatar h-chat-msg-avatar !rounded-md" v-if="type !== 6">
<img :src="default_avatar_url" />
</q-avatar>
......@@ -53,13 +59,7 @@
:uploadProgress="uploadProgress"
/>-->
<!-- 卡片消息 -->
<ChatContentMessageCardVue
v-else-if="type === 6"
:from-myself="fromMyself"
:content="content"
/>
<!-- 消息状态 -->
<div class="w-10 self-stretch flex flex-row justify-center items-center">
<!-- 发送失败 -->
......
<template>
<div :class="fromMyself ? 'bg-secondary' : 'bg-white'" class="w-full h-24 rounded-md font-medium">
<div class="py-3 px-3 flex items-center enable-touch">
<q-icon :name="'img:' + iconUrl" size="28px" class="mr-1" /> {{ content.bank }}
</div>
<div class="pb-3 px-2 flex flex-nowrap justify-between text-base break-all enable-touch">
<div>{{ content.name }}</div>
<div>{{ content.account }}</div>
</div>
<div class="w-full rounded-md font-medium flex items-center">
<div class="flex-grow border-b mr-1 ml-2"></div>
<div class="text-center" v-if="isUser">{{ sentence?.user.value }}</div>
<div class="text-center" v-else>{{ sentence?.staff.value }}</div>
<div class="flex-grow border-b ml-1 mr-2"></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { defineComponent, PropType } from 'vue'
import iconUrl from '@/assets/message_bank_card.png'
import { getUserMsg } from '@/utils/userMsg'
import { eRole } from '@/types/roleType'
import { CHAT_CONST_LIST } from '@/config/chat'
export default defineComponent({
props: { fromMyself: Boolean, content: Object },
setup() {
return { iconUrl }
export default defineComponent({
props: {
fromMyself: Boolean,
content: {
required: true,
type: Object as PropType<{ bank: string, name: string, account: string }>,
}
},
setup(props) {
const isUser = getUserMsg()?.role === eRole.user
const sentence = CHAT_CONST_LIST.find(i => i.user.id === props?.content.bank)
return { iconUrl, isUser, sentence, CHAT_CONST_LIST }
},
})
</script>
......@@ -6,21 +6,19 @@
@click="inputType === 1 ? (inputType = 2) : (inputType = 1)"
class="w-7 h-7 mx-2.5 text-center select-none focus:outline-none"
>
<i v-if="inputType === 1" class="iconfont text-primary text-xl"
>&#xe604;</i
>
<i v-if="inputType === 1" class="iconfont text-primary text-xl">&#xe604;</i>
<i v-else class="iconfont text-primary text-xl">&#xe60d;</i>
</button>
<ChatInputTextVue
v-if="inputType === 1"
@send="sendChatMessage"
@send="handleSend"
@click="showMenu = false"
class="pl-5"
/>
<!-- 没有输入文字,显示 `加号` 按钮 -->
<!-- v-if="!inputText" -->
<!-- v-if="!inputText" -->
<button
v-if="false"
@click="showMenu ? (showMenu = false) : (showMenu = true)"
......@@ -31,30 +29,14 @@
<!-- 有输入文字,显示 `发送` 按钮 -->
<button
v-else
@click="inputText.trim().length !== 0 &&sendChatMessage({ type: 1, content: { content: inputText } })"
class="
mx-2.5
px-4
py-1.5
flex
items-center
rounded-md
text-center
select-none
focus:outline-none
text-app-white
"
@click="inputText.trim().length !== 0 && handleSend()"
class="mx-2.5 px-4 py-1.5 flex items-center rounded-md text-center select-none focus:outline-none text-app-white"
style="background: rgb(7, 193, 99)"
>
发送
</button>
>发送</button>
</div>
<!-- input menu -->
<div
v-show="showMenu"
class="min-h-input-menu flex items-center px-8 text-sm text-subtle"
>
<div v-show="showMenu" class="min-h-input-menu flex items-center px-8 text-sm text-subtle">
<ChatInputAlbumVue />
<!-- <ChatInputCameraVue /> -->
</div>
......@@ -69,13 +51,16 @@ import { MessageContent } from "@/types/chat-message";
import ChatInputTextVue from "./ChatInputText.vue";
import ChatInputAlbumVue from "./ChatInputAlbum.vue";
import ChatInputCameraVue from "./ChatInputCamera.vue";
import { target } from "@/store/appCallerStore";
// import { from } from "@/store/appCallerStore";
import { getFromId, target } from "@/store/appCallerStore";
import { textInputStore } from "@/store/textInputStore";
import { v4 as uuidv4 } from 'uuid'
import Icon from "@/components/common/Icon.vue";
import { useRoute } from "vue-router";
export default defineComponent({
props: {
serviceShowValue: String,
},
components: {
ChatInputTextVue,
ChatInputAlbumVue,
......@@ -83,7 +68,7 @@ export default defineComponent({
Icon,
},
setup() {
setup(props) {
const route = useRoute();
const enum InputType {
text = 1,
......@@ -104,7 +89,7 @@ export default defineComponent({
type: ChatMessageTypes;
content: MessageContent;
}) => {
messageStore.sendMessage({type: payload.type, content: payload.content, target: route.query.targetId as string});
messageStore.sendMessage({ type: payload.type, content: payload.content, target: route.query.targetId as string });
textInputStore.clearTextMessage();
};
......@@ -126,11 +111,33 @@ export default defineComponent({
// }
// })
const handleSend = () => {
console.log('handle send');
if (props.serviceShowValue === "人工服务") {
messageStore.displayNewMessage({
content: {
content: inputText.value
},
from: getFromId() as string,
target: target,
uuid: uuidv4(),
state: 'success',
datetime: new Date().getTime(),
type: ChatMessageTypes.robot,
})
textInputStore.clearTextMessage();
} else {
sendChatMessage({ type: 1, content: { content: inputText.value } })
}
}
return {
inputType,
inputText,
showMenu,
sendChatMessage,
handleSend
// showReceiptInput,
};
},
......
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 to comment