net-analysis.vue 9.27 KB
<template>
  <div class="net-wrapper">
    <div class="net-stats">
      <div class="stat-card net-billing">
        <div class="stat-icon-circle">
          <i class="el-icon-wallet"></i>
        </div>
        <div class="stat-content">
          <div class="stat-title">开单总额</div>
          <div class="stat-value">¥{{ formatMoney(extra.actualAmount || 0) }}</div>
        </div>
      </div>
      <div class="stat-card net-refund">
        <div class="stat-icon-circle">
          <i class="el-icon-warning-outline"></i>
        </div>
        <div class="stat-content">
          <div class="stat-title">退卡总额</div>
          <div class="stat-value">¥{{ formatMoney(extra.refundAmount || 0) }}</div>
        </div>
      </div>
      <div class="stat-card net-amount">
        <div class="stat-icon-circle">
          <i class="el-icon-trophy"></i>
        </div>
        <div class="stat-content">
          <div class="stat-title">完成业绩(净额)</div>
          <div class="stat-value">¥{{ formatMoney((extra.actualAmount || 0) - (extra.refundAmount || 0)) }}</div>
        </div>
      </div>
    </div>
    <div class="net-tabs">
      <el-radio-group v-model="innerType" size="small" @change="resetAndFetch">
        <el-radio-button label="billing">开单明细</el-radio-button>
        <el-radio-button label="refund">退卡明细</el-radio-button>
      </el-radio-group>
    </div>
    <div class="net-table-card">
      <el-table v-loading="loading" :data="displayList" size="small" height="300" border stripe>
        <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label"
          :width="col.width" :min-width="col.minWidth">
          <template slot-scope="scope">
            <span v-if="col.type === 'money'">¥{{ formatMoney(scope.row[col.prop]) }}</span>
            <span v-else>{{ scope.row[col.prop] || '—' }}</span>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-bar">
        <el-pagination layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" :total="pagination.total"
          :current-page="pagination.pageIndex" :page-size="pagination.pageSize" @size-change="handleSizeChange"
          @current-change="handleCurrentChange" />
      </div>
    </div>
  </div>
</template>

<script>
import request from '@/utils/request'
import dayjs from 'dayjs'
import { kpiDrillMixin } from './mixins'

