瀏覽代碼

feat: 增加关键词提示和推荐知识功能

闪电 10 月之前
父節點
當前提交
a1014c1fac
共有 8 個文件被更改,包括 272 次插入91 次删除
  1. 1 1
      src/App.vue
  2. 70 44
      src/assets/style.css
  3. 3 0
      src/permission.js
  4. 1 1
      src/store/commonSelect/common.js
  5. 15 0
      src/store/modules/keys.js
  6. 3 1
      src/utils/tools.js
  7. 36 0
      src/utils/trie.js
  8. 143 44
      src/views/main/phone/index.vue

+ 1 - 1
src/App.vue

@@ -8,7 +8,7 @@ import { handleThemeStyle } from '@/utils/theme'
8 8
 
9 9
 onMounted(() => {
10 10
   nextTick(() => {
11
-    // 初始化主题样式  
11
+    // 初始化主题样式
12 12
     handleThemeStyle(useSettingsStore().theme)
13 13
   })
14 14
 })

+ 70 - 44
src/assets/style.css

@@ -616,6 +616,14 @@ video {
616 616
   position: sticky;
617 617
 }
618 618
 
619
+.-right-1 {
620
+  right: -0.25rem;
621
+}
622
+
623
+.-top-1 {
624
+  top: -0.25rem;
625
+}
626
+
619 627
 .bottom-0 {
620 628
   bottom: 0px;
621 629
 }
@@ -660,14 +668,6 @@ video {
660 668
   grid-column: span 3 / span 3;
661 669
 }
662 670
 
663
-.col-span-4 {
664
-  grid-column: span 4 / span 4;
665
-}
666
-
667
-.col-span-6 {
668
-  grid-column: span 6 / span 6;
669
-}
670
-
671 671
 .m-2 {
672 672
   margin: 0.5rem;
673 673
 }
@@ -774,10 +774,6 @@ video {
774 774
   margin-top: auto;
775 775
 }
776 776
 
777
-.mt-6 {
778
-  margin-top: 1.5rem;
779
-}
780
-
781 777
 .line-clamp-2 {
782 778
   overflow: hidden;
783 779
   display: -webkit-box;
@@ -825,6 +821,18 @@ video {
825 821
   height: 4rem;
826 822
 }
827 823
 
824
+.h-20 {
825
+  height: 5rem;
826
+}
827
+
828
+.h-4 {
829
+  height: 1rem;
830
+}
831
+
832
+.h-40 {
833
+  height: 10rem;
834
+}
835
+
828 836
 .h-5 {
829 837
   height: 1.25rem;
830 838
 }
@@ -849,18 +857,18 @@ video {
849 857
   height: 100%;
850 858
 }
851 859
 
852
-.h-20 {
853
-  height: 5rem;
854
-}
855
-
856
-.h-40 {
857
-  height: 10rem;
860
+.max-h-96 {
861
+  max-height: 24rem;
858 862
 }
859 863
 
860 864
 .max-h-screen {
861 865
   max-height: 100vh;
862 866
 }
863 867
 
868
+.min-h-96 {
869
+  min-height: 24rem;
870
+}
871
+
864 872
 .min-h-screen {
865 873
   min-height: 100vh;
866 874
 }
@@ -897,10 +905,18 @@ video {
897 905
   width: 8rem;
898 906
 }
899 907
 
908
+.w-4 {
909
+  width: 1rem;
910
+}
911
+
900 912
 .w-40 {
901 913
   width: 10rem;
902 914
 }
903 915
 
916
+.w-5\/6 {
917
+  width: 83.333333%;
918
+}
919
+
904 920
 .w-96 {
905 921
   width: 24rem;
906 922
 }
@@ -929,10 +945,6 @@ video {
929 945
   width: 1px;
930 946
 }
931 947
 
932
-.w-5\/6 {
933
-  width: 83.333333%;
934
-}
935
-
936 948
 .max-w-7xl {
937 949
   max-width: 80rem;
938 950
 }
@@ -949,6 +961,16 @@ video {
949 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));
950 962
 }
