Quellcode durchsuchen

订单监测_连接数据库

Gogs vor 1 Monat
Ursprung
Commit
16a4dba479

+ 23 - 1
src/views/order/channel/index.vue

@@ -7,10 +7,20 @@
 
     <div class="chart-card">
       <h3 class="chart-title">商品渠道覆盖 Top 20 趋势</h3>
+      <OrderLoadingPanel
+        v-if="loading"
+        title="正在加载商品渠道数据"
+        detail="正在读取商品在各销售渠道的覆盖情况"
+      />
       <div ref="channelChartRef" style="width: 100%; height: 500px;"></div>
     </div>
 
     <div class="table-card">
+      <OrderLoadingPanel
+        v-if="loading"
+        title="正在加载渠道明细"
+        detail="正在生成商品覆盖平台排名"
+      />
       <div class="table-header">
         <h3 class="chart-title">商品渠道覆盖明细</h3>
         <div class="search-box">
@@ -39,7 +49,10 @@
             <td>{{ item.productCode }}</td>
             <td>{{ item.platformCount }}</td>
           </tr>
-          <tr v-if="!paginatedData.length">
+          <tr v-if="loading">
+            <td colspan="3">正在加载数据...</td>
+          </tr>
+          <tr v-else-if="!paginatedData.length">
             <td colspan="3">没有找到符合条件的数据</td>
           </tr>
         </tbody>
@@ -55,16 +68,21 @@
 
 <script>
 import * as echarts from 'echarts'
