Browse Source

feat: 通话记录前后端接口联调

sunxiao 2 tuần trước cách đây
mục cha
commit
5ae11badb8

+ 2 - 0
.env.development

@@ -7,5 +7,7 @@ VITE_APP_ENV = 'development'
 # 管理系统/开发环境
 VITE_APP_BASE_API =  http://192.168.100.159:8001/
 
+# VITE_APP_BASE_API =  http://192.168.40.21:8001/
+
 VITE_APP_BASE_TEST = http://10.0.0.28:8080/
 VITE_APP_BASE_PROD = http://192.168.9.54:8080/

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

@@ -9,6 +9,13 @@ export const workbenchApi = {
     params
   }),
 
+  /**
+   * 通话记录 - 详情
+   */
+  getCallRecordDetails: params => request({
+    url: `business/record/` + params
+  }),
+
   /**
    * 通话记录 - 查询
    */

+ 8 - 0
src/assets/styles/element-ui.scss

@@ -93,4 +93,12 @@
 
 .el-dropdown .el-dropdown-link{
   color: var(--el-color-primary) !important;
+}
+
+.el-select-dropdown__item.is-selected {
+  color: #165DFF;
+}
+
+.el-select__placeholder {
+ color: #1D2129;
 }

+ 225 - 0
src/components/CallView/index.vue