951 963
 
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;
972
+}
973
+
952 974
 @keyframes spin {
953 975
   to {
954 976
     transform: rotate(360deg);
@@ -967,10 +989,6 @@ video {
967 989
   resize: both;
968 990
 }
969 991
 
970
-.grid-cols-12 {
971
-  grid-template-columns: repeat(12, minmax(0, 1fr));
972
-}
973
-
974 992
 .grid-cols-2 {
975 993
   grid-template-columns: repeat(2, minmax(0, 1fr));
976 994
 }
@@ -991,10 +1009,6 @@ video {
991 1009
   grid-template-columns: repeat(6, minmax(0, 1fr));
992 1010
 }
993 1011
 
994
-.grid-cols-1 {
995
-  grid-template-columns: repeat(1, minmax(0, 1fr));
996
-}
997
-
998 1012
 .flex-col {
999 1013
   flex-direction: column;
1000 1014
 }
@@ -1055,6 +1069,12 @@ video {
1055 1069
   margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
1056 1070
 }
1057 1071
 
1072
+.space-x-6 > :not([hidden]) ~ :not([hidden]) {
1073
+  --tw-space-x-reverse: 0;
1074
+  margin-right: calc(1.5rem * var(--tw-space-x-reverse));
1075
+  margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
1076
+}
1077
+
1058 1078
 .space-y-2 > :not([hidden]) ~ :not([hidden]) {
1059 1079
   --tw-space-y-reverse: 0;
1060 1080
   margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1079,12 +1099,6 @@ video {
1079 1099
   margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
1080 1100
 }
1081 1101
 
1082
-.space-x-6 > :not([hidden]) ~ :not([hidden]) {
1083
-  --tw-space-x-reverse: 0;
1084
-  margin-right: calc(1.5rem * var(--tw-space-x-reverse));
1085
-  margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
1086
-}
1087
-
1088 1102
 .overflow-hidden {
1089 1103
   overflow: hidden;
1090 1104
 }
@@ -1105,10 +1119,6 @@ video {
1105 1119
   border-radius: 9999px !important;
1106 1120
 }
1107 1121
 
1108
-.rounded {
1109
-  border-radius: 0.25rem;
1110
-}
1111
-
1112 1122
 .rounded-full {
1113 1123
   border-radius: 9999px;
1114 1124
 }
@@ -1339,10 +1349,6 @@ video {
1339 1349
   padding-top: 1.75rem;
1340 1350
 }
1341 1351
 
1342
-.pl-\[100px\] {
1343
-  padding-left: 100px;
1344
-}
1345
-
1346 1352
 .text-center {
1347 1353
   text-align: center;
1348 1354
 }
@@ -1373,6 +1379,10 @@ video {
1373 1379
   line-height: 2.25rem;
1374 1380
 }
1375 1381
 
1382
+.text-\[10px\] {
1383
+  font-size: 10px;
1384
+}
1385
+
1376 1386
 .text-base {
1377 1387
   font-size: 1rem;
1378 1388
   line-height: 1.5rem;
@@ -1592,10 +1602,26 @@ video {
1592 1602
   transition-duration: 150ms;
1593 1603
 }
1594 1604
 
1605
+.transition-transform {
1606
+  transition-property: transform;
1607
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1608
+  transition-duration: 150ms;
1609
+}
1610
+
1595 1611
 .duration-300 {
1596 1612
   transition-duration: 300ms;
1597 1613
 }
1598 1614
 
1615
+.ease-in-out {
1616
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1617
+}
1618
+
1619
+.hover\:scale-105:hover {
1620
+  --tw-scale-x: 1.05;
1621
+  --tw-scale-y: 1.05;
1622
+  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));
1623
+}
1624
+
1599 1625
 .hover\:bg-blue-600:hover {
1600 1626
   --tw-bg-opacity: 1;
1601 1627
   background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));

+ 3 - 0
src/permission.js

@@ -10,6 +10,7 @@ import useSystemStore from '@/store/main/system/system'
10 10
 import useSettingsStore from '@/store/modules/settings'
11 11
 import usePermissionStore from '@/store/modules/permission'
12 12
 import useSelectStore from '@/store/commonSelect/common'
13
+import useKeysStore from '@/store/modules/keys'
13 14
 
14 15
 NProgress.configure({ showSpinner: false })
15 16
 
@@ -46,6 +47,8 @@ router.beforeEach((to, from, next) => {
46 47
                   }
47 48
                 })
48 49
                 await useSelectStore().getSelectData()
50
+
51
+                await useKeysStore().setKeysTire()
49 52
                 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
50 53
               })
51 54
           })

+ 1 - 1
src/store/commonSelect/common.js

@@ -83,7 +83,7 @@ const useSelectStore = defineStore('common', {
83 83
             const selectCategoryResult = await getPageListData('/flowable/category')
84 84
             this.categoryListData = getUserSelcet(selectCategoryResult.data, 'categoryName', 'categoryName')
85 85
 
86
-        }
86
+        },
87 87
     }
88 88
 })
