浏览代码

Merge branch 'dev-jcq' of https://s-20coaj910c.zht2.com/cmy/data-marketing-platform into dev-ly

luoy 1 天之前
父节点
当前提交
b77a5c2f79

+ 578 - 57
src/views/count/channel/otherChannel.vue

@@ -159,65 +159,151 @@
               2025/08/19
             </div>
           </div>
-          <Lprogress style="margin-top: 45px;" v-for="i in progressArray" :key="i.label" :label="i.label" :num="i.num" :count="i.count"></Lprogress>
+          <Lprogress style="margin-top: 45px;" v-for="i in progressArray" :key="i.label" :label="i.label" :num="i.num"
+            :count="i.count"></Lprogress>
         </Lcard>
       </div>
-      <Lcard :height="650">
-        <Title left-line title="渠道矩阵分析">
-          <el-popover class="box-item" placement="right" trigger="hover" width="250">
-            <template #reference>
-              <el-icon class="ml-1" style="color: #a4b8cf">
-                <InfoFilled />
-              </el-icon>
-            </template>
-            <template #default>
-              <div class="ant-popover-inner-content">
-                <div class="um-page-tips-content" style="line-height: 24px">
-                  可通过矩阵分析了解各个渠道的价值,在您推广平台选择时也可选择高转化高偏好的渠道作为重点渠道
-                </div>
-              </div>
-            </template>
-          </el-popover>
-        </Title>
-        <div style="margin: 30px 60px">
-          <span>横轴:新增用户(日均)</span>
-          纵轴:<el-select v-model="selectSanDian" style="width: 200px;display: inline-block;">
-            <el-option label="次日留存率(日均)" value="1"></el-option>
-            <el-option label="后日留存率(日均)" value="2"></el-option>
-          </el-select>
+      <Lcard>
+        <div class="flex justify-between items-center">
+          <Title left-line title="渠道新增留存明细">
+          </Title>
+          <div class="box1">
+            <div class="box1-time">
+              <el-radio-group v-model="timeGranularity" style="width: 240px; margin-right: 30px;">
+                <el-radio-button label="week">过去七天</el-radio-button>
+                <el-radio-button label="month">过去三十天</el-radio-button>
+              </el-radio-group>
+              <el-radio-group v-model="timeGranularity" style="width: 240px; margin-right: 30px;">
+                <el-radio-button label="week">留存率</el-radio-button>
+                <el-radio-button label="month">留存数</el-radio-button>
+              </el-radio-group>
+            </div>
+            <div class="link">导出数据
+              <span style="display: inline-block; transform:translateY(3px);">
+                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+                  <g clip-path="url(#clip0_713_1665)">
+                    <path
+                      d="M14 1.33301H1.99992C1.63172 1.33301 1.33324 1.63149 1.33325 1.99969L1.33358 13.9997C1.33359 14.3679 1.63207 14.6663 2.00025 14.6663H14C14.3682 14.6663 14.6667 14.3679 14.6667 13.9997V1.99967C14.6667 1.63148 14.3682 1.33301 14 1.33301Z"
+                      stroke="#167AF0" />
+                    <path d="M6.66968 11.3366H11.1669V6.66992" stroke="#167AF0" stroke-linecap="round"
+                      stroke-linejoin="round" />
+                    <path d="M9.66675 8.16699L10.1667 7.66699L11.1667 6.66699L12.1667 7.66699L12.6667 8.16699"
+                      stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
+                    <path d="M4.66675 1.33301V14.6663" stroke="#167AF0" stroke-linecap="round" />
+                    <path d="M1.33325 4.67959L14.6666 4.66699" stroke="#167AF0" stroke-linecap="round" />
+                    <path d="M2.66675 1.33301H9.33341" stroke="#167AF0" stroke-linecap="round"
+                      stroke-linejoin="round" />
+                    <path d="M2.66675 14.667H9.33341" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
+                    <path d="M14.6667 2.66699V6.00033" stroke="#167AF0" stroke-linecap="round" />
+                    <path d="M1.33325 2.66699V6.00033" stroke="#167AF0" stroke-linecap="round" />
+                  </g>
+                  <defs>
+                    <clipPath id="clip0_713_1665">
+                      <rect width="16" height="16" fill="white" />
+                    </clipPath>
+                  </defs>
+                </svg>
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="line" style="margin: 35px -30px 40px;"></div>
+        <div class="relative">
+          <el-table :data="currentTableData" border>
+            <el-table-column v-for="column in currentTableColumns" :key="column.dataIndex" :prop="column.dataIndex"
+              :label="column.title" :align="column.align" :min-width="column.width">
+              <template #default="scope">
+                <span :class="getCellStyle(column.dataIndex, scope.row[column.dataIndex])">
+                  {{ scope.row[column.dataIndex] }}
+                </span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <div class="flex justify-end mt-2">
+          <el-pagination v-model:current-page="currentPage1" v-model:page-size="pageSize1" background
+            layout="total, prev, pager, next, sizes" :total="tableRows1.length" :page-sizes="[5, 10, 20]" />
         </div>
-        <div ref="SDChartRef" style="width: 100%; height: 480px"></div>
       </Lcard>
       <Lcard>
-        <Title left-line title="渠道明细数据">
-        </Title>
-        <div class="box1" style="margin: 30px 60px">
-          <div class="flex items-center justify-between">
-            <el-radio-group v-model="radioTableDay">
-              <el-radio-button label="today">今日</el-radio-button>
-              <el-radio-button label="yesterday">昨日</el-radio-button>
+        <div class="flex justify-between items-center">
+          <Title left-line title="渠道新增细分">
+          </Title>
+          <div class="box1">
+            <div class="box1-time">
+              <el-radio-group v-model="timeGroup" style="width: 240px; margin-right: 30px;">
+                <el-radio-button label="week">过去七天</el-radio-button>
+                <el-radio-button label="month">过去三十天</el-radio-button>
+              </el-radio-group>
+            </div>
+            <div class="link">导出数据
+              <span style="display: inline-block; transform:translateY(3px);">
+                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+                  <g clip-path="url(#clip0_713_1665)">
+                    <path
+                      d="M14 1.33301H1.99992C1.63172 1.33301 1.33324 1.63149 1.33325 1.99969L1.33358 13.9997C1.33359 14.3679 1.63207 14.6663 2.00025 14.6663H14C14.3682 14.6663 14.6667 14.3679 14.6667 13.9997V1.99967C14.6667 1.63148 14.3682 1.33301 14 1.33301Z"
+                      stroke="#167AF0" />
+                    <path d="M6.66968 11.3366H11.1669V6.66992" stroke="#167AF0" stroke-linecap="round"
+                      stroke-linejoin="round" />
+                    <path d="M9.66675 8.16699L10.1667 7.66699L11.1667 6.66699L12.1667 7.66699L12.6667 8.16699"
+                      stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
+                    <path d="M4.66675 1.33301V14.6663" stroke="#167AF0" stroke-linecap="round" />
+                    <path d="M1.33325 4.67959L14.6666 4.66699" stroke="#167AF0" stroke-linecap="round" />
+                    <path d="M2.66675 1.33301H9.33341" stroke="#167AF0" stroke-linecap="round"
+                      stroke-linejoin="round" />
+                    <path d="M2.66675 14.667H9.33341" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
+                    <path d="M14.6667 2.66699V6.00033" stroke="#167AF0" stroke-linecap="round" />
+                    <path d="M1.33325 2.66699V6.00033" stroke="#167AF0" stroke-linecap="round" />
+                  </g>
+                  <defs>
+                    <clipPath id="clip0_713_1665">
+                      <rect width="16" height="16" fill="white" />
+                    </clipPath>
+                  </defs>
+                </svg>
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="line" style="margin: 35px -30px 40px;"></div>
+        <div class="flex items-center justify-between mb-2 mt-3" style="margin-top: 20px;margin-left: 60px;">
+          <div class="flex items-center">
+            <el-radio-group v-model="lineChartUser">
+              <el-radio-button label="1">设备</el-radio-button>
+              <el-radio-button label="2">国家/地区</el-radio-button>
+              <el-radio-button label="3">省市</el-radio-button>
+              <el-radio-button label="4">版本</el-radio-button>
             </el-radio-group>
-            <el-select class="ml-2" v-model="tableChannel" style="width: 140px" placeholder="渠道选择">
-              <el-option label="全部" value="1"></el-option>
-              <el-option label="未分组" value="2"></el-option>
-            </el-select>
           </div>
-          <div class="link">导出</div>
         </div>
-        <el-table :data="pagedTableRows" border>
-          <el-table-column prop="date" label="日期" min-width="140" />
-          <el-table-column label="新增用户(占比)" min-width="220">
-            <template #default="scope">
-              <div class="flex items-center justify-between w-full">
-                <span>{{ scope.row.newUsers }}</span>
-                <span class="text-gray-500 text-xs">{{ scope.row.ratio }}</span>
-              </div>
-            </template>
-          </el-table-column>
-        </el-table>
-        <div v-if="showDetail1" class="flex justify-end mt-3">
-          <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" background
-            layout="total, prev, pager, next, sizes" :total="tableRows.length" :page-sizes="[5, 10, 20]" />
+        <div class="relative">
+          <div ref="horizontalBarChartRef" style="margin-left: 20px; width: calc(100% - 80px); height: 320px"></div>
+        </div>
+        <div class="mt-3" style="margin-top: 20px;margin-left: 60px;">
+          <div class="flex items-center justify-between mb-2">
+            <div class="text-base font-medium cursor-pointer select-none" @click="showDetail1 = !showDetail1">
+              <!-- {{ showDetail1 ? '收起明细数据' : '展开明细数据' }} -->
+                渠道明细
+            </div>
+            <div>
+              <el-button>导出</el-button>
+            </div>
+          </div>
+          <el-table v-if="showDetail1" :data="pagedTableRows" border>
+            <el-table-column prop="date" label="日期" min-width="140" />
+            <el-table-column label="新增用户(占比)" min-width="220">
+              <template #default="scope">
+                <div class="flex items-center justify-between w-full">
+                  <span>{{ scope.row.newUsers }}</span>
+                  <span class="text-gray-500 text-xs">{{ scope.row.ratio }}</span>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div v-if="showDetail1" class="flex justify-end mt-3">
+            <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" background
+              layout="total, prev, pager, next, sizes" :total="tableRows.length" :page-sizes="[5, 10, 20]" />
+          </div>
         </div>
       </Lcard>
     </div>
