Browse Source

Merge branch 'dev-ly' into dev-cmn

cmy 2 days ago
parent
commit
e4630dd5ea
31 changed files with 5623 additions and 233 deletions
  1. 76 0
      src/components/LYcom/Lprogress/index.vue
  2. 393 0
      src/views/count/channel/allChannel.vue
  3. 10 0
      src/views/count/channel/index.vue
  4. 1113 0
      src/views/count/channel/otherChannel.vue
  5. 11 0
      src/views/count/featureUsage/accessPath/i18n/en.ts
  6. 13 0
      src/views/count/featureUsage/accessPath/i18n/zh-cn.ts
  7. 391 0
      src/views/count/featureUsage/accessPath/index.vue
  8. 189 0
      src/views/count/featureUsage/eventMannage/AddEventModal.vue
  9. 184 0
      src/views/count/featureUsage/eventMannage/BatchImportModal.vue
  10. 511 0
      src/views/count/featureUsage/eventMannage/EventEdit.vue
  11. 184 0
      src/views/count/featureUsage/eventMannage/EventPropEditModal.vue
  12. 399 0
      src/views/count/featureUsage/eventMannage/EventTable.vue
  13. 10 0
      src/views/count/featureUsage/eventMannage/index.vue
  14. 9 0
      src/views/count/main/trend/icons/icon1.svg
  15. 1 0
      src/views/count/main/trend/icons/icon2.svg
  16. 6 7
      src/views/count/main/trend/index.vue
  17. 11 0
      src/views/count/retained/active/i18n/en.ts
  18. 13 0
      src/views/count/retained/active/i18n/zh-cn.ts
  19. 510 0
      src/views/count/retained/active/index.vue
  20. 11 0
      src/views/count/retained/freshness/i18n/en.ts
  21. 13 0
      src/views/count/retained/freshness/i18n/zh-cn.ts
  22. 494 0
      src/views/count/retained/freshness/index.vue
  23. 11 0
      src/views/count/retained/user/i18n/en.ts
  24. 13 0
      src/views/count/retained/user/i18n/zh-cn.ts
  25. 579 0
      src/views/count/retained/user/index.vue
  26. 19 66
      src/views/count/user/activations/index.vue
  27. 9 99
      src/views/count/user/activeUser/index.vue
  28. 14 61
      src/views/count/user/adduser/index.vue
  29. 11 0
      src/views/count/user/versionDistribution/i18n/en.ts
  30. 10 0
      src/views/count/user/versionDistribution/i18n/zh-cn.ts
  31. 405 0
      src/views/count/user/versionDistribution/index.vue

