소스 검색

订单监测_筛选功能

Gogs 2 달 전
부모
커밋
f1071ef0c9
5개의 변경된 파일999개의 추가작업 그리고 1360개의 파일을 삭제
  1. 34 18
      src/api/order.js
  2. 107 53
      src/views/order/channel/index.vue
  3. 217 785
      src/views/order/ordervalue/index.vue
  4. 267 235
      src/views/order/related/index.vue
  5. 374 269
      src/views/order/shopvalue/index.vue

+ 34 - 18
src/api/order.js

@@ -76,10 +76,11 @@ export function getOrderPaymentDecisionFunnel(params) {
   })
 }
 
-export function getOrderCoPurchase() {
+export function getOrderCoPurchase(params) {
   return request({
     url: '/api/analysis/co-purchase',
-    method: 'get'
+    method: 'get',
+    params
   })
 }
 
@@ -96,58 +97,73 @@ export function uploadShopValueFiles(data) {
   })
 }
 
-export function getShopTopProductContribution() {
+export function getShopMaxDate() {
   return request({
-    url: '/api/shop/import/top-product-contribution',
+    url: '/api/shop/import/max-date',
     method: 'get'
   })
 }
 
-export function getShopUnitContribution() {
+export function getShopTopProductContribution(params) {
+  return request({
+    url: '/api/shop/import/top-product-contribution',
+    method: 'get',
+    params
+  })
+}
+
+export function getShopUnitContribution(params) {
   return request({
     url: '/api/shop/import/unit-contribution',
-    method: 'get'
+    method: 'get',
+    params
   })
 }
 
-export function getShopChannelTotalContribution() {
+export function getShopChannelTotalContribution(params) {
   return request({
     url: '/api/shop/import/channel-total-contribution',
-    method: 'get'
+    method: 'get',
+    params
   })
 }
 
-export function getShopChannelContribution() {
+export function getShopChannelContribution(params) {
   return request({
     url: '/api/shop/import/channel-contribution',
-    method: 'get'
+    method: 'get',
+    params
   })
 }
 
-export function getShopChannelRoiValue() {
+export function getShopChannelRoiValue(params) {
   return request({
     url: '/api/shop/import/channel-roi-value',
-    method: 'get'
+    method: 'get',
+    params
   })
 }
 
-export function getShopCrossSellingProducts() {
+export function getShopCrossSellingProducts(params) {
   return request({
     url: '/api/shop/import/cross-selling-products',
-    method: 'get'
+    method: 'get',
+    params
   })
 }
 
-export function getShopDepartmentEfficiency() {
+export function getShopDepartmentEfficiency(params) {
   return request({
     url: '/api/shop/import/department-efficiency',
-    method: 'get'
+    method: 'get',
+    params
   })
 }
 
-export function getShopChannelDiversity() {
+export function getShopChannelDiversity(params) {
   return request({
     url: '/api/shop/import/channel-diversity',
-    method: 'get'
+    method: 'get',
+    params
   })
 }

+ 107 - 53
src/views/order/channel/index.vue

@@ -1,20 +1,30 @@
-<template>
+<template>
   <div class="page-container">
-    <!-- 1. 顶部标题 -->
     <header class="page-header">
       <h1 class="main-title">商品渠道透视</h1>
       <p class="subtitle">分析商品在不同销售渠道的覆盖广度</p>
     </header>
 
-    <!-- 2. 可视化图表 -->
     <div class="chart-card">
       <h3 class="chart-title">商品渠道覆盖 Top 20 趋势</h3>
       <div ref="channelChartRef" style="width: 100%; height: 500px;"></div>
     </div>
 
-    <!-- 3. 数据表格 -->
     <div class="table-card">
-      <h3 class="chart-title">商品渠道覆盖明细</h3>
+      <div class="table-header">
+        <h3 class="chart-title">商品渠道覆盖明细</h3>
+        <div class="search-box">
+          <el-input
+            v-model.trim="skuKeyword"
+            clearable
+            placeholder="输入 SKU 搜索"
+            @keyup.enter.native="applySkuSearch"
+            @clear="applySkuSearch"
+          >
+            <el-button slot="append" icon="el-icon-search" @click="applySkuSearch" />
+          </el-input>
+        </div>
+      </div>
       <table class="data-table">
         <thead>
           <tr>
@@ -29,9 +39,11 @@
             <td>{{ item.productCode }}</td>
             <td>{{ item.platformCount }}</td>
           </tr>
+          <tr v-if="!paginatedData.length">
+            <td colspan="3">没有找到符合条件的数据</td>
+          </tr>
         </tbody>
       </table>
-      <!-- 分页控制器 -->
       <div class="pagination-controls">
         <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
         <span>第 {{ currentPage }} / {{ totalPages }} 页</span>
@@ -42,8 +54,8 @@
 </template>
 
 <script>
-import * as echarts from 'echarts';
-import { getShopCrossSellingProducts } from '@/api/order';
+import * as echarts from 'echarts'
+import { getShopCrossSellingProducts } from '@/api/order'
 
 export default {
   name: 'OrderChannel',
@@ -51,30 +63,45 @@ export default {
     return {
       allProducts: [],
       currentPage: 1,
-      itemsPerPage: 10
-    };
+      itemsPerPage: 10,
+      skuKeyword: '',
+      chartInstance: null
+    }
   },
   computed: {
     paginatedData() {
-      const start = (this.currentPage - 1) * this.itemsPerPage;
-      const end = start + this.itemsPerPage;
-      return this.allProducts.slice(start, end);
+      const start = (this.currentPage - 1) * this.itemsPerPage
+      return this.allProducts.slice(start, start + this.itemsPerPage)
     },
     totalPages() {
-      if (this.allProducts.length === 0) return 1;
-      return Math.ceil(this.allProducts.length / this.itemsPerPage);
+      return Math.max(1, Math.ceil(this.allProducts.length / this.itemsPerPage))
     }
   },
   mounted() {
-    this.fetchData();
+    this.fetchData()
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    this.chartInstance?.dispose()
   },
   methods: {
+    handleResize() {
+      this.chartInstance?.resize()
+    },
+    applySkuSearch() {
+      this.currentPage = 1
+      this.fetchData()
+    },
     initLineChart() {
-      const chartEl = this.$refs.channelChartRef;
-      if (!chartEl) return;
-      const myChart = echarts.init(chartEl);
-      const top20Data = this.allProducts.slice(0, 20);
-      const option = {
+      const chartEl = this.$refs.channelChartRef
+      if (!chartEl) return
+      if (this.chartInstance) {
+        this.chartInstance.dispose()
+      }
+      this.chartInstance = echarts.init(chartEl)
+      const top20Data = this.allProducts.slice(0, 20)
+      this.chartInstance.setOption({
         tooltip: { trigger: 'axis' },
         grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
         xAxis: {
@@ -87,51 +114,53 @@ export default {
           type: 'value',
           name: '覆盖平台数'
         },
-        series: [
-          {
-            name: '覆盖平台数',
-            type: 'line',
-            smooth: true,
-            data: top20Data.map(item => item.platformCount),
-            itemStyle: { color: '#5470C6' },
-            areaStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgba(84, 112, 198, 0.5)' },
-                { offset: 1, color: 'rgba(84, 112, 198, 0)' }
-              ])
-            }
+        series: [{
+          name: '覆盖平台数',
+          type: 'line',
+          smooth: true,
+          data: top20Data.map(item => item.platformCount),
+          itemStyle: { color: '#5470C6' },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(84, 112, 198, 0.5)' },
+              { offset: 1, color: 'rgba(84, 112, 198, 0)' }
+            ])
           }
-        ]
-      };
-      myChart.setOption(option);
-      window.addEventListener('resize', () => myChart.resize());
+        }]
+      }, true)
     },
     async fetchData() {
       try {
-        const response = await getShopCrossSellingProducts();
-        if (response.success) {
-          this.allProducts = response.data || [];
-          this.initLineChart();
+        const response = await getShopCrossSellingProducts({
+          skuKeyword: this.skuKeyword || undefined
+        })
+        if (response?.success) {
+          this.allProducts = response.data || []
+          this.initLineChart()
+          return
         }
+        this.allProducts = []
+        this.initLineChart()
       } catch (error) {
-        console.error('获取商品渠道数据失败:', error);
+        console.error('获取商品渠道数据失败:', error)
+        this.allProducts = []
+        this.initLineChart()
       }
     },
     nextPage() {
       if (this.currentPage < this.totalPages) {
-        this.currentPage += 1;
+        this.currentPage += 1
       }
     },
     prevPage() {
       if (this.currentPage > 1) {
-        this.currentPage -= 1;
+        this.currentPage -= 1
       }
     }
   }
-};
+}
 </script>
 
-
 <style scoped>
 .page-container {
   padding: 20px;
@@ -140,42 +169,64 @@ export default {
   gap: 20px;
   background-color: #f0f2f5;
 }
+
 .page-header {
   margin-bottom: 10px;
 }
+
 .main-title {
   font-size: 24px;
   font-weight: 600;
   color: #333;
 }
+
 .subtitle {
   font-size: 14px;
   color: #999;
 }
-.chart-card, .table-card {
+
+.chart-card,
+.table-card {
   background-color: #fff;
   padding: 20px;
   border-radius: 8px;
-  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
 }
+
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 16px;
+  margin-bottom: 20px;
+}
+
+.search-box {
+  width: 280px;
+}
+
 .chart-title {
   font-size: 18px;
   color: #333;
-  margin-bottom: 20px;
 }
+
 .data-table {
   width: 100%;
   border-collapse: collapse;
 }
-.data-table th, .data-table td {
+
+.data-table th,
+.data-table td {
   padding: 12px 15px;
   border: 1px solid #e0e0e0;
   text-align: left;
 }
+
 .data-table th {
   background-color: #f7f7f7;
   font-weight: 600;
 }
+
 .pagination-controls {
   margin-top: 20px;
   display: flex;
@@ -183,6 +234,7 @@ export default {
   align-items: center;
   gap: 15px;
 }
+
 .pagination-controls button {
   padding: 8px 12px;
   border: 1px solid #ccc;
@@ -191,10 +243,12 @@ export default {
   cursor: pointer;
   transition: all 0.2s;
 }
+
 .pagination-controls button:hover:not(:disabled) {
-    border-color: #5470C6;
-    color: #5470C6;
+  border-color: #5470C6;
+  color: #5470C6;
 }
+
 .pagination-controls button:disabled {
   cursor: not-allowed;
   opacity: 0.5;

+ 217 - 785
src/views/order/ordervalue/index.vue

@@ -1,103 +1,70 @@
 <template>
-
   <div class="order-value-view">
-
-    <!-- 头部区域 -->
-
     <header class="page-header">
-
       <h1 class="page-title">订单价值</h1>
-
       <div class="header-controls">
-
         <span class="control-label">时光回溯控制器</span>
-
+        <el-date-picker
+          v-model="selectedDate"
+          class="date-picker"
+          type="date"
+          value-format="yyyy-MM-dd"
+          :clearable="false"
+          :editable="false"
+          :picker-options="pickerOptions"
+          placeholder="选择上传数据日期"
+          @change="handleDateChange"
+        />
         <div class="control-item-group">
-
-      <button :class="{'time-tab': true, 'active': activeTab === '7d'}" @click="selectDateRange('7d')">最近7天</button>
-
-      <button :class="{'time-tab': true, 'active': activeTab === 'tm'}" @click="selectDateRange('tm')">本月</button>
-
-      <button :class="{'time-tab': true, 'active': activeTab === 'lm'}" @click="selectDateRange('lm')">上月</button>
-
-      <button :class="{'time-tab': true, '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>
-
+          <button :class="['time-tab', { active: activeTab === 'day' }]" @click="selectDateRange('day')">当天</button>
+          <button :class="['time-tab', { active: activeTab === '7d' }]" @click="selectDateRange('7d')">最近7天</button>
+          <button :class="['time-tab', { active: activeTab === 'tm' }]" @click="selectDateRange('tm')">本月</button>
+          <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>
 
-
-
-    <!-- ✨【修改点1:KPI卡片的值现在是动态的】✨ -->
-
     <section class="kpi-cards-grid">
-
-      <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="🏆" />
-
-      <KpiCard title="Top 5 商品贡献比" :value="kpiData.top5Contribution" :trend="kpiData.top5Trend" :trend-color="getTrendColor(kpiData.top5Trend)" icon="🔥" />
-
-      <KpiCard title="平均支付响应" :value="kpiData.averagePaymentTime" :trend="kpiData.avgTimeTrend" :trend-color="getTrendColor(kpiData.avgTimeTrend)" icon="⏱" />
-
+      <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" />
+      <KpiCard title="平均支付响应" :value="kpiData.averagePaymentTime" :trend="kpiData.avgTimeTrend" :trend-color="getTrendColor(kpiData.avgTimeTrend)" icon="T" />
     </section>
 
-    
-
-    <!-- 图表区域 -->
-
     <section class="charts-area">
-
-      <!-- 支付决策漏斗图 -->
-
       <div class="chart-wrapper funnel-chart-wrapper">
-
-        <FunnelChart :date-range="currentDateRange" /> 
-
+        <FunnelChart :date-range="currentDateRange" />
       </div>
-
-      
-
-      <!-- 明星商品价值环图 (Top 5) -->
-
       <div class="chart-wrapper">
-
-        <Top5PieChart :date-range="currentDateRange" /> 
-
+        <Top5PieChart :date-range="currentDateRange" />
       </div>
-
     </section>
 
-    
-
-    <!-- 订单价值漏损分析 (退款) - 铺满整排 -->
-
     <section class="leakage-section">
-
       <div class="chart-wrapper leakage-card-wrapper full-width">
-
         <LeakageCard :date-range="currentDateRange" />
-
       </div>
-
     </section>
-
   </div>
-
 </template>
 
-
-
 <script>
-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 KpiCard from './KpiCard/index.vue'
+import FunnelChart from './FunnelChart/index.vue'
+import Top5PieChart from './Top5PieChart/index.vue'
+import LeakageCard from './LeakageCard/index.vue'
 import {
   getOrderAveragePaymentTime,
   getOrderGmv,
@@ -105,897 +72,362 @@ import {
   getOrderRBig,
   getOrderTop5Percentage,
   uploadOrderValueFiles
-} from '@/api/order';
-
-
+} from '@/api/order'
 
 export default {
-
   name: 'OrderValue',
-
   components: {
-
     KpiCard,
-
     FunnelChart,
-
     Top5PieChart,
-
     LeakageCard
-
   },
-
   data() {
-
     return {
-
-      selectedDate: new Date().toISOString().split('T')[0],
-
+      selectedDate: '',
       activeTab: '7d',
-
       maxDate: '',
-
       currentDateRange: { start: '', end: '' },
-
       uploadingOrder: false,
-
+      pickerOptions: {
+        disabledDate: time => {
+          if (!this.maxDate) return false
+          return time.getTime() > new Date(`${this.maxDate}T23:59:59`).getTime()
+        }
+      },
       kpiData: {
-
-        gmv: '?0',
-
+        gmv: '¥0',
         gmvTrend: '+0%',
-
         p80Contribution: '0%',
-
         p80Trend: '+0%',
-
         top5Contribution: '0%',
-
         top5Trend: '+0%',
-
         averagePaymentTime: '00:00 秒',
-
         avgTimeTrend: '+0%'
-
-      }
-
-    };
-
-  },
-
-  watch: {
-
-    selectedDate(newVal) {
-
-      if (this.activeTab === '7d') {
-
-        const startDate = this.formatYmd(this.addDays(newVal, -6));
-
-        const endDate = this.formatYmd(newVal);
-
-        this.currentDateRange = { start: startDate, end: endDate };
-
-        this.fetchAllApiData({ start: startDate, end: endDate });
-
-      } else {
-
-        this.selectDateRange(this.activeTab);
-
       }
-
     }
-
   },
-
   mounted() {
-
-    this.initDashboard();
-
+    this.initDashboard()
   },
-
   methods: {
-
     triggerOrderUpload() {
-
-      if (this.uploadingOrder) {
-
-        return;
-
-      }
-
-      if (this.$refs.orderUploadInput) {
-
-        this.$refs.orderUploadInput.click();
-
+      if (!this.uploadingOrder && this.$refs.orderUploadInput) {
+        this.$refs.orderUploadInput.click()
       }
-
     },
-
     async handleOrderUploadChange(event) {
-
-      const files = Array.from((event && event.target && event.target.files) || []);
-
-      if (!files.length) {
-
-        return;
-
-      }
-
-      const invalid = files.find(file => !String(file.name || '').toLowerCase().endsWith('.csv'));
-
+      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;
-
+        this.$modal.msgError('仅支持上传 CSV 文件')
+        event.target.value = ''
+        return
       }
-
-      await this.uploadOrderFiles(files);
-
-      event.target.value = '';
-
+      await this.uploadOrderFiles(files)
+      event.target.value = ''
     },
-
     async uploadOrderFiles(files) {
-
-      this.uploadingOrder = true;
-
+      this.uploadingOrder = true
       try {
-
-        const formData = new FormData();
-
-        files.forEach(file => formData.append('files', file));
-
-        const data = await uploadOrderValueFiles(formData);
-
-        if (data && data.success) {
-
-          this.$modal.msgSuccess(data.message || '上传并导入成功');
-
-          await this.initDashboard();
-
-          return;
-
+        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 && data.message) || '上传导入失败');
-
+        this.$modal.msgError(data?.message || '上传导入失败')
       } catch (error) {
-
-        const msg = error && error.response && error.response.data && error.response.data.message
-
-          ? error.response.data.message
-
-          : (error && error.message ? error.message : '上传导入失败');
-
-        this.$modal.msgError(msg);
-
+        const msg = error?.response?.data?.message || error?.message || '上传导入失败'
+        this.$modal.msgError(msg)
       } finally {
-
-        this.uploadingOrder = false;
-
+        this.uploadingOrder = false
       }
-
     },
-
+    handleDateChange() {
+      if (!this.selectedDate) {
+        this.selectedDate = this.maxDate
+      }
+      this.selectDateRange(this.activeTab)
+    },
     pad2(value) {
-
-      return String(value).padStart(2, '0');
-
+      return String(value).padStart(2, '0')
     },
-
     toDate(value) {
-
-      if (value instanceof Date) return new Date(value.getTime());
-
-      if (!value) return new Date();
-
-      return new Date(`${value}T00:00:00`);
-
+      if (value instanceof Date) return new Date(value.getTime())
+      if (!value) return new Date()
+      return new Date(`${value}T00:00:00`)
     },
-
     formatYmd(value) {
-
-      const date = this.toDate(value);
-
-      return `${date.getFullYear()}-${this.pad2(date.getMonth() + 1)}-${this.pad2(date.getDate())}`;
-
+      const date = this.toDate(value)
+      return `${date.getFullYear()}-${this.pad2(date.getMonth() + 1)}-${this.pad2(date.getDate())}`
     },
-
     addDays(value, days) {
-
-      const date = this.toDate(value);
-
-      date.setDate(date.getDate() + days);
-
-      return date;
-
+      const date = this.toDate(value)
+      date.setDate(date.getDate() + days)
+      return date
     },
-
     addMonths(value, months) {
-
-      const date = this.toDate(value);
-
-      date.setMonth(date.getMonth() + months);
-
-      return date;
-
+      const date = this.toDate(value)
+      date.setMonth(date.getMonth() + months)
+      return date
     },
-
     startOfMonth(value) {
-
-      const date = this.toDate(value);
-
-      return new Date(date.getFullYear(), date.getMonth(), 1);
-
+      const date = this.toDate(value)
+      return new Date(date.getFullYear(), date.getMonth(), 1)
     },
-
     endOfMonth(value) {
-
-      const date = this.toDate(value);
-
-      return new Date(date.getFullYear(), date.getMonth() + 1, 0);
-
+      const date = this.toDate(value)
+      return new Date(date.getFullYear(), date.getMonth() + 1, 0)
+    },
+    getCurrentRange(type = this.activeTab) {
+      const baseDate = this.toDate(this.selectedDate || this.maxDate || new Date())
+      if (type === 'day') {
+        const day = this.formatYmd(baseDate)
+        return { start: day, end: day }
+      }
+      if (type === '7d') {
+        return {
+          start: this.formatYmd(this.addDays(baseDate, -6)),
+          end: this.formatYmd(baseDate)
+        }
+      }
+      if (type === 'tm') {
+        return {
+          start: this.formatYmd(this.startOfMonth(baseDate)),
+          end: this.formatYmd(this.endOfMonth(baseDate))
+        }
+      }
+      if (type === 'lm') {
+        const lastMonth = this.addMonths(baseDate, -1)
+        return {
+          start: this.formatYmd(this.startOfMonth(lastMonth)),
+          end: this.formatYmd(this.endOfMonth(lastMonth))
+        }
+      }
+      return {
+        start: '2022-01-01',
+        end: this.maxDate || this.formatYmd(baseDate)
+      }
     },
-
     async fetchAllApiData(customRange = null) {
-
       try {
-
-        let startDate;
-
-        let endDate;
-
-        if (customRange && customRange.start && customRange.end) {
-
-          startDate = customRange.start;
-
-          endDate = customRange.end;
-
-        } else {
-
-          endDate = this.selectedDate;
-
-          startDate = this.formatYmd(this.addDays(endDate, -6));
-
-        }
-
-
-
+        const { start, end } = customRange || this.getCurrentRange()
         const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
-
-          getOrderGmv({ startDate, endDate }),
-
-          getOrderRBig({ startDate, endDate }),
-
-          getOrderTop5Percentage({ startDate, endDate }),
-
-          getOrderAveragePaymentTime({ startDate, endDate })
-
-        ]);
-
-
-
-        const gmvValue = gmvRes || 0;
-
-        this.kpiData.gmv = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(gmvValue);
-
-
-
-        const p80Value = p80Res?.rBigRatio || 0;
-
-        this.kpiData.p80Contribution = `${Math.round(p80Value)}%`;
-
-
-
-        const top5Value = top5Res?.data?.top5Percentage || 0;
-
-        this.kpiData.top5Contribution = `${Math.round(top5Value)}%`;
-
-
-
-        const avgSeconds = avgTimeRes?.averagePaymentSeconds || 0;
-
-        const minutes = Math.floor(avgSeconds / 60);
-
-        const seconds = Math.round(avgSeconds % 60);
-
-        this.kpiData.averagePaymentTime = `${this.pad2(minutes)}:${this.pad2(seconds)} 秒`;
-
-
+          getOrderGmv({ startDate: start, endDate: end }),
+          getOrderRBig({ startDate: start, endDate: end }),
+          getOrderTop5Percentage({ startDate: start, endDate: end }),
+          getOrderAveragePaymentTime({ startDate: start, endDate: end })
+        ])
+
+        const gmvValue = gmvRes || 0
+        this.kpiData.gmv = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(gmvValue)
+        this.kpiData.p80Contribution = `${Math.round(p80Res?.rBigRatio || 0)}%`
+        this.kpiData.top5Contribution = `${Math.round(top5Res?.data?.top5Percentage || 0)}%`
+
+        const avgSeconds = avgTimeRes?.averagePaymentSeconds || 0
+        const minutes = Math.floor(avgSeconds / 60)
+        const seconds = Math.round(avgSeconds % 60)
+        this.kpiData.averagePaymentTime = `${this.pad2(minutes)}:${this.pad2(seconds)} 秒`
 
         if (this.activeTab !== 'tm') {
-
-          this.kpiData.gmvTrend = '+0%';
-
-          this.kpiData.p80Trend = '+0%';
-
-          this.kpiData.top5Trend = '+0%';
-
-          this.kpiData.avgTimeTrend = '+0%';
-
+          this.kpiData.gmvTrend = '+0%'
+          this.kpiData.p80Trend = '+0%'
+          this.kpiData.top5Trend = '+0%'
+          this.kpiData.avgTimeTrend = '+0%'
         }
-
       } catch (error) {
-
-        console.error('获取订单价值数据失败:', error);
-
+        console.error('获取订单价值数据失败:', error)
       }
-
     },
-
     calculateTrend(currentValue, previousValue) {
-
-      if (previousValue === 0) return '+0%';
-
-      const change = ((currentValue - previousValue) / previousValue) * 100;
-
-      const sign = change >= 0 ? '+' : '';
-
-      return `${sign}${change.toFixed(1)}%`;
-
+      if (previousValue === 0) return '+0%'
+      const change = ((currentValue - previousValue) / previousValue) * 100
+      const sign = change >= 0 ? '+' : ''
+      return `${sign}${change.toFixed(1)}%`
     },
-
     parseTimeString(timeString) {
-
-      if (!timeString) return 0;
-
-      const timeMatch = timeString.match(/(\d{2}):(\d{2})/);
-
-      if (timeMatch) {
-
-        const minutes = parseInt(timeMatch[1], 10);
-
-        const seconds = parseInt(timeMatch[2], 10);
-
-        return minutes * 60 + seconds;
-
-      }
-
-      return 0;
-
+      const timeMatch = String(timeString || '').match(/(\d{2}):(\d{2})/)
+      if (!timeMatch) return 0
+      return parseInt(timeMatch[1], 10) * 60 + parseInt(timeMatch[2], 10)
     },
-
     getTrendColor(trend) {
-
-      if (!trend) return 'green';
-
-      const value = parseFloat(trend);
-
-      return value >= 0 ? 'green' : 'red';
-
-    },
-
-    async fetchPreviousMonthData(currentStart) {
-
-      try {
-
-        const currentStartDate = this.toDate(currentStart);
-
-        const previousMonthBase = this.addMonths(currentStartDate, -1);
-
-        const previousMonthStart = this.formatYmd(this.startOfMonth(previousMonthBase));
-
-        const previousMonthEnd = this.formatYmd(this.endOfMonth(previousMonthBase));
-
-
+      return parseFloat(trend) >= 0 ? 'green' : 'red'
+    },
+    async fetchPreviousMonthData(currentStart) {
+      try {
+        const currentStartDate = this.toDate(currentStart)
+        const previousMonthBase = this.addMonths(currentStartDate, -1)
+        const previousMonthStart = this.formatYmd(this.startOfMonth(previousMonthBase))
+        const previousMonthEnd = this.formatYmd(this.endOfMonth(previousMonthBase))
 
         const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
-
           getOrderGmv({ startDate: previousMonthStart, endDate: previousMonthEnd }),
-
           getOrderRBig({ startDate: previousMonthStart, endDate: previousMonthEnd }),
-
           getOrderTop5Percentage({ startDate: previousMonthStart, endDate: previousMonthEnd }),
-
           getOrderAveragePaymentTime({ startDate: previousMonthStart, endDate: previousMonthEnd })
-
-        ]);
-
-
+        ])
 
         return {
-
           gmv: gmvRes || 0,
-
           p80: p80Res?.rBigRatio || 0,
-
           top5: top5Res?.data?.top5Percentage || 0,
-
           avgTime: avgTimeRes?.averagePaymentSeconds || 0
-
-        };
-
+        }
       } catch (error) {
-
-        console.error('获取订单价值数据失败:', error);
-
-        return null;
-
+        console.error('获取上月订单价值数据失败:', error)
+        return null
       }
-
     },
-
     async initDashboard() {
-
       try {
-
-        const res = await getOrderMaxDate();
-
-        this.maxDate = res;
-
-        this.selectedDate = res;
-
-
-
-        const startDate = this.formatYmd(this.addDays(res, -6));
-
-        this.currentDateRange = { start: startDate, end: res };
-
-        await this.fetchAllApiData();
-
-        this.kpiData.gmvTrend = '+0%';
-
-        this.kpiData.p80Trend = '+0%';
-
-        this.kpiData.top5Trend = '+0%';
-
-        this.kpiData.avgTimeTrend = '+0%';
-
+        const res = await getOrderMaxDate()
+        this.maxDate = res
+        this.selectedDate = res
       } catch (error) {
-
-        console.error('获取最大日期失败');
-
-        const today = this.formatYmd(new Date());
-
-        this.maxDate = today;
-
-        this.selectedDate = today;
-
-        const startDate = this.formatYmd(this.addDays(today, -6));
-
-        this.currentDateRange = { start: startDate, end: today };
-
-        await this.fetchAllApiData();
-
-        this.kpiData.gmvTrend = '+0%';
-
-        this.kpiData.p80Trend = '+0%';
-
-        this.kpiData.top5Trend = '+0%';
-
-        this.kpiData.avgTimeTrend = '+0%';
-
+        console.error('获取订单最大日期失败:', error)
+        const today = this.formatYmd(new Date())
+        this.maxDate = today
+        this.selectedDate = today
       }
-
+      await this.selectDateRange(this.activeTab)
     },
-
     async selectDateRange(type) {
-
-      this.activeTab = type;
-
-      const baseDate = this.toDate(this.selectedDate);
-
-      let start;
-
-      let end;
-
-
-
-      if (type === '7d') {
-
-        end = this.formatYmd(baseDate);
-
-        start = this.formatYmd(this.addDays(baseDate, -6));
-
-      } else if (type === 'tm') {
-
-        start = this.formatYmd(this.startOfMonth(baseDate));
-
-        end = this.formatYmd(this.endOfMonth(baseDate));
-
-      } else if (type === 'lm') {
-
-        const lastMonth = this.addMonths(baseDate, -1);
-
-        start = this.formatYmd(this.startOfMonth(lastMonth));
-
-        end = this.formatYmd(this.endOfMonth(lastMonth));
-
-      } else if (type === 'all') {
-
-        start = '2022-01-01';
-
-        end = this.maxDate || this.formatYmd(baseDate);
-
-      }
-
-
-
-      this.currentDateRange = { start, end };
-
-
+      this.activeTab = type
+      const range = this.getCurrentRange(type)
+      this.currentDateRange = range
 
       if (type === 'tm') {
-
-        await this.fetchAllApiData({ start, end });
-
-        const previousMonthData = await this.fetchPreviousMonthData(start);
-
+        await this.fetchAllApiData(range)
+        const previousMonthData = await this.fetchPreviousMonthData(range.start)
         if (previousMonthData) {
-
-          const currentGmv = parseFloat(this.kpiData.gmv.replace(/[?,]/g, '')) || 0;
-
-          const currentP80 = parseFloat(this.kpiData.p80Contribution.replace('%', '')) || 0;
-
-          const currentTop5 = parseFloat(this.kpiData.top5Contribution.replace('%', '')) || 0;
-
-          const currentAvgTime = this.parseTimeString(this.kpiData.averagePaymentTime) || 0;
-
-
-
-          this.kpiData.gmvTrend = this.calculateTrend(currentGmv, previousMonthData.gmv);
-
-          this.kpiData.p80Trend = this.calculateTrend(currentP80, previousMonthData.p80);
-
-          this.kpiData.top5Trend = this.calculateTrend(currentTop5, previousMonthData.top5);
-
-          this.kpiData.avgTimeTrend = this.calculateTrend(currentAvgTime, previousMonthData.avgTime);
-
+          const currentGmv = parseFloat(this.kpiData.gmv.replace(/[¥,]/g, '')) || 0
+          const currentP80 = parseFloat(this.kpiData.p80Contribution.replace('%', '')) || 0
+          const currentTop5 = parseFloat(this.kpiData.top5Contribution.replace('%', '')) || 0
+          const currentAvgTime = this.parseTimeString(this.kpiData.averagePaymentTime)
+
+          this.kpiData.gmvTrend = this.calculateTrend(currentGmv, previousMonthData.gmv)
+          this.kpiData.p80Trend = this.calculateTrend(currentP80, previousMonthData.p80)
+          this.kpiData.top5Trend = this.calculateTrend(currentTop5, previousMonthData.top5)
+          this.kpiData.avgTimeTrend = this.calculateTrend(currentAvgTime, previousMonthData.avgTime)
         }
-
       } else {
-
-        this.fetchAllApiData({ start, end });
-
+        await this.fetchAllApiData(range)
       }
-
-    },
-
-    async fetchAllKpiData() {
-
-      try {
-
-        const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
-
-          getOrderGmv(),
-
-          getOrderRBig(),
-
-          getOrderTop5Percentage(),
-
-          getOrderAveragePaymentTime()
-
-        ]);
-
-
-
-        const gmvValue = gmvRes || 0;
-
-        this.kpiData.gmv = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(gmvValue);
-
-        const p80Value = p80Res?.rBigRatio || 0;
-
-        this.kpiData.p80Contribution = `${Math.round(p80Value)}%`;
-
-        const top5Value = top5Res?.data?.top5Percentage || 0;
-
-        this.kpiData.top5Contribution = `${Math.round(top5Value)}%`;
-
-        const avgSeconds = avgTimeRes?.averagePaymentSeconds || 0;
-
-        const minutes = Math.floor(avgSeconds / 60);
-
-        const seconds = Math.round(avgSeconds % 60);
-
-        this.kpiData.averagePaymentTime = `${this.pad2(minutes)}:${this.pad2(seconds)} 秒`;
-
-      } catch (error) {
-
-        console.error('获取KPI数据失败:', error);
-
-        this.kpiData.gmv = '?0';
-
-        this.kpiData.p80Contribution = '0%';
-
-        this.kpiData.top5Contribution = '0%';
-
-        this.kpiData.averagePaymentTime = '00:00 秒';
-
-      }
-
     }
