|
|
@@ -0,0 +1,332 @@
|
|
|
+<template>
|
|
|
+ <div class="fission-analysis-container p-4">
|
|
|
+ <el-card class="analysis-card" shadow="never">
|
|
|
+ <div class="date-selector-container mb-6">
|
|
|
+ <!-- Top Navigation Section -->
|
|
|
+ <div class="date-period-selector flex items-center gap-4">
|
|
|
+ <el-button-group>
|
|
|
+ <el-button
|
|
|
+ :type="activePeriod === 'yesterday' ? 'primary' : ''"
|
|
|
+ @click="changePeriod('yesterday')"
|
|
|
+ >昨日</el-button>
|
|
|
+ <el-button
|
|
|
+ :type="activePeriod === '7d' ? 'primary' : ''"
|
|
|
+ @click="changePeriod('7d')"
|
|
|
+ >7日</el-button>
|
|
|
+ <el-button
|
|
|
+ :type="activePeriod === '15d' ? 'primary' : ''"
|
|
|
+ @click="changePeriod('15d')"
|
|
|
+ >15日</el-button>
|
|
|
+ <el-button
|
|
|
+ :type="activePeriod === '30d' ? 'primary' : ''"
|
|
|
+ @click="changePeriod('30d')"
|
|
|
+ >30日</el-button>
|
|
|
+ </el-button-group>
|
|
|
+
|
|
|
+ <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"
|
|
|
+ @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 mb-6">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col
|
|
|
+ v-for="(item, index) in metricsCardsData"
|
|
|
+ :key="index"
|
|
|
+ :span="4"
|
|
|
+ >
|
|
|
+ <el-card shadow="never" class="metric-card">
|
|
|
+ <div class="text-gray-500 text-sm mb-2">{{ item.title }}</div>
|
|
|
+ <div class="text-xs text-gray-400 mb-2">{{ displayDateRange }}</div>
|
|
|
+ <div class="flex justify-between items-end mb-2">
|
|
|
+ <span class="text-sm text-gray-500">合计</span>
|
|
|
+ <span class="text-xl font-bold">{{ item.value }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="text-xs text-gray-500 flex items-center">
|
|
|
+ 环比 {{ item.change }}%
|
|
|
+ <span v-if="parseFloat(item.change) > 0" class="text-red-500 ml-1">↑</span>
|
|
|
+ <span v-else-if="parseFloat(item.change) < 0" class="text-green-500 ml-1">↓</span>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Numbers Cards -->
|
|
|
+ <div class="number-metrics mb-6">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col
|
|
|
+ :span="4"
|
|
|
+ v-for="(item, index) in numberMetricsData"
|
|
|
+ :key="index"
|
|
|
+ >
|
|
|
+ <el-card shadow="hover" class="bg-gray-50">
|
|
|
+ <div class="text-2xl font-bold mb-1 text-center">{{ item.value }}</div>
|
|
|
+ <div class="text-sm text-gray-500 text-center">{{ item.label }}</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Chart Section -->
|
|
|
+ <div class="trend-chart-section">
|
|
|
+ <div class="text-lg font-bold mb-4">30日分享人数趋势</div>
|
|
|
+ <div class="h-80 w-full" ref="chartRef"></div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+ import { ref, onMounted, computed, watch, nextTick } from 'vue';
|
|
|
+ import * as echarts from 'echarts';
|
|
|
+ import dayjs from 'dayjs';
|
|
|
+ import { ElMessage } from 'element-plus';
|
|
|
+ import request from '@/utils/request';
|
|
|
+
|
|
|
+ defineOptions({ name: 'ShareDiscountFission' });
|
|
|
+
|
|
|
+ // State
|
|
|
+ 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);
|
|
|
+
|
|
|
+ // 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',
|
|
|
+
|
|
|
+ upsellShareNum: 0,
|
|
|
+ upsellHelpTimes: 0,
|
|
|
+ orderBookNum: 0,
|
|
|
+ orderTotalMoney: 0,
|
|
|
+ upsellTotalMoney: 0,
|
|
|
+ upsellFinalTotalMoney: 0
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const trendData = ref({
|
|
|
+ dates: [],
|
|
|
+ values: []
|
|
|
+ });
|
|
|
+
|
|
|
+ const displayDateRange = computed(() => {
|
|
|
+ if (!dateRange.value) return '';
|
|
|
+ return `${dateRange.value[0]} ~ ${dateRange.value[1]}`;
|
|
|
+ });
|
|
|
+
|
|
|
+ const metricsCardsData = computed(() => {
|
|
|
+ const data = statisticData.value.data;
|
|
|
+ return [
|
|
|
+ { title: '分享人数', value: data.upsellHelpNum, change: data.upsellHelpNumComparison },
|
|
|
+ { title: '成单数', value: data.upsellOrderNum, change: data.upsellOrderNumComparison },
|
|
|
+ { title: '扫码人数', value: data.upsellScanNum, change: data.upsellScanNumComparison },
|
|
|
+ { title: '扫码参与率', value: data.scanJoinRate + '%', change: data.scanJoinRateComparison },
|
|
|
+ { title: '新用户占比', value: data.newUserRate + '%', change: data.newUserRateComparison },
|
|
|
+ { title: '转化率', value: data.conversionRate + '%', change: data.conversionRateComparison },
|
|
|
+ ];
|
|
|
+ });
|
|
|
+
|
|
|
+ const numberMetricsData = computed(() => {
|
|
|
+ const data = statisticData.value.data;
|
|
|
+ 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 },
|
|
|
+ ];
|
|
|
+ });
|
|
|
+
|
|
|
+ const changePeriod = (period) => {
|
|
|
+ activePeriod.value = period;
|
|
|
+ isCustomDateRange.value = false;
|
|
|
+
|
|
|
+ const end = dayjs();
|
|
|
+ let start;
|
|
|
+
|
|
|
+ switch (period) {
|
|
|
+ case 'yesterday':
|
|
|
+ start = end.subtract(1, 'day');
|
|
|
+ break;
|
|
|
+ case '7d':
|
|
|
+ start = end.subtract(6, 'day');
|
|
|
+ break;
|
|
|
+ case '15d':
|
|
|
+ start = end.subtract(14, 'day');
|
|
|
+ break;
|
|
|
+ case '30d':
|
|
|
+ start = end.subtract(29, 'day');
|
|
|
+ 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 = () => {
|
|
|
+ changePeriod('30d');
|
|
|
+ };
|
|
|
+
|
|
|
+ const fetchData = async () => {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await request.get('/marketing/shareDiscount/fission/analysis', {
|
|
|
+ params: {
|
|
|
+ startDate: dateRange.value[0],
|
|
|
+ endDate: dateRange.value[1]
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.data) {
|
|
|
+ statisticData.value.data = res.data.summary || statisticData.value.data;
|
|
|
+ trendData.value = res.data.trend || { dates: [], values: [] };
|
|
|
+ }
|
|
|
+ ElMessage.success('数据已更新');
|
|
|
+ initChart();
|
|
|
+ } catch (error) {
|
|
|
+ // Mock data fallback
|
|
|
+ mockData();
|
|
|
+ ElMessage.success('数据已更新 (Mock)');
|
|
|
+ initChart();
|
|
|
+ } 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 = () => {
|
|
|
+ if (!chartRef.value) return;
|
|
|
+
|
|
|
+ if (chart) {
|
|
|
+ chart.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ chart = echarts.init(chartRef.value);
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis'
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: trendData.value.dates
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value'
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '分享人数',
|
|
|
+ type: 'line',
|
|
|
+ stack: 'Total',
|
|
|
+ smooth: true,
|
|
|
+ data: trendData.value.values,
|
|
|
+ areaStyle: {
|
|
|
+ opacity: 0.3
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: '#409EFF'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ chart.setOption(option);
|
|
|
+ };
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ fetchData();
|
|
|
+ window.addEventListener('resize', () => chart && chart.resize());
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.metric-card {
|
|
|
+ height: 140px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+</style>
|