Просмотр исходного кода

feat(main): 添加语音识别功能及优化界面交互

闪电 месяцев назад: 10
Родитель
Сommit
c5a4b44aa4
4 измененных файлов с 668 добавлено и 62 удалено
  1. 2 1
      package.json
  2. 45 2
      src/assets/style.css
  3. 7 1
      src/components/main/drawer-phone/index.vue
  4. 614 58
      src/views/main/phone/index.vue

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "jiayi",
3
-  "version": "3.8.5",
3
+  "version": "1.0.0",
4 4
   "description": "在线客服管理系统",
5 5
   "author": "加一",
6 6
   "license": "MIT",
@@ -77,6 +77,7 @@
77 77
     "qrcodejs2": "^0.0.2",
78 78
     "qrcodejs2-fix": "^0.0.1",
79 79
     "sortablejs": "^1.15.1",
80
+    "tailwindcss": "3.4.17",
80 81
     "tinymce": "^6.8.2",
81 82
     "vue": "3.2.45",
82 83
     "vue-cropper": "1.0.3",

+ 45 - 2
src/assets/style.css

@@ -588,6 +588,10 @@ video {
588 588
   }
589 589
 }
590 590
 
591
+.pointer-events-none {
592
+  pointer-events: none;
593
+}
594
+
591 595
 .visible {
592 596
   visibility: visible;
593 597
 }
@@ -636,6 +640,10 @@ video {
636 640
   right: 1.5rem;
637 641
 }
638 642
 
643
+.top-0 {
644
+  top: 0px;
645
+}
646
+
639 647
 .z-10 {
640 648
   z-index: 10;
641 649
 }
@@ -829,8 +837,8 @@ video {
829 837
   height: 400px;
830 838
 }
831 839
 
832
-.h-\[600px\] {
833
-  height: 600px;
840
+.h-\[500px\] {
841
+  height: 500px;
834 842
 }
835 843
 
836 844
 .h-full {
@@ -889,6 +897,10 @@ video {
889 897
   width: 24rem;
890 898
 }
891 899
 
900
+.w-\[100\%\] {
901
+  width: 100%;
902
+}
903
+
892 904
 .w-\[320px\] {
893 905
   width: 320px;
894 906
 }
@@ -1073,6 +1085,10 @@ video {
1073 1085
   border-radius: 9999px !important;
1074 1086
 }
1075 1087
 
1088
+.rounded {
1089
+  border-radius: 0.25rem;
1090
+}
1091
+
1076 1092
 .rounded-full {
1077 1093
   border-radius: 9999px;
1078 1094
 }
@@ -1188,20 +1204,38 @@ video {
1188 1204
   background-color: rgb(254 252 232 / var(--tw-bg-opacity, 1));
1189 1205
 }
1190 1206
 
1207
+.bg-gradient-to-b {
1208
+  background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
1209
+}
1210
+
1191 1211
 .bg-gradient-to-r {
1192 1212
   background-image: linear-gradient(to right, var(--tw-gradient-stops));
1193 1213
 }
1194 1214
 
1215
+.bg-gradient-to-t {
1216
+  background-image: linear-gradient(to top, var(--tw-gradient-stops));
1217
+}
1218
+
1195 1219
 .from-blue-500 {
1196 1220
   --tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);
1197 1221
   --tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);
1198 1222
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1199 1223
 }
1200 1224
 
1225
+.from-white {
1226
+  --tw-gradient-from: #fff var(--tw-gradient-from-position);
1227
+  --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);
1228
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1229
+}
1230
+
1201 1231
 .to-blue-600 {
1202 1232
   --tw-gradient-to: #2563eb var(--tw-gradient-to-position);
1203 1233
 }
1204 1234
 
1235
+.to-transparent {
1236
+  --tw-gradient-to: transparent var(--tw-gradient-to-position);
1237
+}
1238
+
1205 1239
 .object-cover {
1206 1240
   -o-object-fit: cover;
1207 1241
      object-fit: cover;
@@ -1254,6 +1288,10 @@ video {
1254 1288
   padding-right: 0.5rem;
1255 1289
 }
1256 1290
 
1291
+.pr-4 {
1292
+  padding-right: 1rem;
1293
+}
1294
+
1257 1295
 .pt-2 {
1258 1296
   padding-top: 0.5rem;
1259 1297
 }
@@ -1363,6 +1401,11 @@ video {
1363 1401
   color: rgb(75 85 99 / var(--tw-text-opacity, 1));
1364 1402
 }
1365 1403
 
1404
+.text-gray-700 {
1405
+  --tw-text-opacity: 1;
1406
+  color: rgb(55 65 81 / var(--tw-text-opacity, 1));
1407
+}
1408
+
1366 1409
 .text-gray-800 {
1367 1410
   --tw-text-opacity: 1;
1368 1411
   color: rgb(31 41 55 / var(--tw-text-opacity, 1));

+ 7 - 1
src/components/main/drawer-phone/index.vue

@@ -353,13 +353,19 @@ const initWs = () => {
353 353
                     break
354 354
                 case 'asr': // 坐席辅助
355 355
                     // incomingBack(telWSData)
356
-                    // asrMessageEvent(telWSData)
356
+                    asrMessageEvent(telWSData)
357 357
                     break
358 358
             }
359 359
         }
360 360
     })
361 361
 }
362 362
 
