jcq 1 dzień temu
rodzic
commit
c108aa24b0

+ 2 - 2
.env

@@ -5,8 +5,8 @@ VITE_IS_MICRO= true
 VITE_PUBLIC_PATH = /
 
 # 后端请求前缀
-# VITE_API_URL = http://192.168.10.101:9999
-VITE_API_URL = http://192.168.3.118:9999
+VITE_API_URL = http://192.168.10.101:9999
+# VITE_API_URL = http://192.168.3.118:9999
 #  VITE_API_URL = http://192.168.3.17:9999
 
 # OAUTH2 密码模式客户端信息

+ 45 - 0
src/api/count/version.ts

@@ -0,0 +1,45 @@
+import request from '/@/utils/request';
+import { appID } from '/@/api/common/common';
+
+//查询全部版本详情
+export const getTrendDetailAll = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/detail',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+//查询单个版本详情
+export const getTrendDetailOne = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/single/detail',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+//查询单个版本用户来源详情
+export const getTrendDetailSource = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/distribution/detail',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+
+//升级用户趋势图
+export const getTrendUpgrade = (data?: Object) => {
+	return request({
+		url: '/stats/user/upgrade/trend',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+
+//查询单个版本用户来源
+export const getTrendSource = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/distribution',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};

+ 21 - 2
src/components/common/filter-select.vue

@@ -38,6 +38,7 @@ interface Props {
   disabled?: boolean
   filterable?: boolean
   wrapperClass?: string
+  showAllVersions?: boolean
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -48,6 +49,7 @@ const props = withDefaults(defineProps<Props>(), {
   disabled: false,
   filterable: false,
   wrapperClass: '',
+  showAllVersions: false,
 })
 
 const emit = defineEmits<{
@@ -74,14 +76,24 @@ const isRemote = computed(() => true)
 
 const placeholderText = computed(() => {
   if (props.placeholder) return props.placeholder
-  return props.type === 'version' ? '全部版本' : '全部渠道'
+  if (props.type === 'version') {
+    return props.showAllVersions ? '全部版本' : '请选择版本'
+  }
+  return '全部渠道'
 })
 
 async function requestOptions(query: string): Promise<SelectOption[]> {
   if (props.type === 'version') {
     const res = await getAppVersion()
     const list: Array<string> = (res as any)?.data || []
-    return list.map((name) => ({ label: String(name), value: name }))
+    const versionOptions = list.map((name) => ({ label: String(name), value: name }))
+
+    // 如果启用了showAllVersions,在列表开头添加"全部版本"选项
+    if (props.showAllVersions) {
+      return [{ label: '全部版本', value: '' }, ...versionOptions]
+    }
+
+    return versionOptions
   }
   if (props.type === 'channel') {
     const res = await getAppChannel()
@@ -102,6 +114,13 @@ function handleRemoteSearch(query: string) {
 
 // initial fetch for remote
 requestOptions('').then((list) => (optionList.value = list))
+
+// 监听showAllVersions变化,重新获取选项
+watch(() => props.showAllVersions, () => {
+  if (props.type === 'version') {
+    requestOptions('').then((list) => (optionList.value = list))
+  }
+})
 </script>
 
 <style scoped lang="scss">

+ 1 - 1
src/views/count/user/activations/components/initiateNumber.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="el-card p-9 mt-3">
+	<div class="el-card p-14 mt-3">
 		<!-- 新增趋势 -->
 		<div class="">
 			<div class="flex items-center justify-between mb-2 mt-3">

+ 1 - 1
src/views/count/user/activeUser/components/AddTrend.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="el-card p-9">
+	<div class="el-card p-14">
 		<!-- 新增趋势 -->
 		<div class="">
 			<div class="flex items-center justify-between mb-2 mt-3">

+ 16 - 3
src/views/count/user/versionDistribution/components/version-source-card.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="mt-2 el-card  p-9">
+	<div class="mt-2 el-card p-14">
 		<div class="flex justify-between">
 			<Title left-line :title="'版本用户来源'">
 				<template #default>
@@ -10,9 +10,22 @@
 						<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>
+									<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
+											>如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span
+										>
+									</p>
 								</div>
 							</div>
 						</template>

+ 460 - 70
src/views/count/user/versionDistribution/components/version-trend-card.vue

@@ -1,10 +1,13 @@
 <template>
-	<div class="mt-2 el-card  p-9">
-		<Title left-line :title="selectedVersionLocal === ''? t('versionDistribution.allVersion') : selectedVersionLocal+t('versionDistribution.version')" >
+	<div class="mt-2 el-card  p-4">
+		<Title left-line
+			:title="selectedVersionLocal === '' ? t('versionDistribution.allVersion') : selectedVersionLocal + 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>
+						<el-icon class="ml-1" style="color: #a4b8cf">
+							<QuestionFilled />
+						</el-icon>
 					</template>
 					<template #default>
 						<div class="ant-popover-inner-content">
@@ -12,24 +15,20 @@
 								<p><span>趋势图展示累计用户排名Top10版本的变化趋势</span></p>
 								<p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
 								<p>
-									<span class="highlight">活跃用户:</span
-									><span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
+									<span
+										class="highlight">活跃用户:</span><span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
 								</p>
 								<p>
-									<span class="highlight">启动次数:</span
-									><span
-										>打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span
-									>
+									<span
+										class="highlight">启动次数:</span><span>打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span>
 								</p>
 								<p>
-									<span class="highlight">版本累计用户(%):</span
-									><span>截止到现在,该版本的累计用户(占累计用户全体的比例);若该版本的用户升级到其他版本,则累计用户会减少</span>
+									<span
+										class="highlight">版本累计用户(%):</span><span>截止到现在,该版本的累计用户(占累计用户全体的比例);若该版本的用户升级到其他版本,则累计用户会减少</span>
 								</p>
 								<p><span class="highlight">升级用户:</span><span>从其他版本升级到该版本的用户(以设备为判断标准)</span></p>
 								<p>
-									<span
-										>如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span
-									>
+									<span>如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span>
 								</p>
 							</div>
 						</div>
@@ -37,24 +36,74 @@
 				</el-popover>
 			</template>
 		</Title>
-		<div class="">
+		<div class=" p-10">
 			<div class="flex items-center justify-between mb-2 mt-3">
-				<div>
-					<el-select v-if="selectedVersionLocal !== ''" v-model="selectedVersionLocal" class="w-[140px] ml-2" style="width: 140px" placeholder="全部频道">
-						<el-option v-for="item in channelOptions" :key="item.value" :label="item.label" :value="item.value" />
+				<div class="flex items-center">
+					<el-select v-if="selectedVersionLocal !== ''" v-model="selectedVersionLocal" class="w-[140px] ml-2"
+						style="width: 140px" placeholder="全部频道">
+						<el-option v-for="item in channelOptions" :key="item.value" :label="item.label"
+							:value="item.value" />
 					</el-select>
-					<el-select v-model="selectedVersionLocal" class="w-[140px] ml-2" style="width: 140px" placeholder="版本对比">
-						<el-option v-for="item in channelOptions" :key="item.value" :label="item.label" :value="item.value" />
+					<el-select v-model="industryCompare" class="!w-[120px] ml-2" clearable
+						@change="handleCompareChange">
+						<el-option label="版本对比" value="version" />
+						<el-option v-if="selectedVersionLocal !== ''" label="渠道对比" value="channel" />
+						<el-option v-if="selectedVersionLocal !== ''" label="时段对比" value="time" />
 					</el-select>
-					<el-button type="primary" class="ml-2">{{ t('versionDistribution.version') }}</el-button>
+
+					<!-- 版本对比和渠道对比使用popover -->
+					<el-popover v-if="industryCompare !== 'time' && industryCompare" placement="bottom" trigger="click"
+						width="400">
+						<template #reference>
+							<el-button class="ml-2">{{ t('versionDistribution.version') }}</el-button>
+						</template>
+						<template #default>
+							<div class="p-3">
+								<div class="mb-3">
+									<label class="text-sm font-medium mb-2 block">{{ getCompareTitle() }}</label>
+									<el-input v-model="searchKeyword" :placeholder="`请搜索${getCompareTypeText()}`"
+										clearable @input="filterCompareOptions" size="small" />
+								</div>
+								<div class="max-h-60 overflow-y-auto">
+									<el-checkbox-group v-model="selectedCompareItems"
+										@change="handleCompareItemsChange">
+										<div v-for="item in filteredCompareOptions" :key="item" class="mb-2">
+											<el-checkbox :label="item" size="small">{{ item }}</el-checkbox>
+										</div>
+									</el-checkbox-group>
+								</div>
+							</div>
+						</template>
+					</el-popover>
+
+					<!-- 时段对比使用日期选择 -->
+					<el-popover v-if="industryCompare === 'time'" placement="bottom" trigger="click" width="300"
+						:visible="timeCompareVisible" :hide-after="0" :persistent="true">
+						<template #reference>
+							<el-button class="ml-2"
+								@click="timeCompareVisible = !timeCompareVisible">{{ t('versionDistribution.version') }}</el-button>
+						</template>
+						<template #default>
+							<div class="p-3">
+								<div class="mb-3">
+									<label class="text-sm font-medium mb-2 block">选择对比时段</label>
+									<el-date-picker v-model="timeCompareRange" type="date" format="YYYY-MM-DD"
+										value-format="YYYY-MM-DD" :disabled-date="disableAfterToday"
+										@change="handleTimeCompareChange" style="width: 100%" :clearable="false" />
+								</div>
+							</div>
+						</template>
+					</el-popover>
+
+					<el-button link class="ml-2" @click="clearCompare()">清除</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="selectedVersionLocal !== ''" label="sjcs">升级用户</el-radio-button>
+						<el-radio-button label="addUser">新增用户</el-radio-button>
+						<el-radio-button label="activeUser">活跃用户</el-radio-button>
+						<el-radio-button label="launchUser">启动次数</el-radio-button>
+						<el-radio-button v-if="selectedVersionLocal !== ''" label="upgradeUser">升级用户</el-radio-button>
 					</el-radio-group>
 				</div>
 			</div>
@@ -74,9 +123,13 @@
 							<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="showDetail = !showDetail">
+					<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]"
+						@click="showDetail = !showDetail">
 						{{ showDetail ? '收起明细数据' : '展开明细数据' }}
-						<el-icon class="ml-2"><ArrowDown v-if="showDetail" /> <ArrowUp v-else /> </el-icon>
+						<el-icon class="ml-2">
+							<ArrowDown v-if="showDetail" />
+							<ArrowUp v-else />
+						</el-icon>
 					</div>
 				</div>
 
@@ -90,14 +143,8 @@
 				<el-table-column prop="ratio" label="启动次数(占比)" align="center" min-width="220"> </el-table-column>
 			</el-table>
 			<div v-if="showDetail" 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]"
-				/>
+				<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>
@@ -108,6 +155,17 @@ 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';
+import { getAppVersion, getAppChannel } from '/@/api/common/common';
+import dayjs from 'dayjs';
+//新增用户tab
+import { getTrend as getAddUserTrend } from '/@/api/count/addUser';
+//活跃用户tab
+import { getTrend as getActiveUserTrend } from '/@/api/count/activeUser';
+//启动次数tab
+import { getTrend as getLaunchTrend } from '/@/api/count/activations';
+
+import { getTrendUpgrade, getTrendDetailAll, getTrendDetailOne, getTrendDetailSource, getTrendSource } from '/@/api/count/version';
+
 
 const { t } = useI18n();
 
@@ -127,6 +185,11 @@ const props = defineProps<{
 	modelValue?: string;
 	selectedVersion?: string;
 	channelOptions: ChannelOption[];
+	showAllVersions?: boolean;
+	formData?: {
+		time?: string[];
+		version?: string;
+	};
 }>();
 
 const emit = defineEmits<{
@@ -141,36 +204,232 @@ watch(
 	}
 );
 watch(selectedVersionLocal, (v) => emit('update:selectedVersion', v));
+watch(() => props.formData, (v) => {
+	formData.value.fromDate = v?.time && v.time[0] ? v.time[0] : '';
+	formData.value.toDate = v?.time && v.time[1] ? v.time[1] : '';
+
+})
 
-const timeGranularity = ref<'hour' | 'day' | 'week' | 'month'>('week');
+// 版本、渠道和时段对比相关状态
+const industryCompare = ref('');
+const selectedCompareItems = ref<string[]>([]);
+const searchKeyword = ref('');
+const compareOptions = ref<string[]>([]);
+const filteredCompareOptions = ref<string[]>([]);
+const previousCompareItems = ref<string[]>([]);
+const timeCompareRange = ref<string>('');
+const timeCompareVisible = ref(false);
+
+const timeGranularity = ref<'addUser' | 'activeUser' | 'launchUser' | 'upgradeUser'>('addUser');
 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 },
-]);
+const masterData = ref<any>({
+	//图表主数据
+	dates: [],
+	items: [],
+});
+
+const lineChartData = ref<any>({
+	//图表数据//显示数据
+	dates: [],
+	items: [],
+});
+
+const colorSchemes = [
+	{ color: '#409EFF' }, // 蓝色
+	{ color: '#67C23A' }, // 绿色
+	{ color: '#E6A23C' }, // 黄色
+	{ color: '#F56C6C' }, // 红色
+];
+
+const chartTimes = ref<string[]>([]);
+
+// 表单数据,用于API调用
+const formData = ref({
+	version: [] as string[],
+	channel: [] as string[],
+	fromDate: '',
+	toDate: '',
+	timeUnit: "day",
+});
+
+const getData = async (type: string) => {
+	//上方图表
+	if (type === 'clearAll') {
+		lineChartData.value = { dates: [], items: [] };
+		industryCompare.value = '';
+	}
+
+	let res;
+	let data;
+	console.log(props.formData);
+	formData.value = {
+		...formData.value,
+		fromDate: props.formData?.time?.[0] || '',
+		toDate: props.formData?.time?.[1] || '',
+	};
+	console.log(formData.value);
+	
+
+	// 根据选择的类型调用不同的API
+	if (timeGranularity.value === 'addUser') {
+		res = await getAddUserTrend({ ...formData.value });
+	}
+	console.log(formData.value);
+	
+
+	// 根据选择的类型调用不同的API
+	if (timeGranularity.value === 'addUser') {
+		res = await getAddUserTrend({ ...formData.value });
+		data = res?.data || [];
+	} else if (timeGranularity.value === 'activeUser') {
+		res = await getActiveUserTrend({ ...formData.value });
+		data = res?.data || [];
+	} else if (timeGranularity.value === 'launchUser') {
+		res = await getLaunchTrend({ ...formData.value });
+		data = res?.data || [];
+	} else if (timeGranularity.value === 'upgradeUser') {
+		res = await getTrendUpgrade({ ...formData.value });
+		data = res?.data || [];
+	}
+
+	initChartData(data);
+};
+
+const initChartData = async (data: any) => {
+	if (!industryCompare.value) {
+		masterData.value = data;
+		lineChartData.value.items = data.items.map((item: any, index: number) => {
+			const randomColorIndex = Math.floor(Math.random() * colorSchemes.length);
+			return {
+				name: getName(item, index, data),
+				type: 'line',
+				smooth: true,
+				data: item.data,
+				itemStyle: {
+					color: colorSchemes[randomColorIndex].color,
+				},
+				lineStyle: {
+					color: colorSchemes[randomColorIndex].color,
+				},
+			};
+		});
+		chartTimes.value = [];
+	} else {
+		lineChartData.value.items.push(
+			data.items.map((item: any, index: number) => {
+				const randomColorIndex = Math.floor(Math.random() * colorSchemes.length);
+				return {
+					name: getName(item, index, data),
+					type: 'line',
+					smooth: true,
+					data: item.data,
+					itemStyle: {
+						color: colorSchemes[randomColorIndex].color,
+					},
+					lineStyle: {
+						color: colorSchemes[randomColorIndex].color,
+					},
+				};
+			})[0]
+		);
+		if (industryCompare.value == 'time') {
+			chartTimes.value.push(data.dates);
+			lineChartData.value.items[0].name = formData.value.fromDate + ' ~ ' + formData.value.toDate;
+		} else {
+			chartTimes.value = [];
+		}
+	}
+	lineChartData.value.dates = masterData.value.dates;
+	initLineChart();
+};
+
+const getName = (item: any, index: number, data: any) => {
+	if (industryCompare.value === 'time') {
+		//日期
+		return formData.value.fromDate + ' ~ ' + formData.value.toDate;
+	} else if (industryCompare.value === 'version') {
+		//版本
+		return item.version === 'All' ? item.name : item.version + ' ' + item.name;
+	} else if (industryCompare.value === 'channel') {
+		//渠道
+		return formData.value.channel[0] || '';
+	} else {
+		return item.name;
+	}
+};
+
+function formatNumber(value: number | string): string {
+	const num = typeof value === 'number' ? value : Number(value || 0);
+	return num.toLocaleString('zh-CN');
+}
+
+const formatterTips = (params: any) => {
+	if (!params || !params.length) return '';
+	const date = params[0]?.axisValue || '';
+	const rows = params
+		.map((p: any, index: number) => {
+			const name = p.seriesName || '';
+			const val = formatNumber(p.data);
+			if (industryCompare.value === 'time' && index != 0) {
+				return `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:180px;">
+							<span style="display:flex;align-items:center;gap:6px;">
+								<span style="width:8px;height:8px;border-radius:50%;background:${p.color}"></span>
+								<span>${chartTimes.value[0][p.dataIndex]}</span>
+							</span>
+							<span style="font-variant-numeric: tabular-nums;">${val}</span>
+						</div>`;
+			} else {
+				return `
+			<div style="margin-bottom:6px;color:#93c5fd;">${industryCompare.value === 'time' ? '' : date}</div>
+				
+				<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:180px;">
+							<span style="display:flex;align-items:center;gap:6px;">
+								<span style="width:8px;height:8px;border-radius:50%;background:${p.color}"></span>
+								<span>${industryCompare.value === 'time' ? p.axisValue : name} </span>
+							</span>
+							<span style="font-variant-numeric: tabular-nums;">${val}</span>
+						</div>`;
+			}
+		})
+		.join('');
+	return `<div style="font-size:12px;">
+			${rows}
+		</div>`;
+};
 
 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 },
+		tooltip: {
+			trigger: 'axis',
+			confine: true,
+			axisPointer: { type: 'line' },
+			borderWidth: 0,
+			backgroundColor: 'rgba(17,24,39,0.9)',
+			textStyle: { color: '#fff' },
+			formatter: (params: any) => {
+				return formatterTips(params);
+			},
+		},
+		legend: {
+			data: lineChartData.value.items.map((item: any) => item.name),
+			top: 'bottom',
+			type: 'scroll', // 支持图例滚动
+		},
+		grid: {
+			left: 40,
+			right: 20,
+			top: 20, // 为图例留出空间
+			bottom: 60,
+		},
 		xAxis: {
 			type: 'category',
-			data: lineChartData.value.map((d) => d.x),
+			data: lineChartData.value.dates,
 			axisLine: { lineStyle: { color: '#e5e7eb' } },
 			axisLabel: { color: '#6b7280' },
 			axisTick: { alignWithLabel: true },
@@ -181,32 +440,30 @@ function initLineChart(): void {
 			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),
-			},
-		],
+		series: lineChartData.value.items,
 	};
 	chartInstance.setOption(option);
 }
 
 onMounted(() => {
-	initLineChart();
+	getData('');
+	initCompareOptions();
 });
 
 watch(timeGranularity, () => {
-	initLineChart();
+	getData('');
 });
 
 watch(selectedVersionLocal, () => {
-	initLineChart();
+	industryCompare.value = '';
+	getData('');
+});
+
+// 监听showAllVersions变化,重新初始化对比选项
+watch(() => props.showAllVersions, () => {
+	if (industryCompare.value === 'version') {
+		initCompareOptions();
+	}
 });
 
 const tableRows = ref<TableRow[]>(
@@ -226,6 +483,141 @@ const pagedTableRows = computed(() => {
 	return tableRows.value.slice(startIndex, startIndex + pageSize.value);
 });
 
+// 版本和渠道对比相关函数
+function handleCompareChange(value: string) {
+	selectedCompareItems.value = [];
+	initCompareOptions();
+	timeCompareVisible.value = value === 'time';
+}
+
+function clearCompare() {
+	selectedCompareItems.value = [];
+	timeCompareRange.value = '';
+	industryCompare.value = '';
+	getData('clearAll');
+}
+
+function getCompareTitle(): string {
+	const typeMap = {
+		version: '选择版本',
+		channel: '选择渠道',
+		time: '选择时段',
+	};
+	return typeMap[industryCompare.value as keyof typeof typeMap] || '选择对比项';
+}
+
+function getCompareTypeText(): string {
+	const typeMap = {
+		version: '版本',
+		channel: '渠道',
+		time: '时段',
+	};
+	return typeMap[industryCompare.value as keyof typeof typeMap] || '';
+}
+
+function filterCompareOptions() {
+	if (!searchKeyword.value) {
+		filteredCompareOptions.value = compareOptions.value;
+	} else {
+		filteredCompareOptions.value = compareOptions.value.filter((item) => item.toLowerCase().includes(searchKeyword.value.toLowerCase()));
+	}
+}
+
+function handleCompareItemsChange(items: string[]) {
+	selectedCompareItems.value = items;
+
+	// 对比当前值和之前的值
+	const currentItems = new Set(items);
+	const previousItems = new Set(previousCompareItems.value);
+
+	// 找出新增的项
+	const addedItems = items.filter((item) => !previousItems.has(item));
+
+	// 找出减少的项(在之前有但现在没有的项)
+	const removedItemIndices = previousCompareItems.value
+		.map((item, index) => ({ item, index }))
+		.filter(({ item }) => !currentItems.has(item))
+		.map(({ index }) => index);
+
+	// 更新 previousCompareItems 为当前值,用于下次对比
+	previousCompareItems.value = items;
+
+	// 先处理主数据项的name
+	if (industryCompare.value === 'version') {
+		// 更新图表数据的主线名称为"全部版本"
+		// 这里可以根据实际需要更新图表数据
+	} else if (industryCompare.value === 'channel') {
+		// 更新图表数据的主线名称为"全部渠道"
+		// 这里可以根据实际需要更新图表数据
+	}
+
+	// 处理全部版本选项的特殊逻辑
+	if (industryCompare.value === 'version' && items.includes('全部版本')) {
+		// 如果选择了"全部版本",可以在这里添加特殊处理逻辑
+		// 例如:清空其他版本选择,或者显示所有版本的数据
+		console.log('选择了全部版本选项');
+	}
+
+	// 清理被取消的对比项
+	if (removedItemIndices.length > 0) {
+		// 从后往前删除,避免索引变化影响
+		removedItemIndices
+			.sort((a, b) => b - a)
+			.forEach(index => {
+				// 清理对应的图表数据
+				// 这里可以根据实际需要清理图表数据
+			});
+	}
+
+	// 根据是否有新增项来决定是否重新获取数据
+	if (addedItems.length > 0) {
+		// 触发数据重新获取
+		getData('');
+	} else if (removedItemIndices.length > 0) {
+		// 如果只是移除了项,则重新初始化图表
+		initLineChart();
+	}
+}
+
+// 初始化对比选项
+async function initCompareOptions() {
+	if (industryCompare.value === 'version') {
+		const res = await getAppVersion();
+		const list: Array<string> = res?.data || [];
+		// 如果props.showAllVersions为true,在列表开头添加"全部版本"选项
+		if (props.showAllVersions) {
+			compareOptions.value = ['全部版本', ...list];
+		} else {
+			compareOptions.value = list;
+		}
+	} else if (industryCompare.value === 'channel') {
+		const res = await getAppChannel();
+		const list: Array<string> = res?.data || [];
+		compareOptions.value = list;
+	}
+	filteredCompareOptions.value = compareOptions.value;
+}
+
+// 时段对比相关函数
+function handleTimeCompareChange(value: string) {
+	timeCompareRange.value = value;
+	formData.value.toDate = timeCompareRange.value;
+	formData.value.fromDate = dayjs(timeCompareRange.value).subtract(7, 'day').format('YYYY-MM-DD');
+	formData.value.channel = props.channelOptions?.map(item => item.value) || [];
+	formData.value.version = selectedVersionLocal.value ? [selectedVersionLocal.value] : [];
+
+	getData('');
+
+	timeCompareVisible.value = false;
+	timeCompareRange.value = '';
+}
+
+function disableAfterToday(date: Date) {
+	const today = new Date();
+	today.setHours(0, 0, 0, 0);
+	return date.getTime() > today.getTime();
+}
+
 const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
 const showDetail = ref(true);
 </script>
@@ -235,5 +627,3 @@ const showDetail = ref(true);
 	color: #2196f3;
 }
 </style>