export default {
  name: 'NetAnalysis',
  mixins: [kpiDrillMixin],
  props: {
    filters: {
      type: Object,
      default: () => ({ startTime: null, endTime: null, storeIds: [], month: null })
    },
    extra: { type: Object, default: () => ({}) },
    storeOptions: { type: Array, default: () => [] }
  },
  data() {
    return {
      loading: false,
      list: [],
      displayList: [],
      pagination: { pageIndex: 1, pageSize: 10, total: 0 },
      innerType: 'billing'
    }
  },
  computed: {
    columns() {
      const base = {
        billing: [
          { prop: 'billingTime', label: '开单时间', minWidth: 120 },
          { prop: 'storeName', label: '门店', minWidth: 120 },
          { prop: 'memberName', label: '会员', minWidth: 120 },
          { prop: 'itemName', label: '品项', minWidth: 140 },
          { prop: 'itemType', label: '品项类型', minWidth: 120 },
          { prop: 'projectNumber', label: '项目数', width: 90 },
          { prop: 'actualPrice', label: '实付金额', width: 110, type: 'money' },
          { prop: 'sourceType', label: '来源类型', minWidth: 110 },
          { prop: 'performanceType', label: '业绩类型', minWidth: 110 },
          { prop: 'beautyType', label: '科美类型', minWidth: 110 }
        ],
        refund: [
          { prop: 'tksj', label: '退卡时间', minWidth: 120 },
          { prop: 'mdmc', label: '门店', minWidth: 120 },
          { prop: 'hymc', label: '会员', minWidth: 120 },
          { prop: 'gklx', label: '顾客类型', minWidth: 100 },
          { prop: 'tkje', label: '退款金额', width: 110, type: 'money' },
          { prop: 'tkyy', label: '退款原因', minWidth: 140 }
        ]
      }
      return base[this.innerType] || base.billing
    }
  },
  watch: {
    filters: {
      deep: true,
      handler() {
        this.resetAndFetch()
      }
    },
    innerType() {
      this.resetAndFetch()
    }
  },
  mounted() {
    this.fetchData()
  },
  methods: {
    resetAndFetch() {
      this.pagination = { ...this.pagination, pageIndex: 1 }
      this.fetchData()
    },
    handleSizeChange(size) {
      this.pagination.pageSize = size
      this.pagination.pageIndex = 1
      this.fetchData()
    },
    handleCurrentChange(page) {
      this.pagination.pageIndex = page
      this.fetchData()
    },
    async fetchData() {
      this.loading = true
      try {
        const range = this.buildDateRange()
        const storeId = this.getStoreId()
        let url = ''
        let data = { currentPage: this.pagination.pageIndex, pageSize: this.pagination.pageSize }

        if (this.innerType === 'billing') {
          url = '/api/Extend/LqKdKdjlb/billing-item-detail-list'
          if (range) {
            data.startTime = `${range.start} 00:00:00`
            data.endTime = `${range.end} 23:59:59`
          }
          if (storeId) data.StoreId = storeId
        } else if (this.innerType === 'refund') {
          url = '/api/Extend/LqHytkHytk'
          if (range) {
            data.tksj = `${range.startTs},${range.endTs}`
          }
          if (storeId) data.md = storeId
        }

        const res = await request({ url, method: 'GET', data })
        await this.handleResponse(res, this.innerType, range)
      } catch (error) {
        console.error('Net analysis load error:', error)
        this.$message.error(error.message || '加载数据失败')
        this.list = []
        this.displayList = []
      } finally {
        this.loading = false
      }
    },
    async handleResponse(res, activeType, range) {
      let list = []
      let pagination = this.pagination

      if (res && res.data) {
        if (res.data.list && res.data.pagination) {
          list = res.data.list
          pagination = {
            pageIndex: res.data.pagination.pageIndex || res.data.pagination.pageNumber || 1,
            pageSize: res.data.pagination.pageSize || this.pagination.pageSize,
            total: res.data.pagination.total || res.data.pagination.totalCount || 0
          }
        } else if (Array.isArray(res.data.list)) {
          list = res.data.list
        } else if (Array.isArray(res.data)) {
          list = res.data
        }
      }

      if (activeType === 'billing') {
        list = list.map(i => ({
          billingTime: i.billingTime || i.yjsj || i.CreateTime ? dayjs(i.billingTime || i.yjsj || i.CreateTime).format('YYYY-MM-DD HH:mm') : '',
          storeName: i.storeName || i.djmdmc || i.store,
          memberName: i.memberName || i.kdhyc || i.MemberName,
          itemName: i.itemName || i.ItemName,
          itemType: i.itemType || i.ItemType,
          projectNumber: i.projectNumber || i.ProjectNumber,
          actualPrice: i.actualPrice || i.ActualPrice || 0,
          sourceType: i.sourceType || i.SourceType || '',
          performanceType: i.performanceType || i.PerformanceType || '',
          beautyType: i.beautyType || i.BeautyType || ''
        }))
      } else if (activeType === 'refund') {
        // res.data.list 已经是退卡主表字段,直接使用
      }

      this.list = list
      this.displayList = list
      this.pagination = pagination
    }
  }
}
</script>

<style lang="scss" scoped>
.net-wrapper {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 16px;

  .net-stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
    margin-bottom: 8px;
  }

  .stat-card {
    background: #fff;
    border: 1px solid #ebeef5;
    border-radius: 10px;
    padding: 16px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
    display: flex;
    align-items: center;
    gap: 16px;

    .stat-icon-circle {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      flex-shrink: 0;
      font-size: 24px;
    }

    .stat-content {
      flex: 1;
      min-width: 0;
    }

    .stat-title {
      font-size: 13px;
      color: #606266;
      margin-bottom: 8px;
    }

    .stat-value {
      font-size: 20px;
      font-weight: 700;
      color: #303133;
    }

    &.net-billing {
      border-left: 4px solid #409EFF;
      .stat-icon-circle {
        background: #409EFF;
      }
    }

    &.net-refund {
      border-left: 4px solid #F56C6C;
      .stat-icon-circle {
        background: #F56C6C;
      }
    }

    &.net-amount {
      border-left: 4px solid #67C23A;
      .stat-icon-circle {
        background: #67C23A;
      }
    }
  }

  .net-tabs {
    display: flex;
    justify-content: center;
    padding: 8px 0;
  }

  .net-table-card {
    background: #fff;
    border: 1px solid #ebeef5;
    border-radius: 10px;
    padding: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
    display: flex;
    flex-direction: column;
    min-height: 450px;
    .pagination-bar {
      flex-shrink: 0;
      margin-top: 12px;
    }
  }
}

.pagination-bar {
  display: flex;
  justify-content: flex-end;
  padding: 10px 0 4px 0;
}
</style>