Parcourir la source

feat: 用户活跃分析逻辑修改

zhaonan il y a 3 jours
Parent
commit
d8693a0372

+ 41 - 7
src/components/analytics-filter-header/index.vue

@@ -25,12 +25,15 @@
 <script setup lang="ts">
 import dayjs from 'dayjs';
 import { getAppList } from '/@/api/marketing/config';
-import { setAppID,APP_ID_STORAGE_KEY } from '/@/api/common/common';
+import { setAppID, APP_ID_STORAGE_KEY } from '/@/api/common/common';
+import { useMessage } from '/@/hooks/message';
+import { ElMessageBox } from 'element-plus';
 
 const props = defineProps<{
 	modelValue: Record<string, any>
-	channelDisabled?: boolean
-	versionDisabled?: boolean
+	channelDisabled?: boolean;
+	versionDisabled?: boolean;
+	chartType?: string;
 }>();
 
 const emit = defineEmits<{
@@ -44,6 +47,7 @@ const formRef = ref();
 const appID = ref(localStorage.getItem(APP_ID_STORAGE_KEY) || '-1');
 
 const appOptions = ref<any[]>([]);
+
 const getAppListData = async () => {
 	const res = await getAppList({ isAll: false });
 	const data = res.data;
@@ -70,7 +74,7 @@ const local = reactive<Record<string, any>>({
 	channel: props.modelValue?.channel,
 	version: props.modelValue?.version,
 });
-
+const lastDate = ref(local.time);
 function disableFuture(date: Date) {
 	const today = new Date();
 	today.setHours(0, 0, 0, 0);
@@ -78,6 +82,36 @@ function disableFuture(date: Date) {
 }
 
 function onRangeChange(val: [string, string] | null) {
+	// 分时活跃用户限制只能选择7天以内
+	if (props.chartType === "分时活跃用户") {
+		const diff = dayjs(val?.[1]).diff(val?.[0], "day");
+		if (diff >= 7) {
+			local.time = lastDate.value;
+			ElMessageBox.confirm("“分时活跃用户”最多可选择7天(或以下)的时间长度", "提示", {
+				showCancelButton: false
+			})
+			return;
+		}
+	} else if (props.chartType === "周活跃度") {
+		const diff = dayjs(val?.[1]).diff(val?.[0], "day");
+		if (diff < 7) {
+			local.time = lastDate.value;
+			ElMessageBox.confirm("“用户周活跃率”须最少选择8天(或以上)的时间长度", "提示", {
+				showCancelButton: false
+			})
+			return;
+		}
+	} else if (props.chartType === "月活跃度") {
+		const diff = dayjs(val?.[1]).diff(val?.[0], "day");
+		if (diff < 30) {
+			local.time = lastDate.value;
+			ElMessageBox.confirm("用户月活跃率”须最少选择31天(或以上)的时间长度", "提示", {
+				showCancelButton: false
+			})
+			return;
+		}
+	}
+	lastDate.value = val;
 	if (!val) {
 		// 同步到外部对象引用
 		if (props.modelValue) {
@@ -116,9 +150,9 @@ onMounted(() => {
 // 监听应用选择变化,更新全局 appID 与父级数据
 watch(appID, (val) => {
 	setAppID(val || '');
-	if(val === '-1'){
+	if (val === '-1') {
 		versionDisabled.value = true
-	}else if(!props.versionDisabled){
+	} else if (!props.versionDisabled) {
 		versionDisabled.value = false
 	}
 	if (props.modelValue) {
@@ -128,7 +162,7 @@ watch(appID, (val) => {
 	}
 });
 
-const channelDisabled = ref( props.channelDisabled === true);
+const channelDisabled = ref(props.channelDisabled === true);
 const versionDisabled = ref(props.versionDisabled === true || appID.value === '-1');
 </script>
 

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

@@ -8,18 +8,20 @@
 						<el-option label="活跃趋势" value="活跃趋势" />
 						<el-option label="活跃构成" value="活跃构成" />
 						<el-option label="活跃粘度" value="活跃粘度" />
-						<el-option label="分时活跃用户" value="分时活跃用户" />
-						<el-option label="周活跃度" value="周活跃度" />
-						<el-option label="月活跃度" value="月活跃度" />
+						<el-option :disabled="isTrendDisabled" label="分时活跃用户" value="分时活跃用户" />
+						<el-option :disabled="isWeekRateDisabled" label="周活跃度" value="周活跃度" />
+						<el-option :disabled="isMonthRateDisabled" label="月活跃度" value="月活跃度" />
 					</el-select>
-					<el-select v-model="industryCompare" class="!w-[120px] ml-2" clearable @change="handleCompareChange">
+					<el-select v-model="industryCompare" class="!w-[120px] ml-2" clearable
+						@change="handleCompareChange">
 						<el-option label="版本对比" value="version" />
 						<el-option label="渠道对比" value="channel" />
 						<el-option label="时段对比" value="time" />
 					</el-select>
 
 					<!-- 版本对比和渠道对比使用popover -->
-					<el-popover v-if="industryCompare !== 'time' && industryCompare" placement="bottom" trigger="click" width="400">
+					<el-popover v-if="industryCompare !== 'time' && industryCompare" placement="bottom" trigger="click"
+						width="400">
 						<template #reference>
 							<el-button class="ml-2">{{ t('addUser.average') }}</el-button>
 						</template>
@@ -27,16 +29,12 @@
 							<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"
-									/>
+									<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">
+									<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>
@@ -47,32 +45,19 @@
 					</el-popover>
 
 					<!-- 时段对比使用日期选择 -->
-					<el-popover
-						v-if="industryCompare === 'time'"
-						placement="bottom"
-						trigger="click"
-						width="300"
-						:visible="timeCompareVisible"
-						:hide-after="0"
-						:persistent="true"
-					>
+					<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('addUser.average') }}</el-button>
+							<el-button class="ml-2" @click="timeCompareVisible = !timeCompareVisible">{{
+								t('addUser.average') }}</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"
-									/>
+									<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>
 							<!-- <div class="flex justify-end">
@@ -84,7 +69,7 @@
 
 					<el-button link class="ml-2" @click="clearCompare(), getData('clearAll')">清除</el-button>
 				</div>
-				<div class="flex items-center">
+				<div class="flex items-center" v-if="chartType !== '分时活跃用户'">
 					<el-radio-group v-model="formData.timeUnit" @change="getData(''), getBottomDetail()">
 						<el-radio-button label="day" :disabled="isDayDisabled">天</el-radio-button>
 						<el-radio-button label="week" :disabled="isWeekDisabled">周</el-radio-button>
@@ -101,7 +86,8 @@
 		<!-- 明细表格 -->
 		<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="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" />
@@ -117,33 +103,30 @@
 				<el-table-column prop="activeUser" label="活跃用户数" align="right" min-width="100" />
 				<el-table-column label="活跃构成(新用户占比)" align="right" min-width="140">
 					<template #default="{ row }">
-						<div v-if="row.newUserRate" class="flex items-center justify-end w-full">{{ (row.newUserRate * 100).toFixed(2) }}%</div>
+						<div v-if="row.newUserRate" class="flex items-center justify-end w-full">{{ (row.newUserRate *
+							100).toFixed(2) }}%</div>
 						<span v-else>--</span>
 					</template>
 				</el-table-column>
 				<el-table-column label="DAU/过去7日活跃用户" align="right" min-width="140">
 					<template #default="{ row }">
-						<div v-if="row.wauRate" class="flex items-center justify-end w-full">{{ (row.wauRate * 100).toFixed(2) }}%</div>
+						<div v-if="row.wauRate" class="flex items-center justify-end w-full">{{ (row.wauRate *
+							100).toFixed(2) }}%</div>
 						<span v-else>--</span>
 					</template>
 				</el-table-column>
 				<el-table-column label="DAU/过去30日活跃用户" align="right" min-width="140">
 					<template #default="{ row }">
-						<div v-if="row.mauRate" class="flex items-center justify-end w-full">{{ (row.mauRate * 100).toFixed(2) }}%</div>
+						<div v-if="row.mauRate" class="flex items-center justify-end w-full">{{ (row.mauRate *
+							100).toFixed(2) }}%</div>
 						<span v-else>--</span>
 					</template>
 				</el-table-column>
 			</el-table>
 			<div v-if="showDetail" class="flex justify-end mt-3">
-				<el-pagination
-					v-model:current-page="pagination.current"
-					v-model:page-size="pagination.size"
-					@change="getBottomDetail"
-					background
-					layout="total, prev, pager, next, sizes"
-					:total="pagination.total"
-					:page-sizes="[5, 10, 20]"
-				/>
+				<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size"
+					@change="getBottomDetail" background layout="total, prev, pager, next, sizes"
+					:total="pagination.total" :page-sizes="[5, 10, 20]" />
 			</div>
 		</div>
 	</div>
@@ -182,6 +165,11 @@ const props = defineProps({
 		default: () => ({}),
 	},
 });
+defineExpose({
+	chartType
+})
+
+const timeDiff = computed(() => dayjs(props.formData.time[1]).diff(props.formData.time[0], "day"));
 
 interface FormDataType {
 	timeUnit: string;
@@ -216,11 +204,15 @@ const loading = ref(false);
 
 const onChangeType = (value: string) => {
 	if (value !== '分时活跃用户') {
+		if (value === "活跃粘度") {
+			formData.value.timeUnit = "day";
+		}
 		getData('clearAll');
 		emit('query', '');
 
 	}
-	if(value === '分时活跃用户' || value === '周活跃度' || value === '月活跃率'){
+	if (value === '分时活跃用户' || value === '周活跃度' || value === '月活跃率') {
+		getData('clearAll');
 		emit('query', 'disabled');
 	}
 };
@@ -260,6 +252,13 @@ const getData = async (type: string) => {
 			formData.value.timeUnit = 'month';
 			res = await getMonthrate(formData.value);
 			data = res.data || [];
+		} else if (chartType.value === '分时活跃用户') {
+			const params = {
+				...formData.value
+			};
+			params.timeUnit = "hour";
+			res = await getTrend(params);
+			data = res.data || [];
 		}
 
 	} catch (error) {
@@ -391,10 +390,58 @@ const rangeDays = computed(() => {
 	if (!start || !end) return 0;
 	return getDaysBetweenDates(start, end);
 });
+const isTrendDisabled = computed(() => {
+	return timeDiff.value >= 7;
+})
+const isWeekRateDisabled = computed(() => {
+	return timeDiff.value < 7
+})
+const isMonthRateDisabled = computed(() => {
+	return timeDiff.value < 30
+})
+const isWeekDisabled = computed(() => {
+	if (chartType.value === "活跃粘度") {
+
+		return true;
+	}
+	if (chartType.value === "月活跃度") {
+		return true;
+	}
+	if (!isTrendDisabled.value) {
+		return true;
+	}
+	if (!isWeekRateDisabled.value && isMonthRateDisabled.value) {
+		if (chartType.value === "活跃粘度") {
+
+			return true;
+		}
+	}
+});
+const isMonthDisabled = computed(() => {
+	// rangeDays.value < 28
+	if (chartType.value === "活跃粘度") {
 
-const isWeekDisabled = computed(() => rangeDays.value < 7);
-const isMonthDisabled = computed(() => rangeDays.value < 28);
-const isDayDisabled = computed(() => false);
+		return true;
+	}
+	if (chartType.value === "周活跃度") {
+		return true;
+	}
+	if (!isTrendDisabled.value) {
+		return true;
+	}
+	if (!isWeekRateDisabled.value && isMonthRateDisabled.value) {
+		return true;
+	}
+});
+const isDayDisabled = computed(() => {
+	if (chartType.value === "周活跃度") {
+		return true;
+	}
+	if (chartType.value === "月活跃度") {
+		return true;
+	}
+	return false;
+});
 
 function ensureValidTimeUnit() {
 	// 不进行自动纠正,保持用户选择
@@ -451,58 +498,58 @@ function filterCompareOptions() {
 }
 
 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') {
-    lineChartData.value.items[0].name = '全部版本';
-  } else if (industryCompare.value === 'channel') {
-    lineChartData.value.items[0].name = '全部渠道';
-  }
-
-  // 清理被取消的对比项
-  if (removedItemIndices.length > 0) {
-    // 从后往前删除,避免索引变化影响
-    removedItemIndices
-      .sort((a, b) => b - a)
-      .forEach(index => {
-        lineChartData.value.items.splice(index + 1, 1);
-      });
-  }
-
-  // 更新表单数据
-  const isVersion = industryCompare.value === 'version';
-  if (isVersion) {
-    formData.value.version = addedItems;
-    formData.value.channel = props.formData?.channel ? [props.formData?.channel] : [];
-  } else {
-    formData.value.channel = addedItems;
-    formData.value.version = props.formData?.version ? [props.formData?.version] : [];
-  }
-
-  // 根据是否有新增项来决定是否重新获取数据
-  if (addedItems.length > 0) {
-    getData('');
-  } else if (removedItemIndices.length > 0) {
-    // 如果只是移除了项,则重新初始化图表
-    initLineChart();
-  }
+	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') {
+		lineChartData.value.items[0].name = '全部版本';
+	} else if (industryCompare.value === 'channel') {
+		lineChartData.value.items[0].name = '全部渠道';
+	}
+
+	// 清理被取消的对比项
+	if (removedItemIndices.length > 0) {
+		// 从后往前删除,避免索引变化影响
+		removedItemIndices
+			.sort((a, b) => b - a)
+			.forEach(index => {
+				lineChartData.value.items.splice(index + 1, 1);
+			});
+	}
+
+	// 更新表单数据
+	const isVersion = industryCompare.value === 'version';
+	if (isVersion) {
+		formData.value.version = addedItems;
+		formData.value.channel = props.formData?.channel ? [props.formData?.channel] : [];
+	} else {
+		formData.value.channel = addedItems;
+		formData.value.version = props.formData?.version ? [props.formData?.version] : [];
+	}
+
+	// 根据是否有新增项来决定是否重新获取数据
+	if (addedItems.length > 0) {
+		getData('');
+	} else if (removedItemIndices.length > 0) {
+		// 如果只是移除了项,则重新初始化图表
+		initLineChart();
+	}
 }
 
 // 初始化对比选项

+ 4 - 2
src/views/count/user/activeUser/index.vue

@@ -45,14 +45,14 @@
 				<div class="mt-2">
 					<el-row shadow="hover" class="">
 						<AnalyticsFilterHeader v-model:model-value="formData" :channel-disabled="channelDisabled"
-							:version-disabled="versionDisabled" />
+							:version-disabled="versionDisabled" :chartType="addTrendRef?.chartType" />
 					</el-row>
 				</div>
 			</div>
 
 			<!-- 新增趋势模块 -->
 			<div class="mt-3">
-				<AddTrend :form-data="formData" @query="isDisabled" />
+				<AddTrend :form-data="formData" @query="isDisabled" ref="addTrendRef" />
 			</div>
 		</div>
 	</div>
@@ -67,6 +67,8 @@ const { t } = useI18n();
 const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
 const AnalyticsFilterHeader = defineAsyncComponent(() => import('/@/components/analytics-filter-header/index.vue'));
 const AddTrend = defineAsyncComponent(() => import('./components/AddTrend.vue'));
+const addTrendRef = ref<{chartType: string}>();
+
 // 计算默认时间范围(半个月前到今天)
 const getDefaultDateRange = () => {
 	const endDate = dayjs();