index.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <template>
  2. <doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" />
  3. <div class="flex flex-col">
  4. <el-row :gutter="16" class="summary">
  5. <el-col v-loading="loading" :sm="6" :xs="12">
  6. <SummaryCard
  7. :value="summary?.userCount || 0"
  8. icon="fa-solid:users"
  9. icon-bg-color="text-blue-500"
  10. icon-color="bg-blue-100"
  11. title="累计会员数"
  12. />
  13. </el-col>
  14. <el-col v-loading="loading" :sm="6" :xs="12">
  15. <SummaryCard
  16. :value="summary?.rechargeUserCount || 0"
  17. icon="fa-solid:user"
  18. icon-bg-color="text-purple-500"
  19. icon-color="bg-purple-100"
  20. title="累计充值人数"
  21. />
  22. </el-col>
  23. <el-col v-loading="loading" :sm="6" :xs="12">
  24. <SummaryCard
  25. :decimals="2"
  26. :value="fenToYuan(summary?.rechargePrice || 0)"
  27. icon="fa-solid:money-check-alt"
  28. icon-bg-color="text-yellow-500"
  29. icon-color="bg-yellow-100"
  30. prefix="¥"
  31. title="累计充值金额"
  32. />
  33. </el-col>
  34. <el-col v-loading="loading" :sm="6" :xs="12">
  35. <SummaryCard
  36. :decimals="2"
  37. :value="fenToYuan(summary?.expensePrice || 0)"
  38. icon="fa-solid:yen-sign"
  39. icon-bg-color="text-green-500"
  40. icon-color="bg-green-100"
  41. prefix="¥"
  42. title="累计消费金额"
  43. />
  44. </el-col>
  45. </el-row>
  46. <el-row :gutter="16" class="mb-4">
  47. <el-col :md="18" :sm="24">
  48. <!-- 会员概览 -->
  49. <MemberFunnelCard />
  50. </el-col>
  51. <el-col :md="6" :sm="24">
  52. <!-- 会员终端 -->
  53. <MemberTerminalCard />
  54. </el-col>
  55. </el-row>
  56. <el-row :gutter="16">
  57. <el-col :md="18" :sm="24">
  58. <el-card shadow="never">
  59. <template #header>
  60. <CardTitle title="会员地域分布" />
  61. </template>
  62. <el-row v-loading="loading">
  63. <el-col :span="10">
  64. <Echart :height="300" :options="areaChartOptions" />
  65. </el-col>
  66. <el-col :span="14">
  67. <el-table :data="areaStatisticsList" :height="300">
  68. <el-table-column
  69. :sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
  70. align="center"
  71. label="省份"
  72. min-width="80"
  73. prop="areaName"
  74. show-overflow-tooltip
  75. sortable
  76. />
  77. <el-table-column
  78. align="center"
  79. label="会员数量"
  80. min-width="105"
  81. prop="userCount"
  82. sortable
  83. />
  84. <el-table-column
  85. align="center"
  86. label="订单创建数量"
  87. min-width="135"
  88. prop="orderCreateUserCount"
  89. sortable
  90. />
  91. <el-table-column
  92. align="center"
  93. label="订单支付数量"
  94. min-width="135"
  95. prop="orderPayUserCount"
  96. sortable
  97. />
  98. <el-table-column
  99. :formatter="fenToYuanFormat"
  100. align="center"
  101. label="订单支付金额"
  102. min-width="135"
  103. prop="orderPayPrice"
  104. sortable
  105. />
  106. </el-table>
  107. </el-col>
  108. </el-row>
  109. </el-card>
  110. </el-col>
  111. <el-col :md="6" :sm="24">
  112. <el-card v-loading="loading" shadow="never">
  113. <template #header>
  114. <CardTitle title="会员性别比例" />
  115. </template>
  116. <Echart :height="300" :options="sexChartOptions" />
  117. </el-card>
  118. </el-col>
  119. </el-row>
  120. </div>
  121. </template>
  122. <script lang="ts" setup>
  123. import * as MemberStatisticsApi from '@/api/mall/statistics/member'
  124. import {
  125. MemberAreaStatisticsRespVO,
  126. MemberSexStatisticsRespVO,
  127. MemberSummaryRespVO,
  128. MemberTerminalStatisticsRespVO
  129. } from '@/api/mall/statistics/member'
  130. import SummaryCard from '@/components/SummaryCard/index.vue'
  131. import { EChartsOption } from 'echarts'
  132. import china from '@/assets/map/json/china.json'
  133. import { areaReplace, fenToYuan } from '@/utils'
  134. import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
  135. import echarts from '@/plugins/echarts'
  136. import { fenToYuanFormat } from '@/utils/formatter'
  137. import MemberFunnelCard from './components/MemberFunnelCard.vue'
  138. import MemberTerminalCard from './components/MemberTerminalCard.vue'
  139. import { CardTitle } from '@/components/Card'
  140. /** 会员统计 */
  141. defineOptions({ name: 'MemberStatistics' })
  142. const loading = ref(true) // 加载中
  143. const summary = ref<MemberSummaryRespVO>() // 会员统计数据
  144. const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 省份会员统计
  145. // 注册地图
  146. echarts?.registerMap('china', china as any)
  147. /** 会员终端统计图配置 */
  148. const terminalChartOptions = reactive<EChartsOption>({
  149. tooltip: {
  150. trigger: 'item',
  151. confine: true,
  152. formatter: '{a} <br/>{b} : {c} ({d}%)'
  153. },
  154. legend: {
  155. orient: 'vertical',
  156. left: 'right'
  157. },
  158. roseType: 'area',
  159. series: [
  160. {
  161. name: '会员终端',
  162. type: 'pie',
  163. label: {
  164. show: false
  165. },
  166. labelLine: {
  167. show: false
  168. },
  169. data: []
  170. }
  171. ]
  172. }) as EChartsOption
  173. /** 会员性别统计图配置 */
  174. const sexChartOptions = reactive<EChartsOption>({
  175. tooltip: {
  176. trigger: 'item',
  177. confine: true,
  178. formatter: '{a} <br/>{b} : {c} ({d}%)'
  179. },
  180. legend: {
  181. orient: 'vertical',
  182. left: 'right'
  183. },
  184. roseType: 'area',
  185. series: [
  186. {
  187. name: '会员性别',
  188. type: 'pie',
  189. label: {
  190. show: false
  191. },
  192. labelLine: {
  193. show: false
  194. },
  195. data: []
  196. }
  197. ]
  198. }) as EChartsOption
  199. const areaChartOptions = reactive<EChartsOption>({
  200. tooltip: {
  201. trigger: 'item',
  202. formatter: (params: any) => {
  203. return `${params?.data?.areaName || params?.name}<br/>
  204. 会员数量:${params?.data?.userCount || 0}<br/>
  205. 订单创建数量:${params?.data?.orderCreateUserCount || 0}<br/>
  206. 订单支付数量:${params?.data?.orderPayUserCount || 0}<br/>
  207. 订单支付金额:${fenToYuan(params?.data?.orderPayPrice || 0)}`
  208. }
  209. },
  210. visualMap: {
  211. text: ['高', '低'],
  212. realtime: false,
  213. calculable: true,
  214. top: 'middle',
  215. inRange: {
  216. color: ['#fff', '#3b82f6']
  217. }
  218. },
  219. series: [
  220. {
  221. name: '会员地域分布',
  222. type: 'map',
  223. map: 'china',
  224. roam: false,
  225. selectedMode: false,
  226. data: []
  227. }
  228. ]
  229. }) as EChartsOption
  230. /** 查询会员统计 */
  231. const getMemberSummary = async () => {
  232. summary.value = await MemberStatisticsApi.getMemberSummary()
  233. }
  234. /** 按照省份,查询会员统计列表 */
  235. const getMemberAreaStatisticsList = async () => {
  236. const list = await MemberStatisticsApi.getMemberAreaStatisticsList()
  237. areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
  238. return {
  239. ...item,
  240. areaName: areaReplace(item.areaName)
  241. }
  242. })
  243. let min = 0
  244. let max = 0
  245. areaChartOptions.series![0].data = areaStatisticsList.value.map((item) => {
  246. min = Math.min(min, item.orderPayUserCount || 0)
  247. max = Math.max(max, item.orderPayUserCount || 0)
  248. return { ...item, name: item.areaName, value: item.orderPayUserCount || 0 }
  249. })
  250. areaChartOptions.visualMap!['min'] = min
  251. areaChartOptions.visualMap!['max'] = max
  252. }
  253. /** 按照性别,查询会员统计列表 */
  254. const getMemberSexStatisticsList = async () => {
  255. const list = await MemberStatisticsApi.getMemberSexStatisticsList()
  256. const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
  257. dictDataList.push({ label: '未知', value: null } as any)
  258. sexChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
  259. const userCount = list.find(
  260. (item: MemberSexStatisticsRespVO) => item.sex === dictData.value
  261. )?.userCount
  262. return {
  263. name: dictData.label,
  264. value: userCount || 0
  265. }
  266. })
  267. }
  268. /** 按照终端,查询会员统计列表 */
  269. const getMemberTerminalStatisticsList = async () => {
  270. const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
  271. const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
  272. dictDataList.push({ label: '未知', value: null } as any)
  273. terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
  274. const userCount = list.find(
  275. (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
  276. )?.userCount
  277. return {
  278. name: dictData.label,
  279. value: userCount || 0
  280. }
  281. })
  282. }
  283. /** 初始化 **/
  284. onMounted(async () => {
  285. loading.value = true
  286. await Promise.all([
  287. getMemberSummary(),
  288. getMemberTerminalStatisticsList(),
  289. getMemberAreaStatisticsList(),
  290. getMemberSexStatisticsList()
  291. ])
  292. loading.value = false
  293. })
  294. </script>
  295. <style lang="scss" scoped>
  296. .summary {
  297. .el-col {
  298. margin-bottom: 1rem;
  299. }
  300. }
  301. </style>