consume-analysis.vue 12.5 KB
<template>
  <div class="billing-wrapper">
    <div class="billing-layout">
      <!-- 左侧:趋势 + 列表 -->
      <div class="billing-left">
        <div class="chart-card trend-card">
          <div class="chart-title">
            <i class="el-icon-date"></i>
            日度消耗趋势
          </div>
          <div ref="consumeTrendChart" class="chart-mini"></div>
        </div>

        <div class="table-card">
          <div class="table-header">
            <div class="table-title">
              <i class="el-icon-document"></i>
              消耗明细清单
            </div>
            <div class="list-filters inline">
              <el-select v-model="listFilter.store" size="mini" placeholder="门店" style="width: 180px;" clearable
                @change="applyListFilter">
                <el-option v-for="s in storeOptions" :key="s.id" :label="s.fullName || s.dm || s.name || s.label"
                  :value="s.id" />
              </el-select>
            </div>
          </div>

          <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-if="col.type === 'healthCoach'">
                  <div v-if="scope.row.lqXhJksyjList && scope.row.lqXhJksyjList.length > 0">
                    <div v-for="(jksyj, index) in scope.row.lqXhJksyjList" :key="index" style="margin-bottom: 2px; font-size: 12px;">
                      <span style="color: #409EFF;">{{ jksyj.jksxm || jksyj.jks || '—' }}</span>
                      <span style="color: #67C23A; margin-left: 4px;">业绩:¥{{ formatMoney(jksyj.jksyj) }}</span>
                      <span style="color: #909399; margin-left: 4px;">手工费:¥{{ formatMoney(jksyj.laborCost) }}</span>
                    </div>
                  </div>
                  <span v-else style="color: #909399;">—</span>
                </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>

      <!-- 右侧:关键指标 -->
      <div class="billing-right" style="min-height: 75vh;">
        <div class="stat-card neon-green compact">
          <div class="stat-icon-circle">
            <i class="el-icon-trophy"></i>
          </div>
          <div class="stat-content">
            <div class="stat-title">消耗金额最高会员</div>
            <div class="stat-body">
              <div class="highlight text-ellipsis-2">
                {{ consumeStats.topMemberAmount.name || '无' }}
                <span class="value-inline">¥{{ formatMoney(consumeStats.topMemberAmount.value) }}</span>
              </div>
            </div>
          </div>
        </div>

        <div class="stat-card neon-orange compact">
          <div class="stat-icon-circle">
            <i class="el-icon-user"></i>
          </div>
          <div class="stat-content">
            <div class="stat-title">消耗次数最多会员</div>
            <div class="stat-body">
              <div class="highlight text-ellipsis-2">
                {{ consumeStats.topMemberTimes.name || '无' }}
                <span class="value-inline">{{ consumeStats.topMemberTimes.count || 0 }} 次</span>
              </div>
            </div>
          </div>
        </div>

        <div class="stat-card neon-blue compact">
          <div class="stat-icon-circle">
            <i class="el-icon-suitcase"></i>
          </div>
          <div class="stat-content">
            <div class="stat-title">科技老师手工费合计</div>
            <div class="stat-body">
              <div class="value-lg">¥{{ formatMoney(consumeStats.techTeacherLaborCostTotal) }}</div>
            </div>
          </div>
        </div>

        <div class="stat-card neon-purple compact">
          <div class="stat-icon-circle">
            <i class="el-icon-user-solid"></i>
          </div>
          <div class="stat-content">
            <div class="stat-title">健康师手工费合计</div>
            <div class="stat-body">
              <div class="value-lg">¥{{ formatMoney(consumeStats.healthCoachLaborCostTotal) }}</div>
            </div>
          </div>
        </div>

        <div class="stat-card neon-red compact">
          <div class="stat-icon-circle">
            <i class="el-icon-coin"></i>
          </div>
          <div class="stat-content">
            <div class="stat-title">单次消耗最大金额</div>
            <div class="stat-body">
              <div class="value-lg">¥{{ formatMoney(consumeStats.maxSingleConsumeAmount) }}</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

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

