index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <template>
  2. <div class="layout-padding">
  3. <div class="!overflow-auto px-1">
  4. <div class="el-card p-9">
  5. <!-- 顶部控制区域 -->
  6. <div class="mb-4">
  7. <div class="flex items-center mb-4">
  8. <Title :title="t('active.analytics')">
  9. <template #default>
  10. <el-popover class="box-item" placement="right" trigger="hover" width="250">
  11. <template #reference>
  12. <el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
  13. </template>
  14. <template #default>
  15. <div class="ant-popover-inner-content">
  16. <div class="um-page-tips-content">
  17. <p><span class="highlight">当日活跃成分:</span></p>
  18. <p><span>报表展现每个天级时间点的当日活跃用户的活跃程度。</span></p>
  19. <p><span>将当日活跃用户按照过去15天(含当天)启动的天数分为1至15组,计数并展示。</span></p>
  20. <p><span>活跃1天的用户,表示这个用户在过去15天中仅有1天启动;</span></p>
  21. <p><span>活跃2天的用户,表示这个用户在过去15天中仅有2天启动;</span></p>
  22. <p><span>…</span></p>
  23. <p><span>活跃15天的用户,表示这个用户在过去15天中15天都启动了。</span></p>
  24. <p><span>活跃天数越多的用户,其活跃程度越高,对APP的价值越大。</span></p>
  25. </div>
  26. </div>
  27. </template>
  28. </el-popover>
  29. </template>
  30. </Title>
  31. </div>
  32. <div class="w-full bg-[#f4f5fa] p-1 pl-2 mb-2">查看<span class="text-[#167AF0] cursor-pointer">用户活跃度功能说明</span></div>
  33. <el-tabs v-model="activeName" class="demo-tabs" type="card" @tab-click="handleClick">
  34. <el-tab-pane label="当日活跃成分" name="first" />
  35. <el-tab-pane label="15日活跃成分" name="second" />
  36. </el-tabs>
  37. <div class="flex items-center justify-between space-x-4">
  38. <div class="flex items-center">
  39. <!-- 显示模式切换 -->
  40. <div class="flex items-center">
  41. <el-radio-grou p v-model="displayMode">
  42. <el-radio-button label="absolute">绝对值</el-radio-button>
  43. <el-radio-button label="percentage">百分比</el-radio-button>
  44. </el-radio-grou>
  45. </div>
  46. <!-- 配色选择 -->
  47. <div class="flex items-center ml-2">
  48. <span class="text-sm text-gray-600 mr-2">配色:</span>
  49. <div class="flex space-x-2">
  50. <div
  51. v-for="scheme in colorSchemes"
  52. :key="scheme.id"
  53. @click="selectColorScheme(scheme.id)"
  54. class="w-4 h-4 rounded cursor-pointer border-2 transition-all"
  55. :class="selectedColorScheme === scheme.id ? 'border-blue-500 scale-110' : 'border-gray-300'"
  56. :style="{ backgroundColor: scheme.upperColor }"
  57. ></div>
  58. </div>
  59. </div>
  60. <!-- 用户成分分析 -->
  61. <div class="flex items-center ml-2">
  62. <el-checkbox v-model="userCompositionAnalysis">用户成分分析:</el-checkbox>
  63. <div class="ml-2 relative">
  64. <div class="w-32 h-2 bg-[#f4f5fa] rounded-full relative">
  65. <!-- 已选择区域 -->
  66. <div
  67. class="absolute top-0 h-full bg-[#e4e5ef] rounded-full"
  68. :style="{
  69. left: `${startPosition}%`,
  70. width: `${endPosition - startPosition}%`,
  71. }"
  72. ></div>
  73. <!-- 开始拖拽手柄 -->
  74. <div
  75. class="absolute top-1 w-2 h-2 bg-[#f4f5fa] border border-gray-300 rounded-full cursor-pointer transform -translate-y-1"
  76. :style="{ left: `calc(${startPosition}% - 4px)` }"
  77. @mousedown="startDrag('start')"
  78. ></div>
  79. <!-- 结束拖拽手柄 -->
  80. <div
  81. class="absolute top-1 w-2 h-2 bg-[#f4f5fa] border border-gray-300 rounded-full cursor-pointer transform -translate-y-1"
  82. :style="{ left: `calc(${endPosition}% - 4px)` }"
  83. @mousedown="startDrag('end')"
  84. ></div>
  85. </div>
  86. <div class="flex justify-between text-xs text-gray-500 mt-1">
  87. <span>0</span>
  88. <span>10</span>
  89. <span>20</span>
  90. <span>30</span>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. <!-- 导出按钮 -->
  96. <el-button type="primary" size="small">
  97. <el-icon class="mr-1"><Download /></el-icon>
  98. 导出
  99. </el-button>
  100. </div>
  101. </div>
  102. <!-- 主图表区域 -->
  103. <div class="mb-4">
  104. <div ref="mainChartRef" style="width: 100%; height: 400px"></div>
  105. </div>
  106. <!-- 下方图表区域 -->
  107. <div>
  108. <div ref="subChartRef" style="width: 100%; height: 200px"></div>
  109. </div>
  110. </div>
  111. </div>
  112. </div>
  113. </template>
  114. <script setup lang="ts">
  115. import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
  116. import * as echarts from 'echarts';
  117. import { QuestionFilled, Download } from '@element-plus/icons-vue';
  118. import { useI18n } from 'vue-i18n';
  119. import type { TabsPaneContext } from 'element-plus';
  120. const activeName = ref('first');
  121. const { t } = useI18n();
  122. const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
  123. const handleClick = (tab: TabsPaneContext, event: Event) => {
  124. console.log(tab, event);
  125. };
  126. // 控制状态
  127. const displayMode = ref('absolute');
  128. const userCompositionAnalysis = ref(false);
  129. // 进度条拖动状态
  130. const startPosition = ref(20);
  131. const endPosition = ref(80);
  132. const isDragging = ref(false);
  133. const dragType = ref<'start' | 'end' | null>(null);
  134. // 拖动功能
  135. function startDrag(type: 'start' | 'end') {
  136. isDragging.value = true;
  137. dragType.value = type;
  138. document.addEventListener('mousemove', handleDrag);
  139. document.addEventListener('mouseup', stopDrag);
  140. }
  141. function handleDrag(event: MouseEvent) {
  142. if (!isDragging.value) return;
  143. // 获取进度条容器
  144. const sliderContainer = document.querySelector('.w-32.h-2.bg-gray-200') as HTMLElement;
  145. if (!sliderContainer) return;
  146. const rect = sliderContainer.getBoundingClientRect();
  147. const percentage = Math.max(0, Math.min(100, ((event.clientX - rect.left) / rect.width) * 100));
  148. if (dragType.value === 'start') {
  149. startPosition.value = Math.min(percentage, endPosition.value - 5);
  150. } else if (dragType.value === 'end') {
  151. endPosition.value = Math.max(percentage, startPosition.value + 5);
  152. }
  153. }
  154. function stopDrag() {
  155. isDragging.value = false;
  156. dragType.value = null;
  157. document.removeEventListener('mousemove', handleDrag);
  158. document.removeEventListener('mouseup', stopDrag);
  159. }
  160. // 配色方案
  161. const selectedColorScheme = ref('blue');
  162. const colorSchemes = ref([
  163. {
  164. id: 'blue',
  165. name: '蓝色系',
  166. upperColor: '#7dd3fc',
  167. lowerColor: '#3b82f6',
  168. },
  169. {
  170. id: 'green',
  171. name: '绿色系',
  172. upperColor: '#86efac',
  173. lowerColor: '#22c55e',
  174. },
  175. {
  176. id: 'purple',
  177. name: '紫色系',
  178. upperColor: '#c4b5fd',
  179. lowerColor: '#8b5cf6',
  180. },
  181. {
  182. id: 'orange',
  183. name: '橙色系',
  184. upperColor: '#fed7aa',
  185. lowerColor: '#f97316',
  186. },
  187. {
  188. id: 'pink',
  189. name: '粉色系',
  190. upperColor: '#f9a8d4',
  191. lowerColor: '#ec4899',
  192. },
  193. ]);
  194. function selectColorScheme(schemeId: string) {
  195. selectedColorScheme.value = schemeId;
  196. // 重新渲染图表以应用新颜色
  197. setTimeout(() => {
  198. initMainChart();
  199. initSubChart();
  200. }, 100);
  201. }
  202. // 图表引用
  203. const mainChartRef = ref<HTMLDivElement | null>(null);
  204. const subChartRef = ref<HTMLDivElement | null>(null);
  205. let mainChart: echarts.ECharts | null = null;
  206. let subChart: echarts.ECharts | null = null;
  207. // 模拟数据 - 05-18 时间点
  208. const timeData = [
  209. '05-18',
  210. '05-18',
  211. '05-18',
  212. '05-18',
  213. '05-18',
  214. '05-18',
  215. '05-18',
  216. '05-18',
  217. '05-18',
  218. '05-18',
  219. '05-18',
  220. '05-18',
  221. '05-18',
  222. '05-18',
  223. '05-18',
  224. '05-18',
  225. '05-18',
  226. '05-18',
  227. '05-18',
  228. '05-18',
  229. '05-18',
  230. '05-18',
  231. '05-18',
  232. '05-18',
  233. '05-18',
  234. '05-18',
  235. '05-18',
  236. '05-18',
  237. '05-18',
  238. '05-18',
  239. '05-18',
  240. '05-18',
  241. '05-18',
  242. '05-18',
  243. '05-18',
  244. '05-18',
  245. '05-18',
  246. '05-18',
  247. '05-18',
  248. '05-18',
  249. '05-18',
  250. '05-18',
  251. '05-18',
  252. '05-18',
  253. '05-18',
  254. '05-18',
  255. '05-18',
  256. '05-18',
  257. '05-18',
  258. '05-18',
  259. '05-18',
  260. '05-18',
  261. '05-18',
  262. '05-18',
  263. '05-18',
  264. '05-18',
  265. '05-18',
  266. '05-18',
  267. '05-18',
  268. '05-18',
  269. '05-18',
  270. '05-18',
  271. '05-18',
  272. '05-18',
  273. '05-18',
  274. '05-18',
  275. '05-18',
  276. '05-18',
  277. '05-18',
  278. '05-18',
  279. '05-18',
  280. '05-18',
  281. ];
  282. // 主图表数据 - 上层区域(浅蓝绿色)- 大幅波动的数据
  283. const upperSeriesData = [
  284. 1200, 800, 600, 400, 800, 1200, 1000, 800, 600, 900, 1100, 800, 950, 750, 550, 350, 750, 1150, 950, 750, 550, 850, 1050, 750, 1100, 900, 700, 500,
  285. 900, 1300, 1100, 900, 700, 1000, 1200, 900, 850, 650, 450, 250, 650, 1050, 850, 650, 450, 750, 950, 650, 1100, 900, 700, 500, 900, 1300, 1100, 900,
  286. 700, 1000, 1200, 900, 850, 650, 450, 250, 650, 1050, 850, 650, 450, 750, 950, 650, 1100, 900, 700, 500, 900, 1300, 1100, 900, 700, 1000, 1200, 900,
  287. 850, 650, 450, 250, 650, 1050, 850, 650, 450, 750, 950, 650, 1100, 900, 700, 500, 900, 1300, 1100, 900, 700, 1000, 1200, 900, 850, 650, 450, 250,
  288. 650, 1050, 850, 650, 450, 750, 950, 650,
  289. ];
  290. // 下层区域数据(蓝色)- 相对平坦的数据
  291. const lowerSeriesData = [
  292. 200, 180, 160, 150, 180, 200, 190, 180, 170, 190, 210, 200, 195, 175, 155, 145, 175, 195, 185, 175, 165, 185, 205, 195, 205, 185, 165, 155, 185,
  293. 205, 195, 185, 175, 195, 215, 205, 205, 185, 165, 155, 185, 205, 195, 185, 175, 195, 215, 205, 205, 185, 165, 155, 185, 205, 195, 185, 175, 195,
  294. 215, 205, 205, 185, 165, 155, 185, 205, 195, 185, 175, 195, 215, 205, 205, 185, 165, 155, 185, 205, 195, 185, 175, 195, 215, 205, 205, 185, 165,
  295. 155, 185, 205, 195, 185, 175, 195, 215, 205, 190, 170, 150, 140, 170, 190, 180, 170, 160, 180, 200, 190,
  296. ];
  297. function initMainChart(): void {
  298. if (!mainChartRef.value) return;
  299. if (mainChart) mainChart.dispose();
  300. // 获取当前选中的配色方案
  301. const currentScheme = colorSchemes.value.find((scheme) => scheme.id === selectedColorScheme.value) || colorSchemes.value[0];
  302. mainChart = echarts.init(mainChartRef.value);
  303. const option: echarts.EChartsOption = {
  304. tooltip: {
  305. trigger: 'axis',
  306. axisPointer: {
  307. type: 'cross',
  308. },
  309. },
  310. legend: {
  311. show: false,
  312. },
  313. grid: {
  314. left: 40,
  315. right: 20,
  316. top: 20,
  317. bottom: 30,
  318. },
  319. xAxis: {
  320. type: 'category',
  321. data: timeData,
  322. axisLine: { lineStyle: { color: '#e5e7eb' } },
  323. axisLabel: { color: '#6b7280' },
  324. axisTick: { alignWithLabel: true },
  325. },
  326. yAxis: {
  327. type: 'value',
  328. min: 0,
  329. max: 1400,
  330. interval: 200,
  331. axisLine: { show: false },
  332. splitLine: { lineStyle: { color: '#f3f4f6' } },
  333. axisLabel: { color: '#6b7280' },
  334. },
  335. series: [
  336. {
  337. name: '上层区域',
  338. type: 'line',
  339. stack: 'total',
  340. areaStyle: {
  341. color: currentScheme.upperColor,
  342. opacity: 0.8,
  343. },
  344. lineStyle: {
  345. color: currentScheme.upperColor,
  346. width: 0,
  347. },
  348. itemStyle: {
  349. color: currentScheme.upperColor,
  350. },
  351. data: upperSeriesData,
  352. smooth: false,
  353. showSymbol: false,
  354. },
  355. {
  356. name: '下层区域',
  357. type: 'line',
  358. stack: 'total',
  359. areaStyle: {
  360. color: currentScheme.lowerColor,
  361. opacity: 1,
  362. },
  363. lineStyle: {
  364. color: currentScheme.lowerColor,
  365. width: 0,
  366. },
  367. itemStyle: {
  368. color: currentScheme.lowerColor,
  369. },
  370. data: lowerSeriesData,
  371. smooth: false,
  372. showSymbol: false,
  373. },
  374. ],
  375. };
  376. mainChart.setOption(option);
  377. }
  378. function initSubChart(): void {
  379. if (!subChartRef.value) return;
  380. if (subChart) subChart.dispose();
  381. // 获取当前选中的配色方案
  382. const currentScheme = colorSchemes.value.find((scheme) => scheme.id === selectedColorScheme.value) || colorSchemes.value[0];
  383. subChart = echarts.init(subChartRef.value);
  384. const option: echarts.EChartsOption = {
  385. tooltip: {
  386. trigger: 'axis',
  387. },
  388. legend: {
  389. show: false,
  390. },
  391. grid: {
  392. left: 40,
  393. right: 20,
  394. top: 20,
  395. bottom: 30,
  396. },
  397. xAxis: {
  398. type: 'category',
  399. data: timeData,
  400. axisLine: { lineStyle: { color: '#e5e7eb' } },
  401. axisLabel: { color: '#6b7280' },
  402. axisTick: { alignWithLabel: true },
  403. },
  404. yAxis: {
  405. type: 'value',
  406. axisLine: { show: false },
  407. splitLine: { lineStyle: { color: '#f3f4f6' } },
  408. axisLabel: { color: '#6b7280' },
  409. },
  410. series: [
  411. {
  412. name: '下层区域',
  413. type: 'line',
  414. areaStyle: {
  415. color: currentScheme.lowerColor,
  416. opacity: 1,
  417. },
  418. lineStyle: {
  419. color: currentScheme.lowerColor,
  420. width: 0,
  421. },
  422. itemStyle: {
  423. color: currentScheme.lowerColor,
  424. },
  425. data: lowerSeriesData,
  426. smooth: false,
  427. showSymbol: false,
  428. },
  429. ],
  430. };
  431. subChart.setOption(option);
  432. }
  433. onMounted(() => {
  434. setTimeout(() => {
  435. initMainChart();
  436. initSubChart();
  437. }, 500);
  438. });
  439. watch(displayMode, () => {
  440. // 当显示模式改变时重新渲染图表
  441. setTimeout(() => {
  442. initMainChart();
  443. initSubChart();
  444. }, 100);
  445. });
  446. watch(userCompositionAnalysis, () => {
  447. // 当用户成分分析开关改变时重新渲染图表
  448. setTimeout(() => {
  449. initMainChart();
  450. initSubChart();
  451. }, 100);
  452. });
  453. </script>
  454. <style lang="scss" scoped>
  455. .el-card {
  456. background: white;
  457. border-radius: 8px;
  458. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  459. }
  460. :deep(.el-tabs__item.is-top.is-active) {
  461. color: #167af0;
  462. background-color: #e8f2fe;
  463. }
  464. .el-radio-button__inner {
  465. border-radius: 4px;
  466. }
  467. .el-radio-button:first-child .el-radio-button__inner {
  468. border-radius: 4px 0 0 4px;
  469. }
  470. .el-radio-button:last-child .el-radio-button__inner {
  471. border-radius: 0 4px 4px 0;
  472. }
  473. </style>