+import OrderLoadingPanel from '../components/OrderLoadingPanel.vue'
 import { getShopCrossSellingProducts } from '@/api/order'
 
 export default {
   name: 'OrderChannel',
+  components: {
+    OrderLoadingPanel
+  },
   data() {
     return {
       allProducts: [],
       currentPage: 1,
       itemsPerPage: 10,
       skuKeyword: '',
+      loading: true,
       chartInstance: null
     }
   },
@@ -130,6 +148,7 @@ export default {
       }, true)
     },
     async fetchData() {
+      this.loading = true
       try {
         const response = await getShopCrossSellingProducts({
           skuKeyword: this.skuKeyword || undefined
@@ -145,6 +164,8 @@ export default {
         console.error('获取商品渠道数据失败:', error)
         this.allProducts = []
         this.initLineChart()
+      } finally {
+        this.loading = false
       }
     },
     nextPage() {
@@ -187,6 +208,7 @@ export default {
 
 .chart-card,
 .table-card {
+  position: relative;
   background-color: #fff;
   padding: 20px;
   border-radius: 8px;

+ 213 - 0
src/views/order/components/OrderLoadingPanel.vue

@@ -0,0 +1,213 @@
+<template>
+  <div :class="['order-loading-panel', { compact }]">
+    <div class="loading-visual">
+      <span class="orbit orbit-one"></span>
+      <span class="orbit orbit-two"></span>
+      <span class="center-dot"></span>
+    </div>
+    <div class="loading-copy">
+      <p class="loading-title">{{ title }}</p>
+      <p class="loading-detail">{{ detail }}</p>
+    </div>
+    <div class="progress-track">
+      <span class="progress-bar"></span>
+    </div>
+    <div class="pulse-row">
+      <span></span>
+      <span></span>
+      <span></span>
+      <span></span>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'OrderLoadingPanel',
+  props: {
+    title: {
+      type: String,
+      default: '正在加载数据'
+    },
+    detail: {
+      type: String,
+      default: '正在从数据库读取最新指标,请稍候'
+    },
+    compact: {
+      type: Boolean,
+      default: false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.order-loading-panel {
+  position: absolute;
+  inset: 0;
+  z-index: 20;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 14px;
+  min-height: 220px;
+  padding: 24px;
+  border-radius: inherit;
+  background:
+    linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(247, 251, 255, 0.92)),
+    radial-gradient(circle at 35% 30%, rgba(24, 141, 240, 0.12), transparent 34%);
+  color: #27364a;
+  text-align: center;
+  backdrop-filter: blur(3px);
+}
+
+.order-loading-panel.compact {
+  min-height: 120px;
+  gap: 10px;
+  padding: 16px;
+}
+
+.loading-visual {
+  position: relative;
+  width: 64px;
+  height: 64px;
+}
+
+.compact .loading-visual {
+  width: 46px;
+  height: 46px;
+}
+
+.orbit {
+  position: absolute;
+  inset: 0;
+  border-radius: 50%;
+  border: 3px solid transparent;
+}
+
+.orbit-one {
+  border-top-color: #188df0;
+  border-right-color: rgba(24, 141, 240, 0.24);
+  animation: spin 1s linear infinite;
+}
+
+.orbit-two {
+  inset: 10px;
+  border-left-color: #4ecdc4;
+  border-bottom-color: rgba(78, 205, 196, 0.25);
+  animation: spin 1.4s linear infinite reverse;
+}
+
+.center-dot {
+  position: absolute;
+  inset: 24px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #188df0, #4ecdc4);
+  box-shadow: 0 0 18px rgba(24, 141, 240, 0.36);
+}
+
+.compact .center-dot {
+  inset: 17px;
+}
+
+.loading-copy {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.loading-title {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2d3d;
+}
+
+.compact .loading-title {
+  font-size: 14px;
+}
+
+.loading-detail {
+  margin: 0;
+  font-size: 13px;
+  color: #7a8797;
+}
+
+.compact .loading-detail {
+  font-size: 12px;
+}
+
+.progress-track {
+  width: min(280px, 70%);
+  height: 7px;
+  overflow: hidden;
+  border-radius: 999px;
+  background: #e9f2fb;
+}
+
+.progress-bar {
+  display: block;
+  width: 45%;
+  height: 100%;
+  border-radius: inherit;
+  background: linear-gradient(90deg, #188df0, #4ecdc4);
+  animation: progressMove 1.35s ease-in-out infinite;
+}
+
+.pulse-row {
+  display: flex;
+  gap: 7px;
+}
+
+.pulse-row span {
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  background: #188df0;
+  opacity: 0.35;
+  animation: pulse 1.1s ease-in-out infinite;
+}
+
+.pulse-row span:nth-child(2) {
+  animation-delay: 0.12s;
+}
+
+.pulse-row span:nth-child(3) {
+  animation-delay: 0.24s;
+}
+
+.pulse-row span:nth-child(4) {
+  animation-delay: 0.36s;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes progressMove {
+  0% {
+    transform: translateX(-110%);
+  }
+  50% {
+    transform: translateX(75%);
+  }
+  100% {
+    transform: translateX(230%);
+  }
+}
+
+@keyframes pulse {
+  0%,
+  100% {
+    transform: translateY(0);
+    opacity: 0.28;
+  }
+  50% {
+    transform: translateY(-5px);
+    opacity: 0.9;
+  }
+}
+</style>

+ 14 - 6
src/views/order/efficiency/index.vue

@@ -6,9 +6,11 @@
     </header>
 
     <section class="chart-area">
-      <div v-if="barChart.loading" class="status-overlay">
-        <p>正在加载部门效率数据...</p>
-      </div>
+      <OrderLoadingPanel
+        v-if="barChart.loading"
+        title="正在加载部门效率数据"
+        detail="正在读取各部门平均销售额"
+      />
       <div v-else-if="barChart.message" :class="['status-overlay', { error: barChart.isError }]">
         <p>{{ barChart.title }}</p>
         <p class="error-message">{{ barChart.message }}</p>
@@ -17,9 +19,11 @@
     </section>
 
     <section class="chart-area">
-      <div v-if="pieChart.loading" class="status-overlay">
-        <p>正在加载渠道多样性数据...</p>
-      </div>
+      <OrderLoadingPanel
+        v-if="pieChart.loading"
+        title="正在加载渠道多样性数据"
+        detail="正在读取各渠道商品覆盖结构"
+      />
       <div v-else-if="pieChart.message" :class="['status-overlay', { error: pieChart.isError }]">
         <p>{{ pieChart.title }}</p>
         <p class="error-message">{{ pieChart.message }}</p>
@@ -31,6 +35,7 @@
 
 <script>
 import * as echarts from 'echarts'
+import OrderLoadingPanel from '../components/OrderLoadingPanel.vue'
 import { getShopChannelDiversity, getShopDepartmentEfficiency } from '@/api/order'
 
 const EMPTY_DATA_HINTS = ['未上传', '请先上传', '暂无数据', '无数据', 'not found', 'no data']
@@ -46,6 +51,9 @@ function createChartState(defaultTitle) {
 
 export default {
   name: 'OrderEfficiency',
+  components: {
+    OrderLoadingPanel
+  },
   data() {
     return {
       barChartInstance: null,

+ 22 - 2
src/views/order/ordervalue/FunnelChart/index.vue

@@ -4,6 +4,12 @@
 
     <h3 class="chart-title">支付决策漏斗图</h3>
 
+    <OrderLoadingPanel
+      v-if="loading"
+      title="正在加载支付决策数据"
+      detail="正在读取订单支付状态与响应时长"
+    />
+
     <div ref="funnelChart" style="width: 100%; height: 300px;"></div>
 
   </div>
@@ -15,12 +21,16 @@
 <script>
 import * as echarts from 'echarts';
 import { getOrderPaymentDecisionFunnel } from '@/api/order';
+import OrderLoadingPanel from '../../components/OrderLoadingPanel.vue';
 
 
 
 export default {
 
   name: 'FunnelChart',
+  components: {
+    OrderLoadingPanel
+  },
 
   props: {
 
@@ -42,7 +52,9 @@ export default {
 
       unpaidOrders: 0,
 
-      rawData: {}
+      rawData: {},
+
+      loading: true
 
     };
 
@@ -90,6 +102,8 @@ export default {
 
     async fetchFunnelData(dateRange = null) {
 
+      this.loading = true;
+
       try {
         const params = dateRange && dateRange.start && dateRange.end
           ? { startDate: dateRange.start, endDate: dateRange.end }
@@ -125,6 +139,8 @@ export default {
 
       this.renderChart();
 
+      this.loading = false;
+
     },
 
     renderChart() {
@@ -133,7 +149,7 @@ export default {
 
       if (!this.rawData || !chartEl) return;
 
-      const myChart = echarts.init(chartEl);
+      const myChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl);
 
       const categories = ['30分钟以上', '5-30分钟', '5分钟内', '未支付'];
 
@@ -231,6 +247,10 @@ export default {
 
   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
 
+  position: relative;
+
+  overflow: hidden;
+
 }
 
 .chart-title {

+ 23 - 0
src/views/order/ordervalue/LeakageCard/index.vue

@@ -2,6 +2,13 @@
 
   <div class="leakage-card">
 
+    <OrderLoadingPanel
+      v-if="loading"
+      compact
+      title="正在加载价值漏损数据"
+      detail="正在读取退款金额与成功交易额"
+    />
+
     <div class="card-header">
 
       <h4 class="card-title">订单价值漏损分析 (退款)</h4>
@@ -46,12 +53,16 @@
 
 <script>
 import { getOrderLeakageRate } from '@/api/order';
+import OrderLoadingPanel from '../../components/OrderLoadingPanel.vue';
 
 
 
 export default {
 
   name: 'LeakageCard',
+  components: {
+    OrderLoadingPanel
+  },
 
   props: {
 
@@ -79,6 +90,8 @@ export default {
 
       },
 
+      loading: true,
+
       currencyFormatter: new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' })
 
     };
@@ -111,6 +124,8 @@ export default {
 
     async fetchLeakageData(dateRange = null) {
 
+      this.loading = true;
+
       try {
         const params = dateRange && dateRange.start && dateRange.end
           ? { startDate: dateRange.start, endDate: dateRange.end }
@@ -132,6 +147,10 @@ export default {
 
         console.error('获取漏损数据失败:', error);
 
+      } finally {
+
+        this.loading = false;
+
       }
 
     }
@@ -160,6 +179,10 @@ export default {
 
   border-left: 4px solid #E6A23C;
 
+  position: relative;
+
+  overflow: hidden;
+
 }
 
 .card-header {

+ 19 - 1
src/views/order/ordervalue/Top5PieChart/index.vue

@@ -4,6 +4,12 @@
 
     <h3 class="chart-title">明星商品价值环图 (Top 5)</h3>
 
+    <OrderLoadingPanel
+      v-if="loading"
+      title="正在加载 Top 5 商品数据"
+      detail="正在汇总商品销售额与贡献占比"
+    />
+
     <div ref="pieChart" style="width: 100%; height: 300px;"></div>
 
     
@@ -29,12 +35,16 @@
 <script>
 import * as echarts from 'echarts';
 import { getOrderTop5Percentage, getOrderTop5Products } from '@/api/order';
+import OrderLoadingPanel from '../../components/OrderLoadingPanel.vue';
 
 
 
 export default {
 
   name: 'Top5PieChart',
+  components: {
+    OrderLoadingPanel
+  },
 
   props: {
 
@@ -58,6 +68,8 @@ export default {
 
       top5Percent: 0,
 
+      loading: true,
+
       colors: ['#3366CC', '#4ECDC4', '#A5D8FF', '#FFB347', '#FF6347']
 
     };
@@ -90,6 +102,8 @@ export default {
 
     async fetchPieData(dateRange = null) {
 
+      this.loading = true;
+
       try {
         const params = dateRange && dateRange.start && dateRange.end
           ? { startDate: dateRange.start, endDate: dateRange.end }
@@ -129,6 +143,10 @@ export default {
 
         console.error('获取Top5商品数据失败:', error);
 
+      } finally {
+
+        this.loading = false;
+
       }
 
     },
@@ -139,7 +157,7 @@ export default {
 
       if (!this.chartData.length || !chartEl) return;
 
-      const myChart = echarts.init(chartEl);
+      const myChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl);
 
       const option = {
 

+ 19 - 66
src/views/order/ordervalue/index.vue

@@ -22,21 +22,16 @@
           <button :class="['time-tab', { active: activeTab === 'lm' }]" @click="selectDateRange('lm')">上月</button>
           <button :class="['time-tab', { active: activeTab === 'all' }]" @click="selectDateRange('all')">全量数据</button>
         </div>
-        <button class="upload-btn" :disabled="uploadingOrder" @click="triggerOrderUpload">
-          {{ uploadingOrder ? '上传中...' : '上传CSV导入' }}
-        </button>
-        <input
-          ref="orderUploadInput"
-          type="file"
-          accept=".csv"
-          multiple
-          style="display: none;"
-          @change="handleOrderUploadChange"
-        >
       </div>
     </header>
 
     <section class="kpi-cards-grid">
+      <OrderLoadingPanel
+        v-if="dashboardLoading"
+        compact
+        title="正在加载核心指标"
+        detail="正在读取 GMV、贡献比与支付响应"
+      />
       <KpiCard title="总交易额 (GMV)" :value="kpiData.gmv" :trend="kpiData.gmvTrend" :trend-color="getTrendColor(kpiData.gmvTrend)" icon="$" />
       <KpiCard title="P80 订单贡献比" :value="kpiData.p80Contribution" :trend="kpiData.p80Trend" :trend-color="getTrendColor(kpiData.p80Trend)" icon="P" />
       <KpiCard title="Top 5 商品贡献比" :value="kpiData.top5Contribution" :trend="kpiData.top5Trend" :trend-color="getTrendColor(kpiData.top5Trend)" icon="5" />
@@ -65,13 +60,13 @@ import KpiCard from './KpiCard/index.vue'
 import FunnelChart from './FunnelChart/index.vue'
 import Top5PieChart from './Top5PieChart/index.vue'
 import LeakageCard from './LeakageCard/index.vue'
+import OrderLoadingPanel from '../components/OrderLoadingPanel.vue'
 import {
   getOrderAveragePaymentTime,
   getOrderGmv,
   getOrderMaxDate,
   getOrderRBig,
-  getOrderTop5Percentage,
-  uploadOrderValueFiles
+  getOrderTop5Percentage
 } from '@/api/order'
 
 export default {
@@ -80,7 +75,8 @@ export default {
     KpiCard,
     FunnelChart,
     Top5PieChart,
-    LeakageCard
+    LeakageCard,
+    OrderLoadingPanel
   },
   data() {
     return {
@@ -88,7 +84,7 @@ export default {
       activeTab: '7d',
       maxDate: '',
       currentDateRange: { start: '', end: '' },
-      uploadingOrder: false,
+      dashboardLoading: true,
       pickerOptions: {
         disabledDate: time => {
           if (!this.maxDate) return false
@@ -111,42 +107,6 @@ export default {
     this.initDashboard()
   },
   methods: {
-    triggerOrderUpload() {
-      if (!this.uploadingOrder && this.$refs.orderUploadInput) {
-        this.$refs.orderUploadInput.click()
-      }
-    },
-    async handleOrderUploadChange(event) {
-      const files = Array.from(event?.target?.files || [])
-      if (!files.length) return
-      const invalid = files.find(file => !String(file.name || '').toLowerCase().endsWith('.csv'))
-      if (invalid) {
-        this.$modal.msgError('仅支持上传 CSV 文件')
-        event.target.value = ''
-        return
-      }
-      await this.uploadOrderFiles(files)
-      event.target.value = ''
-    },
-    async uploadOrderFiles(files) {
-      this.uploadingOrder = true
-      try {
-        const formData = new FormData()
-        files.forEach(file => formData.append('files', file))
-        const data = await uploadOrderValueFiles(formData)
-        if (data?.success) {
-          this.$modal.msgSuccess(data.message || '上传并导入成功')
-          await this.initDashboard()
-          return
-        }
-        this.$modal.msgError(data?.message || '上传导入失败')
-      } catch (error) {
-        const msg = error?.response?.data?.message || error?.message || '上传导入失败'
-        this.$modal.msgError(msg)
-      } finally {
-        this.uploadingOrder = false
-      }
-    },
     handleDateChange() {
       if (!this.selectedDate) {
         this.selectedDate = this.maxDate
@@ -214,6 +174,7 @@ export default {
       }
     },
     async fetchAllApiData(customRange = null) {
+      this.dashboardLoading = true
       try {
         const { start, end } = customRange || this.getCurrentRange()
         const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
@@ -243,6 +204,9 @@ export default {
         console.error('获取订单价值数据失败:', error)
       }
     },
+    finishDashboardLoading() {
+      this.dashboardLoading = false
+    },
     calculateTrend(currentValue, previousValue) {
       if (previousValue === 0) return '+0%'
       const change = ((currentValue - previousValue) / previousValue) * 100
@@ -314,8 +278,10 @@ export default {
           this.kpiData.top5Trend = this.calculateTrend(currentTop5, previousMonthData.top5)
           this.kpiData.avgTimeTrend = this.calculateTrend(currentAvgTime, previousMonthData.avgTime)
         }
+        this.finishDashboardLoading()
       } else {
         await this.fetchAllApiData(range)
+        this.finishDashboardLoading()
       }
     }
   }
@@ -389,25 +355,12 @@ export default {
   border-color: #188df0;
 }
 
-.upload-btn {
-  padding: 6px 12px;
-  border: 1px solid #188df0;
-  background-color: #188df0;
-  color: #fff;
-  border-radius: 4px;
-  cursor: pointer;
-  font-size: 13px;
-}
-
-.upload-btn:disabled {
-  opacity: 0.7;
-  cursor: not-allowed;
-}
-
 .kpi-cards-grid {
+  position: relative;
   display: grid;
   grid-template-columns: repeat(4, 1fr);
   gap: 20px;
+  min-height: 146px;
 }
 
 .charts-area {

+ 16 - 4
src/views/order/related/index.vue

@@ -6,6 +6,11 @@
     </header>
 
     <div class="table-container">
+      <OrderLoadingPanel
+        v-if="loading"
+        title="正在加载商品关联数据"
+        detail="正在读取共同购买关系与热门组合"
+      />
       <div class="table-header">
         <h2 class="table-title">热门商品组合</h2>
         <div class="search-box">
@@ -29,10 +34,7 @@
           </tr>
         </thead>
         <tbody>
-          <tr v-if="loading">
-            <td colspan="3">正在加载数据...</td>
-          </tr>
-          <tr v-else-if="paginatedData.length > 0" v-for="(item, index) in paginatedData" :key="index" class="data-row">
+          <tr v-if="paginatedData.length > 0" v-for="(item, index) in paginatedData" :key="index" class="data-row">
             <td class="sku-cell" :title="`商品A: ${item.productA}`">{{ item.productAId }}</td>
             <td class="sku-cell" :title="`商品B: ${item.productB}`">{{ item.productBId }}</td>
             <td class="count-cell">
@@ -54,6 +56,11 @@
 
     <div class="chart-container">
       <h2 class="chart-title">共购规则网络图</h2>
+      <OrderLoadingPanel
+        v-if="loading"
+        title="正在生成共购网络图"
+        detail="正在计算商品节点与共同购买关系"
+      />
       <div ref="networkChart" style="width: 100%; height: 600px;"></div>
     </div>
   </div>
@@ -61,10 +68,14 @@
 
 <script>
 import * as echarts from 'echarts'
+import OrderLoadingPanel from '../components/OrderLoadingPanel.vue'
 import { getOrderCoPurchase } from '@/api/order'
 
 export default {
   name: 'OrderRelated',
+  components: {
+    OrderLoadingPanel
+  },
   data() {
     return {
       coPurchaseData: [],
@@ -260,6 +271,7 @@ export default {
 .page-header,
 .table-container,
 .chart-container {
+  position: relative;
   background-color: #fff;
   padding: 20px;
   border-radius: 8px;

+ 37 - 81
src/views/order/shopvalue/index.vue

@@ -28,21 +28,12 @@
       </div>
     </header>
 
-    <div class="upload-row">
-      <button class="btn-upload" :disabled="uploadingShop" @click="triggerShopUpload">
-        {{ uploadingShop ? '上传中...' : '上传CSV导入' }}
-      </button>
-      <input
-        ref="shopUploadInput"
-        type="file"
-        accept=".csv"
-        multiple
-        style="display: none;"
-        @change="handleShopUploadChange"
-      >
-    </div>
-
     <div class="chart-card">
+      <OrderLoadingPanel
+        v-if="topProductLoading"
+        title="正在加载店铺价值指标"
+        detail="正在读取 Top 5 商品销售额与贡献占比"
+      />
       <div class="top-product-contribution-container">
         <div class="kpi-card-grid">
           <div class="kpi-card">
@@ -68,6 +59,11 @@
 
     <div class="charts-container">
       <div class="chart-card">
+        <OrderLoadingPanel
+          v-if="chartsLoading"
+          title="正在加载部门业绩数据"
+          detail="正在汇总各部门销量与销售额"
+        />
         <div class="card-header">
           <h3 class="chart-title">各部门业绩分析(销量 / 销售额)</h3>
         </div>
@@ -77,6 +73,11 @@
       </div>
 
       <div class="chart-card">
+        <OrderLoadingPanel
+          v-if="chartsLoading"
+          title="正在加载渠道业绩数据"
+          detail="正在汇总各渠道销量与销售额"
+        />
         <div class="card-header">
           <h3 class="chart-title">各渠道业绩分析(销量 / 销售额)</h3>
         </div>
@@ -86,6 +87,11 @@
       </div>
 
       <div class="chart-card">
+        <OrderLoadingPanel
+          v-if="chartsLoading"
+          title="正在加载平台价值数据"
+          detail="正在分析各平台销量与平均订单价值"
+        />
         <div class="card-header">
           <h3 class="chart-title">各平台价值分析(销量 / 销售额)</h3>
         </div>
@@ -99,18 +105,21 @@
 
 <script>
 import * as echarts from 'echarts'
+import OrderLoadingPanel from '../components/OrderLoadingPanel.vue'
 import {
   getShopChannelContribution,
   getShopChannelRoiValue,
   getShopChannelTotalContribution,
   getShopMaxDate,
   getShopTopProductContribution,
-  getShopUnitContribution,
-  uploadShopValueFiles
+  getShopUnitContribution
 } from '@/api/order'
 
 export default {
   name: 'ShopValue',
+  components: {
+    OrderLoadingPanel
+  },
   data() {
     return {
       topProductData: {
@@ -119,7 +128,8 @@ export default {
         contributionRatio: 0,
         top5Products: []
       },
-      uploadingShop: false,
+      topProductLoading: true,
+      chartsLoading: true,
       selectedDate: '',
       maxDate: '',
       activeTab: '7d',
@@ -146,48 +156,6 @@ export default {
         endDate: this.currentDateRange.end
       }
     },
-    triggerShopUpload() {
-      if (!this.uploadingShop && this.$refs.shopUploadInput) {
-        this.$refs.shopUploadInput.click()
-      }
-    },
-    async handleShopUploadChange(event) {
-      const files = Array.from(event?.target?.files || [])
-      if (!files.length) return
-      const invalid = files.find(file => !String(file.name || '').toLowerCase().endsWith('.csv'))
-      if (invalid) {
-        this.$modal.msgError('仅支持上传 CSV 文件')
-        event.target.value = ''
-        return
-      }
-      await this.uploadShopFiles(files)
-      event.target.value = ''
-    },
-    async uploadShopFiles(files) {
-      this.uploadingShop = true
-      try {
-        const formData = new FormData()
-        files.forEach(file => formData.append('files', file))
-        const data = await uploadShopValueFiles(formData)
-        if (data?.success) {
-          this.$modal.msgSuccess(data.message || '上传并导入成功')
-          await this.initDashboard()
-          return
-        }
-        if (data?.debug) {
-          console.error('[shop-upload-debug]', data.debug)
-        }
-        this.$modal.msgError(data?.message || '上传导入失败')
-      } catch (error) {
-        if (error?.response?.data?.debug) {
-          console.error('[shop-upload-debug]', error.response.data.debug)
-        }
-        const msg = error?.response?.data?.message || error?.message || '上传导入失败'
-        this.$modal.msgError(msg)
-      } finally {
-        this.uploadingShop = false
-      }
-    },
     handleDateChange() {
       if (!this.selectedDate) {
         this.selectedDate = this.maxDate
@@ -324,6 +292,7 @@ export default {
       }, true)
     },
     async fetchTopProductData() {
+      this.topProductLoading = true
       try {
         const response = await getShopTopProductContribution(this.getQueryParams())
         if (!response?.success) return
@@ -342,6 +311,9 @@ export default {
         console.error('获取 Top 5 商品贡献失败:', error)
       }
     },
+    finishTopProductLoading() {
+      this.topProductLoading = false
+    },
     initDualIndicatorBarChart(chartEl, data, categoryKey, seriesConfig) {
       if (!chartEl) return
       const spacePerBar = 120
@@ -451,6 +423,7 @@ export default {
       }, true)
     },
     async fetchData() {
+      this.chartsLoading = true
       try {
         const params = this.getQueryParams()
         const [unitRes, channelTotalRes, channelContributionRes, channelRoiValueRes] = await Promise.all([
@@ -504,9 +477,13 @@ export default {
         }
 
         await this.fetchTopProductData()
+        this.finishTopProductLoading()
+        this.chartsLoading = false
       } catch (error) {
         console.error('获取店铺价值数据失败:', error)
       }
+      this.chartsLoading = false
+      this.finishTopProductLoading()
     }
   }
 }
@@ -575,28 +552,6 @@ export default {
   border-color: #188df0;
 }
 
-.upload-row {
-  display: flex;
-  justify-content: flex-end;
-  margin-bottom: 20px;
-}
-
-.btn-upload {
-  background-color: #2a7f62;
-  color: #fff;
-  border: none;
-  padding: 12px 28px;
-  border-radius: 8px;
-  cursor: pointer;
-  font-size: 16px;
-  font-weight: 600;
-}
-
-.btn-upload:disabled {
-  opacity: 0.7;
-  cursor: not-allowed;
-}
-
 .charts-container {
   display: flex;
   flex-direction: column;
@@ -604,6 +559,7 @@ export default {
 }
 
 .chart-card {
+  position: relative;
   background: #fff;
   padding: 24px;
   border-radius: 4px;