index.vue 9 KB
<template>
  <doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" />

  <div class="flex flex-col">
    <el-row :gutter="16" class="summary">
      <el-col v-loading="loading" :sm="6" :xs="12">
        <SummaryCard
          :value="summary?.userCount || 0"
          icon="fa-solid:users"
          icon-bg-color="text-blue-500"
          icon-color="bg-blue-100"
          title="累计会员数"
        />
      </el-col>
      <el-col v-loading="loading" :sm="6" :xs="12">
        <SummaryCard
          :value="summary?.rechargeUserCount || 0"
          icon="fa-solid:user"
          icon-bg-color="text-purple-500"
          icon-color="bg-purple-100"
          title="累计充值人数"
        />
      </el-col>
      <el-col v-loading="loading" :sm="6" :xs="12">
        <SummaryCard
          :decimals="2"
          :value="fenToYuan(summary?.rechargePrice || 0)"
          icon="fa-solid:money-check-alt"
          icon-bg-color="text-yellow-500"
          icon-color="bg-yellow-100"
          prefix="¥"
          title="累计充值金额"
        />
      </el-col>
      <el-col v-loading="loading" :sm="6" :xs="12">
        <SummaryCard
          :decimals="2"
          :value="fenToYuan(summary?.expensePrice || 0)"
          icon="fa-solid:yen-sign"
          icon-bg-color="text-green-500"
          icon-color="bg-green-100"
          prefix="¥"
          title="累计消费金额"
        />
      </el-col>
    </el-row>
    <el-row :gutter="16" class="mb-4">
      <el-col :md="18" :sm="24">
        <!-- 会员概览 -->
        <MemberFunnelCard />
      </el-col>
      <el-col :md="6" :sm="24">
        <!-- 会员终端 -->
        <MemberTerminalCard />
      </el-col>
    </el-row>
    <el-row :gutter="16">
      <el-col :md="18" :sm="24">
        <el-card shadow="never">
          <template #header>
            <CardTitle title="会员地域分布" />
          </template>
          <el-row v-loading="loading">
            <el-col :span="10">
              <Echart :height="300" :options="areaChartOptions" />
            </el-col>
            <el-col :span="14">
              <el-table :data="areaStatisticsList" :height="300">
                <el-table-column
                  :sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
                  align="center"
                  label="省份"
                  min-width="80"
                  prop="areaName"
                  show-overflow-tooltip
                  sortable
                />
                <el-table-column
                  align="center"
                  label="会员数量"
                  min-width="105"
                  prop="userCount"
                  sortable
                />
                <el-table-column
                  align="center"
                  label="订单创建数量"
                  min-width="135"
                  prop="orderCreateUserCount"
                  sortable
                />
                <el-table-column
                  align="center"
                  label="订单支付数量"
                  min-width="135"
                  prop="orderPayUserCount"
                  sortable
                />
                <el-table-column
                  :formatter="fenToYuanFormat"
                  align="center"
                  label="订单支付金额"
                  min-width="135"
                  prop="orderPayPrice"
                  sortable
                />
              </el-table>
            </el-col>
          </el-row>
        </el-card>
      </el-col>
      <el-col :md="6" :sm="24">
        <el-card v-loading="loading" shadow="never">
          <template #header>
            <CardTitle title="会员性别比例" />
          </template>
          <Echart :height="300" :options="sexChartOptions" />
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script lang="ts" setup>
import * as MemberStatisticsApi from '@/api/mall/statistics/member'
import {
  MemberAreaStatisticsRespVO,
  MemberSexStatisticsRespVO,
  MemberSummaryRespVO,
  MemberTerminalStatisticsRespVO
} from '@/api/mall/statistics/member'
import SummaryCard from '@/components/SummaryCard/index.vue'
import { EChartsOption } from 'echarts'
import china from '@/assets/map/json/china.json'
import { areaReplace, fenToYuan } from '@/utils'
import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
import echarts from '@/plugins/echarts'
import { fenToYuanFormat } from '@/utils/formatter'
import MemberFunnelCard from './components/MemberFunnelCard.vue'
import MemberTerminalCard from './components/MemberTerminalCard.vue'
import { CardTitle } from '@/components/Card'

/** 会员统计 */
defineOptions({ name: 'MemberStatistics' })

