소스 검색

feat: 近期新增需求开发

sunxiao 2 달 전
부모
커밋
406d018331

+ 11 - 0
src/api/voice/call.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+export const callApi = {
+  /**
+   * 停水公告 - 查询
+   */
+  getImportFailList: params => request({
+    url: `/excel/getImportFailList`,
+    params
+  })
+}

+ 34 - 0
src/api/voice/workbench.js

@@ -56,4 +56,38 @@ export const workbenchApi = {
     url: `/front/getSeatsByUserId`,
     params
   }),
+
+  /**
+  * 报警记录 - list
+  */
+  getWarningRecord: params => request({
+    url: `/business/warningRecord/list`,
+    params
+  }),
+
+  /**
+  * 报警记录 - 标记是否已读
+  */
+  putWarningRecord: data => request({
+    url: `/business/warningRecord`,
+    method: 'put',
+    data
+  }),
+
+  /**
+  * 报警记录 - 获取最新的报警条数
+  */
+  getNewestWarningCount: () => request({
+    url: `/business/warningRecord/getNewestWarningCount`,
+  }),
+
+  /**
+  * 报警记录 - 获取最新的报警条数
+  */
+  postAddCloseRecord: () => request({
+    url: `/business/warningRecord/addCloseRecord`,
+    method: 'post',
+  })
+
+  
 }

BIN
src/assets/images/notice/notice.png


+ 11 - 0
src/assets/images/svgs/icon-notive-active.svg

@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
+<g clip-path="url(#clip0_5082_466)">
+<path d="M4.16675 15.8327V7.49935C4.16675 4.27768 6.77841 1.66602 10.0001 1.66602C13.2217 1.66602 15.8334 4.27768 15.8334 7.49935V15.8327M1.66675 15.8327H18.3334" stroke="#4E5969" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.0001 18.334C11.1507 18.334 12.0834 17.4012 12.0834 16.2507V15.834H7.91675V16.2507C7.91675 17.4012 8.8495 18.334 10.0001 18.334Z" stroke="#4E5969" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_5082_466">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 4 - 4
src/components/CallView/index.vue

