Gogs 2 mesiacov pred
rodič
commit
0e25ffee72

+ 6 - 6
src/views/order/channel/index.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <div class="page-container">
     <!-- 1. 顶部标题 -->
     <header class="page-header">
@@ -6,7 +6,7 @@
       <p class="subtitle">分析商品在不同销售渠道的覆盖广度</p>
     </header>
 
-    <!-- 2. 可视化图表  -->
+    <!-- 2. 可视化图表 -->
     <div class="chart-card">
       <h3 class="chart-title">商品渠道覆盖 Top 20 趋势</h3>
       <div ref="channelChartRef" style="width: 100%; height: 500px;"></div>
@@ -85,11 +85,11 @@ export default {
         },
         yAxis: {
           type: 'value',
-          name: '?????'
+          name: '覆盖平台数'
         },
         series: [
           {
-            name: '????',
+            name: '覆盖平台数',
             type: 'line',
             smooth: true,
             data: top20Data.map(item => item.platformCount),
@@ -114,7 +114,7 @@ export default {
           this.initLineChart();
         }
       } catch (error) {
-        console.error('??????:', error);
+        console.error('获取商品渠道数据失败:', error);
       }
     },
     nextPage() {
@@ -199,4 +199,4 @@ export default {
   cursor: not-allowed;
   opacity: 0.5;
 }
-</style>
+</style>

+ 12 - 11
src/views/order/efficiency/index.vue

@@ -62,7 +62,7 @@ export default {
       try {
         const response = await axios.get('/api/shop/import/department-efficiency');
         if (!response.data || !response.data.success) {
-          throw new Error(response.data.message || '??????????');
+          throw new Error(response.data.message || 'REPLACED__');
         }
         const rawData = response.data.data;
         const chartData = Object.entries(rawData).map(([name, value]) => ({
@@ -75,13 +75,13 @@ export default {
         if (chartEl) {
           this.barChartInstance = echarts.init(chartEl);
           const option = {
-            title: { text: '??????????', left: 'center' },
-            tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: '{b}<br/>?????: {c} ?' },
+            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: '?????(?)' },
+            yAxis: { type: 'value', name: '平均销售额(元)' },
             series: [{
-              name: '?????',
+              name: '平均销售额',
               type: 'bar',
               data: chartData.map(item => item.value),
               barWidth: '40%',
@@ -97,7 +97,7 @@ export default {
           this.barChartInstance.setOption(option);
         }
       } catch (err) {
-        this.barChart.error = err.message || '????';
+        this.barChart.error = err.message || '部门效率数据加载失败';
       } finally {
         this.barChart.loading = false;
       }
@@ -107,7 +107,7 @@ export default {
       try {
         const response = await axios.get('/api/shop/import/channel-diversity');
         if (!response.data || !response.data.success) {
-          throw new Error(response.data.message || '???????????');
+          throw new Error(response.data.message || '渠道多样性数据获取失败');
         }
         const rawData = response.data.data;
         const chartData = Object.entries(rawData).map(([name, value]) => ({ name, value }));
@@ -117,11 +117,11 @@ export default {
         if (chartEl) {
           this.pieChartInstance = echarts.init(chartEl);
           const option = {
-            title: { text: '?????????', left: 'center' },
+            title: { text: '渠道商品多样性', left: 'center' },
             tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' },
             legend: { orient: 'vertical', left: 'left', top: '10%' },
             series: [{
-              name: '????',
+              name: '渠道',
               type: 'pie',
               radius: [20, 140],
               center: ['50%', '60%'],
@@ -133,7 +133,7 @@ export default {
           this.pieChartInstance.setOption(option);
         }
       } catch (err) {
-        this.pieChart.error = err.message || '????';
+        this.pieChart.error = err.message || '渠道多样性数据加载失败';
       } finally {
         this.pieChart.loading = false;
       }
@@ -200,4 +200,5 @@ export default {
   color: #999;
   margin-top: 8px;
 }
-</style>
+</style>
+// __MARKER__

+ 131 - 3
src/views/order/ordervalue/FunnelChart/index.vue

@@ -1,129 +1,257 @@
 <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);
+
+        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 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: '???',
+
+            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>

+ 126 - 6
src/views/order/ordervalue/KpiCard/index.vue

@@ -1,121 +1,241 @@
 <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: '??'
+
+      default: '📈'
+
     }
+
   },
+
   computed: {
+
     trendColorMap() {
+
       return {
+
         green: '#38D6A4',
+
         red: '#FF6347'
+
       };
+
     },
+
     iconColorMap() {
+
       return {
-        '???? (GMV)': '#6699FF',
-        'P80 ?????': '#F7D742',
-        'Top 5 ?????': '#FF9966',
-        '??????': '#4ECDC4'
+
+        '总交易额 (GMV)': '#6699FF',
+
+        'P80 订单贡献比': '#F7D742',
+
+        'Top 5 商品贡献比': '#FF9966',
+
+        '平均支付响应': '#4ECDC4'
+
       };
+
     },
+
     iconColor() {
+
       return this.iconColorMap[this.title] || '#6699FF';
+
     },
+
     trendIcon() {
+
       if (!this.trend) return '';
-      return this.trend.includes('-') ? '?' : '?';
+
+      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>

+ 139 - 3
src/views/order/ordervalue/LeakageCard/index.vue

@@ -1,137 +1,273 @@
 <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',
+
+        totalRefundAmount: '¥0.00',
+
         leakageRatePercent: 0,
-        totalSuccessAmount: '?0.00'
+
+        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);
+
+        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>

+ 174 - 2
src/views/order/ordervalue/Top5PieChart/index.vue

@@ -1,173 +1,345 @@
 <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);
+
+        console.error('获取Top5商品数据失败:', 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 ????',
+
+            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>

+ 452 - 11
src/views/order/ordervalue/index.vue

@@ -1,441 +1,882 @@
 <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',
+
+        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)} ??`;
+
+        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);
+
+        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);
+
+        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('???????????????');
+
+        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)} ??`;
+
+        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 = '????';
+
+        console.error('获取KPI数据失败:', error);
+
+        this.kpiData.gmv = '?0';
+
+        this.kpiData.p80Contribution = '0%';
+
+        this.kpiData.top5Contribution = '0%';
+
+        this.kpiData.averagePaymentTime = '00:00 秒';
+
       }
+
     }
+
   }
+
 };
+
 </script>
 
 
+
+
+
 <style scoped>
+
 .header-controls {
+
   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>
+

+ 12 - 12
src/views/order/shopvalue/index.vue

@@ -135,7 +135,7 @@ export default {
         },
         series: [
           {
-            name: '???',
+            name: '销售额',
             type: 'pie',
             radius: ['60%', '80%'],
             avoidLabelOverlap: false,
@@ -180,7 +180,7 @@ export default {
           this.initDonutChart(chartData);
         }
       } catch (error) {
-        console.error('??Top 5??????:', error);
+        console.error('获取Top 5商品贡献失败:', error);
       }
     },
     initDualIndicatorBarChart(chartEl, data, categoryKey, seriesConfig) {
@@ -246,7 +246,7 @@ export default {
 
       const option = {
         tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
-        legend: { data: ['??', '????'], top: 0 },
+        legend: { data: ['销量', '平均订单金额'], top: 0 },
         grid: { left: '80px', right: '80px', top: '40px', bottom: '60px' },
         xAxis: {
           type: 'category',
@@ -257,7 +257,7 @@ export default {
         yAxis: [
           {
             type: 'value',
-            name: '??',
+            name: '销量',
             nameTextStyle: { color: '#666' },
             axisLabel: {
               color: '#5470c6',
@@ -271,7 +271,7 @@ export default {
           },
           {
             type: 'value',
-            name: '????(?)',
+            name: '平均订单金额(元)',
             nameTextStyle: { color: '#e9c46a' },
             axisLabel: {
               color: '#e9c46a',
@@ -283,7 +283,7 @@ export default {
         ],
         series: [
           {
-            name: '??',
+            name: '销量',
             type: 'bar',
             data: data.map(item => item.totalVolume),
             barMaxWidth: '40px',
@@ -291,7 +291,7 @@ export default {
             yAxisIndex: 0
           },
           {
-            name: '????',
+            name: '平均订单金额',
             type: 'bar',
             data: data.map(item => item.avgOrderValue),
             barMaxWidth: '40px',
@@ -325,10 +325,10 @@ export default {
             sortedData,
             'name',
             [
-              { key: 'totalVolume', name: '??', color: '#5470c6' },
+              { key: 'totalVolume', name: '销量', color: '#5470c6' },
               {
                 key: 'totalAmount',
-                name: '????(?)',
+                name: '平均订单金额(元)',
                 color: '#91cc75',
                 formatter: value => `?${(value >= 10000 ? (value / 10000) + 'w' : value)}`
               }
@@ -343,10 +343,10 @@ export default {
             sortedData,
             'name',
             [
-              { key: 'totalVolume', name: '??', color: '#5470c6' },
+              { key: 'totalVolume', name: '销量', color: '#5470c6' },
               {
                 key: 'totalAmount',
-                name: '????(?)',
+                name: '平均订单金额(元)',
                 color: '#e9c46a',
                 formatter: value => `?${(value >= 10000 ? (value / 10000) + 'w' : value)}`
               }
@@ -382,7 +382,7 @@ export default {
 
         this.fetchTopProductData();
       } catch (error) {
-        console.error('??????:', error);
+        console.error('获取店铺价值数据失败:', error);
       }
     }
   }

+ 4 - 0
vue.config.js

@@ -37,6 +37,10 @@ module.exports = {
     open: false,
     proxy: {
       // detail: https://cli.vuejs.org/config/#devserver-proxy
+      '^/api': {
+        target: baseUrl,
+        changeOrigin: true
+      },
       [process.env.VUE_APP_PYTHON_API]: {
         target: pyUrl,
         changeOrigin: true,