浏览代码

Merge branch 'feat-zzby' into master-zzby

闪电 10 月之前
父节点
当前提交
fca3876a9d

+ 137 - 14
src/assets/style.css

@@ -620,10 +620,18 @@ video {
620 620
   right: -0.25rem;
621 621
 }
622 622
 
623
+.-right-2 {
624
+  right: -0.5rem;
625
+}
626
+
623 627
 .-top-1 {
624 628
   top: -0.25rem;
625 629
 }
626 630
 
631
+.-top-2 {
632
+  top: -0.5rem;
633
+}
634
+
627 635
 .bottom-0 {
628 636
   bottom: 0px;
629 637
 }
@@ -797,6 +805,10 @@ video {
797 805
   display: flex;
798 806
 }
799 807
 
808
+.inline-flex {
809
+  display: inline-flex;
810
+}
811
+
800 812
 .table {
801 813
   display: table;
802 814
 }
@@ -885,6 +897,10 @@ video {
885 897
   width: 3rem;
886 898
 }
887 899
 
900
+.w-16 {
901
+  width: 4rem;
902
+}
903
+
888 904
 .w-2\/5 {
889 905
   width: 40%;
890 906
 }
@@ -913,6 +929,10 @@ video {
913 929
   width: 10rem;
914 930
 }
915 931
 
932
+.w-5 {
933
+  width: 1.25rem;
934
+}
935
+
916 936
 .w-5\/6 {
917 937
   width: 83.333333%;
918 938
 }
@@ -957,18 +977,12 @@ video {
957 977
   flex-shrink: 1;
958 978
 }
959 979
 
