Pārlūkot izejas kodu

fix: 静态页面

jcq 1 dienu atpakaļ
vecāks
revīzija
5308f8f72f

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

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

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

@@ -1,6 +1,6 @@
 export default {
-	retainedUser: {
-		analytics:'留存用户',
+	freshness: {
+		title:'用户新鲜度',
 		growth:'前往U-Growth发短信/Push',
 		ai:'查看完整AI简报',
 		addtrend:'新增趋势',

+ 171 - 95
src/views/count/retained/freshness/index.vue

@@ -3,26 +3,49 @@
 		<div class="!overflow-auto px-1">
 			<div class="el-card p-2">
 				<!-- 顶部控制区域 -->
-				<div class="flex items-center justify-between mb-4">
-					<div class="flex items-center">
-						<h2 class="text-lg font-medium mr-2">用户新鲜度</h2>
-						<el-icon class="text-gray-400"><QuestionFilled /></el-icon>
+				<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" size="small">
+							<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" 
+								<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"
@@ -31,31 +54,31 @@
 								></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 
+									<div
 										class="absolute top-0 h-full bg-blue-500 rounded-full"
-										:style="{ 
-											left: `${startPosition}%`, 
-											width: `${endPosition - startPosition}%` 
+										:style="{
+											left: `${startPosition}%`,
+											width: `${endPosition - startPosition}%`,
 										}"
 									></div>
-									
+
 									<!-- 开始拖拽手柄 -->
-									<div 
-										class="absolute top-0 w-2 h-2 bg-white border border-gray-300 rounded-full cursor-pointer transform -translate-y-1"
+									<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 top-0 w-2 h-2 bg-white border border-gray-300 rounded-full cursor-pointer transform -translate-y-1"
+									<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>
@@ -68,10 +91,9 @@
 								</div>
 							</div>
 						</div>
-						
+
 						<!-- 导出按钮 -->
 						<el-button type="primary" size="small">
-							<el-icon class="mr-1"><Download /></el-icon>
 							导出
 						</el-button>
 					</div>
@@ -92,9 +114,13 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, watch, computed } from 'vue';
+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');
@@ -116,14 +142,14 @@ function startDrag(type: 'start' | 'end') {
 
 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') {
@@ -145,32 +171,32 @@ const colorSchemes = ref([
 		id: 'blue',
 		name: '蓝色系',
 		upperColor: '#7dd3fc',
-		lowerColor: '#3b82f6'
+		lowerColor: '#3b82f6',
 	},
 	{
 		id: 'green',
 		name: '绿色系',
 		upperColor: '#86efac',
-		lowerColor: '#22c55e'
+		lowerColor: '#22c55e',
 	},
 	{
 		id: 'purple',
 		name: '紫色系',
 		upperColor: '#c4b5fd',
-		lowerColor: '#8b5cf6'
+		lowerColor: '#8b5cf6',
 	},
 	{
 		id: 'orange',
 		name: '橙色系',
 		upperColor: '#fed7aa',
-		lowerColor: '#f97316'
+		lowerColor: '#f97316',
 	},
 	{
 		id: 'pink',
 		name: '粉色系',
 		upperColor: '#f9a8d4',
-		lowerColor: '#ec4899'
-	}
+		lowerColor: '#ec4899',
+	},
 ]);
 
 function selectColorScheme(schemeId: string) {
@@ -190,77 +216,127 @@ 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'
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'05-18',
+	'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
+	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
+	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];
-	
+	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'
-			}
+				type: 'cross',
+			},
 		},
 		legend: {
-			show: false
+			show: false,
 		},
 		grid: {
 			left: 40,
 			right: 20,
 			top: 20,
-			bottom: 30
+			bottom: 30,
 		},
 		xAxis: {
 			type: 'category',
 			data: timeData,
 			axisLine: { lineStyle: { color: '#e5e7eb' } },
 			axisLabel: { color: '#6b7280' },
-			axisTick: { alignWithLabel: true }
+			axisTick: { alignWithLabel: true },
 		},
 		yAxis: {
 			type: 'value',
@@ -269,7 +345,7 @@ function initMainChart(): void {
 			interval: 200,
 			axisLine: { show: false },
 			splitLine: { lineStyle: { color: '#f3f4f6' } },
-			axisLabel: { color: '#6b7280' }
+			axisLabel: { color: '#6b7280' },
 		},
 		series: [
 			{
@@ -278,18 +354,18 @@ function initMainChart(): void {
 				stack: 'total',
 				areaStyle: {
 					color: currentScheme.upperColor,
-					opacity: 0.8
+					opacity: 0.8,
 				},
 				lineStyle: {
 					color: currentScheme.upperColor,
-					width: 0
+					width: 0,
 				},
 				itemStyle: {
-					color: currentScheme.upperColor
+					color: currentScheme.upperColor,
 				},
 				data: upperSeriesData,
 				smooth: false,
-				showSymbol: false
+				showSymbol: false,
 			},
 			{
 				name: '下层区域',
@@ -297,20 +373,20 @@ function initMainChart(): void {
 				stack: 'total',
 				areaStyle: {
 					color: currentScheme.lowerColor,
-					opacity: 1
+					opacity: 1,
 				},
 				lineStyle: {
 					color: currentScheme.lowerColor,
-					width: 0
+					width: 0,
 				},
 				itemStyle: {
-					color: currentScheme.lowerColor
+					color: currentScheme.lowerColor,
 				},
 				data: lowerSeriesData,
 				smooth: false,
-				showSymbol: false
-			}
-		]
+				showSymbol: false,
+			},
+		],
 	};
 	mainChart.setOption(option);
 }
