|
|
@@ -1,506 +1,655 @@
|
|
|
<template>
|
|
|
- <div class="fission-analysis-container">
|
|
|
- <el-card class="analysis-card mt-4" shadow="never">
|
|
|
- <div class="date-selector-container">
|
|
|
- <!-- Top Navigation Section -->
|
|
|
- <div class="date-period-selector">
|
|
|
- <el-button
|
|
|
- :class="{ active: activePeriod === 'yesterday' }"
|
|
|
- @click="changePeriod('yesterday')"
|
|
|
- >昨日</el-button
|
|
|
- >
|
|
|
- <el-button
|
|
|
- :class="{ active: activePeriod === '7d' }"
|
|
|
- @click="changePeriod('7d')"
|
|
|
- >7日</el-button
|
|
|
- >
|
|
|
- <el-button
|
|
|
- :class="{ active: activePeriod === '15d' }"
|
|
|
- @click="changePeriod('15d')"
|
|
|
- >15日</el-button
|
|
|
- >
|
|
|
- <el-button
|
|
|
- :class="{ active: activePeriod === '30d' }"
|
|
|
- @click="changePeriod('30d')"
|
|
|
- >30日</el-button
|
|
|
- >
|
|
|
-
|
|
|
- <el-date-picker
|
|
|
- v-model="dateRange"
|
|
|
- type="date"
|
|
|
- placeholder="选择日期"
|
|
|
- format="YYYY-MM-DD"
|
|
|
- value-format="YYYY-MM-DD"
|
|
|
- :clearable="false"
|
|
|
- :disabled-date="(date) => false"
|
|
|
- @change="fetchData"
|
|
|
- />
|
|
|
- <div class="date-action-buttons">
|
|
|
- <el-button type="primary" @click="fetchData">提交</el-button>
|
|
|
- <el-button @click="resetFilters">重置</el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Metrics Cards -->
|
|
|
- <div class="metrics-cards">
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="never" class="metric-card">
|
|
|
- <div class="metric-title">今日助力人数</div>
|
|
|
- <div class="metric-date">{{ currentDate }}</div>
|
|
|
- <div class="metric-value-container">
|
|
|
- <div class="metric-label">合计</div>
|
|
|
- <div class="metric-value">{{ metrics.totalUsers }}人</div>
|
|
|
- </div>
|
|
|
- <div class="metric-comparison"
|
|
|
- >环比: {{ metrics.totalUsersChange }}↓</div
|
|
|
- >
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="never" class="metric-card">
|
|
|
- <div class="metric-title">今日人均助力次数</div>
|
|
|
- <div class="metric-date">{{ currentDate }}</div>
|
|
|
- <div class="metric-value-container">
|
|
|
- <div class="metric-label">合计</div>
|
|
|
- <div class="metric-value">{{ metrics.avgHelps }}人</div>
|
|
|
- </div>
|
|
|
- <div class="metric-comparison"
|
|
|
- >环比: {{ metrics.avgHelpsChange }}↓</div
|
|
|
- >
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="never" class="metric-card">
|
|
|
- <div class="metric-title">活动书单扫描/参与占比</div>
|
|
|
- <div class="metric-date">{{ currentDate }}</div>
|
|
|
- <div class="metric-value-container">
|
|
|
- <div class="metric-label">合计</div>
|
|
|
- <div class="metric-value">{{ metrics.scanRate }}</div>
|
|
|
- </div>
|
|
|
- <div class="metric-comparison"
|
|
|
- >环比: {{ metrics.scanRateChange }}↓</div
|
|
|
- >
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="never" class="metric-card">
|
|
|
- <div class="metric-title">今日助力新用户占比</div>
|
|
|
- <div class="metric-date">{{ currentDate }}</div>
|
|
|
- <div class="metric-value-container">
|
|
|
- <div class="metric-label">合计</div>
|
|
|
- <div class="metric-value">{{ metrics.newUserRate }}</div>
|
|
|
- </div>
|
|
|
- <div class="metric-comparison"
|
|
|
- >环比: {{ metrics.newUserRateChange }}↓</div
|
|
|
- >
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Numbers Cards -->
|
|
|
- <div class="number-metrics">
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="4">
|
|
|
- <el-card shadow="hover" class="number-card">
|
|
|
- <div class="number-value">{{ stats.participants }}</div>
|
|
|
- <div class="number-label">参与用户数</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <el-card shadow="hover" class="number-card">
|
|
|
- <div class="number-value">{{ stats.helps }}</div>
|
|
|
- <div class="number-label">助力次数</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <el-card shadow="hover" class="number-card">
|
|
|
- <div class="number-value">{{ stats.ordersCount }}</div>
|
|
|
- <div class="number-label">订单总数</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <el-card shadow="hover" class="number-card">
|
|
|
- <div class="number-value">{{ stats.totalOrderAmount }}</div>
|
|
|
- <div class="number-label">订单预估总金额</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <el-card shadow="hover" class="number-card">
|
|
|
- <div class="number-value">{{ stats.totalAddedPoints }}</div>
|
|
|
- <div class="number-label">加价积分总金额</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <el-card shadow="hover" class="number-card">
|
|
|
- <div class="number-value">{{ stats.totalSaleAmount }}</div>
|
|
|
- <div class="number-label">实际成价总金额</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Chart Section -->
|
|
|
- <div class="trend-chart-section">
|
|
|
- <div class="chart-title">30日分享人数趋势</div>
|
|
|
- <div class="chart-container" ref="chartRef"></div>
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
- </div>
|
|
|
+ <div class="fission-analysis-container">
|
|
|
+ <el-card class="analysis-card mt-4" shadow="never">
|
|
|
+ <div class="date-selector-container">
|
|
|
+ <!-- Top Navigation Section -->
|
|
|
+ <div class="date-period-selector">
|
|
|
+ <el-button
|
|
|
+ :class="{ active: activePeriod === 'yesterday' }"
|
|
|
+ @click="changePeriod('yesterday')"
|
|
|
+ >昨日</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ :class="{ active: activePeriod === '7d' }"
|
|
|
+ @click="changePeriod('7d')"
|
|
|
+ >7日</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ :class="{ active: activePeriod === '15d' }"
|
|
|
+ @click="changePeriod('15d')"
|
|
|
+ >15日</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ :class="{ active: activePeriod === '30d' }"
|
|
|
+ @click="changePeriod('30d')"
|
|
|
+ >30日</el-button
|
|
|
+ >
|
|
|
+
|
|
|
+ <el-date-picker
|
|
|
+ v-model="dateRange"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ format="YYYY-MM-DD"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ :clearable="false"
|
|
|
+ :disabled-date="(date) => false"
|
|
|
+ @change="handleDateRangeChange"
|
|
|
+ />
|
|
|
+ <div class="date-action-buttons">
|
|
|
+ <el-button type="primary" @click="fetchData"
|
|
|
+ >搜索</el-button
|
|
|
+ >
|
|
|
+ <el-button @click="resetFilters">重置</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Metrics Cards -->
|
|
|
+ <div class="metrics-cards">
|
|
|
+ <el-row :gutter="20" class="metrics-row">
|
|
|
+ <el-col v-for="(item, index) in metricsCardsData" :key="index" class="metric-col">
|
|
|
+ <el-card shadow="never" class="metric-card">
|
|
|
+ <div class="metric-title">{{ item.title }}</div>
|
|
|
+ <div class="metric-date">{{ displayDateRange }}</div>
|
|
|
+ <div class="metric-value-container">
|
|
|
+ <div class="metric-label">合计</div>
|
|
|
+ <div class="metric-value">{{ item.value }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="metric-comparison">
|
|
|
+ 环比 {{ item.change }}%
|
|
|
+ <template v-if="parseFloat(item.change) > 0"><span class="arrow-up">↑</span></template>
|
|
|
+ <template v-else-if="parseFloat(item.change) < 0"><span class="arrow-down">↓</span></template>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Numbers Cards -->
|
|
|
+ <div class="number-metrics">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="4" v-for="(item, index) in numberMetricsData" :key="index">
|
|
|
+ <el-card shadow="hover" class="number-card">
|
|
|
+ <div class="number-value">{{ item.value }}</div>
|
|
|
+ <div class="number-label">{{ item.label }}</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Chart Section -->
|
|
|
+ <div class="trend-chart-section">
|
|
|
+ <div class="chart-title">30日分享人数趋势</div>
|
|
|
+ <div class="chart-container" ref="chartRef"></div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
- import { ref, reactive, onMounted, computed, watch } from 'vue';
|
|
|
- import { ArrowDown } from '@element-plus/icons-vue';
|
|
|
- import * as echarts from 'echarts';
|
|
|
- import dayjs from 'dayjs';
|
|
|
-
|
|
|
- // State
|
|
|
- const activePeriod = ref('yesterday');
|
|
|
- const timeSliderValue = ref(12);
|
|
|
- const datePopoverVisible = ref(false);
|
|
|
- const dateDisplayOption = ref('1');
|
|
|
- const chartRef = ref(null);
|
|
|
- let chart = null;
|
|
|
- // Initialize dateRange with current date
|
|
|
- const dateRange = ref(dayjs().toDate());
|
|
|
-
|
|
|
- // Mock data
|
|
|
- const metrics = reactive({
|
|
|
- totalUsers: '266',
|
|
|
- totalUsersChange: '56%',
|
|
|
- avgHelps: '266',
|
|
|
- avgHelpsChange: '56%',
|
|
|
- scanRate: '55%',
|
|
|
- scanRateChange: '56%',
|
|
|
- newUserRate: '55%',
|
|
|
- newUserRateChange: '56%'
|
|
|
- });
|
|
|
-
|
|
|
- const stats = reactive({
|
|
|
- participants: '44351316',
|
|
|
- helps: '44351316',
|
|
|
- ordersCount: '44351316',
|
|
|
- totalOrderAmount: '44351316',
|
|
|
- totalAddedPoints: '48415555',
|
|
|
- totalSaleAmount: '48415555'
|
|
|
- });
|
|
|
-
|
|
|
- const currentDate = computed(() => {
|
|
|
- return dayjs().format('YYYY-MM-DD');
|
|
|
- });
|
|
|
-
|
|
|
- // Methods
|
|
|
- const formatTooltip = (val) => {
|
|
|
- return `${val}:00`;
|
|
|
- };
|
|
|
-
|
|
|
- const changePeriod = (period) => {
|
|
|
- activePeriod.value = period;
|
|
|
-
|
|
|
- // Update dateRange based on the selected period
|
|
|
- switch (period) {
|
|
|
- case 'yesterday':
|
|
|
- dateRange.value = dayjs().subtract(1, 'day').toDate();
|
|
|
- break;
|
|
|
- case '7d':
|
|
|
- dateRange.value = dayjs().subtract(7, 'day').toDate();
|
|
|
- break;
|
|
|
- case '15d':
|
|
|
- dateRange.value = dayjs().subtract(15, 'day').toDate();
|
|
|
- break;
|
|
|
- case '30d':
|
|
|
- dateRange.value = dayjs().subtract(30, 'day').toDate();
|
|
|
- break;
|
|
|
- default:
|
|
|
- dateRange.value = dayjs().toDate();
|
|
|
- }
|
|
|
-
|
|
|
- fetchData();
|
|
|
- };
|
|
|
-
|
|
|
- const fetchData = () => {
|
|
|
- // Here you would fetch real data from your API
|
|
|
- console.log('Fetching data for period:', activePeriod.value);
|
|
|
- console.log('Selected date:', dayjs(dateRange.value).format('YYYY-MM-DD'));
|
|
|
- console.log('Time:', timeSliderValue.value);
|
|
|
-
|
|
|
- // Mock API call and data update
|
|
|
- // In a real app, you would make an API request and update the state
|
|
|
- };
|
|
|
-
|
|
|
- const resetFilters = () => {
|
|
|
- activePeriod.value = 'yesterday';
|
|
|
- dateRange.value = dayjs().subtract(1, 'day').toDate(); // Reset dateRange to yesterday
|
|
|
- timeSliderValue.value = 12;
|
|
|
- fetchData();
|
|
|
- };
|
|
|
-
|
|
|
- const initChart = () => {
|
|
|
- if (chartRef.value) {
|
|
|
- chart = echarts.init(chartRef.value);
|
|
|
-
|
|
|
- // Sample data for the chart
|
|
|
- const option = {
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis',
|
|
|
- axisPointer: {
|
|
|
- type: 'shadow'
|
|
|
- }
|
|
|
- },
|
|
|
- grid: {
|
|
|
- left: '3%',
|
|
|
- right: '4%',
|
|
|
- bottom: '3%',
|
|
|
- containLabel: true
|
|
|
- },
|
|
|
- xAxis: {
|
|
|
- type: 'category',
|
|
|
- data: ['1月', '2月', '3月', '4月', '5月'],
|
|
|
- boundaryGap: false
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- type: 'value',
|
|
|
- min: 0,
|
|
|
- max: 250,
|
|
|
- interval: 50
|
|
|
- },
|
|
|
- series: [
|
|
|
- {
|
|
|
- name: '分享人数',
|
|
|
- type: 'line',
|
|
|
- data: [100, 140, 220, 120, 130],
|
|
|
- smooth: true,
|
|
|
- lineStyle: {
|
|
|
- color: '#5470c6',
|
|
|
- width: 2
|
|
|
+ import { ref, onMounted, computed, watch } from 'vue';
|
|
|
+ import * as echarts from 'echarts';
|
|
|
+ import dayjs from 'dayjs';
|
|
|
+ import request from '@/utils/request';
|
|
|
+ import { ElMessage } from 'element-plus';
|
|
|
+
|
|
|
+ // State
|
|
|
+ const activePeriod = ref('7d');
|
|
|
+ const chartRef = ref(null);
|
|
|
+ let chart = null;
|
|
|
+ // Track if custom date range is used
|
|
|
+ const isCustomDateRange = ref(false);
|
|
|
+ // Initialize dateRange with 7 days range
|
|
|
+ const dateRange = ref([
|
|
|
+ dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().format('YYYY-MM-DD')
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // API data
|
|
|
+ const statisticData = ref(null);
|
|
|
+
|
|
|
+ // Metrics computed from API response
|
|
|
+ const metrics = computed(() => {
|
|
|
+ if (!statisticData.value) {
|
|
|
+ return {
|
|
|
+ totalUsers: '',
|
|
|
+ totalUsersChange: '0',
|
|
|
+ orders: '',
|
|
|
+ ordersChange: '0',
|
|
|
+ scanNum: '',
|
|
|
+ scanNumChange: '0',
|
|
|
+ scanRate: '',
|
|
|
+ scanRateChange: '0',
|
|
|
+ newUserRate: '',
|
|
|
+ newUserRateChange: '0'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = statisticData.value.data;
|
|
|
+
|
|
|
+ // Helper function to format comparison values consistently
|
|
|
+ const formatComparison = (value) => {
|
|
|
+ if (value === null || value === undefined) return '0';
|
|
|
+ // Remove % if present and return just the number as string
|
|
|
+ return String(value).replace('%', '');
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ totalUsers: data.upsellHelpNum || '0',
|
|
|
+ totalUsersChange: formatComparison(data.upsellHelpNumComparison),
|
|
|
+ orders: data.upsellOrderNum || '0',
|
|
|
+ ordersChange: formatComparison(data.upsellOrderNumComparison),
|
|
|
+ scanNum: data.upsellScanNum || '0',
|
|
|
+ scanNumChange: formatComparison(data.upsellScanNumComparison),
|
|
|
+ scanRate: data.scanJoinRate ? `${data.scanJoinRate}%` : '0%',
|
|
|
+ scanRateChange: formatComparison(data.scanJoinRateComparison),
|
|
|
+ newUserRate: data.newUserRate ? `${data.newUserRate}%` : '0%',
|
|
|
+ newUserRateChange: formatComparison(data.newUserRateComparison)
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const stats = computed(() => {
|
|
|
+ if (!statisticData.value) {
|
|
|
+ return {
|
|
|
+ participants: '0',
|
|
|
+ helps: '0',
|
|
|
+ ordersCount: '0',
|
|
|
+ totalOrderAmount: '0',
|
|
|
+ totalAddedPoints: '0',
|
|
|
+ totalSaleAmount: '0'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = statisticData.value.data;
|
|
|
+
|
|
|
+ return {
|
|
|
+ participants: data.upsellShareNum || '0',
|
|
|
+ helps: data.upsellHelpTimes || '0',
|
|
|
+ ordersCount: data.orderBookNum || '0',
|
|
|
+ totalOrderAmount: data.orderTotalMoney || '0',
|
|
|
+ totalAddedPoints: data.upsellTotalMoney || '0',
|
|
|
+ totalSaleAmount: data.upsellFinalTotalMoney || '0'
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 数字指标卡片数据
|
|
|
+ const numberMetricsData = computed(() => {
|
|
|
+ return [
|
|
|
+ { label: '参与用户数', value: stats.value.participants },
|
|
|
+ { label: '助力次数', value: stats.value.helps },
|
|
|
+ { label: '订单总数', value: stats.value.ordersCount },
|
|
|
+ { label: '订单预估总金额', value: stats.value.totalOrderAmount },
|
|
|
+ { label: '加价预估总金额', value: stats.value.totalAddedPoints },
|
|
|
+ { label: '实际加价金额', value: stats.value.totalSaleAmount }
|
|
|
+ ];
|
|
|
+ });
|
|
|
+
|
|
|
+ // 度量指标卡片数据
|
|
|
+ const metricsCardsData = computed(() => {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ title: '助力人数',
|
|
|
+ value: `${metrics.value.totalUsers}人`,
|
|
|
+ change: metrics.value.totalUsersChange
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '参与活动订单数',
|
|
|
+ value: `${metrics.value.orders}单`,
|
|
|
+ change: metrics.value.ordersChange
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '活动书单扫描次数',
|
|
|
+ value: `${metrics.value.scanNum}次`,
|
|
|
+ change: metrics.value.scanNumChange
|
|
|
},
|
|
|
- symbol: 'circle',
|
|
|
- symbolSize: 8,
|
|
|
- itemStyle: {
|
|
|
- color: '#5470c6'
|
|
|
+ {
|
|
|
+ title: '活动书单扫描/参与占比',
|
|
|
+ value: metrics.value.scanRate,
|
|
|
+ change: metrics.value.scanRateChange
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '助力新用户占比',
|
|
|
+ value: metrics.value.newUserRate,
|
|
|
+ change: metrics.value.newUserRateChange
|
|
|
}
|
|
|
- }
|
|
|
- ]
|
|
|
- };
|
|
|
-
|
|
|
- chart.setOption(option);
|
|
|
-
|
|
|
- // Handle resize
|
|
|
- window.addEventListener('resize', () => {
|
|
|
- chart.resize();
|
|
|
- });
|
|
|
-
|
|
|
- // Use ResizeObserver to detect and respond to size changes of the chart container
|
|
|
- // This will ensure the chart resizes properly when tabs change
|
|
|
- const resizeObserver = new ResizeObserver(() => {
|
|
|
- chart.resize();
|
|
|
- });
|
|
|
+ ];
|
|
|
+ });
|
|
|
+
|
|
|
+ // Chart data
|
|
|
+ const chartData = computed(() => {
|
|
|
+ if (!statisticData.value || !statisticData.value.data.chartList) {
|
|
|
+ return {
|
|
|
+ dates: [],
|
|
|
+ values: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const chartList = statisticData.value.data.chartList;
|
|
|
+ return {
|
|
|
+ dates: chartList.map((item) => item.date),
|
|
|
+ values: chartList.map((item) => item.shareNum)
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const currentDate = computed(() => {
|
|
|
+ return dayjs().format('YYYY-MM-DD');
|
|
|
+ });
|
|
|
+
|
|
|
+ const displayDateRange = computed(() => {
|
|
|
+ if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
|
|
|
+ return `${dateRange.value[0]} - ${dateRange.value[1]}`;
|
|
|
+ } else {
|
|
|
+ return currentDate.value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Method to update date range based on period
|
|
|
+ const updateDateRangeByPeriod = (period) => {
|
|
|
+ isCustomDateRange.value = false;
|
|
|
+
|
|
|
+ switch (period) {
|
|
|
+ case 'yesterday':
|
|
|
+ dateRange.value = [
|
|
|
+ dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().subtract(1, 'day').format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
+ break;
|
|
|
+ case '7d':
|
|
|
+ dateRange.value = [
|
|
|
+ dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
+ break;
|
|
|
+ case '15d':
|
|
|
+ dateRange.value = [
|
|
|
+ dayjs().subtract(15, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
+ break;
|
|
|
+ case '30d':
|
|
|
+ dateRange.value = [
|
|
|
+ dayjs().subtract(30, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ dateRange.value = [
|
|
|
+ dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Method to handle period change button clicks
|
|
|
+ const changePeriod = (period) => {
|
|
|
+ activePeriod.value = period;
|
|
|
+ updateDateRangeByPeriod(period);
|
|
|
+ fetchData();
|
|
|
+ };
|
|
|
+
|
|
|
+ // Handle date picker change
|
|
|
+ const handleDateRangeChange = () => {
|
|
|
+ isCustomDateRange.value = true;
|
|
|
+ activePeriod.value = '';
|
|
|
+ fetchData();
|
|
|
+ };
|
|
|
+
|
|
|
+ const getDateRangeForAPI = () => {
|
|
|
+ let startDate, endDate;
|
|
|
+
|
|
|
+ if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
|
|
|
+ startDate = dayjs(dateRange.value[0]).format('YYYY-MM-DD 00:00:00');
|
|
|
+ endDate = dayjs(dateRange.value[1]).format('YYYY-MM-DD 23:59:59');
|
|
|
+ } else {
|
|
|
+ // Fallback if dateRange is not an array
|
|
|
+ startDate = dayjs().subtract(7, 'day').format('YYYY-MM-DD 00:00:00');
|
|
|
+ endDate = dayjs().format('YYYY-MM-DD 23:59:59');
|
|
|
+ }
|
|
|
+
|
|
|
+ return { startDate, endDate };
|
|
|
+ };
|
|
|
+
|
|
|
+ const fetchData = async () => {
|
|
|
+ const { startDate, endDate } = getDateRangeForAPI();
|
|
|
+
|
|
|
+ try {
|
|
|
+ const { data } = await request.get(
|
|
|
+ '/activity/activityUpsellInfo/statistic',
|
|
|
+ {
|
|
|
+ params: {
|
|
|
+ startDate,
|
|
|
+ endDate
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ statisticData.value = data;
|
|
|
+ updateChart();
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('获取数据失败');
|
|
|
+ console.error('Failed to fetch statistic data:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const resetFilters = () => {
|
|
|
+ isCustomDateRange.value = false;
|
|
|
+ activePeriod.value = '7d';
|
|
|
+ updateDateRangeByPeriod('7d');
|
|
|
+ fetchData();
|
|
|
+ };
|
|
|
+
|
|
|
+ const updateChart = () => {
|
|
|
+ if (!chart || !chartData.value.dates.length) return;
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: chartData.value.dates,
|
|
|
+ boundaryGap: false
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ min: 0
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '分享人数',
|
|
|
+ type: 'line',
|
|
|
+ data: chartData.value.values,
|
|
|
+ smooth: true,
|
|
|
+ lineStyle: {
|
|
|
+ color: '#5470c6',
|
|
|
+ width: 2
|
|
|
+ },
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 8,
|
|
|
+ itemStyle: {
|
|
|
+ color: '#5470c6'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ chart.setOption(option);
|
|
|
+ };
|
|
|
+
|
|
|
+ const initChart = () => {
|
|
|
+ if (chartRef.value) {
|
|
|
+ chart = echarts.init(chartRef.value);
|
|
|
+
|
|
|
+ // Initialize with empty data, will be updated after API call
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: [],
|
|
|
+ boundaryGap: false
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ min: 0
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '分享人数',
|
|
|
+ type: 'line',
|
|
|
+ data: [],
|
|
|
+ smooth: true,
|
|
|
+ lineStyle: {
|
|
|
+ color: '#5470c6',
|
|
|
+ width: 2
|
|
|
+ },
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 8,
|
|
|
+ itemStyle: {
|
|
|
+ color: '#5470c6'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ chart.setOption(option);
|
|
|
+
|
|
|
+ // Handle resize
|
|
|
+ window.addEventListener('resize', () => {
|
|
|
+ chart.resize();
|
|
|
+ });
|
|
|
+
|
|
|
+ // Use ResizeObserver to detect and respond to size changes of the chart container
|
|
|
+ const resizeObserver = new ResizeObserver(() => {
|
|
|
+ chart.resize();
|
|
|
+ });
|
|
|
+
|
|
|
+ resizeObserver.observe(chartRef.value);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Method to manually resize chart - can be called from parent components
|
|
|
+ const resizeChart = () => {
|
|
|
+ if (chart) {
|
|
|
+ chart.resize();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Watch for changes in dateRange to update activePeriod
|
|
|
+ watch(dateRange, (newValue) => {
|
|
|
+ if (isCustomDateRange.value) {
|
|
|
+ activePeriod.value = '';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ initChart();
|
|
|
+ activePeriod.value = '7d';
|
|
|
+ updateDateRangeByPeriod('7d');
|
|
|
+ fetchData();
|
|
|
+
|
|
|
+ // Force a resize after component is mounted
|
|
|
+ setTimeout(() => {
|
|
|
+ resizeChart();
|
|
|
+ }, 300);
|
|
|
+ });
|
|
|
+
|
|
|
+ // Expose the resizeChart method to parent components
|
|
|
+ defineExpose({
|
|
|
+ resizeChart
|
|
|
+ });
|
|
|
+</script>
|
|
|
|
|
|
- resizeObserver.observe(chartRef.value);
|
|
|
+<style lang="scss" scoped>
|
|
|
+ .date-selector-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ padding-bottom: 16px;
|
|
|
}
|
|
|
- };
|
|
|
|
|
|
- // Method to manually resize chart - can be called from parent components
|
|
|
- const resizeChart = () => {
|
|
|
- if (chart) {
|
|
|
- chart.resize();
|
|
|
+ .arrow-up {
|
|
|
+ color: #67C23A; /* Element Plus success color (green) */
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
- // Watch for changes in dateRange to update activePeriod
|
|
|
- watch(dateRange, (newValue) => {
|
|
|
- const selectedDate = dayjs(newValue);
|
|
|
- const today = dayjs();
|
|
|
- const yesterday = dayjs().subtract(1, 'day');
|
|
|
- const days7ago = dayjs().subtract(7, 'day');
|
|
|
- const days15ago = dayjs().subtract(15, 'day');
|
|
|
- const days30ago = dayjs().subtract(30, 'day');
|
|
|
-
|
|
|
- // Check if selected date matches any of the predefined periods
|
|
|
- if (selectedDate.isSame(yesterday, 'day')) {
|
|
|
- activePeriod.value = 'yesterday';
|
|
|
- } else if (selectedDate.isSame(days7ago, 'day')) {
|
|
|
- activePeriod.value = '7d';
|
|
|
- } else if (selectedDate.isSame(days15ago, 'day')) {
|
|
|
- activePeriod.value = '15d';
|
|
|
- } else if (selectedDate.isSame(days30ago, 'day')) {
|
|
|
- activePeriod.value = '30d';
|
|
|
- } else {
|
|
|
- // If it doesn't match any predefined period, clear the active selection
|
|
|
- activePeriod.value = '';
|
|
|
+
|
|
|
+ .arrow-down {
|
|
|
+ color: #F56C6C; /* Element Plus danger color (red) */
|
|
|
}
|
|
|
- });
|
|
|
|
|
|
- onMounted(() => {
|
|
|
- initChart();
|
|
|
+ .date-period-selector {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
|
|
|
- // Initialize dateRange based on the default activePeriod (yesterday)
|
|
|
- if (activePeriod.value === 'yesterday') {
|
|
|
- dateRange.value = dayjs().subtract(1, 'day').toDate();
|
|
|
- }
|
|
|
+ .el-button {
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
|
|
|
- // Force a resize after component is mounted
|
|
|
- // This ensures the chart properly fills its container
|
|
|
- setTimeout(() => {
|
|
|
- resizeChart();
|
|
|
- }, 300); // Small delay to ensure rendering is complete
|
|
|
- });
|
|
|
-
|
|
|
- // Expose the resizeChart method to parent components
|
|
|
- defineExpose({
|
|
|
- resizeChart
|
|
|
- });
|
|
|
-</script>
|
|
|
+ .active {
|
|
|
+ color: #409eff;
|
|
|
+ border-color: #409eff;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
-<style lang="scss" scoped>
|
|
|
- .date-selector-container {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- margin-bottom: 20px;
|
|
|
- border-bottom: 1px solid #ebeef5;
|
|
|
- padding-bottom: 16px;
|
|
|
- }
|
|
|
-
|
|
|
- .date-period-selector {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
-
|
|
|
- .el-button {
|
|
|
- border-radius: 4px;
|
|
|
+ .custom-date-range {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
}
|
|
|
|
|
|
- .active {
|
|
|
- color: #409eff;
|
|
|
- border-color: #409eff;
|
|
|
+ .time-range-selector {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .time-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- .custom-date-range {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 16px;
|
|
|
- }
|
|
|
-
|
|
|
- .time-range-selector {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
-
|
|
|
- .time-label {
|
|
|
- font-size: 14px;
|
|
|
- color: #606266;
|
|
|
+
|
|
|
+ .time-slider {
|
|
|
+ width: 200px;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- .time-slider {
|
|
|
- width: 200px;
|
|
|
- }
|
|
|
-
|
|
|
- .date-action-buttons {
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
- }
|
|
|
-
|
|
|
- .date-display {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
- cursor: pointer;
|
|
|
- padding: 6px 12px;
|
|
|
- border: 1px solid #dcdfe6;
|
|
|
- border-radius: 4px;
|
|
|
- margin-bottom: 20px;
|
|
|
- }
|
|
|
-
|
|
|
- .metrics-cards {
|
|
|
- margin-bottom: 24px;
|
|
|
- }
|
|
|
-
|
|
|
- .metric-card {
|
|
|
- border: 1px solid #ebeef5;
|
|
|
- height: 150px;
|
|
|
-
|
|
|
- .metric-title {
|
|
|
- font-size: 16px;
|
|
|
- color: #303133;
|
|
|
- margin-bottom: 4px;
|
|
|
+
|
|
|
+ .date-action-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
- .metric-date {
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- margin-bottom: 8px;
|
|
|
+ .date-display {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
- .metric-value-container {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- margin-bottom: 4px;
|
|
|
-
|
|
|
- .metric-label {
|
|
|
- font-size: 14px;
|
|
|
- color: #606266;
|
|
|
- }
|
|
|
-
|
|
|
- .metric-value {
|
|
|
- font-size: 26px;
|
|
|
- font-weight: bold;
|
|
|
- color: #303133;
|
|
|
- }
|
|
|
+ .metrics-cards {
|
|
|
+ margin-bottom: 24px;
|
|
|
}
|
|
|
|
|
|
- .metric-comparison {
|
|
|
- font-size: 14px;
|
|
|
- color: #606266;
|
|
|
+ .metrics-row {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: nowrap;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- .number-metrics {
|
|
|
- margin-bottom: 24px;
|
|
|
- }
|
|
|
-
|
|
|
- .number-card {
|
|
|
- text-align: center;
|
|
|
- padding: 16px;
|
|
|
- height: 120px;
|
|
|
-
|
|
|
- .number-value {
|
|
|
- font-size: 22px;
|
|
|
- font-weight: bold;
|
|
|
- color: #f6a623;
|
|
|
- margin-bottom: 8px;
|
|
|
+
|
|
|
+ .metric-col {
|
|
|
+ flex: 1;
|
|
|
}
|
|
|
|
|
|
- .number-label {
|
|
|
- font-size: 14px;
|
|
|
- color: #606266;
|
|
|
+ .metric-card {
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ height: 150px;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &:first-child {
|
|
|
+ border-left: 3px solid #409eff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-title {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-date {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value-container {
|
|
|
+ display: flex;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .metric-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value {
|
|
|
+ font-size: 32px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-comparison {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ position: absolute;
|
|
|
+ bottom: 12px;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- .trend-chart-section {
|
|
|
- margin-top: 24px;
|
|
|
- width: 100%;
|
|
|
+ .number-metrics {
|
|
|
+ margin-bottom: 24px;
|
|
|
+ }
|
|
|
|
|
|
- .chart-title {
|
|
|
- font-size: 16px;
|
|
|
- color: #303133;
|
|
|
- margin-bottom: 16px;
|
|
|
+ .number-card {
|
|
|
+ text-align: center;
|
|
|
+ padding: 16px;
|
|
|
+ height: 120px;
|
|
|
+
|
|
|
+ .number-value {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #f6a623;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .number-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- .chart-container {
|
|
|
- height: 350px;
|
|
|
- width: 100%;
|
|
|
- position: relative;
|
|
|
- overflow: hidden;
|
|
|
+ .trend-chart-section {
|
|
|
+ margin-top: 24px;
|
|
|
+ width: 100%;
|
|
|
+
|
|
|
+ .chart-title {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ height: 350px;
|
|
|
+ width: 100%;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- .date-popover-content {
|
|
|
- padding: 12px;
|
|
|
- }
|
|
|
+ .date-popover-content {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
</style>
|