Selaa lähdekoodia

feat(statistics): enhance statistics components with new mall statistics and improved layout

- Added a new `mallStat` component to display mall-related statistics in the main index view.
- Refactored `goodsStat` and `orderStat` components to utilize computed properties for dynamic data handling.
- Improved layout and styling for better presentation of statistics, including responsive design adjustments.
- Updated chart configurations in `sale-card` to include a legend and ensure today's data is represented accurately.
ylong 1 viikko sitten
vanhempi
sitoutus
a67faa55d3

+ 81 - 35
src/views/index/components/goods-stat.vue

@@ -1,38 +1,20 @@
 <template>
     <ele-card>
-        <el-row :gutter="24">
-            <el-col :span="8">
-                <div class="flex justify-center items-center">
-                    <el-icon size="40" color="#E6A23C"><SoldOut /></el-icon>
-                    <div class="flex flex-col ml-6">
-                        <div class="text-lg text-theme">待发货订单</div>
-                        <div class="text-3xl text-theme">{{
-                            detail?.waitSendOrderNum || 0
-                        }}</div>
-                    </div>
-                </div>
-            </el-col>
-            <el-col :span="8">
-                <div class="flex justify-center items-center border-l">
-                    <el-icon size="40" color="#F56C6C"><Wallet /></el-icon>
-                    <div class="flex flex-col ml-6">
-                        <div class="text-lg text-theme">退款待审核</div>
-                        <div class="text-3xl text-theme">{{
-                            detail?.waitRefundOrderNum || 0
-                        }}</div>
-                    </div>
-                </div>
-            </el-col>
-            <el-col :span="8">
-                <div class="flex justify-center items-center border-l">
-                    <el-icon size="40" color="#409EFF"
-                        ><ChatDotSquare
-                    /></el-icon>
-                    <div class="flex flex-col ml-6">
-                        <div class="text-lg text-theme">待处理工单</div>
-                        <div class="text-3xl text-theme">{{
-                            detail?.waitHandleWorkOrderNum || 0
-                        }}</div>
+        <el-row>
+            <el-col
+                v-for="(item, index) in items"
+                :key="item.label"
+                :span="8"
+                :xs="24"
+            >
+                <div
+                    class="shop-item"
+                    :class="{ 'is-split': index > 0 }"
+                >
+                    <div class="shop-badge">商城</div>
+                    <div class="shop-text">
+                        <div class="shop-label">{{ item.label }}</div>
+                        <div class="shop-num">{{ item.value }}</div>
                     </div>
                 </div>
             </el-col>
@@ -41,12 +23,76 @@
 </template>
 
 <script setup>
-    import { SoldOut, Wallet, ChatDotSquare } from '@element-plus/icons-vue';
+    import { computed } from 'vue';
 
