index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. <template>
  2. <div class="layout-padding">
  3. <div class="!overflow-auto px-1">
  4. <div class="el-card p-2">
  5. <div class="flex justify-between">
  6. <Title :title="t('versionDistribution.analytics')" />
  7. <div class="">
  8. <el-button type="primary">{{ t('versionDistribution.aijb') }}</el-button>
  9. </div>
  10. </div>
  11. <div>
  12. <el-row shadow="hover" class="ml10 mt-2">
  13. <el-form :inline="true" :model="formData" @keyup.enter="query" ref="queryRef">
  14. <el-form-item>
  15. <el-date-picker v-model="formData.time" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" />
  16. </el-form-item>
  17. <el-form-item>
  18. <el-select v-model="selectedChannelCompare" class="w-[140px]" placeholder="全部版本">
  19. <el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
  20. </el-select>
  21. </el-form-item>
  22. </el-form>
  23. </el-row>
  24. </div>
  25. </div>
  26. <div class="mt-2 el-card p-2">
  27. <Title left-line :title="selectedChannelCompare === ''? t('versionDistribution.allVersion') : selectedChannelCompare+t('versionDistribution.version')" >
  28. <template #default>
  29. <el-popover class="box-item" placement="right" trigger="hover" width="300">
  30. <template #reference>
  31. <el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
  32. </template>
  33. <template #default>
  34. <div class="ant-popover-inner-content">
  35. <div class="um-page-tips-content" style="line-height: 24px">
  36. <p><span>趋势图展示累计用户排名Top10版本的变化趋势</span></p>
  37. <p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
  38. <p>
  39. <span class="highlight">活跃用户:</span
  40. ><span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
  41. </p>
  42. <p>
  43. <span class="highlight">启动次数:</span
  44. ><span
  45. >打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span
  46. >
  47. </p>
  48. <p>
  49. <span class="highlight">版本累计用户(%):</span
  50. ><span>截止到现在,该版本的累计用户(占累计用户全体的比例);若该版本的用户升级到其他版本,则累计用户会减少</span>
  51. </p>
  52. <p><span class="highlight">升级用户:</span><span>从其他版本升级到该版本的用户(以设备为判断标准)</span></p>
  53. <p>
  54. <span
  55. >如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span
  56. >
  57. </p>
  58. </div>
  59. </div>
  60. </template>
  61. </el-popover>
  62. </template>
  63. </Title>
  64. <div class="">
  65. <div class="flex items-center justify-between mb-2 mt-3">
  66. <div>
  67. <el-select v-if="selectedChannelCompare !== ''" v-model="selectedChannelCompare" class="w-[140px] ml-2" style="width: 140px" placeholder="全部频道">
  68. <el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
  69. </el-select>
  70. <el-select v-model="selectedChannelCompare" class="w-[140px] ml-2" style="width: 140px" placeholder="版本对比">
  71. <el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
  72. </el-select>
  73. <el-button type="primary" class="ml-2">{{ t('versionDistribution.version') }}</el-button>
  74. </div>
  75. <div class="flex items-center">
  76. <el-radio-group v-model="timeGranularity">
  77. <el-radio-button label="hour">新增用户</el-radio-button>
  78. <el-radio-button label="day">活跃用户</el-radio-button>
  79. <el-radio-button label="week">启动次数</el-radio-button>
  80. <el-radio-button v-if="selectedChannelCompare !== ''" label="sjcs">升级用户</el-radio-button>
  81. </el-radio-group>
  82. </div>
  83. </div>
  84. <div class="relative">
  85. <div ref="lineChartRef" style="width: 100%; height: 320px"></div>
  86. </div>
  87. </div>
  88. <!-- 明细表格 -->
  89. <div class="mt-3">
  90. <div class="flex items-center justify-between mb-2">
  91. <div class="flex">
  92. <div v-if="selectedChannelCompare == ''" class="flex items-center">
  93. <el-radio-group v-model="timeGranularity">
  94. <el-radio-button label="hour">今日</el-radio-button>
  95. <el-radio-button label="day">作日 </el-radio-button>
  96. </el-radio-group>
  97. </div>
  98. <div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
  99. {{ showDetail1 ? '收起明细数据' : '展开明细数据' }}
  100. <el-icon class="ml-2"><ArrowDown v-if="showDetail1" /> <ArrowUp v-else /> </el-icon>
  101. </div>
  102. </div>
  103. <div>
  104. <el-button>导出</el-button>
  105. </div>
  106. </div>
  107. <el-table v-if="showDetail1" :data="pagedTableRows" border>
  108. <el-table-column prop="date" label="日期" align="center" min-width="140" />
  109. <el-table-column prop="hyyh" label="启动次数" align="center" min-width="140" />
  110. <el-table-column prop="ratio" label="启动次数(占比)" align="center" min-width="220"> </el-table-column>
  111. </el-table>
  112. <div v-if="showDetail1" class="flex justify-end mt-2">
  113. <el-pagination
  114. v-model:current-page="currentPage"
  115. v-model:page-size="pageSize"
  116. background
  117. layout="total, prev, pager, next, sizes"
  118. :total="tableRows.length"
  119. :page-sizes="[5, 10, 20]"
  120. />
  121. </div>
  122. </div>
  123. </div>
  124. <div v-if="selectedChannelCompare !== ''" class="mt-2 el-card p-2">
  125. <div class="flex justify-between">
  126. <Title left-line :title="'版本用户来源'">
  127. <template #default>
  128. <el-popover class="box-item" placement="right" trigger="hover" width="300">
  129. <template #reference>
  130. <el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
  131. </template>
  132. <template #default>
  133. <div class="ant-popover-inner-content">
  134. <div class="um-page-tips-content" style="line-height: 24px">
  135. <p><span class="highlight">版本用户来源:</span><span>展示各版本用户的主要获取渠道分布</span></p>
  136. <p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
  137. <p><span class="highlight">升级用户比例:</span><span>从其他版本升级到该版本的用户占比</span></p>
  138. </div>
  139. </div>
  140. </template>
  141. </el-popover>
  142. </template>
  143. </Title>
  144. </div>
  145. <div class="flex items-center justify-between mt-3">
  146. <el-select v-model="channelDistribution" style="width: 180px" placeholder="新增用户渠道分布">
  147. <el-option label="新增用户渠道分布" value="distribution" />
  148. </el-select>
  149. <div class="ml-4">
  150. <el-radio-group v-model="timeRange">
  151. <el-radio-button label="yesterday">昨天</el-radio-button>
  152. <el-radio-button label="7days">过去7天</el-radio-button>
  153. <el-radio-button label="30days">过去30天</el-radio-button>
  154. </el-radio-group>
  155. </div>
  156. </div>
  157. <!-- 水平柱状图 -->
  158. <div class="mt-4">
  159. <div class="relative">
  160. <div ref="barChartRef" style="width: 100%; height: 200px"></div>
  161. </div>
  162. </div>
  163. <!-- 明细表格 -->
  164. <div class="mt-4">
  165. <div class="flex items-center justify-between mb-2">
  166. <div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail2 = !showDetail2">
  167. {{ showDetail2 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown v-if="showDetail2" /> <ArrowUp v-else /> </el-icon>
  168. </div>
  169. <div>
  170. <el-button>导出</el-button>
  171. </div>
  172. </div>
  173. <el-table v-if="showDetail2" :data="pagedSourceRows" border>
  174. <el-table-column prop="channel" label="渠道" min-width="140" />
  175. <el-table-column prop="newUsers" label="新增用户" min-width="140" />
  176. <el-table-column prop="upgradeRatio" label="升级用户比例" min-width="140" />
  177. </el-table>
  178. <div v-if="showDetail2" class="flex justify-end mt-3">
  179. <el-pagination
  180. v-model:current-page="sourcePage"
  181. v-model:page-size="sourcePageSize"
  182. background
  183. layout="total, prev, pager, next, sizes"
  184. :total="sourceRows.length"
  185. :page-sizes="[5, 10, 20]"
  186. />
  187. </div>
  188. </div>
  189. </div>
  190. </div>
  191. </div>
  192. </template>
  193. <script setup lang="ts">
  194. import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
  195. import * as echarts from 'echarts';
  196. import { useI18n } from 'vue-i18n';
  197. import { QuestionFilled } from '@element-plus/icons-vue';
  198. const { t } = useI18n();
  199. interface TableRow {
  200. date: string;
  201. newUsers: number;
  202. ratio: string;
  203. }
  204. const formData = ref<Record<string, any>>({});
  205. const query = () => {
  206. console.log(formData.value);
  207. };
  208. const selectedChannelCompare = ref('');
  209. const channelCompareOptions = [
  210. { label: '全部版本', value: '' },
  211. { label: '1.0', value: '1.0' },
  212. { label: '2.0', value: '2.0' },
  213. ];
  214. // 图表相关
  215. const timeGranularity = ref<'hour' | 'day' | 'week' | 'month'>('week');
  216. const lineChartRef = ref<HTMLDivElement | null>(null);
  217. let chartInstance: echarts.ECharts | null = null;
  218. const lineChartData = ref<Array<{ x: string; value: number }>>([
  219. { x: '2025-07-01', value: 900 },
  220. { x: '2025-07-08', value: 1000 },
  221. { x: '2025-07-15', value: 1100 },
  222. { x: '2025-07-22', value: 1000 },
  223. { x: '2025-07-29', value: 600 },
  224. { x: '2025-08-05', value: 300 },
  225. { x: '2025-08-12', value: 250 },
  226. { x: '2025-08-19', value: 200 },
  227. { x: '2025-08-26', value: 650 },
  228. { x: '2025-09-02', value: 950 },
  229. { x: '2025-09-09', value: 900 },
  230. { x: '2025-09-16', value: 120 },
  231. ]);
  232. function initLineChart(): void {
  233. if (!lineChartRef.value) return;
  234. if (chartInstance) chartInstance.dispose();
  235. chartInstance = echarts.init(lineChartRef.value);
  236. const option: echarts.EChartsOption = {
  237. tooltip: { trigger: 'axis' },
  238. grid: { left: 40, right: 20, top: 20, bottom: 30 },
  239. xAxis: {
  240. type: 'category',
  241. data: lineChartData.value.map((d) => d.x),
  242. axisLine: { lineStyle: { color: '#e5e7eb' } },
  243. axisLabel: { color: '#6b7280' },
  244. axisTick: { alignWithLabel: true },
  245. },
  246. yAxis: {
  247. type: 'value',
  248. axisLine: { show: false },
  249. splitLine: { lineStyle: { color: '#f3f4f6' } },
  250. axisLabel: { color: '#6b7280' },
  251. },
  252. series: [
  253. {
  254. name: '新增人数',
  255. type: 'line',
  256. smooth: true,
  257. showSymbol: true,
  258. symbolSize: 6,
  259. itemStyle: { color: '#409EFF' },
  260. lineStyle: { color: '#409EFF' },
  261. data: lineChartData.value.map((d) => d.value),
  262. },
  263. ],
  264. };
  265. chartInstance.setOption(option);
  266. }
  267. onMounted(() => {
  268. initLineChart();
  269. initBarChart();
  270. });
  271. watch(timeGranularity, () => {
  272. // 静态页面:仅重新渲染
  273. initLineChart();
  274. });
  275. // 表格相关(静态数据)
  276. const tableRows = ref<TableRow[]>(
  277. Array.from({ length: 42 }).map((_, idx) => ({
  278. date: `2025-08-${String(11).padStart(2, '0')}`,
  279. newUsers: 727,
  280. hyyh: '115',
  281. ratio: '97.45%',
  282. }))
  283. );
  284. const currentPage = ref(1);
  285. const pageSize = ref(5);
  286. const pagedTableRows = computed(() => {
  287. const startIndex = (currentPage.value - 1) * pageSize.value;
  288. return tableRows.value.slice(startIndex, startIndex + pageSize.value);
  289. });
  290. const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
  291. // 展开/收起明细
  292. const showDetail1 = ref(true);
  293. // 版本用户来源相关
  294. const channelDistribution = ref('distribution');
  295. const timeRange = ref('yesterday');
  296. const barChartRef = ref<HTMLDivElement | null>(null);
  297. let barChartInstance: echarts.ECharts | null = null;
  298. const sourceRows = ref<Array<{ channel: string; newUsers: number; upgradeRatio: string }>>([
  299. { channel: '渠道A', newUsers: 100, upgradeRatio: '10%' },
  300. { channel: '渠道B', newUsers: 80, upgradeRatio: '15%' },
  301. { channel: '渠道C', newUsers: 70, upgradeRatio: '20%' },
  302. { channel: '渠道D', newUsers: 60, upgradeRatio: '25%' },
  303. { channel: '渠道E', newUsers: 50, upgradeRatio: '30%' },
  304. { channel: '渠道F', newUsers: 40, upgradeRatio: '35%' },
  305. { channel: '渠道G', newUsers: 30, upgradeRatio: '40%' },
  306. { channel: '渠道H', newUsers: 20, upgradeRatio: '45%' },
  307. { channel: '渠道I', newUsers: 10, upgradeRatio: '50%' },
  308. { channel: '渠道J', newUsers: 5, upgradeRatio: '55%' },
  309. ]);
  310. function initBarChart(): void {
  311. if (!barChartRef.value) return;
  312. if (barChartInstance) barChartInstance.dispose();
  313. barChartInstance = echarts.init(barChartRef.value);
  314. const option: echarts.EChartsOption = {
  315. tooltip: { trigger: 'axis' },
  316. grid: { left: 40, right: 20, top: 20, bottom: 30 },
  317. xAxis: {
  318. type: 'value',
  319. axisLine: { show: false },
  320. splitLine: { lineStyle: { color: '#f3f4f6' } },
  321. axisLabel: { color: '#6b7280' },
  322. },
  323. yAxis: {
  324. type: 'category',
  325. data: sourceRows.value.map((d) => d.channel),
  326. axisLine: { lineStyle: { color: '#e5e7eb' } },
  327. axisLabel: { color: '#6b7280' },
  328. axisTick: { alignWithLabel: true },
  329. },
  330. series: [
  331. {
  332. name: '新增用户',
  333. type: 'bar',
  334. barWidth: '60%',
  335. itemStyle: { color: '#409EFF' },
  336. data: sourceRows.value.map((d) => d.newUsers),
  337. },
  338. ],
  339. };
  340. barChartInstance.setOption(option);
  341. }
  342. onMounted(() => {
  343. initBarChart();
  344. });
  345. watch(timeRange, () => {
  346. // 静态页面:仅重新渲染
  347. initBarChart();
  348. });
  349. watch(selectedChannelCompare, () => {
  350. // 静态页面:仅重新渲染
  351. initBarChart();
  352. initLineChart();
  353. });
  354. const sourcePage = ref(1);
  355. const sourcePageSize = ref(5);
  356. const pagedSourceRows = computed(() => {
  357. const startIndex = (sourcePage.value - 1) * sourcePageSize.value;
  358. return sourceRows.value.slice(startIndex, startIndex + sourcePageSize.value);
  359. });
  360. const showDetail2 = ref(true);
  361. </script>
  362. <style lang="scss" scoped>
  363. .highlight {
  364. color: #2196f3;
  365. }
  366. .el-form-item--default {
  367. margin-bottom: 0;
  368. }
  369. .el-form.el-form--inline .el-form-item--default.el-form-item:last-of-type,
  370. .el-form.el-form--inline .el-form-item--small.el-form-item:last-of-type {
  371. margin-bottom: 0 !important;
  372. }
  373. </style>