-
   }
-
-};
-
+}
 </script>
 
-
-
-
-
 <style scoped>
-
-.header-controls {
-
+.order-value-view {
   display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
 
+.page-header {
+  display: flex;
+  justify-content: space-between;
   align-items: center;
+  background-color: #fff;
+  padding: 15px 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
 
-  gap: 15px;
-
-  margin-top: 10px;
+.page-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+  margin: 0;
+}
 
+.header-controls {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
 }
 
 .control-label {
-
   font-size: 14px;
-
   color: #666;
-
 }
 
 .control-item-group {
-
   display: flex;
-
   align-items: center;
-
-  gap: 10px;
-
+  gap: 8px;
 }
 
-.date-input {
-
-  padding: 4px 8px;
-
-  border: 1px solid #ddd;
-
-  border-radius: 4px;
-
-  font-size: 14px;
-
+.date-picker {
+  width: 150px;
 }
 
 .time-tab {
-
-  padding: 5px 15px;
-
+  padding: 6px 14px;
   border: 1px solid #ddd;
-
   background-color: #fff;
-
   border-radius: 4px;
-
   cursor: pointer;
-
-  font-size: 14px;
-
+  font-size: 13px;
   transition: all 0.2s;
-
 }
 
 .time-tab:hover {
-
   border-color: #188df0;
-
   color: #188df0;
-
 }
 
 .time-tab.active {
-
   background-color: #188df0;
-
   color: #fff;
-
   border-color: #188df0;
-
 }
 
 .upload-btn {
-
-  padding: 5px 12px;
-
+  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;
-
-}
-
-/* 样式部分和之前一样,不用修改 */
-
-.order-value-view {
-
-  display: flex;
-
-  flex-direction: column;
-
-  gap: 20px;
-
-}
-
-.page-header {
-
-  display: flex;
-
-  justify-content: space-between;
-
-  align-items: center;
-
-  background-color: white;
-
-  padding: 15px 20px;
-
-  border-radius: 8px;
-
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-
-}
-
-.page-title {
-
-  font-size: 18px;
-
-  font-weight: 600;
-
-  color: #333;
-
-  margin: 0;
-
-}
-
-.header-controls {
-
-  display: flex;
-
-  align-items: center;
-
-  font-size: 14px;
-
-}
-
-.control-item {
-
-  margin-left: 20px;
-
-  color: #666;
-
-}
-
-.time-range-selector {
-
-  display: flex;
-
-  align-items: center;
-
-}
-
-.date-input {
-
-  padding: 6px 10px;
-
-  border: 1px solid #ddd;
-
-  border-radius: 4px;
-
-  margin-right: 10px;
-
-}
-
-.time-tab {
-
-  background: white;
-
-  border: 1px solid #ddd;
-
-  padding: 8px 12px;
-
-  margin-left: -1px;
-
-  cursor: pointer;
-
-  font-size: 12px;
-
-  color: #555;
-
-  transition: all 0.2s;
-
-}
-
-.time-tab:first-of-type {
-
-  border-top-left-radius: 4px;
-
-  border-bottom-left-radius: 4px;
-
-}
-
-.time-tab:last-of-type {
-
-  border-top-right-radius: 4px;
-
-  border-bottom-right-radius: 4px;
-
-}
-
-.time-tab.active {
-
-  background-color: #3366CC;
-
-  color: white;
-
-  border-color: #3366CC;
-
-  z-index: 1;
-
 }
 
 .kpi-cards-grid {
-
   display: grid;
-
   grid-template-columns: repeat(4, 1fr);
-
   gap: 20px;
-
 }
 
 .charts-area {
-
   display: flex;
-
   flex-direction: column;
-
   gap: 20px;
-
 }
 
 .leakage-section {
-
   display: flex;
-
 }
 
 .full-width {
-
   width: 100%;
-
 }
 
 .chart-wrapper {
-
-  background-color: white;
-
+  background-color: #fff;
   padding: 15px;
-
   border-radius: 8px;
-
   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-
 }
-
 </style>
-

+ 267 - 235
src/views/order/related/index.vue

@@ -1,14 +1,25 @@
 <template>
   <div class="product-analysis-view">
-    <!-- 页面头部 -->
     <header class="page-header">
       <h1 class="page-title">商品关联透视</h1>
-      <p class="page-subtitle">探索商品之间的共现购买关系,发现最佳销售组合。</p>
+      <p class="page-subtitle">探索商品之间的共同购买关系,发现潜在高价值组合。</p>
     </header>
 
-    <!-- 共现购买关系表格 -->
     <div class="table-container">
-      <h2 class="table-title">热门商品组合</h2>
+      <div class="table-header">
+        <h2 class="table-title">热门商品组合</h2>
+        <div class="search-box">
+          <el-input
+            v-model.trim="skuKeyword"
+            clearable
+            placeholder="输入 SKU 搜索"
+            @keyup.enter.native="applySkuSearch"
+            @clear="applySkuSearch"
+          >
+            <el-button slot="append" icon="el-icon-search" @click="applySkuSearch" />
+          </el-input>
+        </div>
+      </div>
       <table>
         <thead>
           <tr>
@@ -19,47 +30,28 @@
         </thead>
         <tbody>
           <tr v-if="loading">
-            <td colspan="3">正在努力加载数据... 🐾</td>
+            <td colspan="3">正在加载数据...</td>
           </tr>
-          <!-- ✨【保持列位置不变,只调整数字位置】✨ -->
-          <tr v-else-if="coPurchaseData.length > 0" v-for="(item, index) in paginatedData" :key="index" class="data-row">
+          <tr v-else-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" :title="`共同购买次数: ${item.coPurchaseCount}`">
-              <div class="count-wrapper">
-                {{ item.coPurchaseCount }}
-              </div>
+            <td class="count-cell">
+              <div class="count-wrapper">{{ item.coPurchaseCount }}</div>
             </td>
           </tr>
-          <tr v-else-if="!loading && coPurchaseData.length === 0">
-            <td colspan="3">暂时没有找到商品组合数据...</td>
+          <tr v-else>
+            <td colspan="3">没有找到符合条件的商品组合数据。</td>
           </tr>
         </tbody>
       </table>
-      
-      <!-- 分页控件 -->
+
       <div class="pagination">
-        <button 
-          :disabled="currentPage === 1" 
-          @click="prevPage"
-          class="page-button"
-        >
-          上一页
-        </button>
-        <span class="page-info">
-          第 {{ currentPage }} 页,共 {{ totalPages }} 页
-        </span>
-        <button 
-          :disabled="currentPage === totalPages" 
-          @click="nextPage"
-          class="page-button"
-        >
-          下一页
-        </button>
+        <button :disabled="currentPage === 1" @click="prevPage" class="page-button">上一页</button>
+        <span class="page-info">第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
+        <button :disabled="currentPage === totalPages" @click="nextPage" class="page-button">下一页</button>
       </div>
     </div>
 
-    <!-- 网络图部分(保持不变) -->
     <div class="chart-container">
       <h2 class="chart-title">共购规则网络图</h2>
       <div ref="networkChart" style="width: 100%; height: 600px;"></div>
@@ -68,8 +60,8 @@
 </template>
 
 <script>
-import * as echarts from 'echarts';
-import { getOrderCoPurchase } from '@/api/order';
+import * as echarts from 'echarts'
+import { getOrderCoPurchase } from '@/api/order'
 
 export default {
   name: 'OrderRelated',
@@ -78,237 +70,275 @@ export default {
       coPurchaseData: [],
       loading: true,
       currentPage: 1,
-      itemsPerPage: 10
-    };
+      itemsPerPage: 10,
+      skuKeyword: '',
+      chartInstance: null
+    }
   },
   computed: {
     paginatedData() {
-      const start = (this.currentPage - 1) * this.itemsPerPage;
-      const end = start + this.itemsPerPage;
-      return this.coPurchaseData.slice(start, end);
+      const start = (this.currentPage - 1) * this.itemsPerPage
+      return this.coPurchaseData.slice(start, start + this.itemsPerPage)
     },
     totalPages() {
-      return Math.ceil(this.coPurchaseData.length / this.itemsPerPage);
+      return Math.max(1, Math.ceil(this.coPurchaseData.length / this.itemsPerPage))
     }
   },
   mounted() {
-    this.fetchData();
+    this.fetchData()
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    this.chartInstance?.dispose()
   },
   methods: {
     prevPage() {
-      if (this.currentPage > 1) {
-        this.currentPage -= 1;
-      }
+      if (this.currentPage > 1) this.currentPage -= 1
     },
     nextPage() {
-      if (this.currentPage < this.totalPages) {
-        this.currentPage += 1;
-      }
+      if (this.currentPage < this.totalPages) this.currentPage += 1
+    },
+    applySkuSearch() {
+      this.currentPage = 1
+      this.fetchData()
     },
     async fetchData() {
+      this.loading = true
       try {
-        const response = await getOrderCoPurchase();
-        this.coPurchaseData = response || [];
-        this.renderNetworkChart(this.coPurchaseData);
+        const response = await getOrderCoPurchase({
+          skuKeyword: this.skuKeyword || undefined
+        })
+        this.coPurchaseData = response || []
+        this.renderNetworkChart(this.coPurchaseData)
       } catch (error) {
-        console.error("获取共现购买数据失败:", error);
+        console.error('获取共购数据失败:', error)
+        this.coPurchaseData = []
+        this.renderNetworkChart([])
       } finally {
-        this.loading = false;
+        this.loading = false
       }
     },
+    handleResize() {
+      this.chartInstance?.resize()
+    },
     renderNetworkChart(data) {
-      const chartEl = this.$refs.networkChart;
-      if (!chartEl || !data) return;
-      const myChart = echarts.init(chartEl);
-  
-  // 统计每个节点的出现次数(核心度)
-  const nodeCount = {};
-  // 统计每条连线的权重
-  const linkWeights = {};
-  
-  data.forEach(item => {
-    // 统计节点出现次数
-    nodeCount[item.productAId] = (nodeCount[item.productAId] || 0) + 1;
-    nodeCount[item.productBId] = (nodeCount[item.productBId] || 0) + 1;
-    
-    // 统计连线权重
-    const linkKey = `${item.productAId}-${item.productBId}`;
-    linkWeights[linkKey] = (linkWeights[linkKey] || 0) + item.coPurchaseCount;
-  });
-  
-  // 创建节点和连线
-  const nodes = [];
-  const links = [];
-  const nodeSet = new Set();
-  
-  // 用于自动聚类的颜色分类
-  const categories = [
-    { name: '社群1', itemStyle: { color: '#FF6B6B' } },
-    { name: '社群2', itemStyle: { color: '#4ECDC4' } },
-    { name: '社群3', itemStyle: { color: '#45B7D1' } },
-    { name: '社群4', itemStyle: { color: '#96CEB4' } },
-    { name: '社群5', itemStyle: { color: '#FFEAA7' } },
-    { name: '社群6', itemStyle: { color: '#DDA0DD' } }
-  ];
-  
-  // 用于社群聚类的简单算法
-  const nodeCategories = {};
-  let categoryIndex = 0;
-  
-  data.forEach(item => {
-    // 添加节点A
-    if (!nodeSet.has(item.productAId)) {
-      nodeSet.add(item.productAId);
-      // 根据出现次数确定节点大小(核心度)
-      const count = nodeCount[item.productAId];
-      // 节点大小范围:15-50
-      const symbolSize = Math.min(15 + count * 5, 50);
-      
-      // 简单的社群聚类:根据第一个关联商品确定社群
-      if (!nodeCategories[item.productAId]) {
-        nodeCategories[item.productAId] = categoryIndex % categories.length;
-        categoryIndex++;
+      const chartEl = this.$refs.networkChart
+      if (!chartEl) return
+      if (this.chartInstance) {
+        this.chartInstance.dispose()
       }
-      
-      nodes.push({ 
-        id: item.productAId, 
-        name: item.productAId, 
-        symbolSize: symbolSize, 
-        category: nodeCategories[item.productAId],
-        value: count // 用于tooltip显示
-      });
-    }
-    
-    // 添加节点B
-    if (!nodeSet.has(item.productBId)) {
-      nodeSet.add(item.productBId);
-      // 根据出现次数确定节点大小(核心度)
-      const count = nodeCount[item.productBId];
-      // 节点大小范围:15-50
-      const symbolSize = Math.min(15 + count * 5, 50);
-      
-      // 简单的社群聚类:根据第一个关联商品确定社群
-      if (!nodeCategories[item.productBId]) {
-        nodeCategories[item.productBId] = categoryIndex % categories.length;
-        categoryIndex++;
-      }
-      
-      nodes.push({ 
-        id: item.productBId, 
-        name: item.productBId, 
-        symbolSize: symbolSize, 
-        category: nodeCategories[item.productBId],
-        value: count // 用于tooltip显示
-      });
-    }
-    
-    // 添加连线
-    const linkKey = `${item.productAId}-${item.productBId}`;
-    const weight = linkWeights[linkKey];
-    links.push({
-      source: item.productAId,
-      target: item.productBId,
-      // 线条粗细代表关联强度
-      value: weight,
-      // 使用默认线条粗细
-      lineStyle: {
-        width: 1 // 恢复到默认线条粗细
-      }
-    });
-  });
+      this.chartInstance = echarts.init(chartEl)
+
+      const nodeCount = {}
+      const linkWeights = {}
+      data.forEach(item => {
+        nodeCount[item.productAId] = (nodeCount[item.productAId] || 0) + 1
+        nodeCount[item.productBId] = (nodeCount[item.productBId] || 0) + 1
+        const linkKey = `${item.productAId}-${item.productBId}`
+        linkWeights[linkKey] = (linkWeights[linkKey] || 0) + item.coPurchaseCount
+      })
 
-  const option = {
-    tooltip: {
-      formatter: (params) => {
-        if (params.dataType === 'node') {
-          return `${params.name}<br/>出现次数: ${params.data.value}`;
-        } else if (params.dataType === 'edge') {
-          return `${params.data.source} ↔ ${params.data.target}<br/>共购次数: ${params.data.value}`;
+      const categories = [
+        { name: '群组1', itemStyle: { color: '#FF6B6B' } },
+        { name: '群组2', itemStyle: { color: '#4ECDC4' } },
+        { name: '群组3', itemStyle: { color: '#45B7D1' } },
+        { name: '群组4', itemStyle: { color: '#96CEB4' } },
+        { name: '群组5', itemStyle: { color: '#FFEAA7' } },
+        { name: '群组6', itemStyle: { color: '#DDA0DD' } }
+      ]
+
+      const nodes = []
+      const links = []
+      const nodeSet = new Set()
+      const nodeCategories = {}
+      let categoryIndex = 0
+
+      data.forEach(item => {
+        if (!nodeSet.has(item.productAId)) {
+          nodeSet.add(item.productAId)
+          const count = nodeCount[item.productAId]
+          nodeCategories[item.productAId] = nodeCategories[item.productAId] ?? categoryIndex++ % categories.length
+          nodes.push({
+            id: item.productAId,
+            name: item.productAId,
+            symbolSize: Math.min(15 + count * 5, 50),
+            category: nodeCategories[item.productAId],
+            value: count
+          })
         }
-        return '';
-      }
-    },
-    legend: [{
-      data: categories.map(cat => cat.name),
-      bottom: 0, // 将图例放置在容器底部
-      left: 'center', // 将图例水平居中
-      itemGap: 20, // 增加图例项间距
-      itemWidth: 15,
-      itemHeight: 10,
-      backgroundColor: 'white', // 设置图例背景为白色
-      borderRadius: [8, 8, 8, 8], // 四周圆角,与页面其他组件保持一致
-      shadowColor: 'rgba(0, 0, 0, 0.1)', // 设置阴影
-      shadowBlur: 5, // 设置阴影模糊度
-      padding: [15, 20, 15, 20] // 设置内边距
-    }],
-    series: [
-      {
-        type: 'graph',
-        layout: 'force',
-        data: nodes,
-        links: links,
-        categories: categories,
-        roam: true,
-        label: {
-          show: true,
-          position: 'right',
-          formatter: '{b}',
-          fontSize: 12
-        },
-        force: {
-          repulsion: 300, // 适度增加节点间排斥力,使节点在水平方向分散
-          edgeLength: [150, 250], // 调整连线长度范围,平衡水平分散效果
-          gravity: 0.2, // 增加重力,使节点在垂直方向更集中
-          layoutAnimation: true
-        },
-        // 调整图表中心位置,将图表向上移动一点
-        center: ['50%', '40%'],
-        // 设置图表左右边界,只在水平方向拉伸
-        left: 0,
-        right: 0,
-        // 节点样式
-        itemStyle: {
-          borderColor: '#fff',
-          borderWidth: 1,
-          shadowBlur: 10,
-          shadowColor: 'rgba(0, 0, 0, 0.3)'
-        },
-        // 连线样式
-        lineStyle: {
-          opacity: 0.9,
-          curveness: 0 // 直线连接
+        if (!nodeSet.has(item.productBId)) {
+          nodeSet.add(item.productBId)
+          const count = nodeCount[item.productBId]
+          nodeCategories[item.productBId] = nodeCategories[item.productBId] ?? categoryIndex++ % categories.length
+          nodes.push({
+            id: item.productBId,
+            name: item.productBId,
+            symbolSize: Math.min(15 + count * 5, 50),
+            category: nodeCategories[item.productBId],
+            value: count
+          })
+        }
+        links.push({
+          source: item.productAId,
+          target: item.productBId,
+          value: linkWeights[`${item.productAId}-${item.productBId}`],
+          lineStyle: { width: 1 }
+        })
+      })
+
+      this.chartInstance.setOption({
+        tooltip: {
+          formatter: params => {
+            if (params.dataType === 'node') {
+              return `${params.name}<br/>出现次数: ${params.data.value}`
+            }
+            if (params.dataType === 'edge') {
+              return `${params.data.source} -> ${params.data.target}<br/>共购次数: ${params.data.value}`
+            }
+            return ''
+          }
         },
-        // 鼠标悬停效果
-        emphasis: {
-          focus: 'adjacency',
+        legend: [{
+          data: categories.map(item => item.name),
+          bottom: 0,
+          left: 'center',
+          itemGap: 20
+        }],
+        series: [{
+          type: 'graph',
+          layout: 'force',
+          data: nodes,
+          links,
+          categories,
+          roam: true,
+          label: {
+            show: true,
+            position: 'right',
+            formatter: '{b}',
+            fontSize: 12
+          },
+          force: {
+            repulsion: 300,
+            edgeLength: [150, 250],
+            gravity: 0.2,
+            layoutAnimation: true
+          },
+          center: ['50%', '40%'],
+          left: 0,
+          right: 0,
+          itemStyle: {
+            borderColor: '#fff',
+            borderWidth: 1,
+            shadowBlur: 10,
+            shadowColor: 'rgba(0, 0, 0, 0.3)'
+          },
           lineStyle: {
-            width: 10
+            opacity: 0.9,
+            curveness: 0
+          },
+          emphasis: {
+            focus: 'adjacency',
+            lineStyle: {
+              width: 10
+            }
           }
-        }
-      }
-    ]
-  };
-      myChart.setOption(option);
+        }]
+      })
     }
   }
-};
+}
 </script>
 
 <style scoped>
-.product-analysis-view { display: flex; flex-direction: column; gap: 20px; }
-.page-header, .table-container, .chart-container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
-.page-title { font-size: 20px; font-weight: 600; margin: 0; color: #333; }
-.page-subtitle { font-size: 14px; color: #666; margin-top: 8px; }
-.table-title, .chart-title { font-size: 18px; font-weight: 600; margin-bottom: 20px; color: #333; }
-table { width: 100%; border-collapse: collapse; }
-th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #f0f0f0; }
-thead th { background-color: #f8f9fa; font-size: 14px; font-weight: 600; color: #333; }
-.data-row:hover { background-color: #f8f9fa; }
-.sku-cell { font-family: 'Courier New', monospace; font-size: 14px; }
-.count-cell { text-align: left; font-size: 14px; }
-.count-wrapper { font-weight: bold; color: #3366CC; text-align: center; display: inline-block; min-width: 90px; }
+.product-analysis-view {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.page-header,
+.table-container,
+.chart-container {
+  background-color: #fff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.page-title {
+  font-size: 20px;
+  font-weight: 600;
+  margin: 0;
+  color: #333;
+}
+
+.page-subtitle {
+  font-size: 14px;
+  color: #666;
+  margin-top: 8px;
+}
+
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 16px;
+  margin-bottom: 20px;
+}
+
+.table-title,
+.chart-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+}
+
+.search-box {
+  width: 280px;
+}
+
+table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+th,
+td {
+  padding: 12px 15px;
+  text-align: left;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+thead th {
+  background-color: #f8f9fa;
+  font-size: 14px;
+  font-weight: 600;
+  color: #333;
+}
+
+.data-row:hover {
+  background-color: #f8f9fa;
+}
+
+.sku-cell {
+  font-family: 'Courier New', monospace;
+  font-size: 14px;
+}
+
+.count-cell {
+  text-align: left;
+  font-size: 14px;
+}
+
+.count-wrapper {
+  font-weight: bold;
+  color: #3366cc;
+  text-align: center;
+  display: inline-block;
+  min-width: 90px;
+}
 
-/* 分页样式 */
 .pagination {
   display: flex;
   justify-content: center;
@@ -316,6 +346,7 @@ thead th { background-color: #f8f9fa; font-size: 14px; font-weight: 600; color:
   margin-top: 20px;
   gap: 15px;
 }
+
 .page-button {
   padding: 8px 16px;
   background-color: #f0f4f8;
@@ -326,15 +357,16 @@ thead th { background-color: #f8f9fa; font-size: 14px; font-weight: 600; color:
   font-weight: 500;
   transition: all 0.2s;
 }
+
 .page-button:hover:not(:disabled) {
   background-color: #e1e8f0;
-  transform: translateY(-1px);
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 }
+
 .page-button:disabled {
   opacity: 0.5;
   cursor: not-allowed;
 }
+
 .page-info {
   font-size: 14px;
   color: #666;

+ 374 - 269
src/views/order/shopvalue/index.vue

@@ -1,23 +1,49 @@
 <template>
   <div class="shop-value-analysis-page">
-    <!-- 1. 页面主标题 -->
     <header class="page-header">
       <div>
         <h1 class="main-title">店铺价值分析</h1>
         <p class="subtitle">实时监控关键指标与趋势</p>
       </div>
+      <div class="header-actions">
+        <span class="control-label">时间筛选</span>
+        <el-date-picker
+          v-model="selectedDate"
+          class="date-picker"
+          type="date"
+          value-format="yyyy-MM-dd"
+          :clearable="false"
+          :editable="false"
+          :picker-options="pickerOptions"
+          placeholder="选择上传数据日期"
+          @change="handleDateChange"
+        />
+        <div class="tab-group">
+          <button :class="['time-tab', { active: activeTab === 'day' }]" @click="applyDateFilter('day')">当天</button>
+          <button :class="['time-tab', { active: activeTab === '7d' }]" @click="applyDateFilter('7d')">最近7天</button>
+          <button :class="['time-tab', { active: activeTab === 'tm' }]" @click="applyDateFilter('tm')">本月</button>
+          <button :class="['time-tab', { active: activeTab === 'lm' }]" @click="applyDateFilter('lm')">上月</button>
+          <button :class="['time-tab', { active: activeTab === 'all' }]" @click="applyDateFilter('all')">全量数据</button>
+        </div>
+      </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">
+      <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>
 
-    <!-- 新增:Top 5 商品贡献分析 (移到顶部) -->
     <div class="chart-card">
       <div class="top-product-contribution-container">
-        <!-- 顶部的三个指标卡片 -->
         <div class="kpi-card-grid">
           <div class="kpi-card">
             <span class="kpi-title">总销售额</span>
@@ -32,41 +58,36 @@
             <span class="kpi-value">{{ (topProductData.contributionRatio * 100).toFixed(2) }}%</span>
           </div>
         </div>
-        
-        <!-- 下方的环形图 -->
-        <div class="chart-card">
+
+        <div class="chart-card inner-chart-card">
           <h3 class="chart-title">Top 5 商品销售分布</h3>
           <div ref="donutChartRef" style="width: 100%; height: 400px;"></div>
         </div>
       </div>
     </div>
 
-    <!-- 4. 图表卡片区域 -->
     <div class="charts-container">
-      <!-- 部门图表:销量 + 销售金额 -->
       <div class="chart-card">
         <div class="card-header">
-          <h3 class="chart-title">各部门业绩分析(销量/金额)</h3>
+          <h3 class="chart-title">各部门业绩分析(销量 / 销售额)</h3>
         </div>
         <div class="chart-wrapper">
           <div ref="unitContributionChart" class="chart-instance"></div>
         </div>
       </div>
 
-      <!-- 渠道图表:销量 + 销售金额 -->
       <div class="chart-card">
         <div class="card-header">
-          <h3 class="chart-title">各渠道业绩分析(销量/金额)</h3>
+          <h3 class="chart-title">各渠道业绩分析(销量 / 销售额)</h3>
         </div>
         <div class="chart-wrapper">
           <div ref="channelTotalChart" class="chart-instance"></div>
         </div>
       </div>
 
-      <!-- 平台图表:销量 + 销售金额 -->
       <div class="chart-card">
         <div class="card-header">
-          <h3 class="chart-title">各平台价值分析(销量/金额)</h3>
+          <h3 class="chart-title">各平台价值分析(销量 / 销售额)</h3>
         </div>
         <div class="chart-wrapper">
           <div ref="platformChart" class="chart-instance"></div>
@@ -77,15 +98,16 @@
 </template>
 
 <script>
-import * as echarts from 'echarts';
+import * as echarts from 'echarts'
 import {
   getShopChannelContribution,
   getShopChannelRoiValue,
   getShopChannelTotalContribution,
+  getShopMaxDate,
   getShopTopProductContribution,
   getShopUnitContribution,
   uploadShopValueFiles
-} from '@/api/order';
+} from '@/api/order'
 
 export default {
   name: 'ShopValue',
@@ -97,161 +119,248 @@ export default {
         contributionRatio: 0,
         top5Products: []
       },
-      uploadingShop: false
-    };
+      uploadingShop: false,
+      selectedDate: '',
+      maxDate: '',
+      activeTab: '7d',
+      currentDateRange: { start: '', end: '' },
+      pickerOptions: {
+        disabledDate: time => {
+          if (!this.maxDate) return false
+          return time.getTime() > new Date(`${this.maxDate}T23:59:59`).getTime()
+        }
+      }
+    }
   },
   mounted() {
-    this.fetchData();
-    window.addEventListener('resize', this.handleResize);
+    this.initDashboard()
+    window.addEventListener('resize', this.handleResize)
   },
   beforeDestroy() {
-    window.removeEventListener('resize', this.handleResize);
+    window.removeEventListener('resize', this.handleResize)
   },
   methods: {
+    getQueryParams() {
+      return {
+        startDate: this.currentDateRange.start,
+        endDate: this.currentDateRange.end
+      }
+    },
     triggerShopUpload() {
-      if (this.uploadingShop) return;
-      if (this.$refs.shopUploadInput) {
-        this.$refs.shopUploadInput.click();
+      if (!this.uploadingShop && this.$refs.shopUploadInput) {
+        this.$refs.shopUploadInput.click()
       }
     },
     async handleShopUploadChange(event) {
-      const files = Array.from((event && event.target && event.target.files) || []);
-      if (!files.length) {
-        return;
-      }
-      const invalid = files.find(file => !String(file.name || '').toLowerCase().endsWith('.csv'));
+      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;
+        this.$modal.msgError('仅支持上传 CSV 文件')
+        event.target.value = ''
+        return
       }
-      await this.uploadShopFiles(files);
-      event.target.value = '';
+      await this.uploadShopFiles(files)
+      event.target.value = ''
     },
     async uploadShopFiles(files) {
-      this.uploadingShop = true;
+      this.uploadingShop = true
       try {
-        const formData = new FormData();
-        files.forEach(file => formData.append('files', file));
-        const data = await uploadShopValueFiles(formData);
-        if (data && data.success) {
-          this.$modal.msgSuccess(data.message || '上传并导入成功');
-          await this.fetchData();
-          return;
+        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 && data.debug) {
-          console.error('[shop-upload-debug]', data.debug);
+        if (data?.debug) {
+          console.error('[shop-upload-debug]', data.debug)
         }
-        this.$modal.msgError((data && data.message) || '上传导入失败');
+        this.$modal.msgError(data?.message || '上传导入失败')
       } catch (error) {
-        if (error && error.response && error.response.data && error.response.data.debug) {
-          console.error('[shop-upload-debug]', error.response.data.debug);
+        if (error?.response?.data?.debug) {
+          console.error('[shop-upload-debug]', error.response.data.debug)
         }
-        const msg = error && error.response && error.response.data && error.response.data.message
-          ? error.response.data.message
-          : (error && error.message ? error.message : '上传导入失败');
-        this.$modal.msgError(msg);
+        const msg = error?.response?.data?.message || error?.message || '上传导入失败'
+        this.$modal.msgError(msg)
       } finally {
-        this.uploadingShop = false;
+        this.uploadingShop = false
+      }
+    },
+    handleDateChange() {
+      if (!this.selectedDate) {
+        this.selectedDate = this.maxDate
+      }
+      this.applyDateFilter(this.activeTab)
+    },
+    pad2(value) {
+      return String(value).padStart(2, '0')
+    },
+    toDate(value) {
+      if (value instanceof Date) return new Date(value.getTime())
+      if (!value) return new Date()
+      return new Date(`${value}T00:00:00`)
+    },
+    formatYmd(value) {
+      const date = this.toDate(value)
+      return `${date.getFullYear()}-${this.pad2(date.getMonth() + 1)}-${this.pad2(date.getDate())}`
+    },
+    addDays(value, days) {
+      const date = this.toDate(value)
+      date.setDate(date.getDate() + days)
+      return date
+    },
+    addMonths(value, months) {
+      const date = this.toDate(value)
+      date.setMonth(date.getMonth() + months)
+      return date
+    },
+    startOfMonth(value) {
+      const date = this.toDate(value)
+      return new Date(date.getFullYear(), date.getMonth(), 1)
+    },
+    endOfMonth(value) {
+      const date = this.toDate(value)
+      return new Date(date.getFullYear(), date.getMonth() + 1, 0)
+    },
+    buildRange(type = this.activeTab) {
+      const baseDate = this.toDate(this.selectedDate || this.maxDate || new Date())
+      if (type === 'day') {
+        const day = this.formatYmd(baseDate)
+        return { start: day, end: day }
+      }
+      if (type === '7d') {
+        return {
+          start: this.formatYmd(this.addDays(baseDate, -6)),
+          end: this.formatYmd(baseDate)
+        }
+      }
+      if (type === 'tm') {
+        return {
+          start: this.formatYmd(this.startOfMonth(baseDate)),
+          end: this.formatYmd(this.endOfMonth(baseDate))
+        }
+      }
+      if (type === 'lm') {
+        const lastMonth = this.addMonths(baseDate, -1)
+        return {
+          start: this.formatYmd(this.startOfMonth(lastMonth)),
+          end: this.formatYmd(this.endOfMonth(lastMonth))
+        }
       }
+      return {
+        start: '2022-01-01',
+        end: this.maxDate || this.formatYmd(baseDate)
+      }
+    },
+    async initDashboard() {
+      try {
+        const maxDate = await getShopMaxDate()
+        this.maxDate = maxDate
+        this.selectedDate = maxDate
+      } catch (error) {
+        console.error('获取店铺最大日期失败:', error)
+        const today = this.formatYmd(new Date())
+        this.maxDate = today
+        this.selectedDate = today
+      }
+      await this.applyDateFilter(this.activeTab)
+    },
+    async applyDateFilter(type) {
+      this.activeTab = type
+      this.currentDateRange = this.buildRange(type)
+      await this.fetchData()
     },
     formatNumber(num) {
-      if (!num) return '0';
-      return new Intl.NumberFormat().format(Number(num).toFixed(0));
+      if (!num) return '0'
+      return new Intl.NumberFormat().format(Number(num).toFixed(0))
     },
     handleResize() {
-      const refs = this.$refs;
-      if (refs.unitContributionChart) echarts.getInstanceByDom(refs.unitContributionChart)?.resize();
-      if (refs.channelTotalChart) echarts.getInstanceByDom(refs.channelTotalChart)?.resize();
-      if (refs.platformChart) echarts.getInstanceByDom(refs.platformChart)?.resize();
-      if (refs.donutChartRef) echarts.getInstanceByDom(refs.donutChartRef)?.resize();
+      const refs = this.$refs
+      if (refs.unitContributionChart) echarts.getInstanceByDom(refs.unitContributionChart)?.resize()
+      if (refs.channelTotalChart) echarts.getInstanceByDom(refs.channelTotalChart)?.resize()
+      if (refs.platformChart) echarts.getInstanceByDom(refs.platformChart)?.resize()
+      if (refs.donutChartRef) echarts.getInstanceByDom(refs.donutChartRef)?.resize()
     },
     initDonutChart(chartData) {
-      const chartEl = this.$refs.donutChartRef;
-      if (!chartEl) return;
-      const myChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl);
-      const option = {
+      const chartEl = this.$refs.donutChartRef
+      if (!chartEl) return
+      const myChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
+      myChart.setOption({
         tooltip: {
           trigger: 'item',
-          formatter: '{a} <br/>{b} : ?{c} ({d}%)'
+          formatter: '{a}<br/>{b}: ¥{c} ({d}%)'
         },
         legend: {
           orient: 'horizontal',
           bottom: '0%',
           left: 'center'
         },
-        series: [
-          {
-            name: '销售额',
-            type: 'pie',
-            radius: ['60%', '80%'],
-            avoidLabelOverlap: false,
+        series: [{
+          name: '销售额',
+          type: 'pie',
+          radius: ['60%', '80%'],
+          avoidLabelOverlap: false,
+          label: {
+            show: true,
+            position: 'outside',
+            formatter: '{b}: {d}%'
+          },
+          labelLine: {
+            show: true,
+            length: 15,
+            length2: 10
+          },
+          emphasis: {
             label: {
               show: true,
-              position: 'outside',
-              formatter: '{b}: {d}%'
-            },
-            labelLine: {
-              show: true,
-              length: 15,
-              length2: 10
-            },
-            emphasis: {
-              label: {
-                show: true,
-                fontSize: '14',
-                fontWeight: 'bold'
-              }
-            },
-            data: chartData
-          }
-        ]
-      };
-      myChart.setOption(option, true);
-      return myChart;
+              fontSize: 14,
+              fontWeight: 'bold'
+            }
+          },
+          data: chartData
+        }]
+      }, true)
     },
     async fetchTopProductData() {
       try {
-        const response = await getShopTopProductContribution();
-        if (response.success) {
-          const data = response.data;
-          this.topProductData.totalSales = data.totalSales;
-          this.topProductData.top5TotalSales = data.top5TotalSales;
-          this.topProductData.contributionRatio = data.contributionRatio;
-          this.topProductData.top5Products = data.top5Products;
-
-          const chartData = data.top5Products.map(p => ({
-            name: p.productCode,
-            value: p.salesAmount
-          }));
-          this.initDonutChart(chartData);
-        }
+        const response = await getShopTopProductContribution(this.getQueryParams())
+        if (!response?.success) return
+        const data = response.data || {}
+        this.topProductData.totalSales = data.totalSales || 0
+        this.topProductData.top5TotalSales = data.top5TotalSales || 0
+        this.topProductData.contributionRatio = data.contributionRatio || 0
+        this.topProductData.top5Products = data.top5Products || []
+
+        const chartData = this.topProductData.top5Products.map(item => ({
+          name: item.productCode,
+          value: item.salesAmount
+        }))
+        this.initDonutChart(chartData)
       } catch (error) {
-        console.error('获取Top 5商品贡献失败:', error);
+        console.error('获取 Top 5 商品贡献失败:', error)
       }
     },
     initDualIndicatorBarChart(chartEl, data, categoryKey, seriesConfig) {
-      if (!chartEl) return;
-      const spacePerBar = 120;
-      const containerWidth = Math.max(800, spacePerBar * data.length);
-      chartEl.style.width = `${containerWidth}px`;
-
-      let myChart = echarts.getInstanceByDom(chartEl);
-      if (!myChart) myChart = echarts.init(chartEl);
+      if (!chartEl) return
+      const spacePerBar = 120
+      chartEl.style.width = `${Math.max(800, spacePerBar * data.length)}px`
 
-      const categories = data.map(item => item[categoryKey]);
-      const series = seriesConfig.map(({ key, name, color, formatter }) => ({
+      const myChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
+      const categories = data.map(item => item[categoryKey])
+      const series = seriesConfig.map(({ key, name, color }) => ({
         name,
         type: 'bar',
         data: data.map(item => item[key]),
         barMaxWidth: '40px',
         itemStyle: { color, borderRadius: [4, 4, 0, 0] },
-        yAxisIndex: seriesConfig.findIndex(c => c.key === key)
-      }));
+        yAxisIndex: seriesConfig.findIndex(config => config.key === key)
+      }))
 
-      const option = {
+      myChart.setOption({
         tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
-        legend: { data: seriesConfig.map(c => c.name), top: 0 },
+        legend: { data: seriesConfig.map(item => item.name), top: 0 },
         grid: { left: '80px', right: '80px', top: '40px', bottom: '60px' },
         xAxis: {
           type: 'category',
@@ -266,34 +375,28 @@ export default {
           axisLabel: {
             color: '#666',
             formatter: formatter || (value => {
-              if (value >= 1000000) return (value / 1000000) + 'M';
-              if (value >= 1000) return (value / 1000) + 'K';
-              return value;
+              if (value >= 1000000) return `${value / 1000000}M`
+              if (value >= 1000) return `${value / 1000}K`
+              return value
             })
           },
           position: index === 0 ? 'left' : 'right',
           offset: index === 1 ? 40 : 0
         })),
         series
-      };
-
-      myChart.setOption(option, true);
-      return myChart;
+      }, true)
     },
     initPlatformValueChart(chartEl, data) {
-      if (!chartEl) return;
-      const spacePerBar = 140;
-      const containerWidth = Math.max(800, spacePerBar * data.length);
-      chartEl.style.width = `${containerWidth}px`;
-
-      let myChart = echarts.getInstanceByDom(chartEl);
-      if (!myChart) myChart = echarts.init(chartEl);
+      if (!chartEl) return
+      const spacePerBar = 140
+      chartEl.style.width = `${Math.max(800, spacePerBar * data.length)}px`
 
-      const categories = data.map(item => item.name);
+      const myChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl)
+      const categories = data.map(item => item.name)
 
-      const option = {
+      myChart.setOption({
         tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
-        legend: { data: ['销量', '平均订单金额'], top: 0 },
+        legend: { data: ['销量', '销售额'], top: 0 },
         grid: { left: '80px', right: '80px', top: '40px', bottom: '60px' },
         xAxis: {
           type: 'category',
@@ -309,20 +412,19 @@ export default {
             axisLabel: {
               color: '#5470c6',
               formatter: value => {
-                if (value >= 1000000) return (value / 1000000) + 'M';
-                if (value >= 1000) return (value / 1000) + 'K';
-                return value;
+                if (value >= 1000000) return `${value / 1000000}M`
+                if (value >= 1000) return `${value / 1000}K`
+                return value
               }
-            },
-            position: 'left'
+            }
           },
           {
             type: 'value',
-            name: '平均订单金额(元)',
+            name: '销售额(元)',
             nameTextStyle: { color: '#e9c46a' },
             axisLabel: {
               color: '#e9c46a',
-              formatter: value => `?${value.toFixed(2)}`
+              formatter: value => `¥${Number(value).toFixed(2)}`
             },
             position: 'right',
             offset: 40
@@ -338,7 +440,7 @@ export default {
             yAxisIndex: 0
           },
           {
-            name: '平均订单金额',
+            name: '销售额',
             type: 'bar',
             data: data.map(item => item.avgOrderValue),
             barMaxWidth: '40px',
@@ -346,99 +448,71 @@ export default {
             yAxisIndex: 1
           }
         ]
-      };
-
-      myChart.setOption(option, true);
-      return myChart;
+      }, true)
     },
     async fetchData() {
       try {
-        const [
-          unitRes,
-          channelTotalRes,
-          channelContributionRes,
-          channelRoiValueRes
-        ] = await Promise.all([
-          getShopUnitContribution(),
-          getShopChannelTotalContribution(),
-          getShopChannelContribution(),
-          getShopChannelRoiValue()
-        ]);
-
-        if (unitRes.success && unitRes.data) {
-          const sortedData = [...unitRes.data].sort((a, b) => b.totalAmount - a.totalAmount);
-          this.initDualIndicatorBarChart(
-            this.$refs.unitContributionChart,
-            sortedData,
-            'name',
-            [
-              { key: 'totalVolume', name: '销量', color: '#5470c6' },
-              {
-                key: 'totalAmount',
-                name: '平均订单金额(元)',
-                color: '#91cc75',
-                formatter: value => `?${(value >= 10000 ? (value / 10000) + 'w' : value)}`
-              }
-            ]
-          );
+        const params = this.getQueryParams()
+        const [unitRes, channelTotalRes, channelContributionRes, channelRoiValueRes] = await Promise.all([
+          getShopUnitContribution(params),
+          getShopChannelTotalContribution(params),
+          getShopChannelContribution(params),
+          getShopChannelRoiValue(params)
+        ])
+
+        if (unitRes?.success && unitRes.data) {
+          this.initDualIndicatorBarChart(this.$refs.unitContributionChart, unitRes.data, 'name', [
+            { key: 'totalVolume', name: '销量', color: '#5470c6' },
+            {
+              key: 'totalAmount',
+              name: '销售额(元)',
+              color: '#91cc75',
+              formatter: value => `¥${value >= 10000 ? `${value / 10000}w` : value}`
+            }
+          ])
         }
 
-        if (channelTotalRes.success && channelTotalRes.data) {
-          const sortedData = [...channelTotalRes.data].sort((a, b) => b.totalAmount - a.totalAmount);
-          this.initDualIndicatorBarChart(
-            this.$refs.channelTotalChart,
-            sortedData,
-            'name',
-            [
-              { key: 'totalVolume', name: '销量', color: '#5470c6' },
-              {
-                key: 'totalAmount',
-                name: '平均订单金额(元)',
-                color: '#e9c46a',
-                formatter: value => `?${(value >= 10000 ? (value / 10000) + 'w' : value)}`
-              }
-            ]
-          );
+        if (channelTotalRes?.success && channelTotalRes.data) {
+          this.initDualIndicatorBarChart(this.$refs.channelTotalChart, channelTotalRes.data, 'name', [
+            { key: 'totalVolume', name: '销量', color: '#5470c6' },
+            {
+              key: 'totalAmount',
+              name: '销售额(元)',
+              color: '#e9c46a',
+              formatter: value => `¥${value >= 10000 ? `${value / 10000}w` : value}`
+            }
+          ])
         }
 
-        if (
-          channelContributionRes.success &&
-          channelRoiValueRes.success &&
-          channelContributionRes.data &&
-          channelRoiValueRes.data
-        ) {
+        if (channelContributionRes?.success && channelRoiValueRes?.success && channelContributionRes.data && channelRoiValueRes.data) {
           const platformSales = Object.entries(channelContributionRes.data).map(([name, value]) => ({
             name,
             totalVolume: Number(value)
-          }));
-
+          }))
           const platformAvgOrder = Object.entries(channelRoiValueRes.data).map(([name, value]) => ({
             name,
             avgOrderValue: Number(value)
-          }));
-
+          }))
           const mergedData = platformSales
-            .map(sale => ({
-              ...sale,
-              avgOrderValue: platformAvgOrder.find(avg => avg.name === sale.name)?.avgOrderValue || 0
+            .map(item => ({
+              ...item,
+              avgOrderValue: platformAvgOrder.find(avg => avg.name === item.name)?.avgOrderValue || 0
             }))
-            .sort((a, b) => b.totalVolume - a.totalVolume);
+            .sort((a, b) => b.totalVolume - a.totalVolume)
 
-          this.initPlatformValueChart(this.$refs.platformChart, mergedData);
+          this.initPlatformValueChart(this.$refs.platformChart, mergedData)
         }
 
-        this.fetchTopProductData();
+        await this.fetchTopProductData()
       } catch (error) {
-        console.error('获取店铺价值数据失败:', error);
+        console.error('获取店铺价值数据失败:', error)
       }
     }
   }
-};
+}
 </script>
 
-
 <style scoped>
-/* 保持原有样式不变 */
 .shop-value-analysis-page {
   padding: 24px;
   background-color: #f4f7f9;
@@ -447,57 +521,66 @@ export default {
 
 .page-header {
   margin-bottom: 20px;
+  display: flex;
+  justify-content: space-between;
+  gap: 16px;
+  flex-wrap: wrap;
 }
+
 .main-title {
   font-size: 24px;
   font-weight: 600;
   color: #1d2129;
 }
+
 .subtitle {
   font-size: 14px;
   color: #86909c;
   margin-top: 4px;
 }
 
-.kpi-grid {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 20px;
-  margin-bottom: 20px;
-}
-.kpi-card {
-  background: #fff;
-  padding: 20px;
-  border-radius: 4px;
-  border: 1px solid #e5e6eb;
-}
-.kpi-header {
+.header-actions {
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  margin-bottom: 12px;
+  gap: 12px;
+  flex-wrap: wrap;
 }
-.kpi-title {
+
+.control-label {
   font-size: 14px;
-  color: #4e5969;
+  color: #666;
 }
-.kpi-value {
-  font-size: 28px;
-  font-weight: 700;
-  color: #1d2129;
+
+.date-picker {
+  width: 150px;
+}
+
+.tab-group {
+  display: flex;
+  gap: 8px;
 }
-.kpi-comparison {
-  font-size: 12px;
-  margin-top: 8px;
+
+.time-tab {
+  padding: 6px 14px;
+  border: 1px solid #ddd;
+  background: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.time-tab.active {
+  background-color: #188df0;
+  color: #fff;
+  border-color: #188df0;
 }
-.kpi-comparison.positive { color: #00b42a; }
-.kpi-comparison.negative { color: #f53f3f; }
 
 .upload-row {
   display: flex;
   justify-content: flex-end;
   margin-bottom: 20px;
 }
+
 .btn-upload {
   background-color: #2a7f62;
   color: #fff;
@@ -508,13 +591,18 @@ export default {
   font-size: 16px;
   font-weight: 600;
 }
-.btn-upload:disabled { opacity: 0.7; cursor: not-allowed; }
+
+.btn-upload:disabled {
+  opacity: 0.7;
+  cursor: not-allowed;
+}
 
 .charts-container {
   display: flex;
   flex-direction: column;
   gap: 20px;
 }
+
 .chart-card {
   background: #fff;
   padding: 24px;
@@ -524,33 +612,64 @@ export default {
   grid-template-rows: auto 1fr;
   overflow: hidden;
 }
-.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
-.chart-title { font-size: 16px; font-weight: 600; color: #1d2129; }
-.chart-wrapper { overflow-x: auto; overflow-y: hidden; min-width: 0; }
-.chart-instance { height: 400px; }
 
-.chart-wrapper::-webkit-scrollbar { height: 8px; }
-.chart-wrapper::-webkit-scrollbar-track { background: #f1f1f1; }
-.chart-wrapper::-webkit-scrollbar-thumb { background: #c9cdd4; border-radius: 4px; }
+.inner-chart-card {
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.chart-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1d2129;
+}
+
+.chart-wrapper {
+  overflow-x: auto;
+  overflow-y: hidden;
+  min-width: 0;
+}
+
+.chart-instance {
+  height: 400px;
+}
+
+.chart-wrapper::-webkit-scrollbar {
+  height: 8px;
+}
+
+.chart-wrapper::-webkit-scrollbar-track {
+  background: #f1f1f1;
+}
+
+.chart-wrapper::-webkit-scrollbar-thumb {
+  background: #c9cdd4;
+  border-radius: 4px;
+}
 
-/* 新增的Top 5商品贡献分析样式 */
 .top-product-contribution-container {
   display: flex;
   flex-direction: column;
-  gap: 20px; /* 卡片和图表之间的间距 */
+  gap: 20px;
   width: 100%;
 }
 
-/* KPI卡片网格布局 */
 .kpi-card-grid {
   display: grid;
-  grid-template-columns: repeat(3, 1fr); /* 三个卡片平分宽度 */
-  gap: 20px; /* 卡片之间的间距 */
+  grid-template-columns: repeat(3, 1fr);
+  gap: 20px;
 }
 
-/* 单个KPI卡片的样式 */
 .kpi-card {
-  background-color: #ffffff;
+  background-color: #fff;
   padding: 20px;
   border-radius: 8px;
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
@@ -569,18 +688,4 @@ export default {
   font-weight: bold;
   color: #333;
 }
-
-/* 图表卡片的样式 */
-.chart-card {
-  background-color: #ffffff;
-  padding: 20px;
-  border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
-}
-
-.chart-title {
-  font-size: 18px;
-  color: #333;
-  margin-bottom: 20px;
-}
 </style>