-    defineProps({
+    const props = defineProps({
         detail: {
             type: Object,
             default: () => ({})
         }
     });
+
+    const num = (value) => {
+        const count = Number(value);
+        return Number.isFinite(count) ? count : 0;
+    };
+
+    const items = computed(() => [
+        {
+            label: '待发货订单',
+            value: num(props.detail?.shopWaitSendOrderNum)
+        },
+        {
+            label: '退款待审核',
+            value: num(props.detail?.shopWaitRefundOrderNum)
+        },
+        {
+            label: '待处理工单',
+            value: num(props.detail?.shopWaitHandleWorkOrderNum)
+        }
+    ]);
 </script>
+
+<style lang="scss" scoped>
+    .shop-item {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        min-height: 88px;
+    }
+
+    .shop-item.is-split {
+        border-left: 1px solid #ebeef5;
+    }
+
+    .shop-badge {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 64px;
+        height: 64px;
+        border-radius: 50%;
+        background: #f2f3f5;
+        color: #606266;
+        font-size: 16px;
+        flex-shrink: 0;
+    }
+
+    .shop-text {
+        margin-left: 16px;
+    }
+
+    .shop-label {
+        color: #606266;
+        font-size: 16px;
+        line-height: 1.4;
+    }
+
+    .shop-num {
+        margin-top: 4px;
+        color: #303133;
+        font-size: 28px;
+        font-weight: 600;
+        line-height: 1.2;
+    }
+</style>

+ 171 - 0
src/views/index/components/mall-stat.vue

@@ -0,0 +1,171 @@
+<template>
+    <ele-card>
+        <div class="mall-title">
+            <span class="mall-title-bar"></span>
+            <span>商城数据</span>
+        </div>
+        <el-row :gutter="24">
+            <el-col :span="12" :xs="24">
+                <div class="mall-panel">
+                    <div
+                        v-for="item in leftItems"
+                        :key="item.label"
+                        class="mall-cell"
+                    >
+                        <div class="mall-num">{{ item.value }}</div>
+                        <div class="mall-label">
+                            <i class="mall-dot" :class="item.dot"></i>
+                            <span>{{ item.label }}</span>
+                        </div>
+                    </div>
+                </div>
+            </el-col>
+            <el-col :span="12" :xs="24">
+                <div class="mall-panel">
+                    <div
+                        v-for="item in rightItems"
+                        :key="item.label"
+                        class="mall-cell"
+                    >
+                        <div class="mall-num">{{ item.value }}</div>
+                        <div class="mall-label">
+                            <i class="mall-dot" :class="item.dot"></i>
+                            <span>{{ item.label }}</span>
+                        </div>
+                    </div>
+                </div>
+            </el-col>
+        </el-row>
+    </ele-card>
+</template>
+
+<script setup>
+    import { computed } from 'vue';
+
+    const props = defineProps({
+        detail: {
+            type: Object,
+            default: () => ({})
+        }
+    });
+
+    const num = (value) => {
+        const count = Number(value);
+        return Number.isFinite(count) ? count : 0;
+    };
+
+    const leftItems = computed(() => [
+        {
+            label: '预警/催发货订单',
+            value: num(props.detail?.shopUrgeOrderNum),
+            dot: 'is-orange'
+        },
+        {
+            label: '待收货订单',
+            value: num(props.detail?.shopWaitReceiveOrderNum),
+            dot: 'is-green'
+        },
+        {
+            label: '待付款订单',
+            value: num(props.detail?.shopWaitPayOrderNum),
+            dot: 'is-orange'
+        },
+        {
+            label: '售后逾期预警',
+            value: num(props.detail?.shopAfterSaleExpiredNum),
+            dot: 'is-orange'
+        }
+    ]);
+
+    const rightItems = computed(() => [
+        {
+            label: '投诉管理',
+            value: num(props.detail?.shopComplaintsNum),
+            dot: 'is-orange'
+        },
+        {
+            label: '待上架商品数',
+            value: num(props.detail?.shopOnSaleBookNum),
+            dot: 'is-green'
+        },
+        {
+            label: '客户价值',
+            value: num(props.detail?.shopCustomerValue),
+            dot: 'is-orange'
+        },
+        {
+            label: '客单价',
+            value: num(props.detail?.shopAvgOrderMoney),
+            dot: 'is-orange'
+        }
+    ]);
+</script>
+
+<style lang="scss" scoped>
+    .mall-title {
+        display: flex;
+        align-items: center;
+        margin-bottom: 16px;
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+    }
+
+    .mall-title-bar {
+        width: 3px;
+        height: 16px;
+        margin-right: 8px;
+        background: #67c23a;
+    }
+
+    .mall-panel {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        border: 1px solid #ebeef5;
+        border-radius: 4px;
+    }
+
+    .mall-cell {
+        padding: 28px 12px 24px;
+        text-align: center;
+
+        &:nth-child(2n) {
+            border-left: 1px solid #ebeef5;
+        }
+
+        &:nth-child(n + 3) {
+            border-top: 1px solid #ebeef5;
+        }
+    }
+
+    .mall-num {
+        font-size: 28px;
+        line-height: 1.2;
+        font-weight: 600;
+        color: #303133;
+    }
+
+    .mall-label {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-top: 10px;
+        color: #606266;
+        font-size: 14px;
+    }
+
+    .mall-dot {
+        width: 8px;
+        height: 8px;
+        margin-right: 6px;
+        border-radius: 50%;
+
+        &.is-orange {
+            background: #e6a23c;
+        }
+
+        &.is-green {
+            background: #67c23a;
+        }
+    }
+</style>

+ 164 - 59
src/views/index/components/order-stat.vue

@@ -1,66 +1,45 @@
 <template>
     <ele-card>
-        <el-row :gutter="24" justify="center">
-            <el-col :span="8">
-                <div class="flex justify-center items-center mb-5 border-b">
-                    <div class="flex items-center flex-col flex-1 pb-6">
-                        <div class="text-2xl text-theme">{{
-                            detail?.waitFirstCheckOrderNum || 0
-                        }}</div>
-                        <div class="text-base text-theme">待初审订单</div>
-                    </div>
-                    <div class="flex items-center flex-col flex-1 border-l">
-                        <div class="text-2xl text-theme">{{
-                            detail?.waitReceiveOrderNum || 0
-                        }}</div>
-                        <div class="text-base text-theme">待收货订单</div>
-                    </div>
-                </div>
-                <div class="flex justify-center items-center">
-                    <div class="flex items-center flex-col flex-1">
-                        <div class="text-2xl text-theme">{{
-                            detail?.waitCheckOrderNum || 0
-                        }}</div>
-                        <div class="text-base text-theme">待审核订单</div>
-                    </div>
-                    <div class="flex items-center flex-col flex-1 border-l">
-                        <div class="text-2xl text-theme">{{
-                            detail?.waitPayOrderNum || 0
-                        }}</div>
-                        <div class="text-base text-theme">待付款订单</div>
+        <div class="section-title">
+            <span class="section-bar"></span>
+            <span>回收数据</span>
+        </div>
+        <el-row :gutter="24">
+            <el-col :span="12" :xs="24">
+                <div
+                    v-for="(group, index) in leftGroups"
+                    :key="index"
+                    class="stat-panel"
+                >
+                    <div
+                        v-for="item in group"
+                        :key="item.label"
+                        class="stat-cell"
+                    >
+                        <div class="stat-num">{{ item.value }}</div>
+                        <div class="stat-label">
+                            <i class="stat-dot" :class="item.dot"></i>
+                            <span>{{ item.label }}</span>
+                        </div>
                     </div>
                 </div>
             </el-col>
-            <el-col :span="3" />
-            <el-col :span="8">
-                <div class="flex justify-center items-center mb-5 border-b">
-                    <div class="flex items-center flex-col flex-1 pb-6">
-                        <div class="text-2xl text-theme">{{
-                            detail?.stockWarnNum || 0
-                        }}</div>
-                        <div class="text-base text-theme">库存预警</div>
-                    </div>
-                    <div class="flex items-center flex-col flex-1 border-l">
-                        <div class="text-2xl text-theme">{{
-                            detail?.stockFullNum || 0
-                        }}</div>
-                        <div class="text-base text-theme">库存已满</div>
-                    </div>
-                </div>
-                <div class="flex justify-center items-center">
-                    <div class="flex items-center flex-col flex-1">
-                        <div class="text-2xl text-theme">{{
-                            detail?.noFirstCheckNum || 0
-                        }}</div>
-                        <div class="text-base text-theme">24小时未初审</div>
-                    </div>
-                    <div class="flex items-center flex-col flex-1 border-l">
-                        <div class="text-2xl text-theme">{{
-                            detail?.recyclePriceAbnormalNum || 0
-                        }}</div>
-                        <div class="text-base text-theme"
-                            >回收价>出售价(中等)</div
-                        >
+            <el-col :span="12" :xs="24">
+                <div
+                    v-for="(group, index) in rightGroups"
+                    :key="index"
+                    class="stat-panel"
+                >
+                    <div
+                        v-for="item in group"
+                        :key="item.label"
+                        class="stat-cell"
+                    >
+                        <div class="stat-num">{{ item.value }}</div>
+                        <div class="stat-label">
+                            <i class="stat-dot" :class="item.dot"></i>
+                            <span>{{ item.label }}</span>
+                        </div>
                     </div>
                 </div>
             </el-col>
@@ -69,10 +48,136 @@
 </template>
 
 <script setup>
-    defineProps({
+    import { computed } from 'vue';
+
+    const props = defineProps({
         detail: {
             type: Object,
             default: () => ({})
         }
     });
+
+    const num = (value) => {
+        const count = Number(value);
+        return Number.isFinite(count) ? count : 0;
+    };
+
+    const cell = (label, field, dot) => ({
+        label,
+        value: num(props.detail?.[field]),
+        dot
+    });
+
+    const leftGroups = computed(() => [
+        [
+            cell('待初审订单', 'waitFirstCheckOrderNum', 'is-orange'),
+            cell('待收货订单', 'waitReceiveOrderNum', 'is-green'),
+            cell('待审核订单', 'waitCheckOrderNum', 'is-orange'),
+            cell('待付款订单', 'waitPayOrderNum', 'is-orange')
+        ],
+        [
+            cell('待退回订单', 'waitReturnOrderNum', 'is-orange'),
+            cell('已到仓-收货超时', 'arriveReceiveTimeoutNum', 'is-green'),
+            cell('已收货-审核超时', 'receiveCheckTimeoutNum', 'is-orange'),
+            cell('回收催审核', 'recycleUrgeCheckNum', 'is-orange')
+        ],
+        [
+            cell('待处理复审', 'waitRecheckNum', 'is-orange'),
+            cell('待审核提醒', 'waitCheckRemindNum', 'is-green')
+        ]
+    ]);
+
+    const rightGroups = computed(() => [
+        [
+            cell('库存预警', 'stockWarnNum', 'is-orange'),
+            cell('库存已满', 'stockFullNum', 'is-green'),
+            cell('24小时未初审', 'noFirstCheckNum', 'is-orange'),
+            cell('回收价>出售价(中等)', 'recyclePriceAbnormalNum', 'is-orange')
+        ],
+        [
+            cell('投诉管理', 'complaintsNum', 'is-orange'),
+            cell('意见反馈', 'feedbackNum', 'is-green'),
+            cell('上报', 'reportNum', 'is-orange'),
+            cell('揽件超时', 'pickupTimeoutNum', 'is-orange')
+        ],
+        [
+            cell('待入库不良单', 'waitBadStockNum', 'is-orange'),
+            cell('待处理工单', 'waitHandleWorkOrderNum', 'is-green')
+        ]
+    ]);
 </script>
+
+<style lang="scss" scoped>
+    .section-title {
+        display: flex;
+        align-items: center;
+        margin-bottom: 16px;
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+    }
+
+    .section-bar {
+        width: 3px;
+        height: 16px;
+        margin-right: 8px;
+        background: #67c23a;
+    }
+
+    .stat-panel {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        margin-bottom: 16px;
+        border: 1px solid #ebeef5;
+        border-radius: 4px;
+
+        &:last-child {
+            margin-bottom: 0;
+        }
+    }
+
+    .stat-cell {
+        padding: 22px 12px 18px;
+        text-align: center;
+
+        &:nth-child(2n) {
+            border-left: 1px solid #ebeef5;
+        }
+
+        &:nth-child(n + 3) {
+            border-top: 1px solid #ebeef5;
+        }
+    }
+
+    .stat-num {
+        font-size: 28px;
+        line-height: 1.2;
+        font-weight: 600;
+        color: #303133;
+    }
+
+    .stat-label {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-top: 8px;
+        color: #606266;
+        font-size: 14px;
+    }
+
+    .stat-dot {
+        width: 8px;
+        height: 8px;
+        margin-right: 6px;
+        border-radius: 50%;
+        flex-shrink: 0;
+
+        &.is-orange {
+            background: #e6a23c;
+        }
+
+        &.is-green {
+            background: #67c23a;
+        }
+    }
+</style>

+ 55 - 7
src/views/index/components/sale-card.vue

@@ -51,13 +51,23 @@
     import { use } from 'echarts/core';
     import { CanvasRenderer } from 'echarts/renderers';
     import { BarChart } from 'echarts/charts';
-    import { GridComponent, TooltipComponent } from 'echarts/components';
+    import {
+        GridComponent,
+        TooltipComponent,
+        LegendComponent
+    } from 'echarts/components';
     import VChart from 'vue-echarts';
     import { useEcharts } from '@/utils/use-echarts';
     import dayjs from 'dayjs';
     const { proxy } = getCurrentInstance();
 
-    use([CanvasRenderer, BarChart, GridComponent, TooltipComponent]);
+    use([
+        CanvasRenderer,
+        BarChart,
+        GridComponent,
+        TooltipComponent,
+        LegendComponent
+    ]);
 
     const saleChartRef = ref(null);
 
@@ -119,21 +129,51 @@
             });
     };
 
+    /** 接口没返回当天时补上,保证当天出现在横轴 */
+    const withToday = (list) => {
+        const rows = Array.isArray(list) ? list.map((item) => ({ ...item })) : [];
+        const today = dayjs();
+        const todayText = today.format('YYYY-MM-DD');
+        const monthText = today.format('YYYY-MM');
+        const lastDate = String(rows[rows.length - 1]?.date || '');
+        const byMonth = /^\d{4}-\d{2}$/.test(lastDate);
+        const currentKey = byMonth ? monthText : todayText;
+        const hasCurrent = rows.some((item) =>
+            String(item.date || '').startsWith(currentKey)
+        );
+        if (!hasCurrent) {
+            rows.push({
+                date: currentKey,
+                orderNum: 0,
+                shopOrderNum: 0
+            });
+        }
+        return rows;
+    };
+
     /** 更新图表配置 */
     const updateChartOption = () => {
-        const dates = orderData.value.map((item) => item.date);
-        const values = orderData.value.map((item) => item.orderNum);
+        const rows = withToday(orderData.value);
+        const dates = rows.map((item) => item.date);
+        const recycleValues = rows.map((item) => Number(item.orderNum) || 0);
+        const shopValues = rows.map((item) => Number(item.shopOrderNum) || 0);
 
         Object.assign(saleChartOption, {
+            color: ['#409EFF', '#3ec6c3'],
             tooltip: {
                 trigger: 'axis',
                 axisPointer: {
                     type: 'shadow'
                 }
             },
+            legend: {
+                top: 8,
+                data: ['回收端', '商城端']
+            },
             grid: {
                 left: '3%',
                 right: '4%',
+                top: 48,
                 bottom: '3%',
                 containLabel: true
             },
@@ -148,14 +188,22 @@
             ],
             yAxis: [
                 {
-                    type: 'value'
+                    type: 'value',
+                    minInterval: 1
                 }
             ],
             series: [
                 {
-                    name: '订单数量',
+                    name: '回收端',
+                    type: 'bar',
+                    barMaxWidth: 18,
+                    data: recycleValues
+                },
+                {
+                    name: '商城端',
                     type: 'bar',
-                    data: values
+                    barMaxWidth: 18,
+                    data: shopValues
                 }
             ]
         });

+ 2 - 0
src/views/index/index.vue

@@ -2,6 +2,7 @@
     <ele-page class="workplace-page">
         <goodsStat :detail="orderStatInfo" />
         <orderStat :detail="orderStatInfo" />
+        <mallStat :detail="orderStatInfo" />
         <saleCard />
     </ele-page>
 </template>
@@ -17,6 +18,7 @@
     import { EleMessage } from 'ele-admin-plus/es';
     import goodsStat from '@/views/index/components/goods-stat.vue';
     import orderStat from '@/views/index/components/order-stat.vue';
+    import mallStat from '@/views/index/components/mall-stat.vue';
     import saleCard from '@/views/index/components/sale-card.vue';
 
     const { proxy } = getCurrentInstance();