@@ -0,0 +1,225 @@
+<script setup>
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { workbenchApi } from '@/api/voice/workbench';
+import useVoiceStore from "@/store/modules/voice";
+
+import AudioPlayer from '@/components/AudioPlayer';
+import CustomRowItem from '@/components/CustomRowItem';
+
+const props = defineProps({
+  noInit: {
+    type: Boolean,
+    default: false
+  },
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const voiceStore = useVoiceStore();
+const route = useRoute();
+
+const remark = ref('');
+const callDetails = ref({});
+
+const isExpand = ref(false);
+const dialogVisible = ref(false);
+
+const categoryTypeEnum = {
+  0: '人工客服',
+  1: '机器人',
+  2: '机器人转人工'
+}
+
+const typeEnum = {
+  0: '白名单',
+  1: 'AI客服',
+  2: '传统服务'
+}
+
+watch(() => props.data.id, () => {
+  callDetails.value = props.data;
+})
+
+// 编辑
+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 onConfirm = () => {
+  voiceStore.onMakingCall("15810954324");
+}
+
+onMounted(async () => {
+  if (props.noInit) return;
+  const { id } = route.query;
+  const { data } = await workbenchApi.getCallRecordDetails(id);
+  callDetails.value = data;
+});
+</script>
+
+<template>
+  <div class="details-inner">
+    <el-descriptions title="">
+      <el-descriptions-item label="呼入分类" label-class-name="custom-label" class-name="custom-colums"
+        v-if="callDetails.category != 1">
+        <span class="text-[#FF3636]">{{ typeEnum[callDetails.type] }}</span>
+      </el-descriptions-item>
+      <el-descriptions-item label="客服" label-class-name="custom-label" class-name="custom-colums">
+        {{ callDetails.userName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="服务类型" label-class-name="custom-label" class-name="custom-colums">
+        {{ categoryTypeEnum[callDetails.serviceCategory] }}
+      </el-descriptions-item>
+      <el-descriptions-item label="通话发起时间" label-class-name="custom-label" class-name="custom-colums">
+        {{ callDetails.timeBegin }}
+      </el-descriptions-item>
+      <el-descriptions-item label="通话结束时间" label-class-name="custom-label" class-name="custom-colums">
+        {{ callDetails.timeEnd }}
+      </el-descriptions-item>
+      <el-descriptions-item label="通话时长" label-class-name="custom-label" class-name="custom-colums">
+        {{ callDetails.times }}
+      </el-descriptions-item>
+      <el-descriptions-item label="通话类型" label-class-name="custom-label" class-name="custom-colums">
+        {{ callDetails.category == 0 ? '呼入' : '呼出' }}
+      </el-descriptions-item>
+      <el-descriptions-item label="通话状态" label-class-name="custom-label" class-name="custom-colums">
+        {{ callDetails.status == 0 ? '未接听' : '已接通' }}
+      </el-descriptions-item>
+      <el-descriptions-item label="业务类型" label-class-name="custom-label" class-name="custom-colums"
+        v-if="callDetails.category != 1">{{ callDetails.bussinessType }}</el-descriptions-item>
+      <el-descriptions-item label="电话号码" label-class-name="custom-label" class-name="custom-colums">
+        <div class="inline-block">
+          <div class="flex items-center space-x-[4px]">
+            <span>{{ callDetails.phone }}</span>
+            <el-popconfirm width="250" icon-color="#626AEF" title="请确认,是否呼叫该电话号码?" @confirm="onConfirm">
+              <template #reference>
+                <img src="@/assets/images/workbench/icon-call-square.svg" alt="" class="cursor-pointer">
+              </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>
+        </div>
+      </el-descriptions-item>
+    </el-descriptions>
+    <custom-row-item label="备注">
+      <div class="space-x-[14px] flex items-center">
+        <span class="custom-colums">{{ callDetails.remark ? callDetails.remark : '暂无' }}</span>
+        <span class="text-[#165DFF] text-[14px] cursor-pointer" @click="handleEdit">编辑</span>
+      </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>
+    </custom-row-item>
+    <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>
+
+<style lang="scss" scoped>
+.details-inner {
+  :deep(.custom-label) {
+    display: inline-block;
+    width: 84px;
+    color: #86909C;
+    box-sizing: border-box;
+    font-size: 14px;
+    font-weight: normal;
+    line-height: 23px;
+    text-align: left;
+  }
+
+  :deep(.custom-colums) {
+    font-size: 14px;
+    color: #1D2129;
+  }
+
+  .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: 14px;
+    }
+
+    .record-play-content {
+      padding-top: 12px;
+      color: #4E5969;
+      font-family: "PingFang SC";
+      font-size: 13px;
+      line-height: 20px;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-textarea__inner) {
+  background: #f2f4f7;
+}
+</style>

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

@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia';
 import useVoiceStore from "@/store/modules/voice";
 
 const voiceStore = useVoiceStore();
-const { callDialing, callTime } = storeToRefs(voiceStore);
+const { callDialing, callTime, isMakingCall } = storeToRefs(voiceStore);
 
 // 切换展示窗口
 const handleToggle = () => {
@@ -54,7 +54,7 @@ const handleCallAnswered = () => {
     <div class="notice-right flex">
       <ul class="flex items-center space-x-[12px]">
         <li class="icon-off" @click="handleCallDisconnected"></li>
-        <li class="icon-on" @click="handleCallAnswered" v-show="!callDialing"></li>
+        <li class="icon-on" @click="handleCallAnswered" v-show="!callDialing && !isMakingCall"></li>
         <li class="icon-toggle" @click="handleToggle"></li>
       </ul>
     </div>

+ 43 - 4
src/layout/components/TelNoticeBox/index.vue

@@ -3,14 +3,19 @@ import { storeToRefs } from 'pinia';
 import useVoiceStore from "@/store/modules/voice";
 
 import AudioPlayer from '@/components/AudioPlayer';
+import { watchEffect } from 'vue';
 
 const voiceStore = useVoiceStore();
-const { callDialing, callTime } = storeToRefs(voiceStore);
+const { callDialing, callTime, isMakingCall } = storeToRefs(voiceStore);
 
-const activeName = ref('information');
+const activeName = ref('currentRecord');
 
 const isExpand =ref(false);
 
+watchEffect(() => {
+  activeName.value = isMakingCall ? 'information' : 'currentRecord';
+})
+
 const handleToggle = () => {
   voiceStore.noiceBarVisibleState = true;
   voiceStore.noiceBoxVisibleState = false;
@@ -52,7 +57,7 @@ const handleCallAnswered = () => {
         </div>
         <ul class="notice-right space-x-[12px]">
           <li class="icon-off" @click="handleCallDisconnected"></li>
-          <li class="icon-on" @click="handleCallAnswered" v-if="!callDialing"></li>
+          <li class="icon-on" @click="handleCallAnswered" v-if="!callDialing && !isMakingCall"></li>
           <li class="icon-toggle" @click="handleToggle"></li>
         </ul>
       </div>
@@ -67,7 +72,7 @@ const handleCallAnswered = () => {
                   <span>22</span>
                 </li>
                 <li class="flex justify-between">
-                  <span class="label">用户号:</span>
+                  <span class="label">用户号:</span>
                   <span>22</span>
                 </li>
                 <li class="flex justify-between">
@@ -97,6 +102,40 @@ const handleCallAnswered = () => {
           </div>
           <div class="right-content">
             <el-tabs v-model="activeName" class="right-tabs" @tab-click="handleClick">
+              <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>
+                  </ul>
+                </el-scrollbar>
+              </el-tab-pane>
               <el-tab-pane label="停水信息" name="information">
                 <el-scrollbar style="height: 100%;">
                   <ul class="info-list space-y-[8px]">

+ 7 - 1
src/store/modules/voice.js

@@ -3,6 +3,10 @@ import { Timer } from '@/utils/timer';
 import { ElMessage } from 'element-plus'
 
 const useVoiceStore = defineStore('voice', () => {
+
+  // 是否是拨打电话   true: 外呼    false: 来电
+  const isMakingCall = ref(false);
+
   // 刚来电
   const callAnswered = ref(false);
   // 接听中
@@ -30,9 +34,10 @@ const useVoiceStore = defineStore('voice', () => {
       })
     };
     
+    isMakingCall.value = true;
+
     noiceBarVisibleState.value = true;
 
-    alert(phoneNum)
   }
 
   // 接听电话
@@ -61,6 +66,7 @@ const useVoiceStore = defineStore('voice', () => {
 
   return {
     callTime,
+    isMakingCall,
     
     onMakingCall,
     onCallDisconnected,

+ 11 - 137
src/views/voice/call/details.vue

@@ -1,14 +1,11 @@
 <script setup>
-import CustomRowItem from '@/components/CustomRowItem.vue';
-import AudioPlayer from '@/components/AudioPlayer';
-const tabActive = ref(0);
-const isExpand = ref(false);
-const dialogVisible = ref(false);
+import { useRouter } from 'vue-router';
+import CallView from '@/components/CallView';
 
-const tabEnum = ['通话呼入', '通话呼出'];
+const router = useRouter();
 
-const handleChangeTab = (index) => {
-  tabActive.value = index;
+const handleGoBack = () => {
+  router.push('/voice/call');
 }
 </script>
 
@@ -17,70 +14,13 @@ const handleChangeTab = (index) => {
     <div class="details-wrapper">
       <h4 class="title">通话详情</h4>
       <el-scrollbar class="details-scrollbar">
-        <div class="details-inner">
-          <el-descriptions title="">
-            <el-descriptions-item label="呼入问题" label-class-name="custom-label" class-name="custom-colums">
-              <span class="text-[#FF3636]">按键2 漏水报修</span>
-            </el-descriptions-item>
-            <el-descriptions-item label="客服" label-class-name="custom-label"
-              class-name="custom-colums">千华</el-descriptions-item>
-            <el-descriptions-item label="服务类型" label-class-name="custom-label"
-              class-name="custom-colums">人工席位</el-descriptions-item>
-            <el-descriptions-item label="通话发起时间" label-class-name="custom-label" class-name="custom-colums">
-              2024-08-12 12:23:22
-            </el-descriptions-item>
-            <el-descriptions-item label="通话结束时间" label-class-name="custom-label" class-name="custom-colums">2024-08-12
-              12:23:22</el-descriptions-item>
-            <el-descriptions-item label="通话时长" label-class-name="custom-label"
-              class-name="custom-colums">00:23:23</el-descriptions-item>
-            <el-descriptions-item label="通话类型" label-class-name="custom-label"
-              class-name="custom-colums">呼入</el-descriptions-item>
-            <el-descriptions-item label="通话状态" label-class-name="custom-label"
-              class-name="custom-colums">已接通</el-descriptions-item>
-            <el-descriptions-item label="关键词提取" label-class-name="custom-label"
-              class-name="custom-colums">停水咨询</el-descriptions-item>
-            <el-descriptions-item label="电话号码" label-class-name="custom-label" class-name="custom-colums">
-              <div class="inline-block">
-                <div class="flex items-center space-x-[4px]">
-                  <span>186****2345</span>
-                  <img src="@/assets/images/workbench/icon-call-square.svg" alt="" class="cursor-pointer">
-                </div>
-              </div>
-            </el-descriptions-item>
-          </el-descriptions>
-          <custom-row-item label="备注">
-            <div class="space-x-[14px]">
-              <span class="custom-colums">该用户小区停水, 目前没有任何公告和消息说明停水原因, 已经派师傅上门处理</span>
-              <span class="text-[#165DFF] text-[14px] cursor-pointer" @click="dialogVisible = true">编辑</span>
-            </div>
-          </custom-row-item>
-          <custom-row-item label="通话录音">
-            <div class="record-box">
-              <div class="record-play-control space-x-[14px]">
-                <AudioPlayer></AudioPlayer>
-                <span class="text-[#165DFF] cursor-pointer" @click="isExpand = !isExpand">{{ isExpand ? '收起文字' : '转文字'
-                  }}</span>
-              </div>
-              <div class="record-play-content" v-show="isExpand" v-for="item in 10">
-                供水公司客服:您好,欢迎拨打XX市供水公司客服热线,我是客服代表小张,请问有什么可以帮您?
-                用户:你好,我家里的水压最近特别低,想咨询一下是什么原因?
-                供水公司客服:您好,非常感谢您对我们工作的关注。请您告诉我一下您所在的小区名称和具体楼号,我帮您查询一下。
-                用户:好的,我家在阳光小区3号楼。
-                供水公司客服:阳光小区3号楼,好的,请您稍等,我马上为您查询。
-                (等待片刻)
-                供水公司客服:您好,经过查询,近期我们并没有接到关于阳光小区3号楼水压低的报修。请问您家水压低的情况是持续性的还是偶尔出现?有没有规律可循?
-                用户:最近一个星期都比较低,尤其是早上和晚上用水高峰期。
-                供水公司客服:明白了,可能是用水高峰期导致供水压力不足。我们会安排维修人员到现场查看,争取尽快解决问题。请问您方便提供一下联系方式吗?以便我们及时与您沟通。
-              </div>
-            </div>
-          </custom-row-item>
-        </div>
+        <CallView></CallView>
       </el-scrollbar>
-    <ul class="flex justify-center space-x-[12px]">
-      <li class="custom-btn custom-btn_primary">保存</li>
-      <li class="custom-btn custom-btn_default">返回</li>
-    </ul>
+      <ul class="flex justify-center">
+        <li class="custom-btn custom-btn_default" @click="handleGoBack">返回</li>
+      </ul>
     </div>
+
   </div>
 </template>
 
@@ -99,33 +39,6 @@ const handleChangeTab = (index) => {
     padding-bottom: 20px;
   }
 
-  .empty-wrapper {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-flow: column;
-    width: 100%;
-    height: 100%;
-
-    .empty-text {
-      span {
-        display: block;
-        text-align: center;
-        font-weight: bold;
-        font-size: 24px;
-        line-height: 32px;
-
-        &:nth-child(1) {
-          color: #165DFF;
-        }
-
-        &:nth-child(2) {
-          color: #1D2129;
-        }
-      }
-    }
-  }
-
   .details-wrapper {
     height: 100%;
 
@@ -136,46 +49,7 @@ const handleChangeTab = (index) => {
       font-weight: bold;
       line-height: 26px;
     }
-
-    .details-inner {
-      :deep(.custom-label) {
-        display: inline-block;
-        width: 84px;
-        color: #86909C;
-        box-sizing: border-box;
-        font-size: 14px;
-        font-weight: normal;
-        line-height: 23px;
-        text-align: left;
-      }
-
-      :deep(.custom-colums) {
-        font-size: 14px;
-        color: #1D2129;
-      }
-
-      .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: 14px;
-        }
-
-        .record-play-content {
-          padding-top: 12px;
-          color: #4E5969;
-          font-family: "PingFang SC";
-          font-size: 13px;
-          line-height: 20px;
-        }
-      }
-    }
   }
 }
+
 </style>

+ 81 - 136
src/views/voice/call/index.vue

@@ -1,134 +1,73 @@
 <script setup>
+import { useRouter } from 'vue-router'
+import { workbenchApi } from '@/api/voice/workbench';
 import SearchItemWrapper from '@/components/SearchItemWrapper';
 import AudioPlayer from '@/components/AudioPlayer';
 import useTableHeight from '@/composables/useTableHeight';
 
+const router = useRouter();
 const { tableContainer, tableMaxHeight } = useTableHeight();
 
+const value = ref('');
+
+const dataPickerValue = ref([]);
+const loading = ref(false);
+const tableData = ref([]);
 const total = ref(0);
-const value1 = ref('');
+const options = [];
 
 const queryParams = ref({
   pageNum: 1,
-  pageSize: 10
+  pageSize: 10,
+  userName: '',
+  status: '',
+  phone: '',
 })
-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 handleCleanOptions = () => {
-  console.log(123123);
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    userName: '',
+    status: '',
+    phone: '',
+  };
+  dataPickerValue.value = [];
+  getList();
+}
+
+const jumpDetails = ({ id }) => {
+  router.push({
+    path: '/voice/call/details',
+    query: { id }
+  })
 }
 
-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 getList = () => {
+  const [timeBegin, timeEnd] = dataPickerValue.value;
+
+  loading.value = true;
+
+  workbenchApi.getCallRecordList({...queryParams.value, timeBegin, timeEnd}).then(({ rows, total:t }) => {
+    const typeEnum = { 0: '白名单', 1: 'AI客服', 2: '传统服务' };
+    const statusEnum = { 0: '未接听', 1: '已接通' };
+    const serviceCategoryEnum = { 0: '人工坐席', 1: '机器人坐席', 2: '机器人转人工' };
+    tableData.value = rows.map(item => ({
+      ...item,
+      typeText: typeEnum[item.type],
+      statusText: statusEnum[item.status],
+      serviceCategoryText: serviceCategoryEnum[item.serviceCategory]
+    }));
+
+    loading.value = false;
+    total.value = t;
+  })
 };
 
+onMounted(() => {
+  getList();
+})
 
 </script>
 
@@ -138,27 +77,32 @@ function getList() {
       <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.phone"></el-input>
           </SearchItemWrapper>
         </el-col>
         <el-col :span="6">
           <SearchItemWrapper label="客服">
-            <el-select v-model="value" placeholder="Select" size="large">
+            <el-select v-model="queryParams.userName" 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-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 v-model="queryParams.status" placeholder="Select" size="large" :empty-values="[null, undefined]">
+              <el-option label="全部" value="" />
+              <el-option label="未接听" :value="0" />
+              <el-option label="已接通" :value="1" />
             </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 v-model="value" placeholder="Select" size="large" :empty-values="[null, undefined]">
+              <el-option label="全部" value="" />
+              <el-option label="呼入" :value="0" />
+              <el-option label="呼出" :value="1" />
             </el-select>
           </SearchItemWrapper>
         </el-col>
@@ -166,13 +110,13 @@ function getList() {
       <el-row :gutter="24">
         <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" />
+            <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="18">
           <div class="flex items-center justify-start space-x-[30px]">
-            <div class="custom-btn custom-btn_primary">搜索</div>
+            <div class="custom-btn custom-btn_primary" @click="getList">搜索</div>
             <div class="custom-btn custom-btn_default">批量下载语音</div>
             <div class="custom-btn custom-btn_text space-x-[2px]" @click="handleCleanOptions">
               <img src="@/assets/images/workbench/icon-clean.svg" alt="">
@@ -184,32 +128,33 @@ function getList() {
     </div>
     <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" width="100" />
-          <el-table-column prop="address" label="通话状态" align="center" width="100">
+        <el-table :data="tableData" style="width: 100%" :max-height="tableMaxHeight" v-loading="loading">
+          <el-table-column prop="phone" label="用户电话" align="center" width="130" fixed />
+          <el-table-column prop="typeText" label="呼叫类型" align="center" width="100" />
+          <el-table-column prop="statusText" label="通话状态" align="center" width="100">
             <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>
+                <span class="w-[6px] h-[6px] rounded-full" :class="[scope.row.status === 1 ? 'bg-[#65C734]': 'bg-[#c75134]']"></span>
+                <span>{{ scope.row.statusText }}</span>
               </div>
             </template>
           </el-table-column>
-          <el-table-column prop="address" label="用户电话" align="center" />
-          <el-table-column prop="address" label="客服" align="center" />
-          <el-table-column prop="address" label="服务类型" align="center" />
+          <el-table-column prop="timeBegin" label="通话发起时间" align="center" width="180" />
+          <el-table-column prop="timeEnd" label="通话结束时间" align="center" width="180" />
+          <el-table-column prop="times" label="通话时长" align="center" />
           <el-table-column prop="address" label="通话录音" align="center" width="350">
             <template #default="scope">
               <div class="flex justify-center">
-                <AudioPlayer></AudioPlayer>
+                <AudioPlayer :audioUrl="scope.row.url"></AudioPlayer>
               </div>
             </template>
           </el-table-column>
-          <el-table-column prop="address" label="通话发起时间" align="center" />
-          <el-table-column prop="address" label="操作" align="center" fixed="right" width="150">
+          <el-table-column prop="serviceCategoryText" label="服务类型" align="center" width="120"/>
+          <el-table-column prop="userName" label="客服" align="center" />
+          <el-table-column prop="handle" label="操作" align="center" fixed="right" width="150">
             <template #default="scope">
               <div class="flex justify-center space-x-[20px]">
-                <span class="text-[#165DFF] cursor-pointer">详情</span>
+                <span class="text-[#165DFF] cursor-pointer" @click="jumpDetails(scope.row)">详情</span>
                 <span class="text-[#165DFF] cursor-pointer">语音下载</span>
               </div>
             </template>
@@ -245,4 +190,4 @@ function getList() {
     height: 100%;
   }
 }
-</style>
+</style>

+ 393 - 0
src/views/voice/workbench/index copy.vue

@@ -0,0 +1,393 @@
+<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({
+  pageNum: 1,
+  pageSize: 10,
+  category: 0,
+  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);
+
+// 切换tabs
+const handleChangeTab = (index) => {
+  queryParams.value.pageNum = 1;
+  queryParams.value.category = index;
+  tabCallRecordList.value = [];
+  initTabsData();
+}
+
+// 选中通话记录
+const hanldeTabItem = (i) => {
+  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 = [];
+  initTabsData();
+}
+
+// 拨打电话
+const onConfirm = () => {
+  voiceStore.onMakingCall("15810954324");
+}
+
+const initTabsData = async () => {
+  loading.value = true;
+  const { rows, total: t } = await workbenchApi.getCallRecordList(queryParams.value);
+
+  tabCallRecordList.value = [...tabCallRecordList.value, ...rows];
+  total.value = t;
+  loading.value = false;
+}
+
+onMounted(async () => {
+  initTabsData();
+});
+
+const loadMoreData = () => {
+  queryParams.value.pageNum += 1;
+  initTabsData();
+}
+</script>
+
+<template>
+  <div class="workbench-viewport space-x-[16px]">
+    <div class="record-section">
+      <ul class="tabs-nav space-x-[48px]">
+        <li v-for="item, index in tabEnum" :class="['tabs-nav-item', { active: queryParams.category === index }]"
+          :key="item" @click="handleChangeTab(index)">{{ item }}</li>
+      </ul>
+      <div class="tabs-content">
+        <div class="search-inp-wrapper">
+          <div class="search-inp">
+            <input type="text" class="inp" placeholder="请输入电话号码" v-model="queryParams.phone">
+            <div class="btn" @click="onSearch">搜索</div>
+          </div>
+        </div>
+        <div class="search-result-wrapper">
+          <el-scrollbar height="100%">
+            <div 
+              class="search-result-inner space-y-[8px]"
+              v-infinite-scroll="loadMoreData"
+              :infinite-scroll-disabled="disabled"
+              v-show="tabCallRecordList.length"
+            >
+              <RecordCardItem 
+                v-for="item, index in tabCallRecordList"
+                :data="item"
+                :index="index"
+                :active="tabCurrentActive === index"
+                :key="item.id"
+                @on-click="hanldeTabItem(index)"
+              ></RecordCardItem>
+              <div class="flex justify-center text-[#999] text-[12px]">
+                <p class="pb-[6px]" v-if="loading">Loading...</p>
+              </div>
+            </div>
+            
+            <div class="flex items-center justify-center pt-[100px]" v-show="!tabCallRecordList.length">
+              <span class="text-[#999] text-[14px]">暂无数据</span>
+            </div>
+          </el-scrollbar>
+        </div>
+      </div>
+    </div>
+    <div class="details-section">
+      <div class="empty-wrapper" v-show="!callDetails.id">
+        <img src="@/assets/images/workbench/img-empty.png" alt="">
+        <p class="empty-text">
+          <span>Hi, 下午好~</span>
+          <span>欢迎登录智能语音客服</span>
+        </p>
+      </div>
+      <div class="details-wrapper" v-show="callDetails.id">
+        <h4 class="title">通话详情</h4>
+        <el-scrollbar class="details-scrollbar">
+          <CallView></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>
+
+<style lang="scss" scoped>
+$primaryColor: #165DFF;
+
+.workbench-viewport {
+  display: flex;
+  height: 100%;
+  background: #eceff6;
+
+  .record-section {
+    flex-shrink: 0;
+    width: 292px;
+    height: 100%;
+    border-radius: 8px;
+    background: linear-gradient(180deg, #FFF 0%, #FFF 100%);
+
+    .tabs-nav {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 46px;
+      padding-top: 15px;
+      border-bottom: 1px solid #E5E6EB;
+      font-size: 14px;
+      line-height: 20px;
+      color: #4E5969;
+
+      .tabs-nav-item {
+        position: relative;
+        cursor: pointer;
+
+        &.active {
+          color: $primaryColor;
+          font-weight: bold;
+
+          &::after {
+            position: absolute;
+            left: 0;
+            bottom: -6px;
+            content: ' ';
+            display: block;
+            width: 100%;
+            height: 2px;
+            background: $primaryColor;
+          }
+        }
+      }
+
+    }
+
+    .tabs-content {
+      height: calc(100% - 46px);
+
+      .search-inp-wrapper {
+        padding: 12px 22px;
+
+        .search-inp {
+          display: flex;
+          align-items: center;
+          height: 34px;
+          padding: 2px;
+          border-radius: 8px;
+          background: #F2F4F7;
+
+          .inp {
+            width: 100%;
+            padding: 0 10px;
+            background: transparent;
+            outline: none;
+            font-size: 13px;
+            color: #1D2129;
+          }
+
+          .btn {
+            flex-shrink: 0;
+            width: 52px;
+            height: 30px;
+            border-radius: 8px;
+            background: #165DFF;
+            color: #FFF;
+            font-size: 13px;
+            line-height: 30px;
+            text-align: center;
+            cursor: pointer;
+          }
+        }
+      }
+
+      .search-result-wrapper {
+        height: calc(100% - 58px);
+
+        .search-result-inner {
+          padding: 0 22px;
+        }
+      }
+    }
+  }
+
+  .details-section {
+    width: 100%;
+    min-width: 700px;
+    height: 100%;
+    border-radius: 8px;
+    overflow: hidden;
+    padding: 20px;
+    background: #fff;
+
+    .details-scrollbar {
+      height: calc(100% - 50px);
+    }
+
+    .empty-wrapper {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-flow: column;
+      width: 100%;
+      height: 100%;
+
+      .empty-text {
+        span {
+          display: block;
+          text-align: center;
+          font-weight: bold;
+          font-size: 24px;
+          line-height: 32px;
+
+          &:nth-child(1) {
+            color: #165DFF;
+          }
+
+          &:nth-child(2) {
+            color: #1D2129;
+          }
+        }
+      }
+    }
+
+    .details-wrapper {
+      height: 100%;
+
+      .title {
+        margin-bottom: 24px;
+        color: #1D2129;
+        font-size: 18px;
+        font-weight: bold;
+        line-height: 26px;
+      }
+
+      .details-inner {
+        :deep(.custom-label) {
+          display: inline-block;
+          width: 84px;
+          color: #86909C;
+          box-sizing: border-box;
+          font-size: 14px;
+          font-weight: normal;
+          line-height: 23px;
+          text-align: left;
+        }
+
+        :deep(.custom-colums) {
+          font-size: 14px;
+          color: #1D2129;
+        }
+
+        .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: 14px;
+          }
+
+          .record-play-content {
+            padding-top: 12px;
+            color: #4E5969;
+            font-family: "PingFang SC";
+            font-size: 13px;
+            line-height: 20px;
+          }
+        }
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-textarea__inner) {
+  background: #f2f4f7;
+}
+</style>

+ 3 - 116
src/views/voice/workbench/index.vue

@@ -1,12 +1,12 @@
 <script setup>
 import { ElMessage } from 'element-plus';
-import { storeToRefs } from 'pinia';
 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({
   pageNum: 1,
@@ -166,81 +166,7 @@ const loadMoreData = () => {
       <div class="details-wrapper" v-show="callDetails.id">
         <h4 class="title">通话详情</h4>
         <el-scrollbar class="details-scrollbar">
-          <div class="details-inner">
-            <el-descriptions title="">
-              <el-descriptions-item label="呼入分类" label-class-name="custom-label" class-name="custom-colums">
-                <span class="text-[#FF3636]">{{ typeEnum[callDetails.type] }}</span>
-              </el-descriptions-item>
-              <el-descriptions-item label="客服" label-class-name="custom-label" class-name="custom-colums">
-                {{ callDetails.userName }}
-              </el-descriptions-item>
-              <el-descriptions-item label="服务类型" label-class-name="custom-label" class-name="custom-colums">
-                {{ categoryTypeEnum[callDetails.serviceCategory] }}
-              </el-descriptions-item>
-              <el-descriptions-item label="通话发起时间" label-class-name="custom-label" class-name="custom-colums">
-                {{ callDetails.timeBegin }}
-              </el-descriptions-item>
-              <el-descriptions-item label="通话结束时间" label-class-name="custom-label" class-name="custom-colums">
-                {{ callDetails.timeEnd }}
-              </el-descriptions-item>
-              <el-descriptions-item label="通话时长" label-class-name="custom-label" class-name="custom-colums">
-                {{callDetails.times }}
-              </el-descriptions-item>
-              <el-descriptions-item label="通话类型" label-class-name="custom-label" class-name="custom-colums">
-                {{ callDetails.category == 0 ? '呼入': '呼出' }}
-              </el-descriptions-item>
-              <el-descriptions-item label="通话状态" label-class-name="custom-label" class-name="custom-colums">
-                {{ callDetails.status == 0 ? '未接听' : '已接通' }}
-              </el-descriptions-item>
-              <el-descriptions-item label="业务类型" label-class-name="custom-label" class-name="custom-colums">{{ callDetails.bussinessType }}</el-descriptions-item>
-              <el-descriptions-item label="电话号码" label-class-name="custom-label" class-name="custom-colums">
-                <div class="inline-block">
-                  <div class="flex items-center space-x-[4px]">
-                    <span>{{ callDetails.phone }}</span>
-                    <el-popconfirm
-                      width="250"
-                      icon-color="#626AEF"
-                      title="请确认,是否呼叫该电话号码?"
-                      @confirm="onConfirm"
-                    >
-                      <template #reference>
-                        <img src="@/assets/images/workbench/icon-call-square.svg" alt="" class="cursor-pointer">
-                      </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>
-                </div>
-              </el-descriptions-item>
-            </el-descriptions>
-            <custom-row-item label="备注">
-              <div class="space-x-[14px]">
-                <span class="custom-colums">{{ callDetails.remark ? callDetails.remark : '暂无' }}</span>
-                <span class="text-[#165DFF] text-[14px] cursor-pointer" @click="handleEdit">编辑</span>
-              </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>
-            </custom-row-item>
-          </div>
+          <CallView :data="callDetails" noInit></CallView>
         </el-scrollbar>
       </div>
     </div>
@@ -412,47 +338,8 @@ $primaryColor: #165DFF;
         font-weight: bold;
         line-height: 26px;
       }
-
-      .details-inner {
-        :deep(.custom-label) {
-          display: inline-block;
-          width: 84px;
-          color: #86909C;
-          box-sizing: border-box;
-          font-size: 14px;
-          font-weight: normal;
-          line-height: 23px;
-          text-align: left;
-        }
-
-        :deep(.custom-colums) {
-          font-size: 14px;
-          color: #1D2129;
-        }
-
-        .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: 14px;
-          }
-
-          .record-play-content {
-            padding-top: 12px;
-            color: #4E5969;
-            font-family: "PingFang SC";
-            font-size: 13px;
-            line-height: 20px;
-          }
-        }
-      }
     }
+
   }
 }