Browse Source

fix: 页面

jcq 4 days ago
parent
commit
078fe677b7

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

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

@@ -0,0 +1,418 @@
+<template>
+	<div class="layout-padding">
+		<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>
+					<div class="flex items-center space-x-4">
+						<!-- 显示模式切换 -->
+						<div class="flex items-center">
+							<el-radio-group v-model="displayMode" size="small">
+								<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 top-0 w-2 h-2 bg-white 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-0 w-2 h-2 bg-white 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>
+						
+						<!-- 导出按钮 -->
+						<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 } from 'vue';
+import * as echarts from 'echarts';
+import { QuestionFilled, Download } from '@element-plus/icons-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>