+ 76 - 0
src/components/LYcom/Lprogress/index.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="l-progress">
+    <span class="l-progress-label">{{ label }}</span>
+    <div class="l-progress-bar-container">
+      <div 
+        class="l-progress-bar" 
+        :style="{ width: percentage + '%' }"
+      ></div>
+    </div>
+    <span class="l-progress-value">{{ percentage }}%</span>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+// 定义组件props
+interface Props {
+  label: string
+  num: number
+  count: number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  label: '',
+  num: 0,
+  count: 0
+})
+
+// 计算占比百分比
+const percentage = computed(() => {
+  if (props.count === 0) return 0
+  return Math.min(100, Math.max(0, (props.num / props.count) * 100))
+})
+</script>
+
+<style scoped lang="scss">
+.l-progress {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  
+  .l-progress-label {
+    font-size: 14px;
+    color: #121212;
+    white-space: nowrap;
+    margin-right: 12px;
+    display: inline-block;
+    width: 80px;
+    text-align: right;
+  }
+  
+  .l-progress-bar-container {
+    flex: 1;
+    height: 10px;
+    background-color: #e4e7ed;
+    border-radius: 5px;
+    overflow: hidden;
+    margin-right: 12px;
+    
+    .l-progress-bar {
+      height: 100%;
+      background: linear-gradient(90deg, #409eff, #66b1ff);
+      border-radius: 5px;
+      transition: width 0.3s ease;
+    }
+  }
+  
+  .l-progress-value {
+    font-size: 14px;
+    color: #121212;
+    font-weight: 500;
+    white-space: nowrap;
+  }
+}
+</style>

+ 393 - 0
src/views/count/channel/allChannel.vue

@@ -0,0 +1,393 @@
+<template>
+  <div class="layout-padding">
+    <div class="!overflow-auto px-1">
+      <Lcard :height="150">
+        <template #default>
+          <div class="box1">
+            <Title style="margin-bottom: 10px;" 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>
+                </template>
+              </el-popover>
+            </Title>
+            <span class="link">查看完整AI简报</span>
+          </div>
+          <div class="box1" style="margin-top: 20px;">
+            <el-select v-model="selectChannel" style="width: 200px;">
+              <el-option label="全部渠道" value="1"></el-option>
+              <el-option label="安卓" value="2"></el-option>
+              <el-option label="苹果" value="3"></el-option>
+            </el-select>
+            <div class="box1-time">
+              <el-radio-group v-model="timeGroup">
+                <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-date-picker style="float: left;" v-model="timeRange" type="datetimerange" range-separator="To"
+                start-placeholder="Start date" end-placeholder="End date" />
+            </div>
+          </div>
+        </template>
+      </Lcard>
+      <Lcard :height="550">
+        <Title left-line title="渠道趋势对比">
+        </Title>
+        <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="hour">新增用户</el-radio-button>
+              <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-button label="month">新增次日留存率</el-radio-button>
+            </el-radio-group>
+          </div>
+          <div class="flex items-center">
+            <el-select v-model="selectVersion" style="width: 200px;">
+              <el-option label="全部版本" value="1"></el-option>
+              <el-option label="1.0.1" value="2"></el-option>
+              <el-option label="1.02" value="3"></el-option>
+            </el-select>
+          </div>
+        </div>
+        <el-select v-model="selectLineChannel" multiple clearable style="width: 400px;margin: 20px 60px;">
+          <el-option label="App Store" value="1"></el-option>
+          <el-option label="应用宝" value="2"></el-option>
+          <el-option label="安卓" value="3"></el-option>
+        </el-select>
+        <div class="relative">
+          <div ref="qChartRef" style="width: 100%; height: 320px"></div>
+        </div>
+      </Lcard>
+      <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>
+        </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>
+            </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>
+      </Lcard>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+// import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import { useI18n } from 'vue-i18n';
+import * as echarts from 'echarts';
+// 引入组件
+const Lcard = defineAsyncComponent(() => import('/@/components/LYcom/Lcard/index.vue'));
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const { t } = useI18n();
+
+const props = defineProps({
+  channel: {
+    type: String,
+    default: '1',
+  },
+});
+const emit = defineEmits(['update:channel'])
+
+// 渠道选择
+const selectChannel = computed({
+  get() {
+    return props.channel;
+  },
+  set(val) {
+    emit('update:channel', val);
+    console.log(val);
+  },
+})
+
+const timeGroup = ref('1')
+const timeRange: any = ref(null);
+watch(timeGroup, (newVal) => {
+  const now = new Date();
+  let start: Date;
+  let end: Date = new Date(now);
+
+  switch (newVal) {
+    case 'day':
+      // 设置为今天 00:00:00 到 23:59:59
+      start = new Date(now);
+      start.setHours(0, 0, 0, 0);
+      end.setHours(23, 59, 59, 999);
+      break;
+    case 'week':
+      // 设置为本周第一天(周日)到本周最后一天(周六)
+      start = new Date(now);
+      const day = start.getDay();
+      const diff = start.getDate() - day;
+      start.setDate(diff);
+      start.setHours(0, 0, 0, 0);
+      end = new Date(start);
+      end.setDate(start.getDate() + 6);
+      end.setHours(23, 59, 59, 999);
+      break;
+    case 'month':
+      // 设置为本月第一天到本月最后一天
+      start = new Date(now.getFullYear(), now.getMonth(), 1);
+      start.setHours(0, 0, 0, 0);
+      end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
+      end.setHours(23, 59, 59, 999);
+      break;
+    default:
+      // 其他情况清空时间范围
+      timeRange.value = null;
+      return;
+  }
+
+  timeRange.value = [start, end];
+}, { immediate: true });
+
+const selectVersion = ref('1') // 选择版本
+
+const selectLineChannel = ref([]) // 选择线形图渠道
+
+const selectSanDian = ref('1') // 选择散点图纵坐标
+
+const radioTableDay = ref('1') // 选择表格时间
+const tableChannel = ref('1') // 选择表格渠道
+
+let qualityChart: echarts.ECharts | null = null;
+const qChartRef = ref(null);
+const lineChartUser = ref(null);
+onMounted(() => {
+  setTimeout(() => {
+    initQualityChart();
+    initScatterChart();
+  }, 500)
+});
+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 {
+  console.log(qChartRef.value, qChartRef);
+  if (!qChartRef.value) return;
+  if (qualityChart) qualityChart.dispose();
+  qualityChart = echarts.init(qChartRef.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);
+}
+
+
+interface TableRow {
+  date: string;
+  newUsers: number;
+  ratio: string;
+}
+// 展开/收起明细
+const showDetail1 = ref(true);
+const pagedTableRows = computed(() => {
+  const startIndex = (currentPage.value - 1) * pageSize.value;
+  return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+// 表格相关(静态数据)
+const currentPage = ref(1);
+const pageSize = ref(5);
+const tableRows = ref<TableRow[]>(
+  Array.from({ length: 42 }).map((_, idx) => ({
+    date: `2025-08-${String(11).padStart(2, '0')}`,
+    newUsers: 727,
+    ratio: '97.45%',
+  }))
+);
+
+
+// 散点图相关代码
+const SDChartRef = ref(null);
+let scatterChart: echarts.ECharts | null = null;
+
+// 散点图数据
+const scatterData = ref([
+  { name: 'App Store', x: 120, y: 45.2, symbolSize: 80 },
+  { name: '应用宝', x: 210, y: 38.7, symbolSize: 120 },
+  { name: '华为应用市场', x: 98, y: 32.5, symbolSize: 65 },
+  { name: '小米应用商店', x: 180, y: 41.3, symbolSize: 110 },
+  { name: 'OPPO软件商店', x: 156, y: 36.8, symbolSize: 95 },
+  { name: 'vivo应用商店', x: 134, y: 34.2, symbolSize: 85 },
+  { name: '360手机助手', x: 178, y: 39.1, symbolSize: 105 },
+  { name: '应用汇', x: 89, y: 28.6, symbolSize: 60 },
+  { name: '安卓市场', x: 145, y: 35.7, symbolSize: 90 },
+  { name: '91助手', x: 112, y: 31.4, symbolSize: 70 }
+]);
+
+// 初始化散点图
+function initScatterChart(): void {
+  if (!SDChartRef.value) return;
+  if (scatterChart) scatterChart.dispose();
+
+  scatterChart = echarts.init(SDChartRef.value);
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'item',
+      formatter: (params: any) => {
+        return `${params.name}<br/>新增用户: ${params.data.x}<br/>留存率: ${params.data.y}%`;
+      }
+    },
+    grid: {
+      left: '7%',
+      right: '12%',
+      top: '10%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'value',
+      name: '新增用户(日均)',
+      scale: true,
+      axisLine: {
+        show: true
+      },
+      splitLine: {
+        show: false
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: '次日留存率(日均)',
+      scale: true,
+      axisLine: {
+        show: true
+      },
+      splitLine: {
+        show: true
+      },
+      axisLabel: {
+        formatter: '{value}%'
+      }
+    },
+    series: [{
+      type: 'scatter',
+      symbolSize: (data: any) => data[2],
+      data: scatterData.value.map(item => [item.x, item.y, item.symbolSize, item.name]),
+      itemStyle: {
+        color: (params: any) => {
+          const colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
+          return colors[params.dataIndex % colors.length];
+        }
+      },
+      label: {
+        show: true,
+        formatter: (params: any) => params.data[3],
+        position: 'inside',
+        fontSize: 10,
+        color: '#fff'
+      }
+    }]
+  };
+
+  scatterChart.setOption(option);
+}
+
+
+</script>
+<style lang="scss" scoped>
+.link {
+  color: #167af0;
+  cursor: pointer;
+}
+
+.box1 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.line {
+  margin: 60px -30px 30px;
+  height: 1px;
+  background-color: #E6E6E6;
+}
+</style>

+ 10 - 0
src/views/count/channel/index.vue

@@ -0,0 +1,10 @@
+<template>
+  <AllChannel v-if="channel === '1'" v-model:channel="channel"></AllChannel>
+  <OtherChannel v-else v-model:channel="channel"></OtherChannel>
+</template>
+<script setup lang="ts" name="countMainTrend">
+const AllChannel = defineAsyncComponent(() => import('./allChannel.vue'));
+const OtherChannel = defineAsyncComponent(() => import('./otherChannel.vue'));
+
+const channel = ref('1')
+</script>

+ 1113 - 0
src/views/count/channel/otherChannel.vue

@@ -0,0 +1,1113 @@
+<template>
+  <div class="layout-padding">
+    <div class="!overflow-auto px-1">
+      <Lcard>
+        <div class="flex justify-start items-center">
+          <el-icon @click="() => selectChannel = '1'" class="ml-1" style="cursor: pointer; color: #333333; margin-right: 25px">
+            <ArrowLeftBold />
+          </el-icon>
+          <el-select v-model="selectChannel" style="width: 100px;">
+            <el-option label="全部渠道" value="1"></el-option>
+            <el-option label="安卓" value="2"></el-option>
+            <el-option label="苹果" value="3"></el-option>
+          </el-select>
+        </div>
+      </Lcard>
+      <Lcard>
+        <div class="flex justify-between items-center">
+          <Title left-line title="渠道趋势">
+          </Title>
+          <div class="box1">
+            <el-select v-model="selectVersion" style="width: 140px; margin-right: 30px;">
+              <el-option label="全部版本" value="1"></el-option>
+              <el-option label="1.0.1" value="2"></el-option>
+              <el-option label="1.02" value="3"></el-option>
+            </el-select>
+            <div class="box1-time">
+              <el-date-picker style="float: left; width: 240px; margin-right: 30px;" v-model="timeRange"
+                type="datetimerange" range-separator="To" start-placeholder="Start date" end-placeholder="End date" />
+              <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-button label="5">新增次日留存率</el-radio-button>
+            </el-radio-group>
+          </div>
+        </div>
+        <el-select placeholder="对比分析" v-model="selectLineChannel" multiple clearable
+          style="width: 140px;margin: 20px 60px;">
+          <el-option label="时段对比" value="1"></el-option>
+          <el-option label="渠道对比" value="2"></el-option>
+          <el-option label="版本对比" value="3"></el-option>
+        </el-select>
+        <div class="relative">
+          <div ref="qChartRef" style="margin-left: 60px; 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 class="flex justify-between">
+        <Lcard style="width: calc(50% - 5px);">
+          <div class="flex justify-between items-center">
+            <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">
+                    渠道活跃度,展示指定渠道用户的昨日活跃/过去7天活跃、昨日活跃/过去30天活跃的信息。<br>
+                    通过这两个指标,您可以了解到该渠道用户的粘着度。昨日活跃/过去30天活跃越接入100%,<br>
+                    用户越活跃,流失率越低,粘性越强这里的活跃用户是去重后的活跃用户
+                  </div>
+                </template>
+              </el-popover>
+            </Title>
+            <div style="font-size: 15px; color: #646464;">
+              2025/08/19
+            </div>
+          </div>
+          <div class="flex justify-between items-center">
+            <div ref="qdqsRef1" style="width: 48%; height: 220px;"></div>
+            <div ref="qdqsRef2" style="width: 48%; height: 220px;"></div>
+          </div>
+        </Lcard>
+        <Lcard style="width: calc(50% - 5px);">
+          <div class="flex justify-between items-center">
+            <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">
+                    展示了渠道近期的留存率情况,可以帮助您了解该渠道用户的忠诚度<br>
+                    次日留存率: 某日的新增用户在次日启动过应用的比例<br>
+                    7日留存率: 某日的新增用户在7天后启动过应用的比例<br>
+                    14日留存率: 某日的新增用户在14天后启动过应用的比例<br>
+                    这里展示的是渠道前天在昨天的次日留存率、8天前在昨天的7日留存率和15天前在昨天的14日留存率
+                  </div>
+                </template>
+              </el-popover>
+            </Title>
+            <div style="font-size: 15px; color: #646464;">
+              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>
+        </Lcard>
+      </div>
+      <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>
+      </Lcard>
+      <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="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>
+          </div>
+        </div>
+        <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>
+  </div>
+</template>
+
+<script lang="ts" setup>
+// import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import { useI18n } from 'vue-i18n';
+import * as echarts from 'echarts';
+// 引入组件
+const Lcard = defineAsyncComponent(() => import('/@/components/LYcom/Lcard/index.vue'));
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const Lprogress = defineAsyncComponent(() => import('/@/components/LYcom/Lprogress/index.vue'));
+const { t } = useI18n();
+
+const props = defineProps({
+  channel: {
+    type: String,
+    default: '1',
+  },
+});
+const emit = defineEmits(['update:channel'])
+
+// 渠道选择
+const selectChannel = computed({
+  get() {
+    return props.channel;
+  },
+  set(val) {
+    emit('update:channel', val);
+    console.log(val);
+  },
+})
+const timeGroup = ref('1')
+const timeRange: any = ref(null);
+watch(timeGroup, (newVal) => {
+  const now = new Date();
+  let start: Date;
+  let end: Date = new Date(now);
+
+  switch (newVal) {
+    case 'day':
+      // 设置为今天 00:00:00 到 23:59:59
+      start = new Date(now);
+      start.setHours(0, 0, 0, 0);
+      end.setHours(23, 59, 59, 999);
+      break;
+    case 'week':
+      // 设置为本周第一天(周日)到本周最后一天(周六)
+      start = new Date(now);
+      const day = start.getDay();
+      const diff = start.getDate() - day;
+      start.setDate(diff);
+      start.setHours(0, 0, 0, 0);
+      end = new Date(start);
+      end.setDate(start.getDate() + 6);
+      end.setHours(23, 59, 59, 999);
+      break;
+    case 'month':
+      // 设置为本月第一天到本月最后一天
+      start = new Date(now.getFullYear(), now.getMonth(), 1);
+      start.setHours(0, 0, 0, 0);
+      end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
+      end.setHours(23, 59, 59, 999);
+      break;
+    default:
+      // 其他情况清空时间范围
+      timeRange.value = null;
+      return;
+  }
+
+  timeRange.value = [start, end];
+}, { immediate: true });
+
+const selectVersion = ref('1') // 选择版本
+
+const selectLineChannel = ref([]) // 选择线形图渠道
+
+const selectSanDian = ref('1') // 选择散点图纵坐标
+
+const radioTableDay = ref('1') // 选择表格时间
+const tableChannel = ref('1') // 选择表格渠道
+
+let qualityChart: echarts.ECharts | null = null;
+const qChartRef = ref(null);
+const lineChartUser = ref(null);
+onMounted(() => {
+  setTimeout(() => {
+    initQualityChart();
+    initChart1();
+    initChart2();
+    initHorizontalBarChart();
+  }, 500)
+});
+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 {
+  console.log(qChartRef.value, qChartRef);
+  if (!qChartRef.value) return;
+  if (qualityChart) qualityChart.dispose();
+  qualityChart = echarts.init(qChartRef.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);
+}
+
+
+interface TableRow {
+  date: string;
+  newUsers: number;
+  ratio: string;
+}
+// 展开/收起明细
+const showDetail1 = ref(true);
+const pagedTableRows = computed(() => {
+  const startIndex = (currentPage.value - 1) * pageSize.value;
+  return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+// 表格相关(静态数据)
+const currentPage = ref(1);
+const pageSize = ref(5);
+const tableRows = ref<TableRow[]>(
+  Array.from({ length: 42 }).map((_, idx) => ({
+    date: `2025-08-${String(11).padStart(2, '0')}`,
+    newUsers: 727,
+    ratio: '97.45%',
+  }))
+);
+
+
+// 饼图进度条相关代码
+const qdqsRef1 = ref(null);
+const qdqsCount1 = ref(85);
+const qdqsRef2 = ref(null);
+const qdqsCount2 = ref(65);
+let chart1: echarts.ECharts | null = null;
+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: [
+      {
+        type: 'pie',
+        radius: ['60%', '70%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: false,
+        startAngle: 270,
+        endAngle: 270 - (qdqsCount1.value / 100 * 360),
+        silent: true,
+        itemStyle: {
+          color: '#167AF0',
+          borderRadius: 10
+        },
+        data: [100],
+        label: {
+          show: false
+        },
+        emphasis: {
+          disabled: true
+        },
+      },
+      {
+        type: 'pie',
+        radius: ['60%', '70%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: false,
+        startAngle: 360,
+        endAngle: 360,
+        itemStyle: {
+          color: '#409eff'
+        },
+        data: [65, 35],
+        label: {
+          show: false
+        },
+        emphasis: {
+          disabled: true
+        }
+      }
+    ],
+    graphic: {
+      elements: [
+        {
+          type: 'text',
+          key: 'percent-text',
+          z: 100,
+          left: 'center',
+          top: 'center',
+          style: {
+            text: qdqsCount1.value + '%',
+            fontSize: 24,
+            fontWeight: 'bold',
+            fill: '#121212'
+          }
+        },
+        {
+          type: 'text',
+          key: 'label-text',
+          z: 100,
+          left: 'center',
+          bottom: '3%',
+          style: {
+            text: '昨日活跃/过去7天活跃',
+            fontSize: 14,
+            fill: '#121212'
+          }
+        }
+      ]
+    }
+  };
+
+  chart1.setOption(option);
+}
+
+// 初始化第二个饼图进度条
+function initChart2(): void {
+  if (!qdqsRef2.value) return;
+  if (chart2) chart2.dispose();
+
+  chart2 = echarts.init(qdqsRef2.value);
+
+  const option: echarts.EChartsOption = {
+    series: [
+      {
+        type: 'pie',
+        radius: ['60%', '70%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: false,
+        startAngle: 270,
+        endAngle: 270 - (qdqsCount2.value / 100 * 360),
+        silent: true,
+        itemStyle: {
+          color: '#00BC71',
+          borderRadius: 10
+        },
+        data: [100],
+        label: {
+          show: false
+        },
+        emphasis: {
+          disabled: true
+        },
+      },
+      {
+        type: 'pie',
+        radius: ['60%', '70%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: false,
+        startAngle: 360,
+        endAngle: 360,
+        itemStyle: {
+          color: '#409eff'
+        },
+        data: [65, 35],
+        label: {
+          show: false
+        },
+        emphasis: {
+          disabled: true
+        }
+      }
+    ],
+    graphic: {
+      elements: [
+        {
+          type: 'text',
+          key: 'percent-text',
+          z: 100,
+          left: 'center',
+          top: 'center',
+          style: {
+            text: qdqsCount2.value + '%',
+            fontSize: 24,
+            fontWeight: 'bold',
+            fill: '#121212'
+          }
+        },
+        {
+          type: 'text',
+          key: 'label-text',
+          z: 100,
+          left: 'center',
+          bottom: '3%',
+          style: {
+            text: '昨日活跃/过去30天活跃',
+            fontSize: 14,
+            fill: '#121212'
+          }
+        }
+      ]
+    }
+  };
+
+  chart2.setOption(option);
+}
+
+
+// 横向进度条相关
+const progressArray = ref([
+  {
+    label: '次日留存率',
+    num: 80,
+    count: 200
+  },
+  {
+    label: '7日留存率',
+    num: 80,
+    count: 200
+  },
+  {
+    label: '14日留存率',
+    num: 40,
+    count: 200
+  },
+])
+
+
+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 {
+  color: #167af0;
+  cursor: pointer;
+}
+
+.box1 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.line {
+  margin: 60px -30px 30px;
+  height: 1px;
+  background-color: #E6E6E6;
+}
+</style>

+ 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>

+ 189 - 0
src/views/count/featureUsage/eventMannage/AddEventModal.vue

@@ -0,0 +1,189 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="添加事件"
+    width="500px"
+    :before-close="handleClose"
+  >
+    <el-form 
+      :model="form" 
+      :rules="rules" 
+      ref="formRef"
+      label-width="100px"
+    >
+      <el-form-item label="选择应用" prop="selectedApps" required>
+        <el-select 
+          v-model="form.selectedApps" 
+          multiple 
+          placeholder="请选择应用"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="app in appOptions"
+            :key="app.value"
+            :label="app.label"
+            :value="app.value"
+          />
+        </el-select>
+      </el-form-item>
+      
+      <el-form-item label="事件ID" prop="eventId" required>
+        <el-input 
+          v-model="form.eventId" 
+          placeholder="请输入事件ID"
+        />
+      </el-form-item>
+      
+      <el-form-item label="显示名称" prop="displayName" required>
+        <el-input 
+          v-model="form.displayName" 
+          placeholder="请输入显示名称"
+        />
+      </el-form-item>
+      
+      <el-form-item label="事件类型" prop="eventType" required>
+        <el-select 
+          v-model="form.eventType" 
+          placeholder="请选择事件类型"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="type in eventTypeOptions"
+            :key="type.value"
+            :label="type.label"
+            :value="type.value"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    
+    <div class="tips">
+      <div class="tips-content">
+        说明:<br>
+        事件标识符不可更改,与代码中"String event_id"保持一致:参数(key)无需手动注册,每个事件下最多支持100个参数同时计算。为保证数据准确性,请使用(英文、数字、下划线(_)、中划线(-)、小数点(.)及加号(+))定义事件标识符或属性标识符。
+      </div>
+    </div>
+    
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleAdd">添加</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+
+interface Props {
+  modelValue: boolean
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'add', data: any): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 表单引用
+const formRef = ref<FormInstance>()
+
+// 可见性绑定
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 表单数据
+const form = reactive({
+  selectedApps: [] as string[],
+  eventId: '',
+  displayName: '',
+  eventType: '' // 0: 多参数类型事件, 1: 计算事件
+})
+
+// 应用选项
+const appOptions = [
+  { label: '教育行业Demo', value: 'education' },
+  { label: '游戏行业Demo', value: 'game' },
+  { label: '通用行业Demo', value: 'general' }
+]
+
+// 事件类型选项
+const eventTypeOptions = [
+  { label: '多参数类型事件', value: 0 },
+  { label: '计算事件', value: 1 }
+]
+
+// 表单验证规则
+const rules = reactive<FormRules>({
+  selectedApps: [
+    { required: true, message: '请选择应用', trigger: 'change' }
+  ],
+  eventId: [
+    { required: true, message: '请输入事件ID', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_.\-+]+$/, message: '只能包含字母、数字、下划线(_)、中划线(-)、小数点(.)及加号(+)', trigger: 'blur' },
+    { max: 128, message: '长度不能超过128个字符', trigger: 'blur' }
+  ],
+  displayName: [
+    { required: true, message: '请输入显示名称', trigger: 'blur' }
+  ],
+  eventType: [
+    { required: true, message: '请选择事件类型', trigger: 'change' }
+  ]
+})
+
+// 关闭弹窗
+const handleClose = () => {
+  visible.value = false
+  // 重置表单
+  form.selectedApps = []
+  form.eventId = ''
+  form.displayName = ''
+  form.eventType = ''
+  // 清除表单验证
+  formRef.value?.resetFields()
+}
+
+// 处理添加事件
+const handleAdd = async () => {
+  if (!formRef.value) return
+  
+  await formRef.value.validate((valid) => {
+    if (valid) {
+      emit('add', {
+        apps: form.selectedApps,
+        eventId: form.eventId,
+        displayName: form.displayName,
+        eventType: form.eventType
+      })
+      handleClose()
+    }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.tips {
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 12px;
+  margin-top: 20px;
+  
+  .tips-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 184 - 0
src/views/count/featureUsage/eventMannage/BatchImportModal.vue

@@ -0,0 +1,184 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="批量导入事件"
+    width="500px"
+    :before-close="handleClose"
+  >
+    <el-form :model="form" label-width="80px">
+      <el-form-item label="选择应用" required>
+        <el-select 
+          v-model="form.selectedApps" 
+          multiple 
+          placeholder="请选择应用"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="app in appOptions"
+            :key="app.value"
+            :label="app.label"
+            :value="app.value"
+          />
+        </el-select>
+      </el-form-item>
+      
+      <el-form-item label="选择文件">
+        <el-button @click="selectFile">
+          <el-icon><Upload /></el-icon>
+          选择Excel文件
+        </el-button>
+        <br>
+        <div v-if="form.fileName" class="file-name">{{ form.fileName }}</div>
+      </el-form-item>
+      
+      <el-form-item label=" ">
+        <el-button @click="downloadTemplate">
+          <el-icon><Download /></el-icon>
+          Excel模板下载
+        </el-button>
+      </el-form-item>
+    </el-form>
+    
+    <div class="tips">
+      <div class="tips-title">说明:</div>
+      <div class="tips-content">
+        <p>① 命名规范:事件标识符(event id)、事件属性标识符(key)命名支持字母(建议小写,避免使用中文)、数字、下划线 (_)、中划线 (-)、小数点 (.)及加号(+) 。event id和key长度不能超过128个字节。</p>
+        <p>② 事件类型,1表示计算事件,0表示多参数类型事件</p>
+        <p>③ 属性类型:number数值型(可计算),string字符串</p>
+      </div>
+    </div>
+    
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleImport" :disabled="!canImport">导入</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive } from 'vue'
+import { Upload, Download } from '@element-plus/icons-vue'
+
+interface Props {
+  modelValue: boolean
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'import', data: any): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 可见性绑定
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 表单数据
+const form = reactive({
+  selectedApps: [] as string[],
+  fileName: '',
+  file: null as File | null
+})
+
+// 应用选项
+const appOptions = [
+  { label: '教育行业Demo', value: 'education' },
+  { label: '游戏行业Demo', value: 'game' },
+  { label: '通用行业Demo', value: 'general' }
+]
+
+// 是否可以导入(已选择应用和文件)
+const canImport = computed(() => {
+  return form.selectedApps.length > 0 && form.file !== null
+})
+
+// 关闭弹窗
+const handleClose = () => {
+  visible.value = false
+  // 重置表单
+  form.selectedApps = []
+  form.fileName = ''
+  form.file = null
+}
+
+// 选择文件
+const selectFile = () => {
+  const input = document.createElement('input')
+  input.type = 'file'
+  input.accept = '.xlsx,.xls'
+  input.onchange = (e) => {
+    const file = (e.target as HTMLInputElement).files?.[0]
+    if (file) {
+      form.file = file
+      form.fileName = file.name
+    }
+  }
+  input.click()
+}
+
+// 下载模板
+const downloadTemplate = () => {
+  // 这里可以实现实际的模板下载逻辑
+  console.log('下载模板')
+  // 示例:创建一个虚拟的下载链接
+  const link = document.createElement('a')
+  link.href = 'data:application/vnd.ms-excel;base64,UEsDBAoAAAAAALJdHFYAAAAA' // 示例base64
+  link.download = '事件导入模板.xlsx'
+  link.click()
+}
+
+// 处理导入
+const handleImport = () => {
+  if (!canImport.value) return
+  
+  emit('import', {
+    apps: form.selectedApps,
+    file: form.file
+  })
+  
+  handleClose()
+}
+</script>
+
+<style scoped lang="scss">
+.file-name {
+  margin-top: 8px;
+  font-size: 14px;
+  color: #606266;
+}
+
+.tips {
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 12px;
+  margin-top: 20px;
+  
+  .tips-title {
+    font-weight: bold;
+    margin-bottom: 8px;
+    color: #303133;
+  }
+  
+  .tips-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+    
+    p {
+      margin: 0 0 4px 0;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 511 - 0
src/views/count/featureUsage/eventMannage/EventEdit.vue

@@ -0,0 +1,511 @@
+<template>
+  <div class="layout-padding">
+    <div class="!overflow-auto px-1">
+      <Lcard style=" margin-bottom: 0px;">
+        <div class="flex justify-start items-center">
+          <el-icon @click="back" class="ml-1" style="cursor: pointer; color: #333333; margin-right: 10px">
+            <ArrowLeftBold />
+          </el-icon>
+          <Title title="编辑事件详情">
+          </Title>
+        </div>
+        <div style="margin: 20px 60px;">
+          <!-- 添加事件详情展示区域 -->
+          <div class="event-detail-card">
+            <!-- <div class="event-detail-header">
+              <h3>事件基本信息</h3>
+            </div> -->
+            <div class="event-detail-content">
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">事件名称:</span>
+                  <span class="detail-value">用户注册</span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">事件标识符:</span>
+                  <span class="detail-value">user_register</span>
+                </div>
+              </div>
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">计算状态:</span>
+                  <span class="detail-value">
+                    <el-tag type="success">计算中</el-tag>
+                  </span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">近三十天上报数:</span>
+                  <span class="detail-value">12,560</span>
+                </div>
+              </div>
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">上报平台:</span>
+                  <span class="detail-value">Android、IOS</span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">应用:</span>
+                  <span class="detail-value">教育行业Demo</span>
+                </div>
+              </div>
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">埋点触发时机:</span>
+                  <span class="detail-value">用户完成注册流程时触发</span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">操作人:</span>
+                  <span class="detail-value">nina</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div style="margin: 20px 60px;">
+          <el-radio-group v-model="eventTypeRadio">
+            <el-radio-button label="1">自定义属性</el-radio-button>
+            <el-radio-button label="2">预制全局属性</el-radio-button>
+          </el-radio-group>
+          <div class="line" style="margin: 0 0 20px 0;"></div>
+          <div class="flex search-box" style="margin-bottom: 20px;">
+            <el-radio-group v-model="eventTypeRadio">
+              <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-group>
+            <el-input placeholder="请输入事件名/事件标识符" style="width: 230px;"></el-input>
+            <el-button @click="handleAddProperty" style="margin-left: auto;" type="primary">新增属性</el-button>
+            <el-button>批量停止计算</el-button>
+          </div>
+          <el-table :height="700" :data="pagedTableRows" border>
+            <el-table-column prop="propertyName" label="属性名称" min-width="120" />
+            <el-table-column prop="propertyIdentifier" label="属性标识符" min-width="140" />
+            <el-table-column label="属性类型" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getPropertyType(scope.row.propertyType)">{{ getPropertyTypeName(scope.row.propertyType)
+                }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="计算状态" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="remark" label="属性备注" min-width="120" show-overflow-tooltip />
+            <el-table-column label="应用" min-width="120">
+              <template #default="scope">
+                {{ getAppText(scope.row.apps) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" min-width="160" fixed="right">
+              <template #default="scope">
+                <el-button @click="handleEditProperty(scope.row)" type="primary" link>编辑</el-button>
+                <!-- <el-button type="primary" link>详情</el-button> -->
+                <el-button v-if="scope.row.status === '计算中'" type="primary" link>暂停</el-button>
+                <el-button v-else-if="scope.row.status === '暂停计算'" type="primary" link>启用</el-button>
+                <el-button type="danger" link>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div 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>
+      <EventPropEditModal v-model="showPropertyModal" :is-edit="isEditProperty" :data="currentPropertyData"
+        @submit="handleSubmitProperty" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" name="countMainTrend" setup>
+// import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import { useI18n } from 'vue-i18n';
+import * as echarts from 'echarts';
+// 引入组件
+const Lcard = defineAsyncComponent(() => import('/@/components/LYcom/Lcard/index.vue'));
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const EventPropEditModal = defineAsyncComponent(() => import('./EventPropEditModal.vue'))
+const { t } = useI18n();
+const eventTypeRadio = ref('1')
+
+onMounted(() => {
+  setTimeout(() => {
+  }, 500);
+});
+
+// 编辑属性相关
+const showPropertyModal = ref(false)
+const isEditProperty = ref(false)
+const currentPropertyData = ref<any>(null)
+
+// 在 script 部分添加
+const handleAddProperty = () => {
+  isEditProperty.value = false
+  currentPropertyData.value = null
+  showPropertyModal.value = true
+}
+
+const handleEditProperty = (data: any) => {
+  isEditProperty.value = true
+  currentPropertyData.value = data
+  showPropertyModal.value = true
+}
+
+const handleSubmitProperty = (data: any) => {
+  console.log('提交属性数据:', data)
+  // 这里可以实现实际的新增或编辑逻辑
+  // 例如发送请求到后端
+}
+
+const emit = defineEmits(['update:eventId'])
+
+const back = () => {
+  emit('update:eventId', null)
+}
+
+const searchData = reactive({
+  eventType: '1',
+  channel: '',
+  eventName: '',
+  eventHas: '',
+  appId: '',
+  evnetUser: '',
+})
+
+// 表格相关(静态数据)
+const currentPage = ref(1);
+const pageSize = ref(10);
+
+const pagedTableRows = computed(() => {
+  const startIndex = (currentPage.value - 1) * pageSize.value;
+  return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+
+
+// 修改表格数据结构
+interface EventTableRow {
+  id: string;
+  propertyName: string; // 属性名称
+  propertyIdentifier: string; // 属性标识符
+  propertyType: string; // 属性类型
+  status: string; // 计算状态
+  remark: string; // 属性备注
+  apps: string[]; // 应用
+}
+
+// 更新表格静态数据
+const tableRows = ref<EventTableRow[]>([
+  {
+    id: '1',
+    propertyName: '用户ID',
+    propertyIdentifier: 'user_id',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户的唯一标识符',
+    apps: ['教育行业Demo', '游戏行业Demo']
+  },
+  {
+    id: '2',
+    propertyName: '访问时长',
+    propertyIdentifier: 'visit_duration',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户单次访问的时长',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '3',
+    propertyName: '设备型号',
+    propertyIdentifier: 'device_model',
+    propertyType: 'string',
+    status: '暂停计算',
+    remark: '用户使用的设备型号',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '4',
+    propertyName: '页面路径',
+    propertyIdentifier: 'page_path',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户访问的页面路径',
+    apps: ['教育行业Demo', '游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '5',
+    propertyName: '点击次数',
+    propertyIdentifier: 'click_count',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '页面元素被点击的次数',
+    apps: ['游戏行业Demo']
+  },
+  {
+    id: '6',
+    propertyName: '会话ID',
+    propertyIdentifier: 'session_id',
+    propertyType: 'string',
+    status: '未注册',
+    remark: '用户会话的唯一标识',
+    apps: ['通用行业Demo']
+  },
+  {
+    id: '7',
+    propertyName: '地理位置',
+    propertyIdentifier: 'geo_location',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户的地理位置信息',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '8',
+    propertyName: '购买金额',
+    propertyIdentifier: 'purchase_amount',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户购买商品的金额',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '9',
+    propertyName: '分享平台',
+    propertyIdentifier: 'share_platform',
+    propertyType: 'string',
+    status: '暂停计算',
+    remark: '用户分享到的平台',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '10',
+    propertyName: '搜索关键词',
+    propertyIdentifier: 'search_keyword',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户搜索时使用的关键词',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '11',
+    propertyName: '错误代码',
+    propertyIdentifier: 'error_code',
+    propertyType: 'number',
+    status: '未注册',
+    remark: '系统错误代码',
+    apps: ['通用行业Demo']
+  },
+  {
+    id: '12',
+    propertyName: '网络类型',
+    propertyIdentifier: 'network_type',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户当前的网络连接类型',
+    apps: ['教育行业Demo', '游戏行业Demo']
+  },
+  {
+    id: '13',
+    propertyName: '用户年龄',
+    propertyIdentifier: 'user_age',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户的年龄信息',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '14',
+    propertyName: '操作系统',
+    propertyIdentifier: 'os_version',
+    propertyType: 'string',
+    status: '暂停计算',
+    remark: '设备的操作系统版本',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '15',
+    propertyName: '页面停留时间',
+    propertyIdentifier: 'page_stay_time',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户在页面停留的时间',
+    apps: ['教育行业Demo', '游戏行业Demo', '通用行业Demo']
+  }
+]);
+
+// 获取属性类型标签样式
+const getPropertyType = (type: string) => {
+  switch (type) {
+    case 'string':
+      return 'primary';
+    case 'number':
+      return 'success';
+    default:
+      return 'info';
+  }
+};
+
+// 获取属性类型名称
+const getPropertyTypeName = (type: string) => {
+  switch (type) {
+    case 'string':
+      return '字符串';
+    case 'number':
+      return '数值型';
+    default:
+      return type;
+  }
+};
+
+// 获取应用显示文本(复用原来的方法)
+const getAppText = (apps: string[]) => {
+  if (apps.length === 0) return '-';
+  if (apps.length > 2) return `${apps[0]}等${apps.length}个`;
+  return apps.join('、');
+};
+
+// 状态标签样式(保持不变)
+const getStatusType = (status: string) => {
+  switch (status) {
+    case '计算中':
+      return 'success';
+    case '暂停计算':
+      return 'warning';
+    case '未注册':
+      return 'info';
+    default:
+      return 'info';
+  }
+};
+</script>
+<style lang="scss" scoped>
+.search-box>div {
+  margin-right: 20px;
+}
+
+/* 添加事件详情样式 */
+.event-detail-card {
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  margin-bottom: 20px;
+
+  .event-detail-header {
+    padding: 12px 20px;
+    border-bottom: 1px solid #e4e7ed;
+    background-color: #f5f7fa;
+
+    h3 {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .event-detail-content {
+    padding: 20px;
+
+    .detail-row {
+      display: flex;
+      margin-bottom: 16px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    .detail-item {
+      flex: 1;
+      display: flex;
+
+      .detail-label {
+        width: 120px;
+        font-size: 14px;
+        color: #606266;
+        text-align: right;
+        margin-right: 12px;
+        flex-shrink: 0;
+      }
+
+      .detail-value {
+        flex: 1;
+        font-size: 14px;
+        color: #303133;
+
+        :deep(.el-tag) {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+}
+
+/* 响应式处理 */
+@media (max-width: 768px) {
+  .event-detail-card {
+    .event-detail-content {
+      .detail-row {
+        flex-direction: column;
+        margin-bottom: 12px;
+      }
+
+      .detail-item {
+        margin-bottom: 8px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        .detail-label {
+          width: auto;
+          text-align: left;
+          margin-right: 8px;
+        }
+      }
+    }
+  }
+}
+
+.msg {
+  height: 30px;
+  background-color: #f4f5fa;
+  line-height: 30px;
+  font-size: 12px;
+}
+
+.link {
+  color: #167af0;
+  cursor: pointer;
+}
+
+.box1 {
+  display: flex;
+  margin-top: 30px;
+
+  .card-box1,
+  .card-box2 {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: start;
+  }
+
+  .card-box1 {
+    .info-card {
+      width: 48%;
+    }
+  }
+
+  .card-box2 {
+    .info-card {
+      width: 33%;
+    }
+  }
+}
+
+.line {
+  margin: 60px -30px 30px;
+  height: 1px;
+  background-color: #E6E6E6;
+}
+</style>

+ 184 - 0
src/views/count/featureUsage/eventMannage/EventPropEditModal.vue

@@ -0,0 +1,184 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="isEdit ? '编辑属性' : '新增属性'"
+    width="500px"
+    :before-close="handleClose"
+  >
+    <el-form 
+      :model="form" 
+      :rules="rules" 
+      ref="formRef"
+      label-width="100px"
+    >
+      <el-form-item label="属性名称" prop="propertyName" required>
+        <el-input 
+          v-model="form.propertyName" 
+          :disabled="isEdit"
+          placeholder="请输入属性名称"
+        />
+      </el-form-item>
+      
+      <el-form-item label="属性标识符" prop="propertyIdentifier" required>
+        <el-input 
+          v-model="form.propertyIdentifier" 
+          :disabled="isEdit"
+          placeholder="请输入属性标识符"
+        />
+        <div class="form-item-tip">支持字母、数字、下划线(_)、中划线(-)、小数点(.)及加号(+)</div>
+      </el-form-item>
+      
+      <el-form-item label="属性类型" prop="propertyType" required>
+        <el-select 
+          v-model="form.propertyType" 
+          :disabled="isEdit"
+          placeholder="请选择属性类型"
+          style="width: 100%"
+        >
+          <el-option label="字符串" value="string" />
+          <el-option label="数值型" value="number" />
+        </el-select>
+      </el-form-item>
+      
+      <el-form-item label="属性备注" prop="remark">
+        <el-input 
+          v-model="form.remark" 
+          type="textarea"
+          :rows="3"
+          placeholder="请输入属性备注"
+        />
+      </el-form-item>
+      
+      <el-form-item label="应用" prop="apps" required>
+        <el-select 
+          v-model="form.apps" 
+          multiple
+          placeholder="请选择应用"
+          style="width: 100%"
+        >
+          <el-option label="教育行业Demo" value="education" />
+          <el-option label="游戏行业Demo" value="game" />
+          <el-option label="通用行业Demo" value="general" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">确定</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive, watch } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+
+interface Props {
+  modelValue: boolean
+  isEdit: boolean
+  data?: any
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'submit', data: any): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 表单引用
+const formRef = ref<FormInstance>()
+
+// 可见性绑定
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 表单数据
+const form = reactive({
+  propertyName: '',
+  propertyIdentifier: '',
+  propertyType: '',
+  remark: '',
+  apps: [] as string[]
+})
+
+// 表单验证规则
+const rules = reactive<FormRules>({
+  propertyName: [
+    { required: true, message: '请输入属性名称', trigger: 'blur' }
+  ],
+  propertyIdentifier: [
+    { required: true, message: '请输入属性标识符', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_.\-+]+$/, message: '只能包含字母、数字、下划线(_)、中划线(-)、小数点(.)及加号(+)', trigger: 'blur' },
+    { max: 128, message: '长度不能超过128个字符', trigger: 'blur' }
+  ],
+  propertyType: [
+    { required: true, message: '请选择属性类型', trigger: 'change' }
+  ],
+  apps: [
+    { required: true, message: '请选择应用', trigger: 'change' }
+  ]
+})
+
+// 监听数据变化,用于编辑时填充表单
+watch(() => props.data, (newVal) => {
+  if (newVal && props.isEdit) {
+    form.propertyName = newVal.propertyName || ''
+    form.propertyIdentifier = newVal.propertyIdentifier || ''
+    form.propertyType = newVal.propertyType || ''
+    form.remark = newVal.remark || ''
+    form.apps = newVal.apps || []
+  }
+}, { immediate: true })
+
+// 重置表单
+const resetForm = () => {
+  form.propertyName = ''
+  form.propertyIdentifier = ''
+  form.propertyType = ''
+  form.remark = ''
+  form.apps = []
+  formRef.value?.resetFields()
+}
+
+// 关闭弹窗
+const handleClose = () => {
+  visible.value = false
+  resetForm()
+}
+
+// 处理提交
+const handleSubmit = async () => {
+  if (!formRef.value) return
+  
+  await formRef.value.validate((valid) => {
+    if (valid) {
+      emit('submit', {
+        ...form,
+        isEdit: props.isEdit
+      })
+      handleClose()
+    }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.form-item-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 399 - 0
src/views/count/featureUsage/eventMannage/EventTable.vue

@@ -0,0 +1,399 @@
+<template>
+  <div class="layout-padding">
+    <div class="!overflow-auto px-1">
+      <Lcard style="height: calc(100vh - 96px); margin-bottom: 0px;">
+        <div class="flex justify-between items-center">
+          <Title left-line title="事件管理">
+          </Title>
+          <div>
+            <el-button @click="() => showBatchImportModal = true" icon="Download">批量导入事件</el-button>
+            <el-button @click="() => showAddEventModal = true" icon="Plus" type="primary">添加事件</el-button>
+          </div>
+        </div>
+        <div style="margin: 20px 60px;">
+          <el-radio-group v-model="eventTypeRadio">
+            <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-group>
+          <div class="line" style="margin: 0 0 20px 0;"></div>
+          <div class="flex search-box" style="margin-bottom: 20px;">
+            <el-input placeholder="请输入事件名/事件标识符" style="width: 230px;"></el-input>
+            <el-select placeholder="选择平台" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="Android" value="1"></el-option>
+              <el-option label="IOS" value="2"></el-option>
+              <el-option label="IPad" value="3"></el-option>
+              <el-option label="Harmony" value="4"></el-option>
+            </el-select>
+            <el-select placeholder="近三十天事件上报数" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="有数据" value="1"></el-option>
+              <el-option label="无数据" value="2"></el-option>
+            </el-select>
+            <el-select placeholder="选择应用" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="教育行业Demo" value="1"></el-option>
+              <el-option label="游戏行业Demo" value="2"></el-option>
+              <el-option label="通用行业Demo" value="3"></el-option>
+            </el-select>
+            <el-select placeholder="操作人" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="nina" value="1"></el-option>
+              <el-option label="luoyu" value="2"></el-option>
+              <el-option label="jcq" value="3"></el-option>
+              <el-option label="lwh" value="4"></el-option>
+            </el-select>
+          </div>
+          <el-table :height="500" :data="pagedTableRows" border>
+            <el-table-column prop="eventName" label="事件名" min-width="100" />
+            <el-table-column prop="eventIdentifier" label="事件标识符" min-width="140" />
+            <el-table-column label="计算状态" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="近30天事件上报数" min-width="100" align="center">
+              <template #default="scope">
+                <span v-if="scope.row.reportCount > 0">{{ scope.row.reportCount.toLocaleString() }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="上报平台" min-width="100">
+              <template #default="scope">
+                {{ getPlatformText(scope.row.platforms) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="应用" min-width="100">
+              <template #default="scope">
+                {{ getAppText(scope.row.apps) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" min-width="200" fixed="right">
+              <template #default="scope">
+                <el-button @click="eventEdit(scope.row)" type="primary" link>编辑</el-button>
+                <el-button type="primary" link>详情</el-button>
+                <el-button v-if="scope.row.status === '计算中'" type="primary" link>暂停</el-button>
+                <el-button v-else-if="scope.row.status === '暂停计算'" type="primary" link>启用</el-button>
+                <el-button v-else type="primary" link>注册</el-button>
+                <el-button type="danger" link>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div 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>
+      <!-- 添加批量导入弹窗 -->
+      <BatchImportModal v-model="showBatchImportModal" @import="handleBatchImport" />
+      <!-- 添加事件弹窗 -->
+      <AddEventModal v-model="showAddEventModal" @add="handleAddEvent" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" name="countMainTrend" setup>
+// import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import { useI18n } from 'vue-i18n';
+import * as echarts from 'echarts';
+// 引入组件
+const Lcard = defineAsyncComponent(() => import('/@/components/LYcom/Lcard/index.vue'));
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const { t } = useI18n();
+const eventTypeRadio = ref('1')
+
+onMounted(() => {
+  setTimeout(() => {
+  }, 500);
+});
+
+const emit = defineEmits(['update:eventId'])
+
+// 编辑事件
+const eventEdit = (data: any) => {
+  console.log(data);
+  emit('update:eventId', data.id)
+}
+
+// 导入批量导入弹窗组件
+const BatchImportModal = defineAsyncComponent(() => import('./BatchImportModal.vue'))
+
+// 控制批量导入弹窗显示
+const showBatchImportModal = ref(false)
+
+// 处理批量导入
+const handleBatchImport = (data: any) => {
+  console.log('批量导入数据:', data)
+  // 这里可以实现实际的导入逻辑
+  // 例如发送请求到后端处理Excel文件
+}
+
+// 导入添加事件弹窗组件
+const AddEventModal = defineAsyncComponent(() => import('./AddEventModal.vue'))
+
+// 控制添加事件弹窗显示
+const showAddEventModal = ref(false)
+
+// 处理添加事件
+const handleAddEvent = (data: any) => {
+  console.log('添加事件数据:', data)
+  // 这里可以实现实际的添加事件逻辑
+  // 例如发送请求到后端创建新事件
+}
+
+const searchData = reactive({
+  eventType: '1',
+  channel: '',
+  eventName: '',
+  eventHas: '',
+  appId: '',
+  evnetUser: '',
+})
+
+// 表格相关数据
+interface EventTableRow {
+  id: string;
+  eventName: string;
+  eventIdentifier: string;
+  status: string;
+  reportCount: number;
+  platforms: string[];
+  apps: string[];
+  operator: string;
+}
+
+// 表格相关(静态数据)
+const currentPage = ref(1);
+const pageSize = ref(10);
+
+const tableRows = ref<EventTableRow[]>([
+  {
+    id: '1',
+    eventName: '用户注册',
+    eventIdentifier: 'user_register',
+    status: '计算中',
+    reportCount: 12560,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '2',
+    eventName: '用户登录',
+    eventIdentifier: 'user_login',
+    status: '计算中',
+    reportCount: 45210,
+    platforms: ['Android', 'IOS', 'Harmony'],
+    apps: ['教育行业Demo', '游戏行业Demo'],
+    operator: 'luoyu'
+  },
+  {
+    id: '3',
+    eventName: '商品浏览',
+    eventIdentifier: 'product_view',
+    status: '暂停计算',
+    reportCount: 0,
+    platforms: ['Android'],
+    apps: ['游戏行业Demo'],
+    operator: 'jcq'
+  },
+  {
+    id: '4',
+    eventName: '加入购物车',
+    eventIdentifier: 'add_to_cart',
+    status: '计算中',
+    reportCount: 8650,
+    platforms: ['Android', 'IOS'],
+    apps: ['游戏行业Demo'],
+    operator: 'lwh'
+  },
+  {
+    id: '5',
+    eventName: '完成支付',
+    eventIdentifier: 'payment_complete',
+    status: '计算中',
+    reportCount: 3240,
+    platforms: ['Android', 'IOS', 'Harmony'],
+    apps: ['教育行业Demo', '通用行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '6',
+    eventName: '分享应用',
+    eventIdentifier: 'share_app',
+    status: '未注册',
+    reportCount: 0,
+    platforms: [],
+    apps: [],
+    operator: 'luoyu'
+  },
+  {
+    id: '7',
+    eventName: '查看个人资料',
+    eventIdentifier: 'view_profile',
+    status: '计算中',
+    reportCount: 15680,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo', '游戏行业Demo', '通用行业Demo'],
+    operator: 'jcq'
+  },
+  {
+    id: '8',
+    eventName: '搜索功能',
+    eventIdentifier: 'search_function',
+    status: '暂停计算',
+    reportCount: 0,
+    platforms: ['IOS'],
+    apps: ['教育行业Demo'],
+    operator: 'lwh'
+  },
+  {
+    id: '9',
+    eventName: '视频播放',
+    eventIdentifier: 'video_play',
+    status: '计算中',
+    reportCount: 27890,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '10',
+    eventName: '收藏商品',
+    eventIdentifier: 'favorite_product',
+    status: '计算中',
+    reportCount: 5620,
+    platforms: ['Android', 'IOS', 'Harmony'],
+    apps: ['游戏行业Demo', '通用行业Demo'],
+    operator: 'luoyu'
+  },
+  {
+    id: '11',
+    eventName: '评论功能',
+    eventIdentifier: 'comment_function',
+    status: '未注册',
+    reportCount: 0,
+    platforms: [],
+    apps: [],
+    operator: 'jcq'
+  },
+  {
+    id: '12',
+    eventName: '消息推送点击',
+    eventIdentifier: 'push_notification_click',
+    status: '计算中',
+    reportCount: 9850,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo', '游戏行业Demo'],
+    operator: 'lwh'
+  },
+  {
+    id: '13',
+    eventName: '页面停留',
+    eventIdentifier: 'page_stay',
+    status: '计算中',
+    reportCount: 31560,
+    platforms: ['Android', 'Harmony'],
+    apps: ['通用行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '14',
+    eventName: '应用启动',
+    eventIdentifier: 'app_launch',
+    status: '暂停计算',
+    reportCount: 0,
+    platforms: ['Android'],
+    apps: ['教育行业Demo'],
+    operator: 'luoyu'
+  },
+  {
+    id: '15',
+    eventName: '退出应用',
+    eventIdentifier: 'app_exit',
+    status: '未注册',
+    reportCount: 0,
+    platforms: [],
+    apps: [],
+    operator: 'jcq'
+  }
+]);
+
+const pagedTableRows = computed(() => {
+  const startIndex = (currentPage.value - 1) * pageSize.value;
+  return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+
+// 获取平台显示文本
+const getPlatformText = (platforms: string[]) => {
+  if (platforms.length === 0) return '-';
+  if (platforms.length > 2) return `${platforms[0]}等${platforms.length}个`;
+  return platforms.join('、');
+};
+
+// 获取应用显示文本
+const getAppText = (apps: string[]) => {
+  if (apps.length === 0) return '-';
+  if (apps.length > 2) return `${apps[0]}等${apps.length}个`;
+  return apps.join('、');
+};
+
+// 状态标签样式
+const getStatusType = (status: string) => {
+  switch (status) {
+    case '计算中':
+      return 'success';
+    case '暂停计算':
+      return 'warning';
+    case '未注册':
+      return 'info';
+    default:
+      return 'info';
+  }
+};
+</script>
+<style lang="scss" scoped>
+.search-box>div {
+  margin-right: 20px;
+}
+
+.msg {
+  height: 30px;
+  background-color: #f4f5fa;
+  line-height: 30px;
+  font-size: 12px;
+}
+
+.link {
+  color: #167af0;
+  cursor: pointer;
+}
+
+.box1 {
+  display: flex;
+  margin-top: 30px;
+
+  .card-box1,
+  .card-box2 {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: start;
+  }
+
+  .card-box1 {
+    .info-card {
+      width: 48%;
+    }
+  }
+
+  .card-box2 {
+    .info-card {
+      width: 33%;
+    }
+  }
+}
+
+.line {
+  margin: 60px -30px 30px;
+  height: 1px;
+  background-color: #E6E6E6;
+}
+</style>

+ 10 - 0
src/views/count/featureUsage/eventMannage/index.vue

@@ -0,0 +1,10 @@
+<template>
+  <EventTable v-if="!eventId" v-model:eventId="eventId"></EventTable>
+  <EventEdit v-else v-model:eventId="eventId"></EventEdit>
+</template>
+<script setup lang="ts" name="countMainTrend">
+const EventTable = defineAsyncComponent(() => import('./EventTable.vue'));
+const EventEdit = defineAsyncComponent(() => import('./EventEdit.vue'));
+
+const eventId = ref(null)
+</script>

+ 9 - 0
src/views/count/main/trend/icons/icon1.svg

@@ -1,3 +1,12 @@
 <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
 <rect width="48" height="48" rx="14" fill="#167AF0" fill-opacity="0.1"/>
+<g clip-path="url(#clip0_554_3349)">
+<path d="M24 23.2507C25.0652 23.2507 26.1064 22.9348 26.9921 22.3431C27.8778 21.7513 28.568 20.9102 28.9757 19.9261C29.3833 18.942 29.4899 17.8591 29.2821 16.8144C29.0743 15.7697 28.5614 14.8101 27.8082 14.0569C27.055 13.3037 26.0954 12.7908 25.0507 12.583C24.006 12.3752 22.9231 12.4818 21.939 12.8894C20.955 13.2971 20.1138 13.9874 19.5221 14.873C18.9303 15.7587 18.6144 16.7999 18.6144 17.8651C18.6144 19.2934 19.1818 20.6633 20.1918 21.6733C21.2018 22.6833 22.5717 23.2507 24 23.2507ZM30.7824 25.4395C29.0628 24.187 27.03 23.4351 24.9093 23.2672C22.7886 23.0993 20.6628 23.5219 18.7675 24.4882C16.8723 25.4544 15.2816 26.9266 14.1718 28.7416C13.062 30.5565 12.4764 32.6433 12.48 34.7707V35.5195H27.4272C26.5728 34.7725 25.958 33.7898 25.6602 32.6946C25.3623 31.5994 25.3947 30.4407 25.7531 29.3638C26.1115 28.287 26.7801 27.34 27.6749 26.6419C28.5697 25.9437 29.6507 25.5254 30.7824 25.4395Z" fill="#167AF0"/>
+<path d="M34.08 29.7594H32.64V28.3194C32.64 27.9375 32.4883 27.5712 32.2183 27.3012C31.9482 27.0311 31.5819 26.8794 31.2 26.8794C30.8181 26.8794 30.4519 27.0311 30.1818 27.3012C29.9117 27.5712 29.76 27.9375 29.76 28.3194V29.7594H28.32C27.9381 29.7594 27.5719 29.9111 27.3018 30.1812C27.0317 30.4512 26.88 30.8175 26.88 31.1994C26.88 31.5813 27.0317 31.9476 27.3018 32.2176C27.5719 32.4877 27.9381 32.6394 28.32 32.6394H29.76V34.0794C29.76 34.4613 29.9117 34.8276 30.1818 35.0976C30.4519 35.3677 30.8181 35.5194 31.2 35.5194C31.5819 35.5194 31.9482 35.3677 32.2183 35.0976C32.4883 34.8276 32.64 34.4613 32.64 34.0794V32.6394H34.08C34.4619 32.6394 34.8282 32.4877 35.0983 32.2176C35.3683 31.9476 35.52 31.5813 35.52 31.1994C35.52 30.8175 35.3683 30.4512 35.0983 30.1812C34.8282 29.9111 34.4619 29.7594 34.08 29.7594Z" fill="#167AF0"/>
+</g>
+<defs>
+<clipPath id="clip0_554_3349">
+<rect width="23.04" height="23.04" fill="white" transform="translate(12.48 12.4795)"/>
+</clipPath>
+</defs>
 </svg>

File diff suppressed because it is too large
+ 1 - 0
src/views/count/main/trend/icons/icon2.svg


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

@@ -92,7 +92,7 @@
                </div>
             </template>
          </Lcard>
-         <Lcard :height="975">
+         <Lcard>
             <el-date-picker style="float: left;" v-model="timeRange" type="datetimerange" range-separator="To"
                start-placeholder="Start date" end-placeholder="End date" />
             <div class="link" style="float: right;">订阅</div>
@@ -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>

Some files were not shown because too many files changed in this diff