123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- <template>
- <div class="voice-assistant">
- <!-- 聊天框 -->
- <el-scrollbar class="h100" noresize ref="scrollbarRef" max-height="400px" v-if="messageList.length" @click="stopScroll">
- <div class="chat-box" ref="chatBoxRef">
- <div v-for="(item, index) in messageList" :key="index" class="chat-item" :class="item.body?.content?.callSentenceInfo?.role">
- <div v-if="item.body?.content?.callSentenceInfo?.role === 'user'" class="user">
- <img v-lazy="getImageUrl('order/user.png')" alt="" class="user-avatar" src="" />
- <div class="user-name">{{ item.body?.content?.callerNumber }}</div>
- <div class="user-content">{{ item.body?.content?.callSentenceInfo.text }}</div>
- <div class="user-date">{{ formatDate(item.timestamps, 'YYYY-mm-dd HH:MM:SS') }}</div>
- </div>
- <div v-else class="agent">
- <img v-lazy="getImageUrl('order/service.png')" alt="" class="agent-avatar" src="" />
- <div class="agent-name">{{ item.body?.content?.calledNumber }}</div>
- <div class="agent-content">{{ item.body?.content?.callSentenceInfo.text }}</div>
- <div class="agent-date">{{ formatDate(item.timestamps, 'YYYY-mm-dd HH:MM:SS') }}</div>
- </div>
- </div>
- <el-text class="end-call" tag="p" v-if="talkEnd">-- 通话结束 --</el-text>
- </div>
- </el-scrollbar>
- <Empty v-else />
- <el-button @click.stop="keepScroll" class="keep-scroll" title="回到底部" circle v-if="!isScrollBottom && !talkEnd">
- <SvgIcon name="ele-ArrowDown" size="18px" />
- </el-button>
- <!-- 识别内容 通话结束才展示-->
- <div class="recognize-box" v-if="talkEnd && messageList.length">
- <transition name="el-zoom-in-bottom">
- <div v-show="!searchCol" class="transition-box">
- <el-button link @click="closeSearch" class="transition-box-close">
- <SvgIcon name="ele-Close" class="ml3" size="18px" />
- </el-button>
- <div class="transition-box-title">
- <el-text tag="b" size="large">通话小结</el-text>
- </div>
- <el-scrollbar class="h100 transition-box-content" noresize ref="scrollbarRef" max-height="180px">
- <div class="call-item">
- <el-text tag="b" class="call-item-label">客户姓名:</el-text>
- <p class="call-item-value">{{ recognizeList?.name }}</p>
- </div>
- <!-- <div class="call-item">
- <el-text tag="b" class="call-item-label">年龄:</el-text>
- <p class="call-item-value">{{ recognizeList?.age }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">性别:</el-text>
- <p class="call-item-value">{{ recognizeList?.sex }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">身份证号:</el-text>
- <p class="call-item-value">{{ recognizeList?.id_card }}</p>
- </div>-->
- <div class="call-item">
- <el-text tag="b" class="call-item-label">地址:</el-text>
- <p class="call-item-value">{{ recognizeList?.address }}</p>
- </div>
- <!-- <div class="call-item">
- <el-text tag="b" class="call-item-label">公司名称:</el-text>
- <p class="call-item-value">{{ recognizeList?.company_name }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">通话小结:</el-text>
- <p class="call-item-value">{{ recognizeList?.call_detail_content }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">受理类型:</el-text>
- <p class="call-item-value">{{ recognizeList?.acceptance_type }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">推送分类:</el-text>
- <p class="call-item-value">{{ recognizeList?.push_classification }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">热点分类:</el-text>
- <p class="call-item-value">{{ recognizeList?.hotspot_classification }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">事件目的:</el-text>
- <p class="call-item-value">{{ recognizeList?.event_purpose }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">工单标题:</el-text>
- <p class="call-item-value">{{ recognizeList?.record_title }}</p>
- </div>
- <div class="call-item">
- <el-text tag="b" class="call-item-label">诉求详情:</el-text>
- <p class="call-item-value">{{ recognizeList?.appeal_detatls }}</p>
- </div>-->
- </el-scrollbar>
- <div class="flex-end mt20">
- <el-button @click="closeSearch" class="default-button"> 取消</el-button>
- <el-button type="primary" @click="fillSingle">一键填单</el-button>
- </div>
- </div>
- </transition>
- <el-button type="primary" @click="closeSearch" v-show="searchCol">
- 通话小结
- <SvgIcon name="ele-ArrowUp" class="ml3" size="18px" />
- </el-button>
- </div>
- </div>
- </template>
- <script setup lang="ts" name="orderAcceptVoiceAssistant">
- import { nextTick, onActivated, onDeactivated, onMounted, ref, watch } from 'vue';
- import { ElMessageBox } from 'element-plus';
- import { getImageUrl } from '@/utils/tools';
- import { useRoute } from 'vue-router';
- import { formatDate } from '@/utils/formatTime';
- const emit = defineEmits(['orderOverwrite']);
- import mittBus from '@/utils/mitt';
- import { voiceAssistantContent } from "@/api/todo/voiceAssistant";
- // 消息列表
- const messageList = ref<any>([
- /*{
- body: {
- content: {
- callSentenceInfo: {
- text: '你好,我是小智,有什么可以帮您的吗?',
- role: 'agent',
- },
- calledNumber: '1009',
- callerNumber: '19136073037',
- },
- },
- timestamps: new Date().getTime(),
- },
- {
- body: {
- content: {
- callSentenceInfo: {
- text: '你好,我是小智,有什么可以帮您的吗?',
- role: 'user',
- },
- },
- calledNumber: '1009',
- callerNumber: '19136073037',
- },
- timestamps: new Date().getTime(),
- },*/
- ]); // 消息列表
- const recognizeList = ref<EmptyObjectType>({}); // 识别信息
- const route = useRoute();
- const talkEnd = ref(false); // 通话结束
- const wsReceive = (message: any) => {
- try {
- const data = JSON.parse(message.data);
- if (data.body.bisType === 3) {
- // 转写消息
- if (data.body.content.action === 0) {
- // 通话开始
- console.log('通话转写开始了');
- talkEnd.value = false;
- }
- if (data.body.content.action === 1) {
- // 通话中
- if (messageList.value.length) {
- const item = messageList.value.find((item: any) => item.body.content.callSentenceInfo.index === data.body.content.callSentenceInfo.index);
- if (item) {
- item.body.content.callSentenceInfo.text = data.body.content.callSentenceInfo.text;
- } else {
- messageList.value.push(data);
- }
- } else {
- messageList.value.push(data);
- }
- scrollToBottom();
- console.log('通话消息转写内容:', messageList.value);
- }
- if (data.body.content.action === 4) {
- // 通话结束
- setTimeout(() => {
- //通话结束
- talkEnd.value = true;
- scrollToBottom(); //滚动到底部
- stopScroll(); //停止滚动
- }, 1000);
- console.log('通话转写结束了');
- }
- }
- } catch (error) {
- console.log('坐席辅助收到消息', message);
- }
- };
- const scrollbarRef = ref<RefType>();
- const chatBoxRef = ref<RefType>();
- const searchCol = ref(true); // 展开/收起
- // 展开/收起 通话小结
- const closeSearch = () => {
- if (!searchCol.value) {
- // 展开
- // 获取识别内容
- getRecognize();
- }
- searchCol.value = !searchCol.value;
- };
- // 获取识别内容
- const getRecognize = async () => {
- try {
- const callId = route.params.callId;
- const {result} = await voiceAssistantContent(typeof callId === "string" ? callId :'');
- recognizeList.value = result;
- }catch (error) {
- console.log('获取识别内容失败', error);
- }
- };
- // 停止滚动
- const stopScroll = () => {
- isScrollBottom.value = false;
- };
- // 滚动到底部
- const isScrollBottom = ref(true); // 是否需要滚动到底部
- const isAutoScroll = ref(true); // 是否自动滚动
- const scrollToBottom = async () => {
- if (!isScrollBottom.value) return;
- await nextTick(); // 等待 DOM 更新
- const max = chatBoxRef.value?.clientHeight;
- scrollbarRef.value?.setScrollTop(max);
- isAutoScroll.value = true;
- };
- watch(messageList.value, (val) => {
- scrollToBottom();
- });
- // 继续滚动
- const keepScroll = () => {
- isScrollBottom.value = true;
- scrollToBottom();
- };
- // 一键填单
- const fillSingle = () => {
- ElMessageBox.confirm(`确定要一键填单吗?填单后会覆盖之前填写的内容,请谨慎操作`, '提示', {
- confirmButtonText: '确认',
- cancelButtonText: '取消',
- type: 'warning',
- draggable: true,
- cancelButtonClass: 'default-button',
- autofocus: false,
- })
- .then(() => {
- emit('orderOverwrite', recognizeList.value);
- searchCol.value = !searchCol.value;
- })
- .catch(() => {});
- };
- watch(
- () => scrollbarRef.value?.wrapRef,
- () => {
- scrollbarRef.value?.wrapRef.addEventListener('mousewheel', () => {
- stopScroll();
- });
- }
- );
- // 订阅消息
- const subscribe = () => {
- // 接受消息
- mittBus.on('wsReceive', (message: any) => {
- const data = JSON.parse(message.data);
- if (data.body.content.callId === route.params.callId) {
- // 判断是否是当前通话
- wsReceive(message);
- }
- });
- };
- onMounted(() => {
- // 进入页面订阅
- subscribe();
- });
- onActivated(() => {
- // 缓存进入重新订阅
- subscribe();
- });
- onDeactivated(() => {
- // 缓存离开取消订阅
- mittBus.off('wsReceive');
- });
- </script>
- <style scoped lang="scss">
- .voice-assistant {
- width: 100%;
- height: 100%;
- min-height: 400px;
- position: relative;
- .keep-scroll {
- position: absolute;
- right: 10px;
- bottom: 30px;
- z-index: 100;
- }
- .chat-box {
- width: 100%;
- padding-bottom: 50px;
- .agent {
- justify-content: flex-end;
- }
- .chat-item {
- word-break: break-all;
- position: relative;
- color: var(--el-color-white);
- margin: 30px 10px 10px 10px;
- display: flex;
- .user {
- &-content {
- border-radius: var(--el-border-radius-base);
- background-color: var(--el-color-primary);
- padding: 10px;
- position: relative;
- max-width: 400px;
- box-shadow: 4px 0 10px 0 rgba(0, 0, 0, 0.3);
- margin-left: 60px;
- &::after {
- position: absolute;
- content: '';
- top: calc(50% - 10px);
- left: -10px;
- width: 0;
- height: 0;
- border-top: 10px solid transparent;
- border-right: 10px solid var(--el-color-primary);
- border-bottom: 10px solid transparent;
- }
- }
- &-name {
- font-size: var(--el-font-size-base);
- color: var(--el-text-color-regular);
- margin-bottom: 5px;
- margin-left: 65px;
- }
- &-date {
- font-size: var(--el-font-size-extra-small);
- color: var(--el-text-color-placeholder);
- position: absolute;
- bottom: -20px;
- left: 65px;
- }
- &-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- position: absolute;
- left: 0;
- top: calc(50% - 15px);
- }
- }
- .agent {
- &-content {
- border-radius: var(--el-border-radius-base);
- background-color: var(--el-color-success);
- padding: 10px;
- position: relative;
- justify-content: flex-end;
- max-width: 400px;
- box-shadow: -4px 0 10px 0 rgba(0, 0, 0, 0.3);
- margin-right: 60px;
- &::after {
- position: absolute;
- content: '';
- top: calc(50% - 10px);
- right: -10px;
- width: 0;
- height: 0;
- border-top: 10px solid transparent;
- border-left: 10px solid var(--el-color-success);
- border-bottom: 10px solid transparent;
- }
- }
- &-name {
- font-size: var(--el-font-size-base);
- color: var(--el-text-color-regular);
- margin-bottom: 5px;
- text-align: right;
- margin-right: 65px;
- }
- &-date {
- font-size: var(--el-font-size-extra-small);
- color: var(--el-text-color-placeholder);
- position: absolute;
- bottom: -20px;
- right: 65px;
- }
- &-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- position: absolute;
- right: 0;
- top: calc(50% - 15px);
- }
- }
- }
- .end-call {
- width: 100%;
- text-align: center;
- margin-top: 20px;
- }
- }
- .recognize-box {
- position: absolute;
- bottom: 0;
- width: 100%;
- background-color: var(--el-color-white);
- .call-item {
- margin-bottom: 10px;
- display: flex;
- align-content: center;
- .call-item-label {
- width: 100px;
- text-align: right;
- margin-right: 10px;
- }
- .call-item-value {
- flex: 1;
- }
- }
- .transition-box {
- position: absolute;
- bottom: 0;
- width: 100%;
- height: 300px;
- background-color: var(--el-color-primary-light-9);
- border-radius: var(--el-border-radius-base);
- padding: 10px;
- &-close {
- position: absolute;
- right: 10px;
- top: 10px;
- }
- &-title {
- border-bottom: var(--el-border);
- border-color: var(--el-color-info);
- padding-bottom: 10px;
- }
- .transition-box-content {
- border: var(--el-border);
- border-radius: var(--el-border-radius-base);
- border-color: var(--el-color-info);
- margin-top: 10px;
- height: 180px !important;
- padding: 10px;
- .transition-box-content-item {
- display: flex;
- align-items: center;
- margin-bottom: 10px;
- .transition-box-content-item-title {
- width: 75px;
- margin-right: 3px;
- }
- .transition-box-content-item-content {
- flex: 1;
- }
- }
- }
- }
- }
- }
- </style>
|