export default {
  name: 'ConsumeAnalysis',
  mixins: [kpiDrillMixin],
  props: {
    filters: {
      type: Object,
      default: () => ({ startTime: null, endTime: null, storeIds: [], month: null })
    },
    storeOptions: { type: Array, default: () => [] }
  },
  data() {
    return {
      loading: false,
      list: [],
      displayList: [],
      consumeStats: {
        topMemberAmount: { name: '', value: 0 },
        topMemberTimes: { name: '', count: 0 },
        techTeacherLaborCostTotal: 0,
        healthCoachLaborCostTotal: 0,
        maxSingleConsumeAmount: 0
      },
      pagination: { pageIndex: 1, pageSize: 10, total: 0 },
      listFilter: {
        store: ''
      },
      columns: [
        { prop: 'consumeTime', 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: 'totalPrice', label: '消耗金额', width: 110, type: 'money' },
        { prop: 'healthCoachInfo', label: '健康师业绩信息', minWidth: 200, type: 'healthCoach' }
      ]
    }
  },
  watch: {
    filters: {
      deep: true,
      handler() {
        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()
        const url = '/api/Extend/LqXhHyhk/consume-item-detail-list'
        const data = {
          currentPage: this.pagination.pageIndex,
          pageSize: this.pagination.pageSize
        }
        if (range) {
          data.startTime = `${range.start} 00:00:00`
          data.endTime = `${range.end} 23:59:59`
        }
        if (this.listFilter.store) {
          data.StoreId = this.listFilter.store
        } else if (storeId) {
          data.storeId = storeId
        }

        // 获取统计数据
        const month = this.getMonth()
        const statsRes = await request({
          url: '/api/Extend/LqReport/get-consume-drill-statistics',
          method: 'POST',
          data: {
            statisticsMonth: month,
            storeIds: this.filters && this.filters.storeIds ? this.filters.storeIds : []
          }
        })
        this.applyConsumeStatistics(statsRes && statsRes.data)

        // 获取列表数据
        const res = await request({ url, method: 'GET', data })
        await this.handleResponse(res, range)
      } catch (error) {
        console.error('Consume analysis load error:', error)
        this.$message.error(error.message || '加载数据失败')
        this.list = []
        this.displayList = []
      } finally {
        this.loading = false
      }
    },
    async handleResponse(res, 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
          }
        }
      }

      list = list.map(i => ({
        consumeTime: i.consumeTime || i.hksj || i.CreateTime ? dayjs(i.consumeTime || i.hksj || i.CreateTime).format('YYYY-MM-DD HH:mm') : '',
        storeName: i.storeName || i.mdmc,
        memberName: i.memberName || i.hymc,
        itemName: i.itemName || i.pxmc || i.ItemName,
        itemType: i.itemType || i.ItemType,
        projectNumber: i.projectNumber || i.projectNumber || i.ProjectNumber,
        totalPrice: i.totalPrice || i.totalPrice || i.xfje || 0,
        lqXhJksyjList: i.lqXhJksyjList || []
      }))

      this.list = list
      this.displayList = list
      if (res && res.data && res.data.pagination) {
        this.pagination = {
          pageIndex: res.data.pagination.pageIndex || res.data.pagination.pageNumber || this.pagination.pageIndex,
          pageSize: res.data.pagination.pageSize || this.pagination.pageSize,
          total: res.data.pagination.total || res.data.pagination.totalCount || 0
        }
      }
    },
    applyConsumeStatistics(statistics) {
      if (!statistics) {
        this.consumeStats = {
          topMemberAmount: { name: '', value: 0 },
          topMemberTimes: { name: '', count: 0 },
          techTeacherLaborCostTotal: 0,
          healthCoachLaborCostTotal: 0,
          maxSingleConsumeAmount: 0
        }
        this.renderConsumeTrend([])
        return
      }

      const trend = (statistics.DailyTrend || []).map(i => ({
        date: i.Date,
        amount: i.Amount,
        memberCount: i.MemberCount
      }))
      this.renderConsumeTrend(trend)

      const memberStats = statistics.MemberStats || {}
      this.consumeStats = {
        topMemberAmount: {
          name: memberStats.TopAmountMemberName || '—',
          value: memberStats.TopAmountValue || 0
        },
        topMemberTimes: {
          name: memberStats.TopTimesMemberName || '—',
          count: memberStats.TopTimesCount || 0
        },
        techTeacherLaborCostTotal: statistics.TechTeacherLaborCostTotal || 0,
        healthCoachLaborCostTotal: statistics.HealthCoachLaborCostTotal || 0,
        maxSingleConsumeAmount: statistics.MaxSingleConsumeAmount || 0
      }
    },
    renderConsumeTrend(trend) {
      const dom = this.$refs.consumeTrendChart
      if (!dom) return
      const chart = echarts.init(dom)
      const dates = trend.map(i => i.date)
      chart.setOption({
        tooltip: { trigger: 'axis' },
        legend: { data: ['消耗金额', '消耗人数'], textStyle: { color: '#606266' } },
        grid: { left: '6%', right: '4%', top: '15%', bottom: '28%' },
        xAxis: { type: 'category', data: dates, axisLine: { lineStyle: { color: '#dcdfe6' } }, axisLabel: { color: '#606266', rotate: 40 } },
        yAxis: [
          { type: 'value', name: '金额', axisLabel: { color: '#606266' }, splitLine: { lineStyle: { color: '#ebeef5' } } },
          { type: 'value', name: '人数', axisLabel: { color: '#606266' }, splitLine: { show: false } }
        ],
        series: [
          { name: '消耗金额', type: 'bar', data: trend.map(i => i.amount), itemStyle: { color: '#67C23A' }, barWidth: 12 },
          { name: '消耗人数', type: 'line', yAxisIndex: 1, data: trend.map(i => i.memberCount), smooth: true, itemStyle: { color: '#409EFF' } }
        ]
      })
    },
    applyListFilter() {
      this.pagination.pageIndex = 1
      this.fetchData()
    }
  }
}
</script>

<style lang="scss" scoped>
@import './common-styles.scss';
</style>