Browse Source

feat: 前后端接口联调,下载,统计版块

sunxiao 1 day ago
parent
commit
039257552f

+ 1 - 1
.env.development

@@ -1,5 +1,5 @@
 # 页面标题
-VITE_APP_TITLE = LibraAI智能体运营平台
+VITE_APP_TITLE = 佳木斯智能语音客服
 
 # 开发环境配置
 VITE_APP_ENV = 'development'

+ 1 - 1
.env.production

@@ -1,5 +1,5 @@
 # 页面标题
-VITE_APP_TITLE = LibraAI智能体运营平台
+VITE_APP_TITLE = 佳木斯智能语音客服
 
 # 生产环境配置
 VITE_APP_ENV = 'production'

+ 1 - 2
index.html

@@ -8,9 +8,8 @@
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
   <script src="https://static.fuxicarbon.com/hs-cti/socket.io.min.js"></script>
   <script src="https://static.fuxicarbon.com/hs-cti/SIP.min.js"></script>
-  <!-- <script src="hs-cti.es6.umd.js"></script> -->
   <link rel="icon" href="/favicon.ico">
-  <title>若依管理系统</title>
+  <title>佳木斯智能语音客服</title>
   <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
   <style>
     html,

BIN
public/favicon.ico


+ 12 - 0
src/api/voice/adress.js

@@ -0,0 +1,12 @@
+import request from '@/utils/request'
+
+export const adressApi = {
+  /**
+   * 获取 省 - 市 - 区
+   */
+  getAdressData: params => request({
+    url: `/front/getCities`,
+    params
+  }),
+
+}

+ 27 - 0
src/api/voice/analyse.js

@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+export const analyseApi = {
+  /**
+   * 通话统计接口
+   */
+  getCallRecordCountInfo: params => request({
+    url: `/front/callRecordCountInfo`,
+    params
+  }),
+  
+  /**
+   * 人工客服统计
+   */
+  getUserCallCount: params => request({
+    url: `/front/userCallCount`,
+    params
+  }),
+
+  /**
+   * 机器人统计
+   */
+  getRobotCallCount: params => request({
+    url: `/front/robotCallCount`,
+    params
+  }),
+}

+ 53 - 0
src/api/voice/notice.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+export const noticeApi = {
+  /**
+   * 停水公告 - 查询
+   */
+  getWaterList: params => request({
+    url: `/business/water/list`,
+    params
+  }),
+
+  /**
+   * 停水公告 - 查询 - 详情
+   */
+  getWater: params => request({
+    url: `/business/water/${params}`
+  }),
+  
+  /**
+  * 停水公告 - 删除
+  */
+ delWater: ids => request({
+   url: `/business/water/${ids}`,
+   method: 'delete',
+ }),
+
+  /**
+  * 停水公告 - 新增
+  */
+  postWater: data => request({
+    url: `/business/water`,
+    method: 'post',
+    data
+  }),
+
+  /**
+  * 停水公告 - 修改
+  */
+  putWater: data => request({
+    url: `/business/water`,
+    method: 'put',
+    data
+  }),
+
+  /**
+  * 停水公告 - 查询关联关系
+  */
+  getExtraListByType: params => request({
+    url: `/business/water/getExtraListByType`,
+    params
+  }),
+
+}

+ 113 - 0
src/api/voice/services.js

@@ -0,0 +1,113 @@
+import request from '@/utils/request'
+
+export const servicesApi = {
+  /**
+   * 小区列表
+   */
+  getNeighborhoodList: params => request({
+    url: `/business/neighborhood/list`,
+    params
+  }),
+
+  /**
+   * 泵站列表
+   */
+  getPumpingList: params => request({
+    url: `business/station/getSimplePumpingStationList`,
+    params
+  }),
+
+  /**
+   * 小区 - 新增
+   */
+  postNeighborhood: data => request({
+    url: `/business/neighborhood`,
+    method: 'post',
+    data
+  }),
+
+  /**
+   * 小区 - 修改
+   */
+  putNeighborhood: data => request({
+    url: `/business/neighborhood`,
+    method: 'put',
+    data
+  }),
+
+  /**
+   * 小区 - 删除
+   */
+  delNeighborhood: ids => request({
+    url: `/business/neighborhood/${ids}`,
+    method: 'delete'
+  }),
+
+  /**
+   * 泵站列表
+   */
+  getStationList: params => request({
+    url: `/business/station/list`,
+    params
+  }),
+
+  /**
+   * 泵站 - 新增
+   */
+  postStation: data => request({
+    url: `/business/station`,
+    method: 'post',
+    data
+  }),
+
+  /**
+   * 小区 - 修改
+   */
+  putStation: data => request({
+    url: `/business/station`,
+    method: 'put',
+    data
+  }),
+
+  /**
+   * 小区 - 删除
+   */
+  delStation: ids => request({
+    url: `/business/station/${ids}`,
+    method: 'delete'
+  }),
+
+  /**
+   * 泵站 - 查询关联的小区
+   */
+  getSimpleNeighbourhoodList: params => request({
+    url: `/business/station/getSimpleNeighbourhoodList`,
+    params
+  }),
+
+  /**
+   * 泵站 - 小区泵站 - 关联的楼层
+   */
+  getBuildingsAndFlag: params => request({
+    url: `/business/station/getBuildingsAndFlagByID2`,
+    params
+  }),
+
+  /**
+   * 泵站 - 关联后的最终保存
+   */
+  postSaveFinalData: data => request({
+    url: `/business/station/addPumpingStationAndNeighbourhoodBuildings`,
+    method: 'post',
+    data
+  }),
+
+  /**
+   * 泵站 - 关联泵站 服务小区 table 关联泵站
+   */
+  getStation: params => request({
+    url: `/business/neighborhood/getPumpingStationAndNeighbourhoodBuildingsById`,
+    params
+  })
+
+}

+ 37 - 0
src/api/voice/whiteList.js

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+export const waiteListApi = {
+  /**
+   * 白名单 - 查询
+   */
+  getWhiteList: params => request({
+    url: `/business/whitelist/list`,
+    params
+  }),
+
+  /**
+   * 白名单 - 新增
+   */
+  postWhiteList: data => request({
+    url: `/business/whitelist`,
+    method: 'post',
+    data
+  }),
+
+  /**
+   * 小区 - 修改
+   */
+  putWhiteList: data => request({
+    url: `/business/whitelist`,
+    method: 'put',
+    data
+  }),
+
+  /**
+   * 白名单 - 删除
+   */
+  delWhiteList: ids => request({
+    url: `/business/whitelist/${ids}`,
+    method: 'delete'
+  }),
+}

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

@@ -24,4 +24,36 @@ export const workbenchApi = {
     method: 'put',
     data
   }),
+
+  /**
+   * sessionId - 查询
+   */
+  getSessionInfo: params => request({
+    url: `/business/record/getCallBySessionId`,
+    params
+  }),
+
+  /**
+   * 语音转文字
+   */
+  getVoiceToText: params => request({
+    url: `/business/record/updateFile2TextById`,
+    params
+  }),
+
+  /**
+   * 查询客服
+   */
+  getAgentList: params => request({
+    url: `/business/record/getAgentList`,
+    params
+  }),
+
+  /**
+   * 获取某个用户的坐席的outId信息
+   */
+  getSeatsByUser: params => request({
+    url: `/front/getSeatsByUserId`,
+    params
+  }),
 }

+ 8 - 0
src/assets/icons/svg/whiteList-active.svg

@@ -0,0 +1,8 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15.375 2.25H2.625C2.00368 2.25 1.5 2.75368 1.5 3.375V14.625C1.5 15.2463 2.00368 15.75 2.625 15.75H15.375C15.9963 15.75 16.5 15.2463 16.5 14.625V3.375C16.5 2.75368 15.9963 2.25 15.375 2.25Z" fill="#165DFF" stroke="#165DFF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M1.5 5.25H16.5" stroke="white" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
+<path d="M7.5 9H13.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 12H13.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.5 9H5.25" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.5 12H5.25" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 8 - 0
src/assets/icons/svg/whiteList.svg

@@ -0,0 +1,8 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15.375 2.25H2.625C2.00368 2.25 1.5 2.75368 1.5 3.375V14.625C1.5 15.2463 2.00368 15.75 2.625 15.75H15.375C15.9963 15.75 16.5 15.2463 16.5 14.625V3.375C16.5 2.75368 15.9963 2.25 15.375 2.25Z" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M1.5 5.25H16.5" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 9H13.5" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 12H13.5" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.5 9H5.25" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.5 12H5.25" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 17 - 25
src/components/CallView/index.vue

@@ -6,6 +6,9 @@ import useVoiceStore from "@/store/modules/voice";
 
 import AudioPlayer from '@/components/AudioPlayer';
 import CustomRowItem from '@/components/CustomRowItem';
