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