123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- <template>
- <div class="layout-padding">
- <div class="!overflow-auto px-1">
- <div class="el-card p-9">
- <!-- 顶部控制区域 -->
- <div class="mb-4">
- <div class="flex items-center mb-4">
- <Title :title="t('active.analytics')">
- <template #default>
- <el-popover class="box-item" placement="right" trigger="hover" width="250">
- <template #reference>
- <el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
- </template>
- <template #default>
- <div class="ant-popover-inner-content">
- <div class="um-page-tips-content">
- <p><span class="highlight">当日活跃成分:</span></p>
- <p><span>报表展现每个天级时间点的当日活跃用户的活跃程度。</span></p>
- <p><span>将当日活跃用户按照过去15天(含当天)启动的天数分为1至15组,计数并展示。</span></p>
- <p><span>活跃1天的用户,表示这个用户在过去15天中仅有1天启动;</span></p>
- <p><span>活跃2天的用户,表示这个用户在过去15天中仅有2天启动;</span></p>
- <p><span>…</span></p>
- <p><span>活跃15天的用户,表示这个用户在过去15天中15天都启动了。</span></p>
- <p><span>活跃天数越多的用户,其活跃程度越高,对APP的价值越大。</span></p>
- </div>
- </div>
- </template>
- </el-popover>
- </template>
- </Title>
- </div>
- <div class="w-full bg-[#f4f5fa] p-1 pl-2 mb-2">查看<span class="text-[#167AF0] cursor-pointer">用户活跃度功能说明</span></div>
- <el-tabs v-model="activeName" class="demo-tabs" type="card" @tab-click="handleClick">
- <el-tab-pane label="当日活跃成分" name="first" />
- <el-tab-pane label="15日活跃成分" name="second" />
- </el-tabs>
- <div class="flex items-center justify-between space-x-4">
- <div class="flex items-center">
- <!-- 显示模式切换 -->
- <div class="flex items-center">
- <el-radio-grou p v-model="displayMode">
- <el-radio-button label="absolute">绝对值</el-radio-button>
- <el-radio-button label="percentage">百分比</el-radio-button>
- </el-radio-grou>
- </div>
- <!-- 配色选择 -->
- <div class="flex items-center ml-2">
- <span class="text-sm text-gray-600 mr-2">配色:</span>
- <div class="flex space-x-2">
- <div
- v-for="scheme in colorSchemes"
- :key="scheme.id"
- @click="selectColorScheme(scheme.id)"
- class="w-4 h-4 rounded cursor-pointer border-2 transition-all"
- :class="selectedColorScheme === scheme.id ? 'border-blue-500 scale-110' : 'border-gray-300'"
- :style="{ backgroundColor: scheme.upperColor }"
- ></div>
- </div>
- </div>
- <!-- 用户成分分析 -->
- <div class="flex items-center ml-2">
- <el-checkbox v-model="userCompositionAnalysis">用户成分分析:</el-checkbox>
- <div class="ml-2 relative">
- <div class="w-32 h-2 bg-[#f4f5fa] rounded-full relative">
- <!-- 已选择区域 -->
- <div
- class="absolute top-0 h-full bg-[#e4e5ef] rounded-full"
- :style="{
- left: `${startPosition}%`,
- width: `${endPosition - startPosition}%`,
- }"
- ></div>
- <!-- 开始拖拽手柄 -->
- <div
- class="absolute top-1 w-2 h-2 bg-[#f4f5fa] border border-gray-300 rounded-full cursor-pointer transform -translate-y-1"
- :style="{ left: `calc(${startPosition}% - 4px)` }"
- @mousedown="startDrag('start')"
- ></div>
- <!-- 结束拖拽手柄 -->
- <div
- class="absolute top-1 w-2 h-2 bg-[#f4f5fa] border border-gray-300 rounded-full cursor-pointer transform -translate-y-1"
- :style="{ left: `calc(${endPosition}% - 4px)` }"
- @mousedown="startDrag('end')"
- ></div>
- </div>
- <div class="flex justify-between text-xs text-gray-500 mt-1">
- <span>0</span>
- <span>10</span>
- <span>20</span>
- <span>30</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 导出按钮 -->
- <el-button type="primary" size="small">
- <el-icon class="mr-1"><Download /></el-icon>
- 导出
- </el-button>
- </div>
- </div>
- <!-- 主图表区域 -->
- <div class="mb-4">
- <div ref="mainChartRef" style="width: 100%; height: 400px"></div>
- </div>
- <!-- 下方图表区域 -->
- <div>
- <div ref="subChartRef" style="width: 100%; height: 200px"></div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
- import * as echarts from 'echarts';
- import { QuestionFilled, Download } from '@element-plus/icons-vue';
- import { useI18n } from 'vue-i18n';
- import type { TabsPaneContext } from 'element-plus';
- const activeName = ref('first');
- const { t } = useI18n();
- const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
- const handleClick = (tab: TabsPaneContext, event: Event) => {
- console.log(tab, event);
- };
- // 控制状态
- const displayMode = ref('absolute');
- const userCompositionAnalysis = ref(false);
- // 进度条拖动状态
- const startPosition = ref(20);
- const endPosition = ref(80);
- const isDragging = ref(false);
- const dragType = ref<'start' | 'end' | null>(null);
- // 拖动功能
- function startDrag(type: 'start' | 'end') {
- isDragging.value = true;
- dragType.value = type;
- document.addEventListener('mousemove', handleDrag);
- document.addEventListener('mouseup', stopDrag);
- }
- function handleDrag(event: MouseEvent) {
- if (!isDragging.value) return;
- // 获取进度条容器
- const sliderContainer = document.querySelector('.w-32.h-2.bg-gray-200') as HTMLElement;
- if (!sliderContainer) return;
- const rect = sliderContainer.getBoundingClientRect();
- const percentage = Math.max(0, Math.min(100, ((event.clientX - rect.left) / rect.width) * 100));
- if (dragType.value === 'start') {
- startPosition.value = Math.min(percentage, endPosition.value - 5);
- } else if (dragType.value === 'end') {
- endPosition.value = Math.max(percentage, startPosition.value + 5);
- }
- }
- function stopDrag() {
- isDragging.value = false;
- dragType.value = null;
- document.removeEventListener('mousemove', handleDrag);
- document.removeEventListener('mouseup', stopDrag);
- }
- // 配色方案
- const selectedColorScheme = ref('blue');
- const colorSchemes = ref([
- {
- id: 'blue',
- name: '蓝色系',
- upperColor: '#7dd3fc',
- lowerColor: '#3b82f6',
- },
- {
- id: 'green',
- name: '绿色系',
- upperColor: '#86efac',
- lowerColor: '#22c55e',
- },
- {
- id: 'purple',
- name: '紫色系',
- upperColor: '#c4b5fd',
- lowerColor: '#8b5cf6',
- },
- {
- id: 'orange',
- name: '橙色系',
- upperColor: '#fed7aa',
- lowerColor: '#f97316',
- },
- {
- id: 'pink',
- name: '粉色系',
- upperColor: '#f9a8d4',
- lowerColor: '#ec4899',
- },
- ]);
- function selectColorScheme(schemeId: string) {
- selectedColorScheme.value = schemeId;
- // 重新渲染图表以应用新颜色
- setTimeout(() => {
- initMainChart();
- initSubChart();
- }, 100);
- }
- // 图表引用
- const mainChartRef = ref<HTMLDivElement | null>(null);
- const subChartRef = ref<HTMLDivElement | null>(null);
- let mainChart: echarts.ECharts | null = null;
- let subChart: echarts.ECharts | null = null;
- // 模拟数据 - 05-18 时间点
- const timeData = [
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- '05-18',
- ];
- // 主图表数据 - 上层区域(浅蓝绿色)- 大幅波动的数据
- const upperSeriesData = [
- 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,
- 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,
- 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,
- 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,
- 650, 1050, 850, 650, 450, 750, 950, 650,
- ];
- // 下层区域数据(蓝色)- 相对平坦的数据
- const lowerSeriesData = [
- 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,
- 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,
- 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,
- 155, 185, 205, 195, 185, 175, 195, 215, 205, 190, 170, 150, 140, 170, 190, 180, 170, 160, 180, 200, 190,
- ];
- function initMainChart(): void {
- if (!mainChartRef.value) return;
- if (mainChart) mainChart.dispose();
- // 获取当前选中的配色方案
- const currentScheme = colorSchemes.value.find((scheme) => scheme.id === selectedColorScheme.value) || colorSchemes.value[0];
- mainChart = echarts.init(mainChartRef.value);
- const option: echarts.EChartsOption = {
- tooltip: {
- trigger: 'axis',
- axisPointer: {
- type: 'cross',
- },
- },
- legend: {
- show: false,
- },
- grid: {
- left: 40,
- right: 20,
- top: 20,
- bottom: 30,
- },
- xAxis: {
- type: 'category',
- data: timeData,
- axisLine: { lineStyle: { color: '#e5e7eb' } },
- axisLabel: { color: '#6b7280' },
- axisTick: { alignWithLabel: true },
- },
- yAxis: {
- type: 'value',
- min: 0,
- max: 1400,
- interval: 200,
- axisLine: { show: false },
- splitLine: { lineStyle: { color: '#f3f4f6' } },
- axisLabel: { color: '#6b7280' },
- },
- series: [
- {
- name: '上层区域',
- type: 'line',
- stack: 'total',
- areaStyle: {
- color: currentScheme.upperColor,
- opacity: 0.8,
- },
- lineStyle: {
- color: currentScheme.upperColor,
- width: 0,
- },
- itemStyle: {
- color: currentScheme.upperColor,
- },
- data: upperSeriesData,
- smooth: false,
- showSymbol: false,
- },
- {
- name: '下层区域',
- type: 'line',
- stack: 'total',
- areaStyle: {
- color: currentScheme.lowerColor,
- opacity: 1,
- },
- lineStyle: {
- color: currentScheme.lowerColor,
- width: 0,
- },
- itemStyle: {
- color: currentScheme.lowerColor,
- },
- data: lowerSeriesData,
- smooth: false,
- showSymbol: false,
- },
- ],
- };
- mainChart.setOption(option);
- }
- function initSubChart(): void {
- if (!subChartRef.value) return;
- if (subChart) subChart.dispose();
- // 获取当前选中的配色方案
- const currentScheme = colorSchemes.value.find((scheme) => scheme.id === selectedColorScheme.value) || colorSchemes.value[0];
- subChart = echarts.init(subChartRef.value);
- const option: echarts.EChartsOption = {
- tooltip: {
- trigger: 'axis',
- },
- legend: {
- show: false,
- },
- grid: {
- left: 40,
- right: 20,
- top: 20,
- bottom: 30,
- },
- xAxis: {
- type: 'category',
- data: timeData,
- axisLine: { lineStyle: { color: '#e5e7eb' } },
- axisLabel: { color: '#6b7280' },
- axisTick: { alignWithLabel: true },
- },
- yAxis: {
- type: 'value',
- axisLine: { show: false },
- splitLine: { lineStyle: { color: '#f3f4f6' } },
- axisLabel: { color: '#6b7280' },
- },
- series: [
- {
- name: '下层区域',
- type: 'line',
- areaStyle: {
- color: currentScheme.lowerColor,
- opacity: 1,
- },
- lineStyle: {
- color: currentScheme.lowerColor,
- width: 0,
- },
- itemStyle: {
- color: currentScheme.lowerColor,
- },
- data: lowerSeriesData,
- smooth: false,
- showSymbol: false,
- },
- ],
- };
- subChart.setOption(option);
- }
- onMounted(() => {
- setTimeout(() => {
- initMainChart();
- initSubChart();
- }, 500);
- });
- watch(displayMode, () => {
- // 当显示模式改变时重新渲染图表
- setTimeout(() => {
- initMainChart();
- initSubChart();
- }, 100);
- });
- watch(userCompositionAnalysis, () => {
- // 当用户成分分析开关改变时重新渲染图表
- setTimeout(() => {
- initMainChart();
- initSubChart();
- }, 100);
- });
- </script>
- <style lang="scss" scoped>
- .el-card {
- background: white;
- border-radius: 8px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- }
- :deep(.el-tabs__item.is-top.is-active) {
- color: #167af0;
- background-color: #e8f2fe;
- }
- .el-radio-button__inner {
- border-radius: 4px;
- }
- .el-radio-button:first-child .el-radio-button__inner {
- border-radius: 4px 0 0 4px;
- }
- .el-radio-button:last-child .el-radio-button__inner {
- border-radius: 0 4px 4px 0;
- }
- </style>
|