index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. <script setup>
  2. import dayjs from 'dayjs';
  3. import autofit from 'autofit.js'
  4. import * as echarts from 'echarts';
  5. import useUserStore from '@/store/modules/user';
  6. import { dashboardApi } from '@/api/voice/dashboard';
  7. import { getEchartLineOption, getEchart3dOption, getEchartBarOption } from './echartConfig';
  8. import NumberAnimation from '@/components/NumberAnimation';
  9. import usePermissionStore from '@/store/modules/permission';
  10. const userStore = useUserStore();
  11. const permissionStore = usePermissionStore();
  12. const screenData = ref({});
  13. const dateInfo = ref({
  14. weekDay: '',
  15. today: '',
  16. time: ''
  17. })
  18. let timer = null;
  19. let dataTimer = null;
  20. let myLineChart = null;
  21. let my3dChart = null;
  22. let myBarChart = null;
  23. const echartLineRef = ref(null);
  24. const echart3dRef = ref(null);
  25. const echartBarRef = ref(null);
  26. const updateTime = () => {
  27. const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
  28. const todayWeekDay = dayjs().day();
  29. dateInfo.value = {
  30. weekDay: '星期' + weekDays[todayWeekDay],
  31. today: dayjs().format('YYYY-MM-DD'),
  32. time: dayjs().format('HH:mm:ss')
  33. }
  34. }
  35. const formatSeconds = (seconds, num) => {
  36. // 将秒转换为分钟
  37. let minutes = seconds / num;
  38. // 使用 toLocaleString 方法格式化数字,保留两位小数
  39. // 并添加千位分隔符
  40. let formattedMinutes = minutes.toLocaleString('en-US', {
  41. minimumFractionDigits: 2,
  42. maximumFractionDigits: 2
  43. });
  44. return formattedMinutes;
  45. }
  46. // 将秒转换为小时、分钟和秒 00:12 d的函数
  47. const formatSecondsToTime = (seconds) => {
  48. // 计算完整的分钟数和剩余的秒数
  49. let minutes = Math.floor(seconds / 60);
  50. let remainingSeconds = seconds % 60;
  51. // 如果有超过60分钟的部分,计算小时
  52. let hours = Math.floor(minutes / 60);
  53. minutes = minutes % 60;
  54. // 确保分钟和秒数都是两位数,不足两位前面补零
  55. let formattedMinutes = String(minutes).padStart(2, '0');
  56. let formattedSeconds = String(remainingSeconds).padStart(2, '0');
  57. // 如果有小时,则包含在输出中
  58. if (hours > 0) {
  59. let formattedHours = String(hours).padStart(2, '0');
  60. return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
  61. } else {
  62. return `${formattedMinutes}:${formattedSeconds}`;
  63. }
  64. }
  65. const initEchart = async() => {
  66. const { data } = await dashboardApi.getHomeScreenCount()
  67. const {
  68. recent7DayAndCounts, recent7DayAndCountsLastYear,
  69. recent7MonthAndCounts, recent7MonthAndCountsLastYear,
  70. businessTop7,
  71. ai7DayAndCounts
  72. } = data;
  73. screenData.value = {
  74. ...data,
  75. todayInTimesAvg: formatSecondsToTime(data.todayInTimesAvg),
  76. // inTimes: formatSeconds(data.inTimes, 60),
  77. // inTimesAvg: formatSecondsToTime(data.inTimesAvg),
  78. // totalTimes: formatSeconds(data.totalTimes, 3600),
  79. // robotInTimes: formatSeconds(data.robotInTimes, 60),
  80. // robotTimesAvg: formatSecondsToTime(data.robotTimesAvg),
  81. // humanTimesAvg: formatSecondsToTime(data.humanTimesAvg),
  82. // humanInTimes: formatSeconds(data.humanInTimes, 60)
  83. };
  84. // 近7日电话呼入量 - 折线图
  85. const lineSeriesData = [
  86. { name: '近七日', data:recent7DayAndCounts },
  87. // { name: '同比', data:recent7DayAndCountsLastYear }
  88. ];
  89. const lineXAxisData = recent7DayAndCounts.map(item => dayjs(item.date).format('MM.DD'))
  90. // 近七日Ai处理情况
  91. const barXAxisData = ai7DayAndCounts.map(item => dayjs(item.date).format('MM.DD'))
  92. const seriesData = ai7DayAndCounts.map(item => item.aiCounts)
  93. const seriesData1 = ai7DayAndCounts.map(item => item.totalCounts)
  94. myLineChart.setOption(getEchartLineOption({
  95. xAxisData: lineXAxisData,
  96. seriesData: lineSeriesData
  97. }));
  98. // 本月业务类型 TOP7
  99. my3dChart.setOption(getEchart3dOption(businessTop7));
  100. myBarChart.setOption(getEchartBarOption({
  101. barXAxisData,
  102. seriesData,
  103. seriesData1
  104. }));
  105. }
  106. const windowResize = () => {
  107. setTimeout(() => {
  108. myLineChart.resize();
  109. my3dChart.resize();
  110. myBarChart.resize();
  111. }, 300)
  112. }
  113. const hasRouterAuth = () => {
  114. return permissionStore.routes.some(route => {
  115. return route.path === '/voice/dashboard'
  116. })
  117. }
  118. onMounted(() => {
  119. autofit.init({
  120. dw: 1920,
  121. dh: 1080,
  122. el:"#screen-view",
  123. resize: true,
  124. })
  125. myLineChart = echarts.init(echartLineRef.value);
  126. my3dChart = echarts.init(echart3dRef.value);
  127. myBarChart = echarts.init(echartBarRef.value);
  128. timer = setInterval(updateTime, 1000);
  129. dataTimer = setInterval(initEchart, 5 * 60 * 1000);
  130. updateTime();
  131. initEchart();
  132. window.addEventListener('resize', windowResize, false);
  133. });
  134. onUnmounted(() => {
  135. clearInterval(timer);
  136. clearInterval(dataTimer);
  137. })
  138. </script>
  139. <template>
  140. <div class="dashboard-viewport">
  141. <div id="screen-view">
  142. <div class="header">
  143. <div class="date space-x-[24px]">
  144. <div class="time">{{ dateInfo.time }}</div>
  145. <ul class="week">
  146. <li>{{dateInfo.weekDay}}</li>
  147. <li class="num">{{ dateInfo.today }}</li>
  148. </ul>
  149. </div>
  150. <div class="line"></div>
  151. <div class="user space-x-[10px]">
  152. <img :src="userStore.avatar" alt="" class="avatar">
  153. <span>{{ userStore.nickName }}</span>
  154. </div>
  155. </div>
  156. <div class="main">
  157. <div class="top">
  158. <div class="count-list">
  159. <div class="count-item">
  160. <p class="num"><NumberAnimation :to="screenData.todayInTotal"></NumberAnimation></p>
  161. <p>今日呼入总量</p>
  162. </div>
  163. <div class="count-item">
  164. <p class="num"><NumberAnimation :to="screenData.todayAiTotal"></NumberAnimation></p>
  165. <p>今日AI处理量<span class="text-[#00FCFF]">「占比{{screenData.todayAiRate || 0}}%」</span></p>
  166. </div>
  167. <div class="count-item">
  168. <p class="num">{{ screenData.todayInTimesAvg || "00:00" }}</p>
  169. <p>今日平均呼入时长</p>
  170. </div>
  171. <div class="count-item">
  172. <p class="num"><NumberAnimation :to="screenData.totalCounts"></NumberAnimation></p>
  173. <p style="display: flex; align-items: center;">
  174. <span style="margin-right: 5px;">累计呼入总量</span>
  175. <el-tooltip
  176. effect="dark"
  177. content="开始时间: 2025年01月03日"
  178. placement="top"
  179. >
  180. <el-icon><Warning /></el-icon>
  181. </el-tooltip>
  182. </p>
  183. </div>
  184. <!-- <div class="count-item"><p class="num">{{ screenData.totalTimes }}<span class="text-[14px] text-[#fff] opacity-70">小时</span></p><p>累计呼入时长</p></div>
  185. <div class="count-item"><p class="num">{{ screenData.totalCounts }}</p><p>累计呼入总量</p></div> -->
  186. </div>
  187. </div>
  188. <div class="middle">
  189. <div class="echart-list">
  190. <div class="echart-item">
  191. <div class="content" ref="echartLineRef"></div>
  192. </div>
  193. <div class="echart-item">
  194. <div class="content echart-content" ref="echart3dRef"></div>
  195. </div>
  196. <div class="echart-item">
  197. <div class="content" ref="echartBarRef"></div>
  198. </div>
  199. </div>
  200. </div>
  201. <div class="bottom">
  202. <div class="left">
  203. <div class="title-status">当前在线人工坐席:3人 机器人坐席:10个</div>
  204. <ul class="statistic-list">
  205. <li class="statistic-item">
  206. <p class="title">AI处理量</p>
  207. <div class="numeric-stats-box">
  208. <p class="num"><NumberAnimation :to="screenData.todayAiTotal"></NumberAnimation></p>
  209. <span class="ratio green">占比 {{screenData.todayAiRate || 0}}%</span>
  210. </div>
  211. </li>
  212. <li class="statistic-item">
  213. <p class="title">人工处理量</p>
  214. <div class="numeric-stats-box">
  215. <p class="num"><NumberAnimation :to="screenData.todayHumanTotal"></NumberAnimation></p>
  216. <span class="ratio green">占比 {{screenData.todayHumanRate || 0}}%</span>
  217. </div>
  218. </li>
  219. <li class="statistic-item">
  220. <p class="title">电话接通量</p>
  221. <div class="numeric-stats-box">
  222. <p class="num"><NumberAnimation :to="screenData.todaySuccessCounts"></NumberAnimation></p>
  223. <span class="ratio green">占比 {{screenData.todaySuccessRate || 0}}%</span>
  224. </div>
  225. </li>
  226. <li class="statistic-item">
  227. <p class="title">今日呼入总量</p>
  228. <div class="numeric-stats-box">
  229. <p class="num"><NumberAnimation :to="screenData.todayInTotal"></NumberAnimation></p>
  230. </div>
  231. </li>
  232. <!-- <li class="statistic-item"><p class="title">通话时长</p><p class="num space-x-[4px]"><span>{{ screenData.robotInTimes }}</span><span class="unit">分钟</span></p></li>
  233. <li class="statistic-item"><p class="title">平均通话时长</p><p class="num">{{ screenData.robotTimesAvg }}</p></li>
  234. <li class="statistic-item"><p class="title">转人工总量</p><p class="num">{{ screenData.transfer2Human }}</p></li>
  235. <li class="statistic-item"><p class="title">当前排队总数</p><p class="num">300</p></li>
  236. <li class="statistic-item"><p class="title">分流比例</p><p class="num">{{ screenData.robotStreamRate }}%</p></li> -->
  237. </ul>
  238. </div>
  239. <div class="right">
  240. <ul class="statistic-list">
  241. <li class="statistic-item">
  242. <p class="title">AI处理量</p>
  243. <div class="numeric-stats-box">
  244. <p class="num"><NumberAnimation :to="screenData.yesterdayAiTotal"></NumberAnimation></p>
  245. <span class="ratio red">占比 {{screenData.yesterdayAiRate || 0}}%</span>
  246. </div>
  247. </li>
  248. <li class="statistic-item">
  249. <p class="title">人工处理量</p>
  250. <div class="numeric-stats-box">
  251. <p class="num"><NumberAnimation :to="screenData.yesterdayHumanTotal"></NumberAnimation></p>
  252. <span class="ratio red">占比 {{screenData.yesterdayHumanRate || 0}}%</span>
  253. </div>
  254. </li>
  255. <li class="statistic-item">
  256. <p class="title">电话接通量</p>
  257. <div class="numeric-stats-box">
  258. <p class="num"><NumberAnimation :to="screenData.yesterdaySuccessCounts"></NumberAnimation></p>
  259. <span class="ratio red">占比 {{screenData.yesterdaySuccessRate || 0}}%</span>
  260. </div>
  261. </li>
  262. <li class="statistic-item">
  263. <p class="title">昨日呼入总量</p>
  264. <div class="numeric-stats-box">
  265. <p class="num"><NumberAnimation :to="screenData.yesterdayInTotal"></NumberAnimation></p>
  266. </div>
  267. </li>
  268. <!-- <li class="statistic-item"><p class="title">今日呼入总量</p><p class="num">{{ screenData.humanInTotal }}</p></li>
  269. <li class="statistic-item"><p class="title">通话时长</p><p class="num space-x-[4px]"><span>{{ screenData.humanInTimes }}</span><span class="unit">分钟</span></p></li>
  270. <li class="statistic-item"><p class="title">平均通话时长</p><p class="num">{{ screenData.humanTimesAvg }}</p></li>
  271. <li class="statistic-item"><p class="title">置闲人数</p><p class="num">{{ screenData.onlineTotal }}</p></li>
  272. <li class="statistic-item"><p class="title">当前排队总数</p><p class="num"></p></li>
  273. <li class="statistic-item"><p class="title">分流比例</p><p class="num">{{ screenData.humanStreamRate }}%</p></li> -->
  274. </ul>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. </div>
  280. </template>
  281. <style lang="scss" scoped>
  282. .dashboard-viewport {
  283. width: 100vw;
  284. height: 100vh;
  285. overflow: hidden;
  286. background: url('@/assets/images/dashboard/bg-main.png') no-repeat;
  287. background-size: 100% 100%;
  288. #screen-view {
  289. height: 100%;
  290. display: flex;
  291. flex-flow: column;
  292. padding-bottom: 30px;
  293. }
  294. }
  295. .header {
  296. flex-shrink: 0;
  297. display: flex;
  298. align-items: center;
  299. justify-content: flex-end;
  300. height: 110px;
  301. padding: 26px 26px 46px 26px;
  302. background: url('@/assets/images/dashboard/bg-title.png') no-repeat;
  303. background-size: cover;
  304. color: #fff;
  305. .date {
  306. display: flex;
  307. align-items: center;
  308. font-weight: 500;
  309. color: #BFDFFF;
  310. .time {
  311. width: 120px;
  312. text-shadow: 0px 1px 3px rgba(5, 12, 25, 0.54);
  313. font-family: D-DIN-PRO;
  314. font-size: 36px;
  315. font-weight: bold;
  316. }
  317. .week {
  318. text-align: center;
  319. font-size: 16px;
  320. .num {
  321. font-size: 12px;
  322. font-family: D-DIN-PRO;
  323. }
  324. }
  325. }
  326. .line {
  327. width: 2px;
  328. height: 30px;
  329. margin: 0 38px;
  330. flex-shrink: 0;
  331. background: rgba(205, 233, 251, 0.32);
  332. }
  333. .user {
  334. display: flex;
  335. align-items: center;
  336. color: #FFF;
  337. font-size: 16px;
  338. .avatar {
  339. width: 32px;
  340. height: 32px;
  341. border-radius: 50%;
  342. background: #fff;
  343. }
  344. }
  345. }
  346. .main {
  347. flex: 1;
  348. display: flex;
  349. flex-flow: column;
  350. padding: 20px 50px 18px 50px;
  351. .top {
  352. height: 120px;
  353. flex-shrink: 0;
  354. .count-list {
  355. display: grid;
  356. gap: 12px;
  357. grid-template-columns: repeat(4, minmax(260px, 1fr));
  358. height: 100%;
  359. .count-item:nth-child(1) {
  360. background: url("@/assets/images/dashboard/bg-count-01.png") no-repeat;
  361. background-size: 100% 100%;
  362. }
  363. .count-item:nth-child(2) {
  364. background: url("@/assets/images/dashboard/bg-count-02.png") no-repeat;
  365. background-size: 100% 100%;
  366. }
  367. .count-item:nth-child(3) {
  368. background: url("@/assets/images/dashboard/bg-count-03.png") no-repeat;
  369. background-size: 100% 100%;
  370. }
  371. .count-item:nth-child(4) {
  372. background: url("@/assets/images/dashboard/bg-count-04.png") no-repeat;
  373. background-size: 100% 100%;
  374. }
  375. .count-item:nth-child(5) {
  376. background: url("@/assets/images/dashboard/bg-count-05.png") no-repeat;
  377. background-size: contain;
  378. }
  379. .count-item:nth-child(6) {
  380. background: url("@/assets/images/dashboard/bg-count-06.png") no-repeat;
  381. background-size: contain;
  382. }
  383. .count-item {
  384. padding: 10px 0 0 22px;
  385. color: #FFF;
  386. .num {
  387. font-family: D-DIN-PRO;
  388. font-weight: bold;
  389. font-size: 34px;
  390. }
  391. .text {
  392. color: #E6FFF5;
  393. font-size: 16px;
  394. font-weight: 400px;
  395. }
  396. }
  397. }
  398. }
  399. .middle {
  400. height: 350px;
  401. margin-top: 36px;
  402. .echart-list {
  403. display: flex;
  404. align-items: center;
  405. height: 100%;
  406. .echart-item:nth-child(1) {
  407. margin-right: 12px;
  408. background: url('@/assets/images/dashboard/bg-middle-title-01.png') center top no-repeat, url('@/assets/images/dashboard/bg-middle.png') center center no-repeat;
  409. background-size: contain, 100% 100%;
  410. }
  411. .echart-item:nth-child(2) {
  412. margin-right: 12px;
  413. background: url('@/assets/images/dashboard/bg-middle-title-02.png') center top no-repeat, url('@/assets/images/dashboard/bg-middle.png') center center no-repeat;
  414. background-size: contain, 100% 100%;
  415. }
  416. .echart-item:nth-child(3) {
  417. background: url('@/assets/images/dashboard/bg-middle-title-03.png') center top no-repeat, url('@/assets/images/dashboard/bg-middle.png') center center no-repeat;
  418. background-size: contain, 100% 100%;
  419. }
  420. .echart-item {
  421. width: 33%;
  422. height: 100%;
  423. padding-top: 44px;
  424. }
  425. .content {
  426. width: 100%;
  427. height: 100%;
  428. }
  429. .echart-content {
  430. background: url('https://static.fuxicarbon.com/bigModel/pc/bg-3d-echart.png') left center no-repeat;
  431. background-size: 60% 50%;
  432. background-position: 0px 80%;
  433. }
  434. }
  435. }
  436. .bottom {
  437. flex-shrink: 0;
  438. display: grid;
  439. grid-template-columns: repeat(2, 1fr);
  440. gap: 12px;
  441. height: 350px;
  442. margin-top: 24px;
  443. .left {
  444. position: relative;
  445. padding-top: 44px;
  446. background: url('@/assets/images/dashboard/bg-middle-title-04.png') left top no-repeat, url('@/assets/images/dashboard/bg-bottom.png') left center no-repeat;
  447. background-size: 100% 44px, 100% 100%;
  448. .title-status {
  449. position: absolute;
  450. top: 0;
  451. right: 0;
  452. display: flex;
  453. align-items: center;
  454. justify-content: flex-end;
  455. width: 100%;
  456. height: 44px;
  457. padding-right: 30px;
  458. font-size: 14px;
  459. color: rgba(255, 255, 255, 0.8);
  460. }
  461. }
  462. .right {
  463. padding-top: 44px;
  464. background: url('@/assets/images/dashboard/bg-middle-title-05.png') left top no-repeat, url('@/assets/images/dashboard/bg-bottom.png') left center no-repeat;
  465. background-size: 100% 44px, 100% 100%;
  466. }
  467. .statistic-list {
  468. display: grid;
  469. grid-template-columns: repeat(2, 1fr);
  470. grid-template-rows: repeat(2, 1fr);
  471. gap: 15px;
  472. height: 100%;
  473. padding: 30px 42px;
  474. .statistic-item {
  475. height: 100%;
  476. padding: 20px 36px 0 36px;
  477. background: url('@/assets/images/dashboard/bg-bottom-item.png') center center no-repeat;
  478. background-size: 100% 100%;
  479. overflow: hidden;
  480. .numeric-stats-box {
  481. display: flex;
  482. justify-content: space-between;
  483. align-items: flex-end;
  484. height: 50px;
  485. .ratio {
  486. font-size: 14px;
  487. line-height: 24px;
  488. }
  489. .green {
  490. color: #00F040;
  491. }
  492. .red {
  493. color: #FF7300
  494. }
  495. }
  496. .title {
  497. color: #E6FFF5;
  498. font-size: 18px;
  499. line-height: 26px;
  500. letter-spacing: 2.25px;
  501. }
  502. .num {
  503. color: #FFF;
  504. text-shadow: 0px 0px 3.75px rgba(58, 206, 237, 0.50);
  505. font-family: D-DIN-PRO;
  506. font-size: 36px;
  507. font-weight: bold;
  508. line-height: 50px;
  509. }
  510. .unit {
  511. color: #9c9c9c;
  512. font-size: 18px;
  513. font-weight: 400;
  514. letter-spacing: 2.25px;
  515. }
  516. }
  517. }
  518. }
  519. }
  520. </style>