960
-.transform {
961
-  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
980
+.flex-grow {
981
+  flex-grow: 1;
962 982
 }
963 983
 
964
-@keyframes pulse {
965
-  50% {
966
-    opacity: .5;
967
-  }
968
-}
969
-
970
-.animate-pulse {
971
-  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
984
+.transform {
985
+  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
972 986
 }
973 987
 
974 988
 @keyframes spin {
@@ -1049,6 +1063,10 @@ video {
1049 1063
   gap: 0.5rem;
1050 1064
 }
1051 1065
 
1066
+.gap-3 {
1067
+  gap: 0.75rem;
1068
+}
1069
+
1052 1070
 .gap-4 {
1053 1071
   gap: 1rem;
1054 1072
 }
@@ -1127,10 +1145,6 @@ video {
1127 1145
   border-radius: 0.5rem;
1128 1146
 }
1129 1147
 
1130
-.rounded {
1131
-  border-radius: 0.25rem;
1132
-}
1133
-
1134 1148
 .rounded-t-lg {
1135 1149
   border-top-left-radius: 0.5rem;
1136 1150
   border-top-right-radius: 0.5rem;
@@ -1148,6 +1162,11 @@ video {
1148 1162
   border-top-width: 1px;
1149 1163
 }
1150 1164
 
1165
+.border-blue-200 {
1166
+  --tw-border-opacity: 1;
1167
+  border-color: rgb(191 219 254 / var(--tw-border-opacity, 1));
1168
+}
1169
+
1151 1170
 .border-blue-500 {
1152 1171
   --tw-border-opacity: 1;
1153 1172
   border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
@@ -1158,6 +1177,11 @@ video {
1158 1177
   border-color: rgb(243 244 246 / var(--tw-border-opacity, 1));
1159 1178
 }
1160 1179
 
1180
+.border-gray-300 {
1181
+  --tw-border-opacity: 1;
1182
+  border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
1183
+}
1184
+
1161 1185
 .border-green-500 {
1162 1186
   --tw-border-opacity: 1;
1163 1187
   border-color: rgb(34 197 94 / var(--tw-border-opacity, 1));
@@ -1238,6 +1262,11 @@ video {
1238 1262
   background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1));
1239 1263
 }
1240 1264
 
1265
+.bg-red-500 {
1266
+  --tw-bg-opacity: 1;
1267
+  background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
1268
+}
1269
+
1241 1270
 .bg-white {
1242 1271
   --tw-bg-opacity: 1;
1243 1272
   background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
@@ -1260,22 +1289,42 @@ video {
1260 1289
   background-image: linear-gradient(to top, var(--tw-gradient-stops));
1261 1290
 }
1262 1291
 
1292
+.from-blue-50 {
1293
+  --tw-gradient-from: #eff6ff var(--tw-gradient-from-position);
1294
+  --tw-gradient-to: rgb(239 246 255 / 0) var(--tw-gradient-to-position);
1295
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1296
+}
1297
+
1263 1298
 .from-blue-500 {
1264 1299
   --tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);
1265 1300
   --tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);
1266 1301
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1267 1302
 }
1268 1303
 
1304
+.from-gray-100 {
1305
+  --tw-gradient-from: #f3f4f6 var(--tw-gradient-from-position);
1306
+  --tw-gradient-to: rgb(243 244 246 / 0) var(--tw-gradient-to-position);
1307
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1308
+}
1309
+
1269 1310
 .from-white {
1270 1311
   --tw-gradient-from: #fff var(--tw-gradient-from-position);
1271 1312
   --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);
1272 1313
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1273 1314
 }
1274 1315
 
1316
+.to-blue-100 {
1317
+  --tw-gradient-to: #dbeafe var(--tw-gradient-to-position);
1318
+}
1319
+
1275 1320
 .to-blue-600 {
1276 1321
   --tw-gradient-to: #2563eb var(--tw-gradient-to-position);
1277 1322
 }
1278 1323
 
1324
+.to-gray-200 {
1325
+  --tw-gradient-to: #e5e7eb var(--tw-gradient-to-position);
1326
+}
1327
+
1279 1328
 .to-transparent {
1280 1329
   --tw-gradient-to: transparent var(--tw-gradient-to-position);
1281 1330
 }
@@ -1305,11 +1354,26 @@ video {
1305 1354
   padding: 1.5rem;
1306 1355
 }
1307 1356
 
1357
+.px-2 {
1358
+  padding-left: 0.5rem;
1359
+  padding-right: 0.5rem;
1360
+}
1361
+
1362
+.px-3 {
1363
+  padding-left: 0.75rem;
1364
+  padding-right: 0.75rem;
1365
+}
1366
+
1308 1367
 .px-4 {
1309 1368
   padding-left: 1rem;
1310 1369
   padding-right: 1rem;
1311 1370
 }
1312 1371
 
1372
+.py-1 {
1373
+  padding-top: 0.25rem;
1374
+  padding-bottom: 0.25rem;
1375
+}
1376
+
1313 1377
 .py-2 {
1314 1378
   padding-top: 0.5rem;
1315 1379
   padding-bottom: 0.5rem;
@@ -1383,6 +1447,11 @@ video {
1383 1447
   line-height: 2.25rem;
1384 1448
 }
1385 1449
 
1450
+.text-4xl {
1451
+  font-size: 2.25rem;
1452
+  line-height: 2.5rem;
1453
+}
1454
+
1386 1455
 .text-\[10px\] {
1387 1456
   font-size: 10px;
1388 1457
 }
@@ -1428,6 +1497,10 @@ video {
1428 1497
   font-style: italic;
1429 1498
 }
1430 1499
 
1500
+.leading-relaxed {
1501
+  line-height: 1.625;
1502
+}
1503
+
1431 1504
 .text-blue-300 {
1432 1505
   --tw-text-opacity: 1;
1433 1506
   color: rgb(147 197 253 / var(--tw-text-opacity, 1));
@@ -1443,6 +1516,11 @@ video {
1443 1516
   color: rgb(37 99 235 / var(--tw-text-opacity, 1));
1444 1517
 }
1445 1518
 
1519
+.text-blue-800 {
1520
+  --tw-text-opacity: 1;
1521
+  color: rgb(30 64 175 / var(--tw-text-opacity, 1));
1522
+}
1523
+
1446 1524
 .text-gray-400 {
1447 1525
   --tw-text-opacity: 1;
1448 1526
   color: rgb(156 163 175 / var(--tw-text-opacity, 1));
@@ -1532,6 +1610,10 @@ video {
1532 1610
   -moz-osx-font-smoothing: grayscale;
1533 1611
 }
1534 1612
 
1613
+.opacity-0 {
1614
+  opacity: 0;
1615
+}
1616
+
1535 1617
 .opacity-70 {
1536 1618
   opacity: 0.7;
1537 1619
 }
@@ -1600,6 +1682,18 @@ video {
1600 1682
   transition-duration: 150ms;
1601 1683
 }
1602 1684
 
1685
+.transition-colors {
1686
+  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
1687
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1688
+  transition-duration: 150ms;
1689
+}
1690
+
1691
+.transition-opacity {
1692
+  transition-property: opacity;
1693
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1694
+  transition-duration: 150ms;
1695
+}
1696
+
1603 1697
 .transition-shadow {
1604 1698
   transition-property: box-shadow;
1605 1699
   transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -1646,11 +1740,36 @@ video {
1646 1740
   background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
1647 1741
 }
1648 1742
 
1743
+.hover\:from-blue-100:hover {
1744
+  --tw-gradient-from: #dbeafe var(--tw-gradient-from-position);
1745
+  --tw-gradient-to: rgb(219 234 254 / 0) var(--tw-gradient-to-position);
1746
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1747
+}
1748
+
1749
+.hover\:from-gray-200:hover {
1750
+  --tw-gradient-from: #e5e7eb var(--tw-gradient-from-position);
1751
+  --tw-gradient-to: rgb(229 231 235 / 0) var(--tw-gradient-to-position);
1752
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1753
+}
1754
+
1755
+.hover\:to-blue-200:hover {
1756
+  --tw-gradient-to: #bfdbfe var(--tw-gradient-to-position);
1757
+}
1758
+
1759
+.hover\:to-gray-300:hover {
1760
+  --tw-gradient-to: #d1d5db var(--tw-gradient-to-position);
1761
+}
1762
+
1649 1763
 .hover\:text-blue-800:hover {
1650 1764
   --tw-text-opacity: 1;
1651 1765
   color: rgb(30 64 175 / var(--tw-text-opacity, 1));
1652 1766
 }
1653 1767
 
1768
+.hover\:text-gray-600:hover {
1769
+  --tw-text-opacity: 1;
1770
+  color: rgb(75 85 99 / var(--tw-text-opacity, 1));
1771
+}
1772
+
1654 1773
 .hover\:text-white:hover {
1655 1774
   --tw-text-opacity: 1;
1656 1775
   color: rgb(255 255 255 / var(--tw-text-opacity, 1));
@@ -1666,4 +1785,8 @@ video {
1666 1785
   --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
1667 1786
   --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
1668 1787
   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1788
+}
1789
+
1790
+.group:hover .group-hover\:opacity-100 {
1791
+  opacity: 1;
1669 1792
 }

+ 350 - 0
src/components/callLogs/audioPlayer.vue

@@ -0,0 +1,350 @@
1
+<template>
2
+  <div class="h-full bg-white p-6 flex flex-col">
3
+    <!-- 头部 -->
4
+    <div class="flex justify-between items-center mb-8">
5
+      <h2 class="text-xl font-medium">通话录音</h2>
6
+      <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600" @click="drawerVisible = false">
7
+        <Close />
8
+      </el-icon>
9
+    </div>
10
+    <!-- 音频播放器 -->
11
+    <div class="bg-gray-50 rounded-lg p-6 mb-6">
12
+      <div class="flex justify-center mb-4">
13
+        <audio ref="audioPlayer" @timeupdate="updateTime"></audio>
14
+        <el-button class="w-16 h-16 !rounded-full flex items-center justify-center"
15
+          :type="isPlaying ? 'danger' : 'primary'" @click="togglePlay">
16
+          <el-icon class="text-4xl">
17
+            <component :is="isPlaying ? 'VideoPause' : 'VideoPlay'" />
18
+          </el-icon>
19
+        </el-button>
20
+      </div>
21
+      <!-- 进度条 -->
22
+      <div class="mb-2">
23
+        <el-slider v-model="progress" :max="100" @change="handleProgressChange" />
24
+      </div>
25
+      <!-- 时间显示 -->
26
+      <div class="flex justify-between text-sm text-gray-500">
27
+        <span>{{ formatTime(currentTime) }}</span>
28
+        <span>{{ formatTime(duration) }}</span>
29
+      </div>
30
+    </div>
31
+    <!-- 操作按钮 -->
32
+    <div class="mb-6">
33
+      <el-button type="primary" class="w-full !rounded-button whitespace-nowrap" @click="handleDownload">
34
+        <el-icon class="mr-2">
35
+          <Download />
36
+        </el-icon>
37
+        下载录音
38
+      </el-button>
39
+    </div>
40
+    <!-- 转写内容 -->
41
+    <div class="bg-white rounded-lg p-4 flex flex-col flex-grow overflow-hidden mb-6"
42
+      v-if="transcriptionMessages.length">
43
+      <h3 class="text-lg font-medium mb-4">语音转写内容</h3>
44
+      <div class="text-gray-600 leading-relaxed overflow-y-auto flex-grow space-y-2">
45
+        <div v-for="(message, index) in transcriptionMessages" :key="index" :class="[
46
+            'p-3 transition-colors rounded-lg cursor-pointer group',
47
+            message.direction === 1 ? 'bg-gradient-to-r from-gray-100 to-gray-200 hover:from-gray-200 hover:to-gray-300' : 'bg-gradient-to-r from-blue-50 to-blue-100 hover:from-blue-100 hover:to-blue-200'
48
+          ]">
49
+          <div class="flex justify-between items-center mb-2">
50
+            <div class="flex items-center gap-2">
51
+              <div class="text-xs text-gray-400">{{ message.timestamp }}</div>
52
+              <div :class="[
53
+                'px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap',
54
+                message.direction === 1 ? 'bg-gray-200 text-gray-700 border border-gray-300' : 'bg-blue-100 text-blue-600 border border-blue-200'
55
+              ]">
56
+                {{ message.direction === 1 ? '客服' : '客户' }}
57
+              </div>
58
+            </div>
59
+            <el-button v-if="currentIndex !== index" class="!rounded-button opacity-0 group-hover:opacity-100 transition-opacity" size="small"
60
+              type="text" @click.stop="playSegment(message.startTime, message.endTime, index)">
61
+              <el-icon class="text-lg">
62
+                <VideoPlay />
63
+              </el-icon>
64
+            </el-button>
65
+            <el-icon v-else class="text-lg">
66
+              <VideoPause />
67
+            </el-icon>
68
+          </div>
69
+          <div class="text-gray-700">{{ message.page_content }}</div>
70
+          <!-- <div class="text-xs text-gray-400 mt-1">置信度: {{ message.confidence }}%</div> -->
71
+        </div>
72
+      </div>
73
+    </div>
74
+    <!-- 关键词模块 -->
75
+    <div class="mb-6" v-if="keywords.length">
76
+      <h2 class="text-lg font-semibold mb-4">关键词</h2>
77
+      <div class="flex flex-wrap gap-3">
78
+        <div v-for="(keyword, index) in keywords" :key="index"
79
+          class="relative inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
80
+          {{ keyword.text }}
81
+          <span v-if="keyword.count > 1"
82
+            class="absolute -top-2 -right-2 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">{{
83
+            keyword.count }}</span>
84
+        </div>
85
+      </div>
86
+    </div>
87
+  </div>
88
+</template>
89
+
90
+<script lang="ts" setup>
91
+import { ref, reactive, watch, onMounted } from 'vue';
92
+import { ElMessage } from 'element-plus';
93
+import { Close, VideoPlay, VideoPause, Download } from '@element-plus/icons-vue';
94
+import { flatten } from 'lodash';
95
+
96
+import { getPageListData } from '@/api/main/system/system';
97
+import { stripHtmlByRegex, formatMilliseconds } from '@/utils/tools';
98
+import { exportFileWav } from '@/utils';
99
+
100
+const drawerVisible = ref(false);
101
+const deleteDialogVisible = ref(false);
102
+const isPlaying = ref(false);
103
+// const recordId = ref(null);
104
+// const filePath = ref(null);
105
+const audioPlayer = ref(null);
106
+const props = defineProps({
107
+  recordId: {
108
+    type: Number,
109
+    default: null,
110
+  },
111
+  filePath: {
112
+    type: String,
113
+    default: null,
114
+  },
115
+});
116
+
117
+watch(props, (newValue, oldValue) => {
118
+  if (newValue) {
119
+    console.log(newValue, 'newValue')
120
+    if (newValue.recordId !== oldValue.recordId) {
121
+      getDetail();
122
+    }
123
+    if (newValue.filePath !== oldValue.filePath) {
124
+      loadRemoteAudio()
125
+    }
126
+    
127
+   
128
+  }
129
+}, {
130
+  deep: true,
131
+});
132
+
133
+const progress = ref(0);
134
+const currentTime = ref(0);
135
+const duration = ref(180); // 示例时长3分钟
136
+const transcriptionMessages: any = ref([]);
137
+const formatTime = (seconds: number): string => {
138
+  console.log(seconds, 'seconds');
139
+  const minutes = Math.floor(seconds / 60);
140
+  const remainingSeconds = Math.floor(seconds % 60);
141
+  return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
142
+};
143
+
144
+const timer = ref(null);
145
+
146
+const togglePlay = (isFragment = false) => {
147
+  if (!isFragment) currentIndex.value = 0;
148
+  isPlaying.value = !isPlaying.value;
149
+  if (isPlaying.value) {
150
+    play();
151
+    // 模拟播放进度
152
+    timer.value = setInterval(() => {
153
+      if (progress.value >= 100 || (isFragment && currentTime.value >= endTime.value)) {
154
+        pause();
155
+        return;
156
+      }
157
+
158
+      currentTime.value+= 1;
159
+
160
+      progress.value = (currentTime.value / duration.value) * 100;
161
+      currentTime.value = (progress.value / 100) * duration.value;
162
+    }, 1000);
163
+  } else {
164
+    pause();
165
+  }
166
+};
167
+const handleProgressChange = (value: number) => {
168
+  // currentTime.value = (value / 100) * duration.value;
169
+};
170
+
171
+const handleDownload = () => {
172
+  ElMessage.success('录音文件下载中...');
173
+  exportFileWav(props.filePath);
174
+};
175
+let keywords = reactive([]);
176
+
177
+const getDetail = () => {
178
+  // props.recordId = 526;
179
+  if (!props.recordId) return;
180
+  getPageListData(`/call/translate/${props.recordId}`).then(({ data }) => {
181
+    console.log(data, 'res')
182
+    if (data.translate) {
183
+      const translate = JSON.parse(data.translate);
184
+      transcriptionMessages.value = translate.map((msg: any) => {
185
+
186
+        const msgInfo: any = {
187
+          direction: msg.Number.toString().length > 4 ? 2 : 1,
188
+          page_content: msg.Speech || '',
189
+          startTime: 0,
190
+          endTime: 0,
191
+          timestamp: '',
192
+        };
193
+        // console.log(msg.Time, 'msgInfo');
194
+        if (msg.Time) {
195
+          const times = JSON.parse(msg.Time);
196
+          if (times?.length) {
197
+            const timeArray = flatten(times);
198
+            const time = timeArray?.[0];
199
+            if (time) {
200
+              msgInfo.timestamp = formatMilliseconds(time);
201
+              msgInfo.startTime = timeArray[0];
202
+              msgInfo.endTime = timeArray[timeArray.length - 1];
203
+            }
204
+          }
205
+        }
206
+
207
+        return msgInfo;
208
+
209
+      })
210
+
211
+    }
212
+
213
+    if (data.translateJson) {
214
+      keywords = JSON.parse(data.translateJson);
215
+
216
+    }
217
+  })
218
+}
219
+
220
+const startTime = ref(0);
221
+const endTime = ref(0);
222
+// const duration = ref(0);
223
+// const isPlaying = ref(false);
224
+// const currentTime = ref(0);
225
+const currentIndex = ref(-1);
226
+const playSegment = (sTime: number, eTime: number, index) => {
227
+  currentIndex.value = index;
228
+  if (isPlaying.value) isPlaying.value = false
229
+  if (timer.value) clearInterval(timer.value);
230
+  progress.value = (sTime / 1000 / duration.value) * 100;
231
+  currentTime.value = sTime / 1000;
232
+  audioPlayer.value.currentTime = currentTime.value;
233
+  endTime.value = eTime / 1000;
234
+
235
+  togglePlay(true);
236
+
237
+  // isPlaying.value = true;
238
+  // playVideoSegment();
239
+};
240
+
241
+// // 播放指定区间
242
+// const playVideoSegment = () => {
243
+//   if (!audioPlayer.src) return;
244
+
245
+//   audioPlayer.currentTime = startTime.value;
246
+//   audioPlayer.play();
247
+//   isPlaying.value = true;
248
+
249
+//   // 模拟播放到指定片段结束
250
+//   const timer = setInterval(() => {
251
+//     if (currentTime.value >= endTime.value || !isPlaying.value) {
252
+//       clearInterval(timer);
253
+//       isPlaying.value = false;
254
+//       return;
255
+//     }
256
+//     progress.value = (currentTime.value / duration.value) * 100;
257
+//     currentTime.value += 1;
258
+//   }, 1000);
259
+
260
+//   // 设置区间结束监听
261
+//   const checkEnd = () => {
262
+//     if (audioPlayer.currentTime >= endTime.value) {
263
+//       pause();
264
+//       audioPlayer.currentTime = startTime.value; // 回到起点
265
+//     } else if (isPlaying.value) {
266
+//       requestAnimationFrame(checkEnd);
267
+//     }
268
+//   };
269
+//   requestAnimationFrame(checkEnd);
270
+// };
271
+
272
+// 暂停播放
273
+const pause = () => {
274
+  console.log(audioPlayer, 'audioPlayer');
275
+  audioPlayer.value.pause();
276
+  isPlaying.value = false;
277
+  currentIndex.value = -1;
278
+  // 清除定时器
279
+  clearInterval(timer.value);
280
+}
281
+const play = () => {
282
+  audioPlayer.value.play();
283
+  // isPlaying.value = true;
284
+}
285
+
286
+// 更新当前时间
287
+const updateTime = () => {
288
+  console.log('audioPlayer.value.currentTime', audioPlayer.value.currentTime)
289
+  currentTime.value = audioPlayer.value.currentTime;
290
+}
291
+
292
+const loadRemoteAudio = async () => {
293
+  if (!props.filePath) return
294
+
295
+  try {
296
+    // 创建音频对象
297
+    const audio = audioPlayer.value
298
+    audio.src = props.filePath
299
+
300
+    // 等待元数据加载
301
+    await new Promise((resolve, reject) => {
302
+      audio.onloadedmetadata = () => {
303
+        duration.value = audio.duration
304
+        console.log(duration.value, 'duration.value')
305
+        startTime.value = 0
306
+        endTime.value = audio.duration
307
+        resolve(1)
308
+      }
309
+
310
+      audio.onerror = (e) => {
311
+        reject(new Error('音频加载失败'))
312
+      }
313
+
314
+      // 设置超时处理
315
+      setTimeout(() => {
316
+        reject(new Error('音频加载超时'))
317
+      }, 10000)
318
+    })
319
+  } catch (err) {
320
+    console.log(err)
321
+  } finally {
322
+  }
323
+}
324
+
325
+onMounted(() => {
326
+  // props.recordId = props.recordId;
327
+  console.log('recordId', props.recordId, props.recordId, `props.filePath=>${props.filePath}`);
328
+  // props.filePath = 'http://1.194.161.64:9000/files/luyin/20250228/1002/eb445cb0-8fbb-44dc-af52-d1e11b778d53.wav';
329
+  if (props.filePath) {
330
+    loadRemoteAudio();
331
+  } 
332
+  getDetail();
333
+});
334
+</script>
335
+<style scoped>
336
+.el-drawer__body {
337
+  padding: 0;
338
+  overflow: hidden;
339
+}
340
+
341
+/* 隐藏滚动条但保留滚动功能 */
342
+.overflow-y-auto::-webkit-scrollbar {
343
+  display: none;
344
+}
345
+
346
+.overflow-y-auto {
347
+  -ms-overflow-style: none;
348
+  scrollbar-width: none;
349
+}
350
+</style>

+ 8 - 0
src/utils/tools.js

@@ -27,3 +27,11 @@ export const hidePhone = (phone) => {
27 27
 export const stripHtmlByRegex = (html) => {
28 28
     return html.replace(/<[^>]*>/g, '').replace(/&[^;]+;/g, '');
29 29
 }
30
+
31
+export const formatMilliseconds = (milliseconds) => {
32
+    const totalSeconds = Math.floor(milliseconds / 1000);
33
+    const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, '0');
34
+    const minutes = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, '0');
35
+    const seconds = (totalSeconds % 60).toString().padStart(2, '0');
36
+    return `${minutes}:${seconds}`;
37
+}

+ 27 - 8
src/views/main/phone/index.vue

@@ -469,7 +469,7 @@ import { getPageListData, createPageData } from '@/api/main/system/system';
469 469
 import { userDecryptToAsterisk } from '@/utils/aes';
470 470
 import knowledgeList from "@/views/main/knowledgeBase/knowledgeList/cpns/konwlegelist/konwlegelist";
471 471
 
472
-import { max,flatten, keys } from 'lodash';
472
+import { flatten } from 'lodash';
473 473
 import { getOfffixNuber, getCallSate } from '@/utils/index';
474 474
 import { stripHtmlByRegex } from '@/utils/tools';
475 475
 
@@ -559,6 +559,20 @@ const loadMoreCallData = async () => {
559 559
     }
560 560
     callLoading.value = false;
561 561
 };
562
+
563
+const saveTranslate = (callid, translate) => {
564
+    console.log(callid, translate, 'adsfasdf')
565
+    if (!translate || !translate.length || !callid) return;
566
+    createPageData('/call/translate', {
567
+        callid,
568
+        translateJson: JSON.stringify(translate),
569
+    }).then(() => {
570
+
571
+    }).catch(() => {
572
+
573
+    })
574
+}
575
+
562 576
 const handleCallScroll = async (event: Event) => {
563 577
     const target = event.target as HTMLElement;
564 578
     const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
@@ -762,6 +776,11 @@ const submit = async () => {
762 776
                 workordercate: form.value.type[form.value.type.length - 1],
763 777
             };
764 778
             if (callid.value) params.callId = callid.value;
779
+
780
+            if (keywords.value.length > 0 && callid.value) {
781
+                saveTranslate(callid.value, keywords.value);
782
+            }
783
+
765 784
             // 提交表单
766 785
             createPageData('/order/workorder', params).then((data) => {
767 786
                 console.log(data, 'submit');
@@ -917,13 +936,13 @@ window.addEventListener('AsrMessageEvent', (msg: any) => {
917 936
     transcripts.value.push(msgInfo);
918 937
 })
919 938
 
920
-function formatMilliseconds(milliseconds) {
921
-    const totalSeconds = Math.floor(milliseconds / 1000);
922
-    const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, '0');
923
-    const minutes = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, '0');
924
-    const seconds = (totalSeconds % 60).toString().padStart(2, '0');
925
-    return `${minutes}:${seconds}`;
926
-}
939
+// function formatMilliseconds(milliseconds) {
940
+//     const totalSeconds = Math.floor(milliseconds / 1000);
941
+//     const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, '0');
942
+//     const minutes = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, '0');
943
+//     const seconds = (totalSeconds % 60).toString().padStart(2, '0');
944
+//     return `${minutes}:${seconds}`;
945
+// }
927 946
 
928 947
 const aiSubmit = async () => {
929 948
     aiLoading.value = true;

+ 11 - 9
src/views/main/telephone/callRecord/callRecord.vue

@@ -50,14 +50,10 @@
50 50
         >
51 51
       </template>
52 52
     </page-content>
53
-    <el-dialog title="录音" v-model="open" width="700px" append-to-body>
54
-      <audio-player ref="pageModalRef" :filePath="filePath"></audio-player>
55
-      <template #footer>
56
-        <div class="dialog-footer">
57
-          <el-button @click="open = false">关 闭</el-button>
58
-        </div>
59
-      </template>
60
-    </el-dialog>
53
+    <el-drawer title="通话录音" v-model="open" width="500px" :with-header="false" append-to-body direction="rtl">
54
+      <audio-player v-if="open" :filePath="filePath" :recordId="clickId"/>
55
+    
56
+    </el-drawer>
61 57
     <el-dialog
62 58
       title="创建工单"
63 59
       v-model="openCreatOrder"
@@ -83,7 +79,8 @@
83 79
 
84 80
   import PageSearch from '@/components/page-search';
85 81
   import PageContent from '@/components/page-content';
86
-  import audioPlayer from './cpns/audioPlayer';
82
+  // import audioPlayer from './cpns/audioPlayer';
83
+  import audioPlayer from '@/components/callLogs/audioPlayer';
87 84
 
88 85
   import addOrder from '@/components/workOrder/add-order/add-order';
89 86
   import pageOrder from '@/components/page-order';
@@ -139,10 +136,14 @@
139 136
           callPhoneFlag.value = false;
140 137
         }
141 138
       });
139
+      const clickId = ref(0);
142 140
       //查看详情
143 141
       const handleViewData = (data) => {
144 142
         console.log(data.recordPath);
145 143
         filePath.value = data.recordPath;
144
+        console.log(data, 'data')
145
+        clickId.value = data.id;
146
+        console.log(clickId.value, 'clickId.value')
146 147
         open.value = true;
147 148
       };
148 149
 
@@ -206,6 +207,7 @@
206 207
       }
207 208
 
208 209
       return {
210
+        clickId,
209 211
         getCallSate,
210 212
         router,
211 213
         getTimeLimit,

+ 10 - 0
tsconfig.json

@@ -0,0 +1,10 @@
1
+{
2
+  "compilerOptions": {
3
+    "baseUrl": ".",
4
+    "paths": {
5
+      "@/*": ["src/*"],
6
+      "@/api/*": ["api/*"]
7
+    }
8
+  },
9
+  "include": ["src", "api"]
10
+}