MrShi
2026-05-19 ce06ca62a0dd65d4a8fb57126948449c804ad77e
small-program/shop/pages/revenue-analysis/revenue-analysis.vue
@@ -7,17 +7,41 @@
               :key="item.value"
               class="quick-tab"
               :class="{ active: currentRange === item.value }"
               @tap="currentRange = item.value"
               @click="currentRange = item.value"
            >
               {{ item.label }}
            </view>
         </view>
         <view class="date-bar">
            <text class="date-placeholder">开始时间</text>
            <text class="date-separator">-</text>
            <text class="date-placeholder">结束时间</text>
         <view v-if="currentRange === 'custom'" class="date-bar">
               <view class="date-item" @click="showStartDatePicker = true">
                  <text :class="startDate ? 'date-text' : 'date-placeholder'">{{ formatPickerDate(startDate) || '开始时间' }}</text>
         </view>
               <text class="date-separator">-</text>
               <view class="date-item" @click="showEndDatePicker = true">
                  <text :class="endDate ? 'date-text' : 'date-placeholder'">{{ formatPickerDate(endDate) || '结束时间' }}</text>
               </view>
            </view>
            <!-- 开始日期选择器 -->
            <u-datetime-picker
               :show="showStartDatePicker"
               mode="date"
               v-model="startDate"
               @confirm="onStartDateConfirm"
               @cancel="showStartDatePicker = false"
               placeholder="选择开始日期"
            ></u-datetime-picker>
            <!-- 结束日期选择器 -->
            <u-datetime-picker
               :show="showEndDatePicker"
               mode="date"
               v-model="endDate"
               @confirm="onEndDateConfirm"
               @cancel="showEndDatePicker = false"
               placeholder="选择结束日期"
            ></u-datetime-picker>
      </view>
      <view class="section-card metrics-card">
@@ -47,23 +71,11 @@
         <view class="chart-content">
            <view class="chart-area">
               <view
                  class="echart-host"
                  :id="chartDomId"
                  :prop="chartOptionText"
                  :change:prop="chartRenderer.renderChart"
               ></view>
               <view class="chart-fallback"></view>
            </view>
            <view class="legend-list">
               <view v-for="item in luggageDistribution" :key="item.name" class="legend-item">
                  <view class="legend-left">
                     <view class="legend-dot" :style="{ background: item.color }"></view>
                     <text class="legend-name">{{ item.name }}</text>
                  </view>
                  <text class="legend-value">{{ item.value }} | {{ item.percent }}%</text>
               </view>
               <qiun-data-charts
                  type="ring"
                  :opts="opts"
                  :chartData="chartData"
                  @getIndex="onRingChartClick" />
            </view>
         </view>
      </view>