+import VoiceToText from '@/components/VoiceToText';
+
+const emit = defineEmits(['onEnd'])
 
 const props = defineProps({
   noInit: {
@@ -24,7 +27,6 @@ const route = useRoute();
 const remark = ref('');
 const callDetails = ref({});
 
-const isExpand = ref(false);
 const dialogVisible = ref(false);
 
 const categoryTypeEnum = {
@@ -73,12 +75,17 @@ const onConfirm = () => {
   voiceStore.onMakingCall("15810954324");
 }
 
-onMounted(async () => {
-  if (props.noInit) return;
-  const { id } = route.query;
-  const { data } = await workbenchApi.getCallRecordDetails(id);
-  callDetails.value = data;
-});
+// 语音转换文字
+const onVoiceParsed = ({ parsedVoiceContent, id }) => {
+  emit('onEnd', { parsedVoiceContent, id });
+}
+
+// onMounted(async () => {
+//   if (props.noInit) return;
+//   const { id } = route.query;
+//   const { data } = await workbenchApi.getCallRecordDetails(id);
+//   callDetails.value = data;
+// });
 </script>
 
 <template>
@@ -135,24 +142,9 @@ onMounted(async () => {
       </div>
     </custom-row-item>
     <custom-row-item label="通话录音">
-      <div class="record-box">
-        <div class="record-play-control space-x-[14px]">
-          <AudioPlayer :audioUrl="callDetails.url"></AudioPlayer>
-          <span class="text-[#165DFF] cursor-pointer" @click="isExpand = !isExpand">{{ isExpand ? '收起文字' : '转文字'
-            }}</span>
-        </div>
-        <div class="record-play-content" v-show="isExpand">
-          供水公司客服:您好,欢迎拨打XX市供水公司客服热线,我是客服代表小张,请问有什么可以帮您?
-          用户:你好,我家里的水压最近特别低,想咨询一下是什么原因?
-          供水公司客服:您好,非常感谢您对我们工作的关注。请您告诉我一下您所在的小区名称和具体楼号,我帮您查询一下。
-          用户:好的,我家在阳光小区3号楼。
-          供水公司客服:阳光小区3号楼,好的,请您稍等,我马上为您查询。
-          (等待片刻)
-          供水公司客服:您好,经过查询,近期我们并没有接到关于阳光小区3号楼水压低的报修。请问您家水压低的情况是持续性的还是偶尔出现?有没有规律可循?
-          用户:最近一个星期都比较低,尤其是早上和晚上用水高峰期。
-          供水公司客服:明白了,可能是用水高峰期导致供水压力不足。我们会安排维修人员到现场查看,争取尽快解决问题。请问您方便提供一下联系方式吗?以便我们及时与您沟通。
-        </div>
-      </div>
+      <VoiceToText @on-parsed="onVoiceParsed" :content="callDetails.parsedVoiceContent" :id="callDetails.id">
+        <AudioPlayer :audioUrl="callDetails.url"></AudioPlayer>
+      </VoiceToText>
     </custom-row-item>
     <el-dialog v-model="dialogVisible" title="编辑备注" width="530" modal-class="custom-workbench-dialog" align-center>
       <template #header>

+ 103 - 0
src/components/VoiceToText/index.vue

@@ -0,0 +1,103 @@
+<script setup>
+import { ElMessage } from 'element-plus'
+import { workbenchApi } from '@/api/voice/workbench';
+
+const emit = defineEmits(['onParsed']);
+const props = defineProps({
+  content: {
+    type: String,
+    default: ''  //  0 未转化  1 转换
+  },
+  id: {
+    type: Number,
+    default: 0
+  }
+});
+
+const parsedVoiceList = ref([]);
+const loadingStatus = ref({});
+const isExpandStatus = ref({});
+
+const loading = computed(() => loadingStatus.value['loading' + props.id]);
+
+watch(() => props.id, () => {
+  parsedVoiceList.value = props.content? JSON.parse(props.content) : [];
+  Object.keys(isExpandStatus.value).forEach(key => {
+    isExpandStatus.value[key] = false;
+  })
+})
+
+const handleTransformVoiceToText = async () => {
+  if ( loadingStatus.value['loading' + props.id] ) return;
+
+  let id = props.id;
+
+  loadingStatus.value['loading' + id] = true;
+
+  if ( !props.content ) {
+    try {
+      ElMessage.warning('语音开始转换中, 请耐心等待,切勿进行其他操作');
+      const { data } = await workbenchApi.getVoiceToText({ id });
+      if ( data ) {
+        if ( props.id === id ) {
+          parsedVoiceList.value = JSON.parse(data);
+          emit('onParsed', { parsedVoiceContent: data, id });
+        }
+      }
+    } catch (error) {}
+  } else {
+    parsedVoiceList.value = JSON.parse(props.content);
+  }
+
+  isExpandStatus.value['id' + id] = !isExpandStatus.value['id' + id];
+
+  loadingStatus.value['loading' + id] = false;
+}
+</script>
+
+<template>
+  <div class="flex-1 text-[#1D2129]" >
+    <div class="record-box">
+      <div class="record-play-control space-x-[14px]">
+        <slot></slot>
+        <span
+          class="text-[#165DFF] text-[12px] cursor-pointer"
+          @click="handleTransformVoiceToText">{{ isExpandStatus['id' + id] ? '收起文字' : '转文字'}}
+        </span>
+        <el-icon class="is-loading" v-show="loading">
+          <Loading />
+        </el-icon>
+      </div>
+      <ul class="record-play-content" v-show="isExpandStatus['id' + id]">
+        <li v-for="item, index in parsedVoiceList" :key="index">
+          <span>
+            {{ item.Text }}
+          </span>
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.record-box {
+  width: 100%;
+  padding: 16px;
+  border-radius: 8px;
+  background: linear-gradient(90deg, #F6F5F8 0%, #FFF 100%);
+
+  .record-play-control {
+    display: flex;
+    align-items: center;
+    padding: 0;
+    font-size: 12px;
+  }
+
+  .record-play-content {
+    padding-top: 12px;
+    font-family: "PingFang SC";
+    font-size: 13px;
+    line-height: 20px;
+  }
+}
+</style>

+ 4 - 3
src/composables/useTableHeight.js

@@ -5,7 +5,7 @@ const useTableHeight = () => {
 
   const tableMaxHeight = ref(0);
 
-  const getTableContainerHeight = () => {
+  const getTableContainerHeight = (el) => {
     const containerRect = tableContainer.value.getBoundingClientRect();
     const height = containerRect.height;
 
@@ -20,11 +20,12 @@ const useTableHeight = () => {
   
   onUnmounted(() => {
     window.removeEventListener('resize', getTableContainerHeight);
-  })
+  });
 
   return {
     tableContainer,
-    tableMaxHeight
+    tableMaxHeight,
+    getTableContainerHeight
   }
 }
 

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

@@ -1,23 +1,30 @@
 <script setup>
 import { storeToRefs } from 'pinia';
 import useVoiceStore from "@/store/modules/voice";
+import usePermissionStore from "@/store/modules/permission";
 import { ElMessageBox } from 'element-plus';
 import TelCallBoard from './TelCallBoard.vue'
 import useUserStore from '@/store/modules/user';
+import { onMounted, watchEffect } from 'vue';
 
 const voiceStore = useVoiceStore();
+const usePermission = usePermissionStore();
 const { proxy } = getCurrentInstance();
 
 const { callAnswered } = storeToRefs(voiceStore);
 const systemState = ref(false);
 const userStore = useUserStore();
+const isShowCallPanel = ref(false); 
 
 const SYSTEM_STAT_ENUM = [
   {label: '置闲', state: true, icon: 'online-icon'},
   {label: '置忙', state: false, icon: 'offline-icon'}
 ];
 
-const num = ref(null);
+watchEffect(() => {
+  // const routes = usePermission.routes;
+  // isShowCallPanel.value = routes.findIndex(({ name }) => name === 'Console') !== -1;
+})
 
 // 修改在线状态
 const handlePopoverItem = ({ state, label }) => {
@@ -47,6 +54,7 @@ const logout = () => {
     })
   }).catch(() => { });
 }
+
 </script>
 
 <template>
@@ -57,9 +65,7 @@ const logout = () => {
     </div>
 
     <div class="navbar-right flex items-center space-x-[12px]">
-      <!-- callAnswered -->
-      <el-dropdown trigger="click" popper-class="custom-dropdown" size="small" :disabled="callAnswered" class="forbid">
-        <!-- class="forbid" -->
+      <el-dropdown trigger="click" popper-class="custom-dropdown" size="small" :disabled="callAnswered" class="forbid" v-if="voiceStore.isAuthPane">
         <div :class="['system-state-wrapper', {forbid: callAnswered}]">
           <div :class="['system-state-btn', {forbid: callAnswered}]">
             <p class="flex items-center space-x-[4px]">
@@ -83,7 +89,7 @@ const logout = () => {
         </template>
       </el-dropdown>
       
-      <TelCallBoard />
+      <TelCallBoard v-if="voiceStore.isAuthPane"/>
 
       <div class="avatar-wrapper flex items-center space-x-[4px]">
         <div class="avatar-img">

+ 1 - 1
src/layout/components/Sidebar/SidebarItem.vue

@@ -76,7 +76,7 @@ function hasTitle(title){
 }
 
 function getIconPath (iconName) {
-  const whitelist = ['workbench', 'call', 'notice', 'analyse','services'];
+  const whitelist = ['workbench', 'call', 'notice', 'analyse','services', 'whiteList'];
   return whitelist.includes(iconName) ? iconName + '-active' : iconName;
 }
 </script>

+ 1 - 1
src/layout/components/TelNoticeBar/index.vue

@@ -73,7 +73,7 @@ const handleCallAnswered = () => {
   height: 88px;
   padding: 0 24px;
   background: #000;
-  z-index: 100;
+  z-index: 1100;
   border-radius: 8px;
   color: #fff;
   transition: top ease-out .5s;

+ 95 - 120
src/layout/components/TelNoticeBox/index.vue

@@ -1,16 +1,23 @@
 <script setup>
 import { storeToRefs } from 'pinia';
+import { ElMessage } from 'element-plus';
 import useVoiceStore from "@/store/modules/voice";
+import { workbenchApi } from '@/api/voice/workbench';
 
 import AudioPlayer from '@/components/AudioPlayer';
+import VoiceToText from '@/components/VoiceToText';
 import { watchEffect } from 'vue';
 
 const voiceStore = useVoiceStore();
 const { callDialing, callTime, isMakingCall } = storeToRefs(voiceStore);
 
-const activeName = ref('currentRecord');
+const callRecords = ref([]);
+const cutOffWaters = ref([]);
+const userInfos = ref([]);
+const dataSource = ref({});
+const currentCallRecords = ref('');
 
-const isExpand =ref(false);
+const activeName = ref('currentRecord');
 
 watchEffect(() => {
   activeName.value = isMakingCall ? 'information' : 'currentRecord';
@@ -30,6 +37,33 @@ const handleCallDisconnected = () => {
 const handleCallAnswered = () => {
   voiceStore.onCallAnswered();
 }
+
+const handleVoiceParsed = ({ parsedVoiceContent, id }) => {
+  callRecords.value.forEach((item) => {
+    if ( item.id = id ) {
+      item.parsedVoiceContent = parsedVoiceContent;
+    }
+  });
+}
+
+const onSaveRemark = () => {
+  const { id, remark } = dataSource.value;
+  workbenchApi.putCallRecord({ id, remark });
+  ElMessage.success('保存备注成功');
+}
+
+// 弹框打开
+const onDialogOpen = async () => {
+  const { data } = await workbenchApi.getSessionInfo({ sessionId:199320 });
+  callRecords.value = data.callRecords;
+  cutOffWaters.value = data.cutOffWaters;
+  userInfos.value = data.userInfos;
+  if ( data.currentCallRecords ) {
+    currentCallRecords.value = JSON.parse(data.currentCallRecords).data
+  }
+  dataSource.value = data;
+}
+
 </script>
 
 <template>
@@ -40,6 +74,7 @@ const handleCallAnswered = () => {
     modal-class="tel-notice-dialog"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
+    @open="onDialogOpen"
   >
     <div class="tel-notice-inner">
       <div class="tel-header">
@@ -66,72 +101,51 @@ const handleCallAnswered = () => {
         <div class="tel-body_inner space-x-[16px]">
           <div class="left-content">
             <el-scrollbar style="height: 100%;">
-              <ul class="record-list space-y-[10px]">
-                <li class="flex justify-between">
+              <div class="record-list space-y-[10px]">
+                <div class="flex justify-between row">
                   <span class="label">通话次数:</span>
-                  <span>22</span>
-                </li>
-                <li class="flex justify-between">
-                  <span class="label">用户户号:</span>
-                  <span>22</span>
-                </li>
-                <li class="flex justify-between">
-                  <span class="label">水费剩余:</span>
-                  <span>22</span>
-                </li>
-                <li class="flex justify-between space-x-[8px]">
-                  <span class="label">关联小区:</span>
-                  <ul class="text-[#1D2129] text-[12px] leading-[18px]">
-                    <li>1.黑龙江省佳木斯市向阳区市区光复路1546号2单元902号</li>
-                    <li>2.黑龙江省佳木斯市东风区胜利东路2单元902号</li>
-                  </ul>
-                </li>
-                <li class="flex justify-between">
-                  <span class="label">抄表员手机号:</span>
-                  <span>13699009900</span>
-                </li>
-                <li class="flex justify-between space-x-[8px]">
-                  <span class="label">用户备注:</span>
+                  <span>{{dataSource.counts}}</span>
+                </div>
+
+                <ul v-for="item, index in userInfos" :key="index" class="user-info_list">
+                  <li class="flex justify-between row">
+                    <span class="label">用户户号:</span>
+                    <span>{{ item.cardNo }}</span>
+                  </li>
+                  <li class="flex justify-between row">
+                    <!-- 问金龙 是否有该字段 -->
+                    <span class="label">欠费金额:</span> 
+                    <span>{{ item.waterFees }}</span>
+                  </li>
+                  <li class="flex justify-between space-x-[8px] row">
+                    <span class="label">关联小区:</span>
+                    <ul class="text-[#1D2129] text-[12px] leading-[18px]">
+                      <li>{{ item.pumpingStationAddress }}</li>
+                    </ul>
+                  </li>
+                  <li class="flex justify-between row">
+                    <span class="label">抄表员手机号:</span>
+                    <span>{{ item.phone }}</span>
+                  </li>
+                </ul>
+
+                <div class="flex justify-between space-x-[8px] row">
+                  <span class="label">备注:</span>
                   <div class="space-y-[8px]">
-                    <el-input type="textarea" :autosize="{ minRows: 5, maxRows: 6 }" resize="none" />
-                    <span class="save-btn cursor-pointer">保存备注</span>
+                    <el-input type="textarea" :autosize="{ minRows: 5, maxRows: 6 }" resize="none" v-model="dataSource.remark"/>
+                    <span class="save-btn cursor-pointer" @click="onSaveRemark">保存备注</span>
                   </div>
-                </li>
-              </ul>
+                </div>
+              </div>
             </el-scrollbar>
           </div>
           <div class="right-content">
-            <el-tabs v-model="activeName" class="right-tabs" @tab-click="handleClick">
+            <el-tabs v-model="activeName" class="right-tabs">
               <el-tab-pane label="当前通话记录" name="currentRecord" v-if="!isMakingCall">
                 <el-scrollbar style="height: 100%; padding: 14px; background: #fff; border-radius: 8px;">
                   <ul class="text-[12px]">
-                    <li>
-                      供水公司客服:您好,欢迎拨打XX市供水公司客服热线,我是客服代表小张,请问有什么可以帮您?
-
-                        用户:你好,我家里的水压最近特别低,想咨询一下是什么原因?
-
-                        供水公司客服:您好,非常感谢您对我们工作的关注。请您告诉我一下您所在的小区名称和具体楼号,我帮您查询一下。
-
-                        用户:好的,我家在阳光小区3号楼。
-
-                        供水公司客服:阳光小区3号楼,好的,请您稍等,我马上为您查询。
-
-                        供水公司客服:您好,经过查询,近期我们并没有接到关于阳光小区3号楼水压低的报修。请问您家水压低的情况是持续性的还是偶尔出现?有没有规律可循?
-
-                        用户:最近一个星期都比较低,尤其是早上和晚上用水高峰期。
-
-                        供水公司客服:明白了,可能是用水高峰期导致供水压力不足。我们会安排维修人员到现场查看,争取尽快解决问题。请问您方便提供一下联系方式吗?以便我们及时与您沟通。
-
-                        用户:好的,我电话是138XXXX1234。
-
-                        供水公司客服:好的,已记录您的联系方式。我们会尽快安排维修人员上门检查,预计在2个工作日内,请您保持电话畅通。另外,如果问题紧急,您可以尝试联系物业或邻居了解是否也有类似情况,以便共同解决。
-
-                        用户:好的,谢谢。
-
-                        供水公司客服:不客气,给您带来不便,敬请谅解。我们会尽快为您解决问题。如果您还有其他疑问或需求,请随时拨打我们的客服热线。祝您生活愉快,再见!
-
-                        用户:再见!
-                        
+                    <li v-for="item in currentCallRecords">
+                      <span v-for="val in item">{{ val }}</span>
                     </li>
                   </ul>
                 </el-scrollbar>
@@ -139,9 +153,12 @@ const handleCallAnswered = () => {
               <el-tab-pane label="停水信息" name="information">
                 <el-scrollbar style="height: 100%;">
                   <ul class="info-list space-y-[8px]">
-                    <li class="px-[14px] py-[14px] rounded-[8px] bg-white space-y-[4px]" v-for="item in 10">
+                    <li class="px-[14px] py-[14px] rounded-[8px] bg-white space-y-[4px]" v-for="item in cutOffWaters" :key="item.id">
                       <p class="text-[#000] text-[14px] font-bold leading-[20px]">停水信息:</p>
-                      <p class="text-[#4E5969] text-[12px] leading-[18px]">黑龙江省佳木斯市东风区胜利东路2单元902号</p>
+                      <p class="text-[#4E5969] text-[12px] leading-[18px]">原因:{{ item.reason }}</p>
+                      <p class="text-[#4E5969] text-[12px] leading-[18px]">停水时间:{{ item.timeBegin }}</p>
+                      <p class="text-[#4E5969] text-[12px] leading-[18px]">恢复时间:{{ item.timeEnd }}(预计)</p>
+                      <p class="text-[#4E5969] text-[12px] leading-[18px]">与业主相关的小区:{{ item.neighbourhoodName }}</p>
                     </li>
                   </ul>
                 </el-scrollbar>
@@ -149,56 +166,29 @@ const handleCallAnswered = () => {
               <el-tab-pane label="通话记录" name="record">
                 <el-scrollbar style="height: 100%;">
                   <ul class="info-list space-y-[8px]">
-                    <li class="px-[14px] py-[14px] rounded-[8px] bg-white space-y-[4px]" v-for="item in 10">
+                    <li class="px-[14px] py-[14px] rounded-[8px] bg-white space-y-[4px]" v-for="item in callRecords" :key="item.id">
                       <p class="text-[#000] text-[14px] font-bold leading-[20px]">2024-08-26 12:12:22</p>
                       <ul class="text-[#4E5969] text-[12px] leading-[18px] space-y-[4px]">
                         <li class="flex">
-                          <span class="w-[100px]">通话状态:</span>
-                          <span class="w-[100px] text-[#1D2129]">123123</span>
+                          <span class="w-[100px]">通话状态:</span> 
+                          <span class="w-[100px] text-[#1D2129]">{{ item.status === 0 ? '未接听' : '已接通' }}</span>
                         </li>
                         <li class="flex">
-                          <span class="w-[100px]">通话关键词:</span>
-                          <span class="flex-1 text-[#1D2129]">123123</span>
+                          <span class="w-[100px]">通话关键词:</span> 
+                          <span class="flex-1 text-[#1D2129]">{{ item.bussinessType }}</span>
                         </li>
                         <li class="flex">
                           <span class="w-[100px]">备注:</span>
                           <span class="flex-1 text-[#1D2129]">
-                            该用户小区停水, 用户小区停水, 目前没有任何公告和消息说明停水原因,目前没有任何公告和消息说明停水原因,目前没有任何公告和消息说明停水原因, 已经派师傅上门处理 已经派师傅上门处理
+                            {{ item.remark }}
                           </span>
                         </li>
                         <li class="flex">
                           <span class="w-[100px]">通话录音:</span>
-                          <div class="flex-1 text-[#1D2129]">
-                            <div class="record-box">
-                              <div class="record-play-control space-x-[14px]">
-                                <AudioPlayer></AudioPlayer>
-                                <span class="text-[#165DFF] text-[12px] cursor-pointer" @click="isExpand = !isExpand">{{ isExpand ?
-                                  '收起文字' : '转文字'
-                                  }}</span>
-                              </div>
-                              <div class="record-play-content" v-show="isExpand">
-                                供水公司客服:您好,欢迎拨打XX市供水公司客服热线,我是客服代表小张,请问有什么可以帮您?
-                                用户:你好,我家里的水压最近特别低,想咨询一下是什么原因?
-                                供水公司客服:您好,非常感谢您对我们工作的关注。请您告诉我一下您所在的小区名称和具体楼号,我帮您查询一下。
-                                用户:好的,我家在阳光小区3号楼。
-                                供水公司客服:阳光小区3号楼,好的,请您稍等,我马上为您查询。
-                                (等待片刻)
-                                供水公司客服:您好,经过查询,近期我们并没有接到关于阳光小区3号楼水压低的报修。请问您家水压低的情况是持续性的还是偶尔出现?有没有规律可循?
-                                用户:最近一个星期都比较低,尤其是早上和晚上用水高峰期。
-                                供水公司客服:明白了,可能是用水高峰期导致供水压力不足。我们会安排维修人员到现场查看,争取尽快解决问题。请问您方便提供一下联系方式吗?以便我们及时与您沟通。
-                              </div>
-                            </div>
-                          </div>
+                          <VoiceToText @on-parsed="handleVoiceParsed" :content="item.parsedVoiceContent" :id="item.id">
+                            <AudioPlayer :audioUrl="item.url"></AudioPlayer>
+                          </VoiceToText>
                         </li>
-                        
-                        <!-- <CustomRowItem label="通话状态">
-                          <span>正常</span>
-                        </CustomRowItem>
-                        <CustomRowItem label="通话关键词">
-                          <span>停水咨询</span>
-                        </CustomRowItem>
-                        <CustomRowItem label="通话录音">
-                        </CustomRowItem> -->
                       </ul>
                     </li>
                   </ul>
@@ -292,7 +282,7 @@ const handleCallAnswered = () => {
 
       .tel-body_inner {
         display: flex;
-        height: 400px;
+        height: 500px;
 
         .left-content {
           flex-shrink: 0;
@@ -304,7 +294,12 @@ const handleCallAnswered = () => {
           .record-list {
             padding: 16px;
 
-            li {
+            .user-info_list:not(:last-child) {
+              padding-bottom: 10px;
+              border-bottom: 1px solid #dcdfe6;
+            }
+
+            .row {
               font-size: 12px;
               line-height: 18px;
 
@@ -354,27 +349,7 @@ const handleCallAnswered = () => {
 
                 .info-list {
                   border-radius: 8px;
-                  .record-box {
-                    width: 100%;
-                    padding: 16px;
-                    border-radius: 8px;
-                    background: linear-gradient(90deg, #F6F5F8 0%, #FFF 100%);
-
-                    .record-play-control {
-                      display: flex;
-                      align-items: center;
-                      padding: 0;
-                      font-size: 12px;
-                    }
-
-                    .record-play-content {
-                      padding-top: 12px;
-                      // color: #4E5969;
-                      font-family: "PingFang SC";
-                      font-size: 13px;
-                      line-height: 20px;
-                    }
-                  }
+                 
                 }
               }
             }

+ 12 - 4
src/layout/index.vue

@@ -4,14 +4,22 @@ import Sidebar from './components/Sidebar'
 import TelNoticeBar from './components/TelNoticeBar'
 import TelNoticeBox from './components/TelNoticeBox'
 import { AppMain } from './components'
-import { onMounted } from 'vue';
+import useVoiceStore from "@/store/modules/voice";
+import useUserStore from "@/store/modules/user";
+import { workbenchApi } from '@/api/voice/workbench';
 
+const useVoice = useVoiceStore();
+const useUser = useUserStore();
 
 onMounted(() => {
+  if ( useVoice.isAuthPane ) {
 
-  // const { Scene, getInstance, LoggerLevels ,CTIEvent} = window.HS_CTI
-  // console.log( Scene );
-
+    workbenchApi.getSeatsByUser({ id: useUser.id }).then(res => {
+      // 用于初始化客户id
+      console.log("res", res);
+    })
+    // useVoice.HS_CTI_INSTANCE(useUser.agentId);
+  }
 })
 </script>
 

+ 4 - 2
src/main.js

@@ -9,6 +9,7 @@ import locale from 'element-plus/es/locale/lang/zh-cn'
 import '@/assets/styles/index.scss' // global css
 import '@/assets/styles/tailwind.css'
 
+
 import App from './App'
 import store from './store'
 import router from './router'
@@ -16,7 +17,7 @@ import directive from './directive' // directive
 
 // 注册指令
 import plugins from './plugins' // plugins
-import { download } from '@/utils/request'
+import { download, getDownload } from '@/utils/request'
 
 // svg图标
 import 'virtual:svg-icons-register'
@@ -50,6 +51,7 @@ const app = createApp(App)
 // 全局方法挂载
 app.config.globalProperties.useDict = useDict
 app.config.globalProperties.download = download
+app.config.globalProperties.getDownload = getDownload
 app.config.globalProperties.parseTime = parseTime
 app.config.globalProperties.resetForm = resetForm
 app.config.globalProperties.handleTree = handleTree
@@ -82,4 +84,4 @@ app.use(ElementPlus, {
   size: Cookies.get('size') || 'default'
 })
 
-app.mount('#app')
+app.mount('#app')

+ 3 - 2
src/store/modules/user.js

@@ -11,7 +11,8 @@ const useUserStore = defineStore(
       name: '',
       avatar: '',
       roles: [],
-      permissions: []
+      permissions: [],
+      agentId: ''
     }),
     actions: {
       // 登录
@@ -36,7 +37,6 @@ const useUserStore = defineStore(
           getInfo().then(res => {
             const user = res.user
             const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar;
-
             if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
               this.roles = res.roles
               this.permissions = res.permissions
@@ -46,6 +46,7 @@ const useUserStore = defineStore(
             this.id = user.userId
             this.name = user.userName
             this.avatar = avatar
+            this.agentId = user.outId
             resolve(res)
           }).catch(error => {
             reject(error)

+ 124 - 4
src/store/modules/voice.js

@@ -1,9 +1,15 @@
 import { ref } from 'vue';
 import { Timer } from '@/utils/timer';
 import { ElMessage } from 'element-plus'
+import usePermissionStore from './permission';
+
 
 const useVoiceStore = defineStore('voice', () => {
 
+  const usePermission = usePermissionStore();
+
+  let HS_CTI = null;
+
   // 是否是拨打电话   true: 外呼    false: 来电
   const isMakingCall = ref(false);
 
@@ -13,7 +19,7 @@ const useVoiceStore = defineStore('voice', () => {
   const callDialing = ref(false);
 
   // 横条来电显示
-  const noiceBarVisibleState = ref(false);
+  const noiceBarVisibleState = ref(true);
   // 盒子来电显示
   const noiceBoxVisibleState = ref(false);
   
@@ -24,7 +30,10 @@ const useVoiceStore = defineStore('voice', () => {
   // 开始时间
   const startTime = '';
   const endTime = '';
-  
+
+  // 是否有拨打电话权限
+  const isAuthPane = computed(() => usePermission.routes.findIndex(({ name }) => name === 'Console') !== -1);
+
   // 拨打电话
   const onMakingCall = (phoneNum) => {
     if ( callAnswered.value ) {
@@ -37,7 +46,6 @@ const useVoiceStore = defineStore('voice', () => {
     isMakingCall.value = true;
 
     noiceBarVisibleState.value = true;
-
   }
 
   // 接听电话
@@ -64,7 +72,116 @@ const useVoiceStore = defineStore('voice', () => {
     // TODO 这里需要补充其他逻辑
   }
 
+  // 置忙
+  const setBusy = () => {
+    HS_CTI.setBusy().then(res => { console.log(res) })
+  }
+
+  // 置闲
+  const setIdle = () => {
+    HS_CTI.setIdle().then(res => { console.log(res) })
+  }
+
+  // 获取坐席状态
+  const getAgentStatus = () => {
+    HS_CTI.getAgentStatus().then(res => { console.log(res) })
+  }
+
+  // 主动外呼
+  const makeCall = () => {
+    HS_CTI.makeCall({ called: '' }).then(res => { console.log(res) })
+  }
+
+  // 接听电话
+  const answer = () => {
+    HS_CTI.answer().then(res => { console.log(res) })
+  }
+
+  // 挂断电话
+  const bye = () => {
+    HS_CTI.bye().then(res => { console.log(res) })
+  }
+
+  // 卸载实例
+  const unInit = () => {
+    HS_CTI.unInit()
+  }
+
+  // 下面开始事件监听
+  const listenScoketEvent = () => {
+    HS_CTI.on(CTIEvent.OnAgentWorkReport, ({ workStatus, description }) => {
+      message.info(`OnAgentWorkReport: ${workStatus}: ${description}`)
+
+      // 销毁实例调用签出接口成功后 - 坐席签出
+      if ( workStatus === -1 ) {
+        ElMessage({
+          message: '坐席签出成功',
+          type: 'success',
+          plain: true,
+        })
+      }
+
+      // 登录CTI 成功
+      if ( workStatus === 0 ) {
+        ElMessage({
+          message: '坐席登入成功',
+          type: 'success',
+          plain: true,
+        })
+      }
+
+      // 登录CTI 成功
+      if ( workStatus === 2 ) {
+        ElMessage({
+          message: '登入成功',
+          type: 'success',
+          plain: true,
+        })
+      }
+
+      // 调用置闲接口成功后
+      if ( workStatus === 2 ) {
+        ElMessage({
+          message: '坐席状态变更为:置闲',
+          type: 'success',
+          plain: true,
+        })
+      }
+
+      // 调用置忙接口成功后
+      if ( workStatus === 3 ) {
+        ElMessage({
+          message: '坐席状态变更为:置忙',
+          type: 'success',
+          plain: true,
+        })
+      }
+    })
+  }
+
+  // 初始化 通话实例
+  const HS_CTI_INSTANCE = (agentId) => {
+    const { Scene, getInstance, LoggerLevels , CTIEvent} = window.HS_CTI;
+
+    HS_CTI = getInstance({
+      // 业务返回的坐席outId
+      agent_id,
+      // 根据城市可能不一样,
+      saas_id:'mdj',
+      // 业务场景详见 Scene 枚举,
+      scene: Scene.Manual,
+      // SDK 日志等级
+      loggerLevel: LoggerLevels.debug,
+      // 环境变量
+      env: 'development'
+    })
+
+    HS_CTI.init()
+  }
+
   return {
+    isAuthPane,
+
     callTime,
     isMakingCall,
     
@@ -75,7 +192,10 @@ const useVoiceStore = defineStore('voice', () => {
     callAnswered,
     callDialing,
     noiceBarVisibleState,
-    noiceBoxVisibleState
+    noiceBoxVisibleState,
+
+    // 通话相关
+    HS_CTI_INSTANCE,
   }
 })
 

+ 27 - 1
src/utils/request.js

@@ -17,7 +17,7 @@ const service = axios.create({
   // axios中请求配置有baseURL选项,表示请求URL公共部分
   baseURL: import.meta.env.VITE_APP_BASE_API,
   // 超时
-  timeout: 10000
+  timeout: 1000000
 })
 
 // request拦截器
@@ -149,4 +149,30 @@ export function download(url, params, filename, config) {
   })
 }
 
+
+// 通用下载方法 - get
+export function getDownload(url, params, filename) {
+  downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
+  return service.get(url + '?' + tansParams(params), {
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+    responseType: 'blob',
+  }).then(async (data) => {
+    const isBlob = blobValidate(data);
+    if (isBlob) {
+      const blob = new Blob([data])
+      saveAs(blob, filename)
+    } else {
+      const resText = await data.text();
+      const rspObj = JSON.parse(resText);
+      const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+      ElMessage.error(errMsg);
+    }
+    downloadLoadingInstance.close();
+  }).catch((r) => {
+    console.error(r)
+    ElMessage.error('下载文件出现错误,请联系管理员!')
+    downloadLoadingInstance.close();
+  })
+}
+
 export default service

+ 144 - 46
src/views/voice/analyse/index.vue

@@ -1,7 +1,13 @@
 <script setup>
+import { analyseApi } from '@/api/voice/analyse';
 import BaseLayoutViewport from '@/components/BaseLayout';
 const total = ref(0);
 const value1 = ref('');
+
+const callRecordCountInfo = ref({});
+const userTableData = ref([]);
+const robotTableData = ref([]);
+
 const queryParams = ref({
   pageNum: 1,
   pageSize: 10
@@ -108,6 +114,31 @@ const tableData = [
     address: 'No. 189, Grove St, Los Angeles',
   },
 ]
+
+onMounted(() => {
+  analyseApi.getCallRecordCountInfo().then(({ data }) => {
+    callRecordCountInfo.value = data;
+  })
+
+  analyseApi.getUserCallCount().then(({ data }) => {
+    const statusEnum = {
+      1: '置忙',
+      2: '置闲',
+      3: '通话中',
+      4: '后处理',
+      5: '拨号中'
+    }
+    userTableData.value = data.map(item => ({
+      ...item,
+      statusText: statusEnum[item.status]
+    }));
+  })
+
+  analyseApi.getRobotCallCount().then(({ data }) => {
+    robotTableData.value = data;
+  })
+
+})
 </script>
 
 <template>
@@ -120,7 +151,7 @@ const tableData = [
             <div class="analyse-item-box">
               <p class="card-title">累计电话量</p>
               <div class="pt-[38px]">
-                <span class="num">4000</span>
+                <span class="num">{{ callRecordCountInfo.total }}</span>
               </div>
             </div>
             <div class="analyse-item-box">
@@ -128,32 +159,61 @@ const tableData = [
               <ul class="flex items-center justify-center pt-[28px]">
                 <li>
                   <span class="text">电话量</span>
-                  <span class="num">4000</span>
+                  <span class="num">{{ callRecordCountInfo.humanTotal }}</span>
                 </li>
                 <li class="line"></li>
                 <li>
                   <span class="text">总量占比</span>
-                  <span class="num">90%</span>
+                  <span class="num">{{ callRecordCountInfo.humanPercent }}</span>
                 </li>
               </ul>
             </div>
             <div class="analyse-item-box right-box">
               <p class="card-title">AI客服</p>
               <div class="flex justify-around">
-                <div v-for="itenm in 3">
+                <div>
                   <p class="card-sub-title">白名单直呼</p>
                   <ul class="flex items-center justify-center pt-[5px]">
                     <li>
                       <span class="text">电话量</span>
-                      <span class="num">4000</span>
+                      <span class="num">{{ callRecordCountInfo.whiteListTotal }}</span>
                     </li>
                     <li class="line"></li>
                     <li>
                       <span class="text">总量占比</span>
-                      <span class="num">90%</span>
+                      <span class="num">{{ callRecordCountInfo.whiteListPercent }}</span>
+                    </li>
+                  </ul>
+                  <p class="count-num pt-[8px]">12345转入次数 <span class="text-[#65C734] font-bold">{{ callRecordCountInfo.specialCount }}</span></p>
+                </div>
+                <div>
+                  <p class="card-sub-title">AI机器人</p>
+                  <ul class="flex items-center justify-center pt-[5px]">
+                    <li>
+                      <span class="text">电话量</span>
+                      <span class="num">{{ callRecordCountInfo.aiTotal }}</span>
+                    </li>
+                    <li class="line"></li>
+                    <li>
+                      <span class="text">总量占比</span>
+                      <span class="num">{{ callRecordCountInfo.aiPercent }}</span>
+                    </li>
+                  </ul>
+                  <p class="count-num pt-[8px]">转人工次数 <span class="text-[#65C734] font-bold">{{ callRecordCountInfo.transferCount }}</span></p>
+                </div>
+                <div>
+                  <p class="card-sub-title">传统服务</p>
+                  <ul class="flex items-center justify-center pt-[5px]">
+                    <li>
+                      <span class="text">电话量</span>
+                      <span class="num">{{ callRecordCountInfo.traditionTotal }}</span>
+                    </li>
+                    <li class="line"></li>
+                    <li>
+                      <span class="text">总量占比</span>
+                      <span class="num">{{ callRecordCountInfo.traditionPercent }}</span>
                     </li>
                   </ul>
-                  <p class="count-num pt-[8px]">12345转入次数 <span class="text-[#65C734] font-bold">22</span></p>
                 </div>
               </div>
             </div>
@@ -163,9 +223,33 @@ const tableData = [
         <div class="layout-card">
           <h4 class="title">人工客服状态</h4>
           <ul class="status-list">
-            <li class="status-item" v-for="item in 7">
-              <span class="text">在线</span>
-              <span class="num">4</span>
+            <li class="status-item">
+              <span class="text">登录总数</span>
+              <span class="num">{{ callRecordCountInfo.transferCount }}</span>
+            </li>
+            <li class="status-item">
+              <span class="text">置闲</span>
+              <span class="num">{{ callRecordCountInfo.idlePerson }}</span>
+            </li>
+            <li class="status-item">
+              <span class="text">置忙</span>
+              <span class="num">{{ callRecordCountInfo.busyPerson }}</span>
+            </li>
+            <li class="status-item">
+              <span class="text">通话中</span>
+              <span class="num">{{ callRecordCountInfo.onlinePerson }}</span>
+            </li>
+            <li class="status-item">
+              <span class="text">拨号中</span>
+              <span class="num">{{ callRecordCountInfo.dialPerson }}</span>
+            </li>
+            <li class="status-item">
+              <span class="text">累计通话</span>
+              <span class="num">{{ callRecordCountInfo.personCount }}</span>
+            </li>
+            <li class="status-item">
+              <span class="text">累计通话时长</span>
+              <span class="num">{{ callRecordCountInfo.personTotal }}</span>
             </li>
           </ul>
         </div>
@@ -173,10 +257,36 @@ const tableData = [
         <div class="layout-card">
           <h4 class="title">机器人客服状态</h4>
           <ul class="status-list robot-list">
-            <li class="status-item" v-for="item in 7">
+            <li class="status-item">
               <div class="status-item-inner">
                 <span class="text">在线</span>
-                <span class="num">4</span>
+                <span class="num">{{ callRecordCountInfo.robotOnLineCount }}</span>
+              </div>
+            </li>
+            <li class="status-item">
+              <div class="status-item-inner">
+                <span class="text">异常</span>
+                <span class="num">{{ callRecordCountInfo.robotExceptionCount }}</span>
+              </div>
+            </li>
+            <li class="status-item">
+              <div class="status-item-inner">
+                <span class="text">转人工</span>
+                <span class="num">{{ callRecordCountInfo.robotTransfer }}</span>
+                <span class="text-[14px]"> 次</span>
+              </div>
+            </li>
+            <li class="status-item">
+              <div class="status-item-inner">
+                <span class="text">累计通话</span>
+                <span class="num">{{ callRecordCountInfo.robotCount }}</span>
+              </div>
+            </li>
+            <li class="status-item">
+              <div class="status-item-inner">
+                <span class="text">累计通话时长</span>
+                <span class="num">{{ callRecordCountInfo.robotTotal }}</span>
+                <span class="text-[14px]"> h</span>
               </div>
             </li>
           </ul>
@@ -185,37 +295,29 @@ const tableData = [
         <div class="layout-card">
           <h4 class="title">人工客服统计</h4>
           
-          <el-table :data="tableData" style="width: 100%" >
-            <el-table-column label="序号" align="center" type="index" width="50" fixed/>
-            <el-table-column prop="name" label="通话类型" align="center" />
-            <el-table-column prop="address" label="通话状态" align="center">
-              <template #default="scope">
-                <div class="flex items-center justify-center space-x-[6px]">
-                  <span class="w-[6px] h-[6px] bg-[#65C734] rounded-full"></span>
-                  <span>已接通</span>
-                </div>
-              </template>
-            </el-table-column>
-            <el-table-column prop="address" label="用户电话" align="center" />
-            <el-table-column prop="address" label="客服" align="center" />
-            <el-table-column prop="address" label="服务类型" align="center" />
-            <el-table-column prop="address" label="通话录音" align="center" width="350">
-              <template #default="scope">
-                <div class="flex justify-center">
-                  <AudioPlayer></AudioPlayer>
-                </div>
-              </template>
-            </el-table-column>
-            <el-table-column prop="address" label="通话发起时间" align="center" />
-            <el-table-column prop="address" label="操作" align="center" fixed="right"/>
+          <el-table :data="userTableData" style="width: 100%" >
+            <el-table-column prop="userId" label="客服编号" align="center" width="150" fixed/>
+            <el-table-column prop="userName" label="姓名" align="center" />
+            <el-table-column prop="status" label="当前状态" align="center" />
+            <el-table-column prop="inTodayCount" label="今日呼入" align="center" />
+            <el-table-column prop="inAllCount" label="累计呼入" align="center" />
+            <el-table-column prop="outTodayCount" label="今日呼出" align="center" />
+            <el-table-column prop="outAllCount" label="累计呼出" align="center" />
+            <el-table-column prop="totalTimes" label="累计通话时长" align="center" />
+          </el-table>
+        </div>
+
+        <div class="layout-card">
+          <h4 class="title">机器人客服统计</h4>
+          
+          <el-table :data="robotTableData" style="width: 100%" >
+            <el-table-column prop="userId" label="客服编号" align="center" width="150" fixed/>
+            <el-table-column prop="userName" label="客服名称" align="center" />
+            <el-table-column prop="inTodayCount" label="今日呼入" align="center" />
+            <el-table-column prop="inAllCount" label="累计呼入" align="center" />
+            <el-table-column prop="没有" label="今日转人工" align="center" />
+            <el-table-column prop="没有" label="累计转人工" align="center" />
           </el-table>
-          <pagination
-            v-show="total >= 0"
-            :total="total"
-            v-model:page="queryParams.pageNum"
-            v-model:limit="queryParams.pageSize"
-            @pagination="getList"
-          />
         </div>
       </div>
     </el-scrollbar>
@@ -227,14 +329,10 @@ const tableData = [
   background: #f1f5fd;
   .layout-card {
     width: 100%;
-    // height: 100%;
     padding: 20px;
     border-radius: 8px;
     background: #fff;
 
-    // &:last-child {
-    //   padding-bottom: 0px;
-    // }
   }
 
   .title {

+ 15 - 2
src/views/voice/call/details.vue

@@ -1,12 +1,25 @@
 <script setup>
-import { useRouter } from 'vue-router';
+import { useRouter, useRoute } from 'vue-router';
+import { workbenchApi } from '@/api/voice/workbench';
 import CallView from '@/components/CallView';
 
 const router = useRouter();
+const route = useRoute();
+const callDetails = ref({});
 
 const handleGoBack = () => {
   router.push('/voice/call');
 }
+
+const handleVoiceParsed = ({ parsedVoiceContent }) => {
+  callDetails.value.parsedVoiceContent = parsedVoiceContent
+}
+
+onMounted(async () => {
+  const { id } = route.query;
+  const { data } = await workbenchApi.getCallRecordDetails(id);
+  callDetails.value = data;
+});
 </script>
 
 <template>
@@ -14,7 +27,7 @@ const handleGoBack = () => {
     <div class="details-wrapper">
       <h4 class="title">通话详情</h4>
       <el-scrollbar class="details-scrollbar">
-        <CallView></CallView>
+        <CallView :data="callDetails" @on-end="handleVoiceParsed"></CallView>
       </el-scrollbar>
       <ul class="flex justify-center">
         <li class="custom-btn custom-btn_default" @click="handleGoBack">返回</li>

+ 26 - 7
src/views/voice/call/index.vue

@@ -6,6 +6,7 @@ import AudioPlayer from '@/components/AudioPlayer';
 import useTableHeight from '@/composables/useTableHeight';
 
 const router = useRouter();
+const { proxy } = getCurrentInstance();
 const { tableContainer, tableMaxHeight } = useTableHeight();
 
 const value = ref('');
@@ -14,12 +15,12 @@ const dataPickerValue = ref([]);
 const loading = ref(false);
 const tableData = ref([]);
 const total = ref(0);
-const options = [];
+const agentList = ref([]);
 
 const queryParams = ref({
   pageNum: 1,
   pageSize: 10,
-  userName: '',
+  userId: '',
   status: '',
   phone: '',
 })
@@ -29,7 +30,7 @@ const handleCleanOptions = () => {
   queryParams.value = {
     pageNum: 1,
     pageSize: 10,
-    userName: '',
+    userId: '',
     status: '',
     phone: '',
   };
@@ -44,6 +45,19 @@ const jumpDetails = ({ id }) => {
   })
 }
 
+// 批量下载
+const handleBatchDownload = () => {
+  const [timeBegin, timeEnd] = dataPickerValue.value;
+  proxy.getDownload("/business/record/downloadBatchByCondition", {
+    ...queryParams.value, timeBegin, timeEnd
+  }, `${new Date().getTime()}.zip`);
+}
+
+// 单独下载
+const handleDownload = ({ id }) => {
+  proxy.getDownload("/business/record/downloadById", { id }, `${new Date().getTime()}.wav`);
+}
+
 const getList = () => {
   const [timeBegin, timeEnd] = dataPickerValue.value;
 
@@ -66,6 +80,9 @@ const getList = () => {
 };
 
 onMounted(() => {
+  workbenchApi.getAgentList().then(({ data }) => {
+    agentList.value = data;
+  })
   getList();
 })
 
@@ -82,9 +99,9 @@ onMounted(() => {
         </el-col>
         <el-col :span="6">
           <SearchItemWrapper label="客服">
-            <el-select v-model="queryParams.userName" placeholder="请选择" size="large" :empty-values="[null, undefined]">
+            <el-select v-model="queryParams.userId" placeholder="请选择" size="large" :empty-values="[null, undefined]">
               <el-option label="全部" value="" />
-              <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+              <el-option v-for="item in agentList" :key="item.id" :label="item.name" :value="item.id" />
             </el-select>
           </SearchItemWrapper>
         </el-col>
@@ -117,7 +134,9 @@ onMounted(() => {
         <el-col :span="18">
           <div class="flex items-center justify-start space-x-[30px]">
             <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
-            <div class="custom-btn custom-btn_default">批量下载语音</div>
+            <div class="custom-btn custom-btn_default" @click="handleBatchDownload">
+              批量下载语音 
+            </div>
             <div class="custom-btn custom-btn_text space-x-[2px]" @click="handleCleanOptions">
               <img src="@/assets/images/workbench/icon-clean.svg" alt="">
               <span>清除条件</span>
@@ -155,7 +174,7 @@ onMounted(() => {
             <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">语音下载</span>
+                <span class="text-[#165DFF] cursor-pointer" @click="handleDownload(scope.row)">语音下载</span>
               </div>
             </template>
           </el-table-column>

+ 166 - 86
src/views/voice/notice/add.vue

@@ -1,43 +1,113 @@
 <script setup>
+import { useRouter, useRoute } from 'vue-router';
+import { servicesApi } from '@/api/voice/services';
+import { noticeApi } from '@/api/voice/notice';
+import { ElMessage } from 'element-plus'
 import BaseLayoutViewport from '@/components/BaseLayout';
 
-const form = reactive({
-  name: '',
-  region: '',
-  date1: '',
-  date2: '',
-  delivery: false,
-  type: [],
-  resource: '',
-  desc: '',
+const router = useRouter();
+const route = useRoute();
+
+
+const formRef = ref(null);
+const pumpingValue = ref(null);
+const housingValue = ref(null);
+const pumpingOptions = ref([]);
+const housingOptions = ref([]);
+
+const formData = ref({
+  timeBegin: '',
+  timeEnd: '',
+  reason: ''
 })
 
-const tableData = [
-  {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-02',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-04',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-01',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },{
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-]
+const tableData = ref([]);
+
+const rules = reactive({
+  timeBegin: [
+    { required: true, message: '请选择停水时间', trigger: 'blur' },
+  ],
+  reason: [
+    { required: true, message: '请输入停水原因', trigger: 'blur' },
+  ]
+});
+
+// 删除
+const handleDelRow = (row) => {
+  tableData.value.splice(row.$index, 1);
+}
+
+// 添加泵站
+const addTableData = async (type) => {
+  if ( type == 0 && !pumpingValue.value) {
+    return ElMessage.warning("请先选择泵站");
+  }
+  if ( type == 1 && !housingValue.value) {
+    return ElMessage.warning("请先选择小区");
+  }
+  const { data } = await noticeApi.getExtraListByType({ type, id: type == 0 ? pumpingValue.value : housingValue.value })
+  
+  const combined = tableData.value.concat(data);
+
+  const unique = Array.from(new Map(combined.map(item => [JSON.stringify([item.neighbourhoodId, item.pumpingStationId]), item])).values());
+
+  tableData.value = unique;
+}
+
+// 最终提交
+const submitForm = async () => {
+  if (!formRef.value) return;
+  await formRef.value.validate(async (valid) => {
+    if (valid) {
+      if ( !tableData.value.length ) {
+        return ElMessage.warning("请先选择停水范围");
+      }
+      const id = route.query.id;
+      if (id) {
+        await noticeApi.putWater({...formData.value, extraResList: tableData.value}).then(() => {
+          ElMessage.success("停水公告修改成功");
+        })
+      } else {
+        await noticeApi.postWater({...formData.value, extraResList: tableData.value}).then(() => {
+          ElMessage.success("停水公告添加成功");
+        })
+      }
+      goBack();
+    }
+  })
+}
+
+const goBack = () => {
+  router.push('/voice/notice')
+}
+
+onMounted(() => {
+  const id = route.query.id;
+  if ( id ) {
+    noticeApi.getWater(id).then(({ data }) => {
+      const { timeBegin, timeEnd, reason, extraResList } = data;
+      formData.value = {
+        timeBegin,
+        timeEnd,
+        reason,
+        id
+      }
+      tableData.value = extraResList;
+    })
+  }
+
+
+  // 泵站
+  servicesApi.getPumpingList().then(res => {
+    pumpingOptions.value = res.data;
+  })
+  // 小区
+  servicesApi.getNeighborhoodList().then(res => {
+    housingOptions.value = res.rows;
+  })
+
+
+})
 </script>
 
 <template>
@@ -55,58 +125,72 @@ const tableData = [
       </div>
   
       <div class="pt-[20px]">
-        <el-form :model="form" label-width="auto" label-position="left" style="width: 780px;">
+        <el-form :model="formData" :rules="rules" ref="formRef" label-width="auto" label-position="left" style="width: 900px;">
           <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="停水时间">
-                <el-input v-model="form.name" />
+            <el-col :span="24" style="display: flex; align-items: flex-start; justify-content: space-between;">
+              <el-form-item label="停水时间" prop="timeBegin">
+                <div>
+                  <el-date-picker
+                    v-model="formData.timeBegin"
+                    type="datetime"
+                    placeholder="请选择停水时间"
+                    style="width: 280px;"
+                    value-format="YYYY-MM-DD HH:mm:ss"
+                  />
+                </div>
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="恢复供水时间">
-                <el-input v-model="form.name" />
-                <p class="text-[12px] text-[#999]">如无法确定恢复供水时间, 则不填写</p>
+              <el-form-item label="恢复供水时间" prop="timeEnd">
+                <div>
+                  <el-date-picker
+                    v-model="formData.timeEnd"
+                    type="datetime"
+                    placeholder="请选择恢复供水时间"
+                    style="width: 280px;"
+                    value-format="YYYY-MM-DD HH:mm:ss"
+                  />
+                  <p class="h-[22px] text-[12px] text-[#999]">如无法确定恢复供水时间, 则不填写</p>
+                </div>
               </el-form-item>
             </el-col>
             <el-col :span="24">
-              <el-form-item label="停水原因">
-                <el-input v-model="form.name" type="textarea" :autosize="{ minRows: 8, maxRows: 6 }" resize="none"/>
+              <el-form-item label="停水原因" prop="reason">
+                <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="停水范围">
+              <el-form-item label="停水范围" required>
                 <ul class="select-group">
                   <li class="space-x-[5px]">
                     <span>根据泵站添加:</span>
                     <el-select
-                      v-model="value"
-                      placeholder="Select"
-                      style="width: 148px;"
+                      v-model="pumpingValue"
+                      placeholder="请选择"
+                      style="width: 200px;"
                     >
                       <el-option
-                        v-for="item in options"
-                        :key="item.value"
-                        :label="item.label"
-                        :value="item.value"
+                        v-for="item in pumpingOptions"
+                        :key="item.id"
+                        :label="item.name"
+                        :value="item.id"
                       />
                     </el-select>
-                    <div class="custom-btn custom-btn_primary">添加</div>
+                    <div class="custom-btn custom-btn_primary" @click="addTableData(0)">添加</div>
                   </li>
                   <li class="space-x-[5px]">
-                    <span>根据泵站添加:</span>
+                    <span>根据小区添加:</span>
                     <el-select
-                      v-model="value"
-                      placeholder="Select"
-                      style="width: 148px;"
+                      v-model="housingValue"
+                      placeholder="请选择"
+                      style="width: 200px;"
                     >
                       <el-option
-                        v-for="item in options"
-                        :key="item.value"
-                        :label="item.label"
-                        :value="item.value"
+                        v-for="item in housingOptions"
+                        :key="item.id"
+                        :label="item.name"
+                        :value="item.id"
                       />
                     </el-select>
-                    <div class="custom-btn custom-btn_primary">添加</div>
+                    <div class="custom-btn custom-btn_primary" @click="addTableData(1)">添加</div>
                   </li>
                 </ul>
               </el-form-item>
@@ -116,36 +200,23 @@ const tableData = [
       </div>
   
       <div class="table-card">
-        <el-table :data="tableData" style="width: 100%">
-          <el-table-column label="序号" align="center" type="index" width="50" fixed/>
-          <el-table-column prop="name" label="通话类型" align="center" />
-          <el-table-column prop="address" label="通话状态" align="center">
-            <template #default="scope">
-              <div class="flex items-center justify-center space-x-[6px]">
-                <span class="w-[6px] h-[6px] bg-[#65C734] rounded-full"></span>
-                <span>已接通</span>
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column prop="address" label="用户电话" align="center" />
-          <el-table-column prop="address" label="客服" align="center" />
-          <el-table-column prop="address" label="服务类型" align="center" />
-          <el-table-column prop="address" label="通话录音" align="center" width="350">
+        <el-table :data="tableData" style="width: 100%" >
+          <el-table-column prop="pumpingStationName" label="所属泵站" align="center" />
+          <el-table-column prop="neighbourhoodName" label="小区名称" align="center"></el-table-column>
+          <el-table-column prop="neighbourhoodNumberNames" label="楼号" align="center" />
+          <el-table-column prop="neighbourhoodAddress" label="详细地址" align="center" />
+          <el-table-column prop="address" label="操作" align="center" width="150">
             <template #default="scope">
-              <div class="flex justify-center">
-                <AudioPlayer></AudioPlayer>
-              </div>
+              <span class="text-[#165DFF] cursor-pointer" @click="handleDelRow(scope)">删除</span>
             </template>
           </el-table-column>
-          <el-table-column prop="address" label="通话发起时间" align="center" />
-          <el-table-column prop="address" label="操作" align="center" fixed="right"/>
         </el-table>
       </div>
     </div>
     <template #footer>
       <ul class="flex justify-center space-x-[12px]">
-        <li class="custom-btn custom-btn_primary">保存</li>
-        <li class="custom-btn custom-btn_default">返回</li>
+        <li class="custom-btn custom-btn_primary" @click="submitForm">保存</li>
+        <li class="custom-btn custom-btn_default" @click="goBack">返回</li>
       </ul>
     </template>
   </BaseLayoutViewport>
@@ -160,6 +231,15 @@ const tableData = [
   line-height: 26px;
 }
 
+:deep(.el-date-editor) {
+  .el-input__wrapper {
+    height: 36px;
+  }
+  &.el-input {
+    height: 36px;
+  }
+}
+
 .reply-tips-card {
   display: flex;
   align-items: start;

+ 104 - 81
src/views/voice/notice/index.vue

@@ -1,66 +1,82 @@
 <script setup>
+import { ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
 import SearchItemWrapper from '@/components/SearchItemWrapper';
-import AudioPlayer from '@/components/AudioPlayer';
 import useTableHeight from '@/composables/useTableHeight';
+import { noticeApi } from '@/api/voice/notice';
 
 const { tableContainer, tableMaxHeight } = useTableHeight();
 
 const router = useRouter();
 
+const loading = ref(false);
+const dataPickerValue = ref([]);
 const total = ref(0);
-const value1 = ref('');
+const tableData = ref([]);
 const queryParams = ref({
   pageNum: 1,
-  pageSize: 10
+  pageSize: 10,
+  neighbourhoodName: '',  // 小区名称
+  status: ''              // 停水状态
 })
 
-const options = [
-  {
-    value: 'Option1',
-    label: 'Option1',
-  },
-  {
-    value: 'Option2',
-    label: 'Option2',
-  },
-  {
-    value: 'Option3',
-    label: 'Option3',
-  },
-  {
-    value: 'Option4',
-    label: 'Option4',
-  },
-  {
-    value: 'Option5',
-    label: 'Option5',
-  },
-]
-
-const tableData = [
-  {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-]
-
-function getList() {
-  console.log(queryParams.value);
-  // loading.value = true;
-  // listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {
-  //   loading.value = false;
-  //   userList.value = res.rows;
-  //   total.value = res.total;
-  // });
+const statusEnum = {
+  0: '未知',
+  1: '待停水',
+  2: '停水中',
+  3: '已恢复'
+}
+
+// 气泡弹窗 - 是否删除该项
+const onConfirm = async ({ id }) => {
+  await noticeApi.delWater(id);
+  ElMessage.success('删除停水公告成功');
+}
+
+const getList = async () => {
+  const [timeBegin, timeEnd] = dataPickerValue.value;
+
+  loading.value = true;
+
+  const { rows, total: t } = await noticeApi.getWaterList({...queryParams.value, timeBegin, timeEnd});
+
+  tableData.value = rows.map(item => ({
+    ...item,
+    statusText: statusEnum[item.status]
+  }));
+  
+  loading.value = false;
+  total.value = t;
 };
 
+const jumpDetails = ({ id }) => {
+  router.push({
+    path: "notice/add",
+    query: { id }
+  });
+}
+
+// 跳转 - 添加停水公告
 const handleJumpWaterAdd = () => {
-  console.log(123);
-  router.push("notice/add")
+  router.push("notice/add");
 }
 
+// 清除检索条件
+const handleCleanOptions = () => {
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    neighbourhoodName: '',  // 小区名称
+    status: ''              // 停水状态
+  };
+  dataPickerValue.value = [];
+  getList();
+}
+
+onMounted(() => {
+  getList();
+})
+
 </script>
 
 <template>
@@ -69,25 +85,27 @@ const handleJumpWaterAdd = () => {
       <el-row :gutter="24" class="mb-[24px]">
         <el-col :span="6">
           <SearchItemWrapper>
-            <el-input class="search-input" placeholder="用户电话号码"></el-input>
+            <el-input class="search-input" placeholder="小区名称" v-model="queryParams.neighbourhoodName"></el-input>
           </SearchItemWrapper>
         </el-col>
         <el-col :span="6">
-          <SearchItemWrapper label="客服">
-            <el-date-picker v-model="value1" type="daterange" range-separator="-" start-placeholder="起始日期"
-              end-placeholder="结束日期" style="width: 100%;" :editable="false" />
+          <SearchItemWrapper label="停水状态">
+            <el-select v-model="queryParams.status" placeholder="Select" size="large" :empty-values="[null, undefined]">
+              <el-option label="全部" value="" />
+              <el-option :label="val" :value="key" v-for="val, key in statusEnum" :key="val" />
+            </el-select>
           </SearchItemWrapper>
         </el-col>
         <el-col :span="6">
-          <SearchItemWrapper label="通话状态">
-            <el-date-picker v-model="value1" type="daterange" range-separator="-" start-placeholder="起始日期"
-              end-placeholder="结束日期" style="width: 100%;" :editable="false" />
+          <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="6">
           <div class="flex items-center justify-end space-x-[10px]">
-            <div class="custom-btn custom-btn_primary">搜索</div>
-            <div class="custom-btn custom-btn_default">重置</div>
+            <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
+            <div class="custom-btn custom-btn_default" @click="handleCleanOptions">重置</div>
           </div>
         </el-col>
       </el-row>
@@ -102,46 +120,51 @@ const handleJumpWaterAdd = () => {
 
     <div class="table-card">
       <div style="height: 100%;" ref="tableContainer">
-        
-        <el-table :data="tableData" style="width: 100%" :max-height="tableMaxHeight">
-          <el-table-column label="序号" align="center" type="index" width="50" fixed/>
-          <el-table-column prop="name" label="通话类型" align="center" />
-          <el-table-column prop="address" label="通话状态" align="center">
-            <template #default="scope">
-              <div class="flex items-center justify-center space-x-[6px]">
-                <span class="w-[6px] h-[6px] bg-[#65C734] rounded-full"></span>
-                <span>已接通</span>
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column prop="address" label="用户电话" align="center" />
-          <el-table-column prop="address" label="客服" align="center" />
-          <el-table-column prop="address" label="服务类型" align="center" />
-          <el-table-column prop="address" label="通话录音" align="center" width="350">
+        <el-table :data="tableData" style="width: 100%" :max-height="tableMaxHeight" v-loading="loading">
+          <el-table-column prop="reason" label="停水原因" align="center" :width="320" show-overflow-tooltip/>
+          <el-table-column prop="timeBegin" label="停水时间" align="center" />
+          <el-table-column prop="timeEnd" label="恢复供水时间" align="center" />
+          <el-table-column prop="neighbourhoodName" label="停水范围(小区)" align="center" show-overflow-tooltip></el-table-column>
+          <el-table-column prop="statusText" label="停水状态" align="center" />
+          <el-table-column prop="createBy" label="创建人" align="center" />
+          <el-table-column prop="createTime" label="创建时间" align="center" />
+          <el-table-column prop="address" label="操作" align="center" fixed="right">
             <template #default="scope">
-              <div class="flex justify-center">
-                <AudioPlayer></AudioPlayer>
+              <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">
+                  <el-popconfirm
+                    width="220"
+                    icon-color="#626AEF"
+                    title="确认要删除本条数据吗?"
+                    @confirm="onConfirm(scope.row)"
+                  >
+                    <template #reference>
+                      <span>删除</span>
+                    </template>
+                    <template #actions="{ confirm, cancel }">
+                      <el-button size="small" @click="cancel">否</el-button>
+                      <el-button type="primary" size="small" @click="confirm">是</el-button>
+                    </template>
+                  </el-popconfirm>
+                </span>
               </div>
             </template>
           </el-table-column>
-          <el-table-column prop="address" label="通话发起时间" align="center" />
-          <el-table-column prop="address" label="操作" align="center" fixed="right"/>
         </el-table>
         
         <pagination
-            v-show="total >= 0"
-            :total="total"
-            v-model:page="queryParams.pageNum"
-            v-model:limit="queryParams.pageSize"
-            @pagination="getList"
-          />
+          v-show="total >= 0"
+          :total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
       </div>
-
     </div>
   </div>
 </template>
 
-
 <style lang="scss" scoped>
 .notice-viewprot {
   display: flex;

+ 740 - 212
src/views/voice/services/index.vue

@@ -1,255 +1,504 @@
 <script setup>
+import { ElMessage } from 'element-plus';
+import { Search } from '@element-plus/icons-vue'
+import { adressApi } from '@/api/voice/adress';
+import { servicesApi } from '@/api/voice/services';
 import SearchItemWrapper from '@/components/SearchItemWrapper';
 
+const loading = ref(false);
+const activeName = ref('first');
+const adressValue = ref([]);
 const total = ref(0);
-const value1 = ref('');
-const dialogVisible = ref(true);
+const tableData = ref([]);
+// 新增小区弹窗
+const housingFormRef = ref(null);
+const housingDialogVisible = ref(false);
+// 新智泵站弹窗
+const pumpFormRef = ref(null);
+const pumpDialogVisible = ref(false);
+// 关联小区
+const relationVisible = ref(false);
+const dialogDataSource = ref([]);
+const housingActiveIndex = ref(0);
+const stationVisible = ref(false);
+const checkList = ref([]);
+const stationList = ref([]);
+
+const housingFormData = ref({
+  name: '',  
+  selectOptions: [],
+  address: '',
+  buildingNum: '',
+  buildingNameList: []
+});
+
+const pumpFormData = ref({
+  name: '',  
+  selectOptions: []
+});
+
 const queryParams = ref({
   pageNum: 1,
-  pageSize: 10
+  pageSize: 10,
+  neighbourhoodName: '',  // 小区名称
+  status: ''              // 停水状态
+})
+
+const housingRules = reactive({
+  name: [
+    { required: true, message: '请输入小区名称', trigger: 'blur' },
+  ],
+  selectOptions: [
+    { required: true, message: '请选择地区', trigger: 'change', type: 'array' },
+  ],
+  address: [
+    { required: true, message: '请输入详细地址', trigger: 'blur' },
+  ],
+  buildingNameList: [
+    { required: true, validator: (rule, value, callback) => {
+      if (!housingFormData.value.buildingNameList.length) {
+        return callback(new Error('请添加楼号'))
+      }
+      callback()
+    }}
+  ]
+});
+
+const floorList = computed(() => {
+  return dialogDataSource.value[housingActiveIndex.value]?.neighbourhoodNumberAndAddFlags;
 })
-const options = [
-  {
-    value: 'Option1',
-    label: 'Option1',
-  },
-  {
-    value: 'Option2',
-    label: 'Option2',
-  },
-  {
-    value: 'Option3',
-    label: 'Option3',
-  },
-  {
-    value: 'Option4',
-    label: 'Option4',
-  },
-  {
-    value: 'Option5',
-    label: 'Option5',
-  },
-]
-
-const tableData = [
-  {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-02',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-04',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-01',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  }, {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-02',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-04',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-01',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  }, {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-02',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-04',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-01',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  }, {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-02',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-04',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-  {
-    date: '2016-05-01',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
-  },
-]
+
+// 关联数据
+const contactCheckData = computed(() => {
+  console.log("dialogDataSource.value", dialogDataSource.value);
+  const result = dialogDataSource.value.map(({ name, checkList }) => {
+    const tempObj = { name, floorList: '' };
+    tempObj.floorList = checkList.map(item => item.buildingsNames).join('、');
+    return tempObj;
+  }).filter(item => item.floorList);
+  return result;
+})
+
+const props = {
+  lazy: true,
+  lazyLoad: async (node, resolve) => {
+    let nodes = null;
+    const { level } = node;
+    switch( level ) {
+      case 0 :
+        nodes = await getCityTree({ type: 1 }, level);
+        break;
+      case 1 :
+        nodes = await getCityTree({ type: 2, pid: node.value }, level);
+        break;
+      case 2 :
+        nodes = await getCityTree({ type: 3, pid: node.value}, level)
+        break;
+    }
+    resolve( nodes );
+  }
+}
+
+async function getCityTree (params, level) {
+  const { data } = await adressApi.getAdressData(params);
+  const nodes = data.map(item => ({
+    value: item.id,
+    label: item.name,
+    leaf: level >= 2,
+  }))
+  return nodes;
+}
+
+// 当tabs切换
+const handleTabClick = (val) => {
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    name: '',       // 小区名称
+  };
+  adressValue.value = [];
+  getList();
+}
+
+// 添加楼层 - 确定
+const handleAddName = () => {
+  const buildingNum = housingFormData.value.buildingNum;
+  if ( buildingNum === '' ) {
+    return ElMessage.warning('请填写内容后,进行添加')
+  }
+  if (housingFormData.value.buildingNameList.includes(buildingNum)) {
+    return ElMessage.warning('该楼层号,已添加')
+  }
+  housingFormData.value.buildingNameList.push(buildingNum);
+  housingFormData.value.buildingNum = '';
+}
+
+// 删除标签
+const handleCloseTag = ( index ) => {
+  housingFormData.value.buildingNameList.splice(index, 1);
+}
+
+// 清除检索条件
+const handleCleanOptions = () => {
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    name: '',
+  };
+  adressValue.value = [];
+  getList();
+}
+
+const resetHousingData = () => {
+  
+  housingDialogVisible.value = false;
+  pumpDialogVisible.value = false;
+
+  housingFormData.value = {
+    name: '',  
+    selectOptions: [],
+    address: '',
+    buildingNum: '',
+    buildingNameList: []
+  }
+
+  pumpFormData.value = {
+    name: '',  
+    selectOptions: []
+  }
+
+  housingFormRef.value?.resetFields();
+  pumpFormRef.value?.resetFields();
+}
+
+
+const resetPumpData = () => {
+
+  pumpDialogVisible.value = false;
+
+  pumpFormData.value = {
+    name: '',
+    selectOptions: []
+  }
+
+  pumpFormRef.value?.resetFields();
+}
+
+// 编辑数据
+const handleEditData = ({ id, name, buildingsNames, address, provinceId, cityId, countryId }) => {
+  if ( activeName.value === 'first' ) {
+    housingFormData.value = {
+      id,
+      name,
+      selectOptions: [provinceId, cityId, countryId],
+      address,
+      buildingNameList: buildingsNames.split('、')
+    }
+    housingDialogVisible.value = true;
+  } else {
+    pumpFormData.value = {
+      id,
+      name,
+      selectOptions: [provinceId, cityId, countryId],
+    }
+    pumpDialogVisible.value = true;
+  }
+}
+
+// 删除数据
+const onConfirm = async ({ id }) => {
+  if ( activeName.value === 'first' ) {
+    await servicesApi.delNeighborhood(id);
+  } else {
+    await servicesApi.delStation(id);
+  }
+  ElMessage.success('删除成功');
+  getList();
+}
+
+// 小区弹窗 - 确定
+const onHousingDialogConfirm = () => {
+  if (!housingFormRef.value) return;
+  housingFormRef.value.validate(async (valid, fields) => {
+    if (valid) {
+      const { id, name, selectOptions, address, buildingNameList } = housingFormData.value;
+      const [provinceId, cityId, countryId] = selectOptions;
+      if ( id ) {
+        await servicesApi.putNeighborhood({ id, name, address, buildingNameList, provinceId, cityId, countryId })
+        ElMessage.success('小区修改成功')
+      } else {
+        await servicesApi.postNeighborhood({ name, address, buildingNameList, provinceId, cityId, countryId })
+        ElMessage.success('小区新增成功')
+      }
+      resetHousingData();
+      getList();
+    }
+  })
+}
+
+// 泵站弹窗 - 确定
+const onPumpDialogConfirm = () => {
+  if (!pumpFormRef.value) return;
+  pumpFormRef.value.validate(async (valid, fields) => {
+    if (valid) {
+      const { id, name, selectOptions } = pumpFormData.value;
+      const [provinceId, cityId, countryId] = selectOptions;
+
+      if ( id ) {
+        await servicesApi.putStation({ name, provinceId, cityId, countryId, id })
+        ElMessage.success('泵站修改成功')
+      } else {
+        await servicesApi.postStation({ name, provinceId, cityId, countryId })
+        ElMessage.success('泵站新增成功')
+      }
+      resetHousingData();
+      getList();
+    }
+  })
+}
+
+// 显示 关联小区 - dialog
+const handleRelation = ({ id }) => {
+  relationVisible.value = true;
+  openRelationDialog(id);  
+}
+
+// dialog - 关联小区 - 打开
+const openRelationDialog = (pumpingStationId) => {
+  servicesApi.getSimpleNeighbourhoodList({ pumpingStationId }).then(({ data }) => {
+    dialogDataSource.value = data.map(item => {
+      item.checkList = item.neighbourhoodNumberAndAddFlags.filter(item => item.addStatus == 2)
+      return item;
+    });
+    checkList.value = floorList.value.filter(item => item.addStatus == 2).map(item => item.buildingId);
+  })
+}
+
+// dialog 切换小区
+const onDialogHousingItem = (index) => {
+  housingActiveIndex.value = index;
+  checkList.value = dialogDataSource.value[housingActiveIndex.value].checkList.map(item => item.buildingId);
+}
+
+// 选中发生变化
+const onCheckoutGroupChange = (floorIds) => {
+  const currentData = dialogDataSource.value[housingActiveIndex.value];
+  currentData.checkList = floorIds.map(id => {
+    let tempItem = null;
+    currentData.neighbourhoodNumberAndAddFlags.forEach(item => {
+      if ( item.buildingId == id ) {
+        tempItem = item;
+      }
+    });
+    return tempItem;
+  }).filter(Boolean);
+}
+
+// 保存小区
+const onRelationSave = () => {
+
+  const data = dialogDataSource.value.map(item => {
+    return {
+      ...item,
+      neighbourhoodNumberAndAddFlags: item.checkList
+    };
+  })
+
+  servicesApi.postSaveFinalData(data).then(() => {
+    onRelationCancel();
+    ElMessage.success('操作成功');
+  })
+
+}
+
+const handleShowStationDialog = (row) => {
+  servicesApi.getStation({ id: row.id }).then(res => {
+    stationVisible.value = true;
+    stationList.value = res.data;
+  })
+}
+
+const onRelationCancel = () => {
+  housingActiveIndex.value = 0;
+  relationVisible.value = false;
+  checkList.value = [];
+  dialogDataSource.value = [];
+}
+
+// table数据
+const getList = async () => {
+  loading.value = true;
+
+  const [ provinceId, cityId, countryId ] = adressValue.value;
+  let rows = null;
+  let t = null;
+
+  if ( activeName.value === 'first' ) {
+    const res = await servicesApi.getNeighborhoodList({ ...queryParams.value, provinceId, cityId, countryId });
+    rows = res.rows;
+    t = res.total;
+  } else {
+    const res = await servicesApi.getStationList({ ...queryParams.value, provinceId, cityId, countryId });
+    rows = res.rows;
+    t = res.total;
+  }
+
+  loading.value = false;
+  tableData.value = rows;
+  total.value = t;
+}
+
+onMounted(() => {
+  getList();
+})
+
 </script>
 
 <template>
-
   <div class="server-viewport">
-    <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
+    <el-tabs v-model="activeName" class="demo-tabs" @tab-change="handleTabClick">
       <el-tab-pane label="服务小区管理" name="first">
-        <div class="search-card">
-          <el-row :gutter="24" class="mb-[24px]">
-            <el-col :span="6">
-              <SearchItemWrapper>
-                <el-input class="search-input" placeholder="用户电话号码"></el-input>
-              </SearchItemWrapper>
-            </el-col>
-            <el-col :span="6">
-              <SearchItemWrapper label="客服">
-                <el-select v-model="value" placeholder="Select" size="large">
-                  <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
-                </el-select>
-              </SearchItemWrapper>
-            </el-col>
-            <el-col :span="6">
-              <SearchItemWrapper label="通话状态">
-                <el-select v-model="value" placeholder="Select" size="large">
-                  <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
-                </el-select>
-              </SearchItemWrapper>
-            </el-col>
-            <el-col :span="6">
-              <div class="flex items-center justify-end space-x-[10px]">
-                <div class="custom-btn custom-btn_primary">搜索</div>
-                <div class="custom-btn custom-btn_default">重置</div>
-              </div>
-            </el-col>
-          </el-row>
-        </div>
-
-        <div class="add-btn-card">
-          <div class="btn space-x-[5px]" @click="handleJumpWaterAdd">
-            <el-icon>
-              <CirclePlus />
-            </el-icon>
-            <span>新增小区</span>
+        <div class="top-inner">
+          <div class="search-card">
+            <el-row :gutter="24" class="mb-[24px]">
+              <el-col :span="6">
+                <SearchItemWrapper>
+                  <el-input class="search-input" placeholder="小区名称" v-model="queryParams.name"></el-input>
+                </SearchItemWrapper>
+              </el-col>
+              <el-col :span="6">
+                <SearchItemWrapper label="区/县">
+                  <el-cascader :props="props" style="width: 100%;" v-model="adressValue"/>
+                </SearchItemWrapper>
+              </el-col>
+              <el-col :span="6" :offset="6">
+                <div class="flex items-center justify-end space-x-[10px]">
+                  <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
+                  <div class="custom-btn custom-btn_default" @click="handleCleanOptions">重置</div>
+                </div>
+              </el-col>
+            </el-row>
+          </div>
+  
+          <div class="add-btn-card">
+            <div class="btn space-x-[5px]" @click="housingDialogVisible = true">
+              <el-icon>
+                <CirclePlus />
+              </el-icon>
+              <span>新增小区</span>
+            </div>
           </div>
         </div>
 
-        <div class="table-card">
-          <el-table :data="tableData" style="width: 100%" max-height="calc(100vh - 366px)">
-            <el-table-column label="序号" align="center" type="index" width="50" fixed />
-            <el-table-column prop="name" label="通话类型" align="center" />
-            <el-table-column prop="address" label="通话状态" align="center">
+        <div class="table-card" ref="tableContainer">
+          <el-table :data="tableData" style="width: 100%" max-height="calc(100vh - 376px)" v-loading="loading">
+            <el-table-column prop="name" label="小区名称" align="center" />
+            <el-table-column prop="buildingsNames" label="楼号" align="center" />
+            <el-table-column prop="address" label="地址" align="center" />
+            <el-table-column prop="pumpingStationNames" label="关联泵站" align="center">
               <template #default="scope">
-                <div class="flex items-center justify-center space-x-[6px]">
-                  <span class="w-[6px] h-[6px] bg-[#65C734] rounded-full"></span>
-                  <span>已接通</span>
-                </div>
+                <span class="station-name" @click="handleShowStationDialog(scope.row)">{{ scope.row.pumpingStationNames }}</span>
               </template>
             </el-table-column>
-            <el-table-column prop="address" label="用户电话" align="center" />
-            <el-table-column prop="address" label="客服" align="center" />
-            <el-table-column prop="address" label="服务类型" align="center" />
-            <el-table-column prop="address" label="通话录音" align="center" width="350">
+            <el-table-column prop="createBy" label="创建人" align="center" />
+            <el-table-column prop="createTime" label="创建时间" align="center" />
+            <el-table-column prop="address" label="操作" align="center" width="120">
               <template #default="scope">
-                <div class="flex justify-center">
-                  <AudioPlayer></AudioPlayer>
+                <div class="flex justify-center space-x-[20px]">
+                  <span class="text-[#165DFF] cursor-pointer" @click="handleEditData(scope.row)">编辑</span>
+                  <el-popconfirm
+                      width="220"
+                      icon-color="#626AEF"
+                      title="确认要删除本条数据吗?"
+                      @confirm="onConfirm(scope.row)"
+                    >
+                      <template #reference>
+                        <span class="text-[#165DFF] cursor-pointer">删除</span>
+                      </template>
+                      <template #actions="{ confirm, cancel }">
+                        <el-button size="small" @click="cancel">否</el-button>
+                        <el-button type="primary" size="small" @click="confirm">是</el-button>
+                      </template>
+                    </el-popconfirm>
                 </div>
               </template>
             </el-table-column>
-            <el-table-column prop="address" label="通话发起时间" align="center" />
-            <el-table-column prop="address" label="操作" align="center" fixed="right" />
           </el-table>
 
           <pagination v-show="total >= 0" :total="total" v-model:page="queryParams.pageNum"
             v-model:limit="queryParams.pageSize" @pagination="getList" />
         </div>
       </el-tab-pane>
-      <el-tab-pane label="泵站管理" name="second">
+      <el-tab-pane label="泵站管理" name="second" >
+      
         <div class="search-card">
           <el-row :gutter="24" class="mb-[24px]">
             <el-col :span="6">
               <SearchItemWrapper>
-                <el-input class="search-input" placeholder="用户电话号码"></el-input>
-              </SearchItemWrapper>
-            </el-col>
-            <el-col :span="6">
-              <SearchItemWrapper label="客服">
-                <el-select v-model="value" placeholder="Select" size="large">
-                  <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
-                </el-select>
+                <el-input class="search-input" placeholder="泵站名称" v-model="queryParams.name"></el-input>
               </SearchItemWrapper>
             </el-col>
             <el-col :span="6">
-              <SearchItemWrapper label="通话状态">
-                <el-select v-model="value" placeholder="Select" size="large">
-                  <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
-                </el-select>
+              <SearchItemWrapper label="区/县">
+                <el-cascader :props="props" style="width: 100%;" v-model="adressValue"/>
               </SearchItemWrapper>
             </el-col>
-            <el-col :span="6">
+            <el-col :span="6" :offset="6">
               <div class="flex items-center justify-end space-x-[10px]">
-                <div class="custom-btn custom-btn_primary">搜索</div>
-                <div class="custom-btn custom-btn_default">重置</div>
+                <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
+                <div class="custom-btn custom-btn_default" @click="handleCleanOptions">重置</div>
               </div>
             </el-col>
           </el-row>
         </div>
 
         <div class="add-btn-card">
-          <div class="btn space-x-[5px]" @click="handleJumpWaterAdd">
+          <div class="btn space-x-[5px]" @click="pumpDialogVisible = true">
             <el-icon>
               <CirclePlus />
             </el-icon>
-            <span>新增小区</span>
+            <span>新增泵站</span>
           </div>
         </div>
 
-        <div class="table-card">
-          <el-table :data="tableData" style="width: 100%" max-height="calc(100vh - 366px)">
-            <el-table-column label="序号" align="center" type="index" width="50" fixed />
-            <el-table-column prop="name" label="通话类型" align="center" />
-            <el-table-column prop="address" label="通话状态" align="center">
+        <div class="table-card" >
+          <el-table :data="tableData" style="width: 100%" max-height="calc(100vh - 376px)">
+            <el-table-column prop="name" label="泵站名称" align="center" />
+            <el-table-column prop="buildingsNames" label="楼号" align="center" />
+            <el-table-column prop="address" label="地址" align="center">
               <template #default="scope">
-                <div class="flex items-center justify-center space-x-[6px]">
-                  <span class="w-[6px] h-[6px] bg-[#65C734] rounded-full"></span>
-                  <span>已接通</span>
-                </div>
+                <p class="space-x-[10px]">
+                  <span>{{ scope.row.provinceName }}</span>
+                  <span>{{ scope.row.cityName }}</span>
+                  <span>{{ scope.row.countryName }}</span>
+                </p>
               </template>
             </el-table-column>
-            <el-table-column prop="address" label="用户电话" align="center" />
-            <el-table-column prop="address" label="客服" align="center" />
-            <el-table-column prop="address" label="服务类型" align="center" />
-            <el-table-column prop="address" label="通话录音" align="center" width="350">
+            <el-table-column prop="neighbourhoodAndBuildings" label="关联小区" align="center" />
+            <el-table-column prop="createBy" label="创建人" align="center" />
+            <el-table-column prop="createTime" label="创建时间" align="center" />
+            <el-table-column prop="address" label="操作" align="center" width="200">
               <template #default="scope">
-                <div class="flex justify-center">
-                  <AudioPlayer></AudioPlayer>
+                <div class="flex justify-center space-x-[20px]">
+                  <span class="text-[#165DFF] cursor-pointer" @click="handleEditData(scope.row)">编辑</span>
+                  <el-popconfirm
+                    width="220"
+                    icon-color="#626AEF"
+                    title="确认要删除本条数据吗?"
+                    @confirm="onConfirm(scope.row)"
+                  >
+                    <template #reference>
+                      <span class="text-[#165DFF] cursor-pointer">删除</span>
+                    </template>
+                    <template #actions="{ confirm, cancel }">
+                      <el-button size="small" @click="cancel">否</el-button>
+                      <el-button type="primary" size="small" @click="confirm">是</el-button>
+                    </template>
+                  </el-popconfirm>
+                  <span class="text-[#165DFF] cursor-pointer" @click="handleRelation(scope.row)">关联小区</span>
                 </div>
               </template>
             </el-table-column>
-            <el-table-column prop="address" label="通话发起时间" align="center" />
-            <el-table-column prop="address" label="操作" align="center" fixed="right" />
           </el-table>
 
           <pagination v-show="total >= 0" :total="total" v-model:page="queryParams.pageNum"
@@ -258,34 +507,40 @@ const tableData = [
       </el-tab-pane>
     </el-tabs>
 
-
-    <el-dialog v-model="dialogVisible" title="编辑备注" width="600" modal-class="custom-workbench-dialog" align-center>
-      <template #header="{ close, titleId, titleClass }">
+    <el-dialog
+      v-model="housingDialogVisible"
+      title="编辑备注"
+      width="600"
+      modal-class="custom-workbench-dialog"
+      align-center
+      @closed="resetHousingData"
+    >
+      <template #header>
         <div class="dialog-header">
           <h4>新增小区</h4>
         </div>
       </template>
       <div class="dialog-body">
         <div class="dialog-form_inner">
-          <el-form :model="form" label-width="auto" style="width: 100%;">
-            <el-form-item label="小区名称">
-              <el-input />
+          <el-form :model="housingFormData" label-width="auto" style="width: 100%;" :rules="housingRules" ref="housingFormRef">
+            <el-form-item label="小区名称" prop="name">
+              <el-input v-model.trim="housingFormData.name" placeholder="请输入"/>
             </el-form-item>
-            <el-form-item label="地区">
-              <el-input />
+            <el-form-item label="地区" prop="selectOptions">
+              <el-cascader :props="props" style="width: 100%;" v-model="housingFormData.selectOptions"/>
             </el-form-item>
-            <el-form-item label="详细地址">
-              <el-input />
+            <el-form-item label="详细地址" prop="address">
+              <el-input placeholder="请输入" v-model.trim="housingFormData.address"/>
             </el-form-item>
-            <el-form-item label="楼号">
+            <el-form-item label="楼号" prop="buildingNameList">
               <div class="floor-add-card">
                 <div class="flex space-x-[10px]">
-                  <el-input />
-                  <p>添加</p>
+                  <el-input placeholder="请输入" v-model="housingFormData.buildingNum" :validate-event="false"/>
+                  <p @click="handleAddName">添加</p>
                 </div>
                 <ul class="floor-list">
-                  <li v-for="item in 20">
-                    <el-tag type="primary" closable >{{ item }}#</el-tag>
+                  <li v-for="item, index in housingFormData.buildingNameList">
+                    <el-tag type="primary" closable @close="handleCloseTag(index)">{{ item }}</el-tag>
                   </li>
                 </ul>
               </div>
@@ -295,8 +550,153 @@ const tableData = [
       </div>
       <template #footer>
         <div class="dialog-footer space-x-[14px]">
-          <div class="custom-btn custom-btn_primary">确定</div>
-          <div class="custom-btn custom-btn_default">取消</div>
+          <div class="custom-btn custom-btn_primary" @click="onHousingDialogConfirm">确定</div>
+          <div class="custom-btn custom-btn_default" @click="resetHousingData">取消</div>
+        </div>
+      </template>
+    </el-dialog>
+
+    <el-dialog
+      v-model="pumpDialogVisible"
+      title="编辑备注"
+      width="600"
+      modal-class="custom-workbench-dialog"
+      align-center
+      @closed="resetPumpData"
+    >
+      <template #header>
+        <div class="dialog-header">
+          <h4>新增泵站</h4>
+        </div>
+      </template>
+      <div class="dialog-body">
+        <div class="dialog-form_inner">
+          <el-form :model="pumpFormData" label-width="auto" style="width: 100%;" :rules="housingRules" ref="pumpFormRef">
+            <el-form-item label="泵站名称" prop="name">
+              <el-input v-model.trim="pumpFormData.name" placeholder="请输入"/>
+            </el-form-item>
+            <el-form-item label="地区" prop="selectOptions">
+              <el-cascader :props="props" style="width: 100%;" v-model="pumpFormData.selectOptions"/>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer space-x-[14px]">
+          <div class="custom-btn custom-btn_primary" @click="onPumpDialogConfirm">确定</div>
+          <div class="custom-btn custom-btn_default" @click="resetHousingData">取消</div>
+        </div>
+      </template>
+    </el-dialog>
+
+    <el-dialog
+      v-model="relationVisible"
+      title="关联小区"
+      width="750"
+      class="relation-dialog"
+    >
+      <template #header>
+        <div class="dialog-header pl-[16px]">
+          <h4>关联小区</h4>
+        </div>
+      </template>
+
+      <div class="relation-dialog-body space-x-[16px]">
+        <div class="dialog-inner_left">
+          <div class="header">
+            <el-input :suffix-icon="Search"></el-input>
+          </div>
+          <div class="body">
+            <div class="body-inner_left">
+              <el-scrollbar style="height: 100%;">
+                <ul class="housing-list">
+                  <li
+                    v-for="item, index in dialogDataSource"
+                    :key="item.id"
+                    :class="{active: housingActiveIndex === index}"
+                    @click="onDialogHousingItem(index)"
+                  >
+                    <el-tooltip
+                      effect="dark"
+                      placement="left"
+                      :content="item.name"
+                    >
+                      <p>{{ item.name }}</p>
+                    </el-tooltip>
+                  </li>
+                </ul>
+              </el-scrollbar>
+            </div>
+            <div class="body-inner_right">
+              <el-scrollbar style="height: 100%;">
+                <el-checkbox-group v-model="checkList" @change="onCheckoutGroupChange">
+                  <div v-for="item, index in floorList" :key="index">
+                    <el-checkbox :label="item.buildingsNames" :value="item.buildingId" :disabled="item.addStatus === 1"/>
+                  </div>
+                </el-checkbox-group>
+              </el-scrollbar>
+            </div>
+          </div>
+          <div>
+
+          </div>
+        </div>
+        <div class="dialog-inner_right">
+          <div class="header">
+            已选小区
+          </div>
+          <div class="body">
+            <el-scrollbar style="height: 100%;">
+              <ul class="body_inner space-y-[10px]">
+                <li v-for="item, index in contactCheckData" :key="index">
+                  <p class="name">{{ item.name }}</p>
+                  <p>{{ item.floorList }}</p>
+                </li>
+              </ul>
+            </el-scrollbar>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer space-x-[14px]">
+          <div class="custom-btn custom-btn_primary" @click="onRelationSave">保存</div>
+          <div class="custom-btn custom-btn_default" @click="onRelationCancel">取消</div>
+        </div>
+      </template>
+    </el-dialog>
+
+    <el-dialog
+      v-model="stationVisible"
+      title="关联泵站"
+      width="650"
+      class="relation-dialog"
+    >
+      <template #header>
+        <div class="dialog-header pl-[16px]">
+          <h4>关联泵站</h4>
+        </div>
+      </template>
+
+      <div class="station-dialog-body space-x-[16px]">
+        <el-scrollbar style="height: 100%;" >
+          <div class="space-y-[20px]">
+            <ul v-for="item, index in stationList" :key="index">
+              <li class="space-x-[10px]">
+                <span class="flex-shrink-0">泵站名称:</span>
+                <span>{{ item.pumpingStationName === 'NULL' ? '' : item.pumpingStationName }}</span>
+              </li>
+              <li class="space-x-[10px] flex">
+                <span class="flex-shrink-0">关联楼号:</span>
+                <span>{{ item.buildingsNames }}</span>
+              </li>
+            </ul>
+          </div>
+        </el-scrollbar>
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer space-x-[14px]">
+          <div class="custom-btn custom-btn_default" @click="stationVisible = false">关闭</div>
         </div>
       </template>
     </el-dialog>
@@ -312,8 +712,33 @@ const tableData = [
   border-radius: 8px;
   background: #FFF;
 
+  .station-name {
+    cursor: pointer;
+    &:hover {
+      color: #165DFF;
+    }
+  }
+
+  .custom-tab-pane {
+    display: flex;
+    flex-flow: column;
+    height: 100%;
+
+    .top-inner {
+      flex-shrink: 0;
+    }
+
+    .table-card {
+      height: 100%;
+    }
+  }
+
   .search-card {
     border-bottom: 1px dashed #E5E6EB;
+
+    :deep(.el-input__wrapper) {
+      box-shadow: none;
+    }
   }
 
   .add-btn-card {
@@ -368,6 +793,10 @@ const tableData = [
     grid-gap: 4px;
   }
 
+  :deep(.el-tabs) {
+    height: 100%;
+  }
+
   :deep(.el-tabs__item) {
     color: #4E5969;
   }
@@ -380,4 +809,103 @@ const tableData = [
     background-color: #165DFF;
   }
 }
+
+.relation-dialog-body {
+  display: flex;
+  height: 360px;
+  padding: 16px;
+  border-radius: 4px;
+  background: #f1f3f7;    
+
+  .dialog-inner_left, .dialog-inner_right {
+    height: 100%;
+    border-radius: 4px;
+  }
+  .dialog-inner_left {
+    width: 70%;
+    padding: 10px;
+    background: #fff;
+    .header {
+      padding-bottom: 10px;
+    }
+    .body {
+      display: flex;
+      height: calc(100% - 42px);
+      .body-inner_left, .body-inner_right {
+        height: 100%;
+      }
+      .body-inner_left {
+        width: 60%;
+        border-right: 1px solid #eff1f5;
+        .housing-list {
+          padding-right: 10px;
+          line-height: 30px;
+          li {
+            padding-left: 10px;
+            cursor: pointer;
+            p {
+              white-space: nowrap; 
+              overflow: hidden; 
+              text-overflow: ellipsis; 
+            }
+            &:hover {
+              background: #eff1f5;
+            }
+          }
+          .active {
+            background: #eff1f5;
+          }
+        }
+      }
+      .body-inner_right {
+        flex: 1;
+        padding: 0 10px;
+      }
+    }
+  }
+
+  .dialog-inner_right {
+    width: 30%;
+    background: #fff;
+    .header {
+      padding: 10px;
+      border-bottom: 1px solid #eff1f5;
+      line-height: 21px;
+    }
+    .body {
+      height: calc(100% - 42px);
+      .body_inner {
+        padding: 10px;
+        li {
+          padding-bottom: 10px;
+          p:first-child {
+            color: #1D2129;
+            font-weight: bold;
+          }
+          p:nth-child(2) {
+            color: #606266;
+          }
+        }
+        li:not(:last-child) {
+          border-bottom: 1px solid #eff1f5;
+        }
+        .name {
+          white-space: nowrap; 
+          overflow: hidden; 
+          text-overflow: ellipsis; 
+        }
+      }
+    }
+  }
+}
+
+  .station-dialog-body {
+    padding: 0 16px;
+  }
+</style>
+
+<style lang="scss">
+.relation-dialog {
+  padding: 16px 0;
+}
 </style>

+ 264 - 0
src/views/voice/whiteList/index.vue

@@ -0,0 +1,264 @@
+<script setup>
+import { ElMessage } from 'element-plus'
+import SearchItemWrapper from '@/components/SearchItemWrapper';
+import useTableHeight from '@/composables/useTableHeight';
+import { waiteListApi } from '@/api/voice/whiteList';
+
+const { tableContainer, tableMaxHeight } = useTableHeight();
+
+const loading = ref(false);
+const total = ref(0);
+const tableData = ref([]);
+const dialogVisible = ref(false);
+const formRef = ref(null);
+const rules = {
+  phone: [
+    { required: true, message: '请输入电话号码', trigger: 'blur' },
+  ],
+  description: [
+    { required: true, message: '请输入备注', trigger: 'blur' }
+  ]
+}
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  phone: '',
+  description: ''
+})
+
+const formData = ref({
+  id: '',
+  phone: '',
+  description: ''
+});
+
+// 气泡弹窗 - 是否删除该项
+const onConfirm = async (row) => {
+  await waiteListApi.delWhiteList(row.id);
+  getList();
+  ElMessage.success('删除成功');
+}
+
+const getList = async () => {
+
+  loading.value = true;
+
+  const { rows, total: t } = await waiteListApi.getWhiteList({...queryParams.value});
+
+  tableData.value = rows
+  
+  loading.value = false;
+
+  total.value = t;
+};
+
+// 弹窗 - 新增
+const handleAddPhoneNum = () => {
+  dialogVisible.value = true;
+}
+
+// 弹窗 - 编辑
+const handleEditRow = ({ id, phone, description }) => {
+  dialogVisible.value = true;
+  formData.value = { id, phone, description };
+}
+
+// 弹窗 - 确定
+const onDialogConfirm = () => {
+  formRef.value.validate(async (valid) => {
+    if ( valid ) {
+      dialogVisible.value = false;
+      if ( !formData.value.id ) {
+        await waiteListApi.postWhiteList(formData.value);
+        ElMessage.success('新增电话号码成功');
+      } else {
+        await waiteListApi.putWhiteList(formData.value);
+        ElMessage.success('更新电话号码成功');
+      }
+      getList();
+    }
+  })
+}
+
+// 弹窗 - 取消
+const onDialogCancel = () => {
+  formData.value = {
+    id: '',
+    phone: '',
+    description: ''
+  };
+  dialogVisible.value = false;
+}
+
+// 清除检索条件
+const handleCleanOptions = () => {
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    phone: '',
+    description: '' 
+  };
+  getList();
+}
+
+onMounted(() => {
+  getList();
+})
+
+</script>
+
+<template>
+  <div class="notice-viewprot">
+    <div class="search-card">
+      <el-row :gutter="24" class="mb-[24px]">
+        <el-col :span="6">
+          <SearchItemWrapper>
+            <el-input class="search-input" placeholder="电话号码" v-model="queryParams.phone"></el-input>
+          </SearchItemWrapper>
+        </el-col>
+        <el-col :span="6">
+          <SearchItemWrapper>
+            <el-input class="search-input" placeholder="备注" v-model="queryParams.description"></el-input>
+          </SearchItemWrapper>
+        </el-col>
+        <el-col :span="6" :offset="6">
+          <div class="flex items-center justify-end space-x-[10px]">
+            <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
+            <div class="custom-btn custom-btn_default" @click="handleCleanOptions">重置</div>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+    
+    <div class="add-btn-card">
+      <div class="btn space-x-[5px]" @click="handleAddPhoneNum">
+        <el-icon><CirclePlus /></el-icon>
+        <span>新增电话</span>
+      </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="phone" label="电话号码" align="center" :width="130"/>
+          <el-table-column prop="description" label="备注" align="center" />
+          <el-table-column prop="address" label="操作" align="center" :width="130">
+            <template #default="scope">
+              <div class="flex justify-center space-x-[20px]">
+                <span class="text-[#165DFF] cursor-pointer" @click="handleEditRow(scope.row)">编辑</span>
+                <span class="text-[#165DFF] cursor-pointer">
+                  <el-popconfirm
+                    width="220"
+                    icon-color="#626AEF"
+                    title="确认要删除本条数据吗?"
+                    @confirm="onConfirm(scope.row)"
+                  >
+                    <template #reference>
+                      <span>删除</span>
+                    </template>
+                    <template #actions="{ confirm, cancel }">
+                      <el-button size="small" @click="cancel">否</el-button>
+                      <el-button type="primary" size="small" @click="confirm">是</el-button>
+                    </template>
+                  </el-popconfirm>
+                </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-dialog
+      v-model="dialogVisible"
+      title="编辑备注"
+      width="600"
+      modal-class="custom-workbench-dialog"
+      align-center
+      @closed="resetDialogStatus"
+    >
+      <template #header>
+        <div class="dialog-header">
+          <h4>电话号码</h4>
+        </div>
+      </template>
+      <div class="dialog-body">
+        <div class="dialog-form_inner">
+          <el-form :model="formData" label-width="auto" style="width: 100%;" :rules="rules" ref="formRef">
+            <el-form-item label="电话号码" prop="phone">
+              <el-input v-model.trim="formData.phone" placeholder="请输入"/>
+            </el-form-item>
+            <el-form-item label="备注" prop="description">
+              <el-input v-model.trim="formData.description" type="textarea" :autosize="{ minRows: 5, maxRows: 6 }" resize="none" placeholder="请输入"/>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer space-x-[14px]">
+          <div class="custom-btn custom-btn_primary" @click="onDialogConfirm">确定</div>
+          <div class="custom-btn custom-btn_default" @click="onDialogCancel">取消</div>
+        </div>
+      </template>
+    </el-dialog>
+    
+  </div>
+</template>
+
+
+<style lang="scss" scoped>
+.notice-viewprot {
+  display: flex;
+  flex-flow: column;
+  width: 100%;
+  height: 100%;
+  padding: 28px 24px 18px 24px;
+  border-radius: 8px;
+  background: #fff;
+
+  .search-card {
+    border-bottom: 1px dashed #E5E6EB;
+  }
+
+  .add-btn-card {
+    display: flex;
+    justify-content: flex-start;
+    margin: 20px 0;
+
+    .btn {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      font-size: 14px;
+      border-radius: 8px;
+      padding: 7px 16px;
+      background: #165DFF;
+      color: #fff;
+      cursor: pointer;
+    }
+  }
+  
+  .table-card {
+    height: 100%;
+  }
+}
+.dialog-form_inner {
+  width: 500px;
+  margin: 0 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+}
+</style>

+ 33 - 79
src/views/voice/workbench/index.vue

@@ -1,11 +1,7 @@
 <script setup>
-import { ElMessage } from 'element-plus';
 import { workbenchApi } from '@/api/voice/workbench';
-import useVoiceStore from "@/store/modules/voice";
 
 import RecordCardItem from './components/RecordCardItem';
-import CustomRowItem from './components/CustomRowItem.vue';
-import AudioPlayer from '@/components/AudioPlayer';
 import CallView from '@/components/CallView';
 
 const queryParams = ref({
@@ -15,83 +11,59 @@ const queryParams = ref({
   phone: ''
 });
 
-const voiceStore = useVoiceStore();
-
-const remark = ref('');
 const tabCallRecordList = ref([]);
 const tabCurrentActive = ref(null);
 const callDetails = ref({});
 
-const isExpand = ref(false);
-const dialogVisible = ref(false);
-
 const total = ref(0);
 const loading = ref(false);
 
 const tabEnum = ['通话呼入', '通话呼出'];
-const categoryTypeEnum = {
-  0: '人工客服',
-  1: '机器人',
-  2: '机器人转人工'
-}
-
-const typeEnum = {
-  0: '白名单',
-  1: 'AI客服',
-  2: '传统服务'
-}
 
 const disabled = computed(() => loading.value || total.value == tabCallRecordList.value.length);
 
+const getTimeOfDayGreeting = computed(() => {
+  const date = new Date();
+  const hour = date.getHours();
+
+  if (hour >= 0 && hour < 12) {
+    return "上午好";
+  } else if (hour >= 12 && hour < 18) {
+    return "下午好";
+  } else {
+    return "晚上好";
+  }
+})
+
 // 切换tabs
 const handleChangeTab = (index) => {
-  queryParams.value.pageNum = 1;
-  queryParams.value.category = index;
-  tabCallRecordList.value = [];
-  initTabsData();
+  if ( queryParams.value.category != index ) {
+    queryParams.value.pageNum = 1;
+    queryParams.value.category = index;
+    tabCurrentActive.value = null;
+    tabCallRecordList.value = [];
+    initTabsData();
+  }
 }
 
 // 选中通话记录
-const hanldeTabItem = (i) => {
+const hanldeTabItem = async (i, id) => {
+  const { data } = await workbenchApi.getCallRecordDetails(id);
+  callDetails.value = data;
   tabCurrentActive.value = i;
-  callDetails.value = tabCallRecordList.value[i];
-}
-
-// 编辑
-const handleEdit = () => {
-  dialogVisible.value = true;
-  remark.value = callDetails.value.remark;
-}
-
-// 弹窗 - 确定
-const onDialogConfirm = () => {
-  const { id } = callDetails.value;
-  workbenchApi.putCallRecord({ id, remark: remark.value }).then(() => {
-    dialogVisible.value = false;
-    callDetails.value.remark = remark.value;
-    ElMessage({
-      message: '备注更改成功',
-      type: 'success',
-    })
-  })
-}
-
-// 弹窗 - 取消
-const onDialogCancel = () => {
-  dialogVisible.value = false;
-  remark.value = callDetails.value.remark;
 }
 
 // 搜索
 const onSearch = () => {
   queryParams.value.pageNum = 1;
   tabCallRecordList.value = [];
+  tabCurrentActive.value = null;
   initTabsData();
 }
 
-// 拨打电话
-const onConfirm = () => {
-  voiceStore.onMakingCall("15810954324");
+// 语音转化完成
+const handleVoiceParsed = ({ parsedVoiceContent }) => {
+  callDetails.value.parsedVoiceContent = parsedVoiceContent;
 }
 
 const initTabsData = async () => {
@@ -123,7 +95,7 @@ const loadMoreData = () => {
       <div class="tabs-content">
         <div class="search-inp-wrapper">
           <div class="search-inp">
-            <input type="text" class="inp" placeholder="请输入电话号码" v-model="queryParams.phone">
+            <input type="text" class="inp" placeholder="请输入电话号码" v-model.trim="queryParams.phone">
             <div class="btn" @click="onSearch">搜索</div>
           </div>
         </div>
@@ -141,7 +113,7 @@ const loadMoreData = () => {
                 :index="index"
                 :active="tabCurrentActive === index"
                 :key="item.id"
-                @on-click="hanldeTabItem(index)"
+                @on-click="hanldeTabItem(index, item.id)"
               ></RecordCardItem>
               <div class="flex justify-center text-[#999] text-[12px]">
                 <p class="pb-[6px]" v-if="loading">Loading...</p>
@@ -156,38 +128,20 @@ const loadMoreData = () => {
       </div>
     </div>
     <div class="details-section">
-      <div class="empty-wrapper" v-show="!callDetails.id">
+      <div class="empty-wrapper" v-show="!callDetails.id || tabCurrentActive === null">
         <img src="@/assets/images/workbench/img-empty.png" alt="">
         <p class="empty-text">
-          <span>Hi, 下午好~</span>
+          <span>Hi, {{ getTimeOfDayGreeting }}~</span>
           <span>欢迎登录智能语音客服</span>
         </p>
       </div>
-      <div class="details-wrapper" v-show="callDetails.id">
+      <div class="details-wrapper" v-show="callDetails.id && tabCurrentActive !== null">
         <h4 class="title">通话详情</h4>
         <el-scrollbar class="details-scrollbar">
-          <CallView :data="callDetails" noInit></CallView>
+          <CallView :data="callDetails" noInit @on-end="handleVoiceParsed"></CallView>
         </el-scrollbar>
       </div>
     </div>
-
-    <el-dialog v-model="dialogVisible" title="编辑备注" width="530" modal-class="custom-workbench-dialog" align-center>
-      <template #header>
-        <div class="dialog-header">
-          <h4>编辑备注</h4>
-        </div>
-      </template>
-      <div class="dialog-body">
-        <el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6 }" resize="none" v-model="remark"></el-input>
-      </div>
-      <template #footer>
-        <div class="dialog-footer space-x-[14px]">
-          <div class="custom-btn custom-btn_primary" @click="onDialogConfirm">确定</div>
-          <div class="custom-btn custom-btn_default" @click="onDialogCancel">取消</div>
-        </div>
-      </template>
-    </el-dialog>
-
   </div>
 </template>