浏览代码

feat(statsAnalysis): enhance sell data components with improved filtering and loading states

- Updated the sell data index view to include dynamic date range buttons and improved loading indicators.
- Refactored refund statistics component to utilize computed properties for better data handling and display.
- Enhanced the order trend chart with a custom tooltip formatter for clearer data representation.
- Improved sales statistics cards to conditionally display comparison values based on availability.
ylong 1 周之前
父节点
当前提交
56b0ddbf9e

+ 58 - 0
src/api/statsAnalysis/sell.js

@@ -0,0 +1,58 @@
+import request from '@/utils/request';
+
+/**
+ * 销售数据-曲线图
+ */
+export async function getOrderDashboardTrend(params) {
+    const res = await request.get('/shop/orderDashboard/trend', { params });
+    if (res.data.code === 200) {
+        return res.data.data || [];
+    }
+    return Promise.reject(new Error(res.data.msg || '获取曲线图数据失败'));
+}
+
+/**
+ * 销售数据-已完结退款统计
+ */
+export async function getOrderDashboardRefundStat(params) {
+    const res = await request.get('/shop/orderDashboard/refundStat', {
+        params
+    });
+    if (res.data.code === 200) {
+        return res.data.data || {};
+    }
+    return Promise.reject(new Error(res.data.msg || '获取退款统计失败'));
+}
+
+/**
+ * 销售数据-顶部实时计数
+ */
+export async function getOrderDashboardRealtimeCount() {
+    const res = await request.get('/shop/orderDashboard/realtimeCount');
+    if (res.data.code === 200) {
+        return res.data.data || {};
+    }
+    return Promise.reject(new Error(res.data.msg || '获取实时计数失败'));
+}
+
+/**
+ * 销售数据-饼图
+ */
+export async function getOrderDashboardPieChart(params) {
+    const res = await request.get('/shop/orderDashboard/pieChart', { params });
+    if (res.data.code === 200) {
+        return res.data.data || {};
+    }
+    return Promise.reject(new Error(res.data.msg || '获取饼图数据失败'));
+}
+
+/**
+ * 销售数据-KPI
+ */
+export async function getOrderDashboardKpiStat(params) {
+    const res = await request.get('/shop/orderDashboard/kpiStat', { params });
+    if (res.data.code === 200) {
+        return res.data.data || {};
+    }
+    return Promise.reject(new Error(res.data.msg || '获取KPI数据失败'));
+}

+ 22 - 4
src/views/statsAnalysis/sellData/components/OrderTrendChart.vue