@@ -293,6 +379,7 @@ onMounted(() => {
     initQualityChart();
     initChart1();
     initChart2();
+    initHorizontalBarChart();
   }, 500)
 });
 const qualityXAxis = ref<string[]>([
@@ -374,9 +461,9 @@ let chart2: echarts.ECharts | null = null;
 function initChart1(): void {
   if (!qdqsRef1.value) return;
   if (chart1) chart1.dispose();
-  
+
   chart1 = echarts.init(qdqsRef1.value);
-  
+
   const option: echarts.EChartsOption = {
     series: [
       {
@@ -448,7 +535,7 @@ function initChart1(): void {
       ]
     }
   };
-  
+
   chart1.setOption(option);
 }
 
@@ -456,9 +543,9 @@ function initChart1(): void {
 function initChart2(): void {
   if (!qdqsRef2.value) return;
   if (chart2) chart2.dispose();
-  
+
   chart2 = echarts.init(qdqsRef2.value);
-  
+
   const option: echarts.EChartsOption = {
     series: [
       {
@@ -530,7 +617,7 @@ function initChart2(): void {
       ]
     }
   };
-  
+
   chart2.setOption(option);
 }
 
@@ -554,6 +641,440 @@ const progressArray = ref([
   },
 ])
 
+
+const timeGranularity = ref('week')
+
+const weekColumns = ref<any[]>([
+  {
+    title: '时间',
+    width: '140px',
+    align: 'center',
+    dataIndex: 'date',
+  },
+  {
+    title: '新增用户',
+    width: '120px',
+    align: 'center',
+    dataIndex: 'newUsers',
+  },
+  {
+    title: '1天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day1',
+  },
+  {
+    title: '2天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day2',
+  },
+  {
+    title: '3天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day3',
+  },
+  {
+    title: '4天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day4',
+  },
+  {
+    title: '5天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day5',
+  },
+  {
+    title: '6天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day6',
+  },
+  {
+    title: '7天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day7',
+  },
+  {
+    title: '14天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day14',
+  },
+  {
+    title: '30天后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'day30',
+  },
+]);
+
+const dayColumns = ref<any[]>([
+  {
+    title: '时间',
+    width: '140px',
+    align: 'center',
+    dataIndex: 'date',
+  },
+  {
+    title: '新增用户',
+    width: '120px',
+    align: 'center',
+    dataIndex: 'newUsers',
+  },
+  {
+    title: '1周后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'week1',
+  },
+  {
+    title: '2周后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'week2',
+  },
+  {
+    title: '3周后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'week3',
+  },
+  {
+    title: '4周后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'week4',
+  },
+  {
+    title: '8周后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'week8',
+  },
+  {
+    title: '12周后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'week12',
+  },
+]);
+
+const monthColumns = ref<any[]>([
+  {
+    title: '时间',
+    width: '140px',
+    align: 'center',
+    dataIndex: 'date',
+  },
+  {
+    title: '新增用户',
+    width: '120px',
+    align: 'center',
+    dataIndex: 'newUsers',
+  },
+  {
+    title: '1月后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'month1',
+  },
+  {
+    title: '2月后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'month2',
+  },
+  {
+    title: '3月后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'month3',
+  },
+  {
+    title: '6月后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'month6',
+  },
+  {
+    title: '12月后',
+    width: '100px',
+    align: 'center',
+    dataIndex: 'month12',
+  },
+]);
+
+// 动态表格数据
+const weekTableData = ref<any[]>([
+  {
+    date: '2025-08-01',
+    newUsers: 1000,
+    day1: '45.2%',
+    day2: '32.1%',
+    day3: '28.5%',
+    day4: '25.8%',
+    day5: '23.4%',
+    day6: '21.7%',
+    day7: '20.3%',
+    day14: '15.6%',
+    day30: '12.8%',
+  },
+  {
+    date: '2025-08-02',
+    newUsers: 950,
+    day1: '46.8%',
+    day2: '33.2%',
+    day3: '29.1%',
+    day4: '26.5%',
+    day5: '24.2%',
+    day6: '22.8%',
+    day7: '21.5%',
+    day14: '16.3%',
+  },
+  {
+    date: '2025-08-02',
+    newUsers: 950,
+    day1: '46.8%',
+    day2: '33.2%',
+    day3: '29.1%',
+    day4: '26.5%',
+    day5: '24.2%',
+    day6: '22.8%',
+    day7: '21.5%',
+  },
+  {
+    date: '2025-08-02',
+    newUsers: 950,
+    day1: '46.8%',
+    day2: '33.2%',
+    day3: '29.1%',
+    day4: '26.5%',
+    day5: '24.2%',
+    day6: '22.8%',
+  },
+  {
+    date: '2025-08-02',
+    newUsers: 950,
+    day1: '46.8%',
+    day2: '33.2%',
+    day3: '29.1%',
+    day4: '26.5%',
+    day5: '24.2%',
+  },
+  {
+    date: '2025-08-02',
+    newUsers: 950,
+    day1: '46.8%',
+    day2: '33.2%',
+    day3: '29.1%',
+    day4: '26.5%',
+  },
+  {
+    date: '2025-08-02',
+    newUsers: 950,
+    day1: '46.8%',
+    day2: '33.2%',
+    day3: '29.1%',
+  },
+  {
+    date: '2025-08-02',
+    newUsers: 950,
+    day1: '46.8%',
+    day2: '33.2%',
+  },
+]);
+
+const dayTableData = ref<any[]>([
+  {
+    date: '2025-08-01',
+    newUsers: 7000,
+    week1: '35.2%',
+    week2: '28.1%',
+    week3: '24.5%',
+    week4: '22.8%',
+    week8: '18.4%',
+    week12: '15.7%',
+  },
+  {
+    date: '2025-08-08',
+    newUsers: 6800,
+    week1: '36.8%',
+    week2: '29.2%',
+    week3: '25.1%',
+    week4: '23.5%',
+    week8: '19.2%',
+    week12: '16.3%',
+  },
+]);
+
+const monthTableData = ref<any[]>([
+  {
+    date: '2025-08-01',
+    newUsers: 30000,
+    month1: '25.2%',
+    month2: '20.1%',
+    month3: '18.5%',
+    month6: '15.8%',
+    month12: '12.4%',
+  },
+  {
+    date: '2025-09-01',
+    newUsers: 32000,
+    month1: '26.8%',
+    month2: '21.2%',
+    month3: '19.1%',
+    month6: '16.5%',
+    month12: '13.2%',
+  },
+]);
+
+// 当前选中的表格数据
+const currentTableData = computed(() => {
+  switch (timeGranularity.value) {
+    case 'day':
+      return dayTableData.value;
+    case 'week':
+      return weekTableData.value;
+    case 'month':
+      return monthTableData.value;
+    default:
+      return dayTableData.value;
+  }
+});
+
+// 当前选中的表格列
+const currentTableColumns = computed(() => {
+  switch (timeGranularity.value) {
+    case 'day':
+      return dayColumns.value;
+    case 'week':
+      return weekColumns.value;
+    case 'month':
+      return monthColumns.value;
+    default:
+      return dayColumns.value;
+  }
+});
+
+const formData = ref<Record<string, any>>({});
+const query = () => {
+  console.log(formData.value);
+};
+
+// 表格相关(静态数据)
+const tableRows1 = ref<TableRow[]>(
+  Array.from({ length: 42 }).map((_, idx) => ({
+    date: `2025-08-${String(11).padStart(2, '0')}`,
+    newUsers: 727,
+    hyyh: '115',
+    ratio: '97.45%',
+  }))
+);
+
+const currentPage1 = ref(1);
+const pageSize1 = ref(5);
+
+function getCellStyle(dataIndex: string, rowText: any) {
+  if (dataIndex === 'date' || dataIndex === 'newUsers' || !rowText) {
+    return '';
+  }
+  return 'flex justify-center items-center absolute left-0 right-0 top-0 bottom-0 bg-[#e6f7ff]';
+}
+
+
+// 渠道新增横向柱状图
+// 横向柱状图相关
+const horizontalBarChartRef = ref(null);
+let horizontalBarChart: echarts.ECharts | null = null;
+
+// 初始化横向柱状图
+function initHorizontalBarChart(): void {
+  if (!horizontalBarChartRef.value) return;
+  if (horizontalBarChart) horizontalBarChart.dispose();
+  
+  horizontalBarChart = echarts.init(horizontalBarChartRef.value);
+  
+  // 数据
+  const channels = ['App Store', '应用宝', '华为应用市场', '小米应用商店', 'OPPO软件商店', 'vivo应用商店'];
+  const newUserCounts = [12500, 9800, 7600, 6800, 5400, 4200];
+  const retentionRates = [45.2, 38.7, 32.5, 41.3, 36.8, 34.2];
+  
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {
+      data: ['新增用户数', '留存率(%)'],
+      top: '2%'
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: [
+      {
+        type: 'value',
+        name: '新增用户数',
+        position: 'top',
+        nameLocation: 'middle',
+        nameGap: 30
+      },
+      {
+        type: 'value',
+        name: '留存率(%)',
+        min: 0,
+        max: 50,
+        position: 'bottom',
+        nameLocation: 'middle',
+        nameGap: 30,
+        axisLabel: {
+          formatter: '{value} %'
+        }
+      }
+    ],
+    yAxis: {
+      type: 'category',
+      data: channels,
+      axisTick: {
+        alignWithLabel: true
+      }
+    },
+    series: [
+      {
+        name: '新增用户数',
+        type: 'bar',
+        barWidth: '20%',
+        data: newUserCounts,
+        itemStyle: {
+          color: '#409eff'
+        }
+      },
+      {
+        name: '留存率(%)',
+        type: 'bar',
+        barWidth: '20%',
+        xAxisIndex: 1,
+        data: retentionRates,
+        itemStyle: {
+          color: '#67c23a'
+        }
+      }
+    ]
+  };
+  
+  horizontalBarChart.setOption(option);
+}
+
 </script>
 <style lang="scss" scoped>
 .link {

+ 11 - 0
src/views/count/featureUsage/accessPath/i18n/en.ts

@@ -0,0 +1,11 @@
+export default {
+	addUser: {
+		analytics:'Added user analysis',
+		growth:'Go to U-Growth to text/push',
+		ai:'Generative AI intelligence',
+		addtrend:'New trends',
+		channel:'Select a channel',
+		userQuality:'User quality',
+		average:'Industry average'
+	},
+};

+ 13 - 0
src/views/count/featureUsage/accessPath/i18n/zh-cn.ts

@@ -0,0 +1,13 @@
+export default {
+	active: {
+		analytics:'用户活跃度',
+		growth:'前往U-Growth发短信/Push',
+		ai:'查看完整AI简报',
+		addtrend:'新增趋势',
+		version:'选择版本',
+		userQuality:'用户质量',
+		average:'行业平均值',
+		selectTime:'选择时间',
+		retained:'留存趋势'
+	},
+};

+ 391 - 0
src/views/count/featureUsage/accessPath/index.vue

@@ -0,0 +1,391 @@
+<template>
+	<div class="layout-padding">
+		<div class="!overflow-auto pl-1">
+			<!-- 顶部控制区域 -->
+			<div class="mb-2 el-card p-2">
+				<div class="flex items-center mb-4">
+					<Title title="页面访问路径" />
+				</div>
+
+				<div class="flex items-center justify-between space-x-4">
+					<div class="flex items-center">
+						<el-select v-model="formData.selectedChannelCompare" class="!w-[180px]" placeholder="版本选择">
+							<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+						</el-select>
+						<el-button type="primary" class="ml-2"
+							><el-icon class="mr-2"><Platform /></el-icon> 管理版本</el-button
+						>
+					</div>
+					<div class="flex items-center">
+						<el-button class="mr-2" type="primary" plain>昨日</el-button>
+						<el-date-picker v-model="formData.time" class="w-[200px]" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" />
+					</div>
+				</div>
+			</div>
+			<div class="el-card p-2">
+				<div class="flex justify-between items-center mb-2">
+					<Title left-line title="访问路径">
+						<template #default>
+							<el-popover class="box-item" placement="right" trigger="hover" width="600">
+								<template #reference>
+									<el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
+								</template>
+								<template #default>
+									<div class="ant-popover-inner-content">
+										<div style="padding: 8px 16px">
+											<span
+												class="um-vc-text"
+												title="页面访问路径描述的是用户从打开到离开应用整个过程中每一步骤的页面访问、跳转情况。页面访问路径是全量统计。如果您在Android应用中使用了Fragment页面统计功能,这里的页面包括您指定统计的activity和Fragment。"
+												style="color: rgb(0, 0, 0); font-size: 12px"
+												>页面访问路径描述的是用户从打开到离开应用整个过程中每一步骤的页面访问、跳转情况。页面访问路径是全量统计。如果您在Android应用中使用了Fragment页面统计功能,这里的页面包括您指定统计的activity和Fragment。</span
+											><span
+												class="um-vc-text"
+												title="页面的高度表现该页面被访问的次数,同一页面在不同步骤中用相同的颜色进行展示。"
+												style="color: rgb(0, 0, 0); font-size: 12px"
+												>页面的高度表现该页面被访问的次数,同一页面在不同步骤中用相同的颜色进行展示。</span
+											><span
+												class="um-vc-text"
+												title="每一步骤中,页面节点按照访问次数大小从上往下排列,会显示每一步总的页面访问次数、占总访问次数比例以及前后两步之间的转化率。"
+												style="color: rgb(0, 0, 0); font-size: 12px"
+												>每一步骤中,页面节点按照访问次数大小从上往下排列,会显示每一步总的页面访问次数、占总访问次数比例以及前后两步之间的转化率。</span
+											><span
+												class="um-vc-text"
+												title="如果页面的总会话数达到50W上限,或者单版本会话数达到10W上限,会进行日志抽样处理。"
+												style="color: rgb(0, 0, 0); font-size: 12px"
+												>如果页面的总会话数达到50W上限,或者单版本会话数达到10W上限,会进行日志抽样处理。</span
+											>
+										</div>
+									</div>
+								</template>
+							</el-popover>
+						</template>
+					</Title>
+					<div class="flex items-center">
+						<el-radio-group v-model="formData.selectedType" class="!flex items-center">
+							<el-radio-button label="1">描述</el-radio-button>
+							<el-radio-button label="2">原名</el-radio-button>
+						</el-radio-group>
+						<el-button link class="ml-2" type="primary">编辑页面描述</el-button>
+					</div>
+				</div>
+				<!-- 顶部统计方块 -->
+				<div class="mb-3 flex items-stretch flex-wrap gap-4">
+					<div class="min-w-[120px]">
+						<div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">第1步</div>
+						<div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
+							<div class="text-[20px] leading-6 font-medium">66,254</div>
+						</div>
+					</div>
+					<div class="flex items-center text-[#3b82f6] text-[13px]">&gt;&nbsp;99.19%</div>
+					<div class="min-w-[140px]">
+						<div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">第2步</div>
+						<div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
+							<div class="text-[20px] leading-6 font-medium">66,770<span class="text-[12px] text-gray-500">(99%)</span></div>
+						</div>
+					</div>
+					<div class="flex items-center text-[#3b82f6] text-[13px]">&gt;&nbsp;0.00%</div>
+					<div class="min-w-[140px]">
+						<div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">第3步</div>
+						<div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
+							<div class="text-[20px] leading-6 font-medium">156<span class="text-[12px] text-gray-500">(19%)</span></div>
+						</div>
+					</div>
+					<div class="flex items-center text-[#3b82f6] text-[13px]">&gt;&nbsp;0.00%</div>
+					<div class="min-w-[140px]">
+						<div class="bg-[#f5f7fa] text-[12px] text-gray-500 px-3 py-1 rounded-t">结束</div>
+						<div class="border border-[#e5e7eb] rounded-b px-3 py-2 text-[#111827]">
+							<div class="text-[20px] leading-6 font-medium">156<span class="text-[12px] text-gray-500">(19%)</span></div>
+						</div>
+					</div>
+				</div>
+				<!-- 主图表区域 -->
+				<div class="mb-4 overflow-x-auto">
+					<div ref="mainChartRef" style="width: 100%; height: 600px"></div>
+				</div>
+			</div>
+			<div class="el-card p-2 mt-2">
+				<div class="flex justify-between items-center mb-2">
+					<Title left-line title="访问详情">
+						<template #default>
+							<el-popover class="box-item" placement="right" trigger="hover" width="500">
+								<template #reference>
+									<el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
+								</template>
+								<template #default>
+									<div class="ant-popover-inner-content">
+										<div style="padding: 8px 16px; ">
+											<span
+												class="um-vc-text"
+												title="页面访问详情展示了用户使用每个页面的使用次数、访问时长以及跳转情况,这些数据可以帮助您分析每个页面的使用情况。"
+												style="color: rgb(0, 0, 0); font-size: 12px"
+												>页面访问详情展示了用户使用每个页面的使用次数、访问时长以及跳转情况,这些数据可以帮助您分析每个页面的使用情况。</span
+											>
+											<div>
+												<span class="um-vc-text" title="访问次数:" style="color: rgb(33, 150, 243); font-size: 12px">访问次数:</span
+												><span class="um-vc-text" title="用户进入当前页面的总次数" style="color: rgb(0, 0, 0); font-size: 12px"
+													>用户进入当前页面的总次数</span
+												>
+											</div>
+											<div>
+												<span class="um-vc-text" title="访问次数占比:" style="color: rgb(33, 150, 243); font-size: 12px">访问次数占比:</span
+												><span class="um-vc-text" title="当前页面访问次数占全部页面访问次数的比例" style="color: rgb(0, 0, 0); font-size: 12px"
+													>当前页面访问次数占全部页面访问次数的比例</span
+												>
+											</div>
+											<div>
+												<span class="um-vc-text" title="平均访问时长:" style="color: rgb(33, 150, 243); font-size: 12px">平均访问时长:</span
+												><span class="um-vc-text" title="用户每次进入当前页面的平均停留时长" style="color: rgb(0, 0, 0); font-size: 12px"
+													>用户每次进入当前页面的平均停留时长</span
+												>
+											</div>
+											<div>
+												<span class="um-vc-text" title="访问时长占比:" style="color: rgb(33, 150, 243); font-size: 12px">访问时长占比:</span
+												><span
+													class="um-vc-text"
+													title="用户在当前页面停留时间总和占用户在全体页面停留的时间总和的比例"
+													style="color: rgb(0, 0, 0); font-size: 12px"
+													>用户在当前页面停留时间总和占用户在全体页面停留的时间总和的比例</span
+												>
+											</div>
+											<div>
+												<span class="um-vc-text" title="跳出率:" style="color: rgb(33, 150, 243); font-size: 12px">跳出率:</span
+												><span class="um-vc-text" title="用户从当前页面离开应用的比例" style="color: rgb(0, 0, 0); font-size: 12px"
+													>用户从当前页面离开应用的比例</span
+												>
+											</div>
+											<div>
+												<span class="um-vc-text" title="跳转情况:" style="color: rgb(33, 150, 243); font-size: 12px">跳转情况:</span
+												><span class="um-vc-text" title="用户从当前页面进入其他页面的概率分布情况" style="color: rgb(0, 0, 0); font-size: 12px"
+													>用户从当前页面进入其他页面的概率分布情况</span
+												>
+											</div>
+										</div>
+									</div>
+								</template>
+							</el-popover>
+						</template>
+					</Title>
+					<div class="flex items-center">
+						
+						<el-button link class="ml-2" type="primary">导出</el-button>
+					</div>
+				</div>
+				<!-- 明细表格 -->
+				<div class="mt-3">
+					<div class="flex items-center justify-between mb-2">
+						<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
+							{{ showDetail1 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown v-if="showDetail1" /> <ArrowUp v-else /> </el-icon>
+						</div>
+						<div>
+							<el-button>导出</el-button>
+						</div>
+					</div>
+					<el-table v-if="showDetail1" :data="pagedTableRows" border>
+						<el-table-column prop="date" label="页面(Activity/Fragment)" align="center" min-width="140" />
+						<el-table-column prop="hyyh" label="描述" align="center" min-width="140" />
+						<el-table-column prop="hyyh" label="访问次数(占比)" align="center" min-width="140" />
+						<el-table-column prop="hyyh" label="平均访问时长(占比)" align="center" min-width="140" />
+						<el-table-column prop="ratio" label="跳出率" align="center" min-width="220" />
+					</el-table>
+					<div v-if="showDetail1" class="flex justify-end mt-2">
+						<el-pagination
+							v-model:current-page="currentPage"
+							v-model:page-size="pageSize"
+							background
+							layout="total, prev, pager, next, sizes"
+							:total="tableRows.length"
+							:page-sizes="[5, 10, 20]"
+						/>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed, defineAsyncComponent, onBeforeUnmount } from 'vue';
+import * as echarts from 'echarts';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+interface TableRow {
+	date: string;
+	newUsers: number;
+	ratio: string;
+}
+const formData = ref<Record<string, any>>({
+	selectedChannelCompare: '',
+});
+const channelCompareOptions = [
+	{ label: '全部版本', value: '' },
+	{ label: '1.0', value: '1.0' },
+	{ label: '2.0', value: '2.0' },
+];
+
+// Sankey 图表
+const mainChartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: echarts.ECharts | null = null;
+
+function initSankey(): void {
+	if (!mainChartRef.value) return;
+	if (chartInstance) chartInstance.dispose();
+	chartInstance = echarts.init(mainChartRef.value);
+
+	// 左列来源节点(带计数)
+	const leftNodes = [
+		{ name: 'Ac_Search (13459)', itemStyle: { color: '#cfe8f3' } },
+		{ name: 'Ac_CourseDetail (13459)', itemStyle: { color: '#dbeafe' } },
+		{ name: 'Fr_SearchKeywords (13459)', itemStyle: { color: '#e9d5ff' } },
+		{ name: 'Ac_Search (13459)#2', itemStyle: { color: '#e0f2fe' } },
+		{ name: 'Ac_CourseDetail (13459)#2', itemStyle: { color: '#dcfce7' } },
+		{ name: '其他 (13459)', itemStyle: { color: '#fef3c7' } },
+	];
+
+	// 中列节点(重复的视频播放目标)
+	const middleNodes = Array.from({ length: 10 }).map((_, i) => ({
+		name: `Ac_VideoPlay (13459)#${i + 1}`,
+		itemStyle: { color: ['#e5e7eb', '#fde68a', '#bbf7d0', '#bfdbfe', '#fbcfe8'][i % 5] },
+	}));
+
+	// 右列汇总/结束节点
+	const rightNodes = [
+		{ name: 'Step3 总计 (156 / 49%)', itemStyle: { color: '#e5e7eb' } },
+		{ name: 'Step4 结束 (158 / 50%)', itemStyle: { color: '#f3f4f6' } },
+	];
+
+	const nodes = [...leftNodes, ...middleNodes, ...rightNodes];
+
+	// 构造链接:左 -> 中,多条细流;中 -> 右 少量
+	const links: Array<{ source: string; target: string; value: number }> = [];
+	const leftToMidValues = [3400, 3000, 2800, 2600, 2000, 1500];
+	leftNodes.forEach((ln, li) => {
+		middleNodes.forEach((mn, mi) => {
+			// 让靠前的中间节点获得更多流量,形成图中“多条细灰线”效果
+			const base = leftToMidValues[li % leftToMidValues.length];
+			const decay = Math.max(0.1, 1 - mi * 0.08);
+			const v = Math.round((base * decay) / 10); // 保持细流
+			if (v > 0) links.push({ source: ln.name, target: mn.name, value: v });
+		});
+	});
+
+	// 中 -> 右,较小比例收敛
+	middleNodes.forEach((mn, idx) => {
+		const v1 = 20 + Math.max(0, 60 - idx * 5);
+		const v2 = 10 + Math.max(0, 40 - idx * 4);
+		links.push({ source: mn.name, target: rightNodes[0].name, value: v1 });
+		links.push({ source: mn.name, target: rightNodes[1].name, value: v2 });
+	});
+
+	const option: echarts.EChartsOption = {
+		tooltip: {
+			trigger: 'item',
+			formatter: (p: any) => {
+				if (p.dataType === 'edge') {
+					return `${p.data.source} → ${p.data.target}<br/>${p.data.value}`;
+				}
+				return p.name;
+			},
+		},
+		series: [
+			{
+				type: 'sankey',
+				data: nodes,
+				links,
+				left: 60,
+				right: 60,
+				top: 6,
+				bottom: 6,
+				nodeWidth: 14,
+				nodeGap: 6,
+				nodeAlign: 'justify',
+				layoutIterations: 0,
+				draggable: false,
+				label: {
+					color: '#475569',
+					fontSize: 11,
+					formatter: (p: any) => {
+						const m = /^(.*) \((\d+)\)/.exec(p.name);
+						if (!m) return p.name;
+						return `{name|${m[1]}} {count|(${m[2]})}`;
+					},
+					rich: {
+						name: { color: '#374151', fontSize: 11 },
+						count: { color: '#9ca3af', fontSize: 10 },
+					},
+				},
+				itemStyle: { borderColor: '#dbe3ec', borderWidth: 1 },
+				lineStyle: { color: '#cbd5e1', curveness: 0.35, opacity: 0.28 },
+				emphasis: { focus: 'adjacency', lineStyle: { opacity: 0.6 } },
+				levels: [
+					{ depth: 0, itemStyle: { color: '#dbeafe' } },
+					{ depth: 1, itemStyle: { color: '#f1f5f9' } },
+					{ depth: 2, itemStyle: { color: '#f3f4f6' } },
+				],
+			},
+		],
+	};
+
+	chartInstance.setOption(option);
+}
+
+// 表格相关(静态数据)
+const tableRows = ref<TableRow[]>(
+	Array.from({ length: 42 }).map((_, idx) => ({
+		date: `2025-08-${String(11).padStart(2, '0')}`,
+		newUsers: 727,
+		hyyh: '115',
+		ratio: '97.45%',
+	}))
+);
+
+const currentPage = ref(1);
+const pageSize = ref(5);
+const pagedTableRows = computed(() => {
+	const startIndex = (currentPage.value - 1) * pageSize.value;
+
+	return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+
+onMounted(() => {
+	setTimeout(() => {
+		initSankey();
+		window.addEventListener('resize', () => chartInstance && chartInstance.resize());
+	}, 300);
+});
+
+onBeforeUnmount(() => {
+	if (chartInstance) {
+		chartInstance.dispose();
+		chartInstance = null;
+	}
+});
+
+// 展开/收起明细
+const showDetail1 = ref(true);
+</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>

+ 5 - 6
src/views/count/main/trend/index.vue

@@ -192,16 +192,15 @@ const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue')
 const InfoCard = defineAsyncComponent(() => import('./infoCard.vue'));
 const { t } = useI18n();
 const timeRange = ref(null);
+const qChartRef = ref<HTMLDivElement | null>(null);
 let qualityChart: echarts.ECharts | null = null;
-const qChartRef = ref(null);
-const lineChartUser = ref(null);
+
 onMounted(() => {
    setTimeout(() => {
       initQualityChart();
-      initCircleChart1();
-      initCircleChart2();
-   }, 500)
-   console.log(qualityChart);
+      initCircleChart2()
+      initCircleChart1()
+   }, 500);
 });
 const qualityXAxis = ref<string[]>([
    '2025-07-01',

+ 11 - 0
src/views/count/retained/active/i18n/en.ts

@@ -0,0 +1,11 @@
+export default {
+	addUser: {
+		analytics:'Added user analysis',
+		growth:'Go to U-Growth to text/push',
+		ai:'Generative AI intelligence',
+		addtrend:'New trends',
+		channel:'Select a channel',
+		userQuality:'User quality',
+		average:'Industry average'
+	},
+};

+ 13 - 0
src/views/count/retained/active/i18n/zh-cn.ts

@@ -0,0 +1,13 @@
+export default {
+	active: {
+		analytics:'用户活跃度',
+		growth:'前往U-Growth发短信/Push',
+		ai:'查看完整AI简报',
+		addtrend:'新增趋势',
+		version:'选择版本',
+		userQuality:'用户质量',
+		average:'行业平均值',
+		selectTime:'选择时间',
+		retained:'留存趋势'
+	},
+};

+ 510 - 0
src/views/count/retained/active/index.vue

@@ -0,0 +1,510 @@
+<template>
+	<div class="layout-padding">
+		<div class="!overflow-auto px-1">
+			<div class="el-card p-2">
+				<!-- 顶部控制区域 -->
+				<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>

+ 11 - 0
src/views/count/retained/freshness/i18n/en.ts

@@ -0,0 +1,11 @@
+export default {
+	addUser: {
+		analytics:'Added user analysis',
+		growth:'Go to U-Growth to text/push',
+		ai:'Generative AI intelligence',
+		addtrend:'New trends',
+		channel:'Select a channel',
+		userQuality:'User quality',
+		average:'Industry average'
+	},
+};

+ 13 - 0
src/views/count/retained/freshness/i18n/zh-cn.ts

@@ -0,0 +1,13 @@
+export default {
+	freshness: {
+		title:'用户新鲜度',
+		growth:'前往U-Growth发短信/Push',
+		ai:'查看完整AI简报',
+		addtrend:'新增趋势',
+		version:'选择版本',
+		userQuality:'用户质量',
+		average:'行业平均值',
+		selectTime:'选择时间',
+		retained:'留存趋势'
+	},
+};

+ 494 - 0
src/views/count/retained/freshness/index.vue

@@ -0,0 +1,494 @@
+<template>
+	<div class="layout-padding">
+		<div class="!overflow-auto px-1">
+			<div class="el-card p-2">
+				<!-- 顶部控制区域 -->
+				<div class=" mb-4">
+					<div class="flex items-center mb-4">
+						<Title :title="t('freshness.title')">
+							<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" style="width: 340px">
+												<p>
+													<span
+														>报表展示每天活跃用户的成分构成,并提供用户成分分析控件做进一步的分析。用户新鲜度帮您从宏观上了解每日启动用户的新老用户比以及来源结构。</span
+													>
+												</p>
+												<p>
+													<span
+														>某日的活跃用户来源于当天新增用户、1天前新增用户...30天前新增用户、30+天前新增用户。其中当天新增用户与您在当日的推广行为相关,n天前新增用户与n日前的新增用户和n日留存率有关。</span
+													>
+												</p>
+											</div>
+										</div>
+									</template>
+								</el-popover>
+							</template>
+						</Title>
+					</div>
+					<div class="flex items-center space-x-4">
+						<!-- 显示模式切换 -->
+						<div class="flex items-center">
+							<el-radio-group v-model="displayMode" >
+								<el-radio-button label="absolute">绝对值</el-radio-button>
+								<el-radio-button label="percentage">百分比</el-radio-button>
+							</el-radio-group>
+						</div>
+
+						<!-- 配色选择 -->
+						<div class="flex items-center">
+							<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">
+							<el-checkbox v-model="userCompositionAnalysis">用户成分分析:</el-checkbox>
+							<div class="ml-2 relative">
+								<div class="w-32 h-2 bg-gray-200 rounded-full relative">
+									<!-- 已选择区域 -->
+									<div
+										class="absolute top-0 h-full bg-blue-500 rounded-full"
+										:style="{
+											left: `${startPosition}%`,
+											width: `${endPosition - startPosition}%`,
+										}"
+									></div>
+
+									<!-- 开始拖拽手柄 -->
+									<div
+										class="absolute w-2 h-2 bg-white border top-1 border-gray-300 rounded-full cursor-pointer transform -translate-y-1"
+										:style="{ left: `calc(${startPosition}% - 4px)` }"
+										@mousedown="startDrag('start')"
+									></div>
+
+									<!-- 结束拖拽手柄 -->
+									<div
+										class="absolute w-2 h-2 bg-white border top-1 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>
+
+						<!-- 导出按钮 -->
+						<el-button type="primary" size="small">
+							导出
+						</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';
+
+const { t } = useI18n();
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+
+// 控制状态
+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);
+}
+
+.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>

+ 11 - 0
src/views/count/retained/user/i18n/en.ts

@@ -0,0 +1,11 @@
+export default {
+	addUser: {
+		analytics:'Added user analysis',
+		growth:'Go to U-Growth to text/push',
+		ai:'Generative AI intelligence',
+		addtrend:'New trends',
+		channel:'Select a channel',
+		userQuality:'User quality',
+		average:'Industry average'
+	},
+};

+ 13 - 0
src/views/count/retained/user/i18n/zh-cn.ts

@@ -0,0 +1,13 @@
+export default {
+	retainedUser: {
+		analytics:'留存用户',
+		growth:'前往U-Growth发短信/Push',
+		ai:'查看完整AI简报',
+		addtrend:'新增趋势',
+		version:'选择版本',
+		userQuality:'用户质量',
+		average:'行业平均值',
+		selectTime:'选择时间',
+		retained:'留存趋势'
+	},
+};

+ 579 - 0
src/views/count/retained/user/index.vue

@@ -0,0 +1,579 @@
+<template>
+	<div class="layout-padding">
+		<div class="!overflow-auto px-1">
+			<div class="el-card p-2">
+				<div class="flex justify-between">
+					<Title :title="t('retainedUser.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" style="line-height: 24px">
+											<p>
+												某段时间内的新增用户(活跃用户),经过一段时间后,又继续使用应用的被认作是留存用户;这部分用户占当时新增用户(活跃用户)的比例即是留存率。例如,5月份新增用户200,这200人在6月份启动过应用的有100人,7月份启动过应用的有80人,8月份启动过应用的有50人;则5月新增用户一个月后的留存率是50%,两个月后的留存率是40%,三个月后的留存率是25%。
+											</p>
+											<p>注:“活跃用户留存”仅支持查看2018年4月1日之后的留存情况</p>
+										</div>
+									</div>
+								</template>
+							</el-popover>
+						</template>
+					</Title>
+					<div class="">
+						<el-button type="primary">{{ t('activeUser.growth') }}</el-button>
+						<el-button type="primary">{{ t('activeUser.ai') }}</el-button>
+					</div>
+				</div>
+				<div>
+					<el-row shadow="hover" class="ml10 mt-2">
+						<el-form :inline="true" :model="formData" @keyup.enter="query" ref="queryRef">
+							<el-form-item>
+								<el-date-picker v-model="formData.time" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" />
+							</el-form-item>
+							<el-form-item>
+								<el-select v-model="selectedChannelCompare" class="w-[140px]" placeholder="全部渠道">
+									<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+								</el-select>
+							</el-form-item>
+							<el-form-item>
+								<el-select v-model="selectedChannelCompare" class="w-[140px]" placeholder="全部版本">
+									<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+								</el-select>
+							</el-form-item>
+						</el-form>
+					</el-row>
+				</div>
+			</div>
+			<div class="mt-2 el-card p-2">
+				<div class="border-b pb-4">
+					<div class="flex items-center justify-between mb-2 mt-3">
+						<div>
+							<el-select v-model="selectedChannelCompare" class="w-[140px] ml-2" style="width: 140px" placeholder="版本对比">
+								<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+						</div>
+
+						<div class="flex items-center">
+							<el-radio-group v-model="timeGranularity">
+								<el-radio-button label="day">天</el-radio-button>
+								<el-radio-button label="week">周</el-radio-button>
+								<el-radio-button label="month">月</el-radio-button>
+							</el-radio-group>
+							<el-radio-group v-model="timeGranularity" class="ml-2">
+								<el-radio-button label="day">留存率</el-radio-button>
+								<el-radio-button label="week">留存数</el-radio-button>
+							</el-radio-group>
+							<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" style="line-height: 24px">
+											<p><span class="highlight">留存率:</span> 某段时间内的新增用户(活跃用户),经过一段时间后,又继续使用应用的比例</p>
+											<p><span class="highlight">留存数:</span> 某段时间内的新增用户(活跃用户),经过一段时间后,又继续使用应用的用户数</p>
+										</div>
+									</div>
+								</template>
+							</el-popover>
+							<el-button class="ml-2">导出</el-button>
+						</div>
+					</div>
+
+					<div class="relative">
+						<el-table :data="currentTableData" border>
+							<el-table-column
+								v-for="column in currentTableColumns"
+								:key="column.dataIndex"
+								:prop="column.dataIndex"
+								:label="column.title"
+								:align="column.align"
+								:min-width="column.width"
+							>
+								<template #default="scope">
+									<span :class="getCellStyle(column.dataIndex, scope.row[column.dataIndex])">
+										{{ scope.row[column.dataIndex] }}
+									</span>
+								</template>
+							</el-table-column>
+						</el-table>
+					</div>
+					<div v-if="showDetail1" class="flex justify-end mt-2">
+						<el-pagination
+							v-model:current-page="currentPage"
+							v-model:page-size="pageSize"
+							background
+							layout="total, prev, pager, next, sizes"
+							:total="tableRows.length"
+							:page-sizes="[5, 10, 20]"
+						/>
+					</div>
+				</div>
+
+				<div class="flex items-center justify-between mb-2 mt-4">
+					<Title left-line :title="t('retainedUser.retained')" />
+					<div class="flex items-center w-[140px]">
+						<el-select v-model="selectedChannelCompare" class="w-[140px]" placeholder="全部版本">
+							<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+						</el-select>
+					</div>
+				</div>
+
+				<div class="relative">
+					<div ref="lineChartRef" style="width: 100%; height: 320px"></div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import * as echarts from 'echarts';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+
+interface TableRow {
+	date: string;
+	newUsers: number;
+	hyyh: string;
+	ratio: string;
+}
+
+const dayColumns = ref<any[]>([
+	{
+		title: '时间',
+		width: '140px',
+		align: 'center',
+		dataIndex: 'date',
+	},
+	{
+		title: '新增用户',
+		width: '120px',
+		align: 'center',
+		dataIndex: 'newUsers',
+	},
+	{
+		title: '1天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day1',
+	},
+	{
+		title: '2天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day2',
+	},
+	{
+		title: '3天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day3',
+	},
+	{
+		title: '4天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day4',
+	},
+	{
+		title: '5天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day5',
+	},
+	{
+		title: '6天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day6',
+	},
+	{
+		title: '7天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day7',
+	},
+	{
+		title: '14天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day14',
+	},
+	{
+		title: '30天后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'day30',
+	},
+]);
+
+const weekColumns = ref<any[]>([
+	{
+		title: '时间',
+		width: '140px',
+		align: 'center',
+		dataIndex: 'date',
+	},
+	{
+		title: '新增用户',
+		width: '120px',
+		align: 'center',
+		dataIndex: 'newUsers',
+	},
+	{
+		title: '1周后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'week1',
+	},
+	{
+		title: '2周后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'week2',
+	},
+	{
+		title: '3周后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'week3',
+	},
+	{
+		title: '4周后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'week4',
+	},
+	{
+		title: '8周后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'week8',
+	},
+	{
+		title: '12周后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'week12',
+	},
+]);
+
+const monthColumns = ref<any[]>([
+	{
+		title: '时间',
+		width: '140px',
+		align: 'center',
+		dataIndex: 'date',
+	},
+	{
+		title: '新增用户',
+		width: '120px',
+		align: 'center',
+		dataIndex: 'newUsers',
+	},
+	{
+		title: '1月后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'month1',
+	},
+	{
+		title: '2月后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'month2',
+	},
+	{
+		title: '3月后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'month3',
+	},
+	{
+		title: '6月后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'month6',
+	},
+	{
+		title: '12月后',
+		width: '100px',
+		align: 'center',
+		dataIndex: 'month12',
+	},
+]);
+
+// 动态表格数据
+const dayTableData = ref<any[]>([
+	{
+		date: '2025-08-01',
+		newUsers: 1000,
+		day1: '45.2%',
+		day2: '32.1%',
+		day3: '28.5%',
+		day4: '25.8%',
+		day5: '23.4%',
+		day6: '21.7%',
+		day7: '20.3%',
+		day14: '15.6%',
+		day30: '12.8%',
+	},
+	{
+		date: '2025-08-02',
+		newUsers: 950,
+		day1: '46.8%',
+		day2: '33.2%',
+		day3: '29.1%',
+		day4: '26.5%',
+		day5: '24.2%',
+		day6: '22.8%',
+		day7: '21.5%',
+		day14: '16.3%',
+	},
+	{
+		date: '2025-08-02',
+		newUsers: 950,
+		day1: '46.8%',
+		day2: '33.2%',
+		day3: '29.1%',
+		day4: '26.5%',
+		day5: '24.2%',
+		day6: '22.8%',
+		day7: '21.5%',
+	},
+	{
+		date: '2025-08-02',
+		newUsers: 950,
+		day1: '46.8%',
+		day2: '33.2%',
+		day3: '29.1%',
+		day4: '26.5%',
+		day5: '24.2%',
+		day6: '22.8%',
+	},
+	{
+		date: '2025-08-02',
+		newUsers: 950,
+		day1: '46.8%',
+		day2: '33.2%',
+		day3: '29.1%',
+		day4: '26.5%',
+		day5: '24.2%',
+	},
+	{
+		date: '2025-08-02',
+		newUsers: 950,
+		day1: '46.8%',
+		day2: '33.2%',
+		day3: '29.1%',
+		day4: '26.5%',
+	},
+	{
+		date: '2025-08-02',
+		newUsers: 950,
+		day1: '46.8%',
+		day2: '33.2%',
+		day3: '29.1%',
+	},
+	{
+		date: '2025-08-02',
+		newUsers: 950,
+		day1: '46.8%',
+		day2: '33.2%',
+	},
+]);
+
+const weekTableData = ref<any[]>([
+	{
+		date: '2025-08-01',
+		newUsers: 7000,
+		week1: '35.2%',
+		week2: '28.1%',
+		week3: '24.5%',
+		week4: '22.8%',
+		week8: '18.4%',
+		week12: '15.7%',
+	},
+	{
+		date: '2025-08-08',
+		newUsers: 6800,
+		week1: '36.8%',
+		week2: '29.2%',
+		week3: '25.1%',
+		week4: '23.5%',
+		week8: '19.2%',
+		week12: '16.3%',
+	},
+]);
+
+const monthTableData = ref<any[]>([
+	{
+		date: '2025-08-01',
+		newUsers: 30000,
+		month1: '25.2%',
+		month2: '20.1%',
+		month3: '18.5%',
+		month6: '15.8%',
+		month12: '12.4%',
+	},
+	{
+		date: '2025-09-01',
+		newUsers: 32000,
+		month1: '26.8%',
+		month2: '21.2%',
+		month3: '19.1%',
+		month6: '16.5%',
+		month12: '13.2%',
+	},
+]);
+
+// 当前选中的表格数据
+const currentTableData = computed(() => {
+	switch (timeGranularity.value) {
+		case 'day':
+			return dayTableData.value;
+		case 'week':
+			return weekTableData.value;
+		case 'month':
+			return monthTableData.value;
+		default:
+			return dayTableData.value;
+	}
+});
+
+// 当前选中的表格列
+const currentTableColumns = computed(() => {
+	switch (timeGranularity.value) {
+		case 'day':
+			return dayColumns.value;
+		case 'week':
+			return weekColumns.value;
+		case 'month':
+			return monthColumns.value;
+		default:
+			return dayColumns.value;
+	}
+});
+
+const formData = ref<Record<string, any>>({});
+const query = () => {
+	console.log(formData.value);
+};
+
+const selectedChannelCompare = ref('');
+const channelCompareOptions = [
+	{ label: '渠道对比', value: 'compare' },
+	{ label: '渠道A', value: 'a' },
+	{ label: '渠道B', value: 'b' },
+];
+
+// 图表相关
+const timeGranularity = ref<'hour' | 'day' | 'week' | 'month'>('week');
+const lineChartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: echarts.ECharts | null = null;
+
+const lineChartData = ref<Array<{ x: string; value: number }>>([
+	{ x: '2025-07-01', value: 900 },
+	{ x: '2025-07-08', value: 1000 },
+	{ x: '2025-07-15', value: 1100 },
+	{ x: '2025-07-22', value: 1000 },
+	{ x: '2025-07-29', value: 600 },
+	{ x: '2025-08-05', value: 300 },
+	{ x: '2025-08-12', value: 250 },
+	{ x: '2025-08-19', value: 200 },
+	{ x: '2025-08-26', value: 650 },
+	{ x: '2025-09-02', value: 950 },
+	{ x: '2025-09-09', value: 900 },
+	{ x: '2025-09-16', value: 120 },
+]);
+
+function initLineChart(): void {
+	if (!lineChartRef.value) return;
+	if (chartInstance) chartInstance.dispose();
+	chartInstance = echarts.init(lineChartRef.value);
+	const option: echarts.EChartsOption = {
+		tooltip: { trigger: 'axis' },
+		grid: { left: 40, right: 20, top: 20, bottom: 30 },
+		xAxis: {
+			type: 'category',
+			data: lineChartData.value.map((d) => d.x),
+			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',
+				smooth: true,
+				showSymbol: true,
+				symbolSize: 6,
+				itemStyle: { color: '#409EFF' },
+				lineStyle: { color: '#409EFF' },
+				data: lineChartData.value.map((d) => d.value),
+			},
+		],
+	};
+	chartInstance.setOption(option);
+}
+
+onMounted(() => {
+	setTimeout(() => {
+		initLineChart();
+	}, 500);
+});
+
+watch(timeGranularity, () => {
+	// 静态页面:仅重新渲染
+	initLineChart();
+});
+// 表格相关(静态数据)
+const tableRows = ref<TableRow[]>(
+	Array.from({ length: 42 }).map((_, idx) => ({
+		date: `2025-08-${String(11).padStart(2, '0')}`,
+		newUsers: 727,
+		hyyh: '115',
+		ratio: '97.45%',
+	}))
+);
+
+const currentPage = ref(1);
+const pageSize = ref(5);
+
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+
+// 展开/收起明细
+const showDetail1 = ref(true);
+
+function getCellStyle(dataIndex: string, rowText: any) {
+	if (dataIndex === 'date' || dataIndex === 'newUsers' || !rowText) {
+		return '';
+	}
+	return 'flex justify-center items-center absolute left-0 right-0 top-0 bottom-0 bg-[#e6f7ff]';
+}
+</script>
+ 
+<style lang="scss" scoped>
+.highlight {
+	color: #2196f3;
+}
+.el-form-item--default {
+	margin-bottom: 0;
+}
+.el-form.el-form--inline .el-form-item--default.el-form-item:last-of-type,
+.el-form.el-form--inline .el-form-item--small.el-form-item:last-of-type {
+	margin-bottom: 0 !important;
+}
+</style>

+ 19 - 66
src/views/count/user/activations/index.vue

@@ -13,20 +13,16 @@
 									<div class="ant-popover-inner-content">
 										<div class="um-page-tips-content" style="line-height: 24px">
 											<p>
-												<span class="highlight">活跃用户:</span>
-												<span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
+												<span class="highlight">启动次数:</span
+												><span
+													>打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span
+												>
 											</p>
-											<p><span class="highlight">活跃构成:</span><span>活跃用户中新增用户的占比比例</span></p>
-											<p><span class="highlight">活跃粘度:</span><span>DAU/过去7日活跃用户,DAU/过去30日活跃用户</span></p>
-											<p><span class="highlight">过去7日活跃用户:</span><span>过去7日(不含今日)的活跃用户数(去重)</span></p>
-											<p><span class="highlight">过去30日活跃用户:</span><span>过去30日(不含今日)的活跃用户数(去重)</span></p>
-											<p><span class="highlight">分时活跃用户:</span><span>活跃用户在24小时中的分布情况(每小时间去重)&ZeroWidthSpace;</span></p>
-											<p><span class="highlight">周活跃率:</span><span>周活跃用户占截止本周累计用户的比例</span></p>
-											<p><span class="highlight">月活跃率:</span><span>月活跃用户占截止本月累计用户的比例</span></p>
+											<p><span class="highlight">启动次数占比:</span><span>某日/周/月的启动次数占所选时间段总启动次数的比例</span></p>
 											<p>
-												<span>
-													按天、周或月查看数据可进行版本、渠道的交叉筛选。周区间定义为周日至次周周六。按周(按月)显示时,界面上用每周的周日(每个月的第一日)来代表该周(该月)
-												</span>
+												<span
+													>按天、周或月查看数据可进行版本、渠道的交叉筛选,小时数据最多展示7天并且不支持筛选。周区间定义为周日至次周周六。按周(按月)显示时,界面上用每周的周日(每个月的第一日)来代表该周(该月)</span
+												>
 											</p>
 										</div>
 									</div>
@@ -84,9 +80,9 @@
 				<!-- 明细表格 -->
 				<div class="mt-3">
 					<div class="flex items-center justify-between mb-2">
-						<div class="text-base font-medium cursor-pointer select-none" @click="showDetail1 = !showDetail1">
-							{{ showDetail1 ? '收起明细数据' : '展开明细数据' }}
-						</div>
+						<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
+								{{ showDetail1 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown  v-if="showDetail1"/> <ArrowUp  v-else/> </el-icon>
+							</div>
 						<div>
 							<el-button>导出</el-button>
 						</div>
@@ -94,9 +90,7 @@
 					<el-table v-if="showDetail1" :data="pagedTableRows" border>
 						<el-table-column prop="date" label="日期" align="center" min-width="140" />
 						<el-table-column prop="hyyh" label="启动次数" align="center" min-width="140" />
-						<el-table-column prop="ratio" label="启动次数(占比)" align="center" min-width="220">
-							
-						</el-table-column>
+						<el-table-column prop="ratio" label="启动次数(占比)" align="center" min-width="220"> </el-table-column>
 					</el-table>
 					<div v-if="showDetail1" class="flex justify-end mt-2">
 						<el-pagination
@@ -197,7 +191,6 @@ function initLineChart(): void {
 
 onMounted(() => {
 	initLineChart();
-	initQualityChart();
 });
 
 watch(timeGranularity, () => {
@@ -228,57 +221,17 @@ const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue')
 // 展开/收起明细
 const showDetail1 = ref(true);
 
-// 用户质量(留存率)
-const qualityChartRef = ref<HTMLDivElement | null>(null);
-let qualityChart: echarts.ECharts | null = null;
-
-const qualityXAxis = ref<string[]>([
-	'2025-07-01',
-	'2025-07-08',
-	'2025-07-15',
-	'2025-07-22',
-	'2025-07-29',
-	'2025-08-05',
-	'2025-08-12',
-	'2025-08-19',
-	'2025-08-26',
-	'2025-09-02',
-	'2025-09-09',
-	'2025-09-16',
-]);
-
-const retentionSeries = ref<number[]>([20, 23, 27, 24, 22, 15, 5, 4, 16, 26, 25, 2]);
-const industryAvgSeries = ref<number[]>([16, 18, 20, 24, 25, 24, 16, 10, 15, 22, 21, 12]);
-const peerSameScaleSeries = ref<number[]>([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
 
-function initQualityChart(): void {
-	if (!qualityChartRef.value) return;
-	if (qualityChart) qualityChart.dispose();
-	qualityChart = echarts.init(qualityChartRef.value);
-	const option: echarts.EChartsOption = {
-		tooltip: { trigger: 'axis', valueFormatter: (v) => `${v}%` },
-		legend: { data: ['留存率', '同行业App', '同行业同规模App'] },
-		grid: { left: 40, right: 20, top: 30, bottom: 30 },
-		xAxis: { type: 'category', data: qualityXAxis.value },
-		yAxis: {
-			type: 'value',
-			min: 0,
-			max: 30,
-			axisLabel: { formatter: '{value}%' },
-			splitLine: { lineStyle: { color: '#f3f4f6' } },
-		},
-		series: [
-			{ name: '留存率', type: 'line', smooth: true, data: retentionSeries.value },
-			{ name: '同行业App', type: 'line', smooth: true, data: industryAvgSeries.value, color: '#f59e0b' },
-			{ name: '同行业同规模App', type: 'line', smooth: true, data: peerSameScaleSeries.value, color: '#60a5fa' },
-		],
-	};
-	qualityChart.setOption(option);
-}
 </script>
-
+ 
 <style lang="scss" scoped>
 .highlight {
 	color: #2196f3;
 }
+.el-form-item--default{
+	margin-bottom: 0;
+}
+.el-form.el-form--inline .el-form-item--default.el-form-item:last-of-type, .el-form.el-form--inline .el-form-item--small.el-form-item:last-of-type{
+	margin-bottom: 0 !important;
+}
 </style>

+ 9 - 99
src/views/count/user/activeUser/index.vue

@@ -92,9 +92,9 @@
 				<!-- 明细表格 -->
 				<div class="mt-3">
 					<div class="flex items-center justify-between mb-2">
-						<div class="text-base font-medium cursor-pointer select-none" @click="showDetail1 = !showDetail1">
-							{{ showDetail1 ? '收起明细数据' : '展开明细数据' }}
-						</div>
+						<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
+								{{ showDetail1 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown  v-if="showDetail1"/> <ArrowUp  v-else/> </el-icon>
+							</div>
 						<div>
 							<el-button>导出</el-button>
 						</div>
@@ -212,8 +212,6 @@ function initLineChart(): void {
 
 onMounted(() => {
 	initLineChart();
-	initQualityChart();
-	initBehaviorChart();
 });
 
 watch(timeGranularity, () => {
@@ -246,105 +244,17 @@ const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue')
 // 展开/收起明细
 const showDetail1 = ref(true);
 
-// 用户质量(留存率)
-const qualityChartRef = ref<HTMLDivElement | null>(null);
-let qualityChart: echarts.ECharts | null = null;
-
-const qualityXAxis = ref<string[]>([
-	'2025-07-01',
-	'2025-07-08',
-	'2025-07-15',
-	'2025-07-22',
-	'2025-07-29',
-	'2025-08-05',
-	'2025-08-12',
-	'2025-08-19',
-	'2025-08-26',
-	'2025-09-02',
-	'2025-09-09',
-	'2025-09-16',
-]);
-
-const retentionSeries = ref<number[]>([20, 23, 27, 24, 22, 15, 5, 4, 16, 26, 25, 2]);
-const industryAvgSeries = ref<number[]>([16, 18, 20, 24, 25, 24, 16, 10, 15, 22, 21, 12]);
-const peerSameScaleSeries = ref<number[]>([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
-
-function initQualityChart(): void {
-	if (!qualityChartRef.value) return;
-	if (qualityChart) qualityChart.dispose();
-	qualityChart = echarts.init(qualityChartRef.value);
-	const option: echarts.EChartsOption = {
-		tooltip: { trigger: 'axis', valueFormatter: (v) => `${v}%` },
-		legend: { data: ['留存率', '同行业App', '同行业同规模App'] },
-		grid: { left: 40, right: 20, top: 30, bottom: 30 },
-		xAxis: { type: 'category', data: qualityXAxis.value },
-		yAxis: {
-			type: 'value',
-			min: 0,
-			max: 30,
-			axisLabel: { formatter: '{value}%' },
-			splitLine: { lineStyle: { color: '#f3f4f6' } },
-		},
-		series: [
-			{ name: '留存率', type: 'line', smooth: true, data: retentionSeries.value },
-			{ name: '同行业App', type: 'line', smooth: true, data: industryAvgSeries.value, color: '#f59e0b' },
-			{ name: '同行业同规模App', type: 'line', smooth: true, data: peerSameScaleSeries.value, color: '#60a5fa' },
-		],
-	};
-	qualityChart.setOption(option);
-}
-
-// 用户行为分析(静态)
-const behaviorChartRef = ref<HTMLDivElement | null>(null);
-let behaviorChart: echarts.ECharts | null = null;
 
-const behaviorXAxis = ref<string[]>([
-	'2025-07-01',
-	'2025-07-08',
-	'2025-07-15',
-	'2025-07-22',
-	'2025-07-29',
-	'2025-08-05',
-	'2025-08-12',
-	'2025-08-19',
-	'2025-08-26',
-	'2025-09-02',
-	'2025-09-09',
-	'2025-09-16',
-]);
-
-const activeUsersSeries = ref<number[]>([100, 120, 130, 110, 105, 90, 80, 70, 95, 110, 100, 85]);
-const avgSessionSeries = ref<number[]>([120, 130, 140, 125, 120, 110, 100, 95, 115, 125, 120, 110]);
-const behaviorAvgSeries = ref<number[]>([110, 115, 120, 115, 110, 105, 100, 95, 110, 115, 110, 105]);
-
-function initBehaviorChart(): void {
-	if (!behaviorChartRef.value) return;
-	if (behaviorChart) behaviorChart.dispose();
-	behaviorChart = echarts.init(behaviorChartRef.value);
-	const option: echarts.EChartsOption = {
-		tooltip: { trigger: 'axis', valueFormatter: (v) => `${v}s` },
-		legend: { data: ['活跃用户数', '平均会话时长', '行为均值'] },
-		grid: { left: 40, right: 20, top: 30, bottom: 30 },
-		xAxis: { type: 'category', data: behaviorXAxis.value },
-		yAxis: {
-			type: 'value',
-			min: 0,
-			max: 150,
-			axisLabel: { formatter: '{value}s' },
-			splitLine: { lineStyle: { color: '#f3f4f6' } },
-		},
-		series: [
-			{ name: '活跃用户数', type: 'line', smooth: true, data: activeUsersSeries.value },
-			{ name: '平均会话时长', type: 'line', smooth: true, data: avgSessionSeries.value, color: '#f59e0b' },
-			{ name: '行为均值', type: 'line', smooth: true, data: behaviorAvgSeries.value, color: '#60a5fa' },
-		],
-	};
-	behaviorChart.setOption(option);
-}
 </script>
 
 <style lang="scss" scoped>
 .highlight {
 	color: #2196f3;
 }
+.el-form-item--default{
+	margin-bottom: 0;
+}
+.el-form.el-form--inline .el-form-item--default.el-form-item:last-of-type, .el-form.el-form--inline .el-form-item--small.el-form-item:last-of-type{
+	margin-bottom: 0 !important;
+}
 </style>

+ 14 - 61
src/views/count/user/adduser/index.vue

@@ -12,10 +12,10 @@
 				<div>
 					<el-row shadow="hover" class="ml10">
 						<el-form :inline="true" :model="formData" @keyup.enter="query" ref="queryRef">
-							<el-form-item>
+							<el-form-item >
 								<el-date-picker v-model="formData.time" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" />
 							</el-form-item>
-							<el-form-item prop="appId">
+							<el-form-item   prop="appId">
 								<el-input :placeholder="'请输入应用ID'" clearable v-model="formData.appId" />
 							</el-form-item>
 						</el-form>
@@ -75,9 +75,9 @@
 				<!-- 明细表格 -->
 				<div class="mt-3">
 					<div class="flex items-center justify-between mb-2">
-						<div class="text-base font-medium cursor-pointer select-none" @click="showDetail1 = !showDetail1">
-							{{ showDetail1 ? '收起明细数据' : '展开明细数据' }}
-						</div>
+						<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
+								{{ showDetail1 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown  v-if="showDetail1"/> <ArrowUp  v-else/> </el-icon>
+							</div>
 						<div>
 							<el-button>导出</el-button>
 						</div>
@@ -141,9 +141,9 @@
 				<!-- 明细表格 -->
 				<div class="mt-3">
 					<div class="flex items-center justify-between mb-2">
-						<div class="text-base font-medium cursor-pointer select-none" @click="showDetail1 = !showDetail1">
-							{{ showDetail1 ? '收起明细数据' : '展开明细数据' }}
-						</div>
+						<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
+								{{ showDetail1 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown  v-if="showDetail1"/> <ArrowUp  v-else/> </el-icon>
+							</div>
 						<div>
 							<el-button>导出</el-button>
 						</div>
@@ -259,8 +259,6 @@ function initLineChart(): void {
 onMounted(() => {
 	initLineChart();
 	initQualityChart();
-	console.log(qualityChart);
-	initBehaviorChart();
 });
 
 watch(timeGranularity, () => {
@@ -341,61 +339,16 @@ function initQualityChart(): void {
 
 
 
-
-// 用户行为分析(静态)
-const behaviorChartRef = ref<HTMLDivElement | null>(null);
-let behaviorChart: echarts.ECharts | null = null;
-
-const behaviorXAxis = ref<string[]>([
-	'2025-07-01',
-	'2025-07-08',
-	'2025-07-15',
-	'2025-07-22',
-	'2025-07-29',
-	'2025-08-05',
-	'2025-08-12',
-	'2025-08-19',
-	'2025-08-26',
-	'2025-09-02',
-	'2025-09-09',
-	'2025-09-16',
-]);
-
-const activeUsersSeries = ref<number[]>([100, 120, 130, 110, 105, 90, 80, 70, 95, 110, 100, 85]);
-const avgSessionSeries = ref<number[]>([120, 130, 140, 125, 120, 110, 100, 95, 115, 125, 120, 110]);
-const behaviorAvgSeries = ref<number[]>([110, 115, 120, 115, 110, 105, 100, 95, 110, 115, 110, 105]);
-
-function initBehaviorChart(): void {
-	if (!behaviorChartRef.value) return;
-	if (behaviorChart) behaviorChart.dispose();
-	behaviorChart = echarts.init(behaviorChartRef.value);
-	const option: echarts.EChartsOption = {
-		tooltip: { trigger: 'axis', valueFormatter: (v) => `${v}s` },
-		legend: { data: ['活跃用户数', '平均会话时长', '行为均值'] },
-		grid: { left: 40, right: 20, top: 30, bottom: 30 },
-		xAxis: { type: 'category', data: behaviorXAxis.value },
-		yAxis: {
-			type: 'value',
-			min: 0,
-			max: 150,
-			axisLabel: { formatter: '{value}s' },
-			splitLine: { lineStyle: { color: '#f3f4f6' } },
-		},
-		series: [
-			{ name: '活跃用户数', type: 'line', smooth: true, data: activeUsersSeries.value },
-			{ name: '平均会话时长', type: 'line', smooth: true, data: avgSessionSeries.value, color: '#f59e0b' },
-			{ name: '行为均值', type: 'line', smooth: true, data: behaviorAvgSeries.value, color: '#60a5fa' },
-		],
-	};
-	behaviorChart.setOption(option);
-}
-
-
-
 </script>
 
 <style lang="scss" scoped>
 .highlight{
 	color: #2196f3;
 }
+.el-form-item--default{
+	margin-bottom: 0;
+}
+.el-form.el-form--inline .el-form-item--default.el-form-item:last-of-type, .el-form.el-form--inline .el-form-item--small.el-form-item:last-of-type{
+	margin-bottom: 0 !important;
+}
 </style>

+ 11 - 0
src/views/count/user/versionDistribution/i18n/en.ts

@@ -0,0 +1,11 @@
+export default {
+	addUser: {
+		analytics:'Added user analysis',
+		growth:'Go to U-Growth to text/push',
+		ai:'Generative AI intelligence',
+		addtrend:'New trends',
+		channel:'Select a channel',
+		userQuality:'User quality',
+		average:'Industry average'
+	},
+};

+ 10 - 0
src/views/count/user/versionDistribution/i18n/zh-cn.ts

@@ -0,0 +1,10 @@
+export default {
+	versionDistribution: {
+		analytics:'版本分布',
+		aijb:'查看完整ai简报',
+		addtrend:'新增趋势',
+		version:'版本',
+		allVersion:'Top10 版本',
+		versionComparison:'版本对比'
+	},
+};

+ 405 - 0
src/views/count/user/versionDistribution/index.vue

@@ -0,0 +1,405 @@
+<template>
+	<div class="layout-padding">
+		<div class="!overflow-auto px-1">
+			<div class="el-card p-2">
+				<div class="flex justify-between">
+					<Title :title="t('versionDistribution.analytics')" />
+					<div class="">
+						<el-button type="primary">{{ t('versionDistribution.aijb') }}</el-button>
+					</div>
+				</div>
+				<div>
+					<el-row shadow="hover" class="ml10 mt-2">
+						<el-form :inline="true" :model="formData" @keyup.enter="query" ref="queryRef">
+							<el-form-item>
+								<el-date-picker v-model="formData.time" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" />
+							</el-form-item>
+							<el-form-item>
+								<el-select v-model="selectedChannelCompare" class="w-[140px]" placeholder="全部版本">
+									<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+								</el-select>
+							</el-form-item>
+						</el-form>
+					</el-row>
+				</div>
+			</div>
+			<div class="mt-2 el-card p-2">
+				<Title left-line :title="selectedChannelCompare === ''? t('versionDistribution.allVersion') : selectedChannelCompare+t('versionDistribution.version')" >
+					<template #default>
+						<el-popover class="box-item" placement="right" trigger="hover" width="300">
+							<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" style="line-height: 24px">
+										<p><span>趋势图展示累计用户排名Top10版本的变化趋势</span></p>
+										<p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
+										<p>
+											<span class="highlight">活跃用户:</span
+											><span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
+										</p>
+										<p>
+											<span class="highlight">启动次数:</span
+											><span
+												>打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span
+											>
+										</p>
+										<p>
+											<span class="highlight">版本累计用户(%):</span
+											><span>截止到现在,该版本的累计用户(占累计用户全体的比例);若该版本的用户升级到其他版本,则累计用户会减少</span>
+										</p>
+										<p><span class="highlight">升级用户:</span><span>从其他版本升级到该版本的用户(以设备为判断标准)</span></p>
+										<p>
+											<span
+												>如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span
+											>
+										</p>
+									</div>
+								</div>
+							</template>
+						</el-popover>
+					</template>
+				</Title>
+				<div class="">
+					<div class="flex items-center justify-between mb-2 mt-3">
+						<div>
+							<el-select v-if="selectedChannelCompare !== ''" v-model="selectedChannelCompare" class="w-[140px] ml-2" style="width: 140px" placeholder="全部频道">
+								<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+							<el-select v-model="selectedChannelCompare" class="w-[140px] ml-2" style="width: 140px" placeholder="版本对比">
+								<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+							<el-button type="primary" class="ml-2">{{ t('versionDistribution.version') }}</el-button>
+						</div>
+
+						<div class="flex items-center">
+							<el-radio-group v-model="timeGranularity">
+								<el-radio-button label="hour">新增用户</el-radio-button>
+								<el-radio-button label="day">活跃用户</el-radio-button>
+								<el-radio-button label="week">启动次数</el-radio-button>
+								<el-radio-button v-if="selectedChannelCompare !== ''" label="sjcs">升级用户</el-radio-button>
+							</el-radio-group>
+						</div>
+					</div>
+
+					<div class="relative">
+						<div ref="lineChartRef" style="width: 100%; height: 320px"></div>
+					</div>
+				</div>
+
+				<!-- 明细表格 -->
+				<div class="mt-3">
+					<div class="flex items-center justify-between mb-2">
+						<div class="flex">
+							<div v-if="selectedChannelCompare == ''" class="flex items-center">
+								<el-radio-group v-model="timeGranularity">
+									<el-radio-button label="hour">今日</el-radio-button>
+									<el-radio-button label="day">作日 </el-radio-button>
+								</el-radio-group>
+							</div>
+							<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
+								{{ showDetail1 ? '收起明细数据' : '展开明细数据' }}
+								<el-icon class="ml-2"><ArrowDown v-if="showDetail1" /> <ArrowUp v-else /> </el-icon>
+							</div>
+						</div>
+
+						<div>
+							<el-button>导出</el-button>
+						</div>
+					</div>
+					<el-table v-if="showDetail1" :data="pagedTableRows" border>
+						<el-table-column prop="date" label="日期" align="center" min-width="140" />
+						<el-table-column prop="hyyh" label="启动次数" align="center" min-width="140" />
+						<el-table-column prop="ratio" label="启动次数(占比)" align="center" min-width="220"> </el-table-column>
+					</el-table>
+					<div v-if="showDetail1" class="flex justify-end mt-2">
+						<el-pagination
+							v-model:current-page="currentPage"
+							v-model:page-size="pageSize"
+							background
+							layout="total, prev, pager, next, sizes"
+							:total="tableRows.length"
+							:page-sizes="[5, 10, 20]"
+						/>
+					</div>
+				</div>
+			</div>
+			<div v-if="selectedChannelCompare !== ''" class="mt-2 el-card p-2">
+				<div class="flex justify-between">
+					<Title left-line :title="'版本用户来源'">
+						<template #default>
+							<el-popover class="box-item" placement="right" trigger="hover" width="300">
+								<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" style="line-height: 24px">
+											<p><span class="highlight">版本用户来源:</span><span>展示各版本用户的主要获取渠道分布</span></p>
+											<p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
+											<p><span class="highlight">升级用户比例:</span><span>从其他版本升级到该版本的用户占比</span></p>
+										</div>
+									</div>
+								</template>
+							</el-popover>
+						</template>
+					</Title>
+				</div>
+				<div class="flex items-center justify-between mt-3">
+					<el-select v-model="channelDistribution" style="width: 180px" placeholder="新增用户渠道分布">
+						<el-option label="新增用户渠道分布" value="distribution" />
+					</el-select>
+					<div class="ml-4">
+						<el-radio-group v-model="timeRange">
+							<el-radio-button label="yesterday">昨天</el-radio-button>
+							<el-radio-button label="7days">过去7天</el-radio-button>
+							<el-radio-button label="30days">过去30天</el-radio-button>
+						</el-radio-group>
+					</div>
+				</div>
+
+				<!-- 水平柱状图 -->
+				<div class="mt-4">
+					<div class="relative">
+						<div ref="barChartRef" style="width: 100%; height: 200px"></div>
+					</div>
+				</div>
+
+				<!-- 明细表格 -->
+				<div class="mt-4">
+					<div class="flex items-center justify-between mb-2">
+						<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail2 = !showDetail2">
+							{{ showDetail2 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown v-if="showDetail2" /> <ArrowUp v-else /> </el-icon>
+						</div>
+
+						<div>
+							<el-button>导出</el-button>
+						</div>
+					</div>
+					<el-table v-if="showDetail2" :data="pagedSourceRows" border>
+						<el-table-column prop="channel" label="渠道" min-width="140" />
+						<el-table-column prop="newUsers" label="新增用户" min-width="140" />
+						<el-table-column prop="upgradeRatio" label="升级用户比例" min-width="140" />
+					</el-table>
+					<div v-if="showDetail2" class="flex justify-end mt-3">
+						<el-pagination
+							v-model:current-page="sourcePage"
+							v-model:page-size="sourcePageSize"
+							background
+							layout="total, prev, pager, next, sizes"
+							:total="sourceRows.length"
+							:page-sizes="[5, 10, 20]"
+						/>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import * as echarts from 'echarts';
+import { useI18n } from 'vue-i18n';
+import { QuestionFilled } from '@element-plus/icons-vue';
+
+const { t } = useI18n();
+
+interface TableRow {
+	date: string;
+	newUsers: number;
+	ratio: string;
+}
+
+const formData = ref<Record<string, any>>({});
+const query = () => {
+	console.log(formData.value);
+};
+
+const selectedChannelCompare = ref('');
+const channelCompareOptions = [
+	{ label: '全部版本', value: '' },
+	{ label: '1.0', value: '1.0' },
+	{ label: '2.0', value: '2.0' },
+];
+
+// 图表相关
+const timeGranularity = ref<'hour' | 'day' | 'week' | 'month'>('week');
+const lineChartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: echarts.ECharts | null = null;
+
+const lineChartData = ref<Array<{ x: string; value: number }>>([
+	{ x: '2025-07-01', value: 900 },
+	{ x: '2025-07-08', value: 1000 },
+	{ x: '2025-07-15', value: 1100 },
+	{ x: '2025-07-22', value: 1000 },
+	{ x: '2025-07-29', value: 600 },
+	{ x: '2025-08-05', value: 300 },
+	{ x: '2025-08-12', value: 250 },
+	{ x: '2025-08-19', value: 200 },
+	{ x: '2025-08-26', value: 650 },
+	{ x: '2025-09-02', value: 950 },
+	{ x: '2025-09-09', value: 900 },
+	{ x: '2025-09-16', value: 120 },
+]);
+
+function initLineChart(): void {
+	if (!lineChartRef.value) return;
+	if (chartInstance) chartInstance.dispose();
+	chartInstance = echarts.init(lineChartRef.value);
+	const option: echarts.EChartsOption = {
+		tooltip: { trigger: 'axis' },
+		grid: { left: 40, right: 20, top: 20, bottom: 30 },
+		xAxis: {
+			type: 'category',
+			data: lineChartData.value.map((d) => d.x),
+			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',
+				smooth: true,
+				showSymbol: true,
+				symbolSize: 6,
+				itemStyle: { color: '#409EFF' },
+				lineStyle: { color: '#409EFF' },
+				data: lineChartData.value.map((d) => d.value),
+			},
+		],
+	};
+	chartInstance.setOption(option);
+}
+
+onMounted(() => {
+	initLineChart();
+	initBarChart();
+});
+
+watch(timeGranularity, () => {
+	// 静态页面:仅重新渲染
+	initLineChart();
+});
+
+// 表格相关(静态数据)
+const tableRows = ref<TableRow[]>(
+	Array.from({ length: 42 }).map((_, idx) => ({
+		date: `2025-08-${String(11).padStart(2, '0')}`,
+		newUsers: 727,
+		hyyh: '115',
+		ratio: '97.45%',
+	}))
+);
+
+const currentPage = ref(1);
+const pageSize = ref(5);
+const pagedTableRows = computed(() => {
+	const startIndex = (currentPage.value - 1) * pageSize.value;
+
+	return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+
+// 展开/收起明细
+const showDetail1 = ref(true);
+
+// 版本用户来源相关
+const channelDistribution = ref('distribution');
+const timeRange = ref('yesterday');
+const barChartRef = ref<HTMLDivElement | null>(null);
+let barChartInstance: echarts.ECharts | null = null;
+
+const sourceRows = ref<Array<{ channel: string; newUsers: number; upgradeRatio: string }>>([
+	{ channel: '渠道A', newUsers: 100, upgradeRatio: '10%' },
+	{ channel: '渠道B', newUsers: 80, upgradeRatio: '15%' },
+	{ channel: '渠道C', newUsers: 70, upgradeRatio: '20%' },
+	{ channel: '渠道D', newUsers: 60, upgradeRatio: '25%' },
+	{ channel: '渠道E', newUsers: 50, upgradeRatio: '30%' },
+	{ channel: '渠道F', newUsers: 40, upgradeRatio: '35%' },
+	{ channel: '渠道G', newUsers: 30, upgradeRatio: '40%' },
+	{ channel: '渠道H', newUsers: 20, upgradeRatio: '45%' },
+	{ channel: '渠道I', newUsers: 10, upgradeRatio: '50%' },
+	{ channel: '渠道J', newUsers: 5, upgradeRatio: '55%' },
+]);
+
+function initBarChart(): void {
+	if (!barChartRef.value) return;
+	if (barChartInstance) barChartInstance.dispose();
+	barChartInstance = echarts.init(barChartRef.value);
+	const option: echarts.EChartsOption = {
+		tooltip: { trigger: 'axis' },
+		grid: { left: 40, right: 20, top: 20, bottom: 30 },
+		xAxis: {
+			type: 'value',
+			axisLine: { show: false },
+			splitLine: { lineStyle: { color: '#f3f4f6' } },
+			axisLabel: { color: '#6b7280' },
+		},
+		yAxis: {
+			type: 'category',
+			data: sourceRows.value.map((d) => d.channel),
+			axisLine: { lineStyle: { color: '#e5e7eb' } },
+			axisLabel: { color: '#6b7280' },
+			axisTick: { alignWithLabel: true },
+		},
+		series: [
+			{
+				name: '新增用户',
+				type: 'bar',
+				barWidth: '60%',
+				itemStyle: { color: '#409EFF' },
+				data: sourceRows.value.map((d) => d.newUsers),
+			},
+		],
+	};
+	barChartInstance.setOption(option);
+}
+
+onMounted(() => {
+	initBarChart();
+});
+
+watch(timeRange, () => {
+	// 静态页面:仅重新渲染
+	initBarChart();
+});
+
+watch(selectedChannelCompare, () => {
+	// 静态页面:仅重新渲染
+	initBarChart();
+	initLineChart();
+});
+
+const sourcePage = ref(1);
+const sourcePageSize = ref(5);
+const pagedSourceRows = computed(() => {
+	const startIndex = (sourcePage.value - 1) * sourcePageSize.value;
+
+	return sourceRows.value.slice(startIndex, startIndex + sourcePageSize.value);
+});
+
+const showDetail2 = ref(true);
+</script>
+ 
+<style lang="scss" scoped>
+.highlight {
+	color: #2196f3;
+}
+.el-form-item--default {
+	margin-bottom: 0;
+}
+.el-form.el-form--inline .el-form-item--default.el-form-item:last-of-type,
+.el-form.el-form--inline .el-form-item--small.el-form-item:last-of-type {
+	margin-bottom: 0 !important;
+}
+</style>