@@ -74,8 +86,12 @@
   export default {
      data() {
         return {
            currentRange: 'custom',
            chartDomId: 'luggage-distribution-chart',
            currentRange: 'today',
            startDate: '',
            endDate: '',
            showStartDatePicker: false,
            showEndDatePicker: false,
            minEndDate: '',
            quickTabs: [
               { label: '今日', value: 'today' },
               { label: '近7日', value: '7days' },
@@ -84,81 +100,306 @@
               { label: '自定义', value: 'custom' }
            ],
            metrics: [
               { label: '寄存订单(个)', value: '125', icon: '/shop/static/icon/yingshou_ic_jicun@2x.png' },
               { label: '寄送订单(个)', value: '30', icon: '/shop/static/icon/yingshou_ic_jisong@2x.png' },
               { label: '总订单(个)', value: '155', icon: '/shop/static/icon/yingshou_ic_zongdingdan@2x.png' },
               { label: '总完成订单(个)', value: '143', icon: '/shop/static/icon/yingshou_ic_zongwancheng@2x.png' },
               { label: '总营收(元)', value: '300,000.00', icon: '/shop/static/icon/yingshou_ic_zongyingshou@2x.png' },
               { label: '分店分成(元)', value: '10,000.32', icon: '/shop/static/icon/yingshou_ic_fendian@2x.png' },
               { label: '退款订单(个)', value: '10', icon: '/shop/static/icon/yingshou_ic_tuikuan@2x.png' },
               { label: '责任扣款(元)', value: '300.00', icon: '/shop/static/icon/yingshou_ic_koukuan@2x.png' }
               { label: '寄存订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_jicun@2x.png' },
               { label: '寄送订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_jisong@2x.png' },
               { label: '总订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_zongdingdan@2x.png' },
               { label: '总完成订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_zongwancheng@2x.png' },
               { label: '总营收(元)', value: '0.00', icon: '/shop/static/icon/yingshou_ic_zongyingshou@2x.png' },
               { label: '分店分成(元)', value: '0.00', icon: '/shop/static/icon/yingshou_ic_fendian@2x.png' },
               { label: '退款订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_tuikuan@2x.png' },
               { label: '责任扣款(元)', value: '0.00', icon: '/shop/static/icon/yingshou_ic_koukuan@2x.png' }
            ],
            luggageDistribution: [
               { name: '大号行李箱', value: 35, percent: 35, color: '#3B82F6' },
               { name: '特大号行李箱', value: 20, percent: 20, color: '#64D7C7' },
               { name: '中号行李箱', value: 18, percent: 18, color: '#FFD15C' },
               { name: '小号行李箱', value: 12, percent: 12, color: '#FF8A47' },
               { name: '大号背包', value: 10, percent: 10, color: '#F54786' },
               { name: '小号背包', value: 5, percent: 5, color: '#EE6CB2' }
            ]
         };
            luggageDistribution: [],
            selectedLuggageText: '',
            chartData: {},
            opts: {
               rotate: false,
               rotateLock: false,
               padding: [5,5,5,5],
               dataLabel: false,
               dataLine: false,
               enableScroll: false,
               legend: {
                  show: true,
                  position: "right",
                  lineHeight: 25
      },
      computed: {
         chartOptionText() {
            return JSON.stringify({
               color: this.luggageDistribution.map((item) => item.color),
               title: {
                  show: false,
                  name: ''
               },
               subtitle: {
                  show: false,
                  name: ''
               },
               extra: {
                  ring: {
                     ringWidth: 20,
                     activeOpacity: 0.5,
                     activeRadius: 10,
                     offsetAngle: 0,
                     labelWidth: 15,
                     border: true,
                     borderWidth: 3,
                     borderColor: "#FFFFFF",
                     linearType: "custom"
                  }
               }
            }
         }
      },
      onLoad() {
            this.initDateRange();
            this.getDriverKpiData();
         },
         watch: {
            currentRange(newVal) {
               if (newVal !== 'custom') {
                  this.initDateRange();
                  this.getDriverKpiData();
               }
            },
            startDate(newVal) {
               if (newVal) {
                  // 结束日期最小为开始日期
                  this.minEndDate = new Date(newVal).getTime();
               } else {
                  this.minEndDate = '';
               }
            }
         },
         methods: {
            formatDate(date) {
               return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
            },
            formatPickerDate(value) {
               if (!value) {
                  return '';
               }
               if (typeof value === 'string') {
                  if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
                     return value;
                  }
                  if (/^\d+$/.test(value)) {
                     const timestampDate = new Date(Number(value));
                     return Number.isNaN(timestampDate.getTime()) ? '' : this.formatDate(timestampDate);
                  }
                  return value;
               }
               const date = new Date(value);
               return Number.isNaN(date.getTime()) ? '' : this.formatDate(date);
            },
            isStartDateAfterEndDate(startDate, endDate) {
               if (!startDate || !endDate) {
                  return false;
               }
               return new Date(startDate).getTime() > new Date(endDate).getTime();
            },
            initDateRange() {
               const now = new Date();
               const today = this.formatDate(now);
               switch (this.currentRange) {
                  case 'today':
                     this.startDate = today;
                     this.endDate = today;
                     break;
                  case '7days': {
                     const start = new Date();
                     start.setDate(start.getDate() - 6);
                     this.startDate = this.formatDate(start);
                     this.endDate = today;
                     break;
                  }
                  case '30days': {
                     const start = new Date();
                     start.setDate(start.getDate() - 29);
                     this.startDate = this.formatDate(start);
                     this.endDate = today;
                     break;
                  }
                  case 'halfYear': {
                     const start = new Date();
                     start.setMonth(start.getMonth() - 6);
                     this.startDate = this.formatDate(start);
                     this.endDate = today;
                     break;
                  }
                  default:
                     break;
               }
            },
            async getDriverKpiData() {
               if (this.currentRange === 'custom' && (!this.startDate || !this.endDate)) {
                  uni.showToast({ title: '请选择日期范围', icon: 'none' });
                  return;
               }
               uni.showLoading({ title: '加载中...', mask: true });
               try {
                  const [kpiRes, luggageRes] = await Promise.all([
                     this.$u.api.driverKpi({
                        startDate: this.startDate,
                        endDate: this.endDate
                     }),
                     this.$u.api.shopLuggageType({
                        startDate: this.startDate,
                        endDate: this.endDate
                     })
                  ]);
                  if (kpiRes.code === 200) {
                     this.processDriverKpiData(kpiRes.data);
                  }
                  if (luggageRes.code === 200) {
                     this.processLuggageTypeData(luggageRes.data);
                  }
               } catch (err) {
                  console.error('获取数据失败:', err);
                  uni.showToast({ title: '获取数据失败', icon: 'none' });
               } finally {
                  uni.hideLoading();
               }
            },
            processDriverKpiData(data) {
               console.log('KPI数据:', data);
               const formatAmount = (cents) => {
                  if (typeof cents !== 'number') return '0.00';
                  return (cents / 100).toFixed(2);
               };
               this.metrics = [
                  { label: '寄存订单(个)', value: data.localOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_jicun@2x.png' },
                  { label: '寄送订单(个)', value: data.remoteOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_jisong@2x.png' },
                  { label: '总订单(个)', value: data.totalOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_zongdingdan@2x.png' },
                  { label: '总完成订单(个)', value: data.finishedOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_zongwancheng@2x.png' },
                  { label: '总营收(元)', value: formatAmount(data.totalRevenue), icon: '/shop/static/icon/yingshou_ic_zongyingshou@2x.png' },
                  { label: '分店分成(元)', value: formatAmount(data.shopFeeTotal), icon: '/shop/static/icon/yingshou_ic_fendian@2x.png' },
                  { label: '退款订单(个)', value: data.refundOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_tuikuan@2x.png' },
                  { label: '责任扣款(元)', value: formatAmount(data.deductTotal), icon: '/shop/static/icon/yingshou_ic_koukuan@2x.png' }
               ];
            },
            processLuggageTypeData(data) {
               const colorList = ["#3B82F6", "#64D7C7", "#FFD15C", "#FF8A47", "#F54786", "#EE6666", "#91CB74", "#73C0DE", "#3CA272"];
               this.luggageDistribution = data.map((item, index) => ({
                  name: item.luggageName,
                  value: item.orderCount,
                  count: item.luggageCount,
                  percent: item.orderCount > 0 ? Math.round((item.orderCount / data.reduce((sum, curr) => sum + curr.orderCount, 0)) * 100) : 0,
                  color: colorList[index % colorList.length]
               }));
               this.chartData = {
               series: [
                  {
                     name: '行李类型分布',
                     type: 'pie',
                     radius: ['58%', '78%'],
                     center: ['50%', '50%'],
                     avoidLabelOverlap: false,
                     label: { show: false },
                     labelLine: { show: false },
                     itemStyle: {
                        borderColor: '#ffffff',
                        borderWidth: 4
                     },
                     data: this.luggageDistribution.map((item) => ({
                        value: item.value,
                        name: item.name
                        data: this.luggageDistribution.map(item => ({
                           name: item.name,
                           value: item.value
                     }))
                  }
               ]
            });
         }
      }
   };
</script>
<script>
   import echarts from "echarts"
   export default {
      data() {
         return {
            chart: null
         };
               this.selectedLuggageText = '';
               this.opts.title.name = '';
               this.opts.subtitle.name = '';
      },
      methods: {
         renderChart(optionText) {
            if (!optionText) {
            onRingChartClick(e) {
               const currentIndex = e && typeof e.currentIndex === 'number' ? e.currentIndex : -1;
               const currentItem = this.luggageDistribution[currentIndex];
               if (!currentItem) {
               return;
            }
            this.$nextTick(() => {
               const dom = document.getElementById('luggage-distribution-chart');
               if (!dom) {
                  return;
               this.selectedLuggageText = `${currentItem.name}:${currentItem.count || 0}件,${currentItem.value || 0}单`;
               this.opts = {
                  ...this.opts,
                  title: {
                     ...this.opts.title,
                     name: `${currentItem.count || 0}件`,
                     fontSize: 18,
                     color: '#1f2430'
                  },
                  subtitle: {
                     ...this.opts.subtitle,
                     name: currentItem.name,
                     fontSize: 11,
                     color: '#7a828f'
               }
               if (!this.chart) {
                  this.chart = echarts.init(dom);
               }
               this.chart.setOption(JSON.parse(optionText), true);
               };
               uni.showToast({
                  title: `${currentItem.name} ${currentItem.count || 0}件`,
                  icon: 'none'
            });
            },
            onStartDateConfirm(e) {
               const nextStartDate = this.formatPickerDate(e.value);
               if (this.isStartDateAfterEndDate(nextStartDate, this.endDate)) {
                  this.startDate = '';
                  this.showStartDatePicker = false;
                  uni.showToast({ title: '开始日期不能大于截止日期', icon: 'none' });
                  return;
               }
               this.startDate = nextStartDate;
               this.showStartDatePicker = false;
               this.minEndDate = nextStartDate ? new Date(nextStartDate).getTime() : '';
               if (this.endDate) {
                  this.getDriverKpiData();
               }
            },
            onEndDateConfirm(e) {
               const nextEndDate = this.formatPickerDate(e.value);
               if (this.isStartDateAfterEndDate(this.startDate, nextEndDate)) {
                  this.endDate = '';
                  this.showEndDatePicker = false;
                  uni.showToast({ title: '截止日期不能小于开始日期', icon: 'none' });
                  return;
               }
               this.endDate = nextEndDate;
               this.showEndDatePicker = false;
               if (this.startDate) {
                  this.getDriverKpiData();
               }
            },
            confirmDateRange() {
               if (!this.startDate || !this.endDate) {
                  uni.showToast({ title: '请选择完整的日期范围', icon: 'none' });
                  return;
               }
               const start = new Date(this.startDate);
               const end = new Date(this.endDate);
               if (start > end) {
                  uni.showToast({ title: '开始日期不能大于截止日期', icon: 'none' });
                  return;
               }
               const oneYear = 365 * 24 * 60 * 60 * 1000;
               if (end - start > oneYear) {
                  uni.showToast({ title: '日期区间不能超过一年', icon: 'none' });
                  return;
               }
               this.showDatePicker = false;
               this.getDriverKpiData();
         }
      }
   };
@@ -198,6 +439,67 @@
      font-weight: 500;
      text-align: center;
      color: #50555f;
   }
   /* 日期选择器样式 */
   .date-picker-wrap {
      padding: 24rpx;
      background: #ffffff;
      border-radius: 24rpx 24rpx 0 0;
   }
   .date-picker-title {
      font-size: 32rpx;
      font-weight: 500;
      color: #333333;
      text-align: center;
      margin-bottom: 32rpx;
   }
   .date-picker-content {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 32rpx;
      gap: 24rpx;
   }
   .date-picker-separator {
      font-size: 28rpx;
      color: #999999;
      padding: 0 16rpx;
   }
   .date-picker-footer {
      display: flex;
      gap: 16rpx;
      padding-top: 24rpx;
      border-top: 1rpx solid #eeeeee;
   }
   .date-picker-btn {
      flex: 1;
      height: 72rpx;
      line-height: 72rpx;
      text-align: center;
      border-radius: 36rpx;
      font-size: 28rpx;
      transition: all 0.3s;
   }
   .cancel-btn {
      background: #f5f7fb;
      color: #666666;
   }
   .confirm-btn {
      background: linear-gradient(135deg, #ff8b14 0%, #ff4d0a 100%);
      color: #ffffff;
   }
   .date-text {
      font-size: 28rpx;
      color: #333333;
   }
   .quick-tab.active {
@@ -264,9 +566,9 @@
      flex-shrink: 0;
      width: 68rpx;
      height: 68rpx;
      border-radius: 14rpx;
      background: #ffffff;
      box-shadow: 0 6rpx 18rpx rgba(20, 42, 74, 0.08);
      // border-radius: 14rpx;
      // background: #ffffff;
      // box-shadow: 0 6rpx 18rpx rgba(20, 42, 74, 0.08);
      box-sizing: border-box;
   }
@@ -309,71 +611,20 @@
   .chart-area {
      position: relative;
      flex-shrink: 0;
      width: 270rpx;
      height: 270rpx;
      width: 100%;
      height: 350rpx;
   }
   .echart-host,
   .chart-fallback {
   .chart-detail {
      margin-top: 20rpx;
      font-size: 28rpx;
      line-height: 40rpx;
      color: #5f6775;
      text-align: center;
   }
   .echart-host {
      width: 100%;
      height: 100%;
   }
   .chart-fallback {
      position: absolute;
      left: 0;
      top: 0;
      border-radius: 50%;
      background: conic-gradient(#3b82f6 0 35%, #64d7c7 35% 55%, #ffd15c 55% 73%, #ff8a47 73% 85%, #f54786 85% 95%, #ee6cb2 95% 100%);
      -webkit-mask: radial-gradient(circle, transparent 0 42%, #000 43%);
      mask: radial-gradient(circle, transparent 0 42%, #000 43%);
      opacity: 0.2;
      pointer-events: none;
   }
   .legend-list {
      flex: 1;
      min-width: 0;
   }
   .legend-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12rpx;
   }
   .legend-item + .legend-item {
      margin-top: 18rpx;
   }
   .legend-left {
      display: flex;
      align-items: center;
      gap: 14rpx;
      min-width: 0;
   }
   .legend-dot {
      flex-shrink: 0;
      width: 20rpx;
      height: 20rpx;
      border-radius: 50%;
   }
   .legend-name,
   .legend-value {
      font-size: 30rpx;
      line-height: 42rpx;
   }
   .legend-name {
      color: #6f7683;
   }
   .legend-value {
      flex-shrink: 0;
      font-weight: 600;
      color: #2f333d;
   }
</style>