89 89
 function arrayToTree(list, root) {

+ 15 - 0
src/store/modules/keys.js

@@ -0,0 +1,15 @@
1
+import { getPageListData } from '@/api/main/system/system';
2
+import { buildTrie } from '@/utils/trie';
3
+const useKeysStore = defineStore('keys', {
4
+  state: () => ({
5
+    knowledgeKeys: [],
6
+  }),
7
+  actions: {
8
+    async setKeysTire() {
9
+      const knowledgeKeysResult = await getPageListData('/km/doc/GetDcoKeywords');
10
+      this.knowledgeKeys = buildTrie(knowledgeKeysResult.data ?? []);
11
+    },
12
+  },
13
+});
14
+
15
+export default useKeysStore;

+ 3 - 1
src/utils/tools.js

@@ -24,4 +24,6 @@ export const hidePhone = (phone) => {
24 24
     return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
25 25
 }
26 26
 
27
-
27
+export const stripHtmlByRegex = (html) => {
28
+    return html.replace(/<[^>]*>/g, '').replace(/&[^;]+;/g, '');
29
+}

+ 36 - 0
src/utils/trie.js

@@ -0,0 +1,36 @@
1
+// 构建Trie树
2
+export const buildTrie = (keywords) => {
3
+  const root = { maxLength: 0 };
4
+  let maxLength = 0;
5
+  for (const word of keywords) {
6
+    const normalizedWord = word.toLowerCase();
7
+    maxLength = Math.max(maxLength, normalizedWord.length);
8
+    let node = root;
9
+    for (const char of normalizedWord) {
10
+      if (!node[char]) node[char] = {};
11
+      node = node[char];
12
+    }
13
+    node.isEnd = true;
14
+    node.word = word; // 存储原始单词
15
+  }
16
+  root.maxLength = maxLength;
17
+  return root;
18
+};
19
+
20
+export const findKeyword = (input, trie) => {
21
+  const str = input.toLowerCase();
22
+  const maxLen = trie.maxLength;
23
+  const len = str.length;
24
+  const results = [];
25
+
26
+  for (let i = 0; i < len; i++) {
27
+    let node = trie;
28
+    for (let j = i; j < Math.min(i + maxLen, len); j++) {
29
+      const char = str[j];
30
+      node = node[char];
31
+      if (!node) break;
32
+      if (node.isEnd) results.push(node.word); // 添加找到的关键字到结果数组
33
+    }
34
+  }
35
+  return results; // 返回所有找到的关键字
36
+};

+ 143 - 44
src/views/main/phone/index.vue

@@ -125,9 +125,9 @@
125 125
                         </template>
126 126
                         <div class="space-y-4">
127 127
                             <!-- 实时语音识别区域 -->
128
-                            <div class="bg-white rounded-lg p-4 shadow-sm">
128
+                            <h3 class="text-lg font-medium">语音识别</h3>
129
+                            <div class="bg-white rounded-lg p-4 shadow-sm overflow-y-auto max-h-96 min-h-96">
129 130
                                 <div class="flex justify-between items-center mb-4">
130
-                                    <h3 class="text-lg font-medium">语音识别</h3>
131 131
                                     <!-- <div class="flex items-center space-x-2">
132 132
                                         <el-tag :type="voiceStatus === 'active' ? 'success' : 'info'" size="small">
133 133
                                             {{ voiceStatus === 'active' ? '识别中' : '未开始' }}
@@ -160,26 +160,41 @@ from-white to-transparent pointer-events-none z-10"></div>
160 160
                                         class="absolute left-0 bottom-0 w-full h-16 bg-gradient-to-t from-white to-transparent pointer-events-none">
161 161
                                     </div>
162 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 }}
163
+
164
+                            </div>
165
+
166
+                            <!-- 关键词提示 -->
167
+                            <div class="space-y-2">
168
+                                <h4 class="font-medium text-sm text-gray-600">关键词提示</h4>
169
+                                <div class="flex flex-wrap gap-2">
170
+                                    <el-tag v-for="(keyword, idx) in keywords" class="relative transition-transform duration-300 ease-in-out hover:scale-105" :key="idx" :type="keyword.type"
171
+                                        effect="plain" size="small" @click="getKnowledgeBaseList(keyword.text)">
172
+                                        {{ keyword.text }}
173
+                                        <span
174
+                                            class="absolute -top-1 -right-1 bg-blue-500 text-white text-[10px] rounded-full w-4 h-4 flex items-center justify-center">
175
+                                            {{ keyword.count }}
176
+                                        </span>
177
+                                    </el-tag>
178
+                                </div>
179
+                            </div>
180
+                            <!-- 推荐知识 -->
181
+                            <div class="mt-4 space-y-4">
182
+                                <h4 class="font-medium text-sm text-gray-600">推荐知识</h4>
183
+                                <div class="space-y-4">
184
+                                    <div v-for="(item, idx) in recommendedKnowledge" :key="idx"
185
+                                        class="p-4 bg-white rounded-lg shadow-sm hover:shadow-md transition-all cursor-pointer">
186
+                                        <div class="flex items-center justify-between mb-2">
187
+                                            <h5 class="font-medium">{{ item.title }}</h5>
188
+                                            <el-tag size="small" type="default">{{ item.directoryname }}</el-tag>
189
+                                        </div>
190
+                                        <p class="text-sm text-gray-600 line-clamp-2 mb-2">{{
191
+                                            stripHtmlByRegex(item.content) }}</p>
192
+                                        <div class="flex items-center justify-between text-xs text-gray-500">
193
+                                            <span>发布时间:{{ item.createTime }}</span>
194
+                                            <span>阅读:{{ item.reads }}</span>
180 195
                                         </div>
