Kaynağa Gözat

feat(quality): 新增质检模块相关配置和组件

新增质检模块的配置文件(modal.config.js、search.config.js、content.config.js)和组件(audioPlayer.vue、index.vue、detail.vue、appeal.vue),并更新样式文件以支持新功能。这些更改为质检模块提供了基础配置和交互组件,支持质检列表、详情查看、申诉处理等功能。
闪电 8 ay önce
ebeveyn
işleme
ba793ba2cf

+ 202 - 0
src/assets/style.css

@@ -676,6 +676,94 @@ video {
676 676
   bottom: 0.25rem;
677 677
 }
678 678
 
679
+.-right-0 {
680
+  right: -0px;
681
+}
682
+
683
+.-right-1 {
684
+  right: -0.25rem;
685
+}
686
+
687
+.-right-3 {
688
+  right: -0.75rem;
689
+}
690
+
691
+.-top-0 {
692
+  top: -0px;
693
+}
694
+
695
+.-top-1 {
696
+  top: -0.25rem;
697
+}
698
+
699
+.-top-3 {
700
+  top: -0.75rem;
701
+}
702
+
703
+.bottom-8 {
704
+  bottom: 2rem;
705
+}
706
+
707
+.left-1\/2 {
708
+  left: 50%;
709
+}
710
+
711
+.-left-3 {
712
+  left: -0.75rem;
713
+}
714
+
715
+.-top-6 {
716
+  top: -1.5rem;
717
+}
718
+
719
+.-top-10 {
720
+  top: -2.5rem;
721
+}
722
+
723
+.-left-8 {
724
+  left: -2rem;
725
+}
726
+
727
+.top-0 {
728
+  top: 0px;
729
+}
730
+
731
+.-left-10 {
732
+  left: -2.5rem;
733
+}
734
+
735
+.-left-12 {
736
+  left: -3rem;
737
+}
738
+
739
+.-left-\[200px\] {
740
+  left: -200px;
741
+}
742
+
743
+.-left-\[0px\] {
744
+  left: -0px;
745
+}
746
+
747
+.-left-\[15px\] {
748
+  left: -15px;
749
+}
750
+
751
+.-left-\[30px\] {
752
+  left: -30px;
753
+}
754
+
755
+.-left-\[40px\] {
756
+  left: -40px;
757
+}
758
+
759
+.-left-\[50px\] {
760
+  left: -50px;
761
+}
762
+
763
+.-left-\[55px\] {
764
+  left: -55px;
765
+}
766
+
679 767
 .-z-0 {
680 768
   z-index: 0;
681 769
 }
@@ -873,6 +961,22 @@ video {
873 961
   height: 100%;
874 962
 }
875 963
 
964
+.h-2 {
965
+  height: 0.5rem;
966
+}
967
+
968
+.h-4 {
969
+  height: 1rem;
970
+}
971
+
972
+.h-8 {
973
+  height: 2rem;
974
+}
975
+
976
+.max-h-\[calc\(100vh-280px\)\] {
977
+  max-height: calc(100vh - 280px);
978
+}
979
+
876 980
 .min-h-screen {
877 981
   min-height: 100vh;
878 982
 }
@@ -957,10 +1061,34 @@ video {
957 1061
   width: 15rem;
958 1062
 }
959 1063
 
1064
+.w-3\/4 {
1065
+  width: 75%;
1066
+}
1067
+
1068
+.w-4 {
1069
+  width: 1rem;
1070
+}
1071
+
1072
+.w-8 {
1073
+  width: 2rem;
1074
+}
1075
+
1076
+.w-\[400px\] {
1077
+  width: 400px;
1078
+}
1079
+
960 1080
 .min-w-0 {
961 1081
   min-width: 0px;
962 1082
 }
963 1083
 
1084
+.max-w-7xl {
1085
+  max-width: 80rem;
1086
+}
1087
+
1088
+.max-w-\[80\%\] {
1089
+  max-width: 80%;
1090
+}
1091
+
964 1092
 .flex-1 {
965 1093
   flex: 1 1 0%;
966 1094
 }
@@ -982,6 +1110,11 @@ video {
982 1110
   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));
983 1111
 }
984 1112
 
1113
+.-translate-x-1\/2 {
1114
+  --tw-translate-x: -50%;
1115
+  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));
1116
+}
1117
+
985 1118
 .rotate-180 {
986 1119
   --tw-rotate: 180deg;
987 1120
   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));
@@ -1023,6 +1156,10 @@ video {
1023 1156
   grid-template-columns: repeat(8, minmax(0, 1fr));
1024 1157
 }
1025 1158
 
