|
@@ -0,0 +1,545 @@
|
|
|
+<template>
|
|
|
+ <div class="el-card p-9">
|
|
|
+ <!-- 新增趋势 -->
|
|
|
+ <div class="">
|
|
|
+ <div class="flex items-center justify-between mb-2 mt-3">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <el-select v-model="industryCompare" class="!w-[120px]" 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">
|
|
|
+ <template #reference>
|
|
|
+ <el-button class="ml-2">{{ 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">{{ 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('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" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- <div class="flex justify-end">
|
|
|
+ <el-button size="small" @click="clearTimeCompare">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="confirmTimeCompare">确定</el-button>
|
|
|
+ </div> -->
|
|
|
+ </template>
|
|
|
+ </el-popover>
|
|
|
+
|
|
|
+ <el-button link class="ml-2" @click="clearCompare(), getData('clearAll')">清除</el-button>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center">
|
|
|
+ <el-radio-group v-model="formData.timeUnit" @change="getData">
|
|
|
+ <el-radio-button label="day" :disabled="isDayDisabled">天</el-radio-button>
|
|
|
+ <el-radio-button label="week" :disabled="isWeekDisabled">周</el-radio-button>
|
|
|
+ <el-radio-button label="month" :disabled="isMonthDisabled">月</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="relative">
|
|
|
+ <div ref="lineChartRef" style="width: 100%; height: 320px"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 明细表格 -->
|
|
|
+ <div class="mt-3">
|
|
|
+ <div class="flex items-center justify-between mb-2">
|
|
|
+ <div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]"
|
|
|
+ @click="showDetail = !showDetail">
|
|
|
+ {{ showDetail ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2">
|
|
|
+ <ArrowDown v-if="showDetail" />
|
|
|
+ <ArrowUp v-else />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <el-button type="primary" text>导出</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-table v-if="showDetail" :data="tableData" border>
|
|
|
+ <el-table-column prop="date" label="日期" min-width="140" />
|
|
|
+ <el-table-column prop="date" label="活跃用户数" min-width="140" />
|
|
|
+ <el-table-column prop="date" label="活跃构成(新用户占比)" min-width="140" />
|
|
|
+ <el-table-column prop="date" label="DAU/过去7日活跃用户" min-width="140" />
|
|
|
+ <el-table-column prop="date" label="DAU/过去30日活跃用户" min-width="140" />
|
|
|
+ <el-table-column label="新增用户(占比)" align="right" min-width="220">
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="flex items-center justify-end w-full">
|
|
|
+ <span>{{ scope.row.newUser }}</span>
|
|
|
+ <span class="text-gray-500 text-xs ml-2">({{ (scope.row.newUserRate * 100).toFixed(2)
|
|
|
+ }}%)</span>
|
|
|
+ </div>
|
|
|
+ </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]" />
|
|
|
+ </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';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+import { getDaysBetweenDates } from '/@/utils/formatTime';
|
|
|
+import { getAppVersion, getAppChannel } from '/@/api/common/common';
|
|
|
+import { getTrend, getTrendDetail } from '/@/api/count/addUser';
|
|
|
+
|
|
|
+const { t } = useI18n();
|
|
|
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
|
|
|
+
|
|
|
+const industryCompare = ref('');
|
|
|
+const selectedCompareItems = ref<string[]>([]);
|
|
|
+const searchKeyword = ref('');
|
|
|
+const compareOptions = ref<string[]>([]);
|
|
|
+const filteredCompareOptions = ref<string[]>([]);
|
|
|
+const timeCompareRange = ref<string>('');
|
|
|
+const timeCompareVisible = ref(false);
|
|
|
+
|
|
|
+const masterData = ref(<any>{
|
|
|
+ //图表主数据
|
|
|
+ dates: [],
|
|
|
+ items: [],
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits(['query']);
|
|
|
+const props = defineProps({
|
|
|
+ formData: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+interface FormDataType {
|
|
|
+ timeUnit: string;
|
|
|
+ version: string[];
|
|
|
+ channel: string[];
|
|
|
+ fromDate: string;
|
|
|
+ toDate: string;
|
|
|
+}
|
|
|
+
|
|
|
+const formData = ref<FormDataType>({
|
|
|
+ timeUnit: 'day',
|
|
|
+ version: [],
|
|
|
+ channel: [],
|
|
|
+ fromDate: props.formData?.time[0],
|
|
|
+ toDate: props.formData?.time[1],
|
|
|
+});
|
|
|
+const colorSchemes = [
|
|
|
+ { color: '#409EFF' }, // 蓝色
|
|
|
+ { color: '#67C23A' }, // 绿色
|
|
|
+ { color: '#E6A23C' }, // 黄色
|
|
|
+ { color: '#F56C6C' }, // 红色
|
|
|
+];
|
|
|
+const tableData = ref([]); //表格数据
|
|
|
+const pagination = ref({
|
|
|
+ current: 1, //当前页数
|
|
|
+ total: 0, // 数据总数
|
|
|
+ size: 5, // 每页显示条数
|
|
|
+});
|
|
|
+const previousCompareItems = ref<string[]>([]);
|
|
|
+const chartTimes = ref<string[]>([]);
|
|
|
+
|
|
|
+const getData = async (type: string) => {
|
|
|
+ //上方图表
|
|
|
+ if (type === 'clearAll') {
|
|
|
+ lineChartData.value = { dates: [], items: [] };
|
|
|
+ industryCompare.value = '';
|
|
|
+ }
|
|
|
+ // Guard: 小时维度跨度>=7天时不发起查询,不做自动纠正
|
|
|
+ if (formData.value.timeUnit === 'hour') {
|
|
|
+ const span = getDaysBetweenDates(formData.value.fromDate, formData.value.toDate);
|
|
|
+ if (span >= 7) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const res = await getTrend({ ...formData.value });
|
|
|
+ const data = res?.data || [];
|
|
|
+
|
|
|
+ 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) => {
|
|
|
+ console.log(item);
|
|
|
+
|
|
|
+ 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 = props.formData?.time[0] + ' ~ ' + props.formData?.time[1];
|
|
|
+ } else {
|
|
|
+ chartTimes.value = [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ lineChartData.value.dates = masterData.value.dates;
|
|
|
+ initLineChart();
|
|
|
+};
|
|
|
+const getBottomDetail = async () => {
|
|
|
+ // 下方明细表
|
|
|
+ const resDetail = await getTrendDetail({ ...formData.value, ...pagination.value });
|
|
|
+ const dataDetail = resDetail?.data || [];
|
|
|
+ pagination.value.current = dataDetail.current; //当前页码
|
|
|
+ pagination.value.total = dataDetail.total; //总条数
|
|
|
+ pagination.value.size = dataDetail.size; //每页条数
|
|
|
+ tableData.value = dataDetail.records;
|
|
|
+};
|
|
|
+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>`;
|
|
|
+};
|
|
|
+
|
|
|
+// 根据日期范围禁用不合适的粒度
|
|
|
+const rangeDays = computed(() => {
|
|
|
+ const start = props.formData?.time?.[0];
|
|
|
+ const end = props.formData?.time?.[1];
|
|
|
+ if (!start || !end) return 0;
|
|
|
+ return getDaysBetweenDates(start, end);
|
|
|
+});
|
|
|
+
|
|
|
+const isWeekDisabled = computed(() => rangeDays.value < 7);
|
|
|
+const isMonthDisabled = computed(() => rangeDays.value < 28);
|
|
|
+const isDayDisabled = computed(() => false);
|
|
|
+
|
|
|
+function ensureValidTimeUnit() {
|
|
|
+ // 不进行自动纠正,保持用户选择
|
|
|
+}
|
|
|
+
|
|
|
+function disableAfterToday(date: Date) {
|
|
|
+ const today = new Date();
|
|
|
+ today.setHours(0, 0, 0, 0);
|
|
|
+ return date.getTime() > today.getTime();
|
|
|
+}
|
|
|
+
|
|
|
+// 弹窗相关函数
|
|
|
+function handleCompareChange(value: string) {
|
|
|
+ selectedCompareItems.value = [];
|
|
|
+ initCompareOptions();
|
|
|
+ timeCompareVisible.value = value === 'time';
|
|
|
+}
|
|
|
+function clearCompare() {
|
|
|
+ selectedCompareItems.value = [];
|
|
|
+ timeCompareRange.value = '';
|
|
|
+ formData.value = {
|
|
|
+ ...formData.value,
|
|
|
+ channel: props.formData?.channel ? [props.formData?.channel] : [],
|
|
|
+ version: props.formData?.version ? [props.formData?.version] : [],
|
|
|
+ fromDate: props.formData?.time[0],
|
|
|
+ toDate: props.formData?.time[1],
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+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);
|
|
|
+
|
|
|
+ // 输出结果用于调试或后续处理
|
|
|
+ console.log('新增的项:', addedItems);
|
|
|
+ console.log('减少的项:', removedItemIndices);
|
|
|
+ if (removedItemIndices.length > 0) {
|
|
|
+ lineChartData.value.items.splice(removedItemIndices[0] + 1, 1);
|
|
|
+ }
|
|
|
+ if (industryCompare.value === 'version') {
|
|
|
+ lineChartData.value.items[0].name = '全部版本';
|
|
|
+ } else if (industryCompare.value === 'channel') {
|
|
|
+ lineChartData.value.items[0].name = '全部渠道';
|
|
|
+ }
|
|
|
+ // 更新 previousCompareItems 为当前值,用于下次对比
|
|
|
+ previousCompareItems.value = items;
|
|
|
+
|
|
|
+ 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 {
|
|
|
+ initLineChart();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化对比选项
|
|
|
+async function initCompareOptions() {
|
|
|
+ if (industryCompare.value === 'version') {
|
|
|
+ const res = await getAppVersion();
|
|
|
+ const list: Array<string> = res?.data || [];
|
|
|
+ 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(getDaysBetweenDates(props.formData?.time[0], props.formData?.time[1]), 'day').format('YYYY-MM-DD');
|
|
|
+ formData.value.channel = props.formData?.channel ? [props.formData?.channel] : [];
|
|
|
+ formData.value.version = props.formData?.version ? [props.formData?.version] : [];
|
|
|
+
|
|
|
+ getData('');
|
|
|
+
|
|
|
+ timeCompareVisible.value = false;
|
|
|
+ timeCompareRange.value = '';
|
|
|
+}
|
|
|
+
|
|
|
+const lineChartRef = ref<HTMLDivElement | null>(null);
|
|
|
+let chartInstance: echarts.ECharts | null = null;
|
|
|
+
|
|
|
+const lineChartData = ref<any>({
|
|
|
+ //图表数据//显示数据
|
|
|
+
|
|
|
+ dates: [],
|
|
|
+ items: [],
|
|
|
+});
|
|
|
+
|
|
|
+function initLineChart(): void {
|
|
|
+ if (!lineChartRef.value) return;
|
|
|
+ if (chartInstance) chartInstance.dispose();
|
|
|
+ chartInstance = echarts.init(lineChartRef.value);
|
|
|
+
|
|
|
+ const option: echarts.EChartsOption = {
|
|
|
+ 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.dates,
|
|
|
+ axisLine: { lineStyle: { color: '#e5e7eb' } },
|
|
|
+ axisLabel: { color: '#6b7280' },
|
|
|
+ axisTick: { alignWithLabel: true },
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLine: { show: false },
|
|
|
+ splitLine: { lineStyle: { color: '#f3f4f6' } },
|
|
|
+ axisLabel: { color: '#6b7280' },
|
|
|
+ },
|
|
|
+ series: lineChartData.value.items,
|
|
|
+ };
|
|
|
+ chartInstance.setOption(option);
|
|
|
+}
|
|
|
+
|
|
|
+// 展开/收起明细
|
|
|
+const showDetail = ref(true);
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getData('');
|
|
|
+ getBottomDetail();
|
|
|
+ initCompareOptions();
|
|
|
+});
|
|
|
+
|
|
|
+watch(props.formData, () => {
|
|
|
+ formData.value = {
|
|
|
+ ...formData.value,
|
|
|
+ channel: props.formData?.channel ? [props.formData?.channel] : [],
|
|
|
+ version: props.formData?.version ? [props.formData?.version] : [],
|
|
|
+ fromDate: props.formData?.time[0],
|
|
|
+ toDate: props.formData?.time[1],
|
|
|
+ };
|
|
|
+ formData.value.timeUnit = 'day';
|
|
|
+ ensureValidTimeUnit();
|
|
|
+ getData('');
|
|
|
+});
|
|
|
+
|
|
|
+// 监听外部日期与当前粒度,统一在此处做7天规则的静默纠正
|
|
|
+watch(
|
|
|
+ () => [props.formData?.time?.[0], props.formData?.time?.[1], formData.value.timeUnit],
|
|
|
+ ([start, end, unit]) => {
|
|
|
+ if (!start || !end) return;
|
|
|
+ // 对齐内部查询参数
|
|
|
+ formData.value.fromDate = start as string;
|
|
|
+ formData.value.toDate = end as string;
|
|
|
+ const span = getDaysBetweenDates(start as string, end as string);
|
|
|
+ // 不自动纠正,仅在合法范围内时刷新
|
|
|
+ if (unit === 'hour' && span >= 7) {
|
|
|
+ getData('');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.highlight {
|
|
|
+ color: #2196f3;
|
|
|
+}
|
|
|
+</style>
|