index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <template>
  2. <div class="layout-padding">
  3. <div class="!overflow-auto pl-1">
  4. <!-- 顶部控制区域 -->
  5. <div class="mb-2 el-card p-9">
  6. <div class="flex items-center mb-4">
  7. <Title title="页面访问路径" />
  8. </div>
  9. <div class="flex items-center justify-between space-x-4">
  10. <div class="flex items-center">
  11. <el-select v-model="formData.selectedChannelCompare" class="!w-[180px]" placeholder="版本选择">
  12. <el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
  13. </el-select>
  14. <el-button type="primary" class="ml-2"
  15. ><el-icon class="mr-2"><Platform /></el-icon> 管理版本</el-button
  16. >
  17. </div>
  18. <div class="flex items-center">
  19. <el-button class="mr-2" type="primary" plain>昨日</el-button>
  20. <el-date-picker v-model="formData.time" class="w-[200px]" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" />
  21. </div>
  22. </div>
  23. </div>
  24. <div class="el-card p-9">
  25. <div class="flex justify-between items-center mb-2">
  26. <Title left-line title="访问路径">
  27. <template #default>
  28. <el-popover class="box-item" placement="right" trigger="hover" width="600">
  29. <template #reference>
  30. <el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
  31. </template>
  32. <template #default>
  33. <div class="ant-popover-inner-content">
  34. <div style="padding: 8px 16px">
  35. <span
  36. class="um-vc-text"
  37. title="页面访问路径描述的是用户从打开到离开应用整个过程中每一步骤的页面访问、跳转情况。页面访问路径是全量统计。如果您在Android应用中使用了Fragment页面统计功能,这里的页面包括您指定统计的activity和Fragment。"
  38. style="color: rgb(0, 0, 0); font-size: 12px"
  39. >页面访问路径描述的是用户从打开到离开应用整个过程中每一步骤的页面访问、跳转情况。页面访问路径是全量统计。如果您在Android应用中使用了Fragment页面统计功能,这里的页面包括您指定统计的activity和Fragment。</span
  40. ><span
  41. class="um-vc-text"
  42. title="页面的高度表现该页面被访问的次数,同一页面在不同步骤中用相同的颜色进行展示。"
  43. style="color: rgb(0, 0, 0); font-size: 12px"
  44. >页面的高度表现该页面被访问的次数,同一页面在不同步骤中用相同的颜色进行展示。</span
  45. ><span
  46. class="um-vc-text"
  47. title="每一步骤中,页面节点按照访问次数大小从上往下排列,会显示每一步总的页面访问次数、占总访问次数比例以及前后两步之间的转化率。"
  48. style="color: rgb(0, 0, 0); font-size: 12px"
  49. >每一步骤中,页面节点按照访问次数大小从上往下排列,会显示每一步总的页面访问次数、占总访问次数比例以及前后两步之间的转化率。</span
  50. ><span
  51. class="um-vc-text"
  52. title="如果页面的总会话数达到50W上限,或者单版本会话数达到10W上限,会进行日志抽样处理。"
  53. style="color: rgb(0, 0, 0); font-size: 12px"
  54. >如果页面的总会话数达到50W上限,或者单版本会话数达到10W上限,会进行日志抽样处理。</span
  55. >
  56. </div>
  57. </div>
  58. </template>
  59. </el-popover>
  60. </template>
  61. </Title>
  62. <div class="flex items-center">
  63. <el-radio-group v-model="formData.selectedType" class="!flex items-center">
  64. <el-radio-button label="1">描述</el-radio-button>
  65. <el-radio-button label="2">原名</el-radio-button>
  66. </el-radio-group>
  67. <el-button link class="ml-2" type="primary">编辑页面描述</el-button>
  68. </div>
  69. </div>
  70. <!-- 顶部统计方块 -->
  71. <div class="mb-3 flex items-stretch flex-wrap gap-4">
  72. <div class="min-w-[120px]">
  73. <div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">第1步</div>
  74. <div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
  75. <div class="text-[20px] leading-6 font-medium">66,254</div>
  76. </div>
  77. </div>
  78. <div class="flex items-center text-[#3b82f6] text-[13px]">&gt;&nbsp;99.19%</div>
  79. <div class="min-w-[140px]">
  80. <div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">第2步</div>
  81. <div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
  82. <div class="text-[20px] leading-6 font-medium">66,770<span class="text-[12px] text-gray-500">(99%)</span></div>
  83. </div>
  84. </div>
  85. <div class="flex items-center text-[#3b82f6] text-[13px]">&gt;&nbsp;0.00%</div>
  86. <div class="min-w-[140px]">
  87. <div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">第3步</div>
  88. <div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
  89. <div class="text-[20px] leading-6 font-medium">156<span class="text-[12px] text-gray-500">(19%)</span></div>
  90. </div>
  91. </div>
  92. <div class="flex items-center text-[#3b82f6] text-[13px]">&gt;&nbsp;0.00%</div>
  93. <div class="min-w-[140px]">
  94. <div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">结束</div>
  95. <div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
  96. <div class="text-[20px] leading-6 font-medium">156<span class="text-[12px] text-gray-500">(19%)</span></div>
  97. </div>
  98. </div>
  99. </div>
  100. <!-- 主图表区域 -->
  101. <div class="mb-4 overflow-x-auto">
  102. <div ref="mainChartRef" style="width: 100%; height: 600px"></div>
  103. </div>
  104. </div>
  105. <div class="el-card p-9 mt-2">
  106. <div class="flex justify-between items-center mb-2">
  107. <Title left-line title="访问详情">
  108. <template #default>
  109. <el-popover class="box-item" placement="right" trigger="hover" width="500">
  110. <template #reference>
  111. <el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
  112. </template>
  113. <template #default>
  114. <div class="ant-popover-inner-content">
  115. <div style="padding: 8px 16px; ">
  116. <span
  117. class="um-vc-text"
  118. title="页面访问详情展示了用户使用每个页面的使用次数、访问时长以及跳转情况,这些数据可以帮助您分析每个页面的使用情况。"
  119. style="color: rgb(0, 0, 0); font-size: 12px"
  120. >页面访问详情展示了用户使用每个页面的使用次数、访问时长以及跳转情况,这些数据可以帮助您分析每个页面的使用情况。</span
  121. >
  122. <div>
  123. <span class="um-vc-text" title="访问次数:" style="color: rgb(33, 150, 243); font-size: 12px">访问次数:</span
  124. ><span class="um-vc-text" title="用户进入当前页面的总次数" style="color: rgb(0, 0, 0); font-size: 12px"
  125. >用户进入当前页面的总次数</span
  126. >
  127. </div>
  128. <div>
  129. <span class="um-vc-text" title="访问次数占比:" style="color: rgb(33, 150, 243); font-size: 12px">访问次数占比:</span
  130. ><span class="um-vc-text" title="当前页面访问次数占全部页面访问次数的比例" style="color: rgb(0, 0, 0); font-size: 12px"
  131. >当前页面访问次数占全部页面访问次数的比例</span
  132. >
  133. </div>
  134. <div>
  135. <span class="um-vc-text" title="平均访问时长:" style="color: rgb(33, 150, 243); font-size: 12px">平均访问时长:</span
  136. ><span class="um-vc-text" title="用户每次进入当前页面的平均停留时长" style="color: rgb(0, 0, 0); font-size: 12px"
  137. >用户每次进入当前页面的平均停留时长</span
  138. >
  139. </div>
  140. <div>
  141. <span class="um-vc-text" title="访问时长占比:" style="color: rgb(33, 150, 243); font-size: 12px">访问时长占比:</span
  142. ><span
  143. class="um-vc-text"
  144. title="用户在当前页面停留时间总和占用户在全体页面停留的时间总和的比例"
  145. style="color: rgb(0, 0, 0); font-size: 12px"
  146. >用户在当前页面停留时间总和占用户在全体页面停留的时间总和的比例</span
  147. >
  148. </div>
  149. <div>
  150. <span class="um-vc-text" title="跳出率:" style="color: rgb(33, 150, 243); font-size: 12px">跳出率:</span
  151. ><span class="um-vc-text" title="用户从当前页面离开应用的比例" style="color: rgb(0, 0, 0); font-size: 12px"
  152. >用户从当前页面离开应用的比例</span
  153. >
  154. </div>
  155. <div>
  156. <span class="um-vc-text" title="跳转情况:" style="color: rgb(33, 150, 243); font-size: 12px">跳转情况:</span
  157. ><span class="um-vc-text" title="用户从当前页面进入其他页面的概率分布情况" style="color: rgb(0, 0, 0); font-size: 12px"
  158. >用户从当前页面进入其他页面的概率分布情况</span
  159. >
  160. </div>
  161. </div>
  162. </div>
  163. </template>
  164. </el-popover>
  165. </template>
  166. </Title>
  167. <div class="flex items-center">
  168. <el-button link class="ml-2" type="primary">导出</el-button>
  169. </div>
  170. </div>
  171. <!-- 明细表格 -->
  172. <div class="mt-3">
  173. <el-table v-if="showDetail1" :data="pagedTableRows" border>
  174. <el-table-column prop="date" label="页面(Activity/Fragment)" align="center" min-width="140" />
  175. <el-table-column prop="hyyh" label="描述" align="center" min-width="140" />
  176. <el-table-column prop="hyyh" label="访问次数(占比)" align="center" min-width="140" />
  177. <el-table-column prop="hyyh" label="平均访问时长(占比)" align="center" min-width="140" />
  178. <el-table-column prop="ratio" label="跳出率" align="center" min-width="220" />
  179. </el-table>
  180. <div v-if="showDetail1" class="flex justify-end mt-2">
  181. <el-pagination
  182. v-model:current-page="currentPage"
  183. v-model:page-size="pageSize"
  184. background
  185. layout="total, prev, pager, next, sizes"
  186. :total="tableRows.length"
  187. :page-sizes="[5, 10, 20]"
  188. />
  189. </div>
  190. </div>
  191. </div>
  192. </div>
  193. </div>
  194. </template>
  195. <script setup lang="ts">
  196. import { ref, onMounted, watch, computed, defineAsyncComponent, onBeforeUnmount } from 'vue';
  197. import * as echarts from 'echarts';
  198. import { useI18n } from 'vue-i18n';
  199. const { t } = useI18n();
  200. const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
  201. interface TableRow {
  202. date: string;
  203. newUsers: number;
  204. ratio: string;
  205. }
  206. const formData = ref<Record<string, any>>({
  207. selectedChannelCompare: '',
  208. });
  209. const channelCompareOptions = [
  210. { label: '全部版本', value: '' },
  211. { label: '1.0', value: '1.0' },
  212. { label: '2.0', value: '2.0' },
  213. ];
  214. // Sankey 图表
  215. const mainChartRef = ref<HTMLDivElement | null>(null);
  216. let chartInstance: echarts.ECharts | null = null;
  217. function initSankey(): void {
  218. if (!mainChartRef.value) return;
  219. if (chartInstance) chartInstance.dispose();
  220. chartInstance = echarts.init(mainChartRef.value);
  221. // 左列来源节点(带计数)
  222. const leftNodes = [
  223. { name: 'Ac_Search (13459)', itemStyle: { color: '#cfe8f3' } },
  224. { name: 'Ac_CourseDetail (13459)', itemStyle: { color: '#dbeafe' } },
  225. { name: 'Fr_SearchKeywords (13459)', itemStyle: { color: '#e9d5ff' } },
  226. { name: 'Ac_Search (13459)#2', itemStyle: { color: '#e0f2fe' } },
  227. { name: 'Ac_CourseDetail (13459)#2', itemStyle: { color: '#dcfce7' } },
  228. { name: '其他 (13459)', itemStyle: { color: '#fef3c7' } },
  229. ];
  230. // 中列节点(重复的视频播放目标)
  231. const middleNodes = Array.from({ length: 10 }).map((_, i) => ({
  232. name: `Ac_VideoPlay (13459)#${i + 1}`,
  233. itemStyle: { color: ['#e5e7eb', '#fde68a', '#bbf7d0', '#bfdbfe', '#fbcfe8'][i % 5] },
  234. }));
  235. // 右列汇总/结束节点
  236. const rightNodes = [
  237. { name: 'Step3 总计 (156 / 49%)', itemStyle: { color: '#e5e7eb' } },
  238. { name: 'Step4 结束 (158 / 50%)', itemStyle: { color: '#f3f4f6' } },
  239. ];
  240. const nodes = [...leftNodes, ...middleNodes, ...rightNodes];
  241. // 构造链接:左 -> 中,多条细流;中 -> 右 少量
  242. const links: Array<{ source: string; target: string; value: number }> = [];
  243. const leftToMidValues = [3400, 3000, 2800, 2600, 2000, 1500];
  244. leftNodes.forEach((ln, li) => {
  245. middleNodes.forEach((mn, mi) => {
  246. // 让靠前的中间节点获得更多流量,形成图中“多条细灰线”效果
  247. const base = leftToMidValues[li % leftToMidValues.length];
  248. const decay = Math.max(0.1, 1 - mi * 0.08);
  249. const v = Math.round((base * decay) / 10); // 保持细流
  250. if (v > 0) links.push({ source: ln.name, target: mn.name, value: v });
  251. });
  252. });
  253. // 中 -> 右,较小比例收敛
  254. middleNodes.forEach((mn, idx) => {
  255. const v1 = 20 + Math.max(0, 60 - idx * 5);
  256. const v2 = 10 + Math.max(0, 40 - idx * 4);
  257. links.push({ source: mn.name, target: rightNodes[0].name, value: v1 });
  258. links.push({ source: mn.name, target: rightNodes[1].name, value: v2 });
  259. });
  260. const option: echarts.EChartsOption = {
  261. tooltip: {
  262. trigger: 'item',
  263. formatter: (p: any) => {
  264. if (p.dataType === 'edge') {
  265. return `${p.data.source} → ${p.data.target}<br/>${p.data.value}`;
  266. }
  267. return p.name;
  268. },
  269. },
  270. series: [
  271. {
  272. type: 'sankey',
  273. data: nodes,
  274. links,
  275. left: 60,
  276. right: 60,
  277. top: 6,
  278. bottom: 6,
  279. nodeWidth: 14,
  280. nodeGap: 6,
  281. nodeAlign: 'justify',
  282. layoutIterations: 0,
  283. draggable: false,
  284. label: {
  285. color: '#475569',
  286. fontSize: 11,
  287. formatter: (p: any) => {
  288. const m = /^(.*) \((\d+)\)/.exec(p.name);
  289. if (!m) return p.name;
  290. return `{name|${m[1]}} {count|(${m[2]})}`;
  291. },
  292. rich: {
  293. name: { color: '#374151', fontSize: 11 },
  294. count: { color: '#9ca3af', fontSize: 10 },
  295. },
  296. },
  297. itemStyle: { borderColor: '#dbe3ec', borderWidth: 1 },
  298. lineStyle: { color: '#cbd5e1', curveness: 0.35, opacity: 0.28 },
  299. emphasis: { focus: 'adjacency', lineStyle: { opacity: 0.6 } },
  300. levels: [
  301. { depth: 0, itemStyle: { color: '#dbeafe' } },
  302. { depth: 1, itemStyle: { color: '#f1f5f9' } },
  303. { depth: 2, itemStyle: { color: '#f3f4f6' } },
  304. ],
  305. },
  306. ],
  307. };
  308. chartInstance.setOption(option);
  309. }
  310. // 表格相关(静态数据)
  311. const tableRows = ref<TableRow[]>(
  312. Array.from({ length: 42 }).map((_, idx) => ({
  313. date: `2025-08-${String(11).padStart(2, '0')}`,
  314. newUsers: 727,
  315. hyyh: '115',
  316. ratio: '97.45%',
  317. }))
  318. );
  319. const currentPage = ref(1);
  320. const pageSize = ref(5);
  321. const pagedTableRows = computed(() => {
  322. const startIndex = (currentPage.value - 1) * pageSize.value;
  323. return tableRows.value.slice(startIndex, startIndex + pageSize.value);
  324. });
  325. onMounted(() => {
  326. setTimeout(() => {
  327. initSankey();
  328. window.addEventListener('resize', () => chartInstance && chartInstance.resize());
  329. }, 300);
  330. });
  331. onBeforeUnmount(() => {
  332. if (chartInstance) {
  333. chartInstance.dispose();
  334. chartInstance = null;
  335. }
  336. });
  337. // 展开/收起明细
  338. const showDetail1 = ref(true);
  339. </script>
  340. <style lang="scss" scoped>
  341. .el-card {
  342. background: white;
  343. border-radius: 8px;
  344. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  345. }
  346. :deep(.el-tabs__item.is-top.is-active) {
  347. color: #167af0;
  348. background-color: #e8f2fe;
  349. }
  350. .el-radio-button__inner {
  351. border-radius: 4px;
  352. }
  353. .el-radio-button:first-child .el-radio-button__inner {
  354. border-radius: 4px 0 0 4px;
  355. }
  356. .el-radio-button:last-child .el-radio-button__inner {
  357. border-radius: 0 4px 4px 0;
  358. }
  359. </style>