index.vue 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266
  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. </div>
  8. <el-row :gutter="20">
  9. <el-col :span="3">
  10. <div class="data-card">
  11. <div class="card-title">
  12. <i class="el-icon-user-solid"></i>
  13. 分公司数量
  14. </div>
  15. <div class="card-value highlight">{{dealderCount}}</div>
  16. </div>
  17. </el-col>
  18. <el-col :span="3">
  19. <div class="data-card">
  20. <div class="card-title">
  21. <i class="el-icon-user"></i>
  22. 销售数量
  23. </div>
  24. <div class="card-value highlight">{{groupMgrCount}}</div>
  25. </div>
  26. </el-col>
  27. <el-col :span="3">
  28. <div class="data-card">
  29. <div class="card-title">
  30. <i class="el-icon-shopping-cart-full"></i>
  31. 会员数量
  32. </div>
  33. <div class="card-value highlight">{{memberCount}}</div>
  34. <div class="card-badge">
  35. </div>
  36. </div>
  37. </el-col>
  38. <el-col :span="3">
  39. <div class="data-card">
  40. <div class="card-title">
  41. <i class="el-icon-money"></i>
  42. 可用余额
  43. </div>
  44. <div class="card-value highlight">143,650.07</div>
  45. </div>
  46. </el-col>
  47. <el-col :span="3">
  48. <div class="data-card">
  49. <div class="card-title">
  50. <span>今日消耗</span>
  51. </div>
  52. <div class="card-value highlight">1,093.70</div>
  53. <div class="card-sub">
  54. <span>昨日消耗(元)</span>
  55. <span class="sub-value">1952.8</span>
  56. </div>
  57. <el-progress :percentage="74" :show-text="false" color="#409EFF"></el-progress>
  58. <div class="card-desc">预测不足74天</div>
  59. </div>
  60. </el-col>
  61. <el-col :span="3">
  62. <div class="data-card">
  63. <div class="card-title">
  64. <span class="cdn-label">CDN</span>
  65. 今日
  66. </div>
  67. <div class="card-value highlight">1.79T</div>
  68. <div class="card-sub">
  69. <span>本月</span>
  70. <span class="sub-value">18.45T</span>
  71. </div>
  72. </div>
  73. </el-col>
  74. <el-col :span="3">
  75. <div class="data-card">
  76. <div class="card-title">
  77. <i class="el-icon-message"></i>
  78. 短信剩余条数
  79. </div>
  80. <div class="card-value highlight">{{smsRemainCount}}</div>
  81. </div>
  82. </el-col>
  83. <el-col :span="3">
  84. <div class="data-card">
  85. <div class="card-title">
  86. 平台今日看课人数
  87. </div>
  88. <div class="card-value highlight">{{todayWatchUserCount}}</div>
  89. <div class="card-sub">
  90. <span>配额上限</span>
  91. <span class="sub-value">{{todayWatchUserCount}}/{{versionLimit}}</span>
  92. </div>
  93. <el-progress :percentage="70" :show-text="false" color="#409EFF"></el-progress>
  94. </div>
  95. </el-col>
  96. </el-row>
  97. </el-card>
  98. <!-- 分析概览 (Analysis Overview) -->
  99. <div class="analysis-section" shadow="never">
  100. <div slot="header" class="header">
  101. <span>分析概览</span>
  102. <div class="tab-group">
  103. <el-radio-group v-model="queryTime" size="medium" @change="handleAnalysis">
  104. <el-radio-button label="今日"></el-radio-button>
  105. <el-radio-button label="昨日"></el-radio-button>
  106. <el-radio-button label="本周"></el-radio-button>
  107. <el-radio-button label="本月"></el-radio-button>
  108. <el-radio-button label="上月"></el-radio-button>
  109. </el-radio-group>
  110. </div>
  111. <div class="action-group">
  112. <el-button size="small" plain icon="el-icon-refresh" @click="manualRefresh">手动刷新</el-button>
  113. <el-dropdown @command="handleAutoRefresh" trigger="click">
  114. <el-button size="small" plain>
  115. 自动刷新
  116. <i class="el-icon-arrow-down el-icon--right"></i>
  117. </el-button>
  118. <el-dropdown-menu slot="dropdown">
  119. <el-dropdown-item :command="0" :class="{ 'is-active': !autoRefreshInterval }">关闭</el-dropdown-item>
  120. <el-dropdown-item :command="5" :class="{ 'is-active': autoRefreshInterval === 5 }">5分钟</el-dropdown-item>
  121. <el-dropdown-item :command="10" :class="{ 'is-active': autoRefreshInterval === 10 }">10分钟</el-dropdown-item>
  122. <el-dropdown-item :command="15" :class="{ 'is-active': autoRefreshInterval === 15 }">15分钟</el-dropdown-item>
  123. </el-dropdown-menu>
  124. </el-dropdown>
  125. <el-button size="small" type="primary" @click="refresh">刷新</el-button>
  126. </div>
  127. </div>
  128. </div>
  129. <div>
  130. <el-row :gutter="20">
  131. <el-col :span="12" style="position: relative">
  132. <div class="analysis-card-check" :class="selectedDiv===0?'analysis-card-check-selected color':''" @click="handleToggleDiv(0)">
  133. <div class="analysis-card">
  134. <div class="card-icon"><i class="el-icon-monitor"></i></div>
  135. <div class="card-content">
  136. <div class="card-row">
  137. <span>观看人数</span>
  138. <span class="highlight">{{watchUserCount}}</span>
  139. </div>
  140. <div class="card-row">
  141. <span>完播人数</span>
  142. <span class="highlight">{{completedUserCount}}</span>
  143. </div>
  144. <div class="card-row">
  145. <span>完播率</span>
  146. <span class="highlight">{{completedRate}}%</span>
  147. </div>
  148. </div>
  149. </div>
  150. <div class="analysis-card">
  151. <div class="card-icon"><i class="el-icon-video-play"></i></div>
  152. <div class="card-content">
  153. <div class="card-row">
  154. <span>观看次数</span>
  155. <span class="highlight">{{watchCount}}</span>
  156. </div>
  157. <div class="card-row">
  158. <span>完播次数</span>
  159. <span class="highlight">{{completedCount}}</span>
  160. </div>
  161. <div class="card-row">
  162. <span>视频完播率</span>
  163. <span class="highlight">{{watchRate}}</span>
  164. </div>
  165. </div>
  166. </div>
  167. </div>
  168. </el-col>
  169. <el-col :span="6" style="position: relative">
  170. <div class="analysis-card-check" :class="selectedDiv===1?'analysis-card-check-selected color':''" @click="handleToggleDiv(1)">
  171. <div class="analysis-card">
  172. <div class="card-icon"><i class="el-icon-headset"></i></div>
  173. <div class="card-content">
  174. <div class="card-row">
  175. <span>答题人数</span>
  176. <span class="highlight">{{answerMemberCount}}</span>
  177. </div>
  178. <div class="card-row">
  179. <span>正确人数</span>
  180. <span class="highlight">{{correctUserCount}}</span>
  181. </div>
  182. <div class="card-row">
  183. <span>正确率</span>
  184. <span class="highlight">{{correctRate}}%</span>
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. </el-col>
  190. <el-col :span="6" style="position: relative">
  191. <div class="analysis-card-check" :class="selectedDiv===2?'analysis-card-check-selected color':''" @click="handleToggleDiv(2)">
  192. <div class="analysis-card">
  193. <div class="card-icon"><i class="el-icon-present"></i></div>
  194. <div class="card-content">
  195. <div class="card-row">
  196. <span>答题红包个数</span>
  197. <span class="highlight">{{rewardCount}}</span>
  198. </div>
  199. <div class="card-row">
  200. <span>答题红包金额(元)</span>
  201. <span class="highlight">{{rewardMoney}}</span>
  202. </div>
  203. </div>
  204. </div>
  205. </div>
  206. </el-col>
  207. </el-row>
  208. </div>
  209. <!-- 图表区域 (Charts Area) -->
  210. <transition name="fade">
  211. <el-row :gutter="20" class="charts-section" v-show="selectedDiv===0">
  212. <el-col :span="12">
  213. <el-card shadow="never">
  214. <div slot="header" class="chart-header">
  215. <span>会员观看、完播人数趋势图</span>
  216. <div class="legend">
  217. <div class="legend-item">
  218. <span class="dot viewer-dot"></span>
  219. <span>观看人数</span>
  220. </div>
  221. <div class="legend-item">
  222. <span class="dot complete-dot"></span>
  223. <span>完播人数</span>
  224. </div>
  225. </div>
  226. <el-button size="small" plain class="view-more">平台每日统计 <i class="el-icon-arrow-right"></i></el-button>
  227. </div>
  228. <div ref="viewerChart" class="chart-container"></div>
  229. </el-card>
  230. </el-col>
  231. <el-col :span="12">
  232. <el-card shadow="never">
  233. <div slot="header" class="chart-header">
  234. <span>经销商会员观看TOP10</span>
  235. <div class="legend">
  236. <el-radio-group v-model="viewerType" size="small" @change="handleDealerChartData">
  237. <el-radio-button label="0">按观看人数</el-radio-button>
  238. <el-radio-button label="1">按完播人数</el-radio-button>
  239. </el-radio-group>
  240. </div>
  241. <el-button size="small" plain class="view-more">经销商统计 <i class="el-icon-arrow-right"></i></el-button>
  242. </div>
  243. <div ref="dealerChart" class="chart-container"></div>
  244. </el-card>
  245. </el-col>
  246. </el-row>
  247. </transition>
  248. <transition name="fade">
  249. <el-row>
  250. <el-card shadow="never" v-show="selectedDiv===1">
  251. <div slot="header" class="chart-header">
  252. <span>课程观看TOP10</span>
  253. <div class="legend">
  254. <el-radio-group v-model="viewerType" size="small" @change="handleCourseWatchChart">
  255. <el-radio-button label="0">按观看人数</el-radio-button>
  256. <el-radio-button label="1">按完播人数</el-radio-button>
  257. <el-radio-button label="2">按答题人数</el-radio-button>
  258. <el-radio-button label="3">按正确人数</el-radio-button>
  259. </el-radio-group>
  260. </div>
  261. <div class="legend">
  262. <el-radio-group v-model="delerSort" @change="handleCourseWatchChart">
  263. <el-radio label="DESC">前10名</el-radio>
  264. <el-radio label="ASC">倒数10名</el-radio>
  265. </el-radio-group>
  266. </div>
  267. <div class="legend">
  268. <div class="legend-item">
  269. <span class="dot viewer-dot"></span>
  270. <span>观看人数</span>
  271. </div>
  272. <div class="legend-item">
  273. <span class="dot complete-dot"></span>
  274. <span>完播人数</span>
  275. </div>
  276. <div class="legend-item">
  277. <span class="dot" style="background-color: #E6A23C"></span>
  278. <span>答题人数</span>
  279. </div>
  280. <div class="legend-item">
  281. <span class="dot" style="background-color: #F56C6C"></span>
  282. <span>正确人数</span>
  283. </div>
  284. </div>
  285. <el-button size="small" plain class="view-more">经销商统计 <i class="el-icon-arrow-right"></i></el-button>
  286. </div>
  287. <div ref="courseWatchChart" class="chart-container"></div>
  288. </el-card>
  289. </el-row>
  290. </transition>
  291. <transition name="fade">
  292. <el-row :gutter="20" class="charts-section" v-show="selectedDiv===2">
  293. <el-col :span="12">
  294. <el-card shadow="never">
  295. <div slot="header" class="chart-header">
  296. <span>答题红包金额TOP10</span>
  297. <div class="legend">
  298. <el-radio-group v-model="dataType" size="small" @change="handleAnswerRedPackViewerChart">
  299. <el-radio-button label="0">按经销商排行</el-radio-button>
  300. <el-radio-button label="1">按课程排行</el-radio-button>
  301. </el-radio-group>
  302. </div>
  303. <el-button size="small" plain class="view-more">红包记录 <i class="el-icon-arrow-right"></i></el-button>
  304. </div>
  305. <div ref="answerRedPackViewerChart" class="chart-container"></div>
  306. </el-card>
  307. </el-col>
  308. <el-col :span="12">
  309. <el-card shadow="never">
  310. <div slot="header" class="chart-header">
  311. <span>答题红包金额趋势图</span>
  312. <div class="legend">
  313. <div class="legend-item">
  314. <span class="dot viewer-dot"></span>
  315. <span>答题红包金额</span>
  316. </div>
  317. </div>
  318. <el-button size="small" plain class="view-more">红包记录 <i class="el-icon-arrow-right"></i></el-button>
  319. </div>
  320. <div ref="answerRedPackMoneyViewerChart" class="chart-container"></div>
  321. </el-card>
  322. </el-col>
  323. </el-row>
  324. </transition>
  325. </div>
  326. </template>
  327. <script>
  328. import * as echarts from 'echarts'
  329. import {
  330. analysisPreview,
  331. authorizationInfo,
  332. dealerAggregated, deaMemberTopTen, rewardMoneyTopTen, rewardMoneyTrend,
  333. smsBalance,
  334. watchCourseTopTen, watchEndPlayTrend
  335. } from "@/api/statistics/statistics";
  336. import dayjs from 'dayjs';
  337. const viewCharOption = {
  338. tooltip: {
  339. trigger: 'axis',
  340. axisPointer: {
  341. type: 'shadow'
  342. }
  343. },
  344. grid: {
  345. left: '3%',
  346. right: '4%',
  347. bottom: '3%',
  348. containLabel: true
  349. },
  350. xAxis: {
  351. type: 'category',
  352. 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']
  353. },
  354. yAxis: {
  355. type: 'value'
  356. },
  357. series: [
  358. {
  359. name: '观看人数',
  360. type: 'bar',
  361. data: [],
  362. itemStyle: {
  363. color: '#409EFF'
  364. }
  365. },
  366. {
  367. name: '完播人数',
  368. type: 'bar',
  369. data: [],
  370. itemStyle: {
  371. color: '#67C23A'
  372. }
  373. }
  374. ]
  375. }
  376. const dealerOption = {
  377. tooltip: {
  378. trigger: 'axis',
  379. axisPointer: {
  380. type: 'shadow'
  381. }
  382. },
  383. grid: {
  384. left: '3%',
  385. right: '4%',
  386. bottom: '3%',
  387. containLabel: true
  388. },
  389. xAxis: {
  390. type: 'value'
  391. },
  392. yAxis: {
  393. type: 'category',
  394. data: []
  395. },
  396. series: [
  397. {
  398. name: '观看人数',
  399. type: 'bar',
  400. data: [],
  401. itemStyle: {
  402. color: '#409EFF'
  403. }
  404. }
  405. ]
  406. }
  407. const courseWatchOption = {
  408. tooltip: {
  409. trigger: 'axis',
  410. axisPointer: {
  411. type: 'shadow'
  412. }
  413. },
  414. grid: {
  415. left: '3%',
  416. right: '4%',
  417. bottom: '8%',
  418. top: '3%',
  419. containLabel: true
  420. },
  421. xAxis: {
  422. type: 'category',
  423. data: [],
  424. axisLabel: {
  425. interval: 0,
  426. rotate: 30,
  427. fontSize: 10,
  428. width: 100,
  429. overflow: 'truncate'
  430. }
  431. },
  432. yAxis: {
  433. type: 'value',
  434. splitLine: {
  435. lineStyle: {
  436. type: 'dashed'
  437. }
  438. }
  439. },
  440. series: [
  441. {
  442. name: '观看人数',
  443. type: 'bar',
  444. data: [],
  445. itemStyle: {
  446. color: '#409EFF'
  447. }
  448. },
  449. {
  450. name: '完播人数',
  451. type: 'bar',
  452. data: [],
  453. itemStyle: {
  454. color: '#67C23A'
  455. }
  456. },
  457. {
  458. name: '答题人数',
  459. type: 'bar',
  460. data: [],
  461. itemStyle: {
  462. color: '#E6A23C'
  463. }
  464. },
  465. {
  466. name: '正确人数',
  467. type: 'bar',
  468. data: [],
  469. itemStyle: {
  470. color: '#F56C6C'
  471. }
  472. }
  473. ]
  474. }
  475. const lineChartOption = {
  476. tooltip: {
  477. trigger: 'axis',
  478. axisPointer: {
  479. type: 'cross' // 改为 'cross' 更适合折线图
  480. }
  481. // 你可能想要自定义 tooltip 的 formatter 来显示时间和金额
  482. // formatter: function (params) {
  483. // const point = params[0];
  484. // const date = new Date(point.value[0]);
  485. // // 自定义时间格式
  486. // const formattedTime = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  487. // return `${point.seriesName}<br/>${formattedTime} : ${point.value[1]} 元`;
  488. // }
  489. },
  490. grid: {
  491. left: '3%',
  492. right: '4%',
  493. bottom: '8%', // 如果x轴标签旋转,可能需要更大的 bottom
  494. top: '5%', // 增加一点顶部空间给可能的 Y 轴名称
  495. containLabel: true
  496. },
  497. xAxis: {
  498. type: 'time', // X轴类型改为 'time'
  499. // data: [], // 时间轴不需要单独设置 data,数据在 series 中提供
  500. axisLabel: {
  501. // interval: 0, // 时间轴通常自动处理间隔,可以先移除或注释掉
  502. rotate: 30, // 保留旋转,如果标签可能重叠
  503. fontSize: 10,
  504. // width: 100, // width 和 overflow 对于时间轴可能行为不同,按需调整
  505. // overflow: 'truncate',
  506. formatter: null // ECharts 会自动格式化时间,如需特定格式可用 function 或字符串模板
  507. // 例如: formatter: '{yyyy}-{MM}-{dd}\n{HH}:{mm}'
  508. }
  509. },
  510. yAxis: {
  511. type: 'value',
  512. name: '金额 (元)', // 添加 Y 轴名称
  513. nameLocation: 'end', // 名称位置
  514. nameTextStyle: {
  515. align: 'right',
  516. padding: [0, 10, 0, 0] // 调整名称与轴线的距离
  517. },
  518. splitLine: {
  519. lineStyle: {
  520. type: 'dashed'
  521. }
  522. },
  523. axisLabel: {
  524. formatter: '{value} 元' // 可选:给 Y 轴刻度添加单位
  525. }
  526. },
  527. series: [
  528. {
  529. name: '答题红包金额',
  530. type: 'line', // 系列类型改为 'line'
  531. // data: [], // 数据格式需要是 [[时间1, 金额1], [时间2, 金额2], ...]
  532. // 时间可以是时间戳 (毫秒), 'YYYY-MM-DD HH:mm:ss' 格式字符串, 或者 Date 对象
  533. // 例如: [['2023-10-26 08:00:00', 120], ['2023-10-26 09:00:00', 200]]
  534. data: [
  535. // 示例数据,你需要用实际数据替换
  536. // ['2024-01-01 10:00:00', 5.5],
  537. // ['2024-01-01 11:00:00', 8.2],
  538. // ['2024-01-02 09:30:00', 6.0],
  539. // ['2024-01-03 14:00:00', 10.8],
  540. ],
  541. itemStyle: { // 控制数据点(标记)的样式
  542. color: '#409EFF'
  543. },
  544. lineStyle: { // 控制线的样式
  545. color: '#409EFF'
  546. },
  547. smooth: false, // 是否平滑曲线,可设为 true
  548. symbol: 'circle', // 数据点标记形状,'emptyCircle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
  549. symbolSize: 4 // 数据点标记大小
  550. }
  551. ]
  552. };
  553. const redPackageOption = {
  554. tooltip: {
  555. trigger: 'axis',
  556. axisPointer: {
  557. type: 'shadow'
  558. }
  559. },
  560. grid: {
  561. left: '3%',
  562. right: '4%',
  563. bottom: '8%',
  564. top: '3%',
  565. containLabel: true
  566. },
  567. xAxis: {
  568. type: 'category',
  569. data: [],
  570. axisLabel: {
  571. interval: 0,
  572. rotate: 30,
  573. fontSize: 10,
  574. width: 100,
  575. overflow: 'truncate'
  576. }
  577. },
  578. yAxis: {
  579. type: 'value',
  580. splitLine: {
  581. lineStyle: {
  582. type: 'dashed'
  583. }
  584. }
  585. },
  586. series: [
  587. {
  588. name: '答题红包金额',
  589. type: 'bar',
  590. data: [],
  591. itemStyle: {
  592. color: '#409EFF'
  593. }
  594. }
  595. ]
  596. }
  597. export default {
  598. name: 'StatisticsDashboard',
  599. data() {
  600. return {
  601. dataType: '0',
  602. delerSort: 'DESC',
  603. smsRemainCount: 0,
  604. viewerType: '0',
  605. viewerChart: null,
  606. dealerChart: null,
  607. // 分公司数量
  608. dealderCount: 0,
  609. // 销售数量
  610. groupMgrCount: 0,
  611. // 会员总数量
  612. memberCount: 0,
  613. // 正常会员数量
  614. normalNum: 0,
  615. // 黑名单会员数量
  616. blackNum: 0,
  617. // 观看人数
  618. watchUserCount: 0,
  619. // 完播人数
  620. completedUserCount: 0,
  621. // 完播率
  622. completedRate: 0,
  623. // 观看次数
  624. watchCount:0,
  625. // 完播次数
  626. completedCount: 0,
  627. // 视频完播率
  628. watchRate: 0,
  629. // 答题人数
  630. answerMemberCount: 0,
  631. // 正确人数
  632. correctUserCount: 0,
  633. correctRate: 0.0,
  634. // 答题红包个数
  635. rewardCount: 0,
  636. // 答题红包金额
  637. rewardMoney: 0.0,
  638. queryTime: '今日',
  639. todayWatchUserCount: 0,
  640. versionLimit: 0,
  641. /// 选中的分析概览
  642. selectedDiv: 0,
  643. filterType: 0,
  644. answerRedPackViewerChart: null,
  645. answerRedPackMoneyViewerChart: null
  646. }
  647. },
  648. mounted() {
  649. this.$nextTick(() => {
  650. this.initViewerChart()
  651. this.initDealerChart()
  652. this.initCourseWatchChart();
  653. this.initAnswerRedPackViewerChart();
  654. this.initAnswerRedPackMoneyViewerChart();
  655. // 监听窗口大小变化,重新渲染图表
  656. window.addEventListener('resize', () => {
  657. this.viewerChart && this.viewerChart.resize()
  658. this.dealerChart && this.dealerChart.resize()
  659. })
  660. })
  661. },
  662. created() {
  663. this.refresh();
  664. },
  665. methods: {
  666. // 手动刷新
  667. manualRefresh() {
  668. this.refresh();
  669. },
  670. // 处理自动刷新选项
  671. handleAutoRefresh(command) {
  672. // 清除之前的定时器
  673. if (this.timer) {
  674. clearInterval(this.timer);
  675. this.timer = null;
  676. }
  677. // 设置新的刷新间隔
  678. this.autoRefreshInterval = parseInt(command);
  679. // 如果间隔大于0,设置新的定时器
  680. if (this.autoRefreshInterval > 0) {
  681. this.timer = setInterval(() => {
  682. this.refresh();
  683. }, this.autoRefreshInterval * 60 * 1000); // 转换为毫秒
  684. this.$message.success(`已设置${this.autoRefreshInterval}分钟自动刷新`);
  685. } else {
  686. this.$message.info('已关闭自动刷新');
  687. }
  688. },
  689. refresh() {
  690. dealerAggregated().then(res=>{
  691. if(res.code === 200){
  692. this.dealderCount = res.data.dealderCount;
  693. this.groupMgrCount = res.data.groupMgrCount;
  694. this.memberCount = res.data.memberCount;
  695. this.normalNum = res.data.normalNum;
  696. this.blackNum = res.data.blackNum;
  697. }
  698. })
  699. let param = {
  700. startTime: '',
  701. endTime: ''
  702. };
  703. // 获取当前日期时间
  704. const today = dayjs();
  705. param.startTime = this.formatDate(today);
  706. param.endTime = this.formatDate(today);
  707. analysisPreview(param).then(res=>{
  708. if(res.code === 200){
  709. this.watchUserCount = res.data.watchUserCount;
  710. this.completedUserCount = res.data.completedUserCount;
  711. this.completedRate = res.data.completedRate;
  712. this.watchCount = res.data.watchCount;
  713. this.completedCount = res.data.completedCount;
  714. this.answerMemberCount = res.data.answerMemberCount;
  715. this.correctUserCount = res.data.correctUserCount;
  716. this.correctRate = res.data.correctRate;
  717. this.rewardCount = res.data.rewardCount;
  718. this.rewardMoney = res.data.rewardMoney;
  719. }
  720. })
  721. smsBalance().then(res=>{
  722. if(res.code === 200){
  723. if(res.data == null) {
  724. this.smsRemainCount = 0;
  725. } else {
  726. this.smsRemainCount = res.data;
  727. }
  728. }
  729. })
  730. authorizationInfo().then(res=>{
  731. if(res.code === 200){
  732. this.todayWatchUserCount = res.data.todayWatchUserCount;
  733. this.versionLimit = res.data.versionLimit;
  734. }
  735. })
  736. this.handleCourseWatchChart()
  737. this.handleViewChartData()
  738. // 经销商会员观看TOP10
  739. this.handleDealerChartData()
  740. this.handleAnswerRedPackViewerChart()
  741. this.handleAnswerRedPackMoneyViewerChart()
  742. },
  743. handleToggleDiv(selected){
  744. this.selectedDiv = selected;
  745. if (selected === 1) {
  746. this.$nextTick(() => {
  747. if (this.courseWatchChart) {
  748. this.courseWatchChart.resize();
  749. } else {
  750. }
  751. });
  752. }
  753. else if (selected === 0) {
  754. this.$nextTick(() => {
  755. if (this.viewerChart) this.viewerChart.resize();
  756. if (this.dealerChart) this.dealerChart.resize();
  757. });
  758. } else if (selected === 2) {
  759. this.$nextTick(() => {
  760. if (this.answerRedPackViewerChart) this.answerRedPackViewerChart.resize();
  761. if (this.answerRedPackMoneyViewerChart) this.answerRedPackMoneyViewerChart.resize();
  762. });
  763. }
  764. if(this.selectedDiv === 0){
  765. this.handleViewChartData()
  766. this.handleDealerChartData()
  767. } else if(this.selectedDiv === 1) {
  768. this.handleCourseWatchChart()
  769. } else if(this.selectedDiv === 2) {
  770. this.handleAnswerRedPackViewerChart()
  771. this.handleAnswerRedPackMoneyViewerChart()
  772. }
  773. },
  774. formatDate(date) {
  775. return dayjs(date).format('YYYY-MM-DD');
  776. },
  777. getParam(){
  778. let param = {
  779. startTime: '',
  780. endTime: ''
  781. };
  782. // 获取当前日期时间
  783. const today = dayjs();
  784. let type = 0;
  785. if (this.queryTime === '今日') {
  786. param.startTime = this.formatDate(today);
  787. param.endTime = this.formatDate(today);
  788. type = 0;
  789. } else if (this.queryTime === '昨日') {
  790. const yesterday = today.subtract(1, 'day');
  791. param.startTime = this.formatDate(yesterday);
  792. param.endTime = this.formatDate(yesterday);
  793. type = 1;
  794. } else if (this.queryTime === '本周') {
  795. param.startTime = this.formatDate(today.startOf('week'));
  796. param.endTime = this.formatDate(today.endOf('week'));
  797. type = 2;
  798. } else if (this.queryTime === '本月') {
  799. param.startTime = this.formatDate(today.startOf('month'));
  800. param.endTime = this.formatDate(today.endOf('month'));
  801. type = 3;
  802. } else if (this.queryTime === '上月') {
  803. const lastMonth = today.subtract(1, 'month');
  804. param.startTime = this.formatDate(lastMonth.startOf('month'));
  805. param.endTime = this.formatDate(lastMonth.endOf('month'));
  806. type = 4;
  807. } else {
  808. console.warn(`未知的 queryTime: ${this.queryTime}, 默认使用今日`);
  809. param.startTime = this.formatDate(today);
  810. param.endTime = this.formatDate(today);
  811. }
  812. param.type = type;
  813. param.sort = this.delerSort;
  814. return param;
  815. },
  816. // 分析概览
  817. handleAnalysis(e){
  818. let param = this.getParam();
  819. analysisPreview(param).then(res=>{
  820. if(res.code === 200){
  821. this.watchUserCount = res.data.watchUserCount;
  822. this.completedUserCount = res.data.completedUserCount;
  823. this.completedRate = res.data.completedRate;
  824. this.watchCount = res.data.watchCount;
  825. this.completedCount = res.data.completedCount;
  826. this.answerMemberCount = res.data.answerMemberCount;
  827. this.correctUserCount = res.data.correctUserCount;
  828. this.correctRate = res.data.correctRate;
  829. this.rewardCount = res.data.rewardCount;
  830. this.rewardMoney = res.data.rewardMoney;
  831. }
  832. })
  833. if(this.selectedDiv === 0){
  834. this.handleViewChartData()
  835. this.handleDealerChartData()
  836. } else if(this.selectedDiv === 1) {
  837. this.handleCourseWatchChart()
  838. } else if(this.selectedDiv === 2) {
  839. this.handleAnswerRedPackViewerChart()
  840. this.handleAnswerRedPackMoneyViewerChart()
  841. }
  842. },
  843. handleAnswerRedPackViewerChart(){
  844. let param = this.getParam();
  845. param = {...param,statisticalType:this.viewerType,dataType: this.dataType};
  846. rewardMoneyTopTen(param).then(res=>{
  847. if(res.code === 200){
  848. let data = res.data;
  849. let companyNameList = data.map(e=>e.companyName)
  850. let courseNameList = data.map(e=>e.courseName)
  851. let rewardMoneyList = data.map(e=>e.rewardMoney)
  852. if(this.dataType === '0'){
  853. redPackageOption.xAxis.data = companyNameList;
  854. }else{
  855. redPackageOption.xAxis.data = courseNameList;
  856. }
  857. redPackageOption.series[0].data = rewardMoneyList;
  858. this.answerRedPackViewerChart.setOption(redPackageOption)
  859. }
  860. })
  861. },
  862. handleAnswerRedPackMoneyViewerChart(){
  863. let param = this.getParam();
  864. param = {...param,statisticalType:this.viewerType,dataType: this.dataType};
  865. rewardMoneyTrend(param).then(res=>{
  866. if(res.code === 200){
  867. let data = res.data;
  868. let option = data.map(e=>[e.x,e.rewardMoney])
  869. lineChartOption.series[0].data = option;
  870. this.answerRedPackMoneyViewerChart.setOption(lineChartOption)
  871. }
  872. })
  873. },
  874. handleCourseWatchChart() {
  875. let param = this.getParam();
  876. param = {...param,statisticalType:this.viewerType};
  877. watchCourseTopTen(param).then(res=>{
  878. if(res.code === 200){
  879. let data = res.data;
  880. let watchUserCountList = data.map(e=>e.watchUserCount);
  881. let completedUserCountList = data.map(e=>e.completedUserCount);
  882. let answerUserCountList = data.map(e=>e.answerUserCount);
  883. let correctUserCountList = data.map(e=>e.correctUserCount);
  884. let courseNameList = data.map(e=>e.courseName);
  885. courseWatchOption.xAxis.data = courseNameList;
  886. courseWatchOption.series[0].data = watchUserCountList;
  887. courseWatchOption.series[1].data = completedUserCountList;
  888. courseWatchOption.series[2].data = answerUserCountList;
  889. courseWatchOption.series[3].data = correctUserCountList;
  890. this.courseWatchChart.setOption(courseWatchOption)
  891. }
  892. })
  893. },
  894. handleDealerChartData(){
  895. let param = this.getParam();
  896. // 经销商会员观看TOP10
  897. deaMemberTopTen({...param,statisticalType: this.viewerType}).then(res=>{
  898. if(res.code === 200){
  899. let data = res.data;
  900. let companyNameList = data.map(e=>e.companyName);
  901. let watchUserList = data.map(e=>e.watchUserCount);
  902. dealerOption.yAxis.data = companyNameList;
  903. dealerOption.series[0].data = watchUserList;
  904. this.dealerChart.setOption(dealerOption)
  905. }
  906. })
  907. },
  908. handleViewChartData(){
  909. let param = this.getParam();
  910. watchEndPlayTrend({...param}).then(res=>{
  911. if(res.code === 200){
  912. let data = res.data;
  913. let watchUserCountList = data.map(e=>e.watchUserCount);
  914. let completedUserCountList = data.map(e=>e.completedUserCount);
  915. let xAxis = data.map(e=>e.x);
  916. viewCharOption.series[0].data = watchUserCountList;
  917. viewCharOption.series[1].data = completedUserCountList;
  918. viewCharOption.xAxis.data = xAxis;
  919. this.viewerChart.setOption(viewCharOption);
  920. }
  921. })
  922. },
  923. initViewerChart() {
  924. this.viewerChart = echarts.init(this.$refs.viewerChart)
  925. this.viewerChart.setOption(viewCharOption)
  926. },
  927. initDealerChart() {
  928. this.dealerChart = echarts.init(this.$refs.dealerChart)
  929. this.dealerChart.setOption(dealerOption)
  930. },
  931. initCourseWatchChart() {
  932. this.courseWatchChart = echarts.init(this.$refs.courseWatchChart)
  933. this.courseWatchChart.setOption(courseWatchOption)
  934. },
  935. initAnswerRedPackViewerChart(){
  936. this.answerRedPackViewerChart = echarts.init(this.$refs.answerRedPackViewerChart)
  937. this.answerRedPackViewerChart.setOption(redPackageOption)
  938. },
  939. initAnswerRedPackMoneyViewerChart(){
  940. this.answerRedPackMoneyViewerChart = echarts.init(this.$refs.answerRedPackMoneyViewerChart)
  941. this.answerRedPackMoneyViewerChart.setOption(lineChartOption)
  942. }
  943. },
  944. beforeDestroy() {
  945. // 组件销毁时清除定时器
  946. if (this.timer) {
  947. clearInterval(this.timer);
  948. this.timer = null;
  949. }
  950. // window.removeEventListener('resize', this.resizeHandler)
  951. this.viewerChart && this.viewerChart.dispose()
  952. this.dealerChart && this.dealerChart.dispose()
  953. }
  954. }
  955. </script>
  956. <style scoped>
  957. .action-group .el-button + .el-button,
  958. .action-group .el-dropdown {
  959. margin-left: 10px;
  960. }
  961. .is-active {
  962. color: #409EFF;
  963. font-weight: bold;
  964. }
  965. ::v-deep .el-radio-button__inner:hover {
  966. color: #409EFF; /* 鼠标悬浮时的文字颜色,可以根据需要调整 */
  967. }
  968. ::v-deep .el-radio-button.is-active .el-radio-button__inner {
  969. background-color: #409EFF; /* 选中时的背景色 */
  970. border-color: #409EFF; /* 选中时的边框色 */
  971. color: #FFFFFF; /* 选中时的文字颜色 (通常是白色) */
  972. box-shadow: -1px 0 0 0 #409EFF; /* 处理按钮间的连接缝隙 */
  973. }
  974. /* 如果需要,也可以修改非选中状态下的聚焦(focus)或悬浮(hover)样式 */
  975. /* 例如,让非选中按钮悬浮时边框和文字也变蓝 */
  976. ::v-deep .el-radio-button:not(.is-active) .el-radio-button__inner:hover {
  977. color: #409EFF;
  978. /* border-color: #b3d8ff; Element UI 默认悬浮边框色,可以按需修改 */
  979. }
  980. /* 聚焦时的外框,如果需要的话 */
  981. ::v-deep .el-radio-button:focus:not(.is-checked) .el-radio-button__inner {
  982. /* border-color: #409EFF; */ /* Element UI 默认的 focus 颜色通常关联主题色 */
  983. /* box-shadow: 0 0 2px 2px rgba(64, 158, 255, 0.2); */ /* 示例 focus 光晕 */
  984. }
  985. .statistics-dashboard {
  986. padding: 20px;
  987. background-color: #f5f7fa;
  988. }
  989. .overview-section,
  990. .analysis-section {
  991. margin-bottom: 20px;
  992. border-radius: 4px;
  993. }
  994. .header {
  995. display: flex;
  996. justify-content: space-between;
  997. align-items: center;
  998. font-size: 16px;
  999. font-weight: 500;
  1000. }
  1001. .data-card {
  1002. background-color: #fff;
  1003. border-radius: 4px;
  1004. padding: 15px;
  1005. height: 100px;
  1006. display: flex;
  1007. flex-direction: column;
  1008. position: relative;
  1009. }
  1010. .card-title {
  1011. color: #606266;
  1012. font-size: 14px;
  1013. margin-bottom: 10px;
  1014. }
  1015. .card-value {
  1016. font-size: 24px;
  1017. font-weight: bold;
  1018. margin-top: auto;
  1019. }
  1020. .highlight {
  1021. color: #409EFF;
  1022. }
  1023. .card-sub {
  1024. display: flex;
  1025. justify-content: space-between;
  1026. font-size: 12px;
  1027. color: #909399;
  1028. margin-top: 5px;
  1029. }
  1030. .card-desc {
  1031. font-size: 12px;
  1032. color: #909399;
  1033. margin-top: 5px;
  1034. }
  1035. .card-badge {
  1036. position: absolute;
  1037. top: 15px;
  1038. right: 15px;
  1039. background: #f0f9eb;
  1040. color: #67c23a;
  1041. padding: 2px 5px;
  1042. border-radius: 4px;
  1043. }
  1044. .cdn-label {
  1045. background-color: #409EFF;
  1046. color: white;
  1047. padding: 2px 5px;
  1048. border-radius: 4px;
  1049. margin-right: 5px;
  1050. font-size: 12px;
  1051. }
  1052. .tab-group {
  1053. display: flex;
  1054. gap: 10px;
  1055. }
  1056. .action-group {
  1057. display: flex;
  1058. gap: 10px;
  1059. }
  1060. .analysis-card {
  1061. border-radius: 4px;
  1062. padding: 20px;
  1063. display: flex;
  1064. }
  1065. .card-icon {
  1066. width: 50px;
  1067. height: 50px;
  1068. background-color: rgba(64, 158, 255, 0.1);
  1069. border-radius: 8px;
  1070. display: flex;
  1071. justify-content: center;
  1072. align-items: center;
  1073. font-size: 24px;
  1074. color: #409EFF;
  1075. margin-right: 20px;
  1076. }
  1077. .card-content {
  1078. display: flex;
  1079. }
  1080. .card-row {
  1081. display: flex;
  1082. justify-content: center;
  1083. justify-items: center;
  1084. flex-direction: column;
  1085. padding: 10px;
  1086. .highlight{
  1087. text-align: center;
  1088. margin-top: 1em;
  1089. font-family: BebasNeue;
  1090. color: #1677ff;
  1091. font-size: 26px;
  1092. line-height: 42px;
  1093. font-weight: 400;
  1094. margin-top: 8px;
  1095. }
  1096. font-size: 15px;
  1097. color: #000;
  1098. }
  1099. .charts-section {
  1100. margin-top: 20px;
  1101. }
  1102. .chart-header {
  1103. display: flex;
  1104. justify-content: space-between;
  1105. align-items: center;
  1106. }
  1107. .view-more {
  1108. font-size: 12px;
  1109. }
  1110. .legend {
  1111. display: flex;
  1112. gap: 15px;
  1113. }
  1114. .legend-item {
  1115. display: flex;
  1116. align-items: center;
  1117. font-size: 12px;
  1118. }
  1119. .dot {
  1120. width: 10px;
  1121. height: 10px;
  1122. border-radius: 50%;
  1123. margin-right: 5px;
  1124. }
  1125. .viewer-dot {
  1126. background-color: #409EFF;
  1127. }
  1128. .complete-dot {
  1129. background-color: #67C23A;
  1130. }
  1131. .chart-container {
  1132. height: 350px;
  1133. width: 100%;
  1134. }
  1135. .analysis-card-check{
  1136. display: flex;
  1137. flex-direction: row;
  1138. border: 1px solid transparent;
  1139. background-color: #fff;
  1140. border-radius: 4px;
  1141. }
  1142. .analysis-card-check:hover{
  1143. cursor: pointer;
  1144. }
  1145. .analysis-card-check-selected:after{
  1146. content: "";
  1147. display: block;
  1148. border-width: 15px;
  1149. position: absolute;
  1150. bottom: -30px;
  1151. left: 50%;
  1152. margin-left: -32px;
  1153. border-style: solid dashed dashed solid;
  1154. border-color: #4592FF transparent transparent transparent;
  1155. font-size: 0;
  1156. line-height: 0;
  1157. z-index:1;
  1158. }
  1159. .analysis-card-check-selected:before{
  1160. content: "";
  1161. display: block;
  1162. border-width: 15px;
  1163. position: absolute;
  1164. bottom: -30px;
  1165. left: 50%;
  1166. margin-left: -32px;
  1167. border-style: solid dashed dashed solid;
  1168. border-color: #4592FF transparent transparent transparent;
  1169. font-size: 0;
  1170. line-height: 0;
  1171. z-index:1;
  1172. }
  1173. .analysis-card-check-selected{
  1174. border: 1px solid #4592FF;
  1175. background-color: #e7f1ff;
  1176. }
  1177. .color{
  1178. position: relative;
  1179. border: 1px solid #4592FF;
  1180. background-color: #e7f1ff;
  1181. }
  1182. .color:after {
  1183. bottom: -27px;
  1184. border-color: #E7F1FF transparent transparent transparent;
  1185. }
  1186. .legend-group{
  1187. }
  1188. </style>