right.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <template>
  2. <div
  3. class="seats_box"
  4. v-loading="loading"
  5. element-loading-text="加载中..."
  6. element-loading-svg-view-box="-10, -10, 50, 50"
  7. element-loading-background="rgba(122, 122, 122, 0.1)"
  8. >
  9. <div class="seats_container">
  10. <div
  11. v-for="(item, index) in seatsList"
  12. class="seats_item"
  13. ref="textRefs"
  14. @click="handleClick(index, item)"
  15. >
  16. <img
  17. src="@/assets/img/seats/service.png"
  18. alt=""
  19. class="seats_item_service"
  20. />
  21. <p class="seats_item_tel">{{ item.telNo }}</p>
  22. <p class="seats_item_name" v-if="item.state === 'logout'">未登录</p>
  23. <p class="seats_item_name" v-else>
  24. {{ item.workUserName ? item.workUserName : "未知" }}
  25. </p>
  26. <span @click.stop>
  27. <el-dropdown
  28. @command="handleCommand($event, item)"
  29. class="seats_item_dropdown"
  30. trigger="click"
  31. v-if="item.state === 'busy' && globalState.callCenterIsSignIn"
  32. >
  33. <el-icon class="seats_item_more" size="18">
  34. <Operation />
  35. </el-icon>
  36. <template #dropdown>
  37. <el-dropdown-menu>
  38. <el-dropdown-item command="listen">监 听</el-dropdown-item>
  39. <el-dropdown-item command="interject">插 话</el-dropdown-item>
  40. </el-dropdown-menu>
  41. </template>
  42. </el-dropdown>
  43. </span>
  44. <div class="seats_item_state">
  45. <img :src="getImageUrl('seats/' + item.state + '.png')" alt="" />
  46. <span class="seats_item_telNo">
  47. {{ currentStatusText(item.state) }}</span
  48. >
  49. </div>
  50. </div>
  51. </div>
  52. <el-popover
  53. ref="popoverRef"
  54. :virtual-ref="textRef"
  55. :visible="hidePopover"
  56. virtual-triggering
  57. placement="right-start"
  58. @hide="popoverHide"
  59. width="220"
  60. popper-class="call_popover_popper"
  61. >
  62. <template #default>
  63. <div v-click-outside="onClickOutside" class="call_popover">
  64. <div class="call_popover_item">
  65. <span class="label">坐席名称:</span>{{ call.workUserName }}
  66. </div>
  67. <div class="call_popover_item">
  68. <span class="label">分机号:</span>{{ call.telNo }}
  69. </div>
  70. <div class="call_popover_item">
  71. <span class="label">通话时长:</span
  72. ><span v-if="talkTime">{{ formatDuration(talkTime) }}</span>
  73. </div>
  74. <div class="call_popover_item">
  75. <span class="label">呼入类型:</span>
  76. <el-text v-if="call.callDirection === 'inbound'">呼入</el-text>
  77. <el-text v-if="call.callDirection === 'outbound'">外呼</el-text>
  78. <i></i>
  79. </div>
  80. <div class="call_popover_item">
  81. <span class="label">电话号码:</span>{{ call.otherNumber }}
  82. </div>
  83. <div class="call_popover_item">
  84. <span class="label">今日接听量:</span>{{ call.onStateCount }}
  85. </div>
  86. </div>
  87. </template>
  88. </el-popover>
  89. </div>
  90. </template>
  91. <script setup lang="ts">
  92. import { onMounted, ref, nextTick, onBeforeUnmount, watch } from "vue";
  93. import { getImageUrl } from "@/utils/tools";
  94. import signalR from "@/utils/signalR";
  95. import { ClickOutside as vClickOutside } from "element-plus";
  96. import { formatDuration } from "@/utils/formatTime";
  97. import dayjs from "dayjs";
  98. import { getExtensionStatus } from "api/seats";
  99. import { useIntervalFn } from "@vueuse/core";
  100. import { useGlobalState } from "@/utils/callCenter";
  101. import { Operation } from "@element-plus/icons-vue";
  102. import mittBus from "@/utils/mitt";
  103. const props = defineProps({
  104. data: {
  105. type: Array,
  106. default: () => [],
  107. },
  108. });
  109. const textRefs = ref<EmptyArrayType>([]);
  110. const textRef = ref();
  111. const popoverRef = ref();
  112. const hidePopover = ref(false);
  113. const call = ref<EmptyObjectType>({});
  114. const globalState = useGlobalState();
  115. // 开始签入时长
  116. const talkTime = ref<any>(0); // 通话时长
  117. const talkTimer = ref<any>(null); // 通话时长定时器
  118. const startCallTimer = (second: any) => {
  119. if (second) {
  120. // 从后台获取签入时长
  121. if (second < 0) second = 0; // 防止后台返回的签入时间大于当前时间
  122. talkTime.value = second;
  123. talkTimer.value = useIntervalFn(() => {
  124. talkTime.value++;
  125. }, 1000);
  126. } else {
  127. talkTimer.value = useIntervalFn(() => {
  128. talkTime.value++;
  129. }, 1000);
  130. }
  131. };
  132. const handleClick = (index: number, item: { state: string; telNo: string }) => {
  133. textRef.value = textRefs.value[index];
  134. call.value = item;
  135. talkTime.value = 0;
  136. if (talkTimer.value) talkTimer.value.pause();
  137. if (["busy", "held"].includes(item.state)) {
  138. getExtensionStatus({ telno: item.telNo }).then((res: any) => {
  139. hidePopover.value = true;
  140. call.value = res.result;
  141. // 获取现在时间与签入时间的秒数
  142. talkTime.value = dayjs().diff(dayjs(res.result.answeredAt), "second");
  143. startCallTimer(talkTime.value);
  144. });
  145. } else {
  146. talkTime.value = 0;
  147. call.value.otherNumber = "";
  148. }
  149. };
  150. // 隐藏弹窗
  151. const popoverHide = () => {
  152. if (talkTimer.value) talkTimer.value.pause();
  153. };
  154. const onClickOutside = () => {
  155. hidePopover.value = false;
  156. };
  157. // 设置当前状态的值
  158. const currentStatusText = (state: string) => {
  159. const statusMap: any = {
  160. logout: "签出",
  161. login: "签入",
  162. ready: "示闲",
  163. unready: "小休",
  164. ring: "振铃中",
  165. busy: "通话中",
  166. acw: "整理",
  167. held: "保持",
  168. treeWay: "三方会议",
  169. };
  170. return statusMap[state] || "";
  171. };
  172. const seatsList = ref<any>([]);
  173. watch(
  174. () => props.data,
  175. (newData: any) => {
  176. seatsList.value = newData;
  177. },
  178. { immediate: true }
  179. );
  180. const loading = ref(false);
  181. // 对当前状态进行排序
  182. const sortSeatsList = () => {
  183. // 通话中的排序
  184. const busyData = seatsList.value.filter((item: any) => item.state === "busy");
  185. // 通话中的排序
  186. const ringData = seatsList.value.filter((item: any) => item.state === "ring");
  187. // 话后整理
  188. const acwData = seatsList.value.filter((item: any) => item.state === "acw");
  189. // 保持
  190. const heldData = seatsList.value.filter((item: any) => item.state === "held");
  191. // 三方会议
  192. const treeWayData = seatsList.value.filter(
  193. (item: any) => item.state === "treeWay"
  194. );
  195. // 小休
  196. const unreadyData = seatsList.value.filter(
  197. (item: any) => item.state === "unready"
  198. );
  199. // 示闲
  200. const readyData = seatsList.value.filter(
  201. (item: any) => item.state === "ready"
  202. );
  203. // 签出
  204. const logoutData = seatsList.value.filter(
  205. (item: any) => item.state === "logout"
  206. );
  207. seatsList.value = [
  208. ...busyData,
  209. ...ringData,
  210. ...readyData,
  211. ...acwData,
  212. ...heldData,
  213. ...treeWayData,
  214. ...unreadyData,
  215. ...logoutData,
  216. ];
  217. };
  218. onMounted(async () => {
  219. await nextTick();
  220. // await getSeatsList();
  221. // 接收消息
  222. signalR.SR.on("SeatState", (res: any) => {
  223. const item = seatsList.value.find((item: any) => item.telNo === res.telNo);
  224. item.loading = true;
  225. if (item) {
  226. setTimeout(() => {
  227. item.state = res.state;
  228. item.workUserName = res.workUserName;
  229. item.workUserId = res.workUserId;
  230. item.loading = false;
  231. sortSeatsList();
  232. // hidePopover.value = false;
  233. }, 500);
  234. }
  235. });
  236. mittBus.on("monitorInfo", (data: any) => {
  237. console.log("1111", data);
  238. });
  239. });
  240. // 监听和插话消息
  241. const handleCommand = (command: string, item: any) => {
  242. console.log(command, item);
  243. if (command === "listen") {
  244. // 监听
  245. globalState.callCenterWs.monitor(
  246. item.telNo,
  247. globalState.callCenterWs.username
  248. );
  249. } else if (command === "interject") {
  250. // 强插
  251. globalState.callCenterWs.intercept(
  252. item.telNo,
  253. globalState.callCenterWs.username
  254. );
  255. }
  256. };
  257. onBeforeUnmount(() => {
  258. signalR.SR.off("SeatState");
  259. });
  260. </script>
  261. <style scoped lang="scss">
  262. .seats_box {
  263. width: 100%;
  264. overflow: auto;
  265. height: 100%;
  266. .seats_container {
  267. display: grid;
  268. grid-template-columns: repeat(auto-fill, 100px);
  269. grid-gap: 20px;
  270. .seats_item {
  271. width: 100px;
  272. border: 1px solid var(--el-color-primary-light-3);
  273. border-radius: 8px;
  274. text-align: center;
  275. padding: 7px 0;
  276. animation: bounce-out 0.3s ease;
  277. position: relative;
  278. cursor: pointer;
  279. &_service {
  280. margin: 0 auto;
  281. width: 40px;
  282. }
  283. &_tel {
  284. font-size: var(--el-font-size-medium);
  285. color: var(--el-color-white);
  286. margin-top: 8px;
  287. }
  288. &_dropdown {
  289. position: absolute;
  290. top: 5px;
  291. right: 10px;
  292. outline: none;
  293. }
  294. &_more {
  295. cursor: pointer;
  296. }
  297. &_name {
  298. font-size: var(--el-font-size-medium);
  299. color: var(--el-color-white);
  300. margin-top: 5px;
  301. overflow: hidden;
  302. text-overflow: ellipsis;
  303. white-space: nowrap;
  304. padding: 0 5px;
  305. }
  306. &_state {
  307. display: flex;
  308. align-items: center;
  309. justify-content: center;
  310. margin-top: 8px;
  311. img {
  312. width: 25px;
  313. height: 25px;
  314. }
  315. .seats_item_telNo {
  316. font-size: var(--el-font-size-medium);
  317. color: var(--el-color-white);
  318. margin-left: 5px;
  319. }
  320. }
  321. }
  322. }
  323. }
  324. .call_popover_popper {
  325. .call_popover {
  326. .call_popover_item {
  327. margin-bottom: 5px;
  328. .label {
  329. display: inline-block;
  330. width: 90px;
  331. text-align: right;
  332. }
  333. }
  334. }
  335. }
  336. </style>