index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. <template>
  2. <div class="statistics-dashboard">
  3. <!-- 数据概览 (Data Overview) -->
  4. <el-card class="overview-section" shadow="never">
  5. <div slot="header" class="header">
  6. <span>数据概览</span>
  7. <el-dropdown class="dropdown-menu" trigger="click">
  8. <span class="el-dropdown-link">
  9. 部门方案查看 <i class="el-icon-arrow-down el-icon--right"></i>
  10. </span>
  11. <el-dropdown-menu slot="dropdown">
  12. <el-dropdown-item>全部部门</el-dropdown-item>
  13. <el-dropdown-item>销售部</el-dropdown-item>
  14. <el-dropdown-item>市场部</el-dropdown-item>
  15. </el-dropdown-menu>
  16. </el-dropdown>
  17. </div>
  18. <el-row :gutter="20">
  19. <el-col :span="3">
  20. <div class="data-card">
  21. <div class="card-title">
  22. <i class="el-icon-user-solid"></i>
  23. 分公司数量
  24. </div>
  25. <div class="card-value highlight">{{dealderCount}}</div>
  26. </div>
  27. </el-col>
  28. <el-col :span="3">
  29. <div class="data-card">
  30. <div class="card-title">
  31. <i class="el-icon-user"></i>
  32. 销售数量
  33. </div>
  34. <div class="card-value highlight">{{groupMgrCount}}</div>
  35. </div>
  36. </el-col>
  37. <el-col :span="3">
  38. <div class="data-card">
  39. <div class="card-title">
  40. <i class="el-icon-shopping-cart-full"></i>
  41. 会员数量
  42. </div>
  43. <div class="card-value highlight">{{memberCount}}</div>
  44. <div class="card-badge">
  45. <i class="el-icon-camera"></i>
  46. </div>
  47. </div>
  48. </el-col>
  49. <el-col :span="3">
  50. <div class="data-card">
  51. <div class="card-title">
  52. <i class="el-icon-money"></i>
  53. 可用余额
  54. </div>
  55. <div class="card-value highlight">143,650.07</div>
  56. </div>
  57. </el-col>
  58. <el-col :span="3">
  59. <div class="data-card">
  60. <div class="card-title">
  61. <span>今日消耗</span>
  62. </div>
  63. <div class="card-value highlight">1,093.70</div>
  64. <div class="card-sub">
  65. <span>昨日消耗(元)</span>
  66. <span class="sub-value">1952.8</span>
  67. </div>
  68. <el-progress :percentage="74" :show-text="false" color="#409EFF"></el-progress>
  69. <div class="card-desc">预测不足74天</div>
  70. </div>
  71. </el-col>
  72. <el-col :span="3">
  73. <div class="data-card">
  74. <div class="card-title">
  75. <span class="cdn-label">CDN</span>
  76. 今日
  77. </div>
  78. <div class="card-value highlight">1.79T</div>
  79. <div class="card-sub">
  80. <span>本月</span>
  81. <span class="sub-value">18.45T</span>
  82. </div>
  83. </div>
  84. </el-col>
  85. <el-col :span="3">
  86. <div class="data-card">
  87. <div class="card-title">
  88. <i class="el-icon-message"></i>
  89. 短信剩余条数
  90. </div>
  91. <div class="card-value highlight">{{smsRemainCount}}</div>
  92. </div>
  93. </el-col>
  94. <el-col :span="3">
  95. <div class="data-card">
  96. <div class="card-title">
  97. 平台今日看课人数
  98. </div>
  99. <div class="card-value highlight">{{todayWatchUserCount}}</div>
  100. <div class="card-sub">
  101. <span>配额上限</span>
  102. <span class="sub-value">{{todayWatchUserCount}}/{{versionLimit}}</span>
  103. </div>
  104. <el-progress :percentage="70" :show-text="false" color="#409EFF"></el-progress>
  105. </div>
  106. </el-col>
  107. </el-row>
  108. </el-card>
  109. <!-- 分析概览 (Analysis Overview) -->
  110. <el-card class="analysis-section" shadow="never">
  111. <div slot="header" class="header">
  112. <span>分析概览</span>
  113. <div class="tab-group">
  114. <el-radio-group v-model="queryTime" size="medium" @change="handleAnalysis">
  115. <el-radio-button label="今日"></el-radio-button>
  116. <el-radio-button label="昨日"></el-radio-button>
  117. <el-radio-button label="本周"></el-radio-button>
  118. <el-radio-button label="本月"></el-radio-button>
  119. <el-radio-button label="上月"></el-radio-button>
  120. </el-radio-group>
  121. </div>
  122. <div class="action-group">
  123. <el-button size="small" plain icon="el-icon-refresh">手动刷新</el-button>
  124. <el-button size="small" plain>自动刷新</el-button>
  125. <el-button size="small" type="primary">刷新</el-button>
  126. </div>
  127. </div>
  128. <el-row :gutter="20">
  129. <el-col :span="6">
  130. <div class="analysis-card">
  131. <div class="card-icon"><i class="el-icon-monitor"></i></div>
  132. <div class="card-content">
  133. <div class="card-row">
  134. <span>观看人数</span>
  135. <span class="highlight">{{watchUserCount}}</span>
  136. </div>
  137. <div class="card-row">
  138. <span>完播人数</span>
  139. <span class="highlight">{{completedUserCount}}</span>
  140. </div>
  141. <div class="card-row">
  142. <span>完播率</span>
  143. <span class="highlight">{{completedRate}}%</span>
  144. </div>
  145. </div>
  146. </div>
  147. </el-col>
  148. <el-col :span="6">
  149. <div class="analysis-card">
  150. <div class="card-icon"><i class="el-icon-video-play"></i></div>
  151. <div class="card-content">
  152. <div class="card-row">
  153. <span>观看次数</span>
  154. <span class="highlight">{{watchCount}}</span>
  155. </div>
  156. <div class="card-row">
  157. <span>完播次数</span>
  158. <span class="highlight">{{completedCount}}</span>
  159. </div>
  160. <div class="card-row">
  161. <span>视频完播率</span>
  162. <span class="highlight">{{watchRate}}</span>
  163. </div>
  164. </div>
  165. </div>
  166. </el-col>
  167. <el-col :span="6">
  168. <div class="analysis-card">
  169. <div class="card-icon"><i class="el-icon-headset"></i></div>
  170. <div class="card-content">
  171. <div class="card-row">
  172. <span>答题人数</span>
  173. <span class="highlight">{{answerMemberCount}}</span>
  174. </div>
  175. <div class="card-row">
  176. <span>正确人数</span>
  177. <span class="highlight">{{correctUserCount}}</span>
  178. </div>
  179. <div class="card-row">
  180. <span>正确率</span>
  181. <span class="highlight">{{correctRate}}%</span>
  182. </div>
  183. </div>
  184. </div>
  185. </el-col>
  186. <el-col :span="6">
  187. <div class="analysis-card">
  188. <div class="card-icon"><i class="el-icon-present"></i></div>
  189. <div class="card-content">
  190. <div class="card-row">
  191. <span>答题红包个数</span>
  192. <span class="highlight">{{rewardCount}}</span>
  193. </div>
  194. <div class="card-row">
  195. <span>答题红包金额(元)</span>
  196. <span class="highlight">{{rewardMoney}}</span>
  197. </div>
  198. </div>
  199. </div>
  200. </el-col>
  201. </el-row>
  202. </el-card>
  203. <!-- 图表区域 (Charts Area) -->
  204. <el-row :gutter="20" class="charts-section">
  205. <el-col :span="12">
  206. <el-card shadow="never">
  207. <div slot="header" class="chart-header">
  208. <span>会员观看、完播人数趋势图</span>
  209. <div class="legend">
  210. <div class="legend-item">
  211. <span class="dot viewer-dot"></span>
  212. <span>观看人数</span>
  213. </div>
  214. <div class="legend-item">
  215. <span class="dot complete-dot"></span>
  216. <span>完播人数</span>
  217. </div>
  218. </div>
  219. <el-button size="small" plain class="view-more">平台每日统计 <i class="el-icon-arrow-right"></i></el-button>
  220. </div>
  221. <div ref="viewerChart" class="chart-container"></div>
  222. </el-card>
  223. </el-col>
  224. <el-col :span="12">
  225. <el-card shadow="never">
  226. <div slot="header" class="chart-header">
  227. <span>经销商会员观看TOP10</span>
  228. <div class="legend">
  229. <el-radio-group v-model="viewerType" size="small">
  230. <el-radio-button label="viewers">按观看人数</el-radio-button>
  231. <el-radio-button label="completed">按完播人数</el-radio-button>
  232. </el-radio-group>
  233. </div>
  234. <el-button size="small" plain class="view-more">经销商统计 <i class="el-icon-arrow-right"></i></el-button>
  235. </div>
  236. <div ref="dealerChart" class="chart-container"></div>
  237. </el-card>
  238. </el-col>
  239. </el-row>
  240. </div>
  241. </template>
  242. <script>
  243. import * as echarts from 'echarts'
  244. import {analysisPreview, authorizationInfo, dealerAggregated, smsBalance} from "@/api/statistics/statistics";
  245. import dayjs from 'dayjs';
  246. export default {
  247. name: 'StatisticsDashboard',
  248. data() {
  249. return {
  250. smsRemainCount: 0,
  251. viewerType: 'viewers',
  252. viewerChart: null,
  253. dealerChart: null,
  254. // 分公司数量
  255. dealderCount: 0,
  256. // 销售数量
  257. groupMgrCount: 0,
  258. // 会员总数量
  259. memberCount: 0,
  260. // 正常会员数量
  261. normalNum: 0,
  262. // 黑名单会员数量
  263. blackNum: 0,
  264. // 观看人数
  265. watchUserCount: 0,
  266. // 完播人数
  267. completedUserCount: 0,
  268. // 完播率
  269. completedRate: 0,
  270. // 观看次数
  271. watchCount:0,
  272. // 完播次数
  273. completedCount: 0,
  274. // 视频完播率
  275. watchRate: 0,
  276. // 答题人数
  277. answerMemberCount: 0,
  278. // 正确人数
  279. correctUserCount: 0,
  280. correctRate: 0.0,
  281. // 答题红包个数
  282. rewardCount: 0,
  283. // 答题红包金额
  284. rewardMoney: 0.0,
  285. queryTime: '今日',
  286. todayWatchUserCount: 0,
  287. versionLimit: 0
  288. }
  289. },
  290. mounted() {
  291. this.$nextTick(() => {
  292. this.initViewerChart()
  293. this.initDealerChart()
  294. // 监听窗口大小变化,重新渲染图表
  295. window.addEventListener('resize', () => {
  296. this.viewerChart && this.viewerChart.resize()
  297. this.dealerChart && this.dealerChart.resize()
  298. })
  299. })
  300. },
  301. created() {
  302. dealerAggregated().then(res=>{
  303. if(res.code === 200){
  304. this.dealderCount = res.data.dealderCount;
  305. this.groupMgrCount = res.data.groupMgrCount;
  306. this.memberCount = res.data.memberCount;
  307. this.normalNum = res.data.normalNum;
  308. this.blackNum = res.data.blackNum;
  309. }
  310. })
  311. let param = {
  312. startTime: '',
  313. endTime: ''
  314. };
  315. // 获取当前日期时间
  316. const today = dayjs();
  317. param.startTime = this.formatDate(today);
  318. param.endTime = this.formatDate(today);
  319. analysisPreview(param).then(res=>{
  320. if(res.code === 200){
  321. this.watchUserCount = res.data.watchUserCount;
  322. this.completedUserCount = res.data.completedUserCount;
  323. this.completedRate = res.data.completedRate;
  324. this.watchCount = res.data.watchCount;
  325. this.completedCount = res.data.completedCount;
  326. this.answerMemberCount = res.data.answerMemberCount;
  327. this.correctUserCount = res.data.correctUserCount;
  328. this.correctRate = res.data.correctRate;
  329. this.rewardCount = res.data.rewardCount;
  330. this.rewardMoney = res.data.rewardMoney;
  331. }
  332. })
  333. smsBalance().then(res=>{
  334. if(res.code === 200){
  335. this.smsRemainCount = res.data;
  336. }
  337. })
  338. authorizationInfo().then(res=>{
  339. if(res.code === 200){
  340. this.todayWatchUserCount = res.data.todayWatchUserCount;
  341. this.versionLimit = res.data.versionLimit;
  342. }
  343. })
  344. },
  345. methods: {
  346. formatDate(date) {
  347. return dayjs(date).format('YYYY-MM-DD');
  348. },
  349. // 分析概览
  350. handleAnalysis(){
  351. let param = {
  352. startTime: '',
  353. endTime: ''
  354. };
  355. // 获取当前日期时间
  356. const today = dayjs();
  357. if (this.queryTime === '今日') {
  358. param.startTime = this.formatDate(today);
  359. param.endTime = this.formatDate(today);
  360. } else if (this.queryTime === '昨日') {
  361. const yesterday = today.subtract(1, 'day');
  362. param.startTime = this.formatDate(yesterday);
  363. param.endTime = this.formatDate(yesterday);
  364. } else if (this.queryTime === '本周') {
  365. param.startTime = this.formatDate(today.startOf('week'));
  366. param.endTime = this.formatDate(today.endOf('week'));
  367. } else if (this.queryTime === '本月') {
  368. param.startTime = this.formatDate(today.startOf('month'));
  369. param.endTime = this.formatDate(today.endOf('month'));
  370. } else if (this.queryTime === '上月') {
  371. const lastMonth = today.subtract(1, 'month');
  372. param.startTime = this.formatDate(lastMonth.startOf('month'));
  373. param.endTime = this.formatDate(lastMonth.endOf('month'));
  374. } else {
  375. // 可以添加一个默认处理或错误提示
  376. console.warn(`未知的 queryTime: ${this.queryTime}, 默认使用今日`);
  377. param.startTime = this.formatDate(today);
  378. param.endTime = this.formatDate(today);
  379. }
  380. analysisPreview(param).then(res=>{
  381. if(res.code === 200){
  382. this.watchUserCount = res.data.watchUserCount;
  383. this.completedUserCount = res.data.completedUserCount;
  384. this.completedRate = res.data.completedRate;
  385. this.watchCount = res.data.watchCount;
  386. this.completedCount = res.data.completedCount;
  387. this.answerMemberCount = res.data.answerMemberCount;
  388. this.correctUserCount = res.data.correctUserCount;
  389. this.correctRate = res.data.correctRate;
  390. this.rewardCount = res.data.rewardCount;
  391. this.rewardMoney = res.data.rewardMoney;
  392. }
  393. })
  394. },
  395. initViewerChart() {
  396. this.viewerChart = echarts.init(this.$refs.viewerChart)
  397. const option = {
  398. tooltip: {
  399. trigger: 'axis',
  400. axisPointer: {
  401. type: 'shadow'
  402. }
  403. },
  404. grid: {
  405. left: '3%',
  406. right: '4%',
  407. bottom: '3%',
  408. containLabel: true
  409. },
  410. xAxis: {
  411. type: 'category',
  412. data: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']
  413. },
  414. yAxis: {
  415. type: 'value'
  416. },
  417. series: [
  418. {
  419. name: '观看人数',
  420. type: 'bar',
  421. data: [50, 20, 40, 120, 700, 800, 900, 780, 700, 700, 680, 580, 550, 450, 400, 320, 170, 0, 0, 0, 0, 0, 0, 0],
  422. itemStyle: {
  423. color: '#409EFF'
  424. }
  425. },
  426. {
  427. name: '完播人数',
  428. type: 'bar',
  429. data: [30, 10, 20, 300, 600, 700, 780, 700, 650, 650, 650, 520, 450, 400, 330, 120, 100, 0, 0, 0, 0, 0, 0, 0],
  430. itemStyle: {
  431. color: '#67C23A'
  432. }
  433. }
  434. ]
  435. }
  436. this.viewerChart.setOption(option)
  437. },
  438. initDealerChart() {
  439. this.dealerChart = echarts.init(this.$refs.dealerChart)
  440. const option = {
  441. tooltip: {
  442. trigger: 'axis',
  443. axisPointer: {
  444. type: 'shadow'
  445. }
  446. },
  447. grid: {
  448. left: '3%',
  449. right: '4%',
  450. bottom: '3%',
  451. containLabel: true
  452. },
  453. xAxis: {
  454. type: 'value'
  455. },
  456. yAxis: {
  457. type: 'category',
  458. data: ['七彩公司', '二级经销商A', '内蒙七彩', '呼和浩特分公司', '呼和浩特经销商', '海南经销', '重庆市分公司', '东方分销商', '银田分公司', '重庆一分销']
  459. },
  460. series: [
  461. {
  462. name: '观看人数',
  463. type: 'bar',
  464. data: [2780, 1050, 650, 600, 500, 350, 260, 230, 180, 120],
  465. itemStyle: {
  466. color: '#409EFF'
  467. }
  468. }
  469. ]
  470. }
  471. this.dealerChart.setOption(option)
  472. }
  473. },
  474. watch: {
  475. viewerType(newVal) {
  476. if (newVal === 'viewers') {
  477. this.dealerChart.setOption({
  478. series: [{
  479. data: [2780, 1050, 650, 600, 500, 350, 260, 230, 180, 120]
  480. }]
  481. })
  482. } else {
  483. this.dealerChart.setOption({
  484. series: [{
  485. name: '完播人数',
  486. data: [2500, 980, 620, 580, 480, 320, 240, 210, 160, 100]
  487. }]
  488. })
  489. }
  490. }
  491. },
  492. beforeDestroy() {
  493. window.removeEventListener('resize', this.resizeHandler)
  494. this.viewerChart && this.viewerChart.dispose()
  495. this.dealerChart && this.dealerChart.dispose()
  496. }
  497. }
  498. </script>
  499. <style scoped>
  500. ::v-deep .el-radio-button__inner:hover {
  501. color: #409EFF; /* 鼠标悬浮时的文字颜色,可以根据需要调整 */
  502. }
  503. ::v-deep .el-radio-button.is-active .el-radio-button__inner {
  504. background-color: #409EFF; /* 选中时的背景色 */
  505. border-color: #409EFF; /* 选中时的边框色 */
  506. color: #FFFFFF; /* 选中时的文字颜色 (通常是白色) */
  507. box-shadow: -1px 0 0 0 #409EFF; /* 处理按钮间的连接缝隙 */
  508. }
  509. /* 如果需要,也可以修改非选中状态下的聚焦(focus)或悬浮(hover)样式 */
  510. /* 例如,让非选中按钮悬浮时边框和文字也变蓝 */
  511. ::v-deep .el-radio-button:not(.is-active) .el-radio-button__inner:hover {
  512. color: #409EFF;
  513. /* border-color: #b3d8ff; Element UI 默认悬浮边框色,可以按需修改 */
  514. }
  515. /* 聚焦时的外框,如果需要的话 */
  516. ::v-deep .el-radio-button:focus:not(.is-checked) .el-radio-button__inner {
  517. /* border-color: #409EFF; */ /* Element UI 默认的 focus 颜色通常关联主题色 */
  518. /* box-shadow: 0 0 2px 2px rgba(64, 158, 255, 0.2); */ /* 示例 focus 光晕 */
  519. }
  520. .statistics-dashboard {
  521. padding: 20px;
  522. background-color: #f5f7fa;
  523. }
  524. .overview-section,
  525. .analysis-section {
  526. margin-bottom: 20px;
  527. border-radius: 4px;
  528. }
  529. .header {
  530. display: flex;
  531. justify-content: space-between;
  532. align-items: center;
  533. font-size: 16px;
  534. font-weight: 500;
  535. }
  536. .data-card {
  537. background-color: #fff;
  538. border-radius: 4px;
  539. padding: 15px;
  540. height: 100px;
  541. display: flex;
  542. flex-direction: column;
  543. position: relative;
  544. }
  545. .card-title {
  546. color: #606266;
  547. font-size: 14px;
  548. margin-bottom: 10px;
  549. }
  550. .card-value {
  551. font-size: 24px;
  552. font-weight: bold;
  553. margin-top: auto;
  554. }
  555. .highlight {
  556. color: #409EFF;
  557. }
  558. .card-sub {
  559. display: flex;
  560. justify-content: space-between;
  561. font-size: 12px;
  562. color: #909399;
  563. margin-top: 5px;
  564. }
  565. .card-desc {
  566. font-size: 12px;
  567. color: #909399;
  568. margin-top: 5px;
  569. }
  570. .card-badge {
  571. position: absolute;
  572. top: 15px;
  573. right: 15px;
  574. background: #f0f9eb;
  575. color: #67c23a;
  576. padding: 2px 5px;
  577. border-radius: 4px;
  578. }
  579. .cdn-label {
  580. background-color: #409EFF;
  581. color: white;
  582. padding: 2px 5px;
  583. border-radius: 4px;
  584. margin-right: 5px;
  585. font-size: 12px;
  586. }
  587. .tab-group {
  588. display: flex;
  589. gap: 10px;
  590. }
  591. .action-group {
  592. display: flex;
  593. gap: 10px;
  594. }
  595. .analysis-card {
  596. background-color: #fff;
  597. border-radius: 4px;
  598. padding: 20px;
  599. display: flex;
  600. box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
  601. }
  602. .card-icon {
  603. width: 50px;
  604. height: 50px;
  605. background-color: rgba(64, 158, 255, 0.1);
  606. border-radius: 8px;
  607. display: flex;
  608. justify-content: center;
  609. align-items: center;
  610. font-size: 24px;
  611. color: #409EFF;
  612. margin-right: 20px;
  613. }
  614. .card-content {
  615. flex: 1;
  616. }
  617. .card-row {
  618. display: flex;
  619. justify-content: space-between;
  620. margin-bottom: 10px;
  621. }
  622. .charts-section {
  623. margin-top: 20px;
  624. }
  625. .chart-header {
  626. display: flex;
  627. justify-content: space-between;
  628. align-items: center;
  629. }
  630. .view-more {
  631. font-size: 12px;
  632. }
  633. .legend {
  634. display: flex;
  635. gap: 15px;
  636. }
  637. .legend-item {
  638. display: flex;
  639. align-items: center;
  640. font-size: 12px;
  641. }
  642. .dot {
  643. width: 10px;
  644. height: 10px;
  645. border-radius: 50%;
  646. margin-right: 5px;
  647. }
  648. .viewer-dot {
  649. background-color: #409EFF;
  650. }
  651. .complete-dot {
  652. background-color: #67C23A;
  653. }
  654. .chart-container {
  655. height: 350px;
  656. width: 100%;
  657. }
  658. </style>