181 196
                                     </div>
182
-                                </div> -->
197
+                                </div>
183 198
                             </div>
184 199
                         </div>
185 200
                     </el-tab-pane>
@@ -342,11 +357,11 @@ from-white to-transparent pointer-events-none z-10"></div>
342 357
             <div class="w-[65%] space-y-6">
343 358
                 <div class="bg-white rounded-lg p-6 shadow-sm">
344 359
                     <div class="flex justify-between items-center mb-6">
345
-      <h2 class="text-xl font-medium">工单信息</h2>
346
-      
347
-      <el-button :loading-icon="Eleme" v-if="showAI && showAsr" class="flex items-center gap-1" type="primary" link @click="aiSubmit"
348
-                    :loading="aiLoading">智能填单</el-button>
349
-    </div>
360
+                        <h2 class="text-xl font-medium">工单信息</h2>
361
+
362
+                        <el-button :loading-icon="Eleme" v-if="showAI && showAsr" class="flex items-center gap-1"
363
+                            type="primary" link @click="aiSubmit" :loading="aiLoading">智能填单</el-button>
364
+                    </div>
350 365
                     <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
351 366
                         <el-row :gutter="20">
352 367
                             <el-col :span="12">
@@ -410,6 +425,7 @@ from-white to-transparent pointer-events-none z-10"></div>
410 425
 <script lang="ts" setup name="CallScreen">
411 426
 import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
412 427
 import moment from 'moment';