@@ -31,7 +31,19 @@
 
     const chartOption = computed(() => ({
         tooltip: {
-            trigger: 'axis'
+            trigger: 'axis',
+            formatter(params) {
+                const rows = (params || []).map((item) => {
+                    const value =
+                        item.seriesName === '转化率'
+                            ? `${item.value}%`
+                            : item.value;
+                    return `${item.marker}${item.seriesName}:${value}`;
+                });
+                return [params?.[0]?.axisValue, ...rows]
+                    .filter(Boolean)
+                    .join('<br/>');
+            }
         },
         legend: {
             data: ['订单数', '支付金额', '转化率', '退款金额'],
@@ -48,9 +60,14 @@
             boundaryGap: false,
             data: props.chartData.dates
         },
-        yAxis: {
-            type: 'value'
-        },
+        yAxis: [
+            { type: 'value' },
+            {
+                type: 'value',
+                axisLabel: { formatter: '{value}%' },
+                splitLine: { show: false }
+            }
+        ],
         series: [
             {
                 name: '订单数',
@@ -70,6 +87,7 @@
                 name: '转化率',
                 type: 'line',
                 smooth: true,
+                yAxisIndex: 1,
                 data: props.chartData.conversionRate,
                 itemStyle: { color: '#4caf50' }
             },

+ 129 - 22
src/views/statsAnalysis/sellData/components/RefundStats.vue

@@ -1,32 +1,40 @@
 <template>
-    <div class="refund-stats">
+    <div v-loading="loading" class="refund-stats">
         <div class="header">
             <div class="title">已完结退款</div>
             <div class="filters">
                 <span class="range-text">统计时间: {{ dateRangeText }}</span>
                 <div class="actions">
-                    <!-- Filters can be passed via slots or handled in parent, 
-                but UI shows them here. Let's make them visual only for now or simple props -->
-                    <el-button-group size="small">
-                        <el-button>昨日</el-button>
-                        <el-button type="primary">7日</el-button>
-                        <el-button>30日</el-button>
-                    </el-button-group>
+                    <el-button
+                        v-for="item in dateRanges"
+                        :key="item.value"
+                        size="small"
+                        :type="activeRange === item.value ? 'primary' : ''"
+                        @click="handleRangeChange(item.value)"
+                    >
+                        {{ item.label }}
+                    </el-button>
                     <el-date-picker
+                        v-model="dateRange"
                         size="small"
                         type="daterange"
-                        style="width: 200px; margin: 0 10px"
+                        value-format="YYYY-MM-DD"
+                        range-separator="-"
+                        start-placeholder="开始"
+                        end-placeholder="结束"
+                        style="width: 240px; margin: 0 10px"
+                        @change="handleDateChange"
                     />
-                    <el-button type="primary" size="small">搜索</el-button>
-                    <el-button size="small">重置</el-button>
+                    <el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
+                    <el-button size="small" @click="handleReset">重置</el-button>
                 </div>
             </div>
         </div>
 
         <div class="cards-container">
             <div
-                v-for="(card, index) in refundData"
-                :key="index"
+                v-for="card in refundCards"
+                :key="card.title"
                 class="refund-card"
             >
                 <div class="card-header">
@@ -44,7 +52,7 @@
                         <span class="value">{{ card.subOrders }}</span>
                     </div>
                     <div class="row">
-                        <span class="label">订单退款率</span>
+                        <span class="label">子单退款率</span>
                         <span class="value">{{ card.refundRate }}</span>
                     </div>
 
@@ -65,15 +73,114 @@
 </template>
 
 <script setup>
-    defineProps({
-        dateRangeText: {
-            type: String,
-            default: '2025-09-15 ~ 2025-10-14'
-        },
-        refundData: {
-            type: Array,
-            default: () => []
+    import { computed, onMounted, ref } from 'vue';
+    import dayjs from 'dayjs';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import { getOrderDashboardRefundStat } from '@/api/statsAnalysis/sell';
+
+    const dateRanges = [
+        { label: '昨日', value: 'yesterday' },
+        { label: '7日', value: '7days' },
+        { label: '30日', value: '30days' }
+    ];
+    const cardDefs = [
+        { key: 'all', title: '全部' },
+        { key: 'returnRefund', title: '退货退款' },
+        { key: 'receivedRefund', title: '已收货退款' },
+        { key: 'notReceivedRefund', title: '未收货退款' },
+        { key: 'notShippedRefund', title: '未发货退款' }
+    ];
+    const activeRange = ref('7days');
+    const dateRange = ref([]);
+    const loading = ref(false);
+    const refundStat = ref({});
+
+    function buildDates(value) {
+        const today = dayjs();
+        if (value === 'yesterday') {
+            const day = today.subtract(1, 'day').format('YYYY-MM-DD');
+            return [day, day];
+        }
+        const offset = value === '30days' ? 29 : 6;
+        return [
+            today.subtract(offset, 'day').format('YYYY-MM-DD'),
+            today.format('YYYY-MM-DD')
+        ];
+    }
+
+    function showValue(value) {
+        if (value == null || value === '') return 0;
+        return value;
+    }
+
+    function showRate(value) {
+        if (value == null || value === '') return '0%';
+        const text = String(value);
+        return text.includes('%') ? text : `${text}%`;
+    }
+
+    const dateRangeText = computed(() => {
+        const [start, end] = dateRange.value || [];
+        if (!start || !end) return '-';
+        return `${start} ~ ${end}`;
+    });
+
+    const refundCards = computed(() =>
+        cardDefs.map((item) => {
+            const data = refundStat.value?.[item.key] || {};
+            return {
+                title: item.title,
+                refundUsers: showValue(data.refundUserNum),
+                subOrders: showValue(data.refundSubOrderNum),
+                refundRate: showRate(data.orderRefundRate),
+                refundAmount: showValue(data.refundAmount),
+                amountRate: showRate(data.amountRefundRate)
+            };
+        })
+    );
+
+    async function loadRefund() {
+        const [start, end] = dateRange.value || [];
+        const startDay = start ? dayjs(start) : dayjs();
+        const endDay = end ? dayjs(end) : dayjs();
+        loading.value = true;
+        try {
+            refundStat.value = await getOrderDashboardRefundStat({
+                startTime: startDay.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+                endTime: endDay.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+            });
+        } catch (error) {
+            refundStat.value = {};
+            EleMessage.error(error.message || '获取退款统计失败');
+        } finally {
+            loading.value = false;
+        }
+    }
+
+    function handleRangeChange(value) {
+        activeRange.value = value;
+        dateRange.value = buildDates(value);
+        loadRefund();
+    }
+
+    function handleDateChange() {
+        activeRange.value = '';
+    }
+
+    function handleSearch() {
+        if (!dateRange.value?.length) {
+            handleRangeChange('7days');
+            return;
         }
+        loadRefund();
+    }
+
+    function handleReset() {
+        handleRangeChange('7days');
+    }
+
+    onMounted(() => {
+        handleRangeChange('7days');
     });
 </script>
 

+ 3 - 1
src/views/statsAnalysis/sellData/components/SalesStatCards.vue

@@ -21,7 +21,9 @@
             >
                 <div class="stat-header">{{ item.label }}</div>
                 <div class="stat-value">{{ item.value }}</div>
-                <div class="stat-compare"> 昨日 {{ item.yesterdayValue }} </div>
+                <div v-if="item.showCompare !== false" class="stat-compare">
+                    昨日 {{ item.yesterdayValue }}
+                </div>
             </div>
         </div>
     </div>

+ 190 - 121
src/views/statsAnalysis/sellData/index.vue

@@ -1,29 +1,30 @@
 <template>
     <ele-page>
-        <ele-card :body-style="{ padding: '20px' }">
-            <!-- Filters -->
+        <ele-card v-loading="loading" :body-style="{ padding: '20px' }">
             <div class="filter-container">
                 <div class="date-ranges">
-                    <el-button-group>
-                        <el-button type="primary">实时</el-button>
-                        <el-button>昨日</el-button>
-                        <el-button>7日</el-button>
-                        <el-button>15日</el-button>
-                        <el-button>30日</el-button>
-                    </el-button-group>
+                    <el-button
+                        v-for="item in dateRanges"
+                        :key="item.value"
+                        :type="activeRange === item.value ? 'primary' : ''"
+                        @click="handleRangeChange(item.value)"
+                    >
+                        {{ item.label }}
+                    </el-button>
                 </div>
                 <div class="date-picker-wrapper">
                     <el-date-picker
                         v-model="dateRange"
                         type="daterange"
-                        range-separator="到"
+                        range-separator="-"
                         start-placeholder="开始时间"
                         end-placeholder="结束时间"
-                        size="default"
+                        value-format="YYYY-MM-DD"
+                        @change="handleDateChange"
                     />
                 </div>
-                <el-button type="primary" class="search-btn">搜索</el-button>
-                <el-button>重置</el-button>
+                <el-button type="primary" @click="handleSearch">搜索</el-button>
+                <el-button @click="handleReset">重置</el-button>
             </div>
 
             <!-- Stats Cards -->
@@ -42,133 +43,201 @@
             />
 
             <!-- Refund Stats -->
-            <refund-stats :refund-data="refundData" />
+            <refund-stats />
         </ele-card>
     </ele-page>
 </template>
 
 <script setup>
-    import { ref, reactive } from 'vue';
+    import { computed, onMounted, ref } from 'vue';
+    import dayjs from 'dayjs';
+    import { EleMessage } from 'ele-admin-plus/es';
     import SalesStatCards from './components/SalesStatCards.vue';
     import OrderTrendChart from './components/OrderTrendChart.vue';
     import DistributionCharts from './components/DistributionCharts.vue';
     import RefundStats from './components/RefundStats.vue';
+    import {
+        getOrderDashboardKpiStat,
+        getOrderDashboardPieChart,
+        getOrderDashboardRealtimeCount,
+        getOrderDashboardTrend
+    } from '@/api/statsAnalysis/sell';
 
+    const activeRange = ref('realtime');
     const dateRange = ref([]);
-
-    // Mock Data
-    const statusData = [
-        { label: '待付款', value: 11 },
-        { label: '待发货', value: 11 },
-        { label: '待售后', value: 11 },
-        { label: '待处理投诉', value: 11 }
+    const loading = ref(false);
+    const realtimeCount = ref({});
+    const kpiStat = ref({});
+    const trendList = ref([]);
+    const pieStat = ref({});
+    const dateRanges = [
+        { label: "实时", value: "realtime" },
+        { label: "昨日", value: "yesterday" },
+        { label: "7日", value: "7days" },
+        { label: "30日", value: "30days" }
     ];
 
-    const salesData = [
-        { label: '支付金额', value: '55366', yesterdayValue: '53669' },
-        { label: '访客数', value: '55366', yesterdayValue: '53669' },
-        { label: '支付订单数', value: '55366', yesterdayValue: '53669' },
-        { label: '支付转化率', value: '55366', yesterdayValue: '53669' },
-        { label: '商品浏览量', value: '55366', yesterdayValue: '53669' },
-        { label: '客单价', value: '55366', yesterdayValue: '53669' },
-        { label: '支付买家数', value: '55366', yesterdayValue: '53669' },
-        { label: '加购商品数', value: '55366', yesterdayValue: '53669' },
-        { label: '收藏商品数', value: '55366', yesterdayValue: '53669' },
-        { label: '在途订单数', value: '55366', yesterdayValue: '53669' },
-        { label: '在途金额', value: '55366', yesterdayValue: '53669' },
-        { label: '老客复购率', value: '55366', yesterdayValue: '53669' },
-        { label: '老客复购金额', value: '55366', yesterdayValue: '53669' },
-        { label: '老客复购人数', value: '55366', yesterdayValue: '53669' },
-        { label: '净支付金额', value: '55366', yesterdayValue: '53669' }
-    ];
+    function buildDates(value) {
+        const today = dayjs();
+        if (value === 'yesterday') {
+            const day = today.subtract(1, 'day').format('YYYY-MM-DD');
+            return [day, day];
+        }
+        const offset = value === '7days' ? 6 : value === '30days' ? 29 : 0;
+        return [
+            today.subtract(offset, 'day').format('YYYY-MM-DD'),
+            today.format('YYYY-MM-DD')
+        ];
+    }
 
-    const trendChartData = reactive({
-        dates: [
-            '15日',
-            '16日',
-            '17日',
-            '18日',
-            '19日',
-            '20日',
-            '21日',
-            '22日',
-            '23日',
-            '24日',
-            '25日',
-            '26日',
-            '27日'
-        ],
-        orderCount: [
-            100, 120, 200, 100, 130, 150, 180, 220, 150, 170, 110, 160, 210
-        ],
-        paymentAmount: [
-            150, 100, 230, 110, 100, 170, 210, 110, 108, 150, 190, 180, 170
-        ],
-        conversionRate: [
-            150, 130, 180, 150, 210, 210, 210, 220, 180, 130, 160, 180, 170
-        ],
-        refundAmount: [
-            160, 220, 230, 160, 210, 150, 160, 150, 170, 200, 170, 220, 100
-        ]
+    function buildParams() {
+        const now = dayjs();
+        if (activeRange.value === 'realtime') {
+            return {
+                startTime: now.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+                endTime: now.format('YYYY-MM-DD HH:mm:ss')
+            };
+        }
+        const [start, end] = dateRange.value || [];
+        const startDay = start ? dayjs(start) : now;
+        const endDay = end ? dayjs(end) : now;
+        return {
+            startTime: startDay.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+            endTime: endDay.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+        };
+    }
+
+    function showValue(value) {
+        if (value == null || value === '') {
+            return 0;
+        }
+        return value;
+    }
+
+    function toNumber(value) {
+        if (value == null || value === '') {
+            return 0;
+        }
+        const num = Number(String(value).replace('%', ''));
+        return Number.isFinite(num) ? num : 0;
+    }
+
+    const statusData = computed(() => [
+        { label: '待付款', value: showValue(realtimeCount.value.pendingPayCount) },
+        { label: '待发货', value: showValue(realtimeCount.value.pendingShipCount) },
+        { label: '待售后', value: showValue(realtimeCount.value.pendingAfterSalesCount) },
+        { label: '待处理投诉', value: showValue(realtimeCount.value.pendingComplaintsCount) }
+    ]);
+
+    const salesData = computed(() => {
+        const data = kpiStat.value;
+        return [
+            { label: '支付金额', value: showValue(data.payAmount), yesterdayValue: showValue(data.yesterdayPayAmount) },
+            { label: '访客数', value: showValue(data.visitorNum), yesterdayValue: showValue(data.yesterdayVisitorNum) },
+            { label: '支付订单数', value: showValue(data.payOrderNum), yesterdayValue: showValue(data.yesterdayPayOrderNum) },
+            { label: '支付转化率', value: showValue(data.payConversionRate), yesterdayValue: showValue(data.yesterdayPayConversionRate) },
+            { label: '商品浏览量', value: showValue(data.productViewNum), yesterdayValue: showValue(data.yesterdayProductViewNum) },
+            { label: '客单价', value: showValue(data.avgOrderValue), yesterdayValue: showValue(data.yesterdayAvgOrderValue) },
+            { label: '支付买家数', value: showValue(data.payBuyerNum), yesterdayValue: showValue(data.yesterdayPayBuyerNum) },
+            { label: '加购商品数', value: showValue(data.cartAddNum), yesterdayValue: showValue(data.yesterdayCartAddNum) },
+            { label: '收藏商品数', value: showValue(data.collectProductNum), yesterdayValue: showValue(data.yesterdayCollectProductNum) },
+            { label: '在途订单数', value: showValue(data.inTransitOrderNum), showCompare: false },
+            { label: '在途金额', value: showValue(data.inTransitAmount), showCompare: false },
+            { label: '老客复购率', value: showValue(data.oldCustomerRepurchaseRate), yesterdayValue: showValue(data.yesterdayOldCustomerRepurchaseRate) },
+            { label: '老客复购金额', value: showValue(data.oldCustomerPurchaseAmount), yesterdayValue: showValue(data.yesterdayOldCustomerPurchaseAmount) },
+            { label: '老客复购人数', value: showValue(data.oldCustomerNum), yesterdayValue: showValue(data.yesterdayOldCustomerNum) },
+            { label: '净支付金额', value: showValue(data.netPayAmount), yesterdayValue: showValue(data.yesterdayNetPayAmount) }
+        ];
     });
 
-    const paymentMethodData = [
-        { value: 100, name: '微信支付', itemStyle: { color: '#5b8ff9' } },
-        { value: 150, name: '支付宝支付', itemStyle: { color: '#5ebcb9' } },
-        { value: 224, name: '余额支付', itemStyle: { color: '#4caf50' } }
-    ];
+    const trendChartData = computed(() => ({
+        dates: trendList.value.map((item) => item.timeLabel),
+        orderCount: trendList.value.map((item) => toNumber(item.payOrderNum)),
+        paymentAmount: trendList.value.map((item) => toNumber(item.payAmount)),
+        conversionRate: trendList.value.map((item) => toNumber(item.payConversionRate)),
+        refundAmount: trendList.value.map((item) => toNumber(item.refundAmount))
+    }));
 
-    const orderStatusData = [
-        { value: 122, name: '待发货', itemStyle: { color: '#ff9800' } },
-        { value: 150, name: '已退款', itemStyle: { color: '#5ebcb9' } },
-        { value: 170, name: '待收货', itemStyle: { color: '#4caf50' } },
-        { value: 174, name: '已取消', itemStyle: { color: '#f44336' } },
-        { value: 175, name: '已完成', itemStyle: { color: '#e91e63' } },
-        { value: 100, name: '已发货', itemStyle: { color: '#5b8ff9' } }
-    ];
+    const paymentMethodData = computed(() => [
+        { value: toNumber(pieStat.value.payTypeBalanceNum), name: '余额支付', itemStyle: { color: '#4caf50' } },
+        { value: toNumber(pieStat.value.payTypeAlipayNum), name: '支付宝支付', itemStyle: { color: '#5ebcb9' } },
+        { value: toNumber(pieStat.value.payTypeWechatNum), name: '微信支付', itemStyle: { color: '#5b8ff9' } }
+    ]);
+
+    const orderStatusData = computed(() => [
+        { value: toNumber(pieStat.value.statusPendingPayNum), name: '待付款', itemStyle: { color: '#5ebcb9' } },
+        { value: toNumber(pieStat.value.statusPendingShipNum), name: '待发货', itemStyle: { color: '#ff9800' } },
+        { value: toNumber(pieStat.value.statusPendingReceiveNum), name: '待收货', itemStyle: { color: '#4caf50' } },
+        { value: toNumber(pieStat.value.statusReceivedNum), name: '已收货', itemStyle: { color: '#5b8ff9' } },
+        { value: toNumber(pieStat.value.statusCompletedNum), name: '已完成', itemStyle: { color: '#e91e63' } },
+        { value: toNumber(pieStat.value.statusCancelledNum), name: '已取消', itemStyle: { color: '#f44336' } }
+    ]);
+
+    async function loadDashboard() {
+        loading.value = true;
+        const params = buildParams();
+        await Promise.all([
+            getOrderDashboardRealtimeCount()
+                .then((data) => {
+                    realtimeCount.value = data;
+                })
+                .catch((error) => {
+                    realtimeCount.value = {};
+                    EleMessage.error(error.message || '获取实时计数失败');
+                }),
+            getOrderDashboardKpiStat(params)
+                .then((data) => {
+                    kpiStat.value = data;
+                })
+                .catch((error) => {
+                    kpiStat.value = {};
+                    EleMessage.error(error.message || '获取KPI数据失败');
+                }),
+            getOrderDashboardTrend(params)
+                .then((data) => {
+                    trendList.value = Array.isArray(data) ? data : [];
+                })
+                .catch((error) => {
+                    trendList.value = [];
+                    EleMessage.error(error.message || '获取曲线图数据失败');
+                }),
+            getOrderDashboardPieChart(params)
+                .then((data) => {
+                    pieStat.value = data;
+                })
+                .catch((error) => {
+                    pieStat.value = {};
+                    EleMessage.error(error.message || '获取饼图数据失败');
+                })
+        ]);
+        loading.value = false;
+    }
+
+    function handleRangeChange(value) {
+        activeRange.value = value;
+        dateRange.value = buildDates(value);
+        loadDashboard();
+    }
+
+    function handleDateChange() {
+        activeRange.value = '';
+    }
 
-    const refundData = [
-        {
-            title: '全部',
-            refundUsers: 2720,
-            subOrders: 3089,
-            refundRate: '10.63%',
-            refundAmount: 36738.53,
-            amountRate: '11.03%'
-        },
-        {
-            title: '退货退款',
-            refundUsers: 339,
-            subOrders: 354,
-            refundRate: '1.22%',
-            refundAmount: 36738.53,
-            amountRate: '11.03%'
-        },
-        {
-            title: '已收货退款',
-            refundUsers: 28,
-            subOrders: 3089,
-            refundRate: '10.63%',
-            refundAmount: 36738.53,
-            amountRate: '11.03%'
-        },
-        {
-            title: '未收货退款',
-            refundUsers: 2720,
-            subOrders: 3089,
-            refundRate: '10.63%',
-            refundAmount: 36738.53,
-            amountRate: '11.03%'
-        },
-        {
-            title: '未发货退款',
-            refundUsers: 2720,
-            subOrders: 3089,
-            refundRate: '10.63%',
-            refundAmount: 36738.53,
-            amountRate: '11.03%'
+    function handleSearch() {
+        if (!dateRange.value?.length) {
+            handleRangeChange('realtime');
+            return;
         }
-    ];
+        loadDashboard();
+    }
+
+    function handleReset() {
+        handleRangeChange('realtime');
+    }
+
+    onMounted(() => {
+        handleRangeChange('realtime');
+    });
 </script>
 
 <style scoped lang="scss">