Browse Source

reactor:工单受理调整(智能化对接);
reactor:智能回访调整;
feat:新增扭转待页面;

zhangchong 11 tháng trước cách đây
mục cha
commit
ed094586c6

+ 1 - 1
.env.development

@@ -44,5 +44,5 @@ VITE_RECORD_DOWNLOAD_PREFIX=http://222.213.23.229:10085
 VITE_JTHS_API_URL=http://118.121.58.161:19021
 
 # 捷通华声AppKey
-VITE_JTHS_APPKEY=l7ft7M
+VITE_JTHS_APPKEY=MTAwMDAx
 

+ 1 - 1
.env.production

@@ -44,4 +44,4 @@ VITE_RECORD_DOWNLOAD_PREFIX=http://222.213.23.229:10085
 VITE_JTHS_API_URL=http://118.121.58.161:19021
 
 # 捷通华声AppKey
-VITE_JTHS_APPKEY=l7ft7M
+VITE_JTHS_APPKEY=MTAwMDAx

+ 1 - 1
.env.st

@@ -44,4 +44,4 @@ VITE_RECORD_DOWNLOAD_PREFIX=http://222.213.23.229:10085
 VITE_JTHS_API_URL=http://118.121.58.161:19021
 
 # 捷通华声AppKey
-VITE_JTHS_APPKEY=l7ft7M
+VITE_JTHS_APPKEY=MTAwMDAx

+ 2 - 2
.env.yibin

@@ -41,7 +41,7 @@ VITE_RECORD_PREFIX=http://218.6.151.146:50104
 VITE_RECORD_DOWNLOAD_PREFIX=http://192.168.2.212:29003
 
 # 捷通华声通话记录请求地址
-VITE_JTHS_API_URL=http://118.121.58.161:19021
+VITE_JTHS_API_URL=http://192.168.0.86:19021
 
 # 捷通华声AppKey
-VITE_JTHS_APPKEY=l7ft7M
+VITE_JTHS_APPKEY=MTAwMDAx

+ 19 - 8
src/components/AudioPlayer/index.vue

