index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <template>
  2. <div class="flex flex-col">
  3. <el-row :gutter="16" class="summary">
  4. <el-col :sm="6" :xs="12" v-loading="loading">
  5. <TradeTrendValue
  6. title="累计会员数"
  7. icon="fa-solid:users"
  8. icon-color="bg-blue-100"
  9. icon-bg-color="text-blue-500"
  10. :value="summary?.userCount || 0"
  11. />
  12. </el-col>
  13. <el-col :sm="6" :xs="12" v-loading="loading">
  14. <TradeTrendValue
  15. title="累计充值人数"
  16. icon="fa-solid:user"
  17. icon-color="bg-purple-100"
  18. icon-bg-color="text-purple-500"
  19. :value="summary?.rechargeUserCount || 0"
  20. />
  21. </el-col>
  22. <el-col :sm="6" :xs="12" v-loading="loading">
  23. <TradeTrendValue
  24. title="累计充值金额"
  25. icon="fa-solid:money-check-alt"
  26. icon-color="bg-yellow-100"
  27. icon-bg-color="text-yellow-500"
  28. prefix="¥"
  29. :decimals="2"
  30. :value="fenToYuan(summary?.rechargePrice || 0)"
  31. />
  32. </el-col>
  33. <el-col :sm="6" :xs="12" v-loading="loading">
  34. <TradeTrendValue
  35. title="累计消费金额"
  36. icon="fa-solid:yen-sign"
  37. icon-color="bg-green-100"
  38. icon-bg-color="text-green-500"
  39. prefix="¥"
  40. :decimals="2"
  41. :value="fenToYuan(summary?.expensePrice || 0)"
  42. />
  43. </el-col>
  44. </el-row>
  45. <el-row :gutter="16" class="mb-4">
  46. <el-col :md="18" :sm="24">
  47. <el-card shadow="never">
  48. <template #header>
  49. <div class="flex flex-row items-center justify-between">
  50. <span>会员概览</span>
  51. <!-- 查询条件 -->
  52. <div class="my--2 flex flex-row items-center gap-2">
  53. <el-radio-group v-model="shortcutDays" @change="handleDateTypeChange">
  54. <el-radio-button :label="1">昨天</el-radio-button>
  55. <el-radio-button :label="7">最近7天</el-radio-button>
  56. <el-radio-button :label="30">最近30天</el-radio-button>
  57. </el-radio-group>
  58. <el-date-picker
  59. v-model="queryParams.times"
  60. value-format="YYYY-MM-DD HH:mm:ss"
  61. type="daterange"
  62. start-placeholder="开始日期"
  63. end-placeholder="结束日期"
  64. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  65. :shortcuts="shortcuts"
  66. class="!w-240px"
  67. @change="getMemberAnalyse"
  68. />
  69. </div>
  70. </div>
  71. </template>
  72. <div class="min-w-225 py-1.75" v-loading="analyseLoading">
  73. <div class="relative h-24 flex">
  74. <div class="h-full w-75% bg-blue-50 <lg:w-35% <xl:w-55%">
  75. <div class="ml-15 h-full flex flex-col justify-center">
  76. <div class="font-bold">
  77. 注册用户数量:{{ analyseData?.comparison?.value?.userCount || 0 }}
  78. </div>
  79. <div class="mt-2 text-3.5">
  80. 环比增长率:{{
  81. calculateRelativeRate(
  82. analyseData?.comparison?.value?.userCount,
  83. analyseData?.comparison?.reference?.userCount
  84. )
  85. }}%
  86. </div>
  87. </div>
  88. </div>
  89. <div
  90. class="trapezoid1 ml--38.5 mt-1.5 h-full w-77 flex flex-col items-center justify-center bg-blue-5 text-3.5 text-white"
  91. >
  92. <span class="text-6 font-bold">{{ analyseData?.visitorCount || 0 }}</span>
  93. <span>访客</span>
  94. </div>
  95. </div>
  96. <div class="relative h-24 flex">
  97. <div class="h-full w-75% flex bg-cyan-50 <lg:w-35% <xl:w-55%">
  98. <div class="ml-15 h-full flex flex-col justify-center">
  99. <div class="font-bold">
  100. 活跃用户数量:{{ analyseData?.comparison?.value?.activeUserCount || 0 }}
  101. </div>
  102. <div class="mt-2 text-3.5">
  103. 环比增长率:{{
  104. calculateRelativeRate(
  105. analyseData?.comparison?.value?.activeUserCount,
  106. analyseData?.comparison?.reference?.activeUserCount
  107. )
  108. }}%
  109. </div>
  110. </div>
  111. </div>
  112. <div
  113. class="trapezoid2 ml--28 mt-1.7 h-25 w-56 flex flex-col items-center justify-center bg-cyan-5 text-3.5 text-white"
  114. >
  115. <span class="text-6 font-bold">{{ analyseData?.orderUserCount || 0 }}</span>
  116. <span>下单</span>
  117. </div>
  118. </div>
  119. <div class="relative h-24 flex">
  120. <div class="w-75% flex bg-slate-50 <lg:w-35% <xl:w-55%">
  121. <div class="ml-15 h-full flex flex-row gap-x-16">
  122. <div class="flex flex-col justify-center">
  123. <div class="font-bold">
  124. 充值用户数量:{{ analyseData?.comparison?.value?.rechargeUserCount || 0 }}
  125. </div>
  126. <div class="mt-2 text-3.5">
  127. 环比增长率:{{
  128. calculateRelativeRate(
  129. analyseData?.comparison?.value?.rechargeUserCount,
  130. analyseData?.comparison?.reference?.rechargeUserCount
  131. )
  132. }}%
  133. </div>
  134. </div>
  135. <div class="flex flex-col justify-center">
  136. <div class="font-bold">客单价:{{ fenToYuan(analyseData?.atv || 0) }}</div>
  137. </div>
  138. </div>
  139. </div>
  140. <div
  141. class="trapezoid3 ml--18 mt-3.25 h-23 w-36 flex flex-col items-center justify-center bg-slate-5 text-3.5 text-white"
  142. >
  143. <span class="text-6 font-bold">{{ analyseData?.payUserCount || 0 }}</span>
  144. <span>成交用户</span>
  145. </div>
  146. </div>
  147. </div>
  148. </el-card>
  149. </el-col>
  150. <el-col :md="6" :sm="24">
  151. <el-card shadow="never" header="会员终端" v-loading="loading">
  152. <Echart :height="300" :options="terminalChartOptions" />
  153. </el-card>
  154. </el-col>
  155. </el-row>
  156. <el-row :gutter="16">
  157. <el-col :md="18" :sm="24">
  158. <el-card shadow="never" header="会员地域分布">
  159. <el-row v-loading="loading">
  160. <el-col :span="10">
  161. <Echart :height="300" :options="areaChartOptions" />
  162. </el-col>
  163. <el-col :span="14">
  164. <el-table :data="areaStatisticsList" :height="300">
  165. <el-table-column
  166. label="省份"
  167. prop="areaName"
  168. align="center"
  169. min-width="80"
  170. show-overflow-tooltip
  171. sortable
  172. :sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
  173. />
  174. <el-table-column
  175. label="会员数量"
  176. prop="userCount"
  177. align="center"
  178. min-width="105"
  179. sortable
  180. />
  181. <el-table-column
  182. label="订单创建数量"
  183. prop="orderCreateCount"
  184. align="center"
  185. min-width="135"
  186. sortable
  187. />
  188. <el-table-column
  189. label="订单支付数量"
  190. prop="orderPayCount"
  191. align="center"
  192. min-width="135"
  193. sortable
  194. />
  195. <el-table-column
  196. label="订单支付金额"
  197. prop="orderPayPrice"
  198. align="center"
  199. min-width="135"
  200. sortable
  201. :formatter="fenToYuanFormat"
  202. />
  203. </el-table>
  204. </el-col>
  205. </el-row>
  206. </el-card>
  207. </el-col>
  208. <el-col :md="6" :sm="24">
  209. <el-card shadow="never" header="会员性别比例" v-loading="loading">
  210. <Echart :height="300" :options="sexChartOptions" />
  211. </el-card>
  212. </el-col>
  213. </el-row>
  214. </div>
  215. </template>
  216. <script lang="ts" setup>
  217. import * as TradeMemberApi from '@/api/mall/statistics/member'
  218. import TradeTrendValue from '../trade/components/TradeTrendValue.vue'
  219. import { EChartsOption } from 'echarts'
  220. import china from '@/assets/map/json/china.json'
  221. import dayjs from 'dayjs'
  222. import { fenToYuan } from '@/utils'
  223. import * as DateUtil from '@/utils/formatTime'
  224. import {
  225. MemberAnalyseRespVO,
  226. MemberAreaStatisticsRespVO,
  227. MemberSexStatisticsRespVO,
  228. MemberAnalyseReqVO,
  229. MemberSummaryRespVO,
  230. MemberTerminalStatisticsRespVO
  231. } from '@/api/mall/statistics/member'
  232. import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
  233. import echarts from '@/plugins/echarts'
  234. import { fenToYuanFormat } from '@/utils/formatter'
  235. /** 会员统计 */
  236. defineOptions({ name: 'MemberStatistics' })
  237. const loading = ref(true) // 加载中
  238. const analyseLoading = ref(true) // 会员概览加载中
  239. const queryParams = reactive<MemberAnalyseReqVO>({ times: ['', ''] }) // 会员概览查询参数
  240. const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
  241. const summary = ref<MemberSummaryRespVO>() // 会员统计数据
  242. const analyseData = ref<MemberAnalyseRespVO>() // 会员分析数据
  243. const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 省份会员统计
  244. // 注册地图
  245. echarts?.registerMap('china', china!)
  246. /** 日期快捷选择 */
  247. const shortcuts = [
  248. {
  249. text: '昨天',
  250. value: () => DateUtil.getDayRange(new Date(), -1)
  251. },
  252. {
  253. text: '最近7天',
  254. value: () => DateUtil.getLast7Days()
  255. },
  256. {
  257. text: '本月',
  258. value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')]
  259. },
  260. {
  261. text: '最近30天',
  262. value: () => DateUtil.getLast30Days()
  263. },
  264. {
  265. text: '最近1年',
  266. value: () => DateUtil.getLast1Year()
  267. }
  268. ]
  269. /** 会员终端统计图配置 */
  270. const terminalChartOptions = reactive<EChartsOption>({
  271. tooltip: {
  272. trigger: 'item',
  273. confine: true,
  274. formatter: '{a} <br/>{b} : {c} ({d}%)'
  275. },
  276. legend: {
  277. orient: 'vertical',
  278. left: 'right'
  279. },
  280. roseType: 'area',
  281. series: [
  282. {
  283. name: '会员终端',
  284. type: 'pie',
  285. label: {
  286. show: false
  287. },
  288. labelLine: {
  289. show: false
  290. },
  291. data: []
  292. }
  293. ]
  294. }) as EChartsOption
  295. /** 会员性别统计图配置 */
  296. const sexChartOptions = reactive<EChartsOption>({
  297. tooltip: {
  298. trigger: 'item',
  299. confine: true,
  300. formatter: '{a} <br/>{b} : {c} ({d}%)'
  301. },
  302. legend: {
  303. orient: 'vertical',
  304. left: 'right'
  305. },
  306. roseType: 'area',
  307. series: [
  308. {
  309. name: '会员性别',
  310. type: 'pie',
  311. label: {
  312. show: false
  313. },
  314. labelLine: {
  315. show: false
  316. },
  317. data: []
  318. }
  319. ]
  320. }) as EChartsOption
  321. const areaChartOptions = reactive<EChartsOption>({
  322. tooltip: {
  323. trigger: 'item',
  324. formatter: (params: any) => {
  325. return `${params?.data?.areaName || params?.name}<br/>
  326. 会员数量:${params?.data?.userCount || 0}<br/>
  327. 订单创建数量:${params?.data?.orderCreateCount || 0}<br/>
  328. 订单支付数量:${params?.data?.orderPayCount || 0}<br/>
  329. 订单支付金额:${fenToYuan(params?.data?.orderPayPrice || 0)}`
  330. }
  331. },
  332. visualMap: {
  333. text: ['高', '低'],
  334. realtime: false,
  335. calculable: true,
  336. top: 'middle',
  337. inRange: {
  338. color: ['#fff', '#3b82f6']
  339. }
  340. },
  341. series: [
  342. {
  343. name: '会员地域分布',
  344. type: 'map',
  345. map: 'china',
  346. roam: false,
  347. selectedMode: false,
  348. data: []
  349. }
  350. ]
  351. }) as EChartsOption
  352. /** 计算环比 */
  353. const calculateRelativeRate = (value?: number, reference?: number) => {
  354. // 防止除0
  355. if (!reference) return 0
  356. return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
  357. }
  358. /** 设置时间范围 */
  359. function setTimes() {
  360. const beginDate = dayjs().subtract(shortcutDays.value, 'd')
  361. const yesterday = dayjs().subtract(1, 'd')
  362. queryParams.times = DateUtil.getDateRange(beginDate, yesterday)
  363. }
  364. /** 处理会员概览查询(日期单选按钮组选择后) */
  365. const handleDateTypeChange = async () => {
  366. // 设置时间范围
  367. setTimes()
  368. // 查询数据
  369. await getMemberAnalyse()
  370. }
  371. /** 查询会员统计 */
  372. const getMemberSummary = async () => {
  373. summary.value = await TradeMemberApi.getMemberSummary()
  374. }
  375. /** 按照省份,查询会员统计列表 */
  376. const getMemberAreaStatisticsList = async () => {
  377. const list = await TradeMemberApi.getMemberAreaStatisticsList()
  378. areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
  379. return {
  380. ...item,
  381. areaName: item.areaName
  382. .replace('维吾尔自治区', '')
  383. .replace('壮族自治区', '')
  384. .replace('回族自治区', '')
  385. .replace('自治区', '')
  386. .replace('省', '')
  387. }
  388. })
  389. let min = 0
  390. let max = 0
  391. areaChartOptions.series[0].data = areaStatisticsList.value.map((item) => {
  392. min = Math.min(min, item.orderPayCount)
  393. max = Math.max(max, item.orderPayCount)
  394. return { ...item, name: item.areaName, value: item.orderPayCount || 0 }
  395. })
  396. areaChartOptions.visualMap.min = min
  397. areaChartOptions.visualMap.max = max
  398. }
  399. /** 按照性别,查询会员统计列表 */
  400. const getMemberSexStatisticsList = async () => {
  401. const list = await TradeMemberApi.getMemberSexStatisticsList()
  402. const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
  403. sexChartOptions.series[0].data = dictDataList.map((dictData: DictDataType) => {
  404. const userCount = list.find((item: MemberSexStatisticsRespVO) => item.sex === dictData.value)
  405. ?.userCount
  406. return {
  407. name: dictData.label,
  408. value: userCount || 0
  409. }
  410. })
  411. }
  412. /** 按照终端,查询会员统计列表 */
  413. const getMemberTerminalStatisticsList = async () => {
  414. const list = await TradeMemberApi.getMemberTerminalStatisticsList()
  415. const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
  416. terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
  417. const userCount = list.find(
  418. (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
  419. )?.userCount
  420. return {
  421. name: dictData.label,
  422. value: userCount || 0
  423. }
  424. })
  425. }
  426. /** 查询会员概览数据列表 */
  427. const getMemberAnalyse = async () => {
  428. analyseLoading.value = true
  429. const times = queryParams.times
  430. // 开始与截止在同一天的, 环比出不来, 需要延长一天
  431. if (DateUtil.isSameDay(times[0], times[1])) {
  432. // 前天
  433. times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
  434. }
  435. // 查询数据
  436. analyseData.value = await TradeMemberApi.getMemberAnalyse({ times })
  437. analyseLoading.value = false
  438. }
  439. /** 初始化 **/
  440. onMounted(async () => {
  441. loading.value = true
  442. await Promise.all([
  443. getMemberSummary(),
  444. getMemberTerminalStatisticsList(),
  445. getMemberAreaStatisticsList(),
  446. getMemberSexStatisticsList(),
  447. handleDateTypeChange()
  448. ])
  449. loading.value = false
  450. })
  451. </script>
  452. <style lang="scss" scoped>
  453. .summary {
  454. .el-col {
  455. margin-bottom: 1rem;
  456. }
  457. }
  458. .trapezoid1 {
  459. transform: perspective(5em) rotateX(-11deg);
  460. }
  461. .trapezoid2 {
  462. transform: perspective(7em) rotateX(-20deg);
  463. }
  464. .trapezoid3 {
  465. transform: perspective(3em) rotateX(-13deg);
  466. }
  467. </style>