const loading = ref(true) // 加载中
const summary = ref<MemberSummaryRespVO>() // 会员统计数据
const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 省份会员统计

// 注册地图
echarts?.registerMap('china', china as any)

/** 会员终端统计图配置 */
const terminalChartOptions = reactive<EChartsOption>({
  tooltip: {
    trigger: 'item',
    confine: true,
    formatter: '{a} <br/>{b} : {c} ({d}%)'
  },
  legend: {
    orient: 'vertical',
    left: 'right'
  },
  roseType: 'area',
  series: [
    {
      name: '会员终端',
      type: 'pie',
      label: {
        show: false
      },
      labelLine: {
        show: false
      },
      data: []
    }
  ]
}) as EChartsOption

/** 会员性别统计图配置 */
const sexChartOptions = reactive<EChartsOption>({
  tooltip: {
    trigger: 'item',
    confine: true,
    formatter: '{a} <br/>{b} : {c} ({d}%)'
  },
  legend: {
    orient: 'vertical',
    left: 'right'
  },
  roseType: 'area',
  series: [
    {
      name: '会员性别',
      type: 'pie',
      label: {
        show: false
      },
      labelLine: {
        show: false
      },
      data: []
    }
  ]
}) as EChartsOption

const areaChartOptions = reactive<EChartsOption>({
  tooltip: {
    trigger: 'item',
    formatter: (params: any) => {
      return `${params?.data?.areaName || params?.name}<br/>
会员数量:${params?.data?.userCount || 0}<br/>
订单创建数量:${params?.data?.orderCreateUserCount || 0}<br/>
订单支付数量:${params?.data?.orderPayUserCount || 0}<br/>
订单支付金额:${fenToYuan(params?.data?.orderPayPrice || 0)}`
    }
  },
  visualMap: {
    text: ['高', '低'],
    realtime: false,
    calculable: true,
    top: 'middle',
    inRange: {
      color: ['#fff', '#3b82f6']
    }
  },
  series: [
    {
      name: '会员地域分布',
      type: 'map',
      map: 'china',
      roam: false,
      selectedMode: false,
      data: []
    }
  ]
}) as EChartsOption

/** 查询会员统计 */
const getMemberSummary = async () => {
  summary.value = await MemberStatisticsApi.getMemberSummary()
}

/** 按照省份,查询会员统计列表 */
const getMemberAreaStatisticsList = async () => {
  const list = await MemberStatisticsApi.getMemberAreaStatisticsList()
  areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
    return {
      ...item,
      areaName: areaReplace(item.areaName)
    }
  })
  let min = 0
  let max = 0
  areaChartOptions.series![0].data = areaStatisticsList.value.map((item) => {
    min = Math.min(min, item.orderPayUserCount || 0)
    max = Math.max(max, item.orderPayUserCount || 0)
    return { ...item, name: item.areaName, value: item.orderPayUserCount || 0 }
  })
  areaChartOptions.visualMap!['min'] = min
  areaChartOptions.visualMap!['max'] = max
}

/** 按照性别,查询会员统计列表 */
const getMemberSexStatisticsList = async () => {
  const list = await MemberStatisticsApi.getMemberSexStatisticsList()
  const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
  dictDataList.push({ label: '未知', value: null } as any)
  sexChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
    const userCount = list.find(
      (item: MemberSexStatisticsRespVO) => item.sex === dictData.value
    )?.userCount
    return {
      name: dictData.label,
      value: userCount || 0
    }
  })
}

/** 按照终端,查询会员统计列表 */
const getMemberTerminalStatisticsList = async () => {
  const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
  const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
  dictDataList.push({ label: '未知', value: null } as any)
  terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
    const userCount = list.find(
      (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
    )?.userCount
    return {
      name: dictData.label,
      value: userCount || 0
    }
  })
}

/** 初始化 **/
onMounted(async () => {
  loading.value = true
  await Promise.all([
    getMemberSummary(),
    getMemberTerminalStatisticsList(),
    getMemberAreaStatisticsList(),
    getMemberSexStatisticsList()
  ])
  loading.value = false
})
</script>
<style lang="scss" scoped>
.summary {
  .el-col {
    margin-bottom: 1rem;
  }
}
</style>