-
-

+ 43 - 32
src/views/count/user/versionDistribution/index.vue

@@ -4,59 +4,70 @@
 			<div class="el-card  p-9">
 				<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=" mt-2">
-						<el-form :inline="true" :model="formData" @keyup.enter="query" ref="queryRef">
+				<div class="mt-2">
+					<el-row shadow="hover" class="">
+						<el-form :inline="true" :model="formData" ref="queryRef">
 							<el-form-item>
-								<el-date-picker v-model="formData.time" type="daterange" start-placeholder="开始时间"
-									end-placeholder="结束时间" />
+								<el-date-picker v-model="formData.time" type="daterange" value-format="YYYY-MM-DD"
+									:disabled-date="disableFuture" @change="handleRangeChange" class="!w-[250px]"
+									start-placeholder="开始时间" end-placeholder="结束时间" />
 							</el-form-item>
 							<el-form-item>
-								<el-select v-model="selectedChannelCompare" class="w-[140px]"
-									:placeholder="selectPlaceholder">
-									<el-option v-for="item in channelCompareOptions" :key="item.value"
-										:label="item.label" :value="item.value" />
-								</el-select>
+								<FilterSelect v-model="formData.version" showAllVersions type="version"
+									class="!w-[180px] ml-2" />
 							</el-form-item>
 						</el-form>
 					</el-row>
 				</div>
 			</div>