1159
+.flex-row-reverse {
1160
+  flex-direction: row-reverse;
1161
+}
1162
+
1026 1163
 .flex-col {
1027 1164
   flex-direction: column;
1028 1165
 }
@@ -1031,6 +1168,10 @@ video {
1031 1168
   flex-wrap: wrap;
1032 1169
 }
1033 1170
 
1171
+.items-start {
1172
+  align-items: flex-start;
1173
+}
1174
+
1034 1175
 .items-center {
1035 1176
   align-items: center;
1036 1177
 }
@@ -1079,6 +1220,10 @@ video {
1079 1220
   gap: 15px;
1080 1221
 }
1081 1222
 
1223
+.gap-8 {
1224
+  gap: 2rem;
1225
+}
1226
+
1082 1227
 .space-x-2 > :not([hidden]) ~ :not([hidden]) {
1083 1228
   --tw-space-x-reverse: 0;
1084 1229
   margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@@ -1164,6 +1309,14 @@ video {
1164 1309
   border-top-right-radius: 0.5rem;
1165 1310
 }
1166 1311
 
1312
+.\!rounded-br-none {
1313
+  border-bottom-right-radius: 0px !important;
1314
+}
1315
+
1316
+.\!rounded-tl-none {
1317
+  border-top-left-radius: 0px !important;
1318
+}
1319
+
1167 1320
 .border {
1168 1321
   border-width: 1px;
1169 1322
 }
@@ -1245,6 +1398,16 @@ video {
1245 1398
   background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
1246 1399
 }
1247 1400
 
1401
+.bg-blue-500 {
1402
+  --tw-bg-opacity: 1;
1403
+  background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
1404
+}
1405
+
1406
+.bg-gray-100 {
1407
+  --tw-bg-opacity: 1;
1408
+  background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
1409
+}
1410
+
1248 1411
 .bg-gradient-to-br {
1249 1412
   background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
1250 1413
 }
@@ -1311,6 +1474,14 @@ video {
1311 1474
   padding: 0.25rem;
1312 1475
 }
1313 1476
 
1477
+.\!p-1 {
1478
+  padding: 0.25rem !important;
1479
+}
1480
+
1481
+.p-1\.5 {
1482
+  padding: 0.375rem;
1483
+}
1484
+
1314 1485
 .px-2 {
1315 1486
   padding-left: 0.5rem;
1316 1487
   padding-right: 0.5rem;
@@ -1371,6 +1542,14 @@ video {
1371 1542
   padding-top: 1.75rem;
1372 1543
 }
1373 1544
 
1545
+.pb-6 {
1546
+  padding-bottom: 1.5rem;
1547
+}
1548
+
1549
+.pt-4 {
1550
+  padding-top: 1rem;
1551
+}
1552
+
1374 1553
 .text-center {
1375 1554
   text-align: center;
1376 1555
 }
@@ -1541,6 +1720,16 @@ video {
1541 1720
   color: rgb(245 158 11 / var(--tw-text-opacity, 1));
1542 1721
 }
1543 1722
 
1723
+.text-gray-900 {
1724
+  --tw-text-opacity: 1;
1725
+  color: rgb(17 24 39 / var(--tw-text-opacity, 1));
1726
+}
1727
+
1728
+.text-green-600 {
1729
+  --tw-text-opacity: 1;
1730
+  color: rgb(22 163 74 / var(--tw-text-opacity, 1));
1731
+}
1732
+
1544 1733
 .underline {
1545 1734
   text-decoration-line: underline;
1546 1735
 }
@@ -1681,6 +1870,11 @@ video {
1681 1870
   color: rgb(59 130 246 / var(--tw-text-opacity, 1));
1682 1871
 }
1683 1872
 
1873
+.hover\:text-blue-600:hover {
1874
+  --tw-text-opacity: 1;
1875
+  color: rgb(37 99 235 / var(--tw-text-opacity, 1));
1876
+}
1877
+
1684 1878
 .hover\:shadow-xl:hover {
1685 1879
   --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
1686 1880
   --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
@@ -1697,6 +1891,14 @@ video {
1697 1891
   outline-offset: 2px;
1698 1892
 }
1699 1893
 
1894
+.group:hover .group-hover\:block {
1895
+  display: block;
1896
+}
1897
+
1898
+.group:hover .group-hover\:flex {
1899
+  display: flex;
1900
+}
1901
+
1700 1902
 .group:hover .group-hover\:opacity-100 {
1701 1903
   opacity: 1;
1702 1904
 }

+ 30 - 0
src/components/audio/audioPlayer.vue

@@ -0,0 +1,30 @@
1
+<template>
2
+    <!-- 通话录音 -->
3
+    <div>
4
+        <h3 class="text-gray-900 font-medium mb-4">通话录音</h3>
5
+        <div class="bg-gray-50 p-4 rounded-lg">
6
+          <div class="flex items-center gap-4">
7
+            <button class="flex items-center !rounded-button whitespace-nowrap">
8
+              <el-icon class="mr-2"><Microphone /></el-icon>
9
+              音量
10
+            </button>
11
+            <div class="flex-1 h-2 bg-gray-200 rounded-full">
12
+              <div class="w-3/4 h-full bg-blue-500 rounded-full"></div>
13
+            </div>
14
+            <span class="text-gray-600">02:19</span>
15
+            <button class="text-blue-600 !rounded-button whitespace-nowrap">
16
+              <el-icon><Mute /></el-icon>
17
+            </button>
18
+            <button class="text-blue-600 !rounded-button whitespace-nowrap">
19
+              <el-icon><VideoPlay /></el-icon>
20
+            </button>
21
+            <button class="text-blue-600 !rounded-button whitespace-nowrap">
22
+              <el-icon><Refresh /></el-icon>
23
+            </button>
24
+          </div>
25
+        </div>
26
+    </div>
27
+</template>
28
+<script setup lang="ts">
29
+import { Microphone, Mute, VideoPlay, Refresh } from '@element-plus/icons-vue'
30
+</script>

+ 259 - 0
src/views/quality/appeal.vue

@@ -0,0 +1,259 @@
1
+<!-- 代码已包含 CSS:使用 TailwindCSS , 安装 TailwindCSS 后方可看到布局样式效果 -->
2
+<template>
3
+    <div class="app-container bg-gray-50">
4
+        <div class="mx-auto">
5
+            <div class="flex gap-6">
6
+                <!-- 左侧对话区域 -->
7
+                <div class="flex-1 bg-white rounded-lg shadow-sm p-6">
8
+                    <div class="mb-6">
9
+                        <h2 class="text-xl font-medium">质检详情</h2>
10
+                        <div class="mt-4 grid grid-cols-4 gap-4 text-sm text-gray-600">
11
+                            <div>质检ID: {{ inspectionId }}</div>
12
+                            <div>外呼号码: {{ phoneNumber }}</div>
13
+                            <div>外呼时间: {{ callTime }}</div>
14
+                            <div>坐席: {{ agent }}</div>
15
+                        </div>
16
+                    </div>
17
+                    <div class="space-y-6 overflow-y-auto max-h-[calc(100vh-280px)]  px-4">
18
+                        <template v-for="(msg, index) in messages" :key="index">
19
+                            <div :class="['flex', msg.type === 'agent' ? 'justify-end' : '']">
20
+                                <div
21
+                                    :class="['flex items-start gap-3 max-w-[80%]', msg.type === 'agent' ? 'flex-row-reverse' : '']">
22
+                                    <div class="w-8 h-8 rounded-full bg-gray-200 flex-shrink-0 overflow-hidden">
23
+                                        <el-icon class="w-full h-full p-1.5 text-gray-500">
24
+                                            <User v-if="msg.type === 'customer'" />
25
+                                            <Service v-else />
26
+                                        </el-icon>
27
+                                    </div>
28
+                                    <div
29
+                                        :class="['p-3 rounded-lg relative group', msg.type === 'agent' ? 'bg-blue-500 text-white' : 'bg-gray-100']">
30
+                                        <div class="text-sm mb-1">{{ msg.type === 'agent' ? '坐席' : '客户' }} ({{ msg.time
31
+                                            }})</div>
32
+                                        <div>{{ msg.content }}</div>
33
+                                        <div v-if="msg.warning" class="mt-2 text-xs text-red-500">{{ msg.warning }}
34
+                                        </div>
35
+                                        <el-button v-show="false"
36
+                                            class="group-hover:block absolute -top-3 -right-3 !p-1 !rounded-full bg-red-500 text-white shadow-md"
37
+                                            @click.stop="handleAppeal">
38
+                                            <el-icon class="text-xs">
39
+                                                <Close />
40
+                                            </el-icon>
41
+                                        </el-button>
42
+                                        <div v-if="msg.type === 'agent'" class="hidden group-hover:flex items-center justify-center absolute top-0 -left-[55px] -translate-x-1/2 !p-1 ">
43
+                                            <el-button
44
+                                            @click.stop="handleAppeal">
45
+                                            <el-icon>
46
+                                                <CirclePlus />
47
+                                            </el-icon>
48
+                                            加入申诉
49
+                                        </el-button>
50
+                                        </div>
51
+                                    </div>
52
+                                </div>
53
+                            </div>
54
+                        </template>
55
+                    </div>
56
+                    <div class="mt-6 border-t pt-4">
57
+                        <audio-player ></audio-player>
58
+                        <div class="mt-4 flex items-center text-sm text-gray-500">
59
+                            <span class="mr-4">准确率: {{ accuracy }}%</span>
60
+                            <span>识别率: {{ recognition }}%</span>
61
+                        </div>
62
+                    </div>
63
+                </div>
64
+                <!-- 右侧质检信息 -->
65
+                <div class="w-[400px] bg-white rounded-lg shadow-sm p-6">
66
+                    <div class="flex justify-between items-center mb-6">
67
+                        <h3 class="text-lg font-medium">AI质检</h3>
68
+                        <div class="flex items-center gap-2">
69
+                            <span class="text-green-500">合格</span>
70
+                            <span class="text-blue-500 text-xl font-medium">{{ score }}分</span>
71
+                        </div>
72
+                    </div>
73
+                    <div class="space-y-6 overflow-y-auto max-h-[calc(100vh-280px)]">
74
+                        <div>
75
+                            <h4 class="text-base font-medium mb-4">质检概况</h4>
76
+                            <div class="grid grid-cols-3 gap-4">
77
+                                <div class="text-center p-3 bg-gray-50 rounded">
78
+                                    <div class="text-sm text-gray-500">平均语速</div>
79
+                                    <div class="mt-1 font-medium">{{ stats.speed }}字/分钟</div>
80
+                                </div>
81
+                                <div class="text-center p-3 bg-gray-50 rounded">
82
+                                    <div class="text-sm text-gray-500">平均分值</div>
83
+                                    <div class="mt-1 font-medium">{{ stats.avgScore }}分</div>
84
+                                </div>
85
+                                <div class="text-center p-3 bg-gray-50 rounded">
86
+                                    <div class="text-sm text-gray-500">静音时长</div>
87
+                                    <div class="mt-1 font-medium">{{ stats.silenceTime }}s</div>
88
+                                </div>
89
+                            </div>
90
+                        </div>
91
+                        <div>
92
+                            <h4 class="text-base font-medium mb-4">评分标准</h4>
93
+                            <el-table :data="scoreStandards" border stripe>
94
+                                <el-table-column prop="item" label="命中项" />
95
+                                <el-table-column prop="status" label="是否致命" />
96
+                                <el-table-column prop="score" label="分数">
97
+                                    <template #default="scope">
98
+                                        <span :class="scope.row.score < 0 ? 'text-red-500' : ''">{{ scope.row.score
99
+                                            }}</span>
100
+                                    </template>
101
+                                </el-table-column>
102
+                            </el-table>
103
+                        </div>
104
+                        <div>
105
+                            <h4 class="text-base font-medium mb-4">申诉列表</h4>
106
+                            <div class="space-y-3">
107
+                                <div v-for="(appeal, index) in appeals" :key="index"
108
+                                    class="p-3 bg-gray-50 rounded relative group">
109
+                                    <div class="flex justify-between items-center">
110
+                                        <div class="font-medium">申诉片段 {{ index + 1 }}</div>
111
+                                        <div class="text-sm text-gray-500">{{ appeal.time }}</div>
112
+                                    </div>
113
+                                    <div class="mt-2 text-sm">{{ appeal.content }}</div>
114
+                                    <div class="mt-1 text-xs text-red-500">{{ appeal.warning }}</div>
115
+                                    <el-button
116
+                                        class="hidden group-hover:block absolute -top-0 -right-0 !p-1 !rounded-tl-none !rounded-br-none bg-red-500 text-white shadow-md"
117
+                                        @click.stop="removeAppeal(index)">
118
+                                        <el-icon class="text-xs">
119
+                                            <Close />
120
+                                        </el-icon>
121
+                                    </el-button>
122
+                                </div>
123
+                            </div>
124
+                        </div>
125
+                    </div>
126
+                    <div class="fixed bottom-8 right-8">
127
+                        <el-button type="primary" class="!rounded-button" size="large" @click="handleAppeal">
128
+                            申诉
129
+                            <div
130
+                                class="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-xs text-white flex items-center justify-center">
131
+                                2
132
+                            </div>
133
+                        </el-button>
134
+                    </div>
135
+                </div>
136
+            </div>
137
+        </div>
138
+    </div>
139
+</template>
140
+<script lang="ts" setup>
141
+import { ref } from 'vue';
142
+import audioPlayer from '../../components/audio/audioPlayer.vue';
143
+import { User, Service, VideoPlay, VideoPause, Close } from '@element-plus/icons-vue';
144
+const inspectionId = '893821';
145
+const phoneNumber = '137 8651 5262';
146
+const callTime = '2024-12-09 15:30';
147
+const agent = '李世海';
148
+const messages = ref([
149
+    {
150
+        type: 'agent',
151
+        time: '00:06',
152
+        content: '你好,欢迎拨打......',
153
+        warning: '语速 160字/分钟 命中规则条件:语速不符合标准'
154
+    },
155
+    {
156
+        type: 'customer',
157
+        time: '00:16',
158
+        content: '你好,我想咨询一下......'
159
+    },
160
+    {
161
+        type: 'agent',
162
+        time: '00:59',
163
+        content: '好的,很高兴为您解答。请问您想具体了解哪方面的......',
164
+        warning: '语速 160字/分钟 命中规则条件:语速不符合标准'
165
+    },
166
+    {
167
+        type: 'agent',
168
+        time: '00:59',
169
+        content: '好的,很高兴为您解答。请问您想具体了解哪方面的......',
170
+        warning: '语速 160字/分钟 命中规则条件:语速不符合标准'
171
+    },
172
+    {
173
+        type: 'agent',
174
+        time: '00:59',
175
+        content: '好的,很高兴为您解答。请问您想具体了解哪方面的......',
176
+        warning: '语速 160字/分钟 命中规则条件:语速不符合标准'
177
+    },
178
+    {
179
+        type: 'agent',
180
+        time: '00:59',
181
+        content: '好的,很高兴为您解答。请问您想具体了解哪方面的......',
182
+        warning: '语速 160字/分钟 命中规则条件:语速不符合标准'
183
+    },
184
+    {
185
+        type: 'agent',
186
+        time: '00:59',
187
+        content: '好的,很高兴为您解答。请问您想具体了解哪方面的......',
188
+        warning: '语速 160字/分钟 命中规则条件:语速不符合标准'
189
+    }
190
+]);
191
+const isPlaying = ref(false);
192
+const progress = ref(0);
193
+const currentTime = ref(0);
194
+const totalTime = ref(139);
195
+const accuracy = ref(95.3);
196
+const recognition = ref(98.6);
197
+const score = ref(90);
198
+const stats = ref({
199
+    speed: 150,
200
+    avgScore: 60,
201
+    silenceTime: 20
202
+});
203
+const scoreStandards = ref([
204
+    { item: '语速不规范', status: '不致命', score: -5 },
205
+    { item: '音量不规范', status: '不致命', score: -5 },
206
+    { item: '反问客户', status: '不致命', score: -5 }
207
+]);
208
+const appeals = ref([
209
+    {
210
+        time: '坐席(00:06)',
211
+        content: '你好,欢迎拨打......',
212
+        warning: '命中规则条件:语速不符合标准'
213
+    },
214
+    {
215
+        time: '坐席(00:06)',
216
+        content: '你好,欢迎拨打......',
217
+        warning: '命中规则条件:语速不符合标准'
218
+    }
219
+]);
220
+const togglePlay = () => {
221
+    isPlaying.value = !isPlaying.value;
222
+};
223
+const formatTime = (seconds: number) => {
224
+    const minutes = Math.floor(seconds / 60);
225
+    const remainingSeconds = Math.floor(seconds % 60);
226
+    return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
227
+};
228
+const handleAppeal = () => {
229
+    // 处理申诉逻辑
230
+};
231
+const removeAppeal = (index: number) => {
232
+    appeals.value.splice(index, 1);
233
+};
234
+</script>
235
+<style scoped>
236
+.el-slider {
237
+    --el-slider-button-size: 16px;
238
+    --el-slider-height: 6px;
239
+}
240
+
241
+:deep(.el-table) {
242
+    --el-table-border-color: #e5e7eb;
243
+    --el-table-header-bg-color: #f9fafb;
244
+    --el-table-row-hover-bg-color: #f3f4f6;
245
+}
246
+
247
+:deep(.el-table th) {
248
+    font-weight: 500;
249
+    color: #374151;
250
+}
251
+
252
+:deep(.el-table td) {
253
+    color: #6b7280;
254
+}
255
+
256
+:deep(.el-button) {
257
+    font-weight: 500;
258
+}
259
+</style>

+ 19 - 0
src/views/quality/config/content.config.js

@@ -0,0 +1,19 @@
1
+export const contentTableConfig = {
2
+    title: '质检列表',
3
+    contentTableHeader: true,
4
+    propList: [
5
+        { prop: 'mobile', label: '质检 ID', width: '100', align: 'center', fixed: 'left' },
6
+        { prop: 'name', label: '外呼号码', width: '150' },
7
+        { prop: 'notes', label: '外呼日期', width: '170' },
8
+        { prop: 'notes', label: '致命项', width: '200' },
9
+        { prop: 'notes', label: '质检得分', width: '150' },
10
+        { prop: 'notes', label: '坐席名称', width: '150' },
11
+        { prop: 'notes', label: '录音', width: '150' },
12
+        { prop: 'notes', label: 'AI 质检结果', width: '300' },
13
+        { prop: 'notes', label: 'AI 人工复检时间', width: '170' },
14
+        { prop: 'notes', label: '复检备注', width: '300' },
15
+        { label: '操作', minWidth: '100', slotName: 'handler', fixed: 'right' }
16
+    ],
17
+    showIndexColumn: false,
18
+    showSelectColumn: false,
19
+}

+ 29 - 0
src/views/quality/config/modal.config.js

@@ -0,0 +1,29 @@
1
+export const modalConfig = {
2
+  formItems: [
3
+    {
4
+      field: 'mobile',
5
+      type: 'input',
6
+      label: '电话',
7
+      required : true ,
8
+      placeholder: '请输入电话',
9
+    },
10
+    {
11
+      field: 'name',
12
+      type: 'input',
13
+      label: '名称',
14
+      required : true ,
15
+      placeholder: '请输入名称',
16
+
17
+    },
18
+    {
19
+      field: 'notes',
20
+      type: 'input',
21
+      label: '备注',
22
+      placeholder: '请输入备注',
23
+
24
+    },
25
+    
26
+  ],
27
+  colLayout: { span: 24 },
28
+  itemStyle: {}
29
+}

+ 31 - 0
src/views/quality/config/search.config.js

@@ -0,0 +1,31 @@
1
+export const searchFormConfig = {
2
+  labelWidth: '120px',
3
+  itemStyle: {
4
+    padding: '0px'
5
+  },
6
+  colLayout: {
7
+    span: 6
8
+  },
9
+  formItems: [
10
+    {
11
+      field: 'mobile',
12
+      type: 'input',
13
+      label: '电话',
14
+      placeholder: '请输入电话',
15
+    },
16
+    {
17
+      field: 'name',
18
+      type: 'input',
19
+      label: '名称',
20
+      placeholder: '请输入名称',
21
+
22
+    },
23
+    {
24
+      field: 'notes',
25
+      type: 'input',
26
+      label: '备注',
27
+      placeholder: '请输入备注',
28
+
29
+    },
30
+  ]
31
+}

+ 158 - 0
src/views/quality/detail.vue

@@ -0,0 +1,158 @@
1
+<!-- 代码已包含 CSS:使用 TailwindCSS , 安装 TailwindCSS 后方可看到布局样式效果 -->
2
+
3
+<template>
4
+    <div class="app-container bg-gray-50">
5
+      <div class="mx-auto">
6
+        <!-- 基础信息 -->
7
+        <div class="bg-white rounded-lg p-6 mb-6 shadow-sm">
8
+          <div class="grid grid-cols-4 gap-6 text-gray-600">
9
+            <div>
10
+              <span class="mr-2">质检ID:</span>
11
+              <span class="text-gray-900">892821</span>
12
+            </div>
13
+            <div>
14
+              <span class="mr-2">外呼号码:</span>
15
+              <span class="text-gray-900">137 8651 5262</span>
16
+            </div>
17
+            <div>
18
+              <span class="mr-2">外呼时间:</span>
19
+              <span class="text-gray-900">2024-12-09 15:30</span>
20
+            </div>
21
+            <div>
22
+              <span class="mr-2">坐席:</span>
23
+              <span class="text-gray-900">李世海</span>
24
+            </div>
25
+          </div>
26
+        </div>
27
+  
28
+        <!-- AI质检结果 -->
29
+        <div class="bg-white rounded-lg p-6 mb-6 shadow-sm">
30
+          <div class="flex justify-between items-center mb-8">
31
+            <h2 class="text-xl font-medium">AI质检</h2>
32
+            <div class="flex items-center">
33
+              <span class="text-green-600 mr-4">合格</span>
34
+              <span class="text-blue-600 text-xl font-medium">90分</span>
35
+            </div>
36
+          </div>
37
+  
38
+          <div class="mb-8">
39
+            <h3 class="text-gray-900 font-medium mb-4">质检概况</h3>
40
+            <div class="grid grid-cols-5 gap-6">
41
+              <div class="text-center p-4 bg-gray-50 rounded-lg">
42
+                <div class="text-gray-600 mb-2">平均语速</div>
43
+                <div class="text-gray-900">150字/分钟</div>
44
+              </div>
45
+              <div class="text-center p-4 bg-gray-50 rounded-lg">
46
+                <div class="text-gray-600 mb-2">平均分贝</div>
47
+                <div class="text-gray-900">60分贝</div>
48
+              </div>
49
+              <div class="text-center p-4 bg-gray-50 rounded-lg">
50
+                <div class="text-gray-600 mb-2">静音时长</div>
51
+                <div class="text-gray-900">20s</div>
52
+              </div>
53
+              <div class="text-center p-4 bg-gray-50 rounded-lg">
54
+                <div class="text-gray-600 mb-2">客户情绪</div>
55
+                <div class="text-gray-900">中立</div>
56
+              </div>
57
+              <div class="text-center p-4 bg-gray-50 rounded-lg">
58
+                <div class="text-gray-600 mb-2">客服情绪</div>
59
+                <div class="text-gray-900">中立</div>
60
+              </div>
61
+            </div>
62
+          </div>
63
+  
64
+          <div class="mb-8">
65
+            <h3 class="text-gray-900 font-medium mb-4">评分标准</h3>
66
+            <div class="flex gap-8">
67
+              <div class="flex items-center">
68
+                <el-icon class="mr-2 text-gray-400"><Warning /></el-icon>
69
+                <span>语速不规范 (-5分)</span>
70
+              </div>
71
+              <div class="flex items-center">
72
+                <el-icon class="mr-2 text-gray-400"><Warning /></el-icon>
73
+                <span>音量不规范 (-5分)</span>
74
+              </div>
75
+              <div class="flex items-center">
76
+                <el-icon class="mr-2 text-gray-400"><Warning /></el-icon>
77
+                <span>反问客户 (-5分)</span>
78
+              </div>
79
+            </div>
80
+          </div>
81
+  
82
+          <!-- 通话录音 -->
83
+          <audio-player ></audio-player>
84
+          <!-- <div>
85
+            <h3 class="text-gray-900 font-medium mb-4">通话录音</h3>
86
+            <div class="bg-gray-50 p-4 rounded-lg">
87
+              <div class="flex items-center gap-4">
88
+                <button class="flex items-center !rounded-button whitespace-nowrap">
89
+                  <el-icon class="mr-2"><Microphone /></el-icon>
90
+                  音量
91
+                </button>
92
+                <div class="flex-1 h-2 bg-gray-200 rounded-full">
93
+                  <div class="w-3/4 h-full bg-blue-500 rounded-full"></div>
94
+                </div>
95
+                <span class="text-gray-600">02:19</span>
96
+                <button class="text-blue-600 !rounded-button whitespace-nowrap">
97
+                  <el-icon><Mute /></el-icon>
98
+                </button>
99
+                <button class="text-blue-600 !rounded-button whitespace-nowrap">
100
+                  <el-icon><VideoPlay /></el-icon>
101
+                </button>
102
+                <button class="text-blue-600 !rounded-button whitespace-nowrap">
103
+                  <el-icon><Refresh /></el-icon>
104
+                </button>
105
+              </div>
106
+            </div>
107
+          </div> -->
108
+        </div>
109
+  
110
+        <!-- 申诉处理 -->
111
+        <div class="bg-white rounded-lg p-6 shadow-sm">
112
+          <h2 class="text-xl font-medium mb-6">申诉处理(一)</h2>
113
+          <div class="border-b pb-6 mb-6">
114
+            <div class="flex justify-between mb-4">
115
+              <h3 class="text-gray-900 font-medium">申诉理由</h3>
116
+              <span class="text-green-600">已审核</span>
117
+            </div>
118
+            <p class="text-gray-600 mb-4">关于未按标准流程进行开场白的扣分,实际上我已经按照最新的服务指南进行了标准开场白。另外,所谓的口语化表达是在与客户建立亲和关系的必要交流方式,建议重新审核。</p>
119
+            <div class="text-gray-500">
120
+              <el-icon class="mr-2"><Timer /></el-icon>
121
+              提交时间:2025-01-20 15:45:30
122
+            </div>
123
+          </div>
124
+  
125
+          <div>
126
+            <div class="flex items-start gap-4 mb-4">
127
+              <el-avatar :size="40" src="https://ai-public.mastergo.com/ai/img_res/2712597425b269c796db47f887c21853.jpg" />
128
+              <div class="flex-1">
129
+                <div class="flex justify-between mb-2">
130
+                  <span class="font-medium">马文青</span>
131
+                  <span class="text-gray-500">质检员</span>
132
+                </div>
133
+                <p class="text-gray-600 mb-2">经过重新审核,同意调整"未按标准流程进行开场白"的扣分,但关于口语化表达的问题仍需改进。最终评分调整为95分。</p>
134
+                <div class="text-gray-500">
135
+                  <el-icon class="mr-2"><Timer /></el-icon>
136
+                  提交时间:2025-01-20 15:45:30
137
+                </div>
138
+              </div>
139
+            </div>
140
+          </div>
141
+        </div>
142
+      </div>
143
+    </div>
144
+  </template>
145
+  
146
+  <script lang="ts" setup name="RuleDetail">
147
+  import { ref } from 'vue';
148
+  import { ArrowLeft, Warning, Microphone, Mute, VideoPlay, Refresh, Timer } from '@element-plus/icons-vue';
149
+  import audioPlayer from '../../components/audio/audioPlayer.vue';
150
+  </script>
151
+  
152
+  <style scoped>
153
+  .el-avatar {
154
+    background-color: #f4f4f4;
155
+  }
156
+  </style>
157
+  
158
+  

+ 100 - 0
src/views/quality/index.vue

@@ -0,0 +1,100 @@
1
+<template>
2
+    <div class="app-container">
3
+        <page-search :searchFormConfig="searchFormConfig" @resetBtnClick="handleResetClick"
4
+            @queryBtnClick="handleQueryClick" />
5
+
6
+        <page-content ref="pageContentRef" :contentTableConfig="contentTableConfig" pageName="/system/addressbook" rowKey="id"
7
+            :isExport="false" @newBtnClick="addClick"
8
+            @editBtnClick="edit"></page-content>
9
+        <page-modal :defaultInfo="defaultInfo" :title="'编辑'" ref="pageModalRef" :mainKey="'id'" pageName="/system/addressbook"
10
+            :modalConfig="newModelConfig"></page-modal>
11
+    </div>
12
+</template>
13
+  
14
+<script>
15
+import { defineComponent, ref, computed, watch, getCurrentInstance } from 'vue'
16
+
17
+import PageSearch from '@/components/page-search'
18
+import PageContent from '@/components/page-content'
19
+import PageModal from '@/components/page-modal'
20
+
21
+import { searchFormConfig } from './config/search.config'
22
+import { contentTableConfig } from './config/content.config'
23
+import { usePageSearch } from '@/hooks/use-page-search'
24
+import { usePageModal } from '@/hooks/use-page-modal'
25
+import { modalConfig } from './config/modal.config'
26
+
27
+export default defineComponent({
28
+    name: 'AddressBook',
29
+    components: {
30
+        PageSearch,
31
+        PageContent,
32
+        PageModal,
33
+    },
34
+    setup () {
35
+
36
+        let newModelConfig = modalConfig;
37
+        
38
+        // 1.处理逻辑
39
+        const newCallback = () => { }
40
+        const editCallback = () => { }
41
+
42
+        const [pageContentRef, handleResetClick, handleQueryClick] = usePageSearch()
43
+        const { proxy } = getCurrentInstance()
44
+
45
+        const recordData = ref({})
46
+        const open = ref(false)
47
+
48
+        const clRowData = ref({})
49
+
50
+        // 3.调用hook获取公共变量和函数
51
+        const [pageModalRef, defaultInfo, handleNewData, handleEditData] =
52
+            usePageModal(newCallback, editCallback)
53
+
54
+        function edit(item) {
55
+            console.log(modalConfig, 'formItems')
56
+            newModelConfig = modalConfig.formItems.map((o) => {
57
+                if (o.field === 'extension') {
58
+                    o.otherOptions = {
59
+                        ...o.otherOptions,
60
+                        disabled: true,
61
+                    }
62
+                }
63
+            });
64
+
65
+
66
+            return handleEditData(item);
67
+        }
68
+
69
+        function addClick(item) {
70
+            newModelConfig = modalConfig.formItems.map((o) => {
71
+                if (o.field === 'extension') {
72
+                     
73
+                    o.otherOptions = {
74
+                        ...o.otherOptions,
75
+                        disabled: false,
76
+                    }
77
+                }
78
+            });
79
+            handleNewData(item);
80
+        }
81
+
82
+        return {
83
+            newModelConfig,
84
+            defaultInfo,
85
+            addClick,
86
+            edit,
87
+            pageModalRef,
88
+            clRowData,
89
+            searchFormConfig,
90
+            contentTableConfig,
91
+            pageContentRef,
92
+            handleResetClick,
93
+            handleQueryClick,
94
+            recordData,
95
+            open,
96
+        }
97
+    }
98
+})
99
+</script>
100
+