|
|
@@ -1,6 +1,6 @@
|
|
|
<template>
|
|
|
<div class="fission-analysis-container p-4">
|
|
|
- <el-card class="analysis-card" shadow="never">
|
|
|
+ <el-card class="analysis-card" shadow="never" v-loading="loading">
|
|
|
<div class="date-selector-container mb-6">
|
|
|
<!-- Top Navigation Section -->
|
|
|
<div class="date-period-selector flex items-center gap-4">
|
|
|
@@ -39,6 +39,7 @@
|
|
|
value-format="YYYY-MM-DD"
|
|
|
:clearable="false"
|
|
|
@change="handleDateRangeChange"
|
|
|
+ style="max-width: 400px"
|
|
|
/>
|
|
|
<div class="date-action-buttons">
|
|
|
<el-button type="primary" @click="fetchData"
|
|
|
@@ -120,7 +121,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
- import { ref, onMounted, computed, watch, nextTick } from 'vue';
|
|
|
+ import { ref, onMounted, computed, nextTick, onBeforeUnmount } from 'vue';
|
|
|
import * as echarts from 'echarts';
|
|
|
import dayjs from 'dayjs';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
@@ -132,130 +133,161 @@
|
|
|
const activePeriod = ref('30d');
|
|
|
const chartRef = ref(null);
|
|
|
let chart = null;
|
|
|
- const isCustomDateRange = ref(false);
|
|
|
const dateRange = ref([
|
|
|
dayjs().subtract(29, 'day').format('YYYY-MM-DD'),
|
|
|
dayjs().format('YYYY-MM-DD')
|
|
|
]);
|
|
|
const loading = ref(false);
|
|
|
+ const statisticData = ref(null);
|
|
|
|
|
|
- // Data
|
|
|
- const statisticData = ref({
|
|
|
- data: {
|
|
|
- upsellHelpNum: 0,
|
|
|
- upsellHelpNumComparison: '0',
|
|
|
- upsellOrderNum: 0,
|
|
|
- upsellOrderNumComparison: '0',
|
|
|
- upsellScanNum: 0,
|
|
|
- upsellScanNumComparison: '0',
|
|
|
- scanJoinRate: 0,
|
|
|
- scanJoinRateComparison: '0',
|
|
|
- newUserRate: 0,
|
|
|
- newUserRateComparison: '0',
|
|
|
- conversionRate: 0,
|
|
|
- conversionRateComparison: '0',
|
|
|
+ const formatComparison = (value) => {
|
|
|
+ if (value === null || value === undefined) return '0';
|
|
|
+ return String(value).replace('%', '');
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatPercent = (value) => {
|
|
|
+ if (value === null || value === undefined || value === '') return '0%';
|
|
|
+ const str = String(value);
|
|
|
+ return str.includes('%') ? str : `${str}%`;
|
|
|
+ };
|
|
|
|
|
|
- upsellShareNum: 0,
|
|
|
- upsellHelpTimes: 0,
|
|
|
- orderBookNum: 0,
|
|
|
- orderTotalMoney: 0,
|
|
|
- upsellTotalMoney: 0,
|
|
|
- upsellFinalTotalMoney: 0
|
|
|
+ const formatMoney = (value) => {
|
|
|
+ if (value === null || value === undefined || value === '') {
|
|
|
+ return '¥0';
|
|
|
}
|
|
|
- });
|
|
|
+ const num = Number(value);
|
|
|
+ if (Number.isNaN(num)) return '¥0';
|
|
|
+ return `¥${num.toLocaleString('zh-CN', {
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 2
|
|
|
+ })}`;
|
|
|
+ };
|
|
|
|
|
|
- const trendData = ref({
|
|
|
- dates: [],
|
|
|
- values: []
|
|
|
- });
|
|
|
+ const resultData = computed(() => statisticData.value?.data || {});
|
|
|
|
|
|
const displayDateRange = computed(() => {
|
|
|
- if (!dateRange.value) return '';
|
|
|
+ if (!dateRange.value?.length) return '';
|
|
|
+ if (dateRange.value[0] === dateRange.value[1]) {
|
|
|
+ return dateRange.value[0];
|
|
|
+ }
|
|
|
return `${dateRange.value[0]} ~ ${dateRange.value[1]}`;
|
|
|
});
|
|
|
|
|
|
const metricsCardsData = computed(() => {
|
|
|
- const data = statisticData.value.data;
|
|
|
+ const data = resultData.value;
|
|
|
return [
|
|
|
{
|
|
|
title: '分享人数',
|
|
|
- value: data.upsellHelpNum,
|
|
|
- change: data.upsellHelpNumComparison
|
|
|
+ value: data.reduceHelpNum ?? 0,
|
|
|
+ change: formatComparison(data.reduceHelpNumComparison)
|
|
|
},
|
|
|
{
|
|
|
title: '成单数',
|
|
|
- value: data.upsellOrderNum,
|
|
|
- change: data.upsellOrderNumComparison
|
|
|
+ value: data.reduceOrderNum ?? 0,
|
|
|
+ change: formatComparison(data.reduceOrderNumComparison)
|
|
|
},
|
|
|
{
|
|
|
title: '扫码人数',
|
|
|
- value: data.upsellScanNum,
|
|
|
- change: data.upsellScanNumComparison
|
|
|
+ value: data.reduceCartNum ?? 0,
|
|
|
+ change: formatComparison(data.reduceCartNumComparison)
|
|
|
},
|
|
|
{
|
|
|
title: '扫码参与率',
|
|
|
- value: data.scanJoinRate + '%',
|
|
|
- change: data.scanJoinRateComparison
|
|
|
+ value: formatPercent(data.cartJoinRate),
|
|
|
+ change: formatComparison(data.cartJoinRateComparison)
|
|
|
},
|
|
|
{
|
|
|
title: '新用户占比',
|
|
|
- value: data.newUserRate + '%',
|
|
|
- change: data.newUserRateComparison
|
|
|
+ value: formatPercent(data.newUserRate),
|
|
|
+ change: formatComparison(data.newUserRateComparison)
|
|
|
},
|
|
|
{
|
|
|
title: '转化率',
|
|
|
- value: data.conversionRate + '%',
|
|
|
- change: data.conversionRateComparison
|
|
|
+ value: formatPercent(data.conversionRate),
|
|
|
+ change: formatComparison(data.conversionRateComparison)
|
|
|
}
|
|
|
];
|
|
|
});
|
|
|
|
|
|
const numberMetricsData = computed(() => {
|
|
|
- const data = statisticData.value.data;
|
|
|
+ const data = resultData.value;
|
|
|
return [
|
|
|
- { label: '分享发起人数', value: data.upsellShareNum },
|
|
|
- { label: '助力次数', value: data.upsellHelpTimes },
|
|
|
- { label: '成单本数', value: data.orderBookNum },
|
|
|
- { label: '订单总金额', value: '¥' + data.orderTotalMoney },
|
|
|
- { label: '累计优惠金额', value: '¥' + data.upsellTotalMoney },
|
|
|
- { label: '累计实付金额', value: '¥' + data.upsellFinalTotalMoney }
|
|
|
+ { label: '分享发起人数', value: data.reduceShareNum ?? 0 },
|
|
|
+ { label: '助力次数', value: data.reduceHelpTimes ?? 0 },
|
|
|
+ { label: '成单本数', value: data.orderBookNum ?? 0 },
|
|
|
+ { label: '订单总金额', value: formatMoney(0) },
|
|
|
+ {
|
|
|
+ label: '累计优惠金额',
|
|
|
+ value: formatMoney(data.reduceTotalMoney)
|
|
|
+ },
|
|
|
+ { label: '累计实付金额', value: formatMoney(0) }
|
|
|
];
|
|
|
});
|
|
|
|
|
|
+ const chartData = computed(() => {
|
|
|
+ const chartList = resultData.value.chartList;
|
|
|
+ if (!Array.isArray(chartList) || !chartList.length) {
|
|
|
+ return { dates: [], values: [] };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ dates: chartList.map((item) => item.date),
|
|
|
+ values: chartList.map((item) => item.shareNum ?? 0)
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const getDateRangeForAPI = () => {
|
|
|
+ const [start, end] = dateRange.value || [];
|
|
|
+ return {
|
|
|
+ startDate: dayjs(start || undefined)
|
|
|
+ .startOf('day')
|
|
|
+ .format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ endDate: dayjs(end || undefined)
|
|
|
+ .endOf('day')
|
|
|
+ .format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
const changePeriod = (period) => {
|
|
|
activePeriod.value = period;
|
|
|
- isCustomDateRange.value = false;
|
|
|
-
|
|
|
const end = dayjs();
|
|
|
let start;
|
|
|
|
|
|
switch (period) {
|
|
|
case 'yesterday':
|
|
|
start = end.subtract(1, 'day');
|
|
|
+ dateRange.value = [
|
|
|
+ start.format('YYYY-MM-DD'),
|
|
|
+ start.format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
break;
|
|
|
case '7d':
|
|
|
start = end.subtract(6, 'day');
|
|
|
+ dateRange.value = [
|
|
|
+ start.format('YYYY-MM-DD'),
|
|
|
+ end.format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
break;
|
|
|
case '15d':
|
|
|
start = end.subtract(14, 'day');
|
|
|
+ dateRange.value = [
|
|
|
+ start.format('YYYY-MM-DD'),
|
|
|
+ end.format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
break;
|
|
|
case '30d':
|
|
|
+ default:
|
|
|
start = end.subtract(29, 'day');
|
|
|
+ dateRange.value = [
|
|
|
+ start.format('YYYY-MM-DD'),
|
|
|
+ end.format('YYYY-MM-DD')
|
|
|
+ ];
|
|
|
break;
|
|
|
- default:
|
|
|
- start = end;
|
|
|
}
|
|
|
-
|
|
|
- dateRange.value = [
|
|
|
- start.format('YYYY-MM-DD'),
|
|
|
- end.format('YYYY-MM-DD')
|
|
|
- ];
|
|
|
fetchData();
|
|
|
};
|
|
|
|
|
|
const handleDateRangeChange = () => {
|
|
|
activePeriod.value = '';
|
|
|
- isCustomDateRange.value = true;
|
|
|
};
|
|
|
|
|
|
const resetFilters = () => {
|
|
|
@@ -263,84 +295,43 @@
|
|
|
};
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
+ const { startDate, endDate } = getDateRangeForAPI();
|
|
|
loading.value = true;
|
|
|
try {
|
|
|
- const res = await request.get(
|
|
|
- '/marketing/shareDiscount/fission/analysis',
|
|
|
+ const { data } = await request.get(
|
|
|
+ '/activity/activityReduceInfo/statistic',
|
|
|
{
|
|
|
params: {
|
|
|
- startDate: dateRange.value[0],
|
|
|
- endDate: dateRange.value[1]
|
|
|
+ startDate, endDate
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
|
|
|
- if (res.data) {
|
|
|
- statisticData.value.data =
|
|
|
- res.data.summary || statisticData.value.data;
|
|
|
- trendData.value = res.data.trend || { dates: [], values: [] };
|
|
|
+ if (data?.code !== 200) {
|
|
|
+ ElMessage.error(data?.msg || '获取数据失败');
|
|
|
+ return;
|
|
|
}
|
|
|
- ElMessage.success('数据已更新');
|
|
|
- initChart();
|
|
|
+
|
|
|
+ statisticData.value = data;
|
|
|
+ await nextTick();
|
|
|
+ updateChart();
|
|
|
} catch (error) {
|
|
|
- // Mock data fallback
|
|
|
- mockData();
|
|
|
- ElMessage.success('数据已更新 (Mock)');
|
|
|
- initChart();
|
|
|
+ console.error('Failed to fetch reduce statistic data:', error);
|
|
|
+ ElMessage.error('获取数据失败');
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const mockData = () => {
|
|
|
- statisticData.value.data = {
|
|
|
- upsellHelpNum: 1234,
|
|
|
- upsellHelpNumComparison: '12.5',
|
|
|
- upsellOrderNum: 567,
|
|
|
- upsellOrderNumComparison: '-5.2',
|
|
|
- upsellScanNum: 8901,
|
|
|
- upsellScanNumComparison: '8.9',
|
|
|
- scanJoinRate: 15.5,
|
|
|
- scanJoinRateComparison: '2.1',
|
|
|
- newUserRate: 45.2,
|
|
|
- newUserRateComparison: '1.5',
|
|
|
- conversionRate: 8.9,
|
|
|
- conversionRateComparison: '-0.5',
|
|
|
- upsellShareNum: 2345,
|
|
|
- upsellHelpTimes: 6789,
|
|
|
- orderBookNum: 1200,
|
|
|
- orderTotalMoney: 56000,
|
|
|
- upsellTotalMoney: 12000,
|
|
|
- upsellFinalTotalMoney: 44000
|
|
|
- };
|
|
|
-
|
|
|
- // Generate mock trend data
|
|
|
- const dates = [];
|
|
|
- const values = [];
|
|
|
- const start = dayjs(dateRange.value[0]);
|
|
|
- const end = dayjs(dateRange.value[1]);
|
|
|
- const diff = end.diff(start, 'day');
|
|
|
-
|
|
|
- for (let i = 0; i <= diff; i++) {
|
|
|
- dates.push(start.add(i, 'day').format('MM-DD'));
|
|
|
- values.push(Math.floor(Math.random() * 100) + 50);
|
|
|
- }
|
|
|
- trendData.value = { dates, values };
|
|
|
- };
|
|
|
-
|
|
|
- const initChart = () => {
|
|
|
+ const updateChart = () => {
|
|
|
if (!chartRef.value) return;
|
|
|
|
|
|
- if (chart) {
|
|
|
- chart.dispose();
|
|
|
+ if (!chart) {
|
|
|
+ chart = echarts.init(chartRef.value);
|
|
|
}
|
|
|
|
|
|
- chart = echarts.init(chartRef.value);
|
|
|
-
|
|
|
- const option = {
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis'
|
|
|
- },
|
|
|
+ chart.setOption({
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
grid: {
|
|
|
left: '3%',
|
|
|
right: '4%',
|
|
|
@@ -350,34 +341,35 @@
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
boundaryGap: false,
|
|
|
- data: trendData.value.dates
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- type: 'value'
|
|
|
+ data: chartData.value.dates
|
|
|
},
|
|
|
+ yAxis: { type: 'value', min: 0 },
|
|
|
series: [
|
|
|
{
|
|
|
name: '分享人数',
|
|
|
type: 'line',
|
|
|
- stack: 'Total',
|
|
|
smooth: true,
|
|
|
- data: trendData.value.values,
|
|
|
- areaStyle: {
|
|
|
- opacity: 0.3
|
|
|
- },
|
|
|
- itemStyle: {
|
|
|
- color: '#409EFF'
|
|
|
- }
|
|
|
+ data: chartData.value.values,
|
|
|
+ areaStyle: { opacity: 0.3 },
|
|
|
+ itemStyle: { color: '#409EFF' }
|
|
|
}
|
|
|
]
|
|
|
- };
|
|
|
+ });
|
|
|
+ };
|
|
|
|
|
|
- chart.setOption(option);
|
|
|
+ const handleResize = () => {
|
|
|
+ chart?.resize();
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
fetchData();
|
|
|
- window.addEventListener('resize', () => chart && chart.resize());
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+ });
|
|
|
+
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('resize', handleResize);
|
|
|
+ chart?.dispose();
|
|
|
+ chart = null;
|
|
|
});
|
|
|
</script>
|
|
|
|