@@ -318,36 +394,36 @@ function initMainChart(): void {
 function initSubChart(): void {
 	if (!subChartRef.value) return;
 	if (subChart) subChart.dispose();
-	
+
 	// 获取当前选中的配色方案
-	const currentScheme = colorSchemes.value.find(scheme => scheme.id === selectedColorScheme.value) || colorSchemes.value[0];
-	
+	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'
+			trigger: 'axis',
 		},
 		legend: {
-			show: false
+			show: false,
 		},
 		grid: {
 			left: 40,
 			right: 20,
 			top: 20,
-			bottom: 30
+			bottom: 30,
 		},
 		xAxis: {
 			type: 'category',
 			data: timeData,
 			axisLine: { lineStyle: { color: '#e5e7eb' } },
 			axisLabel: { color: '#6b7280' },
-			axisTick: { alignWithLabel: true }
+			axisTick: { alignWithLabel: true },
 		},
 		yAxis: {
 			type: 'value',
 			axisLine: { show: false },
 			splitLine: { lineStyle: { color: '#f3f4f6' } },
-			axisLabel: { color: '#6b7280' }
+			axisLabel: { color: '#6b7280' },
 		},
 		series: [
 			{
@@ -355,20 +431,20 @@ function initSubChart(): void {
 				type: 'line',
 				areaStyle: {
 					color: currentScheme.lowerColor,
-					opacity: 1
+					opacity: 1,
 				},
 				lineStyle: {
 					color: currentScheme.lowerColor,
-					width: 0
+					width: 0,
 				},
 				itemStyle: {
-					color: currentScheme.lowerColor
+					color: currentScheme.lowerColor,
 				},
 				data: lowerSeriesData,
 				smooth: false,
-				showSymbol: false
-			}
-		]
+				showSymbol: false,
+			},
+		],
 	};
 	subChart.setOption(option);
 }

+ 16 - 6
src/views/count/user/versionDistribution/index.vue

@@ -24,7 +24,7 @@
 				</div>
 			</div>
 			<div class="mt-2 el-card p-2">
-				<Title left-line :title="t('versionDistribution.allVersion')">
+				<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>
@@ -64,6 +64,9 @@
 				<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>
@@ -75,6 +78,7 @@
 								<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>
@@ -88,7 +92,7 @@
 				<div class="mt-3">
 					<div class="flex items-center justify-between mb-2">
 						<div class="flex">
-							<div class="flex items-center">
+							<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>
@@ -121,7 +125,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="mt-2 el-card p-2">
+			<div v-if="selectedChannelCompare !== ''" class="mt-2 el-card p-2">
 				<div class="flex justify-between">
 					<Title left-line :title="'版本用户来源'">
 						<template #default>
@@ -215,9 +219,9 @@ const query = () => {
 
 const selectedChannelCompare = ref('');
 const channelCompareOptions = [
-	{ label: '全部版本', value: '     ' },
-	{ label: '1.0', value: 'a' },
-	{ label: '2.0', value: 'b' },
+	{ label: '全部版本', value: '' },
+	{ label: '1.0', value: '1.0' },
+	{ label: '2.0', value: '2.0' },
 ];
 
 // 图表相关
@@ -370,6 +374,12 @@ watch(timeRange, () => {
 	initBarChart();
 });
 
+watch(selectedChannelCompare, () => {
+	// 静态页面:仅重新渲染
+	initBarChart();
+	initLineChart();
+});
+
 const sourcePage = ref(1);
 const sourcePageSize = ref(5);
 const pagedSourceRows = computed(() => {