|
|
@@ -0,0 +1,506 @@
|
|
|
+<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>
|
|
|
+</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
|
|
|
+ },
|
|
|
+ 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
|
|
|
+ // This will ensure the chart resizes properly when tabs change
|
|
|
+ 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) => {
|
|
|
+ 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 = '';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ initChart();
|
|
|
+
|
|
|
+ // Initialize dateRange based on the default activePeriod (yesterday)
|
|
|
+ if (activePeriod.value === 'yesterday') {
|
|
|
+ dateRange.value = dayjs().subtract(1, 'day').toDate();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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>
|
|
|
+
|
|
|
+<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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .active {
|
|
|
+ color: #409eff;
|
|
|
+ border-color: #409eff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-date {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-comparison {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .number-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
+ }
|
|
|
+</style>
|