Parcourir la source

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/views/lifecycle/overview/index.vue
Zhu Jiaqi il y a 4 mois
Parent
commit
b9e49ddf60

+ 5 - 2
src/api/sales.js

@@ -35,8 +35,11 @@ export function getSalesOverview(params) {
 // 预测销量趋势
 export function predictSalesTrend(params) {
   return request({
-    url: '/statistics/sales/predict',
+    url: 'http://localhost:8085/api/sales/predict',
     method: 'post',
-    data: params
+    data: params,
+    headers: {
+      'Content-Type': 'application/json'
+    }
   })
 }

+ 3 - 1
src/store/modules/permission.js

@@ -112,7 +112,9 @@ export function filterDynamicRoutes(routes) {
 
 function normalizeViewPath(view) {
   if (!view) return view
-  return view.replace('lifecycle/sku-detail', 'lifecycle/skuAnalysis')
+  const normalized = view.replace('lifecycle/sku-detail', 'lifecycle/skuAnalysis')
+  if (normalized.endsWith('.vue')) return normalized
+  return `${normalized}.vue`
 }
 
 export const loadView = (view) => {

+ 16 - 16
src/views/lifecycle/overview/index.vue

@@ -748,19 +748,19 @@ export default {
 
 .page-header {
   margin-bottom: 20px;
-  
+
   h2 {
     font-size: 24px;
     font-weight: 600;
     color: #303133;
     margin-bottom: 8px;
-    
+
     i {
       margin-right: 8px;
       color: #409EFF;
     }
   }
-  
+
   .page-desc {
     color: #909399;
     font-size: 14px;
@@ -875,10 +875,10 @@ export default {
     display: flex;
     justify-content: space-between;
     align-items: flex-start;
-    
+
     .stat-info {
       flex: 1;
-      
+
       .stat-label {
         font-size: 12px;
         color: #909399;
@@ -886,25 +886,25 @@ export default {
         text-transform: uppercase;
         letter-spacing: 0.5px;
       }
-      
+
       .stat-value {
         font-size: 28px;
         font-weight: bold;
         color: #303133;
         margin: 0 0 8px 0;
       }
-      
+
       .stat-desc {
         font-size: 12px;
         color: #909399;
         margin: 0;
-        
+
         &.stat-desc-success {
           color: #67C23A;
         }
       }
     }
-    
+
     .stat-icon {
       width: 48px;
       height: 48px;
@@ -913,32 +913,32 @@ export default {
       align-items: center;
       justify-content: center;
       font-size: 20px;
-      
+
       &.stat-icon-purple {
         background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
         color: #6366f1;
       }
-      
+
       &.stat-icon-blue {
         background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
         color: #3b82f6;
       }
-      
+
       &.stat-icon-teal {
         background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%);
         color: #14b8a6;
       }
-      
+
       &.stat-icon-green {
         background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
         color: #10b981;
       }
-      
+
       &.stat-icon-yellow {
         background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
         color: #f59e0b;
       }
-      
+
       &.stat-icon-red {
         background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
         color: #ef4444;
@@ -951,7 +951,7 @@ export default {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  
+
   .header-desc {
     font-size: 12px;
     color: #909399;

+ 4 - 4
src/views/lifecycle/skuAnalysis/index.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <div class="app-container">
     <!-- 页面标题 -->
     <div class="page-header">
@@ -458,14 +458,14 @@ export default {
         file.name.endsWith('.xlsx') ||
         file.name.endsWith('.xls') ||
         file.name.endsWith('.csv')
-      const isLt20M = file.size / 1024 / 1024 < 20
+      const isLt300M = file.size / 1024 / 1024 < 300
 
       if (!isExcel) {
         this.$modal.msgError('上传文件只能是 xlsx/xls/csv 格式!')
         return false
       }
-      if (!isLt20M) {
-        this.$modal.msgError('上传文件大小不能超过 20MB!')
+      if (!isLt300M) {
+        this.$modal.msgError('上传文件大小不能超过 300MB!')
         return false
       }
       return true

+ 202 - 0
src/views/order/channel/index.vue

@@ -0,0 +1,202 @@
+<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>
+      <table class="data-table">
+        <thead>
+          <tr>
+            <th>排名</th>
+            <th>商品编码</th>
+            <th>覆盖平台数</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(item, index) in paginatedData" :key="item.productCode">
+            <td>{{ (currentPage - 1) * itemsPerPage + index + 1 }}</td>
+            <td>{{ item.productCode }}</td>
+            <td>{{ item.platformCount }}</td>
+          </tr>
+        </tbody>
+      </table>
+      <!-- 分页控制器 -->
+      <div class="pagination-controls">
+        <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
+        <span>第 {{ currentPage }} / {{ totalPages }} 页</span>
+        <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts';
+import axios from 'axios';
+
+export default {
+  name: 'OrderChannel',
+  data() {
+    return {
+      allProducts: [],
+      currentPage: 1,
+      itemsPerPage: 10
+    };
+  },
+  computed: {
+    paginatedData() {
+      const start = (this.currentPage - 1) * this.itemsPerPage;
+      const end = start + this.itemsPerPage;
+      return this.allProducts.slice(start, end);
+    },
+    totalPages() {
+      if (this.allProducts.length === 0) return 1;
+      return Math.ceil(this.allProducts.length / this.itemsPerPage);
+    }
+  },
+  mounted() {
+    this.fetchData();
+  },
+  methods: {
+    initLineChart() {
+      const chartEl = this.$refs.channelChartRef;
+      if (!chartEl) return;
+      const myChart = echarts.init(chartEl);
+      const top20Data = this.allProducts.slice(0, 20);
+      const option = {
+        tooltip: { trigger: 'axis' },
+        grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+        xAxis: {
+          type: 'category',
+          boundaryGap: false,
+          data: top20Data.map(item => item.productCode),
+          axisLabel: { interval: 0, rotate: 30 }
+        },
+        yAxis: {
+          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)' }
+              ])
+            }
+          }
+        ]
+      };
+      myChart.setOption(option);
+      window.addEventListener('resize', () => myChart.resize());
+    },
+    async fetchData() {
+      try {
+        const response = await axios.get('/api/shop/import/cross-selling-products');
+        if (response.data.success) {
+          this.allProducts = response.data.data || [];
+          this.initLineChart();
+        }
+      } catch (error) {
+        console.error('??????:', error);
+      }
+    },
+    nextPage() {
+      if (this.currentPage < this.totalPages) {
+        this.currentPage += 1;
+      }
+    },
+    prevPage() {
+      if (this.currentPage > 1) {
+        this.currentPage -= 1;
+      }
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+.page-container {
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+  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 {
+  background-color: #fff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
+}
+.chart-title {
+  font-size: 18px;
+  color: #333;
+  margin-bottom: 20px;
+}
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+.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;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 15px;
+}
+.pagination-controls button {
+  padding: 8px 12px;
+  border: 1px solid #ccc;
+  background-color: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+.pagination-controls button:hover:not(:disabled) {
+    border-color: #5470C6;
+    color: #5470C6;
+}
+.pagination-controls button:disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+</style>

+ 203 - 0
src/views/order/efficiency/index.vue

@@ -0,0 +1,203 @@
+<template>
+  <div class="department-efficiency-view">
+    <!-- 1. 页面头部 -->
+    <header class="page-header">
+      <h1 class="page-title">综合运营分析</h1>
+      <p class="page-description">分析各部门的平均销售额与渠道商品多样性。</p>
+    </header>
+
+    <!-- 2. 部门运营效率分析 -->
+    <section class="chart-area">
+      <div v-if="barChart.loading" class="status-overlay">
+        <p>hina正在努力加载部门效率数据中...</p>
+      </div>
+      <div v-else-if="barChart.error" class="status-overlay error">
+        <p>部门效率数据加载失败</p>
+        <p class="error-message">{{ barChart.error }}</p>
+      </div>
+      <div ref="barChartRef" style="width: 100%; height: 500px;"></div>
+    </section>
+
+    <!-- 3. 渠道商品多样性分析 -->
+    <section class="chart-area">
+      <div v-if="pieChart.loading" class="status-overlay">
+        <p>hina正在努力加载渠道多样性数据中...</p>
+      </div>
+      <div v-else-if="pieChart.error" class="status-overlay error">
+        <p>渠道多样性数据加载失败</p>
+        <p class="error-message">{{ pieChart.error }}</p>
+      </div>
+      <div ref="pieChartRef" style="width: 100%; height: 500px;"></div>
+    </section>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import * as echarts from 'echarts';
+
+export default {
+  name: 'OrderEfficiency',
+  data() {
+    return {
+      barChartInstance: null,
+      pieChartInstance: null,
+      barChart: { loading: true, error: null },
+      pieChart: { loading: true, error: null }
+    };
+  },
+  mounted() {
+    this.initBarChart();
+    this.initPieChart();
+    window.addEventListener('resize', this.handleResize);
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize);
+    if (this.barChartInstance) this.barChartInstance.dispose();
+    if (this.pieChartInstance) this.pieChartInstance.dispose();
+  },
+  methods: {
+    async initBarChart() {
+      this.barChart.loading = true;
+      try {
+        const response = await axios.get('/api/shop/import/department-efficiency');
+        if (!response.data || !response.data.success) {
+          throw new Error(response.data.message || '??????????');
+        }
+        const rawData = response.data.data;
+        const chartData = Object.entries(rawData).map(([name, value]) => ({
+          name,
+          value: parseFloat(value).toFixed(2)
+        }));
+
+        await this.$nextTick();
+        const chartEl = this.$refs.barChartRef;
+        if (chartEl) {
+          this.barChartInstance = echarts.init(chartEl);
+          const option = {
+            title: { text: '??????????', left: 'center' },
+            tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}<br/>?????: {c} ?' },
+            grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
+            xAxis: { type: 'category', data: chartData.map(item => item.name), axisLabel: { rotate: 30, interval: 0 } },
+            yAxis: { type: 'value', name: '?????(?)' },
+            series: [{
+              name: '?????',
+              type: 'bar',
+              data: chartData.map(item => item.value),
+              barWidth: '40%',
+              itemStyle: {
+                borderRadius: [5, 5, 0, 0],
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                  { offset: 0, color: '#83bff6' },
+                  { offset: 1, color: '#188df0' }
+                ])
+              }
+            }]
+          };
+          this.barChartInstance.setOption(option);
+        }
+      } catch (err) {
+        this.barChart.error = err.message || '????';
+      } finally {
+        this.barChart.loading = false;
+      }
+    },
+    async initPieChart() {
+      this.pieChart.loading = true;
+      try {
+        const response = await axios.get('/api/shop/import/channel-diversity');
+        if (!response.data || !response.data.success) {
+          throw new Error(response.data.message || '???????????');
+        }
+        const rawData = response.data.data;
+        const chartData = Object.entries(rawData).map(([name, value]) => ({ name, value }));
+
+        await this.$nextTick();
+        const chartEl = this.$refs.pieChartRef;
+        if (chartEl) {
+          this.pieChartInstance = echarts.init(chartEl);
+          const option = {
+            title: { text: '?????????', left: 'center' },
+            tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' },
+            legend: { orient: 'vertical', left: 'left', top: '10%' },
+            series: [{
+              name: '????',
+              type: 'pie',
+              radius: [20, 140],
+              center: ['50%', '60%'],
+              roseType: 'area',
+              itemStyle: { borderRadius: 5 },
+              data: chartData
+            }]
+          };
+          this.pieChartInstance.setOption(option);
+        }
+      } catch (err) {
+        this.pieChart.error = err.message || '????';
+      } finally {
+        this.pieChart.loading = false;
+      }
+    },
+    handleResize() {
+      if (this.barChartInstance) this.barChartInstance.resize();
+      if (this.pieChartInstance) this.pieChartInstance.resize();
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+.department-efficiency-view {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  padding: 20px;
+}
+.page-header {
+  padding: 15px 20px;
+  background-color: #ffffff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+.page-title {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+}
+.page-description {
+  margin: 4px 0 0;
+  font-size: 14px;
+  color: #666;
+}
+.chart-area {
+  position: relative;
+  padding: 20px;
+  background-color: #ffffff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+  min-height: 540px;
+}
+.status-overlay {
+  position: absolute;
+  inset: 0; /* a shorthand for top, right, bottom, left */
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  background-color: rgba(255, 255, 255, 0.8);
+  border-radius: 8px;
+  z-index: 10;
+  color: #555;
+  font-size: 16px;
+}
+.status-overlay.error {
+  color: #f56c6c;
+}
+.error-message {
+  font-size: 14px;
+  color: #999;
+  margin-top: 8px;
+}
+</style>

+ 129 - 0
src/views/order/ordervalue/FunnelChart/index.vue

@@ -0,0 +1,129 @@
+<template>
+  <div class="chart-card">
+    <h3 class="chart-title">支付决策漏斗图</h3>
+    <div ref="funnelChart" style="width: 100%; height: 300px;"></div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import * as echarts from 'echarts';
+
+export default {
+  name: 'FunnelChart',
+  props: {
+    dateRange: {
+      type: Object,
+      default: () => ({ start: '', end: '' })
+    }
+  },
+  data() {
+    return {
+      totalOrders: 0,
+      unpaidOrders: 0,
+      rawData: {}
+    };
+  },
+  watch: {
+    dateRange: {
+      handler(newRange) {
+        this.fetchFunnelData(newRange);
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    this.fetchFunnelData();
+  },
+  methods: {
+    getMockData() {
+      return {
+        paidWithin5Mins: 7000,
+        paidBetween5And30Mins: 1000,
+        paidAfter30Mins: 0,
+        unpaidOrders: 2000
+      };
+    },
+    async fetchFunnelData(dateRange = null) {
+      try {
+        let apiUrl = '/api/analysis/payment-decision-funnel';
+        if (dateRange && dateRange.start && dateRange.end) {
+          apiUrl += `?startDate=${dateRange.start}&endDate=${dateRange.end}`;
+        }
+        const response = await axios.get(apiUrl);
+        const data = response.data.data;
+        this.rawData = data;
+        const paidSum = data.paidWithin5Mins + data.paidBetween5And30Mins + data.paidAfter30Mins;
+        this.totalOrders = paidSum + data.unpaidOrders;
+        this.unpaidOrders = data.unpaidOrders;
+      } catch (error) {
+        console.error('???????????????', error);
+        const mockData = this.getMockData();
+        this.rawData = mockData;
+        const paidSum = mockData.paidWithin5Mins + mockData.paidBetween5And30Mins + mockData.paidAfter30Mins;
+        this.totalOrders = paidSum + mockData.unpaidOrders;
+        this.unpaidOrders = mockData.unpaidOrders;
+      }
+      this.renderChart();
+    },
+    renderChart() {
+      const chartEl = this.$refs.funnelChart;
+      if (!this.rawData || !chartEl) return;
+      const myChart = echarts.init(chartEl);
+      const categories = ['30??????', '5-30????', '5?????', '????'];
+      const seriesData = [
+        this.rawData.paidAfter30Mins || 0,
+        this.rawData.paidBetween5And30Mins || 0,
+        this.rawData.paidWithin5Mins || 0,
+        this.totalOrders
+      ];
+      const option = {
+        tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+        grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+        xAxis: {
+          type: 'value',
+          axisLabel: { formatter: value => value.toLocaleString() }
+        },
+        yAxis: {
+          type: 'category',
+          data: categories
+        },
+        series: [
+          {
+            name: '???',
+            type: 'bar',
+            data: seriesData,
+            itemStyle: {
+              color: params => (params.dataIndex === 3 ? '#3366CC' : '#6699FF')
+            },
+            label: {
+              show: true,
+              position: 'right',
+              formatter: '{c}'
+            }
+          }
+        ]
+      };
+      myChart.setOption(option);
+      window.addEventListener('resize', () => myChart.resize());
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+.chart-card {
+  background-color: white;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+.chart-title {
+  text-align: center;
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20px;
+}
+</style>

+ 121 - 0
src/views/order/ordervalue/KpiCard/index.vue

@@ -0,0 +1,121 @@
+<template>
+  <div class="kpi-card">
+    <div class="card-header">
+      <h4 class="card-title">{{ title }}</h4>
+      <span class="icon-wrapper" :style="{ backgroundColor: iconColor }">
+        {{ icon }}
+      </span>
+    </div>
+    
+    <div class="value-area">
+      <p class="main-value">{{ value }}</p>
+      <p class="trend-text" :style="{ color: trendColorMap[trendColor] }">
+        <span class="trend-icon" :style="{ color: trendColorMap[trendColor] }">
+          {{ trendIcon }}
+        </span>
+        {{ Math.abs(parseFloat(trend)) }}% 较上月
+      </p>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'KpiCard',
+  props: {
+    title: String,
+    value: String,
+    trend: String,
+    trendColor: {
+      type: String,
+      default: 'green'
+    },
+    icon: {
+      type: String,
+      default: '??'
+    }
+  },
+  computed: {
+    trendColorMap() {
+      return {
+        green: '#38D6A4',
+        red: '#FF6347'
+      };
+    },
+    iconColorMap() {
+      return {
+        '???? (GMV)': '#6699FF',
+        'P80 ?????': '#F7D742',
+        'Top 5 ?????': '#FF9966',
+        '??????': '#4ECDC4'
+      };
+    },
+    iconColor() {
+      return this.iconColorMap[this.title] || '#6699FF';
+    },
+    trendIcon() {
+      if (!this.trend) return '';
+      return this.trend.includes('-') ? '?' : '?';
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+.kpi-card {
+  background-color: white;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+  display: flex;
+  flex-direction: column;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 15px;
+}
+
+.card-title {
+  font-size: 14px;
+  color: #666;
+  weight: 500;
+  margin: 0;
+  flex-grow: 1;
+}
+
+.icon-wrapper {
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  /* 背景色由 JS 动态控制 */
+}
+
+.main-value {
+  font-size: 24px;
+  font-weight: bold;
+  color: #333;
+  margin: 0; /* 修复: 移除默认边距 */
+}
+
+.trend-text {
+  font-size: 12px;
+  margin-top: 5px;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+}
+
+.trend-icon {
+  margin-right: 4px;
+  font-size: 14px;
+  line-height: 1;
+}
+</style>

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

@@ -0,0 +1,137 @@
+<template>
+  <div class="leakage-card">
+    <div class="card-header">
+      <h4 class="card-title">订单价值漏损分析 (退款)</h4>
+      <span class="icon-wrapper">💧</span>
+    </div>
+    <div class="value-area">
+      <p class="main-value">{{ leakageData.leakageRatePercent }}%</p>
+      <p class="sub-text">价值漏损率</p>
+    </div>
+    <div class="details-area">
+      <div class="detail-item">
+        <p class="detail-label">总退款金额</p>
+        <p class="detail-value refund">{{ leakageData.totalRefundAmount }}</p>
+      </div>
+      <div class="detail-item">
+        <p class="detail-label">总成功交易额</p>
+        <p class="detail-value success">{{ leakageData.totalSuccessAmount }}</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+
+export default {
+  name: 'LeakageCard',
+  props: {
+    dateRange: {
+      type: Object,
+      default: () => ({ start: '', end: '' })
+    }
+  },
+  data() {
+    return {
+      leakageData: {
+        totalRefundAmount: '?0.00',
+        leakageRatePercent: 0,
+        totalSuccessAmount: '?0.00'
+      },
+      currencyFormatter: new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' })
+    };
+  },
+  watch: {
+    dateRange: {
+      handler(newRange) {
+        this.fetchLeakageData(newRange);
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    this.fetchLeakageData();
+  },
+  methods: {
+    async fetchLeakageData(dateRange = null) {
+      try {
+        let apiUrl = '/api/analysis/leakage-rate';
+        if (dateRange && dateRange.start && dateRange.end) {
+          apiUrl += `?startDate=${dateRange.start}&endDate=${dateRange.end}`;
+        }
+        const response = await axios.get(apiUrl);
+        if (response.data) {
+          this.leakageData.totalRefundAmount = this.currencyFormatter.format(response.data.totalRefundAmount || 0);
+          this.leakageData.leakageRatePercent = (response.data.leakageRatePercent || 0).toFixed(1);
+          this.leakageData.totalSuccessAmount = this.currencyFormatter.format(response.data.totalSuccessAmount || 0);
+        }
+      } catch (error) {
+        console.error('?????????:', error);
+      }
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+.leakage-card {
+  background-color: white;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+  border-left: 4px solid #E6A23C;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+}
+.card-title {
+  font-size: 16px;
+  font-weight: 600;
+  margin: 0;
+}
+.icon-wrapper {
+  font-size: 24px;
+  color: #E6A23C;
+}
+.value-area {
+  text-align: center;
+  margin: 10px 0 20px;
+}
+.main-value {
+  font-size: 32px;
+  font-weight: bold;
+  color: #E6A23C;
+  margin: 0;
+}
+.sub-text {
+  font-size: 12px;
+  color: #999;
+  margin-top: 5px;
+}
+.details-area {
+  display: flex;
+  justify-content: space-around;
+  border-top: 1px solid #f0f0f0;
+  padding-top: 15px;
+}
+.detail-item {
+  text-align: center;
+}
+.detail-label {
+  font-size: 12px;
+  color: #999;
+  margin-bottom: 5px;
+}
+.detail-value {
+  font-size: 14px;
+  font-weight: 500;
+  margin: 0;
+}
+.detail-value.refund { color: #F56C6C; }
+.detail-value.success { color: #67C23A; }
+</style>

+ 173 - 0
src/views/order/ordervalue/Top5PieChart/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="chart-card">
+    <h3 class="chart-title">明星商品价值环图 (Top 5)</h3>
+    <div ref="pieChart" style="width: 100%; height: 300px;"></div>
+    
+    <div class="legend-area">
+      <div class="percentage-label">
+        <span class="percent-value">{{ top5Percent }}%</span>
+        <p class="percent-text">Top 5 商品贡献占比</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import * as echarts from 'echarts';
+
+export default {
+  name: 'Top5PieChart',
+  props: {
+    dateRange: {
+      type: Object,
+      default: () => ({ start: '', end: '' })
+    }
+  },
+  data() {
+    return {
+      chartData: [],
+      legendData: [],
+      top5Percent: 0,
+      colors: ['#3366CC', '#4ECDC4', '#A5D8FF', '#FFB347', '#FF6347']
+    };
+  },
+  watch: {
+    dateRange: {
+      handler(newRange) {
+        this.fetchPieData(newRange);
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    this.fetchPieData();
+  },
+  methods: {
+    async fetchPieData(dateRange = null) {
+      try {
+        let productsUrl = '/api/analysis/top5-products';
+        let percentageUrl = '/api/analysis/top5-percentage';
+        if (dateRange && dateRange.start && dateRange.end) {
+          const params = `?startDate=${dateRange.start}&endDate=${dateRange.end}`;
+          productsUrl += params;
+          percentageUrl += params;
+        }
+
+        const productsResponse = await axios.get(productsUrl);
+        const rawProducts = productsResponse.data || [];
+
+        this.chartData = rawProducts.map(product => ({
+          value: parseFloat(product.totalSales),
+          name: `${product.sku} (${product.name})`
+        }));
+
+        this.legendData = rawProducts.map(product => `${product.sku} (${product.name})`);
+
+        const percentageResponse = await axios.get(percentageUrl);
+        if (percentageResponse.data.success) {
+          this.top5Percent = Math.round(percentageResponse.data.data.top5Percentage);
+        }
+
+        this.renderChart();
+      } catch (error) {
+        console.error('?????????:', error);
+      }
+    },
+    renderChart() {
+      const chartEl = this.$refs.pieChart;
+      if (!this.chartData.length || !chartEl) return;
+      const myChart = echarts.init(chartEl);
+      const option = {
+        color: this.colors,
+        tooltip: {
+          trigger: 'item',
+          formatter: params => {
+            const index = this.chartData.findIndex(item => item.name === params.name);
+            const fullLabel = this.legendData[index] || params.name;
+            return `${fullLabel}: ${params.value.toLocaleString()} (${params.percent}%)`;
+          }
+        },
+        legend: {
+          show: true,
+          type: 'scroll',
+          orient: 'horizontal',
+          bottom: 10,
+          data: this.legendData,
+          itemGap: 20,
+          itemWidth: 15,
+          itemHeight: 10,
+          textStyle: { fontSize: 12 },
+          width: '90%',
+          height: 'auto'
+        },
+        series: [
+          {
+            name: 'Top 5 ????',
+            type: 'pie',
+            radius: ['50%', '70%'],
+            center: ['50%', '45%'],
+            data: this.chartData,
+            emphasis: {
+              itemStyle: {
+                shadowBlur: 10,
+                shadowOffsetX: 0,
+                shadowColor: 'rgba(0, 0, 0, 0.5)'
+              }
+            },
+            label: {
+              show: true,
+              formatter: params => params.name.split(' (')[0],
+              position: 'outside'
+            },
+            labelLine: { show: true }
+          }
+        ]
+      };
+      myChart.setOption(option);
+      window.addEventListener('resize', () => myChart.resize());
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+/* 样式部分和之前一样,不用修改 */
+.chart-card {
+  background-color: white;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+  position: relative;
+}
+.chart-title {
+  text-align: center;
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20px;
+}
+.percentage-label {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%); 
+    pointer-events: none; 
+    z-index: 10;
+    text-align: center;
+}
+.percent-value {
+    font-size: 24px;
+    font-weight: bold;
+    color: #333;
+}
+.percent-text {
+    font-size: 12px;
+    color: #666;
+    margin-top: 5px;
+}
+[ref="pieChart"] {
+    position: relative;
+}
+</style>

+ 0 - 0
src/views/order/ordervalue/Top5PieChart_restore/index.vue


+ 441 - 0
src/views/order/ordervalue/index.vue

@@ -0,0 +1,441 @@
+<template>
+  <div class="order-value-view">
+    <!-- 头部区域 -->
+    <header class="page-header">
+      <h1 class="page-title">订单价值</h1>
+      <div class="header-controls">
+        <span class="control-label">时光回溯控制器</span>
+        <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>
+  </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="⏱" />
+    </section>
+    
+    <!-- 图表区域 -->
+    <section class="charts-area">
+      <!-- 支付决策漏斗图 -->
+      <div class="chart-wrapper funnel-chart-wrapper">
+        <FunnelChart :date-range="currentDateRange" /> 
+      </div>
+      
+      <!-- 明星商品价值环图 (Top 5) -->
+      <div class="chart-wrapper">
+        <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 axios from 'axios';
+import KpiCard from './KpiCard/index.vue';
+import FunnelChart from './FunnelChart/index.vue';
+import Top5PieChart from './Top5PieChart/index.vue';
+import LeakageCard from './LeakageCard/index.vue';
+
+export default {
+  name: 'OrderValue',
+  components: {
+    KpiCard,
+    FunnelChart,
+    Top5PieChart,
+    LeakageCard
+  },
+  data() {
+    return {
+      selectedDate: new Date().toISOString().split('T')[0],
+      activeTab: '7d',
+      maxDate: '',
+      currentDateRange: { start: '', end: '' },
+      kpiData: {
+        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();
+  },
+  methods: {
+    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);
+    },
+    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 [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
+          axios.get(`/api/analysis/gmv?startDate=${startDate}&endDate=${endDate}`),
+          axios.get(`/api/analysis/r-big?startDate=${startDate}&endDate=${endDate}`),
+          axios.get(`/api/analysis/top5-percentage?startDate=${startDate}&endDate=${endDate}`),
+          axios.get(`/api/analysis/average-payment-time?startDate=${startDate}&endDate=${endDate}`)
+        ]);
+
+        const gmvValue = gmvRes.data || 0;
+        this.kpiData.gmv = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(gmvValue);
+
+        const p80Value = p80Res.data?.rBigRatio || 0;
+        this.kpiData.p80Contribution = `${Math.round(p80Value)}%`;
+
+        const top5Value = top5Res.data?.data?.top5Percentage || 0;
+        this.kpiData.top5Contribution = `${Math.round(top5Value)}%`;
+
+        const avgSeconds = avgTimeRes.data?.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%';
+        }
+      } catch (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)}%`;
+    },
+    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;
+    },
+    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));
+
+        const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
+          axios.get(`/api/analysis/gmv?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`),
+          axios.get(`/api/analysis/r-big?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`),
+          axios.get(`/api/analysis/top5-percentage?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`),
+          axios.get(`/api/analysis/average-payment-time?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`)
+        ]);
+
+        return {
+          gmv: gmvRes.data || 0,
+          p80: p80Res.data?.rBigRatio || 0,
+          top5: top5Res.data?.data?.top5Percentage || 0,
+          avgTime: avgTimeRes.data?.averagePaymentSeconds || 0
+        };
+      } catch (error) {
+        console.error('????????:', error);
+        return null;
+      }
+    },
+    async initDashboard() {
+      try {
+        const res = await axios.get('/api/analysis/max-date');
+        this.maxDate = res.data;
+        this.selectedDate = res.data;
+
+        const startDate = this.formatYmd(this.addDays(res.data, -6));
+        this.currentDateRange = { start: startDate, end: res.data };
+        await this.fetchAllApiData();
+        this.kpiData.gmvTrend = '+0%';
+        this.kpiData.p80Trend = '+0%';
+        this.kpiData.top5Trend = '+0%';
+        this.kpiData.avgTimeTrend = '+0%';
+      } 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%';
+      }
+    },
+    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 };
+
+      if (type === 'tm') {
+        await this.fetchAllApiData({ start, end });
+        const previousMonthData = await this.fetchPreviousMonthData(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);
+        }
+      } else {
+        this.fetchAllApiData({ start, end });
+      }
+    },
+    async fetchAllKpiData() {
+      try {
+        const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
+          axios.get('/api/analysis/gmv'),
+          axios.get('/api/analysis/r-big'),
+          axios.get('/api/analysis/top5-percentage'),
+          axios.get('/api/analysis/average-payment-time')
+        ]);
+
+        const gmvValue = gmvRes.data || 0;
+        this.kpiData.gmv = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(gmvValue);
+        const p80Value = p80Res.data?.rBigRatio || 0;
+        this.kpiData.p80Contribution = `${Math.round(p80Value)}%`;
+        const top5Value = top5Res.data?.data?.top5Percentage || 0;
+        this.kpiData.top5Contribution = `${Math.round(top5Value)}%`;
+        const avgSeconds = avgTimeRes.data?.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 = '????';
+        this.kpiData.p80Contribution = '????';
+        this.kpiData.top5Contribution = '????';
+        this.kpiData.averagePaymentTime = '????';
+      }
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+.header-controls {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin-top: 10px;
+}
+.control-label {
+  font-size: 14px;
+  color: #666;
+}
+.control-item-group {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+.date-input {
+  padding: 4px 8px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-size: 14px;
+}
+.time-tab {
+  padding: 5px 15px;
+  border: 1px solid #ddd;
+  background-color: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: all 0.2s;
+}
+.time-tab:hover {
+  border-color: #188df0;
+  color: #188df0;
+}
+.time-tab.active {
+  background-color: #188df0;
+  color: #fff;
+  border-color: #188df0;
+}
+/* 样式部分和之前一样,不用修改 */
+.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;
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+</style>

+ 343 - 0
src/views/order/related/index.vue

@@ -0,0 +1,343 @@
+<template>
+  <div class="product-analysis-view">
+    <!-- 页面头部 -->
+    <header class="page-header">
+      <h1 class="page-title">商品关联透视</h1>
+      <p class="page-subtitle">探索商品之间的共现购买关系,发现最佳销售组合。</p>
+    </header>
+
+    <!-- 共现购买关系表格 -->
+    <div class="table-container">
+      <h2 class="table-title">热门商品组合</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>商品 A (SKU)</th>
+            <th>商品 B (SKU)</th>
+            <th>共同购买次数</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-if="loading">
+            <td colspan="3">正在努力加载数据中... 🐾</td>
+          </tr>
+          <!-- ✨【保持列位置不变,只调整数字位置】✨ -->
+          <tr v-else-if="coPurchaseData.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>
+          </tr>
+          <tr v-else-if="!loading && coPurchaseData.length === 0">
+            <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>
+      </div>
+    </div>
+
+    <!-- 网络图部分(保持不变) -->
+    <div class="chart-container">
+      <h2 class="chart-title">共购规则网络图</h2>
+      <div ref="networkChart" style="width: 100%; height: 600px;"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import * as echarts from 'echarts';
+
+export default {
+  name: 'OrderRelated',
+  data() {
+    return {
+      coPurchaseData: [],
+      loading: true,
+      currentPage: 1,
+      itemsPerPage: 10
+    };
+  },
+  computed: {
+    paginatedData() {
+      const start = (this.currentPage - 1) * this.itemsPerPage;
+      const end = start + this.itemsPerPage;
+      return this.coPurchaseData.slice(start, end);
+    },
+    totalPages() {
+      return Math.ceil(this.coPurchaseData.length / this.itemsPerPage);
+    }
+  },
+  mounted() {
+    this.fetchData();
+  },
+  methods: {
+    prevPage() {
+      if (this.currentPage > 1) {
+        this.currentPage -= 1;
+      }
+    },
+    nextPage() {
+      if (this.currentPage < this.totalPages) {
+        this.currentPage += 1;
+      }
+    },
+    async fetchData() {
+      try {
+        const response = await axios.get('/api/analysis/co-purchase');
+        this.coPurchaseData = response.data || [];
+        this.renderNetworkChart(this.coPurchaseData);
+      } catch (error) {
+        console.error("获取共现购买数据失败:", error);
+      } finally {
+        this.loading = false;
+      }
+    },
+    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++;
+      }
+      
+      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 // 恢复到默认线条粗细
+      }
+    });
+  });
+
+  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}`;
+        }
+        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 // 直线连接
+        },
+        // 鼠标悬停效果
+        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; }
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 20px;
+  gap: 15px;
+}
+.page-button {
+  padding: 8px 16px;
+  background-color: #f0f4f8;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  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;
+  font-weight: 500;
+}
+</style>

+ 538 - 0
src/views/order/shopvalue/index.vue

@@ -0,0 +1,538 @@
+<template>
+  <div class="shop-value-analysis-page">
+    <!-- 1. 页面主标题 -->
+    <header class="page-header">
+      <div>
+        <h1 class="main-title">店铺价值分析</h1>
+        <p class="subtitle">实时监控关键指标与趋势</p>
+      </div>
+    </header>
+
+    <!-- 新增: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>
+            <span class="kpi-value">¥{{ formatNumber(topProductData.totalSales) }}</span>
+          </div>
+          <div class="kpi-card">
+            <span class="kpi-title">Top 5 商品总销售额</span>
+            <span class="kpi-value">¥{{ formatNumber(topProductData.top5TotalSales) }}</span>
+          </div>
+          <div class="kpi-card">
+            <span class="kpi-title">Top 5 贡献占比</span>
+            <span class="kpi-value">{{ (topProductData.contributionRatio * 100).toFixed(2) }}%</span>
+          </div>
+        </div>
+        
+        <!-- 下方的环形图 -->
+        <div class="chart-card">
+          <h3 class="chart-title">Top 5 商品销售分布</h3>
+          <div ref="donutChartRef" style="width: 100%; height: 400px;"></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 3. 筛选与操作栏 -->
+    <div class="filter-bar">
+      <div class="filters">
+        <span>筛选:</span>
+        <select><option>全部时间</option></select>
+        <select><option>全部渠道</option></select>
+        <select><option>全部区域</option></select>
+      </div>
+      <div class="actions">
+        <button class="btn-secondary">导出数据</button>
+        <button class="btn-primary" @click="fetchData">刷新</button>
+      </div>
+    </div>
+
+    <!-- 4. 图表卡片区域 -->
+    <div class="charts-container">
+      <!-- 部门图表:销量 + 销售金额 -->
+      <div class="chart-card">
+        <div class="card-header">
+          <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>
+        </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>
+        </div>
+        <div class="chart-wrapper">
+          <div ref="platformChart" class="chart-instance"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import * as echarts from 'echarts';
+
+export default {
+  name: 'ShopValue',
+  data() {
+    return {
+      topProductData: {
+        totalSales: 0,
+        top5TotalSales: 0,
+        contributionRatio: 0,
+        top5Products: []
+      }
+    };
+  },
+  mounted() {
+    this.fetchData();
+    window.addEventListener('resize', this.handleResize);
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize);
+  },
+  methods: {
+    formatNumber(num) {
+      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();
+    },
+    initDonutChart(chartData) {
+      const chartEl = this.$refs.donutChartRef;
+      if (!chartEl) return;
+      const myChart = echarts.getInstanceByDom(chartEl) || echarts.init(chartEl);
+      const option = {
+        tooltip: {
+          trigger: 'item',
+          formatter: '{a} <br/>{b} : ?{c} ({d}%)'
+        },
+        legend: {
+          orient: 'horizontal',
+          bottom: '0%',
+          left: 'center'
+        },
+        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,
+                fontSize: '14',
+                fontWeight: 'bold'
+              }
+            },
+            data: chartData
+          }
+        ]
+      };
+      myChart.setOption(option, true);
+      return myChart;
+    },
+    async fetchTopProductData() {
+      try {
+        const response = await axios.get('/api/shop/import/top-product-contribution');
+        if (response.data.success) {
+          const data = response.data.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);
+        }
+      } catch (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);
+
+      const categories = data.map(item => item[categoryKey]);
+      const series = seriesConfig.map(({ key, name, color, formatter }) => ({
+        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)
+      }));
+
+      const option = {
+        tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+        legend: { data: seriesConfig.map(c => c.name), top: 0 },
+        grid: { left: '80px', right: '80px', top: '40px', bottom: '60px' },
+        xAxis: {
+          type: 'category',
+          data: categories,
+          axisLabel: { interval: 0, rotate: 45, color: '#666', margin: 20 },
+          axisTick: { alignWithLabel: true }
+        },
+        yAxis: seriesConfig.map(({ name, formatter }, index) => ({
+          type: 'value',
+          name,
+          nameTextStyle: { color: '#666' },
+          axisLabel: {
+            color: '#666',
+            formatter: formatter || (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;
+    },
+    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);
+
+      const categories = data.map(item => item.name);
+
+      const option = {
+        tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+        legend: { data: ['??', '????'], top: 0 },
+        grid: { left: '80px', right: '80px', top: '40px', bottom: '60px' },
+        xAxis: {
+          type: 'category',
+          data: categories,
+          axisLabel: { interval: 0, rotate: 45, color: '#666', margin: 20 },
+          axisTick: { alignWithLabel: true }
+        },
+        yAxis: [
+          {
+            type: 'value',
+            name: '??',
+            nameTextStyle: { color: '#666' },
+            axisLabel: {
+              color: '#5470c6',
+              formatter: value => {
+                if (value >= 1000000) return (value / 1000000) + 'M';
+                if (value >= 1000) return (value / 1000) + 'K';
+                return value;
+              }
+            },
+            position: 'left'
+          },
+          {
+            type: 'value',
+            name: '????(?)',
+            nameTextStyle: { color: '#e9c46a' },
+            axisLabel: {
+              color: '#e9c46a',
+              formatter: value => `?${value.toFixed(2)}`
+            },
+            position: 'right',
+            offset: 40
+          }
+        ],
+        series: [
+          {
+            name: '??',
+            type: 'bar',
+            data: data.map(item => item.totalVolume),
+            barMaxWidth: '40px',
+            itemStyle: { color: '#5470c6', borderRadius: [4, 4, 0, 0] },
+            yAxisIndex: 0
+          },
+          {
+            name: '????',
+            type: 'bar',
+            data: data.map(item => item.avgOrderValue),
+            barMaxWidth: '40px',
+            itemStyle: { color: '#e9c46a', borderRadius: [4, 4, 0, 0] },
+            yAxisIndex: 1
+          }
+        ]
+      };
+
+      myChart.setOption(option, true);
+      return myChart;
+    },
+    async fetchData() {
+      try {
+        const [
+          unitRes,
+          channelTotalRes,
+          channelContributionRes,
+          channelRoiValueRes
+        ] = await Promise.all([
+          axios.get('/api/shop/import/unit-contribution'),
+          axios.get('/api/shop/import/channel-total-contribution'),
+          axios.get('/api/shop/import/channel-contribution'),
+          axios.get('/api/shop/import/channel-roi-value')
+        ]);
+
+        if (unitRes.data.success && unitRes.data.data) {
+          const sortedData = [...unitRes.data.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)}`
+              }
+            ]
+          );
+        }
+
+        if (channelTotalRes.data.success && channelTotalRes.data.data) {
+          const sortedData = [...channelTotalRes.data.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 (
+          channelContributionRes.data.success &&
+          channelRoiValueRes.data.success &&
+          channelContributionRes.data.data &&
+          channelRoiValueRes.data.data
+        ) {
+          const platformSales = Object.entries(channelContributionRes.data.data).map(([name, value]) => ({
+            name,
+            totalVolume: Number(value)
+          }));
+
+          const platformAvgOrder = Object.entries(channelRoiValueRes.data.data).map(([name, value]) => ({
+            name,
+            avgOrderValue: Number(value)
+          }));
+
+          const mergedData = platformSales
+            .map(sale => ({
+              ...sale,
+              avgOrderValue: platformAvgOrder.find(avg => avg.name === sale.name)?.avgOrderValue || 0
+            }))
+            .sort((a, b) => b.totalVolume - a.totalVolume);
+
+          this.initPlatformValueChart(this.$refs.platformChart, mergedData);
+        }
+
+        this.fetchTopProductData();
+      } catch (error) {
+        console.error('??????:', error);
+      }
+    }
+  }
+};
+</script>
+
+
+<style scoped>
+/* 保持原有样式不变 */
+.shop-value-analysis-page {
+  padding: 24px;
+  background-color: #f4f7f9;
+  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
+}
+
+.page-header {
+  margin-bottom: 20px;
+}
+.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 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+.kpi-title {
+  font-size: 14px;
+  color: #4e5969;
+}
+.kpi-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #1d2129;
+}
+.kpi-comparison {
+  font-size: 12px;
+  margin-top: 8px;
+}
+.kpi-comparison.positive { color: #00b42a; }
+.kpi-comparison.negative { color: #f53f3f; }
+
+.filter-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: #fff;
+  padding: 16px 20px;
+  border-radius: 4px;
+  border: 1px solid #e5e6eb;
+  margin-bottom: 20px;
+}
+.filters { display: flex; align-items: center; gap: 16px; font-size: 14px; color: #4e5969; }
+.filters select { padding: 4px 8px; border-radius: 4px; border: 1px solid #e5e6eb; }
+.actions { display: flex; gap: 12px; }
+.btn-primary { background-color: #1677ff; color: #fff; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; }
+.btn-secondary { background-color: #f2f3f5; color: #4e5969; border: 1px solid #e5e6eb; padding: 6px 16px; border-radius: 4px; cursor: pointer; }
+
+.charts-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+.chart-card {
+  background: #fff;
+  padding: 24px;
+  border-radius: 4px;
+  border: 1px solid #e5e6eb;
+  display: grid;
+  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; }
+
+/* 新增的Top 5商品贡献分析样式 */
+.top-product-contribution-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px; /* 卡片和图表之间的间距 */
+  width: 100%;
+}
+
+/* KPI卡片网格布局 */
+.kpi-card-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr); /* 三个卡片平分宽度 */
+  gap: 20px; /* 卡片之间的间距 */
+}
+
+/* 单个KPI卡片的样式 */
+.kpi-card {
+  background-color: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+}
+
+.kpi-title {
+  font-size: 14px;
+  color: #666;
+  margin-bottom: 10px;
+}
+
+.kpi-value {
+  font-size: 28px;
+  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>

+ 408 - 20
src/views/sale/overview/index.vue

@@ -35,6 +35,44 @@
       </div>
     </el-card>
 
+    <!-- 视图选择区域 -->
+    <el-card v-if="results.summary" class="mb-20">
+      <div slot="header">
+        <span><i class="el-icon-s-operation"></i> 数据视图选择</span>
+      </div>
+      <div class="view-selector">
+        <el-radio-group v-model="selectedView" @change="handleViewChange">
+          <el-radio-button label="overall">总体概览</el-radio-button>
+          <el-radio-button label="category">按品类查看</el-radio-button>
+          <el-radio-button label="sku">按SKU查看</el-radio-button>
+        </el-radio-group>
+        
+        <!-- 品类选择 -->
+        <el-select v-if="selectedView === 'category' && results.category_list && results.category_list.length > 0" 
+                   v-model="selectedCategory" 
+                   placeholder="选择品类" 
+                   @change="handleCategoryChange"
+                   style="margin-left: 10px; width: 200px;">
+          <el-option v-for="category in results.category_list" 
+                     :key="category" 
+                     :label="category" 
+                     :value="category" />
+        </el-select>
+        
+        <!-- SKU选择 -->
+        <el-select v-if="(selectedView === 'sku' || selectedView === 'category') && getAvailableSkus().length > 0" 
+                   v-model="selectedSku" 
+                   placeholder="选择SKU" 
+                   @change="handleSkuChange"
+                   style="margin-left: 10px; width: 200px;">
+          <el-option v-for="sku in getAvailableSkus()" 
+                     :key="sku" 
+                     :label="sku" 
+                     :value="sku" />
+        </el-select>
+      </div>
+    </el-card>
+
     <!-- 关键指标卡片 -->
     <el-row :gutter="20" class="mb-20">
       <el-col :xs="24" :sm="12" :md="8" :lg="6">
@@ -144,6 +182,58 @@
         </el-card>
       </el-col>
     </el-row>
+
+    <!-- 异常数据详情 -->
+    <el-card v-if="results.anomalies && results.anomalies.anomaly_count > 0" class="mb-20">
+      <div slot="header">
+        <span><i class="el-icon-warning-outline"></i> 异常数据详情</span>
+        <span class="header-desc">共检测到 {{ results.anomalies.anomaly_count }} 个异常,异常率 {{ results.anomalies.anomaly_rate.toFixed(2) }}%</span>
+      </div>
+      <el-table :data="anomalyData" style="width: 100%">
+        <el-table-column prop="date" label="日期" width="120">
+          <template slot-scope="scope">
+            {{ scope.row.date || 'N/A' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="sku" label="SKU" width="180">
+          <template slot-scope="scope">
+            {{ scope.row.sku || 'N/A' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="type" label="异常类型" width="120">
+          <template slot-scope="scope">
+            <el-tag :type="getAnomalyTypeTag(scope.row.type)">
+              {{ getAnomalyTypeName(scope.row.type) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="reason" label="异常原因" min-width="300">
+          <template slot-scope="scope">
+            <span class="anomaly-reason">{{ scope.row.reason }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="value" label="实际值" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.value.toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="expected" label="预期值" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.expected.toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="deviation" label="偏差程度" width="100">
+          <template slot-scope="scope">
+            <el-progress 
+              :percentage="Math.min(scope.row.deviation * 20, 100)" 
+              :color="getDeviationColor(scope.row.deviation)"
+              :stroke-width="10"
+            />
+            <span class="deviation-value">{{ scope.row.deviation.toFixed(2) }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
   </div>
 </template>
 
@@ -178,6 +268,10 @@ export default {
       promotionSeries: [],
       salesSeries: [],
       anomalySeries: [],
+      // 视图选择相关
+      selectedView: 'overall',
+      selectedCategory: '',
+      selectedSku: '',
       // 文件上传相关
       upload: {
         // 是否显示弹出层
@@ -227,14 +321,14 @@ export default {
                       file.name.endsWith('.xlsx') ||
                       file.name.endsWith('.xls') ||
                       file.name.endsWith('.csv')
-      const isLt20M = file.size / 1024 / 1024 < 20
+      const isLt300M = file.size / 1024 / 1024 < 300
 
       if (!isExcel) {
         this.$modal.msgError('上传文件只能是 xlsx/xls/csv 格式!')
         return false
       }
-      if (!isLt20M) {
-        this.$modal.msgError('上传文件大小不能超过 20MB!')
+      if (!isLt300M) {
+        this.$modal.msgError('上传文件大小不能超过 300MB!')
         return false
       }
       return true
@@ -245,11 +339,30 @@ export default {
     },
     /** 自定义上传方法 */
     customUpload(options) {
-      const file = options.file
+      console.log('customUpload called')
+      console.log('options:', options)
+      console.log('options.file:', options.file)
+      if (!options.file) {
+        console.error('No file in options')
+        this.$modal.msgError('请选择要上传的文件')
+        options.onError(new Error('No file to upload'))
+        return
+      }
+      console.log('options.file.raw:', options.file.raw)
+      const file = options.file.raw || options.file
+      console.log('file to upload:', file)
+      if (!file) {
+        console.error('No file to upload')
+        this.$modal.msgError('请选择要上传的文件')
+        options.onError(new Error('No file to upload'))
+        return
+      }
+      console.log('Starting file upload to:', '/statistics/sales/upload')
       this.upload.isUploading = true
       uploadAndAnalyzeSales(file).then(response => {
+        console.log('uploadAndAnalyzeSales response:', response)
         this.upload.isUploading = false
-        if (response.code === 200) {
+        if (response && response.code === 200) {
           this.$modal.msgSuccess('文件上传并分析成功')
           // response.data 就是分析结果
           this.results = response.data || {}
@@ -259,12 +372,14 @@ export default {
           })
           options.onSuccess(response)
         } else {
+          console.error('Upload failed with response:', response)
           this.$modal.msgError(response.msg || '分析失败')
           options.onError(new Error(response.msg || '分析失败'))
         }
         // 重置上传组件
         this.$refs.upload.clearFiles()
       }).catch(error => {
+        console.error('uploadAndAnalyzeSales error:', error)
         this.upload.isUploading = false
         const errorMsg = error.response?.data?.msg || error.message || '文件上传失败,请重试'
         this.$modal.msgError(errorMsg)
@@ -302,27 +417,199 @@ export default {
     },
     /** 计算关键指标 */
     calculateMetrics() {
-      // 模拟数据,实际项目中应从results中计算
-      this.totalSales = 12580
-      this.salesGrowthRate = 12.5
-      this.avgPrice = 89.65
-      this.priceChange = -2.3
-      this.avgPromotion = 15.8
-      this.promotionChange = 3.2
-      this.anomalyDetectionRate = 5.7
+      if (!this.results.summary) {
+        return
+      }
       
-      // 模拟趋势数据
-      this.timeSeries = ['1月', '2月', '3月', '4月', '5月', '6月']
-      this.priceSeries = [95.2, 92.8, 90.5, 88.9, 90.2, 89.6]
-      this.promotionSeries = [12.5, 13.2, 14.8, 15.5, 16.2, 15.8]
-      this.salesSeries = [8500, 9200, 10500, 11200, 11800, 12580]
-      this.anomalySeries = [4.2, 3.8, 5.1, 6.5, 7.2, 5.7]
+      // 根据选择的视图计算指标
+      switch (this.selectedView) {
+        case 'overall':
+          this.calculateOverallMetrics()
+          break
+        case 'category':
+          this.calculateCategoryMetrics()
+          break
+        case 'sku':
+          this.calculateSkuMetrics()
+          break
+        default:
+          this.calculateOverallMetrics()
+      }
+    },
+    
+    /** 计算总体指标 */
+    calculateOverallMetrics() {
+      if (this.results.summary) {
+        this.totalSales = this.results.summary.total_quantity || 0
+        this.avgPrice = this.results.summary.total_revenue / this.results.summary.total_quantity || 0
+        
+        // 模拟增长率和变化率数据
+        this.salesGrowthRate = 12.5
+        this.priceChange = -2.3
+        this.avgPromotion = 15.8
+        this.promotionChange = 3.2
+        this.anomalyDetectionRate = 5.7
+        
+        // 模拟趋势数据
+        this.timeSeries = ['1月', '2月', '3月', '4月', '5月', '6月']
+        this.priceSeries = [95.2, 92.8, 90.5, 88.9, 90.2, 89.6]
+        this.promotionSeries = [12.5, 13.2, 14.8, 15.5, 16.2, 15.8]
+        this.salesSeries = [8500, 9200, 10500, 11200, 11800, 12580]
+        this.anomalySeries = [4.2, 3.8, 5.1, 6.5, 7.2, 5.7]
+      }
+    },
+    
+    /** 计算品类指标 */
+    calculateCategoryMetrics() {
+      if (this.selectedCategory && this.results.categories && this.results.categories[this.selectedCategory]) {
+        const categoryData = this.results.categories[this.selectedCategory]
+        this.totalSales = categoryData.total_quantity || 0
+        this.avgPrice = categoryData.avg_price || 0
+        
+        // 模拟增长率和变化率数据
+        this.salesGrowthRate = 15.2
+        this.priceChange = 1.8
+        this.avgPromotion = 18.5
+        this.promotionChange = 2.5
+        this.anomalyDetectionRate = 4.8
+        
+        // 使用品类的趋势数据
+        this.timeSeries = categoryData.date_series || ['1月', '2月', '3月', '4月', '5月', '6月']
+        this.priceSeries = categoryData.price_series || [90.2, 91.5, 92.8, 93.1, 92.5, 91.8]
+        this.salesSeries = categoryData.quantity_series || [7500, 8200, 9100, 9800, 10500, 11200]
+        this.promotionSeries = [16.5, 17.2, 18.1, 19.0, 18.8, 18.5]
+        this.anomalySeries = [3.8, 4.2, 4.5, 5.1, 4.9, 4.8]
+      } else {
+        // 默认选择第一个品类
+        if (this.results.category_list && this.results.category_list.length > 0) {
+          this.selectedCategory = this.results.category_list[0]
+          this.calculateCategoryMetrics()
+        } else {
+          this.calculateOverallMetrics()
+        }
+      }
+    },
+    
+    /** 计算SKU指标 */
+    calculateSkuMetrics() {
+      if (this.selectedSku && this.results.data && this.results.data[this.selectedSku]) {
+        const skuData = this.results.data[this.selectedSku]
+        this.totalSales = skuData.total_quantity || 0
+        this.avgPrice = skuData.avg_price || 0
+        
+        // 模拟增长率和变化率数据
+        this.salesGrowthRate = 22.8
+        this.priceChange = -0.5
+        this.avgPromotion = 22.5
+        this.promotionChange = 3.8
+        this.anomalyDetectionRate = 3.2
+        
+        // 使用SKU的趋势数据
+        this.timeSeries = skuData.date_series || ['1月', '2月', '3月', '4月', '5月', '6月']
+        this.priceSeries = skuData.price_series || [85.2, 84.8, 85.1, 84.9, 84.7, 84.5]
+        this.salesSeries = skuData.quantity_series || [1200, 1350, 1500, 1650, 1800, 1950]
+        this.promotionSeries = [20.5, 21.2, 21.8, 22.5, 23.0, 22.5]
+        this.anomalySeries = [2.8, 3.1, 3.3, 3.5, 3.2, 3.0]
+      } else {
+        // 默认选择第一个SKU
+        if (this.results.sku_list && this.results.sku_list.length > 0) {
+          this.selectedSku = this.results.sku_list[0]
+          this.calculateSkuMetrics()
+        } else {
+          this.calculateOverallMetrics()
+        }
+      }
+    },
+    
+    /** 处理视图变化 */
+    handleViewChange() {
+      this.calculateMetrics()
+      this.renderCharts()
+    },
+    
+    /** 处理品类变化 */
+    handleCategoryChange() {
+      // Reset selected SKU and select first available in new category
+      const availableSkus = this.getAvailableSkus()
+      if (availableSkus.length > 0) {
+        this.selectedSku = availableSkus[0]
+      } else {
+        this.selectedSku = ''
+      }
+      this.calculateMetrics()
+      this.renderCharts()
+    },
+    
+    /** 处理SKU变化 */
+    handleSkuChange() {
+      this.calculateMetrics()
+      this.renderCharts()
+    },
+    
+    /** 获取可用的SKU列表 */
+    getAvailableSkus() {
+      if (this.selectedView === 'category' && this.selectedCategory && this.results.category_skus) {
+        return this.results.category_skus[this.selectedCategory] || []
+      } else {
+        return this.results.sku_list || []
+      }
+    },
+    
+    /** 获取异常类型标签 */
+    getAnomalyTypeTag(type) {
+      switch (type) {
+        case 'quantity_spike':
+        case 'sku_price_spike':
+          return 'warning'
+        case 'quantity_drop':
+        case 'sku_price_drop':
+          return 'danger'
+        case 'price_spike':
+          return 'info'
+        case 'price_drop':
+          return 'success'
+        default:
+          return 'primary'
+      }
+    },
+    
+    /** 获取异常类型名称 */
+    getAnomalyTypeName(type) {
+      switch (type) {
+        case 'quantity_spike':
+          return '销量激增'
+        case 'quantity_drop':
+          return '销量骤降'
+        case 'price_spike':
+          return '价格上涨'
+        case 'price_drop':
+          return '价格下降'
+        case 'sku_price_spike':
+          return 'SKU涨价'
+        case 'sku_price_drop':
+          return 'SKU降价'
+        default:
+          return '异常'
+      }
+    },
+    
+    /** 获取偏差程度颜色 */
+    getDeviationColor(deviation) {
+      if (deviation > 3) {
+        return '#ff4d4f'
+      } else if (deviation > 2.5) {
+        return '#fa8c16'
+      } else if (deviation > 2) {
+        return '#faad14'
+      } else {
+        return '#52c41a'
+      }
     },
     /** 初始化图表 */
     initCharts() {
       if (this.$refs.priceTrendChart) {
         this.priceTrendChart = echarts.init(this.$refs.priceTrendChart, 'macarons')
-      }
+        }
       if (this.$refs.promotionTrendChart) {
         this.promotionTrendChart = echarts.init(this.$refs.promotionTrendChart, 'macarons')
       }
@@ -577,6 +864,15 @@ export default {
         this.anomalyDetectionChart.resize()
       }
     }
+  },
+  computed: {
+    /** 异常数据列表 */
+    anomalyData() {
+      if (this.results.anomalies && this.results.anomalies.anomalies) {
+        return this.results.anomalies.anomalies.sort((a, b) => b.deviation - a.deviation)
+      }
+      return []
+    }
   }
 }
 </script>
@@ -704,4 +1000,96 @@ export default {
     font-weight: normal;
   }
 }
+
+.view-selector {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+  
+  .el-radio-group {
+    display: flex;
+    align-items: center;
+  }
+  
+  .el-select {
+    margin-top: 5px;
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .view-selector {
+    flex-direction: column;
+    align-items: flex-start;
+    
+    .el-select {
+      width: 100% !important;
+      margin-left: 0 !important;
+    }
+  }
+}
+
+/* Anomaly styles */
+.anomaly-reason {
+  line-height: 1.4;
+  color: #303133;
+}
+
+.deviation-value {
+  display: block;
+  text-align: center;
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+::v-deep .el-table .cell {
+  padding: 12px 10px;
+}
+
+::v-deep .el-table__row:hover {
+  background-color: #f5f7fa !important;
+}
+
+::v-deep .el-tag {
+  margin-right: 0;
+}
+
+/* Anomaly type tag styles */
+::v-deep .el-tag--warning {
+  background-color: #fff7e6;
+  border-color: #ffd591;
+  color: #fa8c16;
+}
+
+::v-deep .el-tag--danger {
+  background-color: #fff1f0;
+  border-color: #ffccc7;
+  color: #f5222d;
+}
+
+::v-deep .el-tag--info {
+  background-color: #e6f7ff;
+  border-color: #91d5ff;
+  color: #1890ff;
+}
+
+::v-deep .el-tag--success {
+  background-color: #f6ffed;
+  border-color: #b7eb8f;
+  color: #52c41a;
+}
+
+/* Progress bar styles */
+::v-deep .el-progress {
+  margin-bottom: 4px;
+}
+
+::v-deep .el-progress-bar__outer {
+  background-color: #f0f0f0;
+}
+
+::v-deep .el-progress-bar__inner {
+  border-radius: 5px;
+}
 </style>

+ 366 - 157
src/views/sale/trendPred/index.vue

@@ -16,6 +16,8 @@
           :http-request="customUpload"
           :disabled="upload.isUploading"
           :on-change="handleFileChange"
+          :on-success="handleUploadSuccess"
+          :on-error="handleUploadError"
           :before-upload="beforeUpload"
           :auto-upload="false"
           :show-file-list="false"
@@ -30,14 +32,22 @@
       <div class="toolbar-status muted" v-else>未上传</div>
     </div>
 
-    <!-- SKU选择 + 预测周期选择 + 基础信息板块 -->
+    <!-- 预测设置 + 基础信息板块 -->
     <div class="bg-white rounded-xl p-6 mb-20 shadow-sm">
       <div class="flex flex-wrap items-center justify-between gap-4 mb-6">
         <div class="flex items-center gap-3">
-          <label class="text-sm font-medium text-gray-700">选择SKU:</label>
-          <select class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm sku-select"
-                  v-model="selectedSku">
-            <option v-for="k in skuOptions" :key="k" :value="k">{{ k }}</option>
+          <label class="text-sm font-medium text-gray-700">预测类型:</label>
+          <select class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
+                  v-model="predictType">
+            <option value="sku">按SKU预测</option>
+            <option value="category">按类别预测</option>
+          </select>
+        </div>
+        <div class="flex items-center gap-3">
+          <label class="text-sm font-medium text-gray-700">{{ predictType === 'sku' ? '选择SKU:' : '选择类别:' }}</label>
+          <select class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
+                  v-model="selectedItem">
+            <option v-for="item in selectOptions" :key="item" :value="item">{{ item }}</option>
           </select>
         </div>
         <div class="flex items-center gap-3">
@@ -52,8 +62,8 @@
 
       <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
         <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">SKU编码</p>
-          <p class="text-lg font-medium text-gray-800 truncate">{{ selectedSku }}</p>
+          <p class="text-xs text-gray-500 uppercase tracking-wide">{{ predictType === 'sku' ? 'SKU编码' : '类别名称' }}</p>
+          <p class="text-lg font-medium text-gray-800 truncate">{{ selectedItem }}</p>
         </div>
         <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
           <p class="text-xs text-gray-500 uppercase tracking-wide">历史总销量</p>
@@ -137,6 +147,34 @@
             <p class="text-xs text-gray-400 mt-1">{{ predictedMinSalesDate }}</p>
           </div>
         </div>
+        
+        <!-- a+x+y模型组件 -->
+        <div v-if="axyComponents" class="pt-4 border-t border-gray-200">
+          <h4 class="text-md font-medium text-gray-700 mb-3">a+x+y模型组件</h4>
+          <div class="space-y-3">
+            <div class="flex justify-between">
+              <span class="text-sm text-gray-500">基础值 (a)</span>
+              <span class="text-sm font-medium text-gray-800">{{ axyComponents.base_value.toFixed(2) }}</span>
+            </div>
+            <div class="flex justify-between">
+              <span class="text-sm text-gray-500">趋势因子 (x)</span>
+              <span class="text-sm font-medium" :class="axyComponents.trend_factor >= 0 ? 'text-green-600' : 'text-red-600'">
+                {{ axyComponents.trend_factor >= 0 ? '+' : '' }}{{ (axyComponents.trend_factor * 100).toFixed(2) }}%
+              </span>
+            </div>
+            <div v-if="axyComponents.seasonal_factors && axyComponents.seasonal_factors.length > 0" class="pt-2">
+              <p class="text-xs text-gray-500 uppercase tracking-wide mb-2">季节性因子 (y)</p>
+              <div class="grid grid-cols-7 gap-2">
+                <div v-for="(factor, index) in axyComponents.seasonal_factors" :key="index" class="text-center">
+                  <span class="text-xs text-gray-400">第{{ index+1 }}天</span>
+                  <p class="text-sm font-medium" :class="factor >= 0 ? 'text-green-600' : 'text-red-600'">
+                    {{ factor >= 0 ? '+' : '' }}{{ factor.toFixed(1) }}
+                  </p>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
       </div>
 
       <!-- 预测详情表格 -->
@@ -151,6 +189,7 @@
               <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">预测销量</th>
               <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">偏差率</th>
               <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">趋势</th>
+              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">置信度</th>
             </tr>
             </thead>
             <tbody class="bg-white divide-y divide-gray-200">
@@ -164,6 +203,9 @@
               <td class="px-4 py-3 text-sm">
                 <span :class="getTrendClass(item.trend)">{{ item.trend }}</span>
               </td>
+              <td class="px-4 py-3 text-sm">
+                <span class="text-sm font-medium" :class="getConfidenceClass(item.confidence)">{{ (item.confidence * 100).toFixed(1) }}%</span>
+              </td>
             </tr>
             </tbody>
           </table>
@@ -203,7 +245,7 @@
   </div>
 </template>
 
-<script setup>
+<script>
 import { ref, computed, onMounted, beforeUnmount, watch } from 'vue'
 import { uploadAndAnalyzeSales, getSalesResults, predictSalesTrend } from '@/api/sales'
 import { getToken } from '@/utils/auth'
@@ -221,6 +263,8 @@ export default {
         pendingFileName: ''
       },
       predictionPeriod: '7',
+      predictType: 'sku',
+      selectedItem: '',
       results: {}
     }
   },
@@ -228,22 +272,21 @@ export default {
     hasResults() {
       return Object.keys(this.results || {}).length > 0
     },
-    selectedSku: {
-      get() {
-        const state = this.$store && this.$store.state && this.$store.state.analysis
-        return (state && state.selectedSku) || ''
-      },
-      set(value) {
-        if (this.$store) {
-          this.$store.dispatch('analysis/selectSku', value)
-        }
+    selectOptions() {
+      if (this.predictType === 'sku') {
+        return this.results.sku_list || []
+      } else {
+        return this.results.category_list || []
       }
     },
-    skuOptions() {
-      return Object.keys(this.results || {}).filter(k => k !== '_analysis_summary_')
-    },
     detail() {
-      return (this.results && this.selectedSku && this.results[this.selectedSku]) || null
+      if (!this.results || !this.selectedItem) return null
+      
+      if (this.predictType === 'sku') {
+        return this.results.data && this.results.data[this.selectedItem] || null
+      } else {
+        return this.results.categories && this.results.categories[this.selectedItem] || null
+      }
     },
     totalHistoricalSales() {
       const detail = this.detail || {}
@@ -299,7 +342,17 @@ export default {
     },
     predictionDetails() {
       const detail = this.detail || {}
-      return detail.prediction_details || []
+      const details = detail.prediction_details || []
+      
+      // 为每个预测项添加置信度
+      return details.map(item => ({
+        ...item,
+        confidence: item.confidence || (this.detail && this.detail.prediction && this.detail.prediction.confidence ? this.detail.prediction.confidence : 0.5)
+      }))
+    },
+    axyComponents() {
+      const detail = this.detail || {}
+      return detail.axymodel_components || null
     },
     mape() {
       const detail = this.detail || {}
@@ -352,62 +405,153 @@ export default {
       this.renderSalesTrend()
     },
     results() {
-      if (!this.results || !this.results[this.selectedSku]) {
-        const first = this.skuOptions[0] || ''
-        if (first) this.$store.dispatch('analysis/selectSku', first)
+      if (!this.results || !this.selectedItem) {
+        const first = this.selectOptions[0] || ''
+        if (first) this.selectedItem = first
       }
     },
     predictionPeriod() {
-      if (this.selectedSku) {
+      if (this.selectedItem) {
         this.predictSales()
       }
+    },
+    predictType() {
+      if (this.results) {
+        const first = this.selectOptions[0] || ''
+        if (first) this.selectedItem = first
+        if (this.selectedItem) {
+          this.predictSales()
+        }
+      }
     }
   },
   methods: {
     /** 获取销售分析结果 */
     getList() {
+      console.log('Getting sales results...')
       getSalesResults().then(response => {
+        console.log('Get results response:', response)
         if (response && response.code === 200 && response.data) {
           const results = response.data || {}
+          console.log('Sales results:', results)
           this.results = results
-          const firstSku = this.pickFirstSku(results)
-          if (firstSku) this.$store.dispatch('analysis/selectSku', firstSku)
+          const firstItem = this.selectOptions[0] || ''
+          console.log('First item:', firstItem)
+          if (firstItem) {
+            this.selectedItem = firstItem
+            console.log('Selected first item:', firstItem)
+          }
           this.$nextTick(() => {
+            console.log('Rendering sales trend after getting results...')
             this.renderSalesTrend()
           })
         }
-      }).catch(() => {
+      }).catch(error => {
+        console.error('Error getting sales results:', error)
         this.results = {}
       })
     },
+    /** 刷新数据 */
+    refreshData() {
+      this.getList()
+      this.$modal.msgSuccess('数据刷新成功')
+    },
     /** 预测销量趋势 */
     predictSales() {
-      if (!this.selectedSku) return
+      if (!this.selectedItem) return
       
       const params = {
-        sku: this.selectedSku,
-        period: parseInt(this.predictionPeriod)
+        sku: this.selectedItem,
+        period: parseInt(this.predictionPeriod),
+        predict_type: this.predictType
       }
       
       predictSalesTrend(params).then(response => {
-        if (response && response.code === 200 && response.data) {
+        console.log('Predict response:', response)
+        if (response && response.success && response.data) {
           const results = response.data || {}
-          this.results = results
+          console.log('Prediction results:', results)
+          // 更新results数据以匹配前端期望的结构
+          if (this.predictType === 'sku') {
+            if (!this.results.data) {
+              this.results.data = {}
+            }
+            this.results.data[this.selectedItem] = {
+              historical_sales: results.historical_data && results.historical_data.quantities ? results.historical_data.quantities : [],
+              predicted_sales: results.prediction && results.prediction.quantities ? results.prediction.quantities : [],
+              date_series: results.historical_data && results.historical_data.dates ? results.historical_data.dates : [],
+              predicted_total_sales: results.prediction && results.prediction.quantities ? results.prediction.quantities.reduce((a, b) => a + b, 0) : 0,
+              predicted_average_daily_sales: results.prediction && results.prediction.quantities ? results.prediction.quantities.reduce((a, b) => a + b, 0) / parseInt(this.predictionPeriod) : 0,
+              predicted_max_sales: results.prediction && results.prediction.quantities ? Math.max(...results.prediction.quantities) : 0,
+              predicted_min_sales: results.prediction && results.prediction.quantities ? Math.min(...results.prediction.quantities) : 0,
+              predicted_max_sales_date: results.prediction && results.prediction.dates && results.prediction.quantities ? results.prediction.dates[results.prediction.quantities.indexOf(Math.max(...results.prediction.quantities))] : null,
+              predicted_min_sales_date: results.prediction && results.prediction.dates && results.prediction.quantities ? results.prediction.dates[results.prediction.quantities.indexOf(Math.min(...results.prediction.quantities))] : null,
+              prediction_accuracy: results.model_evaluation && results.model_evaluation.model_accuracy ? results.model_evaluation.model_accuracy + '%' : '0%',
+              mape: results.model_evaluation && results.model_evaluation.mape ? results.model_evaluation.mape : '0%',
+              rmse: results.model_evaluation && results.model_evaluation.rmse ? results.model_evaluation.rmse : 0,
+              mae: results.model_evaluation && results.model_evaluation.mae ? results.model_evaluation.mae : 0,
+              r_squared: results.model_evaluation && results.model_evaluation.r_squared ? results.model_evaluation.r_squared : 0,
+              model_evaluation: results.model_evaluation || {},
+              axymodel_components: results.axymodel_components || {}
+            }
+          } else {
+            if (!this.results.categories) {
+              this.results.categories = {}
+            }
+            this.results.categories[this.selectedItem] = {
+              historical_sales: results.historical_data && results.historical_data.quantities ? results.historical_data.quantities : [],
+              predicted_sales: results.prediction && results.prediction.quantities ? results.prediction.quantities : [],
+              date_series: results.historical_data && results.historical_data.dates ? results.historical_data.dates : [],
+              predicted_total_sales: results.prediction && results.prediction.quantities ? results.prediction.quantities.reduce((a, b) => a + b, 0) : 0,
+              predicted_average_daily_sales: results.prediction && results.prediction.quantities ? results.prediction.quantities.reduce((a, b) => a + b, 0) / parseInt(this.predictionPeriod) : 0,
+              predicted_max_sales: results.prediction && results.prediction.quantities ? Math.max(...results.prediction.quantities) : 0,
+              predicted_min_sales: results.prediction && results.prediction.quantities ? Math.min(...results.prediction.quantities) : 0,
+              predicted_max_sales_date: results.prediction && results.prediction.dates && results.prediction.quantities ? results.prediction.dates[results.prediction.quantities.indexOf(Math.max(...results.prediction.quantities))] : null,
+              predicted_min_sales_date: results.prediction && results.prediction.dates && results.prediction.quantities ? results.prediction.dates[results.prediction.quantities.indexOf(Math.min(...results.prediction.quantities))] : null,
+              prediction_accuracy: results.model_evaluation && results.model_evaluation.model_accuracy ? results.model_evaluation.model_accuracy + '%' : '0%',
+              mape: results.model_evaluation && results.model_evaluation.mape ? results.model_evaluation.mape : '0%',
+              rmse: results.model_evaluation && results.model_evaluation.rmse ? results.model_evaluation.rmse : 0,
+              mae: results.model_evaluation && results.model_evaluation.mae ? results.model_evaluation.mae : 0,
+              r_squared: results.model_evaluation && results.model_evaluation.r_squared ? results.model_evaluation.r_squared : 0,
+              model_evaluation: results.model_evaluation || {},
+              axymodel_components: results.axymodel_components || {}
+            }
+          }
           this.$nextTick(() => {
             this.renderSalesTrend()
           })
+          this.$modal.msgSuccess(`预测成功!模型准确率:${results.model_evaluation && results.model_evaluation.model_accuracy ? results.model_evaluation.model_accuracy : 0}%`)
+        } else if (response && !response.success) {
+          this.$modal.msgError(response.message || '预测失败,请重试')
+        } else {
+          this.$modal.msgError('预测失败,请重试')
         }
-      }).catch(() => {
-        this.$modal.msgError('预测失败,请重试')
+      }).catch(error => {
+        console.error('预测失败:', error)
+        this.$modal.msgError('预测失败,请检查服务是否正常运行')
       })
     },
     /** 文件选择改变处理 */
     handleFileChange(file, fileList) {
+      console.log('handleFileChange called')
+      console.log('file:', file)
+      console.log('fileList:', fileList)
       if (!fileList || fileList.length === 0) return
       if (!file || !file.raw) return
 
       this.upload.pendingFileName = file.name
       this.upload.fileName = ''
+      console.log('pendingFileName set to:', this.upload.pendingFileName)
+    },
+    handleUploadSuccess(response, file, fileList) {
+      console.log('handleUploadSuccess called')
+      console.log('response:', response)
+      console.log('file:', file)
+    },
+    handleUploadError(error, file, fileList) {
+      console.error('handleUploadError called')
+      console.error('error:', error)
+      console.error('file:', file)
     },
     /** 文件上传前的校验 */
     beforeUpload(file) {
@@ -417,40 +561,67 @@ export default {
         file.name.endsWith('.xlsx') ||
         file.name.endsWith('.xls') ||
         file.name.endsWith('.csv')
-      const isLt20M = file.size / 1024 / 1024 < 20
+      const isLt300M = file.size / 1024 / 1024 < 300
 
       if (!isExcel) {
         this.$modal.msgError('上传文件只能是 xlsx/xls/csv 格式!')
         return false
       }
-      if (!isLt20M) {
-        this.$modal.msgError('上传文件大小不能超过 20MB!')
+      if (!isLt300M) {
+        this.$modal.msgError('上传文件大小不能超过 300MB!')
         return false
       }
       return true
     },
     customUpload(options) {
-      const file = options.file
+      console.log('customUpload called')
+      console.log('options:', options)
+      console.log('options.file:', options.file)
+      if (!options.file) {
+        console.error('No file in options')
+        this.$modal.msgError('请选择要上传的文件')
+        options.onError(new Error('No file to upload'))
+        return
+      }
+      console.log('options.file.raw:', options.file.raw)
+      const file = options.file.raw || options.file
+      console.log('file to upload:', file)
+      if (!file) {
+        console.error('No file to upload')
+        this.$modal.msgError('请选择要上传的文件')
+        options.onError(new Error('No file to upload'))
+        return
+      }
+      console.log('Starting file upload to:', '/statistics/sales/upload')
       this.upload.isUploading = true
       uploadAndAnalyzeSales(file).then(response => {
+        console.log('uploadAndAnalyzeSales response:', response)
         this.upload.isUploading = false
         if (response && response.code === 200) {
           const results = response.data || {}
+          console.log('Upload analysis results:', results)
           this.results = results
-          const firstSku = this.pickFirstSku(results)
-          if (firstSku) this.$store.dispatch('analysis/selectSku', firstSku)
+          const firstItem = this.selectOptions[0] || ''
+          if (firstItem) {
+            this.selectedItem = firstItem
+            console.log('Selected first item:', firstItem)
+            console.log('Results data structure:', this.results)
+          }
           this.$modal.msgSuccess('文件上传并分析成功')
           this.upload.fileName = this.upload.pendingFileName || file.name
           this.upload.pendingFileName = ''
           this.$nextTick(() => {
+            console.log('Rendering sales trend...')
             this.renderSalesTrend()
           })
           options.onSuccess(response)
         } else {
+          console.error('Upload failed with response:', response)
           this.$modal.msgError(response.msg || '分析失败')
           options.onError(new Error(response.msg || '分析失败'))
         }
       }).catch(error => {
+        console.error('uploadAndAnalyzeSales error:', error)
         this.upload.isUploading = false
         const msg = (error && error.message) || '文件上传失败,请重试'
         this.$modal.msgError(msg)
@@ -462,152 +633,183 @@ export default {
       })
     },
     submitUpload() {
+      console.log('submitUpload called')
       const target = this.$refs.toolbarUpload
-      const fileList = target && target.uploadFiles ? target.uploadFiles : []
+      console.log('target:', target)
+      if (!target) {
+        console.error('No upload component reference')
+        this.$modal.msgError('上传组件未初始化')
+        return
+      }
+      const fileList = target.uploadFiles || []
+      console.log('fileList:', fileList)
       if (!fileList || fileList.length === 0) {
+        console.error('No files selected')
         this.$modal.msgError('请选择要上传的文件')
         return
       }
+      console.log('Calling target.submit()')
       target.submit()
     },
     /** 渲染销量趋势与预测图 */
     renderSalesTrend() {
+      console.log('Rendering sales trend...')
       const canvas = this.$refs.salesTrendRef
-      if (!canvas) return
+      if (!canvas) {
+        console.error('No canvas reference found')
+        return
+      }
       
+      console.log('Detail data:', this.detail)
       const detail = this.detail || {}
       const historicalSales = detail.historical_sales || []
       const predictedSales = detail.predicted_sales || []
       const dates = detail.date_series || []
+      
+      console.log('Historical sales:', historicalSales)
+      console.log('Predicted sales:', predictedSales)
+      console.log('Dates:', dates)
+      
+      // 生成标签
       const labels = dates.map(d => formatDate(d))
+      console.log('Labels:', labels)
       
       // 计算趋势线数据
       const trendLineData = this.calculateTrendLine(historicalSales)
+      console.log('Trend line data:', trendLineData)
       
       // 合并历史和预测数据
       const combinedSales = [...historicalSales, ...predictedSales]
       const combinedLabels = [...labels, ...this.generatePredictionDates(parseInt(this.predictionPeriod))]
       
+      console.log('Combined sales:', combinedSales)
+      console.log('Combined labels:', combinedLabels)
+      
       if (this.salesTrendChart) this.salesTrendChart.destroy()
       
-      this.salesTrendChart = new Chart(canvas, {
-        type: 'line',
-        data: {
-          labels: combinedLabels,
-          datasets: [
-            {
-              label: '历史销量',
-              data: [...historicalSales, ...Array(predictedSales.length).fill(null)],
-              borderColor: '#3b82f6',
-              backgroundColor: 'rgba(59,130,246,0.15)',
-              lineTension: 0.25,
-              pointRadius: 0,
-              pointHoverRadius: 6,
-              pointHoverBackgroundColor: '#3b82f6',
-              pointHoverBorderColor: '#ffffff',
-              pointHoverBorderWidth: 2
-            },
-            {
-              label: '预测销量',
-              data: [...Array(historicalSales.length).fill(null), ...predictedSales],
-              borderColor: '#10b981',
-              backgroundColor: 'rgba(16,185,129,0.15)',
-              borderDash: [5, 5],
-              lineTension: 0.25,
-              pointRadius: 0,
-              pointHoverRadius: 6,
-              pointHoverBackgroundColor: '#10b981',
-              pointHoverBorderColor: '#ffffff',
-              pointHoverBorderWidth: 2
-            },
-            {
-              label: '趋势线',
-              data: [...trendLineData, ...Array(predictedSales.length).fill(null)],
-              borderColor: '#8b5cf6',
-              backgroundColor: 'transparent',
-              lineTension: 0.1,
-              pointRadius: 0,
-              pointHoverRadius: 0
-            }
-          ]
-        },
-        options: {
-          responsive: true,
-          maintainAspectRatio: false,
-          tooltips: {
-            enabled: true,
-            backgroundColor: 'rgba(0,0,0,0.8)',
-            titleFontColor: '#ffffff',
-            bodyFontColor: '#ffffff',
-            borderColor: '#374151',
-            borderWidth: 1,
-            cornerRadius: 6,
-            displayColors: true,
-            callbacks: {
-              title: function(context) {
-                return '日期: ' + context[0].label
+      try {
+        this.salesTrendChart = new Chart(canvas, {
+          type: 'line',
+          data: {
+            labels: combinedLabels,
+            datasets: [
+              {
+                label: '历史销量',
+                data: [...historicalSales, ...Array(predictedSales.length).fill(null)],
+                borderColor: '#3b82f6',
+                backgroundColor: 'rgba(59,130,246,0.15)',
+                lineTension: 0.25,
+                pointRadius: 0,
+                pointHoverRadius: 6,
+                pointHoverBackgroundColor: '#3b82f6',
+                pointHoverBorderColor: '#ffffff',
+                pointHoverBorderWidth: 2
+              },
+              {
+                label: '预测销量',
+                data: [...Array(historicalSales.length).fill(null), ...predictedSales],
+                borderColor: '#10b981',
+                backgroundColor: 'rgba(16,185,129,0.15)',
+                borderDash: [5, 5],
+                lineTension: 0.25,
+                pointRadius: 0,
+                pointHoverRadius: 6,
+                pointHoverBackgroundColor: '#10b981',
+                pointHoverBorderColor: '#ffffff',
+                pointHoverBorderWidth: 2
               },
-              label: function(context, data) {
-                const datasetLabel = data.datasets[context.datasetIndex].label
-                const value = context.value
-                return datasetLabel + ': ' + Number(value).toLocaleString()
+              {
+                label: '趋势线',
+                data: [...trendLineData, ...Array(predictedSales.length).fill(null)],
+                borderColor: '#8b5cf6',
+                backgroundColor: 'transparent',
+                lineTension: 0.1,
+                pointRadius: 0,
+                pointHoverRadius: 0
               }
-            }
-          },
-          hover: {
-            mode: 'index',
-            intersect: false
+            ]
           },
-          scales: {
-            xAxes: [{
-              ticks: {
-                maxRotation: 0,
-                autoSkip: true,
-                maxTicksLimit: 12,
-                callback: function(value, index) {
-                  return combinedLabels[index]
+          options: {
+            responsive: true,
+            maintainAspectRatio: false,
+            tooltips: {
+              enabled: true,
+              backgroundColor: 'rgba(0,0,0,0.8)',
+              titleFontColor: '#ffffff',
+              bodyFontColor: '#ffffff',
+              borderColor: '#374151',
+              borderWidth: 1,
+              cornerRadius: 6,
+              displayColors: true,
+              callbacks: {
+                title: function(context) {
+                  return '日期: ' + context[0].label
+                },
+                label: function(context, data) {
+                  const datasetLabel = data.datasets[context.datasetIndex].label
+                  const value = context.value
+                  return datasetLabel + ': ' + Number(value).toLocaleString()
                 }
               }
-            }],
-            yAxes: [{
-              ticks: {
-                beginAtZero: true
-              }
-            }]
-          }
-        },
-        plugins: [{
-          id: 'prediction-area',
-          beforeDatasetsDraw(chart) {
-            const ctx = chart.ctx
-            const chartArea = chart.chartArea
-            const xScale = chart.scales['x-axis-0']
-            
-            if (historicalSales.length === 0 || predictedSales.length === 0) return
-            
-            const predictionStartIndex = historicalSales.length - 1
-            const x1 = xScale.getPixelForValue(predictionStartIndex)
-            
-            ctx.save()
-            ctx.fillStyle = 'rgba(16,185,129,0.05)'
-            ctx.fillRect(x1, chartArea.top, chartArea.right - x1, chartArea.bottom - chartArea.top)
-            
-            ctx.strokeStyle = 'rgba(16,185,129,0.3)'
-            ctx.setLineDash([5, 5])
-            ctx.lineWidth = 1
-            ctx.beginPath()
-            ctx.moveTo(x1, chartArea.top)
-            ctx.lineTo(x1, chartArea.bottom)
-            ctx.stroke()
-            
-            ctx.fillStyle = 'rgba(16,185,129,0.8)'
-            ctx.font = 'bold 12px sans-serif'
-            ctx.textAlign = 'left'
-            ctx.fillText('预测区域', x1 + 10, chartArea.top + 20)
-            ctx.restore()
-          }
-        }]
-      })
+            },
+            hover: {
+              mode: 'index',
+              intersect: false
+            },
+            scales: {
+              xAxes: [{
+                ticks: {
+                  maxRotation: 0,
+                  autoSkip: true,
+                  maxTicksLimit: 12,
+                  callback: function(value, index) {
+                    return combinedLabels[index]
+                  }
+                }
+              }],
+              yAxes: [{
+                ticks: {
+                  beginAtZero: true
+                }
+              }]
+            }
+          },
+          plugins: [{
+            id: 'prediction-area',
+            beforeDatasetsDraw(chart) {
+              const ctx = chart.ctx
+              const chartArea = chart.chartArea
+              const xScale = chart.scales['x-axis-0']
+              
+              if (historicalSales.length === 0 || predictedSales.length === 0) return
+              
+              const predictionStartIndex = historicalSales.length - 1
+              const x1 = xScale.getPixelForValue(predictionStartIndex)
+              
+              ctx.save()
+              ctx.fillStyle = 'rgba(16,185,129,0.05)'
+              ctx.fillRect(x1, chartArea.top, chartArea.right - x1, chartArea.bottom - chartArea.top)
+              
+              ctx.strokeStyle = 'rgba(16,185,129,0.3)'
+              ctx.setLineDash([5, 5])
+              ctx.lineWidth = 1
+              ctx.beginPath()
+              ctx.moveTo(x1, chartArea.top)
+              ctx.lineTo(x1, chartArea.bottom)
+              ctx.stroke()
+              
+              ctx.fillStyle = 'rgba(16,185,129,0.8)'
+              ctx.font = 'bold 12px sans-serif'
+              ctx.textAlign = 'left'
+              ctx.fillText('预测区域', x1 + 10, chartArea.top + 20)
+              ctx.restore()
+            }
+          }]
+        })
+        console.log('Chart rendered successfully')
+      } catch (error) {
+        console.error('Error rendering chart:', error)
+      }
     },
     /** 计算趋势线 */
     calculateTrendLine(data) {
@@ -648,6 +850,13 @@ export default {
       if (trend === '下降') return 'text-red-600 font-medium'
       return 'text-gray-600 font-medium'
     },
+    /** 获取置信度样式类 */
+    getConfidenceClass(confidence) {
+      const confidenceValue = typeof confidence === 'number' ? confidence : parseFloat(confidence)
+      if (confidenceValue >= 0.8) return 'text-green-600'
+      if (confidenceValue >= 0.6) return 'text-yellow-600'
+      return 'text-red-600'
+    },
     /** 导出预测结果 */
     exportResults() {
       if (!this.hasResults) {

+ 282 - 0
src/views/storage/accuracy/index.vue

@@ -0,0 +1,282 @@
+<template>
+  <div class="semi-product">
+    <!-- 半成品概览 -->
+    <el-row :gutter="20" class="overview-row">
+      <el-col :span="6">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-icon" style="background-color: #409eff;">
+              <!-- Replace Box icon with text symbol -->
+              <span style="font-size:28px; color:#fff;">📦</span>
+            </div>
+            <div class="stat-info">
+              <div class="stat-label">半成品总数</div>
+              <div class="stat-value">-</div>
+              <div class="stat-unit">种</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-icon" style="background-color: #67c23a;">
+              <!-- Replace Goods icon with text symbol -->
+              <span style="font-size:28px; color:#fff;">📦</span>
+            </div>
+            <div class="stat-info">
+              <div class="stat-label">库存数量</div>
+              <div class="stat-value">-</div>
+              <div class="stat-unit">件</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-icon" style="background-color: #909399;">
+              <!-- Replace Goods icon with text symbol -->
+              <span style="font-size:28px; color:#fff;">📦</span>
+            </div>
+            <div class="stat-info">
+              <div class="stat-label">预计可生产成品</div>
+              <div class="stat-value">-</div>
+              <div class="stat-unit">件</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 搜索与操作栏 -->
+    <el-card class="search-card">
+      <el-row :gutter="20" align="middle">
+        <el-col :span="6">
+          <el-input
+            placeholder="搜索半成品名称/编码"
+            clearable
+          >
+            <template #prefix>
+              <!-- Replace Search icon with text symbol -->
+              <span style="font-size:16px; color:#909399;">🔍</span>
+            </template>
+          </el-input>
+        </el-col>
+        <el-col :span="4">
+          <el-select placeholder="状态筛选" clearable>
+            <el-option label="全部" value="" />
+            <el-option label="在库" value="in-stock" />
+            <el-option label="在制" value="in-production" />
+            <el-option label="待检" value="pending" />
+            <el-option label="已用" value="used" />
+          </el-select>
+        </el-col>
+        <el-col :span="4">
+          <el-select placeholder="类别筛选" clearable>
+            <el-option label="全部" value="" />
+            <el-option label="电子元件" value="electronics" />
+            <el-option label="机械部件" value="mechanics" />
+            <el-option label="外壳模组" value="housing" />
+            <el-option label="电路板" value="pcb" />
+          </el-select>
+        </el-col>
+        <el-col :span="6">
+          <el-button type="primary">
+            <!-- Replace Search icon with text -->
+            🔍 搜索
+          </el-button>
+          <el-button>
+            <!-- Replace RefreshLeft icon with text -->
+            🔄 重置
+          </el-button>
+        </el-col>
+        <el-col :span="4" style="text-align: right;">
+          <el-button type="success">
+            <!-- Replace Plus icon with text -->
+            ➕ 新增半成品
+          </el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <!-- 可组装成品能力 -->
+    <el-card class="assembly-card" style="margin-bottom: 20px;">
+      <template #header>
+        <div style="display:flex; justify-content:space-between; align-items:center;">
+          <span>可组装成品能力(基于入库库存)</span>
+          <el-button type="primary" size="small">刷新</el-button>
+        </div>
+      </template>
+      <el-table stripe style="width: 100%">
+        <el-table-column label="成品编码" width="180" />
+        <el-table-column label="名称" width="220" />
+        <el-table-column label="可组装数量" width="140" />
+        <el-table-column label="所需半成品及库存" />
+      </el-table>
+    </el-card>
+
+    <!-- 半成品列表 -->
+    <el-card class="table-card">
+      <el-table stripe style="width: 100%">
+        <el-table-column type="selection" width="55" />
+        <el-table-column label="编码" width="120" fixed />
+        <el-table-column label="半成品名称" width="200" />
+        <el-table-column label="类别" width="120" />
+        <el-table-column label="库存数量" width="100" />
+        <el-table-column label="安全库存" width="100" />
+        <el-table-column label="状态" width="100" />
+        <el-table-column label="可用于组装" width="150" />
+        <el-table-column label="供应商" width="150" />
+        <el-table-column label="采购周期" width="100" />
+        <el-table-column label="单价(元)" width="100" />
+        <el-table-column label="库存价值(元)" width="120" />
+        <el-table-column label="操作" width="240" fixed="right">
+          <template #default>
+            <el-button type="primary" size="small">详情</el-button>
+            <el-button type="success" size="small">编辑</el-button>
+            <el-button type="warning" size="small">组装</el-button>
+            <el-button type="danger" size="small">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        :current-page="1"
+        :page-size="10"
+        :total="0"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        style="margin-top: 20px; justify-content: flex-end;"
+      />
+    </el-card>
+
+    <!-- 半成品分析图表 -->
+    <el-row :gutter="20" class="charts-row">
+      <el-col :span="12">
+        <el-card>
+          <template #header>
+            <span>半成品库存分布</span>
+          </template>
+          <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
+            暂无图表数据
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card>
+          <template #header>
+            <span>半成品使用趋势</span>
+          </template>
+          <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
+            暂无图表数据
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+
+
+    <!-- 半成品周转分析 -->
+    <el-card class="turnover-card">
+      <template #header>
+        <span>半成品周转效率分析</span>
+      </template>
+      <el-table stripe>
+        <el-table-column label="半成品名称" width="200" />
+        <el-table-column label="平均周转天数" width="150" />
+        <el-table-column label="使用率" width="120" />
+        <el-table-column label="月消耗量" width="120" />
+        <el-table-column label="再订货点" width="120" />
+        <el-table-column label="库存状态" width="120" />
+        <el-table-column label="优化建议" min-width="250" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+  // REMOVED ALL ICON IMPORTS (no @element-plus/icons-vue dependency)
+</script>
+
+<style scoped>
+  .semi-product {
+    width: 100%;
+  }
+
+  .overview-row {
+    margin-bottom: 20px;
+  }
+
+  .stat-card {
+    cursor: pointer;
+    transition: all 0.3s;
+  }
+
+  .stat-card:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  }
+
+  .stat-content {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+  }
+
+  .stat-icon {
+    width: 56px;
+    height: 56px;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+  }
+
+  .stat-info {
+    flex: 1;
+  }
+
+  .stat-label {
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 8px;
+  }
+
+  .stat-value {
+    font-size: 28px;
+    font-weight: bold;
+    color: #303133;
+    line-height: 1;
+  }
+
+  .stat-unit {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+  }
+
+  .search-card {
+    margin-bottom: 20px;
+  }
+
+  .table-card {
+    margin-bottom: 20px;
+  }
+
+  .charts-row {
+    margin-bottom: 20px;
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .bom-card,
+  .turnover-card {
+    margin-bottom: 20px;
+  }
+</style>

+ 262 - 0
src/views/storage/cost/index.vue

@@ -0,0 +1,262 @@
+<template>
+  <div class="risk-warning">
+    <!-- 风险总览卡片 -->
+    <el-row :gutter="20" class="risk-overview">
+      <el-col :span="6">
+        <el-card class="risk-card critical">
+          <div class="risk-header">
+            <!-- Replace WarningFilled icon with symbol -->
+            <span style="font-size:32px; color:#f56c6c;">⚠️</span>
+            <div class="risk-info">
+              <div class="risk-label">高风险</div>
+              <div class="risk-count">-</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="risk-card warning">
+          <div class="risk-header">
+            <!-- Replace Warning icon with symbol -->
+            <span style="font-size:32px; color:#e6a23c;">⚠️</span>
+            <div class="risk-info">
+              <div class="risk-label">中风险</div>
+              <div class="risk-count">-</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="risk-card info">
+          <div class="risk-header">
+            <!-- Replace InfoFilled icon with symbol -->
+            <span style="font-size:32px; color:#409eff;">ℹ️</span>
+            <div class="risk-info">
+              <div class="risk-label">低风险</div>
+              <div class="risk-count">-</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="risk-card safe">
+          <div class="risk-header">
+            <!-- Replace SuccessFilled icon with symbol -->
+            <span style="font-size:32px; color:#67c23a;">✅</span>
+            <div class="risk-info">
+              <div class="risk-label">正常</div>
+              <div class="risk-count">-</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 风险列表与筛选 -->
+    <el-card class="risk-list-card">
+      <template #header>
+        <div class="card-header">
+          <span>风险预警列表</span>
+          <div class="filter-group">
+            <el-select placeholder="风险等级" size="default" style="width: 120px">
+              <el-option label="全部" value="" />
+              <el-option label="高风险" value="critical" />
+              <el-option label="中风险" value="warning" />
+              <el-option label="低风险" value="info" />
+            </el-select>
+            <el-select placeholder="风险类型" size="default" style="width: 140px">
+              <el-option label="全部" value="" />
+              <el-option label="缺货风险" value="stockout" />
+              <el-option label="超储风险" value="overstock" />
+              <el-option label="滞销风险" value="slow-moving" />
+              <el-option label="周转风险" value="turnover" />
+            </el-select>
+            <el-button type="primary">
+              <!-- Replace Refresh icon with symbol -->
+              🔄 刷新
+            </el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-table stripe style="width: 100%" :loading="false">
+        <el-table-column label="编号" width="80" />
+        <el-table-column label="产品名称" width="200" />
+        <el-table-column label="产品编码" width="120" />
+        <el-table-column label="风险等级" width="120" />
+        <el-table-column label="风险类型" width="140" />
+        <el-table-column label="健康评分" width="150" />
+        <el-table-column label="周转率" width="100" />
+        <el-table-column label="风险评分" width="100" />
+        <el-table-column label="覆盖天数" width="110" />
+        <el-table-column label="当前库存" width="110" />
+        <el-table-column label="日均销量" width="110" />
+        <el-table-column label="风险描述" min-width="200" />
+        <el-table-column label="建议措施" min-width="200" />
+        <el-table-column label="检测时间" width="180" />
+      </el-table>
+
+      <el-pagination
+        :current-page="1"
+        :page-size="10"
+        :total="0"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        style="margin-top: 20px; justify-content: flex-end;"
+      />
+    </el-card>
+
+    <!-- 智能预警规则配置 -->
+    <el-card class="rule-card">
+      <template #header>
+        <div class="card-header">
+          <span>智能预警规则配置</span>
+          <el-button type="primary" size="small">
+            <!-- Replace Plus icon with symbol -->
+            ➕ 添加规则
+          </el-button>
+        </div>
+      </template>
+
+      <el-table stripe>
+        <el-table-column label="规则名称" width="200" />
+        <el-table-column label="触发条件" min-width="300" />
+        <el-table-column label="风险等级" width="120" />
+        <el-table-column label="状态" width="100" />
+        <el-table-column label="触发次数" width="100" />
+      </el-table>
+    </el-card>
+
+    <!-- 反馈优化建议 -->
+    <el-card class="feedback-card">
+      <template #header>
+        <div class="card-header">
+          <span>系统优化建议与反馈</span>
+          <el-tag type="success">智能分析</el-tag>
+        </div>
+      </template>
+
+      <el-timeline>
+        <el-timeline-item
+          timestamp="-"
+          placement="top"
+        >
+          <el-card>
+            <h4>暂无数据</h4>
+            <p class="feedback-content">暂无系统优化建议与反馈信息</p>
+            <div class="feedback-actions">
+              <el-button type="text" size="small">查看详情</el-button>
+            </div>
+          </el-card>
+        </el-timeline-item>
+      </el-timeline>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+  // REMOVED ALL ICON IMPORTS (no @element-plus/icons-vue dependency)
+</script>
+
+<style scoped>
+  .risk-warning {
+    width: 100%;
+  }
+
+  .risk-overview {
+    margin-bottom: 20px;
+  }
+
+  .risk-card {
+    cursor: pointer;
+    transition: all 0.3s;
+  }
+
+  .risk-card:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  }
+
+  .risk-header {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+  }
+
+  .risk-info {
+    flex: 1;
+  }
+
+  .risk-label {
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 8px;
+  }
+
+  .risk-count {
+    font-size: 32px;
+    font-weight: bold;
+    color: #303133;
+  }
+
+  .risk-list-card {
+    margin-bottom: 20px;
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .filter-group {
+    display: flex;
+    gap: 10px;
+  }
+
+  .charts-row {
+    margin-bottom: 20px;
+  }
+
+  .rule-card,
+  .feedback-card {
+    margin-bottom: 20px;
+  }
+
+  .feedback-content {
+    color: #606266;
+    font-size: 14px;
+    margin: 10px 0;
+    line-height: 1.6;
+  }
+
+  .feedback-actions {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-top: 10px;
+  }
+
+  .health-cell {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+  }
+
+  .health-value {
+    font-weight: 600;
+    color: #303133;
+  }
+
+  .score-breakdown {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+  }
+  .score-line {
+    display: flex;
+    justify-content: space-between;
+    font-size: 12px;
+    color: #606266;
+  }
+</style>

+ 185 - 0
src/views/storage/efficiency/index.vue

@@ -0,0 +1,185 @@
+<template>
+  <div class="inventory-overview">
+    <div class="product-analysis">
+      <!-- 搜索框 -->
+      <div class="search-bar">
+        <input
+          v-model="sku"
+          placeholder="输入SKU查询"
+          @keyup.enter="search"
+        >
+        <button @click="search" :disabled="loading">
+          {{ loading ? '查询中...' : '查询' }}
+        </button>
+      </div>
+    </div>
+
+    <!-- SKU指标汇总表格 - 空表格 -->
+    <el-row :gutter="20" class="charts-row">
+      <el-col :span="24">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>SKU指标汇总</span>
+              <el-button type="primary" size="small" @click="handleRefresh">
+                <span style="margin-right: 4px;">🔄</span> 刷新
+              </el-button>
+            </div>
+          </template>
+          <el-table :data="[]" stripe style="width: 100%" v-loading="false">
+            <el-table-column prop="sku" label="SKU" width="200" fixed />
+            <el-table-column prop="attribute" label="属性" width="140">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="spuName" label="SPU" width="200">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="inventory" label="现有库存" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="purchaseAmount" label="入库总资金" width="150" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="turnoverRate" label="库存周转率" align="right">
+              <template #default>
+                <el-tag type="info">—</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+
+
+  </div>
+</template>
+
+<script setup>
+  // 完全移除所有图标依赖,仅保留空的刷新方法
+  const handleRefresh = () => {
+    // 后续可添加数据加载逻辑
+  }
+</script>
+
+<style scoped>
+  .product-analysis {
+    width: 90%;
+    max-width: 1200px;
+    margin: 20px auto;
+    font-family: Arial, sans-serif;
+  }
+
+  /* 搜索栏 */
+  .search-bar {
+    margin-bottom: 20px;
+    display: flex;
+    gap: 10px;
+  }
+  .search-bar input {
+    flex: 1;
+    padding: 8px 12px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+  }
+  .search-bar button {
+    padding: 8px 20px;
+    border: none;
+    background: #409eff;
+    color: #fff;
+    border-radius: 4px;
+    cursor: pointer;
+  }
+  .search-bar button:disabled {
+    background: #a0cfff;
+    cursor: not-allowed;
+  }
+
+  /* 空状态 */
+  .empty {
+    text-align: center;
+    padding: 50px 0;
+    color: #999;
+  }
+
+  /* 分析内容 */
+  .analysis-content {
+    display: grid;
+    gap: 20px;
+  }
+
+  .inventory-overview {
+    width: 100%;
+  }
+
+  .metrics-row {
+    margin-bottom: 20px;
+  }
+
+  .metric-card {
+    cursor: pointer;
+    transition: transform 0.3s;
+  }
+
+  .metric-card:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  }
+
+  .metric-content {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+  }
+
+  .metric-icon {
+    width: 60px;
+    height: 60px;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+  }
+
+  .metric-info {
+    flex: 1;
+  }
+
+  .metric-label {
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 8px;
+  }
+
+  .metric-value {
+    font-size: 28px;
+    font-weight: bold;
+    color: #303133;
+    line-height: 1;
+  }
+
+  .metric-unit {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+  }
+
+  .charts-row {
+    margin-bottom: 20px;
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+</style>

+ 243 - 0
src/views/storage/overview/index.vue

@@ -0,0 +1,243 @@
+<template>
+  <div class="inventory-overview">
+    <!-- 关键指标卡片 - 纯文本替代图标 -->
+    <el-row :gutter="20" class="metrics-row">
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon" style="background-color: #409eff;">
+              <span style="font-size: 30px; color: #fff;">📦</span>
+            </div>
+            <div class="metric-info">
+              <div class="metric-label">总体库存量</div>
+              <div class="metric-value">-</div>
+              <div class="metric-unit">件</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon" style="background-color: #67c23a;">
+              <span style="font-size: 30px; color: #fff;">💰</span>
+            </div>
+            <div class="metric-info">
+              <div class="metric-label">库存价值</div>
+              <div class="metric-value">-</div>
+              <div class="metric-unit">万元</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon" style="background-color: #e6a23c;">
+              <span style="font-size: 30px; color: #fff;">🔄</span>
+            </div>
+            <div class="metric-info">
+              <div class="metric-label">库存周转率</div>
+              <div class="metric-value">-</div>
+              <div class="metric-unit">天</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon" style="background-color: #f56c6c;">
+              <span style="font-size: 30px; color: #fff;">🚚</span>
+            </div>
+            <div class="metric-info">
+              <div class="metric-label">在途库存比例</div>
+              <div class="metric-value">-</div>
+              <div class="metric-unit">%</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 每月入库/销售/库存对比图 - 空容器 -->
+    <el-row :gutter="20" class="charts-row">
+      <el-col :span="24">
+        <el-card>
+          <template #header>
+            <span>入库/销售/库存对比</span>
+          </template>
+          <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
+            <div>暂无数据</div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- SKU指标汇总表格 - 空表格 -->
+    <el-row :gutter="20" class="charts-row">
+      <el-col :span="24">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>SKU指标汇总</span>
+              <el-button type="primary" size="small" @click="handleRefresh">
+                <span style="margin-right: 4px;">🔄</span> 刷新
+              </el-button>
+            </div>
+          </template>
+          <el-table :data="[]" stripe style="width: 100%" v-loading="false">
+            <el-table-column prop="sku" label="SKU" width="200" fixed />
+            <el-table-column prop="attribute" label="属性" width="140">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="spuName" label="SPU" width="200">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="inventory" label="现有库存" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="purchaseAmount" label="入库总资金" width="150" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="turnoverRate" label="库存周转率" align="right">
+              <template #default>
+                <el-tag type="info">—</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- SPU指标汇总表格 - 空表格 -->
+    <el-row :gutter="20" class="charts-row">
+      <el-col :span="24">
+        <el-card>
+          <template #header>
+            <div class="card-header">
+              <span>SPU指标汇总(成品)</span>
+              <el-button type="primary" size="small" @click="handleRefresh">
+                <span style="margin-right: 4px;">🔄</span> 刷新
+              </el-button>
+            </div>
+          </template>
+          <el-table :data="[]" stripe style="width: 100%" v-loading="false">
+            <el-table-column prop="spu" label="SPU" width="220" fixed />
+            <el-table-column prop="attribute" label="属性" width="140">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="skuCount" label="SKU数" width="90" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="inventory" label="现有库存" width="120" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="purchaseAmount" label="入库总资金" width="150" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
+              <template #default>—</template>
+            </el-table-column>
+            <el-table-column prop="turnoverRate" label="库存周转率" align="right">
+              <template #default>
+                <el-tag type="info">—</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+  // 完全移除所有图标依赖,仅保留空的刷新方法
+  const handleRefresh = () => {
+    // 后续可添加数据加载逻辑
+  }
+</script>
+
+<style scoped>
+  .inventory-overview {
+    width: 100%;
+  }
+
+  .metrics-row {
+    margin-bottom: 20px;
+  }
+
+  .metric-card {
+    cursor: pointer;
+    transition: transform 0.3s;
+  }
+
+  .metric-card:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  }
+
+  .metric-content {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+  }
+
+  .metric-icon {
+    width: 60px;
+    height: 60px;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+  }
+
+  .metric-info {
+    flex: 1;
+  }
+
+  .metric-label {
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 8px;
+  }
+
+  .metric-value {
+    font-size: 28px;
+    font-weight: bold;
+    color: #303133;
+    line-height: 1;
+  }
+
+  .metric-unit {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+  }
+
+  .charts-row {
+    margin-bottom: 20px;
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+</style>

+ 451 - 0
src/views/storage/turnover/index.vue

@@ -0,0 +1,451 @@
+<template>
+  <div class="product-analysis">
+    <!-- 产品SKU搜索区域 -->
+    <div class="search-card">
+      <div class="search-row">
+        <div class="search-col search-input-col">
+          <div class="input-wrapper">
+            <span class="input-icon">🔍</span>
+            <input
+              type="text"
+              v-model="skuInput"
+              placeholder="请输入产品SKU(如:J06D01AS1)"
+              class="search-input"
+              @keyup.enter="searchProduct"
+            >
+            <button
+              class="clear-btn"
+              v-if="skuInput"
+              @click="clearInput"
+            >×</button>
+          </div>
+        </div>
+        <div class="search-col search-btn-col">
+          <button
+            class="search-btn"
+            :disabled="loading"
+            @click="searchProduct"
+          >
+            <span v-if="loading" class="loading-spinner"></span>
+            <span v-else>查询分析</span>
+          </button>
+        </div>
+        <div class="search-col search-info-col">
+          <span class="sku-tag" v-if="showProductArea">SKU: {{ skuInput }}</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 初始空状态 -->
+    <div class="empty-init" v-if="!showProductArea">
+      <div class="empty-icon">📦</div>
+      <div class="empty-text">请输入产品SKU进行查询分析</div>
+    </div>
+
+    <!-- 产品分析空框架 -->
+    <div class="analysis-content" v-else>
+      <!-- 关键指标卡片 -->
+      <div class="metrics-row">
+        <div class="metric-card">
+          <div class="metric-item">
+            <div class="metric-label">入库总量</div>
+            <div class="metric-value">-</div>
+            <div class="metric-unit">件</div>
+          </div>
+        </div>
+        <div class="metric-card">
+          <div class="metric-item">
+            <div class="metric-label">销售总量</div>
+            <div class="metric-value">-</div>
+            <div class="metric-unit">件</div>
+          </div>
+        </div>
+        <div class="metric-card">
+          <div class="metric-item">
+            <div class="metric-label">当前库存</div>
+            <div class="metric-value">-</div>
+            <div class="metric-unit">件</div>
+          </div>
+        </div>
+        <div class="metric-card">
+          <div class="metric-item">
+            <div class="metric-label">周转率</div>
+            <div class="metric-value">-</div>
+            <div class="metric-unit">次</div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 分段周转率表格 -->
+      <div class="card-container">
+        <div class="card-header">
+          <span>分段周转率</span>
+        </div>
+        <div class="table-container">
+          <table class="turnover-table">
+            <thead>
+            <tr>
+              <th width="220">统计区间</th>
+              <th width="160">销售出库 (件)</th>
+              <th width="220">时间加权平均库存 (件)</th>
+              <th>周转率 (次)</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr class="empty-row">
+              <td colspan="4">暂无数据</td>
+            </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      <!-- 库存趋势图占位 -->
+      <div class="card-container">
+        <div class="card-header">
+          <span>单品库存变化趋势(入库/销售/库存)</span>
+        </div>
+        <div class="chart-placeholder">
+          暂无图表数据
+        </div>
+      </div>
+
+      <!-- 预测与生命周期 -->
+      <div class="insights-row">
+        <div class="insight-card">
+          <div class="card-header">
+            <span>30天预测摘要</span>
+          </div>
+          <div class="empty-placeholder">
+            暂无预测数据
+          </div>
+        </div>
+        <div class="insight-card">
+          <div class="card-header">
+            <span>生命周期分段</span>
+          </div>
+          <div class="empty-placeholder">
+            暂无生命周期数据
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref } from 'vue'
+
+  // 核心响应式状态
+  const skuInput = ref('')       // SKU输入值
+  const loading = ref(false)     // 加载状态
+  const showProductArea = ref(false) // 是否显示分析区域
+
+  // 清空输入框
+  const clearInput = () => {
+    skuInput.value = ''
+    showProductArea.value = false
+  }
+
+  // 搜索产品(仅UI状态切换,无数据处理)
+  const searchProduct = () => {
+    const sku = skuInput.value.trim()
+    if (!sku) {
+      alert('请输入产品SKU')
+      return
+    }
+
+    // 模拟加载状态
+    loading.value = true
+
+    // 仅延迟切换UI状态,无实际数据请求/处理
+    setTimeout(() => {
+      loading.value = false
+      showProductArea.value = true
+      alert(`已触发SKU "${sku}" 查询,当前为Vue空框架版本,无实际数据展示`)
+    }, 800)
+  }
+</script>
+
+<style scoped>
+  /* 全局基础样式 */
+  .product-analysis {
+    width: 100%;
+    padding: 20px;
+    box-sizing: border-box;
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+    background-color: #f8f9fa;
+    min-height: 100vh;
+  }
+
+  /* 搜索区域样式 */
+  .search-card {
+    background: #ffffff;
+    border-radius: 8px;
+    padding: 20px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+    margin-bottom: 20px;
+  }
+
+  .search-row {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    flex-wrap: wrap;
+  }
+
+  .search-col {
+    flex: 1;
+  }
+
+  .search-input-col {
+    flex: 2;
+    min-width: 300px;
+  }
+
+  .search-btn-col {
+    flex: none;
+    width: 140px;
+  }
+
+  .search-info-col {
+    flex: 1;
+    min-width: 200px;
+  }
+
+  .input-wrapper {
+    position: relative;
+    display: flex;
+    align-items: center;
+  }
+
+  .search-input {
+    width: 100%;
+    height: 40px;
+    padding: 0 40px 0 36px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    font-size: 14px;
+    outline: none;
+    transition: border-color 0.3s;
+  }
+
+  .search-input:focus {
+    border-color: #409eff;
+  }
+
+  .input-icon {
+    position: absolute;
+    left: 12px;
+    font-size: 16px;
+    color: #909399;
+  }
+
+  .clear-btn {
+    position: absolute;
+    right: 12px;
+    width: 20px;
+    height: 20px;
+    border: none;
+    background: transparent;
+    font-size: 16px;
+    color: #909399;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .search-btn {
+    width: 100%;
+    height: 40px;
+    background: #409eff;
+    color: #fff;
+    border: none;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    transition: background-color 0.3s;
+  }
+
+  .search-btn:hover:not(:disabled) {
+    background: #337ecc;
+  }
+
+  .search-btn:disabled {
+    background: #a0cfff;
+    cursor: not-allowed;
+  }
+
+  .loading-spinner {
+    width: 16px;
+    height: 16px;
+    border: 2px solid #fff;
+    border-top: 2px solid transparent;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+  }
+
+  @keyframes spin {
+    to {
+      transform: rotate(360deg);
+    }
+  }
+
+  .sku-tag {
+    display: inline-block;
+    height: 28px;
+    line-height: 28px;
+    padding: 0 10px;
+    background: #ecf5ff;
+    color: #409eff;
+    border-radius: 4px;
+    font-size: 12px;
+  }
+
+  /* 初始空状态 */
+  .empty-init {
+    padding: 100px 0;
+    text-align: center;
+  }
+
+  .empty-icon {
+    font-size: 64px;
+    color: #dcdfe6;
+    margin-bottom: 20px;
+  }
+
+  .empty-text {
+    font-size: 16px;
+    color: #909399;
+  }
+
+  /* 分析区域样式 */
+  .analysis-content {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+  }
+
+  /* 关键指标卡片 */
+  .metrics-row {
+    display: flex;
+    gap: 20px;
+    flex-wrap: wrap;
+  }
+
+  .metric-card {
+    flex: 1;
+    min-width: 200px;
+    background: #ffffff;
+    border-radius: 8px;
+    padding: 20px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+    text-align: center;
+    cursor: pointer;
+    transition: transform 0.3s, box-shadow 0.3s;
+  }
+
+  .metric-card:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  }
+
+  .metric-label {
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 10px;
+  }
+
+  .metric-value {
+    font-size: 32px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 5px;
+  }
+
+  .metric-unit {
+    font-size: 12px;
+    color: #909399;
+  }
+
+  /* 通用卡片容器 */
+  .card-container, .insight-card {
+    background: #ffffff;
+    border-radius: 8px;
+    padding: 20px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+  }
+
+  .card-header {
+    font-size: 16px;
+    font-weight: 500;
+    color: #303133;
+    margin-bottom: 16px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  /* 表格样式 */
+  .table-container {
+    width: 100%;
+    overflow-x: auto;
+  }
+
+  .turnover-table {
+    width: 100%;
+    border-collapse: collapse;
+    text-align: left;
+  }
+
+  .turnover-table th,
+  .turnover-table td {
+    padding: 12px;
+    border: 1px solid #ebeef5;
+  }
+
+  .turnover-table th {
+    background: #f5f7fa;
+    color: #606266;
+    font-weight: 500;
+  }
+
+  .empty-row td {
+    text-align: center;
+    color: #909399;
+  }
+
+  /* 图表占位符 */
+  .chart-placeholder {
+    height: 400px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #909399;
+    border: 1px dashed #dcdfe6;
+    border-radius: 4px;
+  }
+
+  /* 洞察区域 */
+  .insights-row {
+    display: flex;
+    gap: 20px;
+    flex-wrap: wrap;
+  }
+
+  .insight-card {
+    flex: 1;
+    min-width: 300px;
+  }
+
+  .empty-placeholder {
+    padding: 40px 0;
+    text-align: center;
+    color: #909399;
+  }
+</style>

+ 217 - 0
src/views/supply/cost/index.vue

@@ -0,0 +1,217 @@
+<template>
+  <div class="app-container">
+    <div class="page-header">
+      <h2><i class="el-icon-s-order"></i> 供应商成本分析</h2>
+      <p class="page-desc">分析各供应商针对特定产品的报价及成本竞争力</p>
+    </div>
+
+    <!-- 搜索框 -->
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-search"></i> 查询产品成本分析</span>
+      </div>
+      <el-form :inline="true" class="form-inline">
+        <el-form-item label="产品编码">
+          <el-input
+            v-model="inputProductCode"
+            placeholder="请输入产品编码(如:20220606J0100MR4)"
+            style="width: 300px"
+            @keyup.enter.native="fetchCostData(inputProductCode)"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="fetchCostData(inputProductCode)"
+            >分析成本</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 表格数据 -->
+    <el-card v-loading="loading" class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-document"></i> 成本详情表格</span>
+        <el-button
+          v-if="costDetails.length > 0"
+          style="float: right; padding: 3px 0"
+          type="text"
+          @click="exportData"
+        >
+          导出数据
+        </el-button>
+      </div>
+
+      <el-table
+        v-if="costDetails.length > 0"
+        :data="costDetails"
+        highlight-current-row
+        style="width: 100%"
+      >
+        <el-table-column prop="供应商名称" label="供应商名称" width="200" />
+        <el-table-column prop="参考价" label="参考价" width="120">
+          <template slot-scope="scope">
+            ¥{{ parseFloat(scope.row.参考价).toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="成本排名" label="成本排名" width="120">
+          <template slot-scope="scope"> 第{{ scope.row.成本排名 }}名 </template>
+        </el-table-column>
+        <el-table-column prop="成本分数" label="成本分数" width="120">
+          <template slot-scope="scope">
+            {{ parseFloat(scope.row.成本分数).toFixed(2) }}
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div v-else class="no-data">
+        <p v-if="error" class="error">{{ error }}</p>
+        <p v-else>暂无成本数据,请输入产品编码进行分析</p>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getToken } from "@/utils/auth";
+import { download } from "@/utils";
+import request from "@/utils/request";
+
+export default {
+  name: "SupplyCostAnalysis",
+  data() {
+    return {
+      inputProductCode: "",
+      costDetails: [],
+      loading: false,
+      error: "",
+    };
+  },
+  created() {
+    // 如果有传入的 productCode,则自动触发分析
+    if (this.$route.query.productCode) {
+      this.inputProductCode = this.$route.query.productCode;
+      this.fetchCostData(this.inputProductCode);
+    }
+  },
+  methods: {
+    // 获取成本数据
+    async fetchCostData(code) {
+      if (!code) {
+        this.$modal.msgError("请输入产品编码");
+        return;
+      }
+
+      this.loading = true;
+      this.error = "";
+
+      try {
+        // 使用项目标准的request方式调用API
+        const response = await request({
+          url: `http://localhost:5000/api/product/${code}/details`,
+          method: "get",
+        });
+
+        // Flask API通常直接返回数据,没有code字段
+        if (response && response.cost_details !== undefined) {
+          this.costDetails = response.cost_details || [];
+          this.$modal.msgSuccess("成本分析完成");
+        } else {
+          // 尝试其他可能的数据结构
+          if (response && Array.isArray(response)) {
+            this.costDetails = response || [];
+            this.$modal.msgSuccess("成本分析完成");
+          } else {
+            this.error = "获取成本数据失败,API响应格式不匹配";
+            this.$modal.msgError(this.error);
+          }
+        }
+      } catch (err) {
+        console.error("获取成本数据失败:", err);
+        this.error = err.message || "获取成本数据失败";
+        this.$modal.msgError(this.error);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 导出数据
+    exportData() {
+      if (!this.costDetails || this.costDetails.length === 0) {
+        this.$modal.msgError("暂无数据可导出");
+        return;
+      }
+
+      // 创建CSV内容
+      const header = ["供应商名称", "参考价", "成本排名", "成本分数"].join(",");
+      const csvContent = [
+        header,
+        ...this.costDetails.map((row) =>
+          [
+            `"${row.供应商名称}"`,
+            row.参考价,
+            `第${row.成本排名}名`,
+            row.成本分数,
+          ].join(",")
+        ),
+      ].join("\n");
+
+      // 下载文件
+      const blob = new Blob(["\ufeff" + csvContent], {
+        type: "text/csv;charset=utf-8;",
+      });
+      download(blob, `供应商成本分析_${new Date().getTime()}.csv`);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+
+  h2 {
+    font-size: 24px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 8px;
+
+    i {
+      margin-right: 8px;
+      color: #409eff;
+    }
+  }
+
+  .page-desc {
+    color: #909399;
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+.box-card {
+  margin-bottom: 20px;
+}
+
+.form-inline {
+  display: flex;
+  align-items: center;
+}
+
+.no-data {
+  text-align: center;
+  padding: 40px 0;
+  color: #909399;
+
+  .error {
+    color: #f56c6c;
+  }
+}
+
+::v-deep .el-card__header {
+  font-weight: bold;
+}
+</style>

+ 238 - 0
src/views/supply/delivery/index.vue

@@ -0,0 +1,238 @@
+<template>
+  <div class="app-container">
+    <div class="page-header">
+      <h2><i class="el-icon-s-opportunity"></i> 供应商交付分析</h2>
+      <p class="page-desc">分析各供应商针对特定产品的交付表现</p>
+    </div>
+
+    <!-- 搜索框 -->
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-search"></i> 查询产品交付分析</span>
+      </div>
+      <el-form :inline="true" class="form-inline">
+        <el-form-item label="产品编码">
+          <el-input
+            v-model="inputProductCode"
+            placeholder="请输入产品编码(如:20220606J0100MR4)"
+            style="width: 300px"
+            @keyup.enter.native="fetchDeliveryData(inputProductCode)"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="fetchDeliveryData(inputProductCode)"
+            >分析交付</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 表格数据 -->
+    <el-card v-loading="loading" class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-document"></i> 交付详情表格</span>
+        <el-button
+          v-if="deliveryDetails.length > 0"
+          style="float: right; padding: 3px 0"
+          type="text"
+          @click="exportData"
+        >
+          导出数据
+        </el-button>
+      </div>
+
+      <el-table
+        v-if="deliveryDetails.length > 0"
+        :data="deliveryDetails"
+        highlight-current-row
+        style="width: 100%"
+      >
+        <el-table-column prop="供应商名称" label="供应商名称" width="150" />
+        <el-table-column prop="交付分数" label="交付分数" width="120">
+          <template slot-scope="scope">
+            {{ parseFloat(scope.row.交付分数).toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="D1_准时率" label="准时率 (%)" width="120">
+          <template slot-scope="scope">
+            {{ parseFloat(scope.row.D1_准时率).toFixed(2) }}%
+          </template>
+        </el-table-column>
+        <el-table-column prop="D2_平均偏差" label="平均偏差 (天)" width="130">
+          <template slot-scope="scope">
+            {{ parseFloat(scope.row.D2_平均偏差).toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="D3_最长延迟" label="最长延迟 (天)" width="130">
+          <template slot-scope="scope">
+            {{ parseFloat(scope.row.D3_最长延迟).toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="D4_满足率" label="满足率 (%)" width="120">
+          <template slot-scope="scope">
+            {{ parseFloat(scope.row.D4_满足率).toFixed(2) }}%
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div v-else class="no-data">
+        <p v-if="error" class="error">{{ error }}</p>
+        <p v-else>暂无交付数据,请输入产品编码进行分析</p>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getToken } from "@/utils/auth";
+import { download } from "@/utils";
+import request from "@/utils/request";
+
+export default {
+  name: "SupplyDeliveryAnalysis",
+  data() {
+    return {
+      inputProductCode: "",
+      deliveryDetails: [],
+      loading: false,
+      error: "",
+    };
+  },
+  created() {
+    // 如果有传入的 productCode,则自动触发分析
+    if (this.$route.query.productCode) {
+      this.inputProductCode = this.$route.query.productCode;
+      this.fetchDeliveryData(this.inputProductCode);
+    }
+  },
+  methods: {
+    // 获取交付数据
+    async fetchDeliveryData(code) {
+      if (!code) {
+        this.$modal.msgError("请输入产品编码");
+        return;
+      }
+
+      this.loading = true;
+      this.error = "";
+
+      try {
+        // 使用项目标准的request方式调用API
+        const response = await request({
+          url: `http://localhost:5000/api/product/${code}/details`,
+          method: "get",
+        });
+
+        // Flask API通常直接返回数据,没有code字段
+        if (response && response.delivery_details !== undefined) {
+          this.deliveryDetails = response.delivery_details || [];
+          this.$modal.msgSuccess("交付分析完成");
+        } else {
+          // 尝试其他可能的数据结构
+          if (response && Array.isArray(response)) {
+            this.deliveryDetails = response || [];
+            this.$modal.msgSuccess("交付分析完成");
+          } else {
+            this.error = "获取交付数据失败,API响应格式不匹配";
+            this.$modal.msgError(this.error);
+          }
+        }
+      } catch (err) {
+        console.error("获取交付数据失败:", err);
+        this.error = err.message || "获取交付数据失败";
+        this.$modal.msgError(this.error);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 导出数据
+    exportData() {
+      if (!this.deliveryDetails || this.deliveryDetails.length === 0) {
+        this.$modal.msgError("暂无数据可导出");
+        return;
+      }
+
+      // 创建CSV内容
+      const header = [
+        "供应商名称",
+        "交付分数",
+        "准时率(%)",
+        "平均偏差(天)",
+        "最长延迟(天)",
+        "满足率(%)",
+      ].join(",");
+      const csvContent = [
+        header,
+        ...this.deliveryDetails.map((row) =>
+          [
+            `"${row.供应商名称}"`,
+            parseFloat(row.交付分数).toFixed(2),
+            parseFloat(row.D1_准时率).toFixed(2),
+            parseFloat(row.D2_平均偏差).toFixed(2),
+            parseFloat(row.D3_最长延迟).toFixed(2),
+            parseFloat(row.D4_满足率).toFixed(2),
+          ].join(",")
+        ),
+      ].join("\n");
+
+      // 下载文件
+      const blob = new Blob(["\ufeff" + csvContent], {
+        type: "text/csv;charset=utf-8;",
+      });
+      download(blob, `供应商交付分析_${new Date().getTime()}.csv`);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+
+  h2 {
+    font-size: 24px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 8px;
+
+    i {
+      margin-right: 8px;
+      color: #409eff;
+    }
+  }
+
+  .page-desc {
+    color: #909399;
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+.box-card {
+  margin-bottom: 20px;
+}
+
+.form-inline {
+  display: flex;
+  align-items: center;
+}
+
+.no-data {
+  text-align: center;
+  padding: 40px 0;
+  color: #909399;
+
+  .error {
+    color: #f56c6c;
+  }
+}
+
+::v-deep .el-card__header {
+  font-weight: bold;
+}
+</style>

+ 804 - 0
src/views/supply/overall/index.vue

@@ -0,0 +1,804 @@
+<template>
+  <div class="home">
+    <div class="page-header">
+      <div class="page-title">供应商能力综合评分面板</div>
+      <div class="page-subtitle">
+        实时监控供应商表现,智能分析综合能力,辅助决策优化供应链
+      </div>
+    </div>
+
+    <!-- Stats Grid -->
+    <div class="stats-grid">
+      <div class="stat-card">
+        <div class="stat-header">
+          <div class="stat-label">供应商总数</div>
+          <div class="stat-badge badge-time">实时</div>
+        </div>
+        <div class="stat-value">156</div>
+        <div class="stat-trend trend-up">
+          <span>↑ 6.2%</span>
+          <span style="color: #999">较上月</span>
+        </div>
+      </div>
+
+      <div class="stat-card">
+        <div class="stat-header">
+          <div class="stat-label">平均综合得分</div>
+          <div class="stat-badge badge-time">实时</div>
+        </div>
+        <div class="stat-value">82.5</div>
+        <div class="stat-trend trend-up">
+          <span>↑ 3.8%</span>
+          <span style="color: #999">较上月</span>
+        </div>
+      </div>
+
+      <div class="stat-card">
+        <div class="stat-header">
+          <div class="stat-label">预警供应商</div>
+          <div class="stat-badge badge-warning">预警</div>
+        </div>
+        <div class="stat-value">8</div>
+        <div class="stat-trend trend-down">
+          <span>↓ 2</span>
+          <span style="color: #999">低于60分</span>
+        </div>
+      </div>
+
+      <div class="stat-card">
+        <div class="stat-header">
+          <div class="stat-label">优秀供应商</div>
+          <div class="stat-badge badge-time">实时</div>
+        </div>
+        <div class="stat-value">45</div>
+        <div class="stat-trend trend-up">
+          <span>↑ 5</span>
+          <span style="color: #999">≥85分</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- Search and Chart Section -->
+    <div class="content-card">
+      <div class="card-title">🔍 单个产品查询供应商评分</div>
+      <div class="search-box">
+        <div class="input-wrapper">
+          <input
+            v-model="inputProductCode"
+            type="text"
+            class="search-input"
+            placeholder="输入产品编码(如:20220606J0100MR4)"
+          />
+        </div>
+        <button @click="triggerEvaluation" class="search-btn">预测分析</button>
+      </div>
+
+      <div class="chart-container">
+        <div style="font-size: 14px; color: #666; margin-bottom: 15px">
+          产品供应商三维度评分趋势对比
+        </div>
+        <div class="chart-placeholder">
+          <svg
+            class="chart-lines"
+            viewBox="0 0 800 300"
+            preserveAspectRatio="none"
+          >
+            <polyline
+              points="50,250 150,180 250,120 350,80 450,90 550,110 650,140 750,200"
+              fill="none"
+              stroke="rgba(255,255,255,0.6)"
+              stroke-width="3"
+            />
+            <polyline
+              points="50,270 150,220 250,160 350,100 450,95 550,120 650,160 750,220"
+              fill="none"
+              stroke="rgba(255,255,255,0.4)"
+              stroke-width="2"
+              stroke-dasharray="5,5"
+            />
+            <polyline
+              points="50,280 150,240 250,200 350,140 450,130 550,150 650,180 750,240"
+              fill="none"
+              stroke="rgba(255,255,255,0.4)"
+              stroke-width="2"
+              stroke-dasharray="5,5"
+            />
+          </svg>
+          <div style="position: relative; z-index: 1">
+            <div style="font-size: 14px; opacity: 0.8; margin-bottom: 5px">
+              选择产品查看供应商评分趋势
+            </div>
+            <div style="font-size: 12px; opacity: 0.6">
+              蓝线:成本得分 | 红线:交付得分 | 绿线:账期得分
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="info-grid">
+        <div class="info-box">
+          <div class="info-label">评估结果</div>
+          <div class="info-value" style="color: #52c41a">已加载</div>
+          <div class="info-detail">
+            供应商数量: 5家<br />
+            评估完成时间: 2025-10-29 14:30<br />
+            <span style="color: #1890ff">最佳供应商: 华信电子</span>
+          </div>
+        </div>
+
+        <div class="info-box">
+          <div class="info-label">数据周期</div>
+          <div class="info-value">180天</div>
+          <div class="info-detail">
+            评估订单总数: 238笔<br />
+            时间范围: 2025-05-01 至 2025-10-28
+          </div>
+        </div>
+
+        <div class="info-box">
+          <div class="info-label">维度评分范围</div>
+          <div class="info-value">80-95分</div>
+          <div class="info-detail">
+            成本: 80-95分<br />
+            交付: 85-94分<br />
+            账期: 80-90分
+          </div>
+        </div>
+
+        <div class="info-box">
+          <div class="info-label">综合评级分布</div>
+          <div class="info-value">优秀: 5家</div>
+          <div class="info-detail">
+            A级(&ge;85分): 3家<br />
+            B级(70-85分): 2家<br />
+            C级(&lt;70分): 0家
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Weights Configuration Summary -->
+    <div class="content-card">
+      <div class="card-title">📊 评估维度权重配置</div>
+      <div style="margin-bottom: 20px; color: #666; font-size: 14px">
+        当前配置:成本 40% | 交付 40% | 账期 20%
+      </div>
+      <div
+        style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px"
+      >
+        <div class="info-box">
+          <div class="info-label">💰 成本维度</div>
+          <div class="info-value" style="color: #5b6cff">40%</div>
+          <div class="info-detail">
+            评分规则:价格排名<br />
+            第1名: 100分<br />
+            第2名: 90分<br />
+            第3名: 80分
+          </div>
+        </div>
+        <div class="info-box">
+          <div class="info-label">🚚 交付维度</div>
+          <div class="info-value" style="color: #52c41a">40%</div>
+          <div class="info-detail">
+            D1-准时率: 50%<br />
+            D2-平均偏差: 30%<br />
+            D3-最长延迟: 10%<br />
+            D4-数量满足率: 10%
+          </div>
+        </div>
+        <div class="info-box">
+          <div class="info-label">💳 账期维度</div>
+          <div class="info-value" style="color: #ff9800">20%</div>
+          <div class="info-detail">
+            ≥90天: 100分<br />
+            ≥60天: 90分<br />
+            ≥45天: 80分<br />
+            ≥30天: 60分
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Data Statistics -->
+    <div class="content-card">
+      <div class="card-title">📋 评估数据统计</div>
+      <table class="supplier-table">
+        <thead>
+          <tr>
+            <th>维度</th>
+            <th>数据来源</th>
+            <th>评估指标</th>
+            <th>数据记录数</th>
+            <th>数据完整度</th>
+            <th>状态</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td><strong>💰 成本维度</strong></td>
+            <td>采购订单统计.xlsx</td>
+            <td>供应商报价排名</td>
+            <td>2,845条</td>
+            <td>
+              <div class="progress-bar">
+                <div class="progress-fill" style="width: 98.5%"></div>
+              </div>
+              <span style="font-size: 12px; color: #666">98.5%</span>
+            </td>
+            <td><span style="color: #52c41a">● 正常</span></td>
+          </tr>
+          <tr>
+            <td><strong>🚚 交付维度</strong></td>
+            <td>采购数据_双键合并结果.xlsx</td>
+            <td>准时率、偏差、延迟、满足率</td>
+            <td>5,126条</td>
+            <td>
+              <div class="progress-bar">
+                <div class="progress-fill" style="width: 99.2%"></div>
+              </div>
+              <span style="font-size: 12px; color: #666">99.2%</span>
+            </td>
+            <td><span style="color: #52c41a">● 正常</span></td>
+          </tr>
+          <tr>
+            <td><strong>💳 账期维度</strong></td>
+            <td>供应商账期.xlsx</td>
+            <td>结算期限天数</td>
+            <td>156条</td>
+            <td>
+              <div class="progress-bar">
+                <div class="progress-fill" style="width: 100%"></div>
+              </div>
+              <span style="font-size: 12px; color: #666">100%</span>
+            </td>
+            <td><span style="color: #52c41a">● 正常</span></td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- Top 5 Suppliers -->
+    <div class="content-card">
+      <div class="card-title">
+        🏆 Top 5 供应商综合排名
+        <span
+          style="
+            font-size: 12px;
+            color: #999;
+            font-weight: normal;
+            margin-left: 10px;
+          "
+        >
+          基于产品代码: {{ displayedProductCode || "20220606J0100MR4" }}
+        </span>
+      </div>
+
+      <div v-if="loading" class="loading">加载中...</div>
+      <div v-else-if="error" class="error">{{ error }}</div>
+      <div v-else>
+        <table class="supplier-table">
+          <thead>
+            <tr>
+              <th>综合排名</th>
+              <th>供应商名称</th>
+              <th>供应商代码</th>
+              <th>综合得分</th>
+              <th>维度得分</th>
+              <th>参考价</th>
+              <th>成本排名</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr
+              v-for="(supplier, index) in evaluationData"
+              :key="supplier.供应商代码"
+            >
+              <td>
+                <span :class="['rank-badge', getRankClass(index + 1)]">{{
+                  index + 1
+                }}</span>
+              </td>
+              <td>
+                <strong>{{ supplier.供应商名称 }}</strong>
+              </td>
+              <td>{{ supplier.供应商代码 || "N/A" }}</td>
+              <td>
+                <strong style="font-size: 18px; color: #52c41a">{{
+                  parseFloat(supplier.综合得分).toFixed(2)
+                }}</strong>
+                <div class="progress-bar">
+                  <div
+                    class="progress-fill"
+                    :style="{ width: `${supplier.综合得分}%` }"
+                  ></div>
+                </div>
+              </td>
+              <td>
+                <div class="dimension-scores">
+                  <span class="dim-score"
+                    >成本:{{ parseFloat(supplier.成本分数).toFixed(0) }}</span
+                  >
+                  <span class="dim-score"
+                    >交付:{{ parseFloat(supplier.交付分数).toFixed(0) }}</span
+                  >
+                  <span class="dim-score"
+                    >账期:{{ parseFloat(supplier.账期分数).toFixed(0) }}</span
+                  >
+                </div>
+              </td>
+              <td>¥{{ parseFloat(supplier.参考价).toFixed(2) }}</td>
+              <td>第{{ supplier.成本排名 }}名</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from "@/utils/auth";
+import request from "@/utils/request";
+
+export default {
+  name: "SupplyOverallEvaluation",
+  data() {
+    return {
+      inputProductCode: "",
+      displayedProductCode: "",
+      evaluationData: [],
+      loading: false,
+      error: "",
+    };
+  },
+  created() {
+    // 如果有传入的 productCode,则自动触发评估
+    if (this.$route.query.productCode) {
+      this.inputProductCode = this.$route.query.productCode;
+      this.fetchEvaluationData(this.inputProductCode);
+    }
+  },
+  methods: {
+    // 监听路由传入的 productCode
+    triggerEvaluation() {
+      if (this.inputProductCode) {
+        this.fetchEvaluationData(this.inputProductCode);
+      }
+    },
+
+    async fetchEvaluationData(code) {
+      this.loading = true;
+      this.error = "";
+      this.displayedProductCode = code;
+      try {
+        const response = await request({
+          url: "http://localhost:5000/api/evaluate",
+          method: "post",
+          data: {
+            product_code: code,
+          },
+        });
+        // Flask API可能直接返回数据,而不是包装在特定字段中
+        if (Array.isArray(response)) {
+          this.evaluationData = response;
+        } else if (response && response.data) {
+          this.evaluationData = response.data;
+        } else {
+          this.evaluationData = response || [];
+        }
+        this.$modal.msgSuccess("评估完成");
+      } catch (err) {
+        console.error("获取评估数据失败:", err);
+        this.error = err.message || "获取评估数据失败";
+        this.$modal.msgError(this.error);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 获取排名样式类
+    getRankClass(rank) {
+      if (rank === 1) return "rank-1";
+      if (rank === 2) return "rank-2";
+      if (rank === 3) return "rank-3";
+      return "rank-other";
+    },
+  },
+};
+</script>
+
+<style scoped>
+.page-header {
+  margin-bottom: 40px;
+  padding-bottom: 20px;
+  border-bottom: 2px solid rgba(102, 126, 234, 0.1);
+}
+
+.page-title {
+  font-size: 32px;
+  font-weight: 700;
+  margin-bottom: 12px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.page-subtitle {
+  color: #666;
+  font-size: 15px;
+  font-weight: 400;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+  gap: 24px;
+  margin-bottom: 40px;
+}
+
+.stat-card {
+  background: white;
+  padding: 28px;
+  border-radius: 16px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+  border: 1px solid rgba(102, 126, 234, 0.1);
+  position: relative;
+  overflow: hidden;
+}
+
+.stat-card::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 4px;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+}
+
+.stat-card:hover {
+  transform: translateY(-4px);
+  box-shadow: 0 8px 30px rgba(102, 126, 234, 0.15);
+}
+
+.stat-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.stat-label {
+  color: #666;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.stat-badge {
+  padding: 4px 10px;
+  border-radius: 12px;
+  font-size: 11px;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
+.badge-time {
+  background: linear-gradient(135deg, #e6f0ff 0%, #d6e5ff 100%);
+  color: #667eea;
+}
+
+.badge-warning {
+  background: linear-gradient(135deg, #fff7e6 0%, #ffe6cc 100%);
+  color: #ff9800;
+}
+
+.stat-value {
+  font-size: 36px;
+  font-weight: 700;
+  margin-bottom: 12px;
+  color: #2c3e50;
+  line-height: 1.2;
+}
+
+.stat-trend {
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-weight: 500;
+}
+
+.trend-up {
+  color: #52c41a;
+}
+
+.trend-down {
+  color: #ff4d4f;
+}
+
+.content-card {
+  background: white;
+  padding: 32px;
+  border-radius: 16px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  margin-bottom: 24px;
+  border: 1px solid rgba(102, 126, 234, 0.1);
+  transition: all 0.3s;
+}
+
+.content-card:hover {
+  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
+}
+
+.card-title {
+  font-size: 18px;
+  font-weight: 700;
+  margin-bottom: 24px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  color: #2c3e50;
+}
+
+.search-box {
+  display: flex;
+  gap: 16px;
+  margin-bottom: 32px;
+}
+
+.input-wrapper {
+  flex: 1;
+  position: relative;
+}
+
+.search-input {
+  width: 100%;
+  padding: 16px 20px;
+  border: 2px solid #e8ecf1;
+  border-radius: 12px;
+  font-size: 15px;
+  outline: none;
+  transition: all 0.3s;
+  background: #fafbfc;
+  font-weight: 500;
+}
+
+.search-input:focus {
+  border-color: #667eea;
+  background: white;
+  box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
+}
+
+.search-btn {
+  padding: 16px 40px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 12px;
+  font-size: 15px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
+  white-space: nowrap;
+}
+
+.search-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+}
+
+.search-btn:active {
+  transform: translateY(0);
+}
+
+.chart-container {
+  margin-bottom: 32px;
+}
+
+.chart-placeholder {
+  width: 100%;
+  height: 320px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-size: 16px;
+  position: relative;
+  overflow: hidden;
+  box-shadow: 0 8px 30px rgba(102, 126, 234, 0.3);
+}
+
+.chart-lines {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  opacity: 0.2;
+}
+
+.info-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+  gap: 20px;
+}
+
+.info-box {
+  background: linear-gradient(135deg, #f8f9fa 0%, #f0f2f5 100%);
+  padding: 24px;
+  border-radius: 12px;
+  border: 1px solid #e8ecf1;
+  transition: all 0.3s;
+}
+
+.info-box:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+}
+
+.info-label {
+  font-size: 12px;
+  color: #999;
+  margin-bottom: 10px;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
+.info-value {
+  font-size: 28px;
+  font-weight: 700;
+  margin-bottom: 12px;
+  color: #2c3e50;
+}
+
+.info-detail {
+  font-size: 13px;
+  color: #666;
+  line-height: 1.8;
+}
+
+.supplier-table {
+  width: 100%;
+  border-collapse: separate;
+  border-spacing: 0;
+  margin-top: 24px;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
+}
+
+.supplier-table th {
+  background: linear-gradient(135deg, #f8f9fa 0%, #f0f2f5 100%);
+  padding: 16px;
+  text-align: left;
+  font-size: 13px;
+  color: #666;
+  font-weight: 700;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  border-bottom: 2px solid #e8ecf1;
+}
+
+.supplier-table td {
+  padding: 18px 16px;
+  border-bottom: 1px solid #f0f0f0;
+  font-size: 14px;
+  background: white;
+}
+
+.supplier-table tr:last-child td {
+  border-bottom: none;
+}
+
+.supplier-table tr:hover td {
+  background: #f8f9ff;
+}
+
+.rank-badge {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  font-weight: 700;
+  font-size: 14px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.rank-1 {
+  background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
+  color: #8b6914;
+}
+.rank-2 {
+  background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%);
+  color: #666;
+}
+.rank-3 {
+  background: linear-gradient(135deg, #cd7f32 0%, #e6a052 100%);
+  color: white;
+}
+.rank-other {
+  background: #f0f0f0;
+  color: #666;
+}
+
+.progress-bar {
+  width: 100%;
+  height: 8px;
+  background: #f0f0f0;
+  border-radius: 10px;
+  overflow: hidden;
+  margin-top: 8px;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #52c41a 0%, #95de64 100%);
+  border-radius: 10px;
+  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+  box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
+}
+
+.dimension-scores {
+  display: flex;
+  gap: 8px;
+  font-size: 12px;
+  flex-wrap: wrap;
+}
+
+.dim-score {
+  padding: 6px 12px;
+  background: linear-gradient(135deg, #f0f2ff 0%, #e8ecff 100%);
+  border-radius: 8px;
+  font-weight: 600;
+  color: #667eea;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+}
+
+.loading,
+.error {
+  font-size: 16px;
+  margin: 3rem 0;
+  text-align: center;
+  padding: 40px;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+}
+
+.loading {
+  color: #667eea;
+}
+
+.error {
+  color: #ff4d4f;
+  background: #fff5f5;
+  border: 1px solid #ffccc7;
+}
+
+@media (max-width: 1200px) {
+  .stats-grid {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+
+@media (max-width: 768px) {
+  .stats-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .search-box {
+    flex-direction: column;
+  }
+
+  .search-btn {
+    width: 100%;
+  }
+}
+</style>

+ 204 - 0
src/views/supply/payment/index.vue

@@ -0,0 +1,204 @@
+<template>
+  <div class="app-container">
+    <div class="page-header">
+      <h2><i class="el-icon-s-finance"></i> 供应商账期分析</h2>
+      <p class="page-desc">分析各供应商的结算账期条件</p>
+    </div>
+
+    <!-- 搜索框 -->
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-search"></i> 查询产品账期分析</span>
+      </div>
+      <el-form :inline="true" class="form-inline">
+        <el-form-item label="产品编码">
+          <el-input
+            v-model="inputProductCode"
+            placeholder="请输入产品编码(如:20220606J0100MR4)"
+            style="width: 300px"
+            @keyup.enter.native="fetchPaymentData(inputProductCode)"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="fetchPaymentData(inputProductCode)"
+            >分析账期</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 表格数据 -->
+    <el-card v-loading="loading" class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-document"></i> 账期详情表格</span>
+        <el-button
+          v-if="paymentDetails.length > 0"
+          style="float: right; padding: 3px 0"
+          type="text"
+          @click="exportData"
+        >
+          导出数据
+        </el-button>
+      </div>
+
+      <el-table
+        v-if="paymentDetails.length > 0"
+        :data="paymentDetails"
+        highlight-current-row
+        style="width: 100%"
+      >
+        <el-table-column prop="供应商名称" label="供应商名称" width="200" />
+        <el-table-column prop="账期分数" label="账期分数" width="120">
+          <template slot-scope="scope">
+            {{ parseFloat(scope.row.账期分数).toFixed(2) }}
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div v-else class="no-data">
+        <p v-if="error" class="error">{{ error }}</p>
+        <p v-else>暂无账期数据,请输入产品编码进行分析</p>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getToken } from "@/utils/auth";
+import { download } from "@/utils";
+import request from "@/utils/request";
+
+export default {
+  name: "SupplyPaymentAnalysis",
+  data() {
+    return {
+      inputProductCode: "",
+      paymentDetails: [],
+      loading: false,
+      error: "",
+    };
+  },
+  created() {
+    // 如果有传入的 productCode,则自动触发分析
+    if (this.$route.query.productCode) {
+      this.inputProductCode = this.$route.query.productCode;
+      this.fetchPaymentData(this.inputProductCode);
+    }
+  },
+  methods: {
+    // 获取账期数据
+    async fetchPaymentData(code) {
+      if (!code) {
+        this.$modal.msgError("请输入产品编码");
+        return;
+      }
+
+      this.loading = true;
+      this.error = "";
+
+      try {
+        // 使用项目标准的request方式调用API
+        const response = await request({
+          url: `http://localhost:5000/api/product/${code}/details`,
+          method: "get",
+        });
+
+        // Flask API通常直接返回数据,没有code字段
+        if (response && response.payment_details !== undefined) {
+          this.paymentDetails = response.payment_details || [];
+          this.$modal.msgSuccess("账期分析完成");
+        } else {
+          // 尝试其他可能的数据结构
+          if (response && Array.isArray(response)) {
+            this.paymentDetails = response || [];
+            this.$modal.msgSuccess("账期分析完成");
+          } else {
+            this.error = "获取账期数据失败,API响应格式不匹配";
+            this.$modal.msgError(this.error);
+          }
+        }
+      } catch (err) {
+        console.error("获取账期数据失败:", err);
+        this.error = err.message || "获取账期数据失败";
+        this.$modal.msgError(this.error);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 导出数据
+    exportData() {
+      if (!this.paymentDetails || this.paymentDetails.length === 0) {
+        this.$modal.msgError("暂无数据可导出");
+        return;
+      }
+
+      // 创建CSV内容
+      const header = ["供应商名称", "账期分数"].join(",");
+      const csvContent = [
+        header,
+        ...this.paymentDetails.map((row) =>
+          [`"${row.供应商名称}"`, parseFloat(row.账期分数).toFixed(2)].join(",")
+        ),
+      ].join("\n");
+
+      // 下载文件
+      const blob = new Blob(["\ufeff" + csvContent], {
+        type: "text/csv;charset=utf-8;",
+      });
+      download(blob, `供应商账期分析_${new Date().getTime()}.csv`);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+
+  h2 {
+    font-size: 24px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 8px;
+
+    i {
+      margin-right: 8px;
+      color: #409eff;
+    }
+  }
+
+  .page-desc {
+    color: #909399;
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+.box-card {
+  margin-bottom: 20px;
+}
+
+.form-inline {
+  display: flex;
+  align-items: center;
+}
+
+.no-data {
+  text-align: center;
+  padding: 40px 0;
+  color: #909399;
+
+  .error {
+    color: #f56c6c;
+  }
+}
+
+::v-deep .el-card__header {
+  font-weight: bold;
+}
+</style>

+ 399 - 0
src/views/supply/weights/index.vue

@@ -0,0 +1,399 @@
+<template>
+  <div class="app-container">
+    <div class="page-header">
+      <h2><i class="el-icon-setting"></i> 权重配置管理</h2>
+      <p class="page-desc">自定义成本、交付、账期三个维度的评估权重</p>
+    </div>
+
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-edit-outline"></i> 权重配置</span>
+      </div>
+
+      <el-form
+        :model="weights"
+        :rules="rules"
+        ref="weightsForm"
+        label-width="120px"
+      >
+        <el-form-item label="成本权重" prop="成本">
+          <el-slider
+            v-model="weights.成本"
+            :step="0.01"
+            :min="0"
+            :max="1"
+            show-input
+            @change="onWeightChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="交付权重" prop="交付">
+          <el-slider
+            v-model="weights.交付"
+            :step="0.01"
+            :min="0"
+            :max="1"
+            show-input
+            @change="onWeightChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="账期权重" prop="账期">
+          <el-slider
+            v-model="weights.账期"
+            :step="0.01"
+            :min="0"
+            :max="1"
+            show-input
+            @change="onWeightChange"
+          />
+        </el-form-item>
+
+        <el-form-item>
+          <div class="weight-summary">
+            <p>权重总和: {{ totalWeight.toFixed(2) }}</p>
+            <p v-if="Math.abs(totalWeight - 1) > 0.001" class="warning">
+              警告: 权重总和应为1
+            </p>
+          </div>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button
+            type="primary"
+            :disabled="Math.abs(totalWeight - 1) > 0.001"
+            @click="updateWeights"
+          >
+            保存权重
+          </el-button>
+          <el-button @click="resetWeights">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <div
+        v-if="message"
+        class="el-alert"
+        :class="'el-alert--' + (isError ? 'error' : 'success')"
+      >
+        <div class="el-alert__content">
+          <span class="el-alert__description">{{ message }}</span>
+        </div>
+      </div>
+    </el-card>
+
+    <!-- 当前权重展示 -->
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span><i class="el-icon-data-line"></i> 当前评估维度权重配置</span>
+      </div>
+
+      <div class="weight-display">
+        <div class="weight-info">
+          当前配置:成本 {{ (weights.成本 * 100).toFixed(0) }}% | 交付
+          {{ (weights.交付 * 100).toFixed(0) }}% | 账期
+          {{ (weights.账期 * 100).toFixed(0) }}%
+        </div>
+
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="info-box">
+              <div class="info-label">💰 成本维度</div>
+              <div class="info-value" style="color: #5b6cff">
+                {{ (weights.成本 * 100).toFixed(0) }}%
+              </div>
+              <div class="info-detail">
+                评分规则:价格排名<br />
+                第1名: 100分<br />
+                第2名: 90分<br />
+                第3名: 80分
+              </div>
+            </div>
+          </el-col>
+
+          <el-col :span="8">
+            <div class="info-box">
+              <div class="info-label">🚚 交付维度</div>
+              <div class="info-value" style="color: #52c41a">
+                {{ (weights.交付 * 100).toFixed(0) }}%
+              </div>
+              <div class="info-detail">
+                D1-准时率: 50%<br />
+                D2-平均偏差: 30%<br />
+                D3-最长延迟: 10%<br />
+                D4-数量满足率: 10%
+              </div>
+            </div>
+          </el-col>
+
+          <el-col :span="8">
+            <div class="info-box">
+              <div class="info-label">💳 账期维度</div>
+              <div class="info-value" style="color: #ff9800">
+                {{ (weights.账期 * 100).toFixed(0) }}%
+              </div>
+              <div class="info-detail">
+                ≥90天: 100分<br />
+                ≥60天: 90分<br />
+                ≥45天: 80分<br />
+                ≥30天: 60分
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+
+export default {
+  name: "SupplyWeightsConfig",
+  data() {
+    return {
+      weights: {
+        成本: 0.4,
+        交付: 0.4,
+        账期: 0.2,
+      },
+      loading: false,
+      message: "",
+      isError: false,
+      rules: {
+        成本: [
+          { required: true, message: "请输入成本权重", trigger: "blur" },
+          {
+            type: "number",
+            min: 0,
+            max: 1,
+            message: "权重应在0到1之间",
+            trigger: "blur",
+          },
+        ],
+        交付: [
+          { required: true, message: "请输入交付权重", trigger: "blur" },
+          {
+            type: "number",
+            min: 0,
+            max: 1,
+            message: "权重应在0到1之间",
+            trigger: "blur",
+          },
+        ],
+        账期: [
+          { required: true, message: "请输入账期权重", trigger: "blur" },
+          {
+            type: "number",
+            min: 0,
+            max: 1,
+            message: "权重应在0到1之间",
+            trigger: "blur",
+          },
+        ],
+      },
+    };
+  },
+  computed: {
+    // 计算权重总和
+    totalWeight() {
+      return this.weights.成本 + this.weights.交付 + this.weights.账期;
+    },
+  },
+  created() {
+    this.fetchWeights();
+  },
+  methods: {
+    // 获取当前权重配置
+    async fetchWeights() {
+      this.loading = true;
+      try {
+        const response = await request({
+          url: "http://localhost:5000/api/config/weights",
+          method: "get",
+        });
+        // Flask API可能直接返回权重对象,而不是封装在data字段中
+        if (
+          response &&
+          typeof response === "object" &&
+          !Array.isArray(response)
+        ) {
+          this.weights = {
+            成本:
+              response.成本 || response.成本权重 || response.cost_weight || 0.4,
+            交付:
+              response.交付 ||
+              response.交付权重 ||
+              response.delivery_weight ||
+              0.4,
+            账期:
+              response.账期 ||
+              response.账期权重 ||
+              response.payment_weight ||
+              0.2,
+          };
+        } else {
+          this.weights = {
+            成本: 0.4,
+            交付: 0.4,
+            账期: 0.2,
+          };
+        }
+        this.$modal.msgSuccess("权重配置加载成功");
+      } catch (err) {
+        console.error("获取权重配置失败:", err);
+        this.message = "获取权重配置失败";
+        this.isError = true;
+        this.$modal.msgError(this.message);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 更新权重配置
+    async updateWeights() {
+      if (Math.abs(this.totalWeight - 1) > 0.001) {
+        this.message = "权重总和必须为1";
+        this.isError = true;
+        this.$modal.msgError(this.message);
+        return;
+      }
+
+      try {
+        const response = await request({
+          url: "http://localhost:5000/api/config/weights",
+          method: "put",
+          data: this.weights,
+        });
+        // Flask API可能直接返回成功消息,而不是封装在msg字段中
+        if (response && response.message) {
+          this.message = response.message;
+        } else if (response && response.msg) {
+          this.message = response.msg;
+        } else {
+          this.message = "权重更新成功";
+        }
+        this.isError = false;
+        this.$modal.msgSuccess(this.message);
+      } catch (err) {
+        console.error("更新权重配置失败:", err);
+        this.message = err.message || "更新权重配置失败";
+        this.isError = true;
+        this.$modal.msgError(this.message);
+      }
+    },
+
+    // 重置权重
+    resetWeights() {
+      this.weights = {
+        成本: 0.4,
+        交付: 0.4,
+        账期: 0.2,
+      };
+      this.$modal.msgInfo("权重已重置为默认值");
+    },
+
+    // 权重变更事件
+    onWeightChange() {
+      this.message = "";
+      this.isError = false;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+
+  h2 {
+    font-size: 24px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 8px;
+
+    i {
+      margin-right: 8px;
+      color: #409eff;
+    }
+  }
+
+  .page-desc {
+    color: #909399;
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+.box-card {
+  margin-bottom: 20px;
+}
+
+.weight-summary {
+  margin: 20px 0;
+  padding: 15px;
+  background: #f8f9fa;
+  border-radius: 4px;
+  border-left: 4px solid #409eff;
+
+  p {
+    margin: 0 0 5px 0;
+    font-size: 14px;
+    color: #606266;
+  }
+}
+
+.warning {
+  color: #e6a23c;
+  font-weight: 600;
+}
+
+.weight-display {
+  .weight-info {
+    margin-bottom: 20px;
+    color: #666;
+    font-size: 14px;
+  }
+}
+
+.info-box {
+  background: #f8f9fa;
+  padding: 20px;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  transition: all 0.3s;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  }
+
+  .info-label {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 10px;
+    font-weight: 600;
+  }
+
+  .info-value {
+    font-size: 24px;
+    font-weight: 700;
+    margin-bottom: 12px;
+    color: #303133;
+  }
+
+  .info-detail {
+    font-size: 13px;
+    color: #909399;
+    line-height: 1.8;
+  }
+}
+
+::v-deep .el-card__header {
+  font-weight: bold;
+}
+</style>

+ 1 - 1
vue.config.js

@@ -34,7 +34,7 @@ module.exports = {
   devServer: {
     host: '0.0.0.0',
     port: port,
-    open: true,
+    open: false,
     proxy: {
       // detail: https://cli.vuejs.org/config/#devserver-proxy
       [process.env.VUE_APP_PYTHON_API]: {