@@ -242,14 +242,25 @@ const loading = ref(false);
 // 下载
 const downLoad = () => {
 	loading.value = true;
-	fileDownload({ path: import.meta.env.VITE_RECORD_DOWNLOAD_PREFIX + props.recordingAbsolutePath })
-		.then((res: any) => {
-			downloadFileByStream(res, <string>props.fileName);
-			loading.value = false;
-		})
-		.catch(() => {
-			loading.value = false;
-		});
+  if(props.recordingAbsolutePath){ // 有绝对路径需要拼接
+    fileDownload({ path: import.meta.env.VITE_RECORD_DOWNLOAD_PREFIX + props.recordingAbsolutePath })
+      .then((res: any) => {
+        downloadFileByStream(res, <string>props.fileName);
+        loading.value = false;
+      })
+      .catch(() => {
+        loading.value = false;
+      });
+  }else{ // 无绝对路径直接下载
+    fileDownload({ path: props.url })
+      .then((res: any) => {
+        downloadFileByStream(res, <string>props.fileName);
+        loading.value = false;
+      })
+      .catch(() => {
+        loading.value = false;
+      });
+  }
 };
 nextTick(() => {
 	audioRef.value.onerror = () => {

+ 8 - 3
src/layout/navBars/breadcrumb/telControl.vue

@@ -1778,8 +1778,9 @@ const connectVoiceAssistant = async (telNo: string) => {
 	try {
 		const { result } = await voiceAssistant(telNo);
 		const uid = `8#User-${result.uid}`;
-		const subscribe = `/trans/${result.orgCode}/${result.groupUid}/${uid}`;
-		socket.value = useSocket(import.meta.env.VITE_VOICE_ASSISTANT_SOCKET_URL, { uid, subscribe });
+		const trans = `/trans/${result.orgCode}/${result.groupUid}/${uid}`;
+    const notice = `/notice/${result.orgCode}/${result.groupUid}/${uid}`;
+		socket.value = useSocket(import.meta.env.VITE_VOICE_ASSISTANT_SOCKET_URL, { uid, trans,notice });
 		socket.value.on('open', () => {
 			socket.value.send({ id: '', type: 1, from: uid, to: 'sys', timestamps: new Date().getTime(), body: result.userName });
 			console.log('坐席辅助连接成功');
@@ -1803,11 +1804,15 @@ const seatAssistOff = () => {
 const wsReceive = (message: any) => {
 	try {
 		const data = JSON.parse(message.data);
-		const toTransfer = socket.value.socket.opts.subscribe;
+		const toTransfer = socket.value.socket.opts.trans;
+    const toNoticer = socket.value.socket.opts.notice;
 		if (data.to === toTransfer) {
 			// 判断当前转写消息是否是发送给当前用户的
 			mittBus.emit('wsReceive', message);
 		}
+    if (data.to === toNoticer) { // 推送消息
+      mittBus.emit('wsNotice', message);
+    }
 	} catch (e) {
 		return;
 	}

+ 1 - 0
src/types/mitt.d.ts

@@ -17,4 +17,5 @@ declare type MittType = {
 	SeatState?: any; //坐席监控
 	RestApplyPass?: any; //休息申请
 	outboundConnect?: any; //外呼连接
+	wsNotice: any;
 };

+ 16 - 4
src/utils/websocket.ts

@@ -4,7 +4,8 @@ interface SocketOptions {
 	maxReconnectAttempts?: number;
 	isReconnect?: boolean;
 	uid?: string;
-	subscribe?: string;
+	trans?: string;
+	notice?:string;
 }
 
 class Socket {
@@ -23,7 +24,8 @@ class Socket {
 			maxReconnectAttempts: 99, // 最大重连次数
 			isReconnect: true, // 是否需要重连
 			uid: '', // 用户id
-			subscribe: '', // 订阅的频道
+			trans: '', // 订阅的频道
+			notice: '', // 订阅的频道
 			...opts,
 		};
 
@@ -97,15 +99,25 @@ class Socket {
 		// @ts-ignore
 		const moreTime = this.opts.heartbeatInterval + 100;
 		setTimeout(() => {
-			if (!this.opts.subscribe) return;
+			if (!this.opts.trans) return;
 			this.send({
 				id: '',
 				type: 8,
 				from: this.opts.uid,
-				to: this.opts.subscribe,
+				to: this.opts.trans,
 				timestamps: new Date().getTime(),
 				body: 'subscribe',
 			});
+			if (this.opts.notice) {
+				this.send({
+					id: '',
+					type: 8,
+					from: this.opts.uid,
+					to: this.opts.notice,
+					timestamps: new Date().getTime(),
+					body: 'subscribe',
+				});
+			}
 		}, moreTime);
 	}
 	stopHeartbeat() {

+ 97 - 0
src/views/business/visit/component/Reverse-audit.vue

@@ -0,0 +1,97 @@
+<template>
+  <el-dialog v-model="state.dialogVisible" draggable title="扭转评判" width="40%" append-to-body destroy-on-close @close="close">
+    <div class="collapse-container" v-loading="state.loading">
+      <el-form label-width="110px" ref="ruleFormRef" :model="state.ruleForm">
+        <el-row :gutter="35">
+          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="12">
+            <el-form-item label="评判结果" prop="isPass" :rules="[{ required: true, message: '请选择评判结果', trigger: 'change' }]">
+              <el-radio-group v-model="state.ruleForm.isPass">
+                <el-radio :label="true">同意</el-radio>
+                <el-radio :label="false">不同意</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+            <el-form-item label="评判意见" prop="auditContent" :rules="[{ required: true, message: '请填评判意见', trigger: 'blur' }]">
+              <el-input
+                v-model="state.ruleForm.auditContent"
+                type="textarea"
+                :autosize="{ minRows: 6, maxRows: 10 }"
+                placeholder="请填评判意见"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+    <template #footer>
+			<span class="dialog-footer">
+				<el-button @click="closeDialog" class="default-button">取 消</el-button>
+				<el-button type="primary" @click="onAudit(ruleFormRef)" :loading="state.loading">确 定</el-button>
+			</span>
+    </template>
+  </el-dialog>
+</template>
+<script setup lang="ts">
+import { defineAsyncComponent, reactive, ref } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import { secondHandleAuditBatch } from "@/api/business/secondHandle";
+
+// 引入组件
+const AnnexList = defineAsyncComponent(() => import('@/components/AnnexList/index.vue'));
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+// 定义变量内容
+const state = reactive<any>({
+  dialogVisible: false, // 是否显示弹窗
+  loading: false, // 是否显示加载
+  ruleForm: {
+    auditContent: '', // 审批意见
+    isPass: true, // 是否通过
+  },
+});
+const ruleFormRef = ref<RefType>();
+const ids = ref<any>([]);
+// 打开弹窗
+const openDialog = async (val: any) => {
+  state.loading = false;
+  ids.value = val;
+  state.dialogVisible = true;
+};
+// 关闭弹窗
+const closeDialog = () => {
+  state.dialogVisible = false;
+};
+const close = () => {
+  ruleFormRef.value?.clearValidate();
+};
+
+// 审批
+const onAudit = (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  formEl.validate((valid: boolean) => {
+    if (!valid) return;
+    state.loading = true;
+    const request = {
+      ids: ids.value,
+      auditContent: state.ruleForm.auditContent,
+      state: state.ruleForm.isPass ? 2 : 3, // 审核结果 0 待申请 1 待办 2 审批通过 3 审批不通过 4 已办理
+    };
+    secondHandleAuditBatch(request)
+      .then(() => {
+        state.loading = false;
+        closeDialog();
+        emit('updateList');
+        ElMessage.success('操作成功');
+      })
+      .catch(() => {
+        state.loading = false;
+      });
+  });
+};
+defineExpose({
+  openDialog,
+  closeDialog,
+});
+</script>

+ 22 - 2
src/views/business/visit/component/Smart-visit-Detail.vue

@@ -33,10 +33,20 @@
 					<span v-if="item.visitTarget === 20">{{ item.orgProcessingResults?.value }}</span>
 				</span>
 			</template>
-			<template #bmbjtd="{ row }">
+			<!--			<template #bmbjtd="{ row }">
 				<span v-for="item in row.orderVisit?.orderVisitDetails">
 					<span v-if="item.visitTarget === 20">{{ item.orgHandledAttitude?.value }}</span>
 				</span>
+			</template>-->
+			<template #bmsflx="{ row }">
+				<span v-for="item in row.orderVisit?.orderVisitDetails">
+					<span v-if="item.visitTarget === 20">{{ item.isContact === null ? '' : item.isContact === true ? '是' : '否' }}</span>
+				</span>
+			</template>
+			<template #cljg="{ row }">
+				<span v-for="item in row.orderVisit?.orderVisitDetails">
+					<span v-if="item.visitTarget === 20">{{ item.volved === null ? '' : item.volved === true ? '已得到解决' : '未得到解决' }}</span>
+				</span>
 			</template>
 		</ProTable>
 		<!-- 分页 -->
@@ -111,10 +121,20 @@ const columns = ref<any[]>([
 		prop: 'bmbjjg',
 		width: 170,
 	},
-	{
+	/*	{
 		label: '部门办件态度',
 		prop: 'bmbjtd',
 		width: 170,
+	},*/
+	{
+		label: '部门是否联系',
+		prop: 'bmsflx',
+		width: 170,
+	},
+	{
+		label: '处理结果',
+		prop: 'cljg',
+		width: 170,
 	},
 ]);
 // 打开弹窗

+ 131 - 62
src/views/business/visit/component/Visit-detail.vue

@@ -77,13 +77,13 @@
 									{{ state.orderDetail.fromPhone }}
 									<el-button
 										plain
-										title="录音文件"
+										title="人工回访录音"
 										size="small"
 										type="primary"
 										class="ml8"
 										@click="recordFile(state)"
 										v-if="state.recordingAbsolutePath"
-										>录音文件</el-button
+										>人工回访录音</el-button
 									>
 								</el-form-item>
 							</el-col>
@@ -117,6 +117,26 @@
 								<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
 									<el-form-item label="当前回访人">
 										{{ state.orderVisitModel.employeeName }}
+										<el-button
+											plain
+											title="智能回访录音"
+											size="small"
+											type="primary"
+											class="ml8"
+                      @click="onSmartRecord"
+                      v-if="state.smartRecord"
+											>智能回访录音
+										</el-button>
+										<el-button
+											plain
+											title="人工回访录音"
+											size="small"
+											type="primary"
+											class="ml8"
+											@click="recordFile"
+											v-if="state.recordingAbsolutePath"
+											>人工回访录音</el-button
+										>
 									</el-form-item>
 								</el-col>
 								<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" v-if="visitCount">
@@ -127,58 +147,93 @@
 										<span v-if="state.ruleForm.isPutThrough !== null">{{ state.ruleForm.isPutThrough ? '已接通' : '未接通' }}</span>
 									</el-form-item>
 								</el-col>-->
-								<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" >
-                  <el-row v-for="item in state.ruleForm.visitDetails" :key="item.id" :gutter="10">
-                    <!-- 务员评价 -->
-                    <template v-if="item.visitTarget === 10">
-                      <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-                        <el-form-item label="话务员评价">
-                          <el-radio-group v-model="item.seatEvaluate" disabled>
-                            <el-radio :label="item.key" v-for="item in seatEvaluate" :key="item.key">{{ item.value }}</el-radio>
-                          </el-radio-group>
-                        </el-form-item>
-                      </el-col>
-                      <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-                        <el-form-item label="话务员回访内容">
-                          {{ item.visitContent }}
-                        </el-form-item>
-                      </el-col>
-                    </template>
-                    <!-- 部门评价 -->
-                    <template v-if="item.visitTarget === 20">
-                      <el-divider content-position="left">
-                        <el-text tag="b" size="large"> {{ item.visitOrgName }} </el-text>
-                      </el-divider>
-                      <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
-                        <el-form-item label="部门办件结果">
-                          {{ item.orgProcessingResults?.dicDataName }}
-                        </el-form-item>
-                      </el-col>
-                      <!-- 不满意才会选择不满意原因 -->
-                      <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" v-if="item.orgNoSatisfiedReason && item.orgNoSatisfiedReason.length">
-                        <el-form-item label="不满意原因">
-                          {{ item.orgNoSatisfiedReason.map((item) => item.dicDataName).join(',') }}
-                        </el-form-item>
-                      </el-col>
-                      <!--										<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+								<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+									<el-row v-for="item in state.ruleForm.visitDetails" :key="item.id" :gutter="10">
+										<!-- 务员评价 -->
+										<template v-if="item.visitTarget === 10">
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+												<el-form-item label="话务员评价">
+													<el-radio-group v-model="item.seatEvaluate" disabled>
+														<el-radio :label="item.key" v-for="item in seatEvaluate" :key="item.key">{{ item.value }}</el-radio>
+													</el-radio-group>
+												</el-form-item>
+											</el-col>
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+												<el-form-item label="话务员回访内容">
+													{{ item.visitContent }}
+												</el-form-item>
+											</el-col>
+										</template>
+										<!-- 部门评价 -->
+										<template v-if="item.visitTarget === 20">
+											<el-divider content-position="left">
+												<el-text tag="b" size="large"> {{ item.visitOrgName }} </el-text>
+											</el-divider>
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+												<el-form-item label="部门是否联系">
+													{{ item.orgProcessingResults?.dicDataName }}
+												</el-form-item>
+											</el-col>
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+												<el-form-item label="处理结果">
+													{{ item.orgProcessingResults?.dicDataName }}
+												</el-form-item>
+											</el-col>
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+												<el-form-item label="备注">
+													{{ item.orgProcessingResults?.dicDataName }}
+												</el-form-item>
+											</el-col>
+											<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+												<el-form-item label="部门办件结果">
+													{{ item.orgProcessingResults?.dicDataName }}
+												</el-form-item>
+											</el-col>
+											<!-- 不满意才会选择不满意原因 -->
+											<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" v-if="item.orgNoSatisfiedReason && item.orgNoSatisfiedReason.length">
+												<el-form-item label="不满意原因">
+													{{ item.orgNoSatisfiedReason.map((item) => item.dicDataName).join(',') }}
+												</el-form-item>
+											</el-col>
+											<!--										<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
                         <el-form-item label="部门办件态度">
                           {{ item.orgHandledAttitude?.dicDataName }}
                         </el-form-item>
                       </el-col>-->
-                      <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-                        <el-form-item label="部门回访内容">
-                          {{ item.visitContent }}
-                        </el-form-item>
-                      </el-col>
-                    </template>
-                  </el-row>
-                </el-col>
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+												<el-form-item label="部门回访内容">
+													{{ item.visitContent }}
+												</el-form-item>
+											</el-col>
+										</template>
+									</el-row>
+								</el-col>
 							</template>
 							<!-- 编辑 -->
 							<template v-else>
 								<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
 									<el-form-item label="当前回访人">
 										{{ userInfos.name }}
+                    <el-button
+                      plain
+                      title="智能回访录音"
+                      size="small"
+                      type="primary"
+                      class="ml8"
+                      @click="onSmartRecord"
+                      v-if="state.smartRecord"
+                    >智能回访录音
+                    </el-button>
+                    <el-button
+                      plain
+                      title="人工回访录音"
+                      size="small"
+                      type="primary"
+                      class="ml8"
+                      @click="recordFile"
+                      v-if="state.recordingAbsolutePath"
+                    >人工回访录音</el-button
+                    >
 									</el-form-item>
 								</el-col>
 								<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
@@ -228,37 +283,39 @@
 											<el-divider content-position="left">
 												<el-text tag="b" size="large"> {{ item.visitOrgName }} </el-text>
 											</el-divider>
-<!--											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
 												<el-form-item
 													label="部门是否联系"
-													:prop="`visitDetails.${index}.seatEvaluate`"
+													:prop="`visitDetails.${index}.isContact`"
 													:rules="[{ required: true, message: '请选择部门是否联系', trigger: 'change' }]"
 												>
-													<el-radio-group v-model="item.seatEvaluate">
-														<el-radio :label="item.key" v-for="item in seatEvaluate" :key="item.key">{{ item.value }}</el-radio>
+													<el-radio-group v-model="item.isContact">
+														<el-radio :label="true">是</el-radio>
+														<el-radio :label="false">否</el-radio>
 													</el-radio-group>
 												</el-form-item>
 											</el-col>
 											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
 												<el-form-item
 													label="处理结果"
-													:prop="`visitDetails.${index}.seatEvaluate`"
+													:prop="`visitDetails.${index}.volved`"
 													:rules="[{ required: true, message: '请选择处理结果', trigger: 'change' }]"
 												>
-													<el-radio-group v-model="item.seatEvaluate">
-														<el-radio :label="item.key" v-for="item in seatEvaluate" :key="item.key">{{ item.value }}</el-radio>
+													<el-radio-group v-model="item.volved">
+														<el-radio :label="true">已得到解决</el-radio>
+														<el-radio :label="false">未得到解决</el-radio>
 													</el-radio-group>
 												</el-form-item>
 											</el-col>
-											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" v-if="item.volved === false">
 												<el-form-item
-													label="备注"
-													:prop="`visitDetails.${index}.visitContent`"
+													label=""
+													:prop="`visitDetails.${index}.volveConent`"
 													:rules="[{ required: false, message: '请填写备注', trigger: 'blur' }]"
 												>
 													<common-advice
 														@chooseAdvice="chooseAdvice($event, index)"
-														v-model="item.visitContent"
+														v-model="item.volveConent"
 														placeholder="请填写备注"
 														:loading="state.loading"
 														:commonEnum="commonEnum.Seat"
@@ -267,7 +324,7 @@
 														drawerWidth="40%"
 													/>
 												</el-form-item>
-											</el-col>-->
+											</el-col>
 											<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
 												<el-form-item
 													label="部门办件结果"
@@ -311,7 +368,7 @@
 													</el-select>
 												</el-form-item>
 											</el-col>
-											<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+											<!--											<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
 												<el-form-item
 													label="部门办件态度"
 													:prop="`visitDetails.${index}.orgHandledAttitude`"
@@ -332,7 +389,7 @@
 														<el-option v-for="items in visitSatisfaction" :key="items.dicDataValue" :label="items.dicDataName" :value="items" />
 													</el-select>
 												</el-form-item>
-											</el-col>
+											</el-col>-->
 											<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
 												<el-form-item
 													label="部门回访内容"
@@ -354,6 +411,12 @@
 										</template>
 									</el-row>
 								</el-col>
+<!--                <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+                  <el-form-item label="扭转满意度" prop="isPutThrough" :rules="[{ required: false, message: '请选择扭转满意度', trigger: 'change' }]">
+                    <el-checkbox v-model="state.ruleForm.isPutThrough">扭转部门满意度</el-checkbox>
+                    <el-checkbox v-model="state.ruleForm.isPutThrough">扭转坐席满意度</el-checkbox>
+                  </el-form-item>
+                </el-col>-->
 							</template>
 						</el-row>
 					</el-form>
@@ -397,6 +460,7 @@ const state = reactive<any>({
 	dialogVisible: false, // 是否显示弹窗
 	loading: false, // 是否显示加载
 	transform: 'translate(0px, 0px)', // 附件弹窗位置
+  smartRecord:null,
 	ruleForm: {
 		isPutThrough: false, //未接通
 		visitDetails: {},
@@ -431,6 +495,7 @@ const getBaseData = async (id: string) => {
 		state.orderDetail = res.result?.orderVisitModel?.order ?? {};
 		state.orderVisitModel = res.result?.orderVisitModel ?? {};
 		state.recordingAbsolutePath = res.result?.recordingAbsolutePath ?? '';
+    state.smartRecord = res.result?.orderVisitModel.recordUrl ?? '';
 		if (res.result?.orderVisitModel?.isPutThrough !== null) {
 			state.ruleForm.isPutThrough = !res.result?.orderVisitModel?.isPutThrough ?? false;
 		} else {
@@ -498,12 +563,16 @@ const mouseup = () => {
 const closeDialog = () => {
 	state.dialogVisible = false;
 };
-// 查看录音文件
+// 查看人工回访录音文件
 const playRecordRef = ref<RefType>();
 const recordFile = (obj: any) => {
-  console.log(obj,'2121')
-  const fileName = `工单编号-${obj.orderDetail?.no} 录音文件`
-  playRecordRef.value.openDialog(import.meta.env.VITE_RECORD_PREFIX + obj.recordingAbsolutePath,fileName,obj.recordingAbsolutePath);
+	const fileName = `工单编号-${obj.orderDetail?.no} 人工回访录音文件`;
+	playRecordRef.value.openDialog(import.meta.env.VITE_RECORD_PREFIX + obj.recordingAbsolutePath, fileName, obj.recordingAbsolutePath);
+};
+// 查看智能回访录音
+const onSmartRecord = () => {
+	const fileName = `工单编号-${state.orderDetail?.no} 智能回访录音文件`;
+	playRecordRef.value.openDialog( state.smartRecord, fileName);
 };
 // 呼叫
 const onCall = (phoneNumber: string) => {

+ 445 - 0
src/views/business/visit/reverse.vue

@@ -0,0 +1,445 @@
+<template>
+  <div class="business-visit-reverse-container layout-pd">
+    <!-- 搜索  -->
+    <el-card shadow="never">
+      <el-tabs v-model="state.queryParams.AuditState" @tab-change="handleQuery">
+        <el-tab-pane name="1" label="扭转待评判"></el-tab-pane>
+        <el-tab-pane name="2" label="扭转已评判"></el-tab-pane>
+      </el-tabs>
+      <el-form :model="state.queryParams" ref="ruleFormRef" @submit.native.prevent label-width="100px">
+        <el-row :gutter="10">
+          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+            <el-form-item label="工单标题" prop="Title">
+              <el-input v-model="state.queryParams.Title" placeholder="工单标题" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+            <el-form-item label="工单编码" prop="No">
+              <el-input v-model="state.queryParams.No" placeholder="工单编码" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+            <el-form-item label="来源渠道" prop="SourceChannel">
+              <el-select v-model="state.queryParams.SourceChannel" placeholder="请选择来源渠道" clearable class="w100" @change="handleQuery">
+                <el-option v-for="item in state.sourceChannelOptions" :value="item.dicDataValue" :key="item.dicDataValue" :label="item.dicDataName" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="受理类型" prop="AcceptType">
+                <el-select v-model="state.queryParams.AcceptType" placeholder="请选择受理类型" clearable class="w100" @change="handleQuery">
+                  <el-option v-for="item in state.acceptTypeOptions" :value="item.dicDataValue" :key="item.dicDataValue" :label="item.dicDataName" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="会签类型" prop="CounterSignType">
+                <el-select v-model="state.queryParams.CounterSignType" placeholder="会签类型" class="w100" @change="handleQuery" clearable>
+                  <el-option v-for="item in state.counterSignTypeOptions" :value="item.key" :key="item.key" :label="item.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="一级部门名称" prop="OrgLevelOneName">
+                <el-input v-model="state.queryParams.OrgLevelOneName" placeholder="一级部门名称" clearable @keyup.enter="handleQuery" />
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="接办部门" prop="ActualHandleOrgName">
+                <el-input v-model="state.queryParams.ActualHandleOrgName" placeholder="接办部门名称" clearable @keyup.enter="handleQuery" />
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" v-show="!searchCol">
+              <el-form-item label="接办时间" prop="crTime">
+                <el-date-picker
+                  v-model="state.queryParams.crTime"
+                  type="datetimerange"
+                  unlink-panels
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  :shortcuts="shortcuts"
+                  @change="timeStartChangeCr"
+                  value-format="YYYY-MM-DD[T]HH:mm:ss"
+                />
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" v-show="!searchCol">
+              <el-form-item label="办结时间" prop="exTime">
+                <el-date-picker
+                  v-model="state.queryParams.exTime"
+                  type="datetimerange"
+                  unlink-panels
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  :shortcuts="shortcuts"
+                  @change="timeStartChangeEx"
+                  value-format="YYYY-MM-DD[T]HH:mm:ss"
+                />
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" v-show="!searchCol">
+              <el-form-item label="受理时间" prop="slTime">
+                <el-date-picker
+                  v-model="state.queryParams.slTime"
+                  type="datetimerange"
+                  unlink-panels
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  :shortcuts="shortcuts"
+                  @change="timeStartChangeSl"
+                  value-format="YYYY-MM-DD[T]HH:mm:ss"
+                />
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" v-show="!searchCol">
+              <el-form-item label="回访时间" prop="hfTime">
+                <el-date-picker
+                  v-model="state.queryParams.hfTime"
+                  type="datetimerange"
+                  unlink-panels
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  :shortcuts="shortcuts"
+                  @change="timeStartChangeHf"
+                  value-format="YYYY-MM-DD[T]HH:mm:ss"
+                />
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="被回访部门" prop="VisitOrgName">
+                <el-input v-model="state.queryParams.VisitOrgName" placeholder="被回访部门名称" clearable @keyup.enter="handleQuery" />
+              </el-form-item>
+            </el-col>
+          </transition>
+          <transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="部门办件结果" prop="OrgProcessingResults">
+                <el-select
+                  v-model="state.queryParams.OrgProcessingResults"
+                  placeholder="请选择部门办件结果"
+                  clearable
+                  class="w100"
+                  @change="handleQuery"
+                >
+                  <el-option v-for="item in state.visitSatisfaction" :value="item.dicDataValue" :key="item.dicDataValue" :label="item.dicDataName" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </transition>
+          <!--					<transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="部门办件态度" prop="OrgHandledAttitude">
+                <el-input v-model="state.queryParams.OrgHandledAttitude" placeholder="部门办件态度" clearable @keyup.enter="handleQuery" />
+              </el-form-item>
+            </el-col>
+          </transition>-->
+          <transition name="el-zoom-in-top" v-show="!searchCol">
+            <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+              <el-form-item label="不满意原因" prop="OrgNoSatisfiedReason">
+                <el-select
+                  v-model="state.queryParams.OrgNoSatisfiedReason"
+                  placeholder="请选择不满意原因"
+                  clearable
+                  class="w100"
+                  @change="handleQuery"
+                >
+                  <el-option v-for="item in state.dissatisfiedReason" :value="item.dicDataValue" :key="item.dicDataValue" :label="item.dicDataName" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </transition>
+          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
+            <el-form-item label=" ">
+              <div class="flex-end w100">
+                <el-button type="primary" @click="handleQuery" :loading="state.loading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+                <el-button @click="resetQuery(ruleFormRef)" class="default-button" :loading="state.loading">
+                  <SvgIcon name="ele-Refresh" class="mr5" />重置
+                </el-button>
+                <el-button link type="primary" @click="closeSearch" :loading="state.loading">
+                  {{ searchCol ? '展开' : '收起' }}
+                  <SvgIcon :class="{ 'is-reverse': searchCol }" name="ele-ArrowUp" class="mr5 arrow" size="18px" />
+                </el-button>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+    <el-card shadow="never">
+      <ProTable
+        ref="proTableRef"
+        :columns="columns"
+        :data="state.tableData"
+        @updateTable="queryList"
+        :loading="state.loading"
+        :total="state.total"
+        v-model:page-index="state.queryParams.PageIndex"
+        v-model:page-size="state.queryParams.PageSize"
+      >
+        <!-- 表格 header 按钮 -->
+        <template #tableHeader="scope">
+          <el-button
+            type="primary"
+            @click="onReverseMultiple"
+            :disabled="!scope.isSelected"
+            :loading="state.loading"
+            v-auth="'business:visit:reverse:multiple'"
+          >批量扭转退回
+          </el-button>
+        </template>
+        <template #expiredStatusText="{ row }">
+          <span :class="'overdue-status-' + row.order?.expiredStatus" :title="row.order?.expiredStatusText"></span>
+        </template>
+        <template #title="{ row }">
+          <order-detail :order="row.order" @updateList="queryList">{{ row.order?.title }}</order-detail>
+        </template>
+        <template #visitTarget="{ row }">
+          <span v-if="row.visitTarget === 20">{{ row.visitOrgName }}</span>
+        </template>
+        <template #orgProcessingResults="{ row }">
+          <span v-if="row.visitTarget === 20">{{ row.orgProcessingResults?.value }}</span>
+        </template>
+        <template #orgHandledAttitude="{ row }">
+          <span v-if="row.visitTarget === 20">{{ row.orgHandledAttitude?.value }}</span>
+        </template>
+        <template #orgNoSatisfiedReason="{ row }">
+					<span v-if="row.visitTarget === 20">
+						{{ row.orgNoSatisfiedReason?.map((item) => item.value).join(',') }}
+					</span>
+        </template>
+        <!-- 表格操作 -->
+        <template #operation="{ row }">
+          <el-button link type="primary" @click="onReverse(row)" title="扭转退回" v-auth="'business:visit:reverse:multiple'">
+            扭转退回
+          </el-button>
+          <order-detail :order="row.order" @updateList="queryList" />
+        </template>
+      </ProTable>
+    </el-card>
+    <!-- 扭转退回 -->
+    <reverse-audit ref="reverseAuditRef" @updateList="queryList" />
+  </div>
+</template>
+<script setup lang="tsx" name="businessVisitReverse">
+import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
+import { ElButton, FormInstance } from 'element-plus';
+import { formatDate } from '@/utils/formatTime';
+import { screenApplyList, screenBaseData } from '@/api/business/discern';
+import { commonEnum, shortcuts } from '@/utils/constants';
+import other from '@/utils/other';
+import { secondHandleBase } from '@/api/business/secondHandle';
+// 引入组件
+const OrderDetail = defineAsyncComponent(() => import('@/components/OrderDetail/index.vue')); // 工单详情
+const ReverseAudit = defineAsyncComponent(() => import('@/views/business/visit/component/Reverse-audit.vue')); // 扭转退回
+// 定义变量内容
+const ruleFormRef = ref<RefType>(); // 表单ref
+const proTableRef = ref<RefType>(); // 表格ref
+// 表格配置项
+const columns = ref<any[]>([
+  { type: 'selection', fixed: 'left', width: 55, align: 'center' },
+  { prop: 'order.expiredStatusText', label: '超期状态', align: 'center',width: 80 },
+  { prop: 'order.statusText', label: '工单状态', width: 100 },
+  { prop: 'order.sourceChannel', label: '来源方式', width: 100 },
+  { prop: 'order.actualHandleStepName', label: '办理节点', width: 150 },
+  { prop: 'order.no', label: '工单编码', width: 150 },
+  { prop: 'order.title', label: '工单标题', width: 300 },
+  {
+    prop: 'order.startTime',
+    label: '受理时间',
+    width: 170,
+    render: (scope) => {
+      return <span>{formatDate(scope.row.order?.startTime, 'YYYY-mm-dd HH:MM:SS')}</span>;
+    },
+  },
+  { prop: 'order.actualHandleOrgName', label: '接办部门', width: 150 },{
+    prop: 'order.filedTime',
+    label: '办结时间',
+    width: 170,
+    render: (scope) => {
+      return <span>{formatDate(scope.row.order?.filedTime, 'YYYY-mm-dd HH:MM:SS')}</span>;
+    },
+  },
+  { prop: 'visit.employeeName', label: '回访人', width: 170 },
+  { prop: 'order.acceptType', label: '受理类型', width: 120 },
+  { prop: 'order.acceptorName', label: '受理人', width: 170 },
+  { prop: 'order.hotspotName', label: '热点分类', width: 200 },
+  { prop: 'operation', label: '操作', fixed: 'right', width: 170, align: 'center' },
+]);
+const state = reactive<any>({
+  queryParams: {
+    // 查询条件
+    PageIndex: 1,
+    PageSize: 10,
+    AuditState:'1',
+    Keyword: null, // 关键字
+    IsProvince: null,
+    IsHomePage: null,
+    CounterSignType: null, // 会签类型
+    OrgLevelOneName: null, // 一级部门
+    ActualHandleOrgName: null, // 接办部门名称
+    slTime: [], // 受理时间
+    hfTime: [], // 回访时间
+    VisitOrgName: null, // 回访部门
+    OrgProcessingResults: null, // 部门办件结果
+    OrgNoSatisfiedReason: null, // 不满意原因
+  },
+  tableData: [], //表单
+  loading: false, // 加载
+  total: 0, // 总数
+  screenStatus: [], // 甄别状态
+  screenType: [], // 甄别类型
+  counterSignTypeOptions: [], // 会签类型
+  acceptTypeOptions: [], // 受理类型
+  sourceChannelOptions: [], // 来源方式
+  dissatisfiedReason: [], // 不满意原因
+  visitSatisfaction: [], // 满意度
+});
+const fastSearch = ref('all'); // tab位置
+const fastSearchChange = (val: string) => {
+  state.queryParams.PageIndex = 1;
+  state.queryParams.PageSize = 10;
+  fastSearch.value = val;
+  switch (val) {
+    case 'all':
+      state.queryParams.IsProvince = null;
+      break;
+    case 'city':
+      state.queryParams.IsProvince = false;
+      break;
+    case 'province':
+      state.queryParams.IsProvince = true;
+      break;
+  }
+  handleQuery();
+};
+const searchCol = ref(true); // 展开/收起
+// 展开/收起
+const closeSearch = () => {
+  searchCol.value = !searchCol.value;
+};
+const handleTimeChange = (val: string[], startKey: string, endKey: string) => {
+  if (val) {
+    state.queryParams[startKey] = val[0];
+    state.queryParams[endKey] = val[1];
+  } else {
+    state.queryParams[startKey] = '';
+    state.queryParams[endKey] = '';
+  }
+  handleQuery();
+};
+// 实际办理时间
+const timeStartChangeCr = (val: string[]) => {
+  handleTimeChange(val, 'ActualHandleTime', 'EndActualHandleTime');
+};
+// 归档时间
+const timeStartChangeEx = (val: string[]) => {
+  handleTimeChange(val, 'FiledTime', 'EndFiledTime');
+};
+// 受理时间
+const timeStartChangeSl = (val: string[]) => {
+  handleTimeChange(val, 'CreationTime', 'EndCreationTime');
+};
+// 回访时间
+const timeStartChangeHf = (val: string[]) => {
+  handleTimeChange(val, 'VisitTime', 'EndVisitTime');
+};
+// 获取查询条件基础信息
+const getBaseData = async () => {
+  try {
+    const { result } = await screenBaseData();
+    state.screenStatus = result?.screenStatus ?? [];
+    state.screenType = result?.screenType ?? [];
+    state.counterSignTypeOptions = result?.counterSignType ?? [];
+    state.acceptTypeOptions = result?.acceptType ?? [];
+    state.sourceChannelOptions = result?.sourceChannel ?? [];
+    state.visitSatisfaction = result?.visitSatisfaction ?? [];
+    state.dissatisfiedReason = result?.dissatisfiedReason ?? [];
+  } catch (e) {
+    console.log(e);
+  }
+};
+// 手动查询,将页码设置为1
+const handleQuery = () => {
+  state.queryParams.PageIndex = 1;
+  queryList();
+};
+/** 获取列表 */
+const queryList = () => {
+  let request = other.deepClone(state.queryParams);
+  Reflect.deleteProperty(request, 'crTime'); // 删除无用的参数
+  Reflect.deleteProperty(request, 'exTime'); // 删除无用的参数
+  Reflect.deleteProperty(request, 'slTime'); // 删除无用的参数
+  Reflect.deleteProperty(request, 'hfTime'); // 删除无用的参数
+  state.loading = true;
+  screenApplyList(request)
+    .then((response: any) => {
+      state.tableData = response?.result.items ?? [];
+      state.total = response?.result.total;
+      state.loading = false;
+    })
+    .catch(() => {
+      state.loading = false;
+    });
+};
+/** 重置按钮操作 */
+const resetQuery = (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  formEl.resetFields();
+  state.queryParams.ActualHandleTime = null;
+  state.queryParams.EndActualHandleTime = null;
+  state.queryParams.FiledTime = null;
+  state.queryParams.EndFiledTime = null;
+  state.queryParams.CreationTime = null;
+  state.queryParams.EndCreationTime = null;
+  state.queryParams.VisitTime = null;
+  state.queryParams.EndVisitTime = null;
+  state.queryParams.IsHomePage = null;
+  queryList();
+};
+// 批量扭转退回
+const reverseAuditRef = ref<RefType>();
+const onReverseMultiple = () => {
+  const ids = proTableRef.value.selectedList.map((item: any) => item.id);
+  reverseAuditRef.value.openDialog(ids);
+}
+// 扭转退回
+const onReverse = (row: any) => {
+  reverseAuditRef.value.openDialog(row.id);
+}
+onMounted(async () => {
+  await getBaseData();
+  queryList();
+});
+</script>
+<style scoped lang="scss">
+.business-visit-reverse-container {
+  .arrow {
+    transition: transform var(--el-transition-duration);
+    cursor: pointer;
+  }
+  .arrow.is-reverse {
+    transform: rotateZ(-180deg);
+  }
+}
+</style>

+ 2 - 2
src/views/statistics/department/shSatisfied.vue

@@ -20,12 +20,12 @@
 						:clearable="false"
 					/>
 				</el-form-item>
-				<el-form-item label="类型" prop="TypeId">
+<!--				<el-form-item label="类型" prop="TypeId">
 					<el-select v-model="state.queryParams.TypeId" placeholder="类型" @change="handleQuery">
 						<el-option label="办件结果" value="1" />
 						<el-option label="办件态度" value="2" />
 					</el-select>
-				</el-form-item>
+				</el-form-item>-->
 				<el-form-item label="电话线路" prop="CDPN">
 					<el-select v-model="state.queryParams.CDPN" placeholder="请选择电话线路" clearable @change="handleQuery">
 						<el-option v-for="item in state.callForwardingSource" :value="item.dicDataValue" :key="item.dicDataValue" :label="item.dicDataName" />

+ 33 - 33
src/views/tels/callLog/index.vue

@@ -413,19 +413,6 @@ const allColumns = [
 	{ prop: 'onStateText', label: '通话结果' },
 	{ prop: 'callDirectionText', label: '电话方向' },
 	{ prop: 'endByText', label: '挂机类型', width: 120 },
-	{
-		prop: 'createdTime',
-		label: '开始时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.createdTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-	{
-		prop: 'answeredTime',
-		label: '接通时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.answeredTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-	{ prop: 'overTime', label: '挂断时间', width: 170, render: (scope) => <span>{formatDate(scope.row.overTime, 'YYYY-mm-dd HH:MM:SS')}</span> },
 	{
 		prop: 'beginIvrTime',
 		label: 'ivr开始时间',
@@ -468,6 +455,19 @@ const allColumns = [
 			return <span>{formatDate(scope.row.endRingTimg, 'YYYY-mm-dd HH:MM:SS')}</span>;
 		},
 	},
+  {
+    prop: 'createdTime',
+    label: '开始时间',
+    width: 170,
+    render: (scope) => <span>{formatDate(scope.row.createdTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
+  },
+  {
+    prop: 'answeredTime',
+    label: '接通时间',
+    width: 170,
+    render: (scope) => <span>{formatDate(scope.row.answeredTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
+  },
+  { prop: 'overTime', label: '挂断时间', width: 170, render: (scope) => <span>{formatDate(scope.row.overTime, 'YYYY-mm-dd HH:MM:SS')}</span> },
 	{ prop: 'operation', label: '操作', fixed: 'right', width: 310, align: 'center' },
 ];
 // 呼入表头
@@ -481,19 +481,6 @@ const inColumns = [
 	{ prop: 'userName', label: '话务员', width: 120 },
 	{ prop: 'duration', label: '通话时间(秒)', width: 120 },
 	{ prop: 'endByText', label: '挂机类型', width: 120 },
-	{
-		prop: 'createdTime',
-		label: '开始时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.createdTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-	{
-		prop: 'answeredTime',
-		label: '接通时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.answeredTime, ' YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-	{ prop: 'overTime', label: '挂断结束时间', width: 170, render: (scope) => <span>{formatDate(scope.row.overTime, 'YYYY-mm-dd HH:MM:SS')}</span> },
 	{
 		prop: 'beginIvrTime',
 		label: 'ivr开始时间',
@@ -536,6 +523,19 @@ const inColumns = [
 			return <span>{formatDate(scope.row.endRingTimg, 'YYYY-mm-dd HH:MM:SS')}</span>;
 		},
 	},
+  {
+    prop: 'createdTime',
+    label: '开始时间',
+    width: 170,
+    render: (scope) => <span>{formatDate(scope.row.createdTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
+  },
+  {
+    prop: 'answeredTime',
+    label: '接通时间',
+    width: 170,
+    render: (scope) => <span>{formatDate(scope.row.answeredTime, ' YYYY-mm-dd HH:MM:SS')}</span>,
+  },
+  { prop: 'overTime', label: '挂断结束时间', width: 170, render: (scope) => <span>{formatDate(scope.row.overTime, 'YYYY-mm-dd HH:MM:SS')}</span> },
 	{ prop: 'operation', label: '操作', fixed: 'right', width: 310, align: 'center' },
 ];
 // 呼出表头
@@ -570,13 +570,6 @@ const noColumns = [
 	{ prop: 'gateway', label: '中继号码', width: 120 },
 	{ prop: 'userName', label: '话务员' },
 	{ prop: 'callDirectionText', label: '电话方向' },
-	{
-		prop: 'createdTime',
-		label: '开始时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.createdTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-	{ prop: 'overTime', label: '挂断时间', width: 170, render: (scope) => <span>{formatDate(scope.row.overTime, 'YYYY-mm-dd HH:MM:SS')}</span> },
 	{
 		prop: 'beginIvrTime',
 		label: 'ivr开始时间',
@@ -619,6 +612,13 @@ const noColumns = [
 			return <span>{formatDate(scope.row.endRingTimg, 'YYYY-mm-dd HH:MM:SS')}</span>;
 		},
 	},
+  {
+    prop: 'createdTime',
+    label: '开始时间',
+    width: 170,
+    render: (scope) => <span>{formatDate(scope.row.createdTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
+  },
+  { prop: 'overTime', label: '挂断时间', width: 170, render: (scope) => <span>{formatDate(scope.row.overTime, 'YYYY-mm-dd HH:MM:SS')}</span> },
 ];
 const changeTba = () => {
 	ruleFormRef.value.resetFields();

+ 14 - 43
src/views/tels/smartRecord/index.vue

@@ -20,8 +20,8 @@
 								type="datetimerange"
 								unlink-panels
 								range-separator="至"
-								start-placeholder="开始时间"
-								end-placeholder="结束时间"
+								start-placeholder="智能应答进入开始时间"
+								end-placeholder="智能应答进入结束时间"
 								:shortcuts="shortcuts"
 								@change="timeStartChangeCall"
 								value-format="YYYY/MM/DD HH:mm:ss"
@@ -49,14 +49,14 @@
 				@updateTable="queryList"
 				:loading="state.loading"
 				:total="state.total"
-				v-model:page-index="state.queryParams.PageIndex"
-				v-model:page-size="state.queryParams.PageSize"
+				v-model:page-index="state.queryParams.pageno"
+				v-model:page-size="state.queryParams.pagesize"
 				:key="Math.random()"
 			>
 				<!-- 表格操作 -->
 				<template #operation="{ row }">
-					<el-button type="primary" @click="onPlaySoundRecording(row)" title="播放录音" link v-if="row.recordingAbsolutePath">播放录音</el-button>
-					<el-button link type="primary" @click="onDownload(row)" title="下载录音" v-if="row.recordingAbsolutePath"> 下载录音 </el-button>
+					<el-button type="primary" @click="onPlaySoundRecording(row)" title="播放录音" link v-if="row.recordUrl">播放录音</el-button>
+					<el-button link type="primary" @click="onDownload(row)" title="下载录音" v-if="row.recordUrl"> 下载录音 </el-button>
 				</template>
 			</ProTable>
 		</el-card>
@@ -70,12 +70,11 @@ import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
 import type { FormInstance } from 'element-plus';
 import { ElMessageBox } from 'element-plus';
 import { downloadFileByStream } from '@/utils/tools';
-import { callLogPaged } from '@/api/tels/callLog';
 import { formatDate } from '@/utils/formatTime';
 import { shortcuts } from '@/utils/constants';
 import other from '@/utils/other';
 import { fileDownload } from '@/api/public/file';
-import { jthsRecord } from "@/api/todo/voiceAssistant";
+import { jthsRecord } from '@/api/todo/voiceAssistant';
 
 // 引入组件
 const PlayRecord = defineAsyncComponent(() => import('@/views/tels/callLog/component/Play-record.vue')); // 播放录音
@@ -86,46 +85,18 @@ const columns = ref<any[]>([
 	{ prop: 'cpn', label: '主叫号码' },
 	{ prop: 'cdpn', label: '被叫号码' },
 	{
-		prop: 'beginIvrTime',
-		label: 'ivr开始时间',
+		prop: 'questionTime',
+		label: '智能应答进入时间',
 		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.beginIvrTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
+		render: (scope) => <span>{formatDate(scope.row.questionTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
 	},
-	{
-		prop: 'endIvrTime',
-		label: 'ivr结束时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.endIvrTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-
-	{
-		prop: 'beginQueueTime',
-		label: '队列开始时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.beginQueueTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-	{
-		prop: 'endQueueTime',
-		label: '队列结束时间',
-		width: 170,
-		render: (scope) => {
-			return <span>{formatDate(scope.row.endQueueTime, 'YYYY-mm-dd HH:MM:SS')}</span>;
-		},
-	},
-	{
-		prop: 'createdTime',
-		label: '应答开始时间',
-		width: 170,
-		render: (scope) => <span>{formatDate(scope.row.createdTime, 'YYYY-mm-dd HH:MM:SS')}</span>,
-	},
-	{ prop: 'overTime', label: '应答结束时间', width: 170, render: (scope) => <span>{formatDate(scope.row.overTime, 'YYYY-mm-dd HH:MM:SS')}</span> },
 	{ prop: 'operation', label: '操作', fixed: 'right', width: 160, align: 'center' },
 ]);
 // 定义变量内容
 const state = reactive({
 	queryParams: {
-    pageno: 1, // 当前页
-    pagesize: 10, // 每页条数
+		pageno: 1, // 当前页
+		pagesize: 10, // 每页条数
 		StaffNo: null, // 分机号
 		CPN: null, // 中继号码
 		CDPN: null, // 分机号
@@ -160,7 +131,7 @@ const timeStartChangeCall = (val: string[]) => {
 };
 // 手动查询,将页码设置为1
 const handleQuery = () => {
-	state.queryParams.PageIndex = 1;
+	state.queryParams.pageno = 1;
 	queryList();
 };
 /** 通话记录列表 */
@@ -170,7 +141,7 @@ const queryList = async () => {
 		let request = other.deepClone(state.queryParams);
 		Reflect.deleteProperty(request, 'callTime'); // 删除无用的参数
 
-		const {result} = await jthsRecord(request);
+		const { result } = await jthsRecord(request);
 		state.tableData = result?.result ?? [];
 		state.total = result?.total_count ?? 0;
 		state.loading = false;

+ 19 - 57
src/views/todo/seats/accept/Real-time-quality.vue

@@ -1,18 +1,15 @@
 <template>
 	<div class="real-time-quality">
-		<el-scrollbar class="h100" noresize ref="scrollbarRef" max-height="400px" v-if="tagList.length" @click="stopScroll">
+		<el-scrollbar class="h100" noresize ref="scrollbarRef" max-height="400px" v-if="tagList.length">
 			<div class="tag-box" ref="chatBoxRef">
 				<div v-for="(item, index) in tagList" :key="index" class="tag-item flex-center-between">
-					<el-tag>{{ item.tag }}</el-tag>
-					<el-text type="info">{{ formatDate(item.timestamps, 'YYYY-mm-dd HH:MM:SS') }}</el-text>
+					<el-tag>{{ item.name }}</el-tag>
+					<el-text type="info">{{ formatDate(item.timestamp, 'YYYY-mm-dd HH:MM:SS') }}</el-text>
 				</div>
 				<el-text class="end-call" tag="p" v-if="talkEnd">-- 通话结束 --</el-text>
 			</div>
 		</el-scrollbar>
 		<Empty v-else />
-		<el-button @click.stop="keepScroll" class="keep-scroll" title="回到底部" circle v-if="!isScrollBottom && !talkEnd">
-			<SvgIcon name="ele-ArrowDown" size="18px" />
-		</el-button>
 	</div>
 </template>
 <script setup lang="ts" name="orderAcceptRealtimeQuality">
@@ -21,67 +18,32 @@ import mittBus from '@/utils/mitt';
 import other from '@/utils/other';
 import { useRoute } from 'vue-router';
 import { formatDate } from '@/utils/formatTime';
-
 const tagList = ref([
-	/*	{
-		tag: '语速过快',
-		timestamps: new Date().getTime(),
-	},*/
 ]);
 const talkEnd = ref(false);
-const wsReceive = (message: any) => {
+// 订阅消息
+const route = useRoute();
+const subscribe = () => {
+	// 接受消息
+	mittBus.on('wsNotice', (message: any) => {
+		wsNotice(message);
+	});
+};
+const wsNotice = (message: any) => {
 	try {
 		const data = JSON.parse(message.data);
-    console.log(data.body,'语音提醒消息');
-		if (data.body.busiType === 20) {
+		if (data.body.bisType === 20) {
 			//20代表语音提醒消息
-			console.log(data.body,'语音提醒消息');
+			console.log(data.body.content[0].name, '语音提醒消息');
+			tagList.value.push({
+				name: data.body.content[0].name,
+        timestamp: data.body.content[0].timestamp,
+			});
 		}
 	} catch (error) {
 		console.log('坐席辅助收到消息', message);
 	}
 };
-const scrollbarRef = ref<RefType>();
-const chatBoxRef = ref<RefType>();
-// 停止滚动
-const stopScroll = () => {
-	isScrollBottom.value = false;
-};
-watch(
-	() => scrollbarRef.value?.wrapRef,
-	() => {
-		scrollbarRef.value?.wrapRef.addEventListener('mousewheel', () => {
-			stopScroll();
-		});
-	}
-);
-// 继续滚动
-const keepScroll = () => {
-	isScrollBottom.value = true;
-	scrollToBottom();
-};
-// 滚动到底部
-const isScrollBottom = ref(true); // 是否需要滚动到底部
-const isAutoScroll = ref(true); // 是否自动滚动
-const scrollToBottom = async () => {
-	if (!isScrollBottom.value) return;
-	await nextTick(); // 等待 DOM 更新
-	const max = chatBoxRef.value?.clientHeight;
-	scrollbarRef.value?.setScrollTop(max);
-	isAutoScroll.value = true;
-};
-// 订阅消息
-const route = useRoute();
-const subscribe = () => {
-	// 接受消息
-	mittBus.on('wsReceive', (message: any) => {
-		const data = JSON.parse(message.data);
-		if (data.body.content.callId === route.params.callId) {
-			// 判断是否是当前通话
-			wsReceive(message);
-		}
-	});
-};
 onMounted(() => {
 	// 进入页面订阅
 	subscribe();
@@ -92,7 +54,7 @@ onActivated(() => {
 });
 onDeactivated(() => {
 	// 缓存离开取消订阅
-	mittBus.off('wsReceive');
+	mittBus.off('wsNotice');
 });
 </script>
 <style scoped lang="scss">

+ 37 - 33
src/views/todo/seats/accept/Script-navigation.vue

@@ -1,7 +1,6 @@
 <template>
 	<div class="script-navigation">
-		<el-steps direction="vertical" :active="active" finish-status="success">
-<!--      <el-button @click="next">下一步</el-button>-->
+		<el-steps direction="vertical" :active="active">
 			<el-step title="开场语" />
 			<el-step title="确认市民诉求和个人信息" />
 			<el-step title="结束前语" />
@@ -10,48 +9,53 @@
 	</div>
 </template>
 <script setup lang="ts" name="orderAcceptScriptNavigation">
-import { ref,onActivated, onDeactivated, onMounted  } from 'vue';
-import mittBus from "@/utils/mitt";
-import { useRoute } from "vue-router";
+import { ref, onActivated, onDeactivated, onMounted } from 'vue';
+import mittBus from '@/utils/mitt';
+import { useRoute } from 'vue-router';
 const active = ref(0);
-const next = () => {
-	if (active.value++ > 3) active.value = 0;
-};
 // 订阅消息
 const route = useRoute();
 const subscribe = () => {
-  // 接受消息
-  mittBus.on('wsReceive', (message: any) => {
-    const data = JSON.parse(message.data);
-    if (data.body.content.callId === route.params.callId) {
-      // 判断是否是当前通话
-      wsReceive(message);
-    }
-  });
+	// 接受消息
+	mittBus.on('wsReceive', (message: any) => {
+		wsNotice(message);
+	});
 };
-const wsReceive = (message: any) => {
-  try {
-    const data = JSON.parse(message.data);
-    if (data.body.busiType === 32) {  //32代表导航流程
-      //20代表语音提醒消息
-      console.log(data.body,'导航流程');
-    }
-    // console.log(data);
-  } catch (error) {
-    console.log('坐席辅助收到消息', message);
-  }
+const wsNotice = (message: any) => {
+	try {
+		const data = JSON.parse(message.data);
+		if (data.body.bisType === 30) {
+			// 30代表话术命中
+			console.log(data.body.content[0].name, '导航流程');
+			if (data.body.content[0].name === '开场语') {
+				active.value = 1;
+			}
+			if (data.body.content[0].name === '确认市民诉求和个人信息') {
+				active.value = 2;
+			}
+			if (data.body.content[0].name === '结束前语') {
+				active.value = 3;
+			}
+			if (data.body.content[0].name === '结束语') {
+				active.value = 4;
+			}
+		}
+		// console.log(data);
+	} catch (error) {
+		console.log('坐席辅助收到消息', message);
+	}
 };
 onMounted(() => {
-  // 进入页面订阅
-  subscribe();
+	// 进入页面订阅
+	subscribe();
 });
 onActivated(() => {
-  // 缓存进入重新订阅
-  subscribe();
+	// 缓存进入重新订阅
+	subscribe();
 });
 onDeactivated(() => {
-  // 缓存离开取消订阅
-  mittBus.off('wsReceive');
+	// 缓存离开取消订阅
+	mittBus.off('wsReceive');
 });
 </script>
 

+ 85 - 56
src/views/todo/seats/accept/Voice-assistant.vue

@@ -3,27 +3,27 @@
 		<!-- 聊天框 -->
 		<el-scrollbar class="h100" noresize ref="scrollbarRef" max-height="400px" v-if="showMessageList.length" @click="stopScroll">
 			<div class="chat-box" ref="chatBoxRef">
-				<div v-for="(item, index) in showMessageList" :key="index" class="chat-item" :class="item.body?.content?.callSentenceInfo?.role">
-					<div v-if="item.body?.content?.callSentenceInfo?.role === 'user'" class="user">
+				<div v-for="(item, index) in showMessageList" :key="index" class="chat-item" :class="item?.body?.content?.callSentenceInfo?.role">
+					<div v-if="item?.body?.content?.callSentenceInfo?.role === 'user'" class="user">
 						<img v-lazy="getImageUrl('order/user.png')" alt="" class="user-avatar" src="" />
 						<div class="user-name">
 							{{ item.body?.content?.callerNumber }}
-							<div class="user-date">{{ formatDate(item.timestamps, 'YYYY-mm-dd HH:MM:SS') }}</div>
+							<div class="user-date">{{ formatDate(item?.timestamps, 'YYYY-mm-dd HH:MM:SS') }}</div>
 						</div>
-						<div class="user-content">{{ item.body?.content?.callSentenceInfo.text }}</div>
+						<div class="user-content">{{ item?.body?.content?.callSentenceInfo.text }}</div>
 						<div class="user-tag">
-							<el-tag v-for="(items, index) in item.body?.content?.callSentenceInfo.tags" size="small">{{ items }}</el-tag>
+							<el-tag v-for="tag in item?.tags" size="small">{{ tag.name }}</el-tag>
 						</div>
 					</div>
 					<div v-else class="agent">
 						<img v-lazy="getImageUrl('order/service.png')" alt="" class="agent-avatar" src="" />
 						<div class="agent-name">
-							<div class="agent-date">{{ formatDate(item.timestamps, 'YYYY-mm-dd HH:MM:SS') }}</div>
-							{{ item.body?.content?.calledNumber }}
+							<div class="agent-date">{{ formatDate(item?.timestamps, 'YYYY-mm-dd HH:MM:SS') }}</div>
+							{{ item?.body?.content?.calledNumber }}
 						</div>
-						<div class="agent-content">{{ item.body?.content?.callSentenceInfo.text }}</div>
+						<div class="agent-content">{{ item?.body?.content?.callSentenceInfo.text }}</div>
 						<div class="agent-tag">
-							<el-tag v-for="(items, index) in item.body?.content?.callSentenceInfo.tags" size="small">{{ items }}</el-tag>
+							<el-tag v-for="tag in item?.tags" size="small">{{ tag.name }}</el-tag>
 						</div>
 					</div>
 				</div>
@@ -78,43 +78,45 @@ const messageList = ref<any>([
 		timestamps: new Date().getTime(),
 	},*/
 ]); // 消息列表
-const showMessageList = ref([
-	/*  {
-      body: {
-        content: {
-          callSentenceInfo: {
-            text: '你好,我是小智,有什么可以帮您的吗?',
-            role: 'agent',
-            tags:['语速过快','情绪激动']
-          },
-          calledNumber: '1009',
-          callerNumber: '19136073037',
-        },
-      },
-      timestamps: new Date().getTime(),
-    },
-    {
-      body: {
-        content: {
-          callSentenceInfo: {
-            text: '12311?',
-            role: 'user',
-            tags:['语速过快','情绪激动']
-          },
-          calledNumber: '1009',
-          callerNumber: '19136073037',
-        },
-      },
-      timestamps: new Date().getTime(),
-    },*/
+const showMessageList = ref<any>([
+	/* 	{
+		body: {
+			content: {
+				callSentenceInfo: {
+					text: '你好,我是小智,有什么可以帮您的吗?',
+					role: 'agent',
+				},
+				calledNumber: '1009',
+				callerNumber: '19136073037',
+			},
+		},
+		timestamps: new Date().getTime(),
+      tags:['语速过快']
+	},
+	{
+		body: {
+			content: {
+				callSentenceInfo: {
+					text: '12311?',
+					role: 'user',
+
+				},
+				calledNumber: '1009',
+				callerNumber: '19136073037',
+			},
+		},
+		timestamps: new Date().getTime(),
+    tags:['语速过快','语速过快','语速过快']
+	},*/
 ]);
-const recognizeList = ref<EmptyObjectType>({}); // 识别信息
 const route = useRoute();
 const talkEnd = ref(false); // 通话结束
 const wsReceive = (message: any) => {
 	try {
 		const data = JSON.parse(message.data);
+		data.tags = data.tags || [];
 		if (data.body.bisType === 3) {
+			data.tags = data.tags || [];
 			// 转写消息
 			if (data.body.content.action === 0) {
 				// 通话开始
@@ -124,18 +126,20 @@ const wsReceive = (message: any) => {
 			if (data.body.content.action === 1) {
 				// 通话中
 				if (messageList.value.length) {
-					const item = messageList.value.find((item: any) => item.body.content.callSentenceInfo.index === data.body.content.callSentenceInfo.index);
-					if (item) {
-						item.body.content.callSentenceInfo.text = data.body.content.callSentenceInfo.text;
+					const sameIndex = messageList.value.find(
+						(item: any) => item.body.content.callSentenceInfo.index === data.body.content.callSentenceInfo.index
+					);
+					if (sameIndex) {
+						sameIndex.body.content.callSentenceInfo.text = data.body.content.callSentenceInfo.text;
 					} else {
-						messageList.value.push(data);
+						messageList.value = [...messageList.value, data];
 					}
 				} else {
-					messageList.value.push(data);
+					messageList.value = [...messageList.value, data];
 				}
 				scrollToBottom();
 				showMessageList.value = other.deepClone(messageList.value);
-				console.log('通话消息转写内容:', messageList.value);
+				console.log('通话消息转写内容:', showMessageList.value);
 			}
 			if (data.body.content.action === 4) {
 				// 通话结束
@@ -194,6 +198,29 @@ const subscribe = () => {
 			wsReceive(message);
 		}
 	});
+	mittBus.on('wsNotice', (data: any) => {
+		wsNotice(data);
+	});
+};
+// 语音提醒内容
+const tagList = ref([]);
+const wsNotice = (message: any) => {
+	try {
+		const data = JSON.parse(message.data);
+		if (data.body.bisType === 20) {
+			//20代表语音提醒消息
+			console.log(data.body.content[0].name, '语音提醒消息-坐席辅助');
+			tagList.value.push(data.body.content[0]);
+			for (let i of messageList.value) {
+				if (i.body.content.callSentenceInfo.index === data.body.content[0].index) {
+					i.tags.push(data.body.content[0]);
+				}
+			}
+			showMessageList.value = other.deepClone(messageList.value);
+		}
+	} catch (error) {
+		console.log('坐席辅助收到消息', error);
+	}
 };
 // 消息筛选
 const filterMessage = (type: string) => {
@@ -223,6 +250,7 @@ onActivated(() => {
 onDeactivated(() => {
 	// 缓存离开取消订阅
 	mittBus.off('wsReceive');
+	mittBus.off('wsNotice');
 });
 defineExpose({
 	filterMessage,
@@ -251,7 +279,7 @@ defineExpose({
 			word-break: break-all;
 			position: relative;
 			color: var(--el-color-white);
-			margin: 10px 10px 50px 10px;
+			margin: 10px 10px 20px 10px;
 			display: flex;
 			&:last-child {
 				margin-bottom: 0;
@@ -296,11 +324,11 @@ defineExpose({
 					margin-left: 10px;
 				}
 				&-tag {
-					position: absolute;
-					bottom: -30px;
-					left: 65px;
+					margin-left: 65px;
+					margin-top: 10px;
 					:deep(.el-tag) {
-						margin-right: 10px;
+						margin-right: 5px;
+						margin-bottom: 5px;
 					}
 				}
 
@@ -310,7 +338,7 @@ defineExpose({
 					border-radius: 50%;
 					position: absolute;
 					left: 0;
-					top: calc(50% - 15px);
+					top: 16px;
 				}
 			}
 
@@ -355,11 +383,12 @@ defineExpose({
 					margin-right: 10px;
 				}
 				&-tag {
-					position: absolute;
-					bottom: -30px;
-					right: 65px;
+					margin-right: 65px;
+					margin-top: 10px;
+					text-align: right;
 					:deep(.el-tag) {
-						margin-left: 10px;
+						margin-right: 5px;
+						margin-bottom: 5px;
 					}
 				}
 				&-avatar {
@@ -368,7 +397,7 @@ defineExpose({
 					border-radius: 50%;
 					position: absolute;
 					right: 0;
-					top: calc(50% - 15px);
+					top: 16px;
 				}
 			}
 		}