363
+const asrMessageEvent = (msg) => {
364
+  console.log('asrMessageEvent', msg)
365
+  const event = new CustomEvent("AsrMessageEvent", { detail: msg });
366
+  window.dispatchEvent(event);
367
+}
368
+
363 369
 onUnmounted(() => {
364 370
     if (timer) {
365 371
         clearInterval(timer);

+ 614 - 58
src/views/main/phone/index.vue

@@ -15,6 +15,7 @@
15 15
                                 客户档案
16 16
                             </div>
17 17
                         </template>
18
+
18 19
                         <div class="space-y-4">
19 20
                             <div class="bg-white rounded-lg p-4 shadow-sm">
20 21
                                 <div class="flex justify-between items-center mb-4">
@@ -111,7 +112,78 @@
111 112
                             </div>
112 113
                         </div>
113 114
                     </el-tab-pane>
114
-                    
115
+
116
+                    <!-- 坐席助手 -->
117
+                    <el-tab-pane>
118
+                        <template #label>
119
+                            <div class="flex items-center">
120
+                                <el-icon class="mr-2">
121
+                                    <Service />
122
+                                </el-icon>
123
+                                坐席助手
124
+                            </div>
125
+                        </template>
126
+                        <div class="space-y-4">
127
+                            <!-- 实时语音识别区域 -->
128
+                            <div class="bg-white rounded-lg p-4 shadow-sm">
129
+                                <div class="flex justify-between items-center mb-4">
130
+                                    <h3 class="text-lg font-medium">语音识别</h3>
131
+                                    <div class="flex items-center space-x-2">
132
+                                        <el-tag :type="voiceStatus === 'active' ? 'success' : 'info'" size="small">
133
+                                            {{ voiceStatus === 'active' ? '识别中' : '未开始' }}
134
+                                        </el-tag>
135
+                                    </div>
136
+                                </div>
137
+                                <div class="relative mb-4">
138
+                                    <div class="absolute left-0 top-0 w-full h-16 bg-gradient-to-b from
139
+from-white to-transparent pointer-events-none z-10"></div>
140
+                                    <div ref="transcriptContainer" class="h-[500px] overflow-y-auto pr-4 space-y-3">
141
+                                        <div v-for="(item, index) in transcripts" :key="index" class="p-3 rounded-lg"
142
+                                            :class="[item.direction === 2 ? 'bg-blue-50' : 'bg-gray-50']">
143
+                                            <div class="flex items-center mb-1">
144
+                                                <span class="text-xs text-gray-500">{{ item.timestamp }}</span>
145
+                                                <span class="text-xs font-medium ml-2"
146
+                                                    :class="[item.direction === 2  ? 'text-blue-600' : 'text-gray-600']">
147
+                                                    {{ item.direction === 2 ? '客户' : '坐席' }}
148
+                                                </span>
149
+                                                <div class="flex-1"></div>
150
+                                                <!-- <el-tag size="small" effect="plain" class="ml-2"
151
+                                                    :type="item.emotion === 'positive' ? 'success' : item.emotion === 'negative' ? 'danger' : 'info'">
152
+                                                    {{ item.emotion === 'positive' ? '积极' : item.emotion === 'negative'
153
+                                                    ? '消极' : '中性' }}
154
+                                                </el-tag> -->
155
+                                            </div>
156
+                                            <p class="text-sm text-gray-700">{{ item.page_content }}</p>
157
+                                        </div>
158
+                                    </div>
159
+                                    <div
160
+                                        class="absolute left-0 bottom-0 w-full h-16 bg-gradient-to-t from-white to-transparent pointer-events-none">
161
+                                    </div>
162
+                                </div>
163
+                                <!-- 关键词提示 -->
164
+                                <!-- <div class="space-y-2">
165
+                                    <h4 class="font-medium text-sm text-gray-600">关键词提示</h4>
166
+                                    <div class="flex flex-wrap gap-2">
167
+                                        <el-tag v-for="(keyword, idx) in keywords" :key="idx" :type="keyword.type"
168
+                                            effect="plain" size="small">
169
+                                            {{ keyword.text }}
170
+                                        </el-tag>
171
+                                    </div>
172
+                                </div> -->
173
+                                <!-- 建议话术 -->
174
+                                <!-- <div class="mt-4 space-y-2">
175
+                                    <h4 class="font-medium text-sm text-gray-600">建议话术</h4>
176
+                                    <div class="space-y-2">
177
+                                        <div v-for="(suggestion, idx) in suggestions" :key="idx"
178
+                                            class="p-2 bg-gray-50 rounded text-sm text-gray-600 cursor-pointer hover:bg-gray-100">
179
+                                            {{ suggestion }}
180
+                                        </div>
181
+                                    </div>
182
+                                </div> -->
183
+                            </div>
184
+                        </div>
185
+                    </el-tab-pane>
186
+
115 187
                     <!-- 通话记录 -->
116 188
                     <el-tab-pane>
117 189
                         <template #label>
@@ -124,34 +196,35 @@
124 196
                         </template>
125 197
                         <div class="space-y-4">
126 198
                             <!-- <el-input v-model="workOrderSearchQuery" placeholder="搜索工单..." :prefix-icon="Search" /> -->
127
-                            <el-date-picker v-model="callSearchQuery" 
128
-                                type="datetimerange" 
129
-                                @change="callChange"
130
-                                range-separator="至" 
131
-                                start-placeholder="开始日期" 
132
-                                end-placeholder="结束日期"/>
133
-                            <div class="space-y-2 overflow-y-auto" style="height: 100%;"  ref="callContainer" @scroll="handleCallScroll">
199
+                            <el-date-picker v-model="callSearchQuery" type="datetimerange" @change="callChange"
200
+                                range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" />
201
+                            <div class="space-y-2 overflow-y-auto" style="height: 100%;" ref="callContainer"
202
+                                @scroll="handleCallScroll">
134 203
                                 <div v-for="(item, index) in callData" :key="index"
135 204
                                     class="bg-white p-3 rounded-lg shadow-sm hover:shadow-md transition-shadow mb-4 border border-gray-100">
136 205
                                     <div class="flex justify-between items-start">
137
-                                        <div>  
206
+                                        <div>
138 207
                                             <!-- <h4 class="font-medium">{{ getOfffixNuber(item.callType==1 ? item.caller : item.callee) }}</h4> -->
139 208
                                             <div class="flex items-center gap-2 mt-1">
140
-                                                <span v-if="item.callerAgent || item.calleeAgent" style="margin-right: 15px;">
141
-                                                    坐席工号:{{ item.callType==2 ? item.callerAgent : item.calleeAgent  }}
209
+                                                <span v-if="item.callerAgent || item.calleeAgent"
210
+                                                    style="margin-right: 15px;">
211
+                                                    坐席工号:{{ item.callType == 2 ? item.callerAgent : item.calleeAgent }}
142 212
                                                 </span>
143 213
                                                 <span v-if="item.caller || item.callee">
144
-                                                    分机号:{{ item.callType==2 ? item.caller : item.callee  }}
214
+                                                    分机号:{{ item.callType == 2 ? item.caller : item.callee }}
145 215
                                                 </span>
146
-                                                
216
+
147 217
                                             </div>
148 218
                                         </div>
149
-                                        <span class="text-xs text-gray-500">{{ getCallSate(item.callType, item.isAnswer) }}</span>
219
+                                        <span class="text-xs text-gray-500">{{ getCallSate(item.callType, item.isAnswer)
220
+                                            }}</span>
150 221
                                     </div>
151 222
                                     <div class="text-sm text-gray-600 my-2 cursor-pointer">
152
-                                        <div v-if="item.answerTime">通话开始时间:{{item.answerTime}}</div>
153
-                                        <div v-if="item.hangupTime">通话结束时间:{{item.hangupTime}}</div>
154
-                                        <div v-if="item.answerTime && item.hangupTime">通话时长:{{getTimeLimit(item.answerTime, item.hangupTime)}}</div>
223
+                                        <div v-if="item.answerTime">通话开始时间:{{ item.answerTime }}</div>
224
+                                        <div v-if="item.hangupTime">通话结束时间:{{ item.hangupTime }}</div>
225
+                                        <div v-if="item.answerTime && item.hangupTime">
226
+                                            通话时长:{{ getTimeLimit(item.answerTime,
227
+                                            item.hangupTime)}}</div>
155 228
                                     </div>
156 229
                                 </div>
157 230
                                 <div v-if="callLoading" class="flex justify-center items-center py-4">
@@ -176,19 +249,18 @@
176 249
                             </div>
177 250
                         </template>
178 251
                         <div class="space-y-4">
179
-                            <el-date-picker v-model="orderSearchQuery" 
180
-                                type="datetimerange" 
181
-                                @change="orderChange"
182
-                                range-separator="至" 
183
-                                start-placeholder="开始日期" 
184
-                                end-placeholder="结束日期"/>
252
+                            <el-date-picker v-model="orderSearchQuery" type="datetimerange" @change="orderChange"
253
+                                range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" />
185 254
                             <!-- <el-input v-model="workOrderSearchQuery" placeholder="搜索工单..." :prefix-icon="Search" /> -->
186
-                            <div class="space-y-2 overflow-y-auto" style="height: 100%;" ref="workOrderContainer" @scroll="handleScroll">
255
+                            <div class="space-y-2 overflow-y-auto" style="height: 100%;" ref="workOrderContainer"
256
+                                @scroll="handleScroll">
187 257
                                 <div v-for="(item, index) in displayedActivities" :key="index"
188 258
                                     class="bg-white p-3 rounded-lg shadow-sm hover:shadow-md transition-shadow mb-4 border border-gray-100">
189 259
                                     <div class="flex justify-between items-start">
190 260
                                         <div>
191
-                                            <h4 class="font-medium">{{ item.workordercatename ? item.workordercatename.substring(item.workordercatename.lastIndexOf('/') + 1) : '' }}</h4>
261
+                                            <h4 class="font-medium">{{ item.workordercatename ?
262
+                                                item.workordercatename.substring(item.workordercatename.lastIndexOf('/')
263
+                                                + 1) : '' }}</h4>
192 264
                                             <!-- <div class="flex items-center gap-2 mt-1">
193 265
                                                 <el-tag size="small" type="info" class="!rounded-full">{{ item.workordercatename }}</el-tag>
194 266
                                             </div> -->
@@ -209,7 +281,8 @@
209 281
                                                         <Document />
210 282
                                                     </el-icon> -->
211 283
                                                     <span>办理结果</span>
212
-                                                    <span class="text-gray-400 ml-2">{{ item.endtime ? `[${item.endtime}]` : '' }}</span>
284
+                                                    <span class="text-gray-400 ml-2">{{ item.endtime ?
285
+                                                        `[${item.endtime}]` : '' }}</span>
213 286
                                                 </div>
214 287
                                                 <!-- <div class="text-gray-400">处理部门:{{ item.department || '客服部' }}</div> -->
215 288
                                             </div>
@@ -268,7 +341,12 @@
268 341
             <!-- 右侧工单区 -->
269 342
             <div class="w-[65%] space-y-6">
270 343
                 <div class="bg-white rounded-lg p-6 shadow-sm">
271
-                    <h2 class="text-xl font-medium mb-6">工单信息</h2>
344
+                    <div class="flex justify-between items-center mb-6">
345
+      <h2 class="text-xl font-medium">工单信息</h2>
346
+      
347
+      <el-button :loading-icon="Eleme" class="flex items-center gap-1" type="primary" link @click="aiSubmit"
348
+                    :loading="aiLoading">智能填单</el-button>
349
+    </div>
272 350
                     <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
273 351
                         <el-row :gutter="20">
274 352
                             <el-col :span="12">
@@ -283,14 +361,18 @@
283 361
                             </el-col>
284 362
                         </el-row>
285 363
                         <el-form-item label="工单类型" required prop="type">
286
-                            <el-cascader ref="typeCascaderRef" class="w-full" v-model="form.type" :props="{value: 'id',}"  placeholder="请选择工单类型" :options="ticketTypes" filterable clearable @change="handleTypeChange" />
364
+                            <el-cascader ref="typeCascaderRef" class="w-full" v-model="form.type"
365
+                                :props="{ value: 'id', }" placeholder="请选择工单类型" :options="ticketTypes" filterable
366
+                                clearable @change="handleTypeChange" />
287 367
                         </el-form-item>
288 368
                         <el-form-item v-if="form.typeCode === 'complain'" label="投诉部门" required prop="department">
289 369
                             <!-- <el-select v-model="form.department" placeholder="请选择投诉部门" class="w-full">
290 370
                                 <el-option v-for="item in departments" :key="item.value" :label="item.label"
291 371
                                     :value="item.value" />
292 372
                             </el-select> -->
293
-                            <el-cascader ref="deptCascaderRef" class="w-full" v-model="form.department" :props="{checkStrictly: true}" placeholder="请选择投诉科室" :options="departments" filterable @change="deptChange"/>
373
+                            <el-cascader ref="deptCascaderRef" class="w-full" v-model="form.department"
374
+                                :props="{ checkStrictly: true }" placeholder="请选择投诉科室" :options="departments" filterable
375
+                                @change="deptChange" />
294 376
                         </el-form-item>
295 377
                         <el-form-item label="问题描述" required prop="description">
296 378
                             <el-input v-model="form.description" type="textarea" :rows="4" placeholder="请详细描述问题" />
@@ -313,7 +395,8 @@
313 395
                         </el-form-item> -->
314 396
                         <!-- 悬浮提交按钮 -->
315 397
                         <div class="bottom-0 left-0 right-0 p-4 bg-white border-t flex justify-end">
316
-                            <el-button type="primary" class="w-32 !rounded-button whitespace-nowrap" @click="submit">提交工单</el-button>
398
+                            <el-button type="primary" class="w-32 !rounded-button whitespace-nowrap"
399
+                                @click="submit">提交工单</el-button>
317 400
                         </div>
318 401
                     </el-form>
319 402
 
@@ -325,7 +408,7 @@
325 408
     </div>
326 409
 </template>
327 410
 <script lang="ts" setup name="CallScreen">
328
-import { ref, computed, onMounted, onUnmounted } from 'vue';
411
+import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
329 412
 import moment from 'moment';
330 413
 import {
331 414
     UserFilled,
@@ -345,7 +428,9 @@ import {
345 428
     Connection,
346 429
     List,
347 430
     Notebook,
348
-    Edit
431
+    Edit,
432
+    Eleme,
433
+    Loading
349 434
 } from '@element-plus/icons-vue';
350 435
 import { hidePhone } from '@/utils/tools';
351 436
 import { ElMessage } from 'element-plus';
@@ -354,13 +439,15 @@ import { getPageListData, createPageData } from '@/api/main/system/system';
354 439
 import { userDecryptToAsterisk } from '@/utils/aes';
355 440
 import knowledgeList from "@/views/main/knowledgeBase/knowledgeList/cpns/konwlegelist/konwlegelist";
356 441
 
357
-import { max } from 'lodash';
358
-import { getOfffixNuber,getCallSate } from '@/utils/index';
442
+import { max,flatten } from 'lodash';
443
+import { getOfffixNuber, getCallSate } from '@/utils/index';
359 444
 
360 445
 const router = useRouter()
361 446
 let { proxy } = getCurrentInstance()
362 447
 const showCallPanel = ref(false);
363 448
 const searchQuery = ref('');
449
+const aiLoading = ref(false);
450
+const isCanAutoScroll = ref(1); 
364 451
 console.log('proxy', proxy.$route);
365 452
 // console.log(proxy.$route.query.callNumber)
366 453
 const telNumber = ref(proxy.$route.query.phone || proxy.$route.query.callNumber || proxy.$route.params.callNumber);
@@ -395,7 +482,7 @@ const loadMoreData = async () => {
395 482
     loading.value = true;
396 483
     await new Promise(resolve => setTimeout(resolve, 500)); // 模拟加载延迟
397 484
     const res = await getWorkOrders();
398
-    if (res.length === 0) {
485
+    if (res?.length === 0) {
399 486
         noMore.value = true;
400 487
     } else {
401 488
         pageIndex.value++;
@@ -411,15 +498,16 @@ const handleScroll = async (event: Event) => {
411 498
     }
412 499
 };
413 500
 // 通话记录开始
414
-const callData =ref([])
501
+const callData = ref([])
415 502
 const callLoading = ref(false)
416 503
 const callNoMore = ref(false);
417 504
 const loadMoreCallData = async () => {
418 505
     if (callLoading.value || callNoMore.value) return;
419 506
     callLoading.value = true;
420
-    await new Promise(resolve => setTimeout(resolve, 500)); // 模拟加载延迟
507
+    // await new Promise(resolve => setTimeout(resolve, 500)); // 模拟加载延迟
421 508
     const res = await getCalllog();
422
-    if (res.length === 0) {
509
+    console.log(res, 'res')
510
+    if (res?.length === 0) {
423 511
         callNoMore.value = true;
424 512
     } else {
425 513
         // displayedActivities.value.push(...newItems);
@@ -442,9 +530,9 @@ const callAnswerTime = ref()
442 530
 const callHangupTime = ref()
443 531
 function callChange(e) {
444 532
     console.log(e)
445
-    if (e.length>0) {
533
+    if (e.length > 0) {
446 534
         callAnswerTime.value = moment(e[0]).format('YYYY-MM-DD HH:mm:ss')
447
-        callHangupTime.value = moment(e[1]).format('YYYY-MM-DD HH:mm:ss') 
535
+        callHangupTime.value = moment(e[1]).format('YYYY-MM-DD HH:mm:ss')
448 536
     }
449 537
     callPageIndex.value = 1
450 538
     callData.value = []
@@ -460,17 +548,17 @@ const getCalllog = async () => {
460 548
     }
461 549
     if (callAnswerTime.value) {
462 550
         params.answerTime = callAnswerTime.value
463
-    } 
551
+    }
464 552
     if (callHangupTime) {
465 553
         params.hangupTime = callHangupTime.value
466 554
     }
467 555
     const result = await getPageListData('/call/calllog', params);
468 556
     console.log(result, 'callData');
469 557
     if (result.state === 'success') {
470
-        callData.value = callData.value.concat(result.data) 
558
+        callData.value = callData.value.concat(result.data)
471 559
         if (callData.value.length === 0) {
472 560
             callNoMore.value = true;
473
-        }else {
561
+        } else {
474 562
             callNoMore.value = false;
475 563
         }
476 564
         return result.data;
@@ -478,7 +566,7 @@ const getCalllog = async () => {
478 566
     return [];
479 567
 }
480 568
 function getTimeLimit(startTime, endTime) {
481
-  return proxy.getTimeDifference(startTime, endTime);
569
+    return proxy.getTimeDifference(startTime, endTime);
482 570
 }
483 571
 // 通话记录结束
484 572
 onMounted(() => {
@@ -520,7 +608,7 @@ const profile = ref({
520 608
 });
521 609
 const formRef: any = ref(null);
522 610
 const form = ref({
523
-    name: '',
611
+    name: '不详',
524 612
     phone: '',
525 613
     type: '',
526 614
     typeCode: '',
@@ -640,29 +728,29 @@ const submit = async () => {
640 728
                 } else {
641 729
                     ElMessage.error(data.message || '提交失败');
642 730
                 }
643
-                
731
+
644 732
             })
645
-            
733
+
646 734
         } else {
647 735
             ElMessage.error('请填写完整信息');
648 736
         }
649
-        
737
+
650 738
     });
651 739
 }
652 740
 /** 关闭按钮 */
653 741
 function close() {
654
-  const obj = { path: '/' }
655
-  proxy.$tab.closeOpenPage(obj)
742
+    const obj = { path: '/' }
743
+    proxy.$tab.closeOpenPage(obj)
656 744
 }
657 745
 const orderSearchQuery = ref()
658 746
 const createtime = ref()
659 747
 const endtime = ref()
660 748
 
661
-function orderChange (e){
749
+function orderChange(e) {
662 750
     console.log(e)
663
-    if (e.length>0) {
751
+    if (e.length > 0) {
664 752
         createtime.value = moment(e[0]).format('YYYY-MM-DD HH:mm:ss')
665
-        endtime.value = moment(e[1]).format('YYYY-MM-DD HH:mm:ss') 
753
+        endtime.value = moment(e[1]).format('YYYY-MM-DD HH:mm:ss')
666 754
     }
667 755
     pageIndex.value = 1
668 756
     displayedActivities.value = []
@@ -678,7 +766,7 @@ const getWorkOrders = async () => {
678 766
     }
679 767
     if (createtime.value) {
680 768
         params.createtime = createtime.value
681
-    } 
769
+    }
682 770
     if (endtime.value) {
683 771
         params.endtime = endtime.value
684 772
     }
@@ -689,7 +777,7 @@ const getWorkOrders = async () => {
689 777
         console.log(displayedActivities.value, 'activities');
690 778
         if (displayedActivities.value.length === 0) {
691 779
             noMore.value = true;
692
-        } else{
780
+        } else {
693 781
             noMore.value = false;
694 782
         }
695 783
         return result.data;
@@ -700,15 +788,483 @@ const getWorkOrders = async () => {
700 788
 const tabChange = (name) => {
701 789
     console.log(name, 'tabChange');
702 790
     if (name === '1') {
703
-    pageIndex.value = 1
791
+        pageIndex.value = 1
704 792
         callPageIndex.value = 1
705 793
         callData.value = []
706
-       getCalllog();
794
+        getCalllog();
707 795
     } else if (name === '2') {
708 796
         pageIndex.value = 1
709 797
         displayedActivities.value = []
710 798
         getWorkOrders();
711
-    } 
799
+    }
800
+
801
+}
802
+
803
+
804
+// 语音识别状态
805
+const voiceStatus = ref('active');
806
+// 识别记录
807
+const transcripts111 = ref([
808
+    {
809
+        role: 'customer',
810
+        timestamp: '14:30:24',
811
+        content: '您好,我想咨询一下这个智能客服系统的具体使用方法',
812
+        emotion: 'positive'
813
+    },
814
+    {
815
+        role: 'agent',
816
+        timestamp: '14:30:35',
817
+        content: '您好,很高兴为您介绍。我们的智能客服系统主要包含自动应答、多渠道接入、数据分析等核心功能,可以帮助企业提升客服效率',
818
+        emotion: 'positive'
819
+    },
820
+    {
821
+        role: 'customer',
822
+        timestamp: '14:30:48',
823
+        content: '听起来不错,那具体怎么配置自动应答功能呢?',
824
+        emotion: 'neutral'
825
+    },
826
+    {
827
+        role: 'agent',
828
+        timestamp: '14:31:02',
829
+        content: '好的,我来为您详细介绍。首先,您需要登录管理后台,点击右上角的"智能配置"按钮,在弹出的面板中选择"自动应答管理"',
830
+        emotion: 'positive'
831
+    },
832
+    {
833
+        role: 'customer',
834
+        timestamp: '14:31:15',
835
+        content: '找到了,这个界面设计得很直观',
836
+        emotion: 'positive'
837
+    },
838
+    {
839
+        role: 'agent',
840
+        timestamp: '14:31:28',
841
+        content: '是的,接下来您可以在知识库中添加常见问题和答案,系统会自动学习和匹配相似问题',
842
+        emotion: 'positive'
843
+    },
844
+    {
845
+        role: 'customer',
846
+        timestamp: '14:31:45',
847
+        content: '那如果遇到系统无法识别的问题怎么办?',
848
+        emotion: 'neutral'
849
+    },
850
+    {
851
+        role: 'agent',
852
+        timestamp: '14:32:00',
853
+        content: '这种情况系统会自动转人工客服处理,确保客户问题都能得到及时解决。同时系统会记录这些问题,用于后续知识库的优化',
854
+        emotion: 'positive'
855
+    },
856
+    {
857
+        role: 'customer',
858
+        timestamp: '14:32:18',
859
+        content: '明白了,那数据分析功能都包含哪些报表呢?',
860
+        emotion: 'positive'
861
+    },
862
+    {
863
+        role: 'agent',
864
+        timestamp: '14:32:35',
865
+        content: '我们提供了多维度的数据分析报表,包括客服效率分析、问题分类统计、客户满意度趋势、热点问题分析等',
866
+        emotion: 'positive'
867
+    },
868
+    {
869
+        role: 'customer',
870
+        timestamp: '14:32:52',
871
+        content: '这些报表数据可以导出吗?',
872
+        emotion: 'neutral'
873
+    },
874
+    {
875
+        role: 'agent',
876
+        timestamp: '14:33:08',
877
+        content: '可以的,系统支持Excel、PDF等多种格式的报表导出。您可以在报表页面右上角找到导出按钮',
878
+        emotion: 'positive'
879
+    },
880
+    {
881
+        role: 'customer',
882
+        timestamp: '14:33:25',
883
+        content: '太好了,这些功能正是我们需要的',
884
+        emotion: 'positive'
885
+    },
886
+    {
887
+        role: 'agent',
888
+        timestamp: '14:33:40',
889
+        content: '很高兴能帮到您。如果后续使用中有任何问题,随时都可以联系我们的技术支持团队',
890
+        emotion: 'positive'
891
+    },
892
+    {
893
+        role: 'customer',
894
+        timestamp: '14:33:55',
895
+        content: '好的,谢谢你细心的讲解',
896
+        emotion: 'positive'
897
+    },
898
+    {
899
+        role: 'agent',
900
+        timestamp: '14:34:10',
901
+        content: '不客气,为您提供优质的服务是我们的责任。祝您使用愉快!',
902
+        emotion: 'positive'
903
+    }
904
+]);
905
+
906
+const transcripts = ref([
907
+    {
908
+        direction: 1,
909
+        timestamp: '14:30:24',
910
+        page_content: '你好,中国热线请假。',
911
+    }, {
912
+        direction: 2,
913
+        timestamp: '14:30:24',
914
+        page_content: '喂,你好,嗯,听这样哦,我是林州这个横岛岳湖人呃',
915
+    }, {
916
+        direction: 1,
917
+        timestamp: '14:30:24',
918
+        page_content: '什么问题啊?你确实是无法统一工作呃,按照说实体上头到现在都没有工作了',
919
+    }, {
920
+        direction: 2,
921
+        timestamp: '14:30:24',
922
+        page_content: '我来问违约金怎么事儿?嗯,他说他他说是月底工资,',
923
+    }, {
924
+        direction: 1,
925
+        timestamp: '14:30:24',
926
+        page_content: '这个麻问您一下,我这边给您登记反映,您是林州市哪里的?',
927
+    }, {
928
+        direction: 2,
929
+        timestamp: '14:30:24',
930
+        page_content: '林州恒大悦府,恒大悦府',
931
+    }, {
932
+        direction: 1,
933
+        timestamp: '14:30:24',
934
+        page_content: '嗯,乐府乐是那个舒心一个舒心,一个对那个乐是吧?',
935
+    }, {
936
+        direction: 2,
937
+        timestamp: '14:30:24',
938
+        page_content: '嗯,对嗯',
939
+    }, {
940
+        direction: 1,
941
+        timestamp: '14:30:24',
942
+        page_content: '恒大乐府分几七部分啊,',
943
+    }, {
944
+        direction: 2,
945
+        timestamp: '14:30:24',
946
+        page_content: '分级,你们一期二期分不分呃,一期同大月付一期几号楼几单元,十七号楼一单元幺九零一十七号楼一单元幺九零一',
947
+    }, {
948
+        direction: 1,
949
+        timestamp: '14:30:24',
950
+        page_content: '嗯,您怎么称呼先生啊?',
951
+    }, {
952
+        direction: 2,
953
+        timestamp: '14:30:24',
954
+        page_content: '免贵?我就我我姓李李松,你让李先生吧',
955
+    }, {
956
+        direction: 1,
957
+        timestamp: '14:30:24',
958
+        page_content: '给您单心恒大悦府在林州市的哪个路段。您说一下',
959
+    }, {
960
+        direction: 2,
961
+        timestamp: '14:30:24',
962
+        page_content: '在这个这个是河南一个湖,也就他就一个湖,河南一个湖,我也不知道这个是什么路段',
963
+    }, {
964
+        direction: 1,
965
+        timestamp: '14:30:24',
966
+        page_content: '那您没有路段的话,这个位置不明确,我这边就暂不给您反映啊',
967
+    }, {
968
+        direction: 2,
969
+        timestamp: '14:30:24',
970
+        page_content: '嗯,那个是可能是那个呃华城相府,',
971
+    }, {
972
+        direction: 1,
973
+        timestamp: '14:30:24',
974
+        page_content: '哪个路段?',
975
+    }, {
976
+        direction: 2,
977
+        timestamp: '14:30:24',
978
+        page_content: '不是那个那个是呃鲁八大道,',
979
+    }, {
980
+        direction: 1,
981
+        timestamp: '14:30:24',
982
+        page_content: '鲁八大道中段什么大道,',
983
+    }, {
984
+        direction: 2,
985
+        timestamp: '14:30:24',
986
+        page_content: '鲁班哪两个字卢班鲁班嗯,',
987
+    }, {
988
+        direction: 1,
989
+        timestamp: '14:30:24',
990
+        page_content: '鲁班大道与什么路交叉口?',
991
+    }, {
992
+        direction: 2,
993
+        timestamp: '14:30:24',
994
+        page_content: '嗯,鲁班大道一零米,',
995
+    }, 
996
+    {
997
+        direction: 1,
998
+        timestamp: '14:30:24',
999
+        page_content: '你们是是什么路来的,',
1000
+    }, {
1001
+        direction: 2,
1002
+        timestamp: '14:30:24',
1003
+        page_content: '你就说恒大悦府就行了。',
1004
+    }, {
1005
+        direction: 1,
1006
+        timestamp: '14:30:24',
1007
+        page_content: '那个那个零六就你一个恒大悦府必须哪个路段,',
1008
+    }, {
1009
+        direction: 2,
1010
+        timestamp: '14:30:24',
1011
+        page_content: '我们这边也得清楚,鲁班大鲁班大道开元街道,',
1012
+    }, {
1013
+        direction: 1,
1014
+        timestamp: '14:30:24',
1015
+        page_content: '恒大悦府是吗?',
1016
+    }, {
1017
+        direction: 2,
1018
+        timestamp: '14:30:24',
1019
+        page_content: '哦,对,康阳街道的嗯,',
1020
+    }, {
1021
+        direction: 1,
1022
+        timestamp: '14:30:24',
1023
+        page_content: '行好的,这边给您登记一下啊。',
1024
+    }, {
1025
+        direction: 2,
1026
+        timestamp: '14:30:24',
1027
+        page_content: '嗯,好了,不歉。',
1028
+    }, {
1029
+        direction: 1,
1030
+        timestamp: '14:30:24',
1031
+        page_content: '喂,哎,可以了。',
1032
+    }, {
1033
+        direction: 2,
1034
+        timestamp: '14:30:24',
1035
+        page_content: '呃',
1036
+    }, {
1037
+        direction: 1,
1038
+        timestamp: '14:30:24',
1039
+        page_content: '刚才我给你他们啊嗯已经给您登记了,稍后反应呃。',
1040
+    }, {
1041
+        direction: 2,
1042
+        timestamp: '14:30:24',
1043
+        page_content: '登登记了,你登记的,我是你知道我反映的什么吗?',
1044
+    }, {
1045
+        direction: 1,
1046
+        timestamp: '14:30:24',
1047
+        page_content: '你不是家中暖气没热吗?不热吗?',
1048
+    }, {
1049
+        direction: 2,
1050
+        timestamp: '14:30:24',
1051
+        page_content: '这不是不热,是没充暖,我给物业,我给物业联系了,物业,说是呃,他们这里公司就两个人,一共十七栋楼,他问他说这个还能等待几天一栋一栋的开呢,我们的我我们的供暖费都交了。',
1052
+    }, {
1053
+        direction: 2,
1054
+        page_content: '看他几天,你看他几天',
1055
+    }, {
1056
+        direction: 1,
1057
+        page_content: '你给我说个定是不是嗯就是人员少,担心无法正常供暖,是不是啊?',
1058
+    }, {
1059
+        direction: 2,
1060
+        page_content: '他说还得几天,你现在十五号都是显示统一功能。你到现在就你你说排了几天,还得几天的。',
1061
+    }, {
1062
+        direction: 1,
1063
+        page_content: '嗯,好的,这边已经有您同小弟业主反映过了,跟您跟他同甘办理啊。',
1064
+    }, {
1065
+        direction: 2,
1066
+        page_content: '嗯嗯,行好的。',
1067
+    }
1068
+])
1069
+// 关键词提示
1070
+const keywords = ref([
1071
+    {
1072
+        text: '产品使用方法',
1073
+        type: 'primary'
1074
+    },
1075
+    {
1076
+        text: '操作流程',
1077
+        type: 'success'
1078
+    },
1079
+    {
1080
+        text: '正面反馈',
1081
+        type: 'success'
1082
+    },
1083
+    {
1084
+        text: '满意度高',
1085
+        type: 'success'
1086
+    }
1087
+]);
1088
+// 建议话术
1089
+const suggestions = ref([
1090
+    '很高兴能帮到您,如果还有其他问题随时询问',
1091
+    '这个功能确实很实用,您可以根据实际需求来调整配置',
1092
+    '感谢您的正面反馈,我们会继续努力提供更好的服务',
1093
+    '建议您也可以查看我们的帮助文档,里面有更详细的使用说明'
1094
+]);
1095
+const transcriptContainer = ref<HTMLElement | null>(null);
1096
+// 监听新消息,自动滚动到底部
1097
+// 监听新消息和内容变化,自动滚动到底部
1098
+watch(
1099
+    [
1100
+        () => transcripts.value.length,
1101
+        () => transcripts.value.map(t => t.page_content)
1102
+    ],
1103
+    () => {
1104
+        nextTick(() => {
1105
+            console.log(transcriptContainer.value, 'transcriptContainer');
1106
+            if (transcriptContainer.value) {
1107
+                transcriptContainer.value.scrollTop = transcriptContainer.value.scrollHeight;
1108
+            }
1109
+        });
1110
+    },
1111
+    { deep: true }
1112
+);
1113
+
1114
+window.addEventListener('AsrMessageEvent', (msg: any) => {
1115
+    console.log(msg.detail, '接收asr消息');
1116
+    if (!msg?.detail || !msg.detail.Result) return;
1117
+    const msgInfo = {
1118
+        direction: msg.detail.Number.toString().length > 4 ? 2 : 1,
1119
+        page_content: msg.detail.Speech || '',
1120
+        timestamp: '',
1121
+    };
1122
+    console.log(msg.detail.Time, 'msgInfo');
1123
+    if (msg.detail.Time) {
1124
+        const times = JSON.parse(msg.detail.Time);
1125
+
1126
+        console.log(times, 'msgInfo');
1127
+        if (times?.length) {
1128
+            const time = flatten(times)?.[0];
1129
+            if (time) msgInfo.timestamp = formatMilliseconds(time);
1130
+        }
1131
+    }
1132
+
1133
+    console.log(msgInfo, 'msgInfo');
1134
+
1135
+    transcripts.value.push(msgInfo);
1136
+})
1137
+
1138
+function formatMilliseconds(milliseconds) {
1139
+    const totalSeconds = Math.floor(milliseconds / 1000);
1140
+    const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, '0');
1141
+    const minutes = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, '0');
1142
+    const seconds = (totalSeconds % 60).toString().padStart(2, '0');
1143
+    return `${minutes}:${seconds}`;
1144
+}
1145
+
1146
+const aiSubmit = async () => {
1147
+    aiLoading.value = true;
1148
+    let text = transcripts.value.map((o) => o.page_content).join('');
1149
+    if (text?.length > 10) {
1150
+        const tx = `请从以下咨询对话中提取关键信息生成结构化工单:
1151
+        咨询对话内容:${text}
1152
+地理位置提取:识别来访者明确提到的居住地或当前位置
1153
+格式:[省/市/区/街道/楼栋号/房间号](如未提及则标记"未知")
1154
+注意模糊表述(如"北方城市""江浙地区"等需保留原话)
1155
+通话内容总结:
1156
+用第三人称概括核心问题(100字内)
1157
+保留以下关键要素:
1158
+• 主要症状描述(情绪/躯体/行为表现)
1159
+• 持续时间(使用"约X周/月/年"格式)
1160
+• 社会功能影响(工作/学习/人际关系)
1161
+• 既往病史(如提及)
1162
+过滤无关对话(寒暄、重复表述等)
1163
+工单类型分类:
1164
+根据DSM-5标准匹配最相关分类(单选):
1165
+[抑郁障碍] [焦虑障碍] [强迫症] [创伤应激] [人格障碍] [适应障碍] [人际关系] [发展性问题] [其他]
1166
+【处理规范】
1167
+优先识别直接症状陈述(如"失眠三个月""不敢见人")
1168
+注意隐喻表达(如"心里压着石头""像被困住")
1169
+存在自伤/伤人表述时自动触发危机预警协议
1170
+多问题并存时按主诉优先级排序
1171
+示例对话:
1172
+[咨询师]:可以说说最近困扰您的事吗?
1173
+[来访者]:我在杭州工作两年了,最近三个月每天失眠,开会时手抖得厉害,上周在地铁里突然喘不过气...
1174
+
1175
+示例输出:
1176
+【地理位置】:浙江省杭州市
1177
+【内容总结】:来访者诉持续三个月失眠症状,伴有社交场合手抖等躯体化表现,提及近期出现惊恐发作经历(地铁喘不过气),社会功能受损(工作受影响)
1178
+【工单类型】: 焦虑障碍
1179
+`;
1180
+        const res = await getSearchDocs(tx);
1181
+        console.log(res, 'res');
1182
+            
1183
+        res.split('\n').forEach((o) => {
1184
+            if (o.startsWith('【内容总结】')) form.value.description = o.replace('【内容总结】', '').replace(':', '');
1185
+            // if (o.startsWith('【事件地址】')) form.value.address = o.replace('【事件地址】', '').replace(':', '');
1186
+            // if (o.startsWith('【姓名】')) form.value.customerName = o.replace('【姓名】', '').replace(':', '');
1187
+            
1188
+        })
1189
+
1190
+        aiLoading.value = false;
1191
+        
1192
+    } else {
1193
+        aiLoading.value = false;
1194
+    }
1195
+}
1196
+async function getSearchDocs (text) {
1197
+    const params = {
1198
+        query: text,
1199
+        mode: "local_kb",
1200
+        kb_name: "mszsk",
1201
+        top_k: 1,
1202
+        score_threshold: 0.5,
1203
+        stream: true,
1204
+        model: "glm-4",
1205
+        temperature: 0.7,
1206
+        max_tokens: 0,
1207
+        prompt_name: "default",
1208
+        return_direct: false
1209
+    }
1210
+    try {
1211
+        // 发送请求
1212
+        let response = await fetch("http://192.168.1.89:7861/chat/chat",
1213
+            {
1214
+                method: "post",
1215
+                // responseType: "stream",
1216
+                headers: {
1217
+                    "Content-Type": "application/json",
1218
+                },
1219
+                body: JSON.stringify(params),
1220
+            }
1221
+        );
1222
+
1223
+        let resultStr = '';
1224
+
1225
+        // ok字段判断是否成功获取到数据流
1226
+        if (!response.ok) {
1227
+            throw new Error("Network response was not ok");
1228
+        }
1229
+        // 用来获取一个可读的流的读取器(Reader)以流的方式处理响应体数据
1230
+        const reader = response.body.getReader();
1231
+        // 将流中的字节数据解码为文本字符串
1232
+        const textDecoder = new TextDecoder();
1233
+        let result = true;
1234
+        let sqlValue = ''
1235
+
1236
+        while (result) {
1237
+            // done表示流是否已经完成读取  value包含读取到的数据块
1238
+            const { done, value } = await reader.read();
1239
+            if (done) {
1240
+                result = false;
1241
+                // console.log(resultStr, 'resultStr');
1242
+                return resultStr;
1243
+                break;
1244
+            }
1245
+            // 拿到的value就是后端分段返回的数据,大多是以data:开头的字符串
1246
+            // 需要通过decode方法处理数据块,例如转换为文本或进行其他操作
1247
+            const chunkText = textDecoder.decode(value).split("\n").forEach((val) => {
1248
+                if (!val) return;
1249
+                try {
1250
+                    // 后端返回的流式数据一般都是以data:开头的字符,排除掉data:后就是需要的数据
1251
+                    // 具体返回结构可以跟后端约定
1252
+                    let text = val?.replace("data:", "") || ""
1253
+                    const resultData = JSON.parse(text)
1254
+                    let resultText = resultData.choices[0].delta.content
1255
+                    
1256
+                    resultStr += resultText
1257
+                } catch (err) {
1258
+                    // console.log(err)
1259
+                }
1260
+            });
1261
+        }
1262
+
1263
+    } catch (err) {
1264
+        console.log(err)
1265
+
1266
+        return err;
1267
+    }
712 1268
 
713 1269
 }
714 1270