-			<version-trend-card v-model:selectedVersion="selectedChannelCompare"
-				:channel-options="channelCompareOptions" />
-			<version-source-card v-if="selectedChannelCompare !== ''" :selected-version="selectedChannelCompare" />
+			<version-trend-card v-model:selectedVersion="formData.version" :form-data="formData" :channel-options="formData.version" />
+			<version-source-card v-if="formData.version !== ''" :form-data="formData" :selected-version="formData.version" />
 		</div>
 	</div>
 </template>
 
 <script setup lang="ts">
 import { ref, defineAsyncComponent } from 'vue';
-import { useRoute } from 'vue-router';
 import { useI18n } from 'vue-i18n';
-import VersionTrendCard from './components/version-trend-card.vue';
-import VersionSourceCard from './components/version-source-card.vue';
+import dayjs from 'dayjs';
+
+const VersionTrendCard = defineAsyncComponent(() => import('./components/version-trend-card.vue'));
+const VersionSourceCard = defineAsyncComponent(() => import('./components/version-source-card.vue'));
+const FilterSelect = defineAsyncComponent(() => import('/@/components/common/filter-select.vue'));
 
 const { t } = useI18n();
 
-const formData = ref<Record<string, any>>({});
-const route = useRoute();
-const showAllParam = route.query.showAll as string | undefined;
-const showAll = showAllParam === undefined || showAllParam === '' || showAllParam === '1' || showAllParam === 'true';
-const selectPlaceholder = showAll ? '全部版本' : '请选择版本';
-const query = () => {
-	console.log(formData.value);
+const getDefaultDateRange = () => {
+	const endDate = dayjs();
+	const startDate = endDate.subtract(15, 'day');
+	return [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')];
 };
+const formData = ref<Record<string, any>>({
+	time: getDefaultDateRange(), // 时间范围
+});
+
 
-const selectedChannelCompare = ref(showAll ? '' : '1.0');
-const baseOptions = [
-	{ label: '1.0', value: '1.0' },
-	{ label: '2.0', value: '2.0' },
-];
-const channelCompareOptions = showAll ? [{ label: '全部版本', value: '' }, ...baseOptions] : baseOptions;
+function disableFuture(date: Date) {
+	const today = new Date();
+	today.setHours(0, 0, 0, 0);
+	return date.getTime() > today.getTime();
+}
+
+function handleRangeChange(val: [string, string] | null) {
+	if (!val) {
+		return;
+	}
+	const [startStr, endStr] = val;
+	const start = dayjs(startStr);
+	let end = dayjs(endStr);
+	// 限制最大跨度为两年(按天计算,不含边界再减一天)
+	if (end.diff(start, 'day') >= 365 * 2) {
+		end = start.add(2, 'year').subtract(1, 'day');
+		formData.value.time = [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')];
+	}
+}
 const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
 </script>