data-visual.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <template>
  2. <view class="app-page flex flex-col pa-24" :class="[uniHaloPluginPageClass]">
  3. <PluginUnavailable v-if="!uniHaloPluginAvailable" :pluginId="uniHaloPluginId" :error-text="uniHaloPluginAvailableError" />
  4. <template v-else>
  5. <!-- 加载区域 -->
  6. <view v-if="loading == 'loading'" class="loading-wrap pa-24">
  7. <tm-skeleton model="listAvatr"></tm-skeleton>
  8. <tm-skeleton model="listAvatr"></tm-skeleton>
  9. <tm-skeleton model="listAvatr"></tm-skeleton>
  10. <tm-skeleton model="listAvatr"></tm-skeleton>
  11. </view>
  12. <view v-else-if="loading == 'error'" class="content-empty flex flex-center">
  13. <tm-empty icon="icon-wind-cry" label="加载异常"></tm-empty>
  14. </view>
  15. <!-- 内容区域 -->
  16. <view v-else class="content flex flex-col uh-gap-y-12">
  17. <!-- 标签统计 -->
  18. <view class="card bg-white pa-24 round-4">
  19. <view class="card-head flex items-center justify-between">
  20. <view class="card-head_title flex items-end uh-gap-x-4">
  21. <text class="card-head_text">标签统计</text>
  22. <text class="card-head_subtext">(全部标签的文章数量占比)</text>
  23. </view>
  24. <view @click="tagChart.isExpand = !tagChart.isExpand">
  25. <tm-icons v-if="tagChart.isExpand" :size="24" name="icon-angle-down" color="gray"></tm-icons>
  26. <tm-icons v-else :size="24" name="icon-angle-up" color="gray"></tm-icons>
  27. </view>
  28. </view>
  29. <view v-show="tagChart.isExpand" class="card-body flex">
  30. <view class="chart-box">
  31. <qiun-data-charts :type="tagChart.type" :opts="tagChart.opts" :chartData="tagChart.data" :tooltipFormat="tagChart.tooltipFormat" />
  32. </view>
  33. </view>
  34. </view>
  35. <!-- 分类统计 -->
  36. <view class="card bg-white pa-24 round-4">
  37. <view class="card-head flex items-center justify-between">
  38. <view class="card-head_title flex items-end uh-gap-x-4">
  39. <text class="card-head_text">分类统计</text>
  40. <text class="card-head_subtext">(全部分类的文章数量占比)</text>
  41. </view>
  42. <view @click="categoryChart.isExpand = !categoryChart.isExpand">
  43. <tm-icons v-if="categoryChart.isExpand" :size="24" name="icon-angle-down" color="gray"></tm-icons>
  44. <tm-icons v-else :size="24" name="icon-angle-up" color="gray"></tm-icons>
  45. </view>
  46. </view>
  47. <view v-show="categoryChart.isExpand" class="card-body flex">
  48. <view class="chart-box">
  49. <qiun-data-charts
  50. :canvasId="categoryChart.id"
  51. :canvas2d="true"
  52. :ontouch="true"
  53. :type="categoryChart.type"
  54. :opts="categoryChart.opts"
  55. :chartData="categoryChart.data"
  56. :tooltipFormat="categoryChart.tooltipFormat"
  57. />
  58. </view>
  59. </view>
  60. </view>
  61. <!-- 文章发布趋势 -->
  62. <view class="card bg-white pa-24 round-4">
  63. <view class="card-head flex items-center justify-between">
  64. <view class="card-head_title flex items-end uh-gap-x-4">
  65. <text class="card-head_text">文章发布趋势</text>
  66. <text class="card-head_subtext">(按日期统计文章发布数量)</text>
  67. </view>
  68. <view @click="trandArticleChart.isExpand = !trandArticleChart.isExpand">
  69. <tm-icons v-if="trandArticleChart.isExpand" :size="24" name="icon-angle-down" color="gray"></tm-icons>
  70. <tm-icons v-else :size="24" name="icon-angle-up" color="gray"></tm-icons>
  71. </view>
  72. </view>
  73. <view v-show="trandArticleChart.isExpand" class="card-body flex">
  74. <heatmap style="width: 100%" :chartData="trandArticleChart.data"></heatmap>
  75. </view>
  76. </view>
  77. <!-- 评论活跃用户 -->
  78. <view class="card bg-white pa-24 round-4">
  79. <view class="card-head flex items-center justify-between">
  80. <view class="card-head_title flex items-end uh-gap-x-4">
  81. <text class="card-head_text">评论活跃用户</text>
  82. <text class="card-head_subtext">(按评论作者统计评论数量)</text>
  83. </view>
  84. <view @click="userCommentsChart.isExpand = !userCommentsChart.isExpand">
  85. <tm-icons v-if="userCommentsChart.isExpand" :size="24" name="icon-angle-down" color="gray"></tm-icons>
  86. <tm-icons v-else :size="24" name="icon-angle-up" color="gray"></tm-icons>
  87. </view>
  88. </view>
  89. <view v-show="userCommentsChart.isExpand" class="card-body flex">
  90. <view class="chart-box">
  91. <qiun-data-charts
  92. :canvasId="userCommentsChart.id"
  93. :canvas2d="true"
  94. :ontouch="true"
  95. :type="userCommentsChart.type"
  96. :opts="userCommentsChart.opts"
  97. :chartData="userCommentsChart.data"
  98. :tooltipFormat="userCommentsChart.tooltipFormat"
  99. />
  100. </view>
  101. </view>
  102. </view>
  103. <!-- 热门文章 Top10 -->
  104. <view class="card bg-white pa-24 round-4">
  105. <view class="card-head flex items-center justify-between">
  106. <view class="card-head_title flex items-end uh-gap-x-4">
  107. <text class="card-head_text">热门文章前10</text>
  108. <text class="card-head_subtext">(按访问量排序的热门文章)</text>
  109. </view>
  110. <view @click="top10ArticlesChart.isExpand = !top10ArticlesChart.isExpand">
  111. <tm-icons v-if="top10ArticlesChart.isExpand" :size="24" name="icon-angle-down" color="gray"></tm-icons>
  112. <tm-icons v-else :size="24" name="icon-angle-up" color="gray"></tm-icons>
  113. </view>
  114. </view>
  115. <view class="card-body flex">
  116. <view class="chart-box">
  117. <qiun-data-charts
  118. :canvasId="top10ArticlesChart.id"
  119. :canvas2d="true"
  120. :ontouch="true"
  121. :type="top10ArticlesChart.type"
  122. :opts="top10ArticlesChart.opts"
  123. :chartData="top10ArticlesChart.data"
  124. :tooltipFormat="top10ArticlesChart.tooltipFormat"
  125. />
  126. </view>
  127. </view>
  128. </view>
  129. </view>
  130. </template>
  131. </view>
  132. </template>
  133. <script>
  134. import dataStatisticsApi from '@/api/v2/plugin.data-statistics.js';
  135. import { NeedPluginIds, NeedPlugins, checkNeedPluginAvailable } from '@/utils/plugin.js';
  136. import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
  137. import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
  138. import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
  139. import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
  140. import tmIcons from '@/tm-vuetify/components/tm-icons/tm-icons.vue';
  141. import pluginAvailableMixin from '@/common/mixins/pluginAvailable.js';
  142. import PluginUnavailable from '@/components/plugin-unavailable/plugin-unavailable.vue';
  143. import heatmap from '@/components/heatmap/heatmap.vue';
  144. export default {
  145. mixins: [pluginAvailableMixin],
  146. name: 'DataVisual',
  147. components: {
  148. tmSkeleton,
  149. tmTranslate,
  150. tmEmpty,
  151. tmButton,
  152. tmIcons,
  153. heatmap,
  154. PluginUnavailable
  155. },
  156. data() {
  157. return {
  158. loading: 'loading',
  159. statistics: null,
  160. tagChart: {
  161. id: 'tagChart',
  162. isExpand: true,
  163. type: 'ring',
  164. data: {},
  165. tooltipFormat: 'tooltipTag',
  166. opts: {
  167. rotate: false,
  168. rotateLock: false,
  169. color: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316', '#ea7ccc', '#0EA5E9'],
  170. padding: [5, 5, 5, 5],
  171. dataLabel: false,
  172. legend: {
  173. show: false,
  174. position: 'right',
  175. lineHeight: 18,
  176. fontSize: 12
  177. },
  178. title: {
  179. name: '',
  180. fontSize: 16,
  181. color: '#666666'
  182. },
  183. subtitle: {
  184. name: '',
  185. fontSize: 12,
  186. color: '#eee'
  187. },
  188. extra: {
  189. ring: {
  190. ringWidth: 36,
  191. activeOpacity: 0.5,
  192. activeRadius: 10,
  193. offsetAngle: -90,
  194. labelWidth: 15,
  195. border: true,
  196. borderWidth: 1,
  197. borderColor: '#FFFFFF'
  198. },
  199. tooltip: {
  200. legendShape: 'circle',
  201. fontSize: 11
  202. }
  203. }
  204. }
  205. },
  206. categoryChart: {
  207. id: 'categoryChart',
  208. isExpand: true,
  209. type: 'line',
  210. data: {},
  211. tooltipFormat: 'tooltipCategory',
  212. opts: {
  213. color: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316', '#ea7ccc', '#0EA5E9'],
  214. padding: [20, 15, 10, 15],
  215. touchMoveLimit: 24,
  216. enableScroll: true,
  217. legend: {
  218. show: false
  219. },
  220. xAxis: {
  221. disableGrid: true,
  222. fontSize: 11,
  223. scrollShow: true,
  224. itemCount: 6,
  225. boundaryGap: 'center',
  226. format: 'xAxisCategory'
  227. },
  228. yAxis: {
  229. gridType: 'dash',
  230. dashLength: 4,
  231. data: [
  232. {
  233. fontSize: 11
  234. }
  235. ]
  236. },
  237. extra: {
  238. line: {
  239. type: 'curve',
  240. width: 2,
  241. activeType: 'hollow'
  242. },
  243. tooltip: {
  244. legendShape: 'circle',
  245. fontSize: 11
  246. }
  247. }
  248. }
  249. },
  250. trandArticleChart: {
  251. isExpand: true,
  252. type: 'hotmap',
  253. data: [],
  254. opts: {}
  255. },
  256. userCommentsChart: {
  257. id: 'userCommentsChart',
  258. isExpand: true,
  259. loading: true,
  260. type: 'column',
  261. data: {},
  262. tooltipFormat: 'tooltipUserComments',
  263. opts: {
  264. color: ['#EF4444', '#F59E0B', '#14B8A6', '#3B82F6', '#10B981', '#8B5CF6', '#EC4899', '#F97316', '#ea7ccc', '#0EA5E9'],
  265. padding: [20, 15, 10, 10],
  266. touchMoveLimit: 24,
  267. enableScroll: true,
  268. legend: {
  269. show: false
  270. },
  271. xAxis: {
  272. disableGrid: true,
  273. boundaryGap: 'justify',
  274. fontSize: 10,
  275. scrollShow: true,
  276. itemCount: 5,
  277. format: 'xAxisUserComments'
  278. },
  279. yAxis: {
  280. gridType: 'dash',
  281. dashLength: 4,
  282. data: [
  283. {
  284. min: 0,
  285. fontSize: 11
  286. }
  287. ]
  288. },
  289. extra: {
  290. column: {
  291. type: 'group',
  292. width: 22,
  293. activeBgColor: '#000000',
  294. activeBgOpacity: 0.08,
  295. linearType: 'custom',
  296. seriesGap: 5,
  297. linearOpacity: 0.7,
  298. barBorderCircle: true,
  299. customColor: ['#F59E0B']
  300. },
  301. tooltip: {
  302. legendShape: 'circle',
  303. fontSize: 11
  304. }
  305. }
  306. }
  307. },
  308. top10ArticlesChart: {
  309. id: 'top10ArticlesChart',
  310. isExpand: true,
  311. type: 'column',
  312. data: {},
  313. tooltipFormat: 'tooltipTop10Articles',
  314. opts: {
  315. color: ['#EF4444', '#F59E0B', '#14B8A6', '#3B82F6', '#10B981', '#8B5CF6', '#EC4899', '#F97316', '#ea7ccc', '#0EA5E9'],
  316. padding: [20, 15, 10, 10],
  317. touchMoveLimit: 24,
  318. enableScroll: true,
  319. legend: {
  320. show: false
  321. },
  322. xAxis: {
  323. disableGrid: true,
  324. boundaryGap: 'justify',
  325. fontSize: 10,
  326. scrollShow: true,
  327. itemCount: 5,
  328. format: 'xAxisTop10Article'
  329. },
  330. yAxis: {
  331. gridType: 'dash',
  332. dashLength: 4,
  333. data: [
  334. {
  335. min: 0,
  336. fontSize: 11
  337. }
  338. ]
  339. },
  340. extra: {
  341. column: {
  342. type: 'group',
  343. width: 22,
  344. activeBgColor: '#000000',
  345. activeBgOpacity: 0.08,
  346. linearType: 'custom',
  347. seriesGap: 5,
  348. linearOpacity: 0.7,
  349. barBorderCircle: true,
  350. customColor: ['#F59E0B']
  351. },
  352. tooltip: {
  353. legendShape: 'circle',
  354. fontSize: 11
  355. }
  356. }
  357. }
  358. }
  359. };
  360. },
  361. async onReady() {
  362. // 检查插件
  363. this.setPluginId(this.NeedPluginIds.PluginDataStatistics);
  364. this.setPluginError('阿偶,检测到当前插件没有安装或者启用,无法使用功能哦,请联系管理员');
  365. if (!(await this.checkPluginAvailable())) return;
  366. this.fnGetData();
  367. },
  368. onPullDownRefresh() {
  369. if (!this.uniHaloPluginAvailable) {
  370. uni.hideLoading();
  371. uni.stopPullDownRefresh();
  372. return;
  373. }
  374. this.fnGetData();
  375. },
  376. methods: {
  377. fnGetData() {
  378. uni.showLoading({
  379. mask: true,
  380. title: '加载中...'
  381. });
  382. // 设置状态为加载中
  383. if (!this.isLoadMore) {
  384. this.loading = 'loading';
  385. }
  386. this.loadMoreText = '加载中...';
  387. dataStatisticsApi
  388. .getChartData()
  389. .then((res) => {
  390. console.log('获取到统计数据:', res);
  391. this.statistics = res;
  392. this.handleTagChart();
  393. this.handleCategoriesChart();
  394. this.handleTrendArticlesChart();
  395. this.handleUserCommentsChart();
  396. this.handleTop10ArticlesChart();
  397. this.loading = 'success';
  398. })
  399. .catch((err) => {
  400. console.error(err);
  401. this.loading = 'error';
  402. this.loadMoreText = '加载失败,请下拉刷新!';
  403. })
  404. .finally(() => {
  405. setTimeout(() => {
  406. uni.hideLoading();
  407. uni.stopPullDownRefresh();
  408. }, 100);
  409. });
  410. },
  411. // 处理标签统计
  412. handleTagChart() {
  413. const data = this.statistics.tags.sort((a, b) => b.count - a.count);
  414. this.tagChart.data = {
  415. series: [
  416. {
  417. data: data.map((item) => {
  418. return {
  419. name: item.name,
  420. value: item.count
  421. };
  422. })
  423. }
  424. ]
  425. };
  426. },
  427. // 处理分类统计
  428. handleCategoriesChart() {
  429. const data = this.statistics.categories.sort((a, b) => b.total - a.total);
  430. const seriesItemData = data.map((item) => item.total);
  431. if (Math.max(...seriesItemData) < 10) {
  432. this.categoryChart.opts.yAxis.data[0].max = 10;
  433. }
  434. this.categoryChart.data = {
  435. categories: data.map((item) => item.name),
  436. series: [
  437. {
  438. name: '分类',
  439. data: seriesItemData
  440. }
  441. ]
  442. };
  443. },
  444. // 处理文章趋势
  445. handleTrendArticlesChart() {
  446. this.trandArticleChart.data = this.statistics.articles;
  447. },
  448. // 处理评论活跃用户
  449. handleUserCommentsChart() {
  450. this.userCommentsChart.loading = true;
  451. const data = this.statistics.comments.sort((a, b) => b.count - a.count).slice(0, 10);
  452. const seriesItemData = data.map((item) => item.count);
  453. if (Math.max(...seriesItemData) < 10) {
  454. this.userCommentsChart.opts.yAxis.data[0].max = 10;
  455. }
  456. this.userCommentsChart.data = {
  457. categories: data.map((item) => item.username),
  458. series: [
  459. {
  460. name: '评论',
  461. data: seriesItemData
  462. }
  463. ]
  464. };
  465. this.userCommentsChart.loading = false;
  466. },
  467. // 处理热门文章TOP10
  468. handleTop10ArticlesChart() {
  469. const data = this.statistics.top10Articles.sort((a, b) => b.views - a.views).slice(0, 10);
  470. const seriesItemData = data.map((item) => item.views);
  471. if (Math.max(...seriesItemData) < 10) {
  472. this.top10ArticlesChart.opts.yAxis.data[0].max = 10;
  473. }
  474. this.top10ArticlesChart.data = {
  475. categories: data.map((item) => item.name),
  476. series: [
  477. {
  478. name: '评论',
  479. data: seriesItemData
  480. }
  481. ]
  482. };
  483. }
  484. }
  485. };
  486. </script>
  487. <style lang="scss" scoped>
  488. .app-page {
  489. box-sizing: border-box;
  490. width: 100vw;
  491. min-height: 100vh;
  492. color: #353437;
  493. }
  494. .card {
  495. box-sizing: border-box;
  496. background-color: rgba(255, 255, 255, 0.95);
  497. box-shadow: 0 0 12rpx rgba(226, 232, 240, 0.35);
  498. backdrop-filter: blur(6rpx);
  499. // border: 2rpx solid #e8edf4;
  500. }
  501. .card-head {
  502. font-size: 32rpx;
  503. font-weight: bold;
  504. &_title {
  505. }
  506. &_text {
  507. box-sizing: border-box;
  508. position: relative;
  509. padding-left: 24rpx;
  510. font-size: 30rpx;
  511. &:before {
  512. content: '';
  513. position: absolute;
  514. left: 0;
  515. top: 50%;
  516. transform: translateY(-50%);
  517. width: 8rpx;
  518. height: 70%;
  519. background-color: #03a9f4;
  520. border-radius: 12rpx;
  521. }
  522. }
  523. &_subtext {
  524. font-size: 26rpx;
  525. font-weight: normal;
  526. color: #6b7280;
  527. }
  528. }
  529. .card-body {
  530. box-sizing: border-box;
  531. margin-top: 24rpx;
  532. width: 100%;
  533. overflow: hidden;
  534. background-color: #fcfdfe;
  535. border: 2rpx solid #e9eef3;
  536. border-radius: 12rpx;
  537. }
  538. .chart-box {
  539. width: 100%;
  540. height: 320rpx;
  541. }
  542. </style>