@@ -108,7 +108,7 @@ const onVoiceParsed = ({ parsedVoiceContent, id }) => {
   <div class="details-inner">
     <el-descriptions title="">
       <el-descriptions-item label="呼叫类型" label-class-name="custom-label" class-name="custom-colums"
-        v-if="callDetails.category != 1">
+        v-show="callDetails.category != 1">
         <span class="text-[#FF3636]">{{ callDetails.typeText }}</span>
       </el-descriptions-item>
       <el-descriptions-item label="客服" label-class-name="custom-label" class-name="custom-colums">
@@ -134,14 +134,14 @@ const onVoiceParsed = ({ parsedVoiceContent, id }) => {
         {{ callDetails.status == 0 ? '未接听' : '已接通' }}
       </el-descriptions-item>
       <el-descriptions-item label="业务类型" label-class-name="custom-label" class-name="custom-colums"
-        v-if="callDetails.category != 1">{{ callDetails.bussinessType }}</el-descriptions-item>
+        v-show="callDetails.category != 1">{{ callDetails.bussinessType }}</el-descriptions-item>
       <el-descriptions-item label="电话号码" label-class-name="custom-label" class-name="custom-colums">
         <div class="inline-block">
           <div class="flex items-center space-x-[4px]">
             <span>{{ callDetails.phone }}</span>
             <el-popconfirm width="250" icon-color="#626AEF" title="请确认,是否呼叫该电话号码?" @confirm="onConfirm">
               <template #reference>
-                <img src="@/assets/images/workbench/icon-call-square.svg" alt="" class="cursor-pointer" v-if="voiceStore.isAuthPane">
+                <img src="@/assets/images/workbench/icon-call-square.svg" alt="" class="cursor-pointer" v-show="voiceStore.isAuthPane">
               </template>
               <template #actions="{ confirm, cancel }">
                 <el-button size="small" @click="cancel">否</el-button>
@@ -161,7 +161,7 @@ const onVoiceParsed = ({ parsedVoiceContent, id }) => {
         <span class="text-[#165DFF] text-[14px] cursor-pointer" @click="handleEdit">编辑</span>
       </div>
     </custom-row-item>
-    <custom-row-item label="通话录音" v-if="callDetails.url">
+    <custom-row-item label="通话录音" v-show="callDetails.url">
       <VoiceToText @on-parsed="onVoiceParsed" :content="callDetails.parsedVoiceContent" :id="callDetails.id" v-model="modelValue">
         <AudioPlayer :audioUrl="callDetails.url" ref="audioPlayerRef"></AudioPlayer>
       </VoiceToText>

+ 223 - 0
src/layout/components/HeaderGroup/NoticePopover.vue

@@ -0,0 +1,223 @@
+<script setup>
+import { storeToRefs } from 'pinia';
+import useNoticeStore from "@/store/modules/notice";
+import useVoiceStore from "@/store/modules/voice";
+import { workbenchApi } from "@/api/voice/workbench";
+
+const noticeStore = useNoticeStore();
+const voiceStore = useVoiceStore();
+
+const { noticePopupStatus, warningNumber } = storeToRefs(noticeStore);
+const { callAnswered } = storeToRefs(voiceStore);
+const timestamp = ref(new Date().getTime());
+
+const tabData = ref([
+  {
+    label: '全部',
+    value: ''
+  },
+  {
+    label: '未读',
+    value: 0
+  },
+  {
+    label: '已读',
+    value: 1
+  }
+]);
+
+const queryParams = ref({
+  pageNum: 0,
+  pageSize: 2
+});
+
+const total = ref(0);
+const tabActive = ref('');
+const loading = ref(false);
+const warningRecordList = ref([]);
+
+const disabled = computed(() => loading.value || (total.value && total.value == warningRecordList.value.length));
+
+const resetParams = () => {
+  queryParams.value.pageNum = 0;
+  warningRecordList.value = [];
+  total.value = 0;
+  timestamp.value = new Date().getTime();
+}
+
+const handleTabItem = ({ value: read }) => {
+  tabActive.value = read;
+  resetParams();
+  loadMoreData();
+}
+
+const handleNoticeItem = (item) => {
+  const { read, id } = item;
+  if (read == 1) return;
+  workbenchApi.putWarningRecord({ read: 1, id }).then(() => {
+    item.read = 1;
+  })
+}
+
+const loadMoreData = () => {
+  queryParams.value.pageNum += 1;
+  loading.value = true;
+  workbenchApi.getWarningRecord({ ...queryParams.value, read: tabActive.value }).then(({ rows, total: t }) => {
+    warningRecordList.value = [...warningRecordList.value, ...rows];
+    total.value = t;
+    loading.value = false;
+  })
+}
+
+const onBeforeEnter = () => {
+  tabActive.value = 0;
+  resetParams();
+  loadMoreData();
+}
+</script>
+
+<template>
+  <div class="notice-popover-wrapper">
+    <el-popover
+      trigger="click"
+      placement="bottom"
+      v-model:visible="noticePopupStatus"
+      :disabled="callAnswered"
+      :width="342" 
+      :show-arrow="false"
+      :popper-style="{
+        height: '346px',
+        borderRadius: '16px',
+        padding: '0px 0px 0px 0px'
+      }"
+      @before-enter="onBeforeEnter"
+    >
+      <template #reference>
+        <div class="icon"><div class="circle" v-show="warningNumber !=0 "></div></div>
+      </template>
+      <div class="notice-content">
+        <ul class="tab-nav space-x-[30px]">
+          <li 
+            v-for="item in tabData" 
+            :class="['tab-item', { active: tabActive === item.value }]" 
+            :key="item.label"
+            @click="handleTabItem(item)"
+          >{{ item.label }}</li>
+        </ul>
+        <div
+          class="notice-list space-y-[12px] h-[282px]"
+          v-infinite-scroll="loadMoreData"
+          :infinite-scroll-distance="250"
+          :infinite-scroll-disabled="disabled"
+          :key="timestamp"
+        >
+          <div :class="['notice-item', { 'notice-active': item.read == 1 }]" v-for="item, index in warningRecordList" :key="item.id" @click="handleNoticeItem(item)">
+            <h4 class="title" v-show="item.type == 0">有<span class="text-[#FF3636] font-bold">5</span>个连续未接听来电</h4>
+            <h4 class="title" v-show="item.type == 1"><span class="text-[#FF3636] font-bold">10</span>分钟空岗报警</h4>
+            <h4 class="title" v-show="item.type == 2">有连续<span class="text-[#FF3636] font-bold">3</span>个电话5s内秒挂</h4>
+            <p class="content">{{ item.desc }}</p>
+            <p class="time">{{ item.createTime }}</p>
+          </div>
+        </div>
+      </div>
+    </el-popover>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.notice-popover-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 36px;
+  height: 36px;
+  border-radius: 8px;
+  background: #fff;
+
+  .icon {
+    position: relative;
+    width: 20px;
+    height: 20px;
+    background: url('@/assets/images/svgs/icon-notive-active.svg') no-repeat;
+    font-weight: bold;
+    cursor: pointer;
+
+    .circle {
+      position: absolute;
+      display: block;
+      top: 0;
+      right: 0;
+      width: 8px;
+      height: 8px;
+      border: 2px solid #fff;
+      border-radius: 100%;
+      background: #FF3636;
+    }
+  }
+}
+
+.notice-content {
+  .tab-nav {
+    display: flex;
+    align-items: center;
+    padding: 12px 24px;
+    border-bottom: 1px solid #EBEBEB;
+    font-size: 14px;
+    line-height: 20px;
+    color: #4E5969;
+    
+    .tab-item {
+      cursor: pointer;
+    }
+
+    .active {
+      position: relative;
+      color: #165DFF;
+      font-weight: bold;
+
+      &::after {
+        position: absolute;
+        bottom: -13px;
+        content: " ";
+        display: block;
+        width: 28px;
+        height: 3px;
+        border-radius: 10px;
+        background: #165DFF;
+      }
+    }
+  }
+
+  .notice-list {
+    padding: 18px 24px 0 24px;
+    overflow-y: scroll;
+    .notice-item {
+      cursor: pointer;
+      .title {
+        color: #1D2129;
+        font-size: 14px;
+        font-weight: bold;
+        line-height: 22px; 
+      }
+      .content {
+        color: #4E5969;
+        font-size: 12px;
+        line-height: 18px;
+      }
+      .time {
+        color: #86909C;
+        font-size: 10px;
+        line-height: 16px;
+      }
+    }
+    .notice-active {
+      .title, .content, .time {
+        color: #999;
+        span {
+          color: #999;
+        }
+      }
+    }
+  }
+}
+</style>

+ 5 - 1
src/layout/components/HeaderGroup/index.vue

@@ -2,10 +2,11 @@
 import { storeToRefs } from 'pinia';
 import useVoiceStore from "@/store/modules/voice";
 import { ElMessageBox } from 'element-plus';
-import TelCallBoard from './TelCallBoard.vue'
 import useUserStore from '@/store/modules/user';
 import ResetPwdDialog from '@/components/ResetPwdDialog'
 import { workbenchApi } from '@/api/voice/workbench';
+import TelCallBoard from './TelCallBoard.vue'
+import NoticePopover from './NoticePopover.vue'
 
 const voiceStore = useVoiceStore();
 const { proxy } = getCurrentInstance();
@@ -170,6 +171,9 @@ onMounted( () => {
       </el-dropdown>
       
       <TelCallBoard v-if="voiceStore.isAuthPane"/>
+
+      <NoticePopover></NoticePopover>
+
       <div class="avatar-wrapper flex items-center space-x-[4px]">
         <div class="avatar-img">
           <img :src="userStore.avatar" alt="" class="img">

+ 86 - 0
src/layout/components/NoticeBar/index.vue

@@ -0,0 +1,86 @@
+<script setup>
+import { storeToRefs } from 'pinia';
+import useNoticeStore from "@/store/modules/notice";
+import { workbenchApi } from "@/api/voice/workbench";
+import { onMounted, onUnmounted } from 'vue';
+
+let timer = null;
+
+const noticeStore = useNoticeStore();
+
+const { noticeBarStatus, noticePopupStatus, warningNumber } = storeToRefs(noticeStore);
+
+const warningCount = ref(0);
+
+const onShowNoticePopup = () => {
+  noticePopupStatus.value = true;
+  onCloseNoticebar();
+}
+
+const onCloseNoticebar = () => {
+  workbenchApi.postAddCloseRecord().then(() => {
+    noticeBarStatus.value = false;
+  });
+}
+
+const initWaringCount = () => {
+  workbenchApi.getNewestWarningCount().then(({ data }) => {
+    warningCount.value = data;
+    if ( data != 0 ) {
+      warningNumber.value = data;
+      noticeBarStatus.value = true
+    }
+  });
+}
+
+onMounted(() => {
+  initWaringCount();
+  timer = setInterval(() => initWaringCount, 5 * 60 * 1000);
+});
+
+onUnmounted(() => {
+  clearInterval(timer);
+});
+
+</script>
+
+<template>
+  <div class="notice-bar space-x-[10px]" :style="{right: noticeBarStatus ? '24px' : '-100%'} ">
+    <div class="left space-x-[2px]">
+      <img src="@/assets/images/notice/notice.png" alt="" class="img">
+      <p>您有<span class="text-[#FF3636]">{{ warningCount }}</span>条报警未读</p>
+    </div>
+    <div class="right space-x-[6px]">
+      <p class="text-[#165DFF] cursor-pointer" @click="onShowNoticePopup">查看详情</p>
+      <el-icon class="cursor-pointer" @click="onCloseNoticebar"><Close /></el-icon>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.notice-bar {
+  position: absolute;
+  right: 24px;
+  bottom: 24px;
+  display: flex;
+  align-items: center;
+  z-index: 10;
+  height: 36px;
+  padding: 0 15px;
+  border-radius: 8px;
+  background: #FFE1DF;
+  color: #1D2129;
+  font-size: 14px;
+  transition: right ease-out 1s;
+
+  .img {
+    width: 34px;
+    height: 34px;
+  }
+
+  .left, .right {
+    display: flex;
+    align-items: center;
+  }
+}
+</style>

+ 2 - 3
src/layout/components/TelNoticeBox/index.vue

@@ -130,11 +130,10 @@ const onDialogOpen = async () => {
 
                 <ul v-for="item, index in userInfos" :key="index" class="user-info_list space-y-[10px]">
                   <li class="flex justify-between row">
-                    <span class="label">用户号:</span>
-                    <span>{{ item.cardNo }}</span>
+                    <span class="label">用户号:</span>
+                    <span>{{ item.userNo }}</span>
                   </li>
                   <li class="flex justify-between row">
-                    <!-- 问金龙 是否有该字段 -->
                     <span class="label">欠费金额:</span> 
                     <span>{{ item.waterFees }}</span>
                   </li>

+ 2 - 0
src/layout/index.vue

@@ -3,6 +3,7 @@ import HeaderGroup  from './components/HeaderGroup';
 import Sidebar from './components/Sidebar'
 import TelNoticeBar from './components/TelNoticeBar'
 import TelNoticeBox from './components/TelNoticeBox'
+import NoticeBar from './components/NoticeBar'
 import { AppMain } from './components'
 import useVoiceStore from "@/store/modules/voice";
 import useUserStore from "@/store/modules/user";
@@ -71,6 +72,7 @@ onMounted(() => {
     </div>
     <TelNoticeBar></TelNoticeBar>
     <TelNoticeBox></TelNoticeBox>
+    <NoticeBar></NoticeBar>
   </div>
 </template>
 

+ 29 - 0
src/store/modules/notice.js

@@ -0,0 +1,29 @@
+import { ref } from 'vue';
+import { workbenchApi } from "@/api/voice/workbench";
+
+
+const useNoticeStore = defineStore('notice', () => {
+  
+  // 顶部告警状态
+  const noticePopupStatus = ref(false)
+
+  // 底部告警状态
+  const noticeBarStatus = ref(false);
+
+  const warningNumber = ref(0);
+
+  const getWaringCount = () => {
+    getNewestWarningCount().then(res => {
+
+    })
+  }
+
+  return {
+    noticePopupStatus,
+    noticeBarStatus,
+    warningNumber
+  }
+
+})
+
+export default useNoticeStore;

+ 31 - 19
src/views/voice/call/index.vue

@@ -35,6 +35,13 @@ const serviceCategoryOptions = [
   { value: 2, label: '机器人转人工' }
 ]
 
+const callTypeOptions = [
+  { value: 0, label: '白名单' },
+  { value: 1, label: 'AI客服' },
+  { value: 2, label: '传统服务' }
+]
+
+
 // 清除检索条件
 const handleCleanOptions = () => {
   queryParams.value = {
@@ -132,21 +139,17 @@ onMounted(() => {
 <template>
   <div class="call-viewprot">
     <div class="search-card">
-      <el-row :gutter="24" class="mb-[24px]">
-        <el-col :span="6">
+
+      <div class="grid grid-cols-4 2xl:grid-cols-5 gap-[24px]">
           <SearchItemWrapper>
             <el-input class="search-input" placeholder="用户电话号码" v-model="queryParams.phone"></el-input>
           </SearchItemWrapper>
-        </el-col>
-        <el-col :span="6">
           <SearchItemWrapper label="客服名称">
             <el-select v-model="queryParams.userId" placeholder="请选择" size="large" :empty-values="[null, undefined]">
               <el-option label="全部" value="" />
               <el-option v-for="item in agentList" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </SearchItemWrapper>
-        </el-col>
-        <el-col :span="6">
           <SearchItemWrapper label="通话状态">
             <el-select v-model="queryParams.status" placeholder="Select" size="large" :empty-values="[null, undefined]">
               <el-option label="全部" value="" />
@@ -154,8 +157,6 @@ onMounted(() => {
               <el-option label="已接通" :value="1" />
             </el-select>
           </SearchItemWrapper>
-        </el-col>
-        <el-col :span="6">
           <SearchItemWrapper label="通话类型">
             <el-select v-model="queryParams.category" placeholder="全部" size="large" :empty-values="[null, undefined]">
               <el-option label="全部" value="" />
@@ -163,25 +164,33 @@ onMounted(() => {
               <el-option label="呼出" :value="1" />
             </el-select>
           </SearchItemWrapper>
-        </el-col>
-      </el-row>
-      <el-row :gutter="24">
-        <el-col :span="6">
+          <SearchItemWrapper label="通话类型">
+            <el-select v-model="queryParams.category" placeholder="全部" size="large" :empty-values="[null, undefined]">
+              <el-option label="全部" value="" />
+              <el-option label="呼入" :value="0" />
+              <el-option label="呼出" :value="1" />
+            </el-select>
+          </SearchItemWrapper>
           <SearchItemWrapper label="服务类型">
             <el-select v-model="queryParams.serviceCategory" placeholder="请选择" size="large" :empty-values="[null, undefined]">
               <el-option label="全部" value="" />
               <el-option v-for="item in serviceCategoryOptions" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
           </SearchItemWrapper>
-        </el-col>
-        <el-col :span="6">
           <SearchItemWrapper label="通话发起时间">
             <el-date-picker v-model="dataPickerValue" type="daterange" range-separator="-" start-placeholder="起始日期"
               end-placeholder="结束日期" style="width: 100%;" :editable="false" value-format="YYYY-MM-DD"/>
           </SearchItemWrapper>
-        </el-col>
-        <el-col :span="12">
-          <div class="flex items-center justify-start space-x-[30px]">
+          <!-- <SearchItemWrapper label="呼叫类型">
+            <el-select v-model="queryParams.type" placeholder="请选择" size="large" :empty-values="[null, undefined]">
+              <el-option label="全部" value="" />
+              <el-option v-for="item in callTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+          </SearchItemWrapper> -->
+          <SearchItemWrapper label="通话ID">
+            <el-input class="search-input" placeholder="通话ID" v-model="queryParams.sessionId"></el-input>
+          </SearchItemWrapper>
+          <div class="flex items-center justify-between auto-cols-max">
             <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
             <div class="custom-btn custom-btn_default" @click="handleBatchDownload">
               批量下载语音 
@@ -191,8 +200,7 @@ onMounted(() => {
               <span>清除条件</span>
             </div>
           </div>
-        </el-col>
-      </el-row>
+      </div>
     </div>
     <div class="table-card">
       <div style="height: 100%;" ref="tableContainer">
@@ -264,6 +272,10 @@ onMounted(() => {
   border-radius: 8px;
   background: #fff;
 
+  .custom-btn {
+    word-break: keep-all;
+  }
+
   .search-card {
     padding-bottom: 20px;
     margin-bottom: 20px;

+ 250 - 0
src/views/voice/general/index.vue

@@ -0,0 +1,250 @@
+<script setup>
+import { callApi, workbenchApi } from '@/api/voice/call';
+import SearchItemWrapper from '@/components/SearchItemWrapper';
+import AudioPlayer from '@/components/AudioPlayer';
+import useTableHeight from '@/composables/useTableHeight';
+import CallView from '@/components/CallView';
+import dayjs from 'dayjs';
+
+const { proxy } = getCurrentInstance();
+const { tableContainer, tableMaxHeight } = useTableHeight();
+
+const dataPickerValue = ref([]);
+const loading = ref(false);
+const tableData = ref([]);
+const total = ref(0);
+const agentList = ref([]);
+const drawer = ref(false);
+const callDetails = ref({});
+
+const isTransitionVoiceStatus = ref(false);
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  userId: '',
+  status: '',
+  category: '',
+  phone: '',
+  serviceCategory: ''
+})
+
+const serviceCategoryOptions = [
+  { value: 0, label: '人工坐席' },
+  { value: 1, label: '机器人坐席' },
+  { value: 2, label: '机器人转人工' }
+]
+
+const callTypeOptions = [
+  { value: 0, label: '白名单' },
+  { value: 1, label: 'AI客服' },
+  { value: 2, label: '传统服务' }
+]
+
+
+// 清除检索条件
+const handleCleanOptions = () => {
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    userId: '',
+    status: '',
+    category: '',
+    phone: '',
+    serviceCategory: ''
+  };
+  dataPickerValue.value = [];
+  getList();
+}
+
+// 语音转化完成
+const handleVoiceParsed = (item) => {
+  const { parsedVoiceContent } = item;
+  callDetails.value.parsedVoiceContent = parsedVoiceContent;
+}
+
+const jumpDetails = (item) => {
+  callDetails.value = item;
+  drawer.value = true;
+}
+
+// 音频加载完成
+const onAudioLoadDone = ({ durationTime, id }) => {
+  tableData.value.map(item => {
+    if (item.id == id) {
+      item.times = durationTime || ''
+    }
+  })
+}
+
+const handleClose = (done) => {
+  if ( !isTransitionVoiceStatus.value ) {
+    done();
+  } else {
+    proxy.$modal.msgWarning("当前语音正在转换中,请稍后");
+  }
+}
+
+// 批量下载
+const handleBatchDownload = () => {
+  const [timeBegin, timeEnd] = dataPickerValue.value;
+  if ( !timeBegin ) {
+    return proxy.$modal.msgError("请选择通话发起时间");
+  }
+  const now = dayjs();
+  const formattedDateTime = now.format('YYYYMMDDHHmmss');
+  const milliseconds = now.millisecond();
+  const fullFormattedString = `jmsyy${formattedDateTime}${milliseconds}`;
+  proxy.getDownload("/business/record/downloadBatchByCondition", {
+    ...queryParams.value, timeBeginReq: timeBegin, timeEndReq: timeEnd
+  }, `${fullFormattedString}.zip`);
+}
+
+// 单独下载
+const handleDownload = ({ id, sessionId }) => {
+  proxy.getDownload("/business/record/downloadById", { id }, `${sessionId}.wav`);
+}
+
+const getList = () => {
+  const [timeBegin, timeEnd] = dataPickerValue.value || [];
+
+  loading.value = true;
+
+  workbenchApi.getCallRecordList({...queryParams.value, timeBeginReq:timeBegin, timeEndReq: timeEnd}).then(({ rows, total:t }) => {
+    const typeEnum = { 0: '白名单', 1: 'AI客服', 2: '传统服务' };
+    const statusEnum = { 0: '未接听', 1: '已接通' };
+    const serviceCategoryEnum = { 0: '人工坐席', 1: '机器人坐席', 2: '机器人转人工' };
+    tableData.value = rows.map(item => ({
+      ...item,
+      url: item.url ? item.url + '?timstamp=' + new Date().getTime() : '',
+      typeText: item.category == 1 ? '传统服务' : typeEnum[item.type],
+      statusText: statusEnum[item.status],
+      serviceCategoryText: serviceCategoryEnum[item.serviceCategory]
+    }));
+
+    loading.value = false;
+    total.value = t;
+  })
+};
+
+onMounted(() => {
+  workbenchApi.getAgentList().then(({ data }) => {
+    agentList.value = data;
+  })
+  getList();
+})
+
+</script>
+
+<template>
+  <div class="call-viewprot">
+    <div class="search-card">
+      <div class="grid grid-cols-4 gap-[24px]">
+        <SearchItemWrapper label="通话发起时间">
+          <el-date-picker v-model="dataPickerValue" type="daterange" range-separator="-" start-placeholder="起始日期"
+            end-placeholder="结束日期" style="width: 100%;" :editable="false" value-format="YYYY-MM-DD"/>
+        </SearchItemWrapper>
+        <div class="flex items-center justify-start space-x-[20px]">
+          <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
+          <div class="custom-btn custom-btn_default" @click="handleBatchDownload">重置</div>
+        </div>
+      </div>
+    </div>
+    <div class="table-card">
+      <div style="height: 100%;" ref="tableContainer">
+        <el-table :data="tableData" style="width: 100%" :max-height="tableMaxHeight" v-loading="loading">
+          <el-table-column prop="date" label="统计日期" align="center" width="130" fixed />
+          <el-table-column prop="inTotal" label="呼入总量" align="center" width="100">
+            <template #default="scope">
+              <span>{{ !scope.row.category && scope.row.category != 0 ? '' : scope.row.category == 0 ? '呼入' : '呼出' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="successTotal" label="接通总量" align="center" width="100" />
+          <el-table-column prop="failTotal" label="未接通总量" align="center" width="100" />
+          <el-table-column prop="robotHearTotal" label="机器人接听" align="center" width="100">
+            <template #default="scope">
+              <div class="flex items-center justify-center space-x-[6px]">
+                <span class="w-[6px] h-[6px] rounded-full" :class="[scope.row.status === 1 ? 'bg-[#65C734]': 'bg-[#c75134]']" ></span>
+                <span>{{ scope.row.statusText }}</span>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="transferTotal" label="机器人转人工" align="center" width="200" />
+          <el-table-column prop="humanTotal" label="人工接听" align="center" width="180" />
+          <el-table-column prop="robotHandleTotal" label="机器人处理量" align="center" width="180" />
+          <el-table-column prop="humanHandleTotal" label="人工处理量" align="center" width="90"/>
+          <el-table-column prop="robotRate" label="机器人处理率" align="center" width="350">
+            <template #default="scope">
+              <div class="flex justify-center" v-show="scope.row.url">
+                <AudioPlayer :audioUrl="scope.row.url" @loadDone="onAudioLoadDone" :id="scope.row.id"></AudioPlayer>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="humanRate" label="人工处理率" align="center" width="140"/>
+          <el-table-column prop="failRate" label="未接通率" align="center" width="140"/>
+          <!-- <el-table-column prop="handle" label="操作" align="center" fixed="right" width="150">
+            <template #default="scope">
+              <div class="flex justify-center space-x-[20px]">
+                <span class="text-[#165DFF] cursor-pointer" @click="jumpDetails(scope.row)">详情</span>
+                <span class="text-[#165DFF] cursor-pointer" @click="handleDownload(scope.row)">语音下载</span>
+              </div>
+            </template>
+          </el-table-column> -->
+        </el-table>
+        <pagination v-show="total >= 0" :total="total" v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize" @pagination="getList" />
+      </div>
+    </div>
+
+    <el-drawer
+      v-model="drawer"
+      title="通话详情"
+      direction="rtl"
+      :before-close="handleClose"
+      class="voice-drawer"
+      size="900"
+    >
+      <div>
+        <CallView :data="callDetails" noInit @on-end="handleVoiceParsed" v-model="isTransitionVoiceStatus"></CallView>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.call-viewprot {
+  display: flex;
+  flex-flow: column;
+  width: 100%;
+  height: 100%;
+  padding: 28px 24px 18px 24px;
+  border-radius: 8px;
+  background: #fff;
+
+  .custom-btn {
+    word-break: keep-all;
+  }
+
+  .search-card {
+    padding-bottom: 20px;
+    margin-bottom: 20px;
+    border-bottom: 1px dashed #E5E6EB;
+    overflow: hidden;
+    flex-shrink: 0;
+  }
+
+  .table-card {
+    height: 100%;
+  }
+}
+</style>
+
+<style lang="scss">
+.voice-drawer {
+  .el-drawer__header {
+    margin-bottom: 0;
+    color: #333;
+    font-weight: bold;
+  }
+}
+</style>

+ 18 - 9
src/views/voice/notice/add.vue

@@ -81,6 +81,12 @@ const goBack = () => {
   router.push('/voice/notice')
 }
 
+const onCleanTableData = () => {
+  tableData.value = [];
+  pumpingValue.value = [];
+  housingValue.value = [];
+}
+
 onMounted(() => {
   const id = route.query.id;
   if ( id ) {
@@ -105,9 +111,9 @@ onMounted(() => {
   servicesApi.getNeighborhoodList({pageSize: 10000, pageNum: 1}).then(res => {
     housingOptions.value = res.rows;
   })
+})
 
 
-})
 </script>
 
 <template>
@@ -125,8 +131,8 @@ onMounted(() => {
       </div>
   
       <div class="pt-[20px]">
-        <el-form :model="formData" :rules="rules" ref="formRef" label-width="auto" label-position="left" style="width: 900px;">
-          <el-row :gutter="20">
+        <el-form :model="formData" :rules="rules" ref="formRef" label-width="auto" label-position="left">
+          <el-row :gutter="20" style="width: 900px;">
             <el-col :span="24" style="display: flex; align-items: flex-start; justify-content: space-between;">
               <el-form-item label="停水时间" prop="timeBegin">
                 <div>
@@ -157,9 +163,11 @@ onMounted(() => {
                 <el-input v-model="formData.reason" placeholder="请输入停水原因" type="textarea" :autosize="{ minRows: 8, maxRows: 6 }" resize="none"/>
               </el-form-item>
             </el-col>
-            <el-col :span="24">
-              <el-form-item label="停水范围" required>
-                <ul class="select-group">
+          </el-row>
+          <div>
+            <el-form-item label="停水范围" required style="width: 100%;">
+              <div class="flex justify-between">
+                <ul class="select-group space-x-[40px] mr-[20px]" style="width: 784px;">
                   <li class="space-x-[5px]">
                     <span>根据泵站添加:</span>
                     <el-select
@@ -193,9 +201,10 @@ onMounted(() => {
                     <div class="custom-btn custom-btn_primary" @click="addTableData(1)">添加</div>
                   </li>
                 </ul>
-              </el-form-item>
-            </el-col>
-          </el-row>
+                <div class="custom-btn custom-btn_primary" @click="onCleanTableData">重置</div>
+              </div>
+            </el-form-item>
+          </div>
         </el-form>
       </div>
   

+ 0 - 1
src/views/voice/notice/index.vue

@@ -153,7 +153,6 @@ onMounted(() => {
             </template>
           </el-table-column>
         </el-table>
-        
         <pagination
           v-show="total >= 0"
           :total="total"

+ 2 - 2
src/views/voice/upload/index.vue

@@ -83,9 +83,9 @@ onMounted(() => {
               </div>
             </div>
           </div>
-
+          <!-- accept=".xlsx" -->
         <div class="flex justify-center pt-[20px]">
-          <el-upload accept=".xlsx" :limit="1" :action="uploadFileUrl" :file-list="fileList"
+          <el-upload  :limit="1" :action="uploadFileUrl" :file-list="fileList"
             :on-success="handleUploadSuccess" :on-error="handleError" :before-upload="handleBeforeUpload" :show-file-list="false" :headers="headers" ref="fileUpload">
             <el-button type="primary" class="w-[200px]" size="large">上传文件</el-button>
           </el-upload>