428
+import { findKeyword } from '@/utils/trie';
413 429
 import {
414 430
     UserFilled,
415 431
     Phone,
@@ -435,14 +451,14 @@ import {
435 451
 import { hidePhone } from '@/utils/tools';
436 452
 import { ElMessage } from 'element-plus';
437 453
 import useSelectStore from '@/store/commonSelect/common';
454
+import useKeysStore from'@/store/modules/keys'
438 455
 import { getPageListData, createPageData } from '@/api/main/system/system';
439 456
 import { userDecryptToAsterisk } from '@/utils/aes';
440 457
 import knowledgeList from "@/views/main/knowledgeBase/knowledgeList/cpns/konwlegelist/konwlegelist";
441 458
 
442
-import { max,flatten } from 'lodash';
459
+import { max,flatten, keys } from 'lodash';
443 460
 import { getOfffixNuber, getCallSate } from '@/utils/index';
444
-
445
-
461
+import { stripHtmlByRegex } from '@/utils/tools';
446 462
 
447 463
 const router = useRouter()
448 464
 let { proxy } = getCurrentInstance()
@@ -828,23 +844,24 @@ const transcripts: any = ref([
828 844
     //     page_content: '你好,中国热线请假。',
829 845
 ])
830 846
 // 关键词提示
831
-const keywords = ref([
832
-    {
833
-        text: '产品使用方法',
834
-        type: 'primary'
835
-    },
836
-    {
837
-        text: '操作流程',
838
-        type: 'success'
839
-    },
847
+const keywords: any = ref([
840 848
     {
841
-        text: '正面反馈',
842
-        type: 'success'
849
+        text: '强迫症',
850
+        type: 'primary',
851
+        count: 0,
843 852
     },
844
-    {
845
-        text: '满意度高',
846
-        type: 'success'
847
-    }
853
+    // {
854
+    //     text: '操作流程',
855
+    //     type: 'success'
856
+    // },
857
+    // {
858
+    //     text: '正面反馈',
859
+    //     type: 'success'
860
+    // },
861
+    // {
862
+    //     text: '满意度高',
863
+    //     type: 'success'
864
+    // }
848 865
 ]);
849 866
 // 建议话术
850 867
 const suggestions = ref([
@@ -893,7 +910,7 @@ window.addEventListener('AsrMessageEvent', (msg: any) => {
893 910
     }
894 911
 
895 912
     console.log(msgInfo, 'msgInfo');
896
-
913
+    if (msgInfo.page_content.length > 2) checkKeys(msgInfo.page_content);
897 914
     transcripts.value.push(msgInfo);
898 915
 })
899 916
 
@@ -1041,7 +1058,89 @@ async function getSearchDocs (text) {
1041 1058
 
1042 1059
 }
1043 1060
 
1061
+// 推荐知识
1062
+const recommendedKnowledge: any = ref([
1063
+    // {
1064
+    //     title: '智能客服系统功能配置指南',
1065
+    //     content: '本文详细介绍了智能客服系统的核心功能配置方法,包括自动应答规则设置、多渠道接入配置、知识库管理等内容。通过本指南,您可以快速掌握系统的基础配置和高级功能设置。',
1066
+    //     category: '使用指南',
1067
+    //     type: 'primary',
1068
+    //     publishTime: '2024-01-20',
1069
+    //     reads: 1234
1070
+    // },
1071
+    // {
1072
+    //     title: '常见故障诊断与解决方案',
1073
+    //     content: '汇总了系统使用过程中最常见的技术问题和解决方案,包括登录异常、数据同步失败、性能优化等问题的处理方法。本文档由技术支持团队整理,持续更新维护。',
1074
+    //     category: '技术支持',
1075
+    //     type: 'warning',
1076
+    //     publishTime: '2024-01-18',
1077
+    //     reads: 856
1078
+    // },
1079
+    // {
1080
+    //     title: '客服质量管理最佳实践',
1081
+    //     content: '本文从客服管理者的角度,详细介绍了如何提升客服团队的服务质量,包括话术规范、服务标准、质检方案等内容,帮助管理者建立高效的客服质量管理体系。',
1082
+    //     category: '最佳实践',
1083
+    //     type: 'success',
1084
+    //     publishTime: '2024-01-15',
1085
+    //     reads: 678
1086
+    // },
1087
+    // {
1088
+    //     title: '数据分析报表解读指南',
1089
+    //     content: '详细说明了系统各类数据分析报表的含义和使用方法,包括客服效率分析、满意度趋势、问题分类统计等报表的查看和导出方式,帮助您更好地利用数据指导工作。',
1090
+    //     category: '数据分析',
1091
+    //     type: 'info',
1092
+    //     publishTime: '2024-01-12',
1093
+    //     reads: 945
1094
+    // }
1095
+]);
1096
+
1097
+
1098
+const keys = useKeysStore().knowledgeKeys;
1099
+
1100
+console.log(keys, 'keys')
1101
+
1102
+const getKnowledgeBaseList = (key) => {
1103
+    if (key.length < 2) {
1104
+        return
1105
+    }
1106
+    suggestions.value = []
1107
+    
1108
+    getPageListData('/km/doc', {
1109
+        keywords: key,
1110
+        pageNum: 1,
1111
+        pageSize: 5,
1112
+    }).then(({ data, total }) => {
1113
+        recommendedKnowledge.value = data;
1114
+    });
1044 1115
 
1116
+}
1117
+
1118
+const checkKeys = (text: string) => {
1119
+    const list = findKeyword(text, keys);
1120
+    if (list?.length) {
1121
+        setKeys(list)
1122
+    }
1123
+}
1124
+
1125
+const setKeys = (keys: Array<string>) => {
1126
+    const types = ['primary', 'success', 'warning', 'danger'];
1127
+    for (const key of keys) {
1128
+        const index = keywords.value.findIndex((item) => item.text === key);
1129
+        if (index > -1) {
1130
+            keywords.value[index].count++;
1131
+            keywords.value[index].type = types[keywords.value[index].count % types.length];
1132
+        } else {
1133
+            keywords.value.push({
1134
+                text: key,
1135
+                type: 'primary',
1136
+                count: 1,
1137
+            });
1138
+        }
1139
+    }
1140
+
1141
+    keywords.value.sort((a, b) => b.count - a.count);
1142
+
1143
+}
1045 1144
 
1046 1145
 </script>
1047 1146
 <style scoped>