index.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <script setup>
  2. import { workbenchApi } from '@/api/voice/workbench';
  3. import RecordCardItem from './components/RecordCardItem';
  4. import CallView from '@/components/CallView';
  5. const queryParams = ref({
  6. pageNum: 1,
  7. pageSize: 10,
  8. category: 0,
  9. phone: ''
  10. });
  11. const { proxy } = getCurrentInstance();
  12. const tabCallRecordList = ref([]);
  13. const tabCurrentActive = ref(null);
  14. const callDetails = ref({});
  15. const isTransitionVoiceStatus = ref(false);
  16. const total = ref(0);
  17. const loading = ref(false);
  18. const tabEnum = ['通话呼入', '通话呼出'];
  19. const disabled = computed(() => loading.value || total.value == tabCallRecordList.value.length);
  20. const getTimeOfDayGreeting = () => {
  21. const date = new Date();
  22. const hour = date.getHours();
  23. if (hour >= 0 && hour < 12) {
  24. return "上午好";
  25. } else if (hour >= 12 && hour < 18) {
  26. return "下午好";
  27. } else {
  28. return "晚上好";
  29. }
  30. }
  31. // 切换tabs
  32. const handleChangeTab = (index) => {
  33. if ( queryParams.value.category != index ) {
  34. queryParams.value.pageNum = 1;
  35. queryParams.value.category = index;
  36. queryParams.value.phone = '';
  37. tabCurrentActive.value = null;
  38. tabCallRecordList.value = [];
  39. initTabsData();
  40. getTimeOfDayGreeting();
  41. }
  42. }
  43. // 选中通话记录
  44. const hanldeTabItem = async (id) => {
  45. if ( !isTransitionVoiceStatus.value ) {
  46. const { data } = await workbenchApi.getCallRecordDetails(id);
  47. callDetails.value = data;
  48. tabCurrentActive.value = id;
  49. } else {
  50. proxy.$modal.msgWarning("当前语音正在转换中,请稍后");
  51. }
  52. }
  53. // 搜索
  54. const onSearch = ( type ) => {
  55. if ( type === 'refresh' ) {
  56. queryParams.value.phone = '';
  57. }
  58. queryParams.value.pageNum = 1;
  59. tabCallRecordList.value = [];
  60. tabCurrentActive.value = null;
  61. initTabsData();
  62. }
  63. // 语音转化完成
  64. const handleVoiceParsed = ({ parsedVoiceContent }) => {
  65. callDetails.value.parsedVoiceContent = parsedVoiceContent;
  66. }
  67. const initTabsData = async () => {
  68. loading.value = true;
  69. const { rows, total: t } = await workbenchApi.getCallRecordList(queryParams.value);
  70. tabCallRecordList.value = [...tabCallRecordList.value, ...rows];
  71. total.value = t;
  72. loading.value = false;
  73. }
  74. onMounted(async () => {
  75. initTabsData();
  76. });
  77. const loadMoreData = () => {
  78. queryParams.value.pageNum += 1;
  79. initTabsData();
  80. }
  81. </script>
  82. <template>
  83. <div class="workbench-viewport space-x-[16px]">
  84. <div class="record-section">
  85. <ul class="tabs-nav space-x-[48px]">
  86. <li v-for="item, index in tabEnum" :class="['tabs-nav-item', { active: queryParams.category === index }]"
  87. :key="item" @click="handleChangeTab(index)">{{ item }}</li>
  88. </ul>
  89. <div class="tabs-content">
  90. <div class="search-inp-wrapper">
  91. <div class="search-inp">
  92. <input type="text" class="inp" placeholder="请输入电话号码" v-model.trim="queryParams.phone">
  93. <div class="btn" @click="onSearch">搜索</div>
  94. </div>
  95. <el-tooltip
  96. effect="dark"
  97. content="刷新"
  98. placement="top"
  99. >
  100. <el-icon style="cursor: pointer;" @click="onSearch('refresh')"><Refresh /></el-icon>
  101. </el-tooltip>
  102. </div>
  103. <div class="search-result-wrapper">
  104. <el-scrollbar height="100%">
  105. <div
  106. class="search-result-inner space-y-[8px]"
  107. v-infinite-scroll="loadMoreData"
  108. :infinite-scroll-disabled="disabled"
  109. v-show="tabCallRecordList.length"
  110. >
  111. <RecordCardItem
  112. v-for="item, index in tabCallRecordList"
  113. :data="item"
  114. :index="index"
  115. :active="tabCurrentActive === item.id"
  116. :key="item.id"
  117. @on-click="hanldeTabItem(item.id)"
  118. ></RecordCardItem>
  119. <div class="flex justify-center text-[#999] text-[12px]">
  120. <p class="pb-[6px]" v-if="loading">Loading...</p>
  121. </div>
  122. </div>
  123. <div class="flex items-center justify-center pt-[100px]" v-show="!tabCallRecordList.length">
  124. <span class="text-[#999] text-[14px]">暂无数据</span>
  125. </div>
  126. </el-scrollbar>
  127. </div>
  128. </div>
  129. </div>
  130. <div class="details-section">
  131. <div class="empty-wrapper" v-show="!callDetails.id || tabCurrentActive === null">
  132. <img src="@/assets/images/workbench/img-empty.png" alt="">
  133. <p class="empty-text">
  134. <span>Hi, {{ getTimeOfDayGreeting() }}~</span>
  135. <span>欢迎登录智能语音客服</span>
  136. </p>
  137. </div>
  138. <div class="details-wrapper" v-show="callDetails.id && tabCurrentActive !== null">
  139. <h4 class="title">通话详情</h4>
  140. <el-scrollbar class="details-scrollbar">
  141. <CallView :data="callDetails" noInit @on-end="handleVoiceParsed" v-model="isTransitionVoiceStatus"></CallView>
  142. </el-scrollbar>
  143. </div>
  144. </div>
  145. </div>
  146. </template>
  147. <style lang="scss" scoped>
  148. $primaryColor: #165DFF;
  149. .workbench-viewport {
  150. display: flex;
  151. height: 100%;
  152. background: #eceff6;
  153. .record-section {
  154. flex-shrink: 0;
  155. width: 292px;
  156. height: 100%;
  157. border-radius: 8px;
  158. background: linear-gradient(180deg, #FFF 0%, #FFF 100%);
  159. .tabs-nav {
  160. display: flex;
  161. align-items: center;
  162. justify-content: center;
  163. height: 46px;
  164. padding-top: 15px;
  165. border-bottom: 1px solid #E5E6EB;
  166. font-size: 14px;
  167. line-height: 20px;
  168. color: #4E5969;
  169. .tabs-nav-item {
  170. position: relative;
  171. cursor: pointer;
  172. &.active {
  173. color: $primaryColor;
  174. font-weight: bold;
  175. &::after {
  176. position: absolute;
  177. left: 0;
  178. bottom: -6px;
  179. content: ' ';
  180. display: block;
  181. width: 100%;
  182. height: 2px;
  183. background: $primaryColor;
  184. }
  185. }
  186. }
  187. }
  188. .tabs-content {
  189. height: calc(100% - 46px);
  190. .search-inp-wrapper {
  191. display: flex;
  192. align-items: center;
  193. justify-content: space-between;
  194. padding: 12px 22px;
  195. .search-inp {
  196. display: flex;
  197. align-items: center;
  198. height: 34px;
  199. padding: 2px;
  200. border-radius: 8px;
  201. background: #F2F4F7;
  202. .inp {
  203. width: 100%;
  204. padding: 0 10px;
  205. background: transparent;
  206. outline: none;
  207. font-size: 13px;
  208. color: #1D2129;
  209. }
  210. .btn {
  211. flex-shrink: 0;
  212. width: 52px;
  213. height: 30px;
  214. border-radius: 8px;
  215. background: #165DFF;
  216. color: #FFF;
  217. font-size: 13px;
  218. line-height: 30px;
  219. text-align: center;
  220. cursor: pointer;
  221. }
  222. }
  223. }
  224. .search-result-wrapper {
  225. height: calc(100% - 58px);
  226. .search-result-inner {
  227. padding: 0 22px;
  228. }
  229. }
  230. }
  231. }
  232. .details-section {
  233. width: 100%;
  234. min-width: 700px;
  235. height: 100%;
  236. border-radius: 8px;
  237. overflow: hidden;
  238. padding: 20px;
  239. background: #fff;
  240. .details-scrollbar {
  241. height: calc(100% - 50px);
  242. }
  243. .empty-wrapper {
  244. display: flex;
  245. align-items: center;
  246. justify-content: center;
  247. flex-flow: column;
  248. width: 100%;
  249. height: 100%;
  250. .empty-text {
  251. span {
  252. display: block;
  253. text-align: center;
  254. font-weight: bold;
  255. font-size: 24px;
  256. line-height: 32px;
  257. &:nth-child(1) {
  258. color: #165DFF;
  259. }
  260. &:nth-child(2) {
  261. color: #1D2129;
  262. }
  263. }
  264. }
  265. }
  266. .details-wrapper {
  267. height: 100%;
  268. .title {
  269. margin-bottom: 24px;
  270. color: #1D2129;
  271. font-size: 18px;
  272. font-weight: bold;
  273. line-height: 26px;
  274. }
  275. }
  276. }
  277. }
  278. .dialog-footer {
  279. display: flex;
  280. justify-content: center;
  281. }
  282. :deep(.el-textarea__inner) {
  283. background: #f2f4f7;
  284. }
  285. </style>