From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/views/mall/statistics/member/index.vue |  313 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 313 insertions(+), 0 deletions(-)

diff --git a/src/views/mall/statistics/member/index.vue b/src/views/mall/statistics/member/index.vue
new file mode 100644
index 0000000..0e1bbaf
--- /dev/null
+++ b/src/views/mall/statistics/member/index.vue
@@ -0,0 +1,313 @@
+<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>

--
Gitblit v1.8.0