【新增】前端代码第一次提交
This commit is contained in:
121
src/views/mall/statistics/member/components/MemberFunnelCard.vue
Normal file
121
src/views/mall/statistics/member/components/MemberFunnelCard.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="my--1.5 flex flex-row items-center justify-between">
|
||||
<CardTitle title="会员概览" />
|
||||
<!-- 查询条件 -->
|
||||
<ShortcutDateRangePicker @change="handleTimeRangeChange" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="min-w-225 py-1.75" v-loading="loading">
|
||||
<div class="relative h-24 flex">
|
||||
<div class="h-full w-75% bg-blue-50 <lg:w-35% <xl:w-55%">
|
||||
<div class="ml-15 h-full flex flex-col justify-center">
|
||||
<div class="font-bold">
|
||||
注册用户数量:{{ analyseData?.comparison?.value?.registerUserCount || 0 }}
|
||||
</div>
|
||||
<div class="mt-2 text-3.5">
|
||||
环比增长率:{{
|
||||
calculateRelativeRate(
|
||||
analyseData?.comparison?.value?.registerUserCount,
|
||||
analyseData?.comparison?.reference?.registerUserCount
|
||||
)
|
||||
}}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<span class="text-6 font-bold">{{ analyseData?.visitUserCount || 0 }}</span>
|
||||
<span>访客</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-24 flex">
|
||||
<div class="h-full w-75% flex bg-cyan-50 <lg:w-35% <xl:w-55%">
|
||||
<div class="ml-15 h-full flex flex-col justify-center">
|
||||
<div class="font-bold">
|
||||
活跃用户数量:{{ analyseData?.comparison?.value?.visitUserCount || 0 }}
|
||||
</div>
|
||||
<div class="mt-2 text-3.5">
|
||||
环比增长率:{{
|
||||
calculateRelativeRate(
|
||||
analyseData?.comparison?.value?.visitUserCount,
|
||||
analyseData?.comparison?.reference?.visitUserCount
|
||||
)
|
||||
}}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<span class="text-6 font-bold">{{ analyseData?.orderUserCount || 0 }}</span>
|
||||
<span>下单</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-24 flex">
|
||||
<div class="w-75% flex bg-slate-50 <lg:w-35% <xl:w-55%">
|
||||
<div class="ml-15 h-full flex flex-row gap-x-16">
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="font-bold">
|
||||
充值用户数量:{{ analyseData?.comparison?.value?.rechargeUserCount || 0 }}
|
||||
</div>
|
||||
<div class="mt-2 text-3.5">
|
||||
环比增长率:{{
|
||||
calculateRelativeRate(
|
||||
analyseData?.comparison?.value?.rechargeUserCount,
|
||||
analyseData?.comparison?.reference?.rechargeUserCount
|
||||
)
|
||||
}}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="font-bold">客单价:{{ fenToYuan(analyseData?.atv || 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<span class="text-6 font-bold">{{ analyseData?.payUserCount || 0 }}</span>
|
||||
<span>成交用户</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as MemberStatisticsApi from '@/api/mall/statistics/member'
|
||||
import dayjs from 'dayjs'
|
||||
import { calculateRelativeRate, fenToYuan } from '@/utils'
|
||||
import { MemberAnalyseRespVO } from '@/api/mall/statistics/member'
|
||||
import { CardTitle } from '@/components/Card'
|
||||
|
||||
/** 会员概览卡片 */
|
||||
defineOptions({ name: 'MemberFunnelCard' })
|
||||
|
||||
const loading = ref(true) // 加载中
|
||||
const analyseData = ref<MemberAnalyseRespVO>() // 会员分析数据
|
||||
|
||||
/** 查询会员概览数据列表 */
|
||||
const handleTimeRangeChange = async (times: [dayjs.ConfigType, dayjs.ConfigType]) => {
|
||||
loading.value = true
|
||||
// 查询数据
|
||||
analyseData.value = await MemberStatisticsApi.getMemberAnalyse({ times })
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.trapezoid1 {
|
||||
transform: perspective(5em) rotateX(-11deg);
|
||||
}
|
||||
|
||||
.trapezoid2 {
|
||||
transform: perspective(7em) rotateX(-20deg);
|
||||
}
|
||||
|
||||
.trapezoid3 {
|
||||
transform: perspective(3em) rotateX(-13deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<el-card shadow="never" v-loading="loading">
|
||||
<template #header>
|
||||
<CardTitle title="会员终端" />
|
||||
</template>
|
||||
<Echart :height="300" :options="terminalChartOptions" />
|
||||
</el-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as MemberStatisticsApi from '@/api/mall/statistics/member'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { MemberTerminalStatisticsRespVO } from '@/api/mall/statistics/member'
|
||||
import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
|
||||
import { CardTitle } from '@/components/Card'
|
||||
|
||||
/** 会员终端卡片 */
|
||||
defineOptions({ name: 'MemberTerminalCard' })
|
||||
|
||||
const loading = ref(true) // 加载中
|
||||
|
||||
/** 会员终端统计图配置 */
|
||||
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 getMemberTerminalStatisticsList = async () => {
|
||||
loading.value = true
|
||||
const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
|
||||
const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
|
||||
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
|
||||
}
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getMemberTerminalStatisticsList()
|
||||
})
|
||||
</script>
|
||||
313
src/views/mall/statistics/member/index.vue
Normal file
313
src/views/mall/statistics/member/index.vue
Normal file
@@ -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>
|
||||
108
src/views/mall/statistics/product/components/ProductRank.vue
Normal file
108
src/views/mall/statistics/product/components/ProductRank.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<!-- 标题 -->
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<CardTitle title="商品排行" />
|
||||
<!-- 查询条件 -->
|
||||
<ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="handleDateRangeChange" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 排行列表 -->
|
||||
<el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
|
||||
<el-table-column label="商品 ID" prop="spuId" min-width="70" />
|
||||
<el-table-column label="商品图片" align="center" prop="picUrl" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.picUrl"
|
||||
:preview-src-list="[row.picUrl]"
|
||||
class="h-30px w-30px"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品名称" prop="name" min-width="200" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="浏览量" prop="browseCount" min-width="90" sortable="custom" />
|
||||
<el-table-column label="访客数" prop="browseUserCount" min-width="90" sortable="custom" />
|
||||
<el-table-column label="加购件数" prop="cartCount" min-width="105" sortable="custom" />
|
||||
<el-table-column label="下单件数" prop="orderCount" min-width="105" sortable="custom" />
|
||||
<el-table-column label="支付件数" prop="orderPayCount" min-width="105" sortable="custom" />
|
||||
<el-table-column
|
||||
label="支付金额"
|
||||
prop="orderPayPrice"
|
||||
min-width="105"
|
||||
sortable="custom"
|
||||
:formatter="fenToYuanFormat"
|
||||
/>
|
||||
<el-table-column label="收藏数" prop="favoriteCount" min-width="90" sortable="custom" />
|
||||
<el-table-column
|
||||
label="访客-支付转化率(%)"
|
||||
prop="browseConvertPercent"
|
||||
min-width="180"
|
||||
sortable="custom"
|
||||
:formatter="formatConvertRate"
|
||||
/>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getSpuList"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ProductStatisticsApi, ProductStatisticsVO } from '@/api/mall/statistics/product'
|
||||
import { CardTitle } from '@/components/Card'
|
||||
import { buildSortingField } from '@/utils'
|
||||
import { fenToYuanFormat } from '@/utils/formatter'
|
||||
|
||||
/** 商品排行 */
|
||||
defineOptions({ name: 'ProductRank' })
|
||||
|
||||
// 格式化:访客-支付转化率
|
||||
const formatConvertRate = (row: ProductStatisticsVO) => {
|
||||
return `${row.browseConvertPercent}%`
|
||||
}
|
||||
|
||||
const handleSortChange = (params: any) => {
|
||||
queryParams.sortingFields = [buildSortingField(params)]
|
||||
getSpuList()
|
||||
}
|
||||
|
||||
const handleDateRangeChange = (times: any[]) => {
|
||||
queryParams.times = times as []
|
||||
getSpuList()
|
||||
}
|
||||
|
||||
const shortcutDateRangePicker = ref()
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
times: [],
|
||||
sortingFields: {}
|
||||
})
|
||||
const loading = ref(false) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<ProductStatisticsVO[]>([]) // 列表的数据
|
||||
|
||||
/** 查询商品列表 */
|
||||
const getSpuList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ProductStatisticsApi.getProductStatisticsRankPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
await getSpuList()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
304
src/views/mall/statistics/product/components/ProductSummary.vue
Normal file
304
src/views/mall/statistics/product/components/ProductSummary.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<!-- 标题 -->
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<CardTitle title="商品概况" />
|
||||
<!-- 查询条件 -->
|
||||
<ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getProductTrendData">
|
||||
<el-button
|
||||
class="ml-4"
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['statistics:product:export']"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-1" />导出
|
||||
</el-button>
|
||||
</ShortcutDateRangePicker>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 统计值 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :xl="4" :md="8" :sm="24">
|
||||
<SummaryCard
|
||||
title="商品浏览量"
|
||||
tooltip="在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次"
|
||||
icon="ep:view"
|
||||
icon-color="bg-blue-100"
|
||||
icon-bg-color="text-blue-500"
|
||||
prefix=""
|
||||
:decimals="0"
|
||||
:value="trendSummary?.value?.browseCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.browseCount,
|
||||
trendSummary?.reference?.browseCount
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xl="4" :md="8" :sm="24">
|
||||
<SummaryCard
|
||||
title="商品访客数"
|
||||
tooltip="在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个"
|
||||
icon="ep:user-filled"
|
||||
icon-color="bg-purple-100"
|
||||
icon-bg-color="text-purple-500"
|
||||
prefix=""
|
||||
:decimals="0"
|
||||
:value="trendSummary?.value?.browseUserCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.browseUserCount,
|
||||
trendSummary?.reference?.browseUserCount
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xl="4" :md="8" :sm="24">
|
||||
<SummaryCard
|
||||
title="支付件数"
|
||||
tooltip="在选定条件下,成功付款订单的商品件数之和"
|
||||
icon="fa-solid:money-check-alt"
|
||||
icon-color="bg-yellow-100"
|
||||
icon-bg-color="text-yellow-500"
|
||||
prefix=""
|
||||
:decimals="0"
|
||||
:value="trendSummary?.value?.orderPayCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.orderPayCount,
|
||||
trendSummary?.reference?.orderPayCount
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xl="4" :md="8" :sm="24">
|
||||
<SummaryCard
|
||||
title="支付金额"
|
||||
tooltip="在选定条件下,成功付款订单的商品金额之和"
|
||||
icon="ep:warning-filled"
|
||||
icon-color="bg-green-100"
|
||||
icon-bg-color="text-green-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.orderPayPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.orderPayPrice,
|
||||
trendSummary?.reference?.orderPayPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xl="4" :md="8" :sm="24">
|
||||
<SummaryCard
|
||||
title="退款件数"
|
||||
tooltip="在选定条件下,成功退款的商品件数之和"
|
||||
icon="fa-solid:wallet"
|
||||
icon-color="bg-cyan-100"
|
||||
icon-bg-color="text-cyan-500"
|
||||
prefix=""
|
||||
:decimals="0"
|
||||
:value="trendSummary?.value?.afterSaleCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.afterSaleCount,
|
||||
trendSummary?.reference?.afterSaleCount
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xl="4" :md="8" :sm="24">
|
||||
<SummaryCard
|
||||
title="退款金额"
|
||||
tooltip="在选定条件下,成功退款的商品金额之和"
|
||||
icon="fa-solid:award"
|
||||
icon-color="bg-yellow-100"
|
||||
icon-bg-color="text-yellow-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.afterSaleRefundPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.afterSaleRefundPrice,
|
||||
trendSummary?.reference?.afterSaleRefundPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 折线图 -->
|
||||
<el-skeleton :loading="trendLoading" animated>
|
||||
<Echart :height="500" :options="lineChartOptions" />
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ProductStatisticsApi, ProductStatisticsVO } from '@/api/mall/statistics/product'
|
||||
import SummaryCard from '@/components/SummaryCard/index.vue'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { DataComparisonRespVO } from '@/api/mall/statistics/common'
|
||||
import { calculateRelativeRate, fenToYuan } from '@/utils'
|
||||
import download from '@/utils/download'
|
||||
import { CardTitle } from '@/components/Card'
|
||||
import * as DateUtil from '@/utils/formatTime'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/** 商品概况 */
|
||||
defineOptions({ name: 'ProductSummary' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const trendLoading = ref(true) // 商品状态加载中
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
const trendSummary = ref<DataComparisonRespVO<ProductStatisticsVO>>() // 商品状况统计数据
|
||||
const shortcutDateRangePicker = ref()
|
||||
|
||||
/** 折线图配置 */
|
||||
const lineChartOptions = reactive<EChartsOption>({
|
||||
dataset: {
|
||||
dimensions: ['time', 'browseCount', 'browseUserCount', 'orderPayPrice', 'afterSaleRefundPrice'],
|
||||
source: []
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
top: 80,
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
top: 50
|
||||
},
|
||||
series: [
|
||||
{ name: '商品浏览量', type: 'line', smooth: true, itemStyle: { color: '#B37FEB' } },
|
||||
{ name: '商品访客数', type: 'line', smooth: true, itemStyle: { color: '#FFAB2B' } },
|
||||
{ name: '支付金额', type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#1890FF' } },
|
||||
{ name: '退款金额', type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#00C050' } }
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
// 数据区域缩放
|
||||
dataZoom: {
|
||||
yAxisIndex: false // Y轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '商品状况' } // 保存为图片
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
padding: [5, 10]
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: '#7F8B9C'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#F5F7F9'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: '#7F8B9C'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#F5F7F9'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}) as EChartsOption
|
||||
|
||||
/** 处理商品状况查询 */
|
||||
const getProductTrendData = async () => {
|
||||
trendLoading.value = true
|
||||
// 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
if (DateUtil.isSameDay(times[0], times[1])) {
|
||||
// 前天
|
||||
times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
|
||||
}
|
||||
// 查询数据
|
||||
await Promise.all([getProductTrendSummary(), getProductStatisticsList()])
|
||||
trendLoading.value = false
|
||||
}
|
||||
|
||||
/** 查询商品状况数据统计 */
|
||||
const getProductTrendSummary = async () => {
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
trendSummary.value = await ProductStatisticsApi.getProductStatisticsAnalyse({ times })
|
||||
}
|
||||
|
||||
/** 查询商品状况数据列表 */
|
||||
const getProductStatisticsList = async () => {
|
||||
// 查询数据
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
const list: ProductStatisticsVO[] = await ProductStatisticsApi.getProductStatisticsList({ times })
|
||||
// 处理数据
|
||||
for (let item of list) {
|
||||
item.orderPayPrice = fenToYuan(item.orderPayPrice)
|
||||
item.afterSaleRefundPrice = fenToYuan(item.afterSaleRefundPrice)
|
||||
}
|
||||
// 更新 Echarts 数据
|
||||
if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
|
||||
lineChartOptions.dataset['source'] = list
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
// 导出的二次确认
|
||||
await message.exportConfirm()
|
||||
// 发起导出
|
||||
exportLoading.value = true
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
const data = await ProductStatisticsApi.exportProductStatisticsExcel({ times })
|
||||
download.excel(data, '商品状况.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
16
src/views/mall/statistics/product/index.vue
Normal file
16
src/views/mall/statistics/product/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" />
|
||||
|
||||
<!-- 商品概览 -->
|
||||
<ProductSummary />
|
||||
<!-- 商品排行 -->
|
||||
<ProductRank class="mt-16px" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import ProductSummary from './components/ProductSummary.vue'
|
||||
import ProductRank from './components/ProductRank.vue'
|
||||
|
||||
/** 商品统计 */
|
||||
defineOptions({ name: 'ProductStatistics' })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
|
||||
<div class="flex items-center justify-between text-gray-500">
|
||||
<span>{{ title }}</span>
|
||||
<el-tooltip :content="tooltip" placement="top-start" v-if="tooltip">
|
||||
<Icon icon="ep:warning" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="mb-4 text-3xl">
|
||||
<CountTo :prefix="prefix" :end-val="value" :decimals="decimals" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 text-sm">
|
||||
<span class="text-gray-500">环比</span>
|
||||
<span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'">
|
||||
{{ Math.abs(toNumber(percent)) }}%
|
||||
<Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { toNumber } from 'lodash-es'
|
||||
|
||||
/** 交易统计值组件 */
|
||||
defineOptions({ name: 'TradeStatisticValue' })
|
||||
|
||||
defineProps({
|
||||
tooltip: propTypes.string.def(''),
|
||||
title: propTypes.string.def(''),
|
||||
prefix: propTypes.string.def(''),
|
||||
value: propTypes.number.def(0),
|
||||
decimals: propTypes.number.def(0),
|
||||
percent: propTypes.oneOfType([Number, String]).def(0)
|
||||
})
|
||||
</script>
|
||||
363
src/views/mall/statistics/trade/index.vue
Normal file
363
src/views/mall/statistics/trade/index.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<el-row :gutter="16" class="summary">
|
||||
<el-col :sm="6" :xs="12">
|
||||
<TradeStatisticValue
|
||||
tooltip="昨日订单数量"
|
||||
title="昨日订单数量"
|
||||
:value="summary?.value?.yesterdayOrderCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
summary?.value?.yesterdayOrderCount,
|
||||
summary?.reference?.yesterdayOrderCount
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :sm="6" :xs="12">
|
||||
<TradeStatisticValue
|
||||
tooltip="本月订单数量"
|
||||
title="本月订单数量"
|
||||
:value="summary?.value?.monthOrderCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
summary?.value?.monthOrderCount,
|
||||
summary?.reference?.monthOrderCount
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :sm="6" :xs="12">
|
||||
<TradeStatisticValue
|
||||
tooltip="昨日支付金额"
|
||||
title="昨日支付金额"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(summary?.value?.yesterdayPayPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
summary?.value?.yesterdayPayPrice,
|
||||
summary?.reference?.yesterdayPayPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :sm="6" :xs="12">
|
||||
<TradeStatisticValue
|
||||
tooltip="本月支付金额"
|
||||
title="本月支付金额"
|
||||
prefix="¥"
|
||||
::decimals="2"
|
||||
:value="fenToYuan(summary?.value?.monthPayPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(summary?.value?.monthPayPrice, summary?.reference?.monthPayPrice)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<!-- 标题 -->
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<CardTitle title="交易状况" />
|
||||
<!-- 查询条件 -->
|
||||
<ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getTradeTrendData">
|
||||
<el-button
|
||||
class="ml-4"
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['statistics:trade:export']"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-1" />导出
|
||||
</el-button>
|
||||
</ShortcutDateRangePicker>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 统计值 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :md="6" :sm="12" :xs="24">
|
||||
<SummaryCard
|
||||
title="营业额"
|
||||
tooltip="商品支付金额、充值金额"
|
||||
icon="fa-solid:yen-sign"
|
||||
icon-color="bg-blue-100"
|
||||
icon-bg-color="text-blue-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.turnoverPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.turnoverPrice,
|
||||
trendSummary?.reference?.turnoverPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="6" :sm="12" :xs="24">
|
||||
<SummaryCard
|
||||
title="商品支付金额"
|
||||
tooltip="用户购买商品的实际支付金额,包括微信支付、余额支付、支付宝支付、线下支付金额(拼团商品在成团之后计入,线下支付订单在后台确认支付后计入)"
|
||||
icon="fa-solid:shopping-cart"
|
||||
icon-color="bg-purple-100"
|
||||
icon-bg-color="text-purple-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.orderPayPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.orderPayPrice,
|
||||
trendSummary?.reference?.orderPayPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="6" :sm="12" :xs="24">
|
||||
<SummaryCard
|
||||
title="充值金额"
|
||||
tooltip="用户成功充值的金额"
|
||||
icon="fa-solid:money-check-alt"
|
||||
icon-color="bg-yellow-100"
|
||||
icon-bg-color="text-yellow-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.rechargePrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.rechargePrice,
|
||||
trendSummary?.reference?.rechargePrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="6" :sm="12" :xs="24">
|
||||
<SummaryCard
|
||||
title="支出金额"
|
||||
tooltip="余额支付金额、支付佣金金额、商品退款金额"
|
||||
icon="ep:warning-filled"
|
||||
icon-color="bg-green-100"
|
||||
icon-bg-color="text-green-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.expensePrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.expensePrice,
|
||||
trendSummary?.reference?.expensePrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="6" :sm="12" :xs="24">
|
||||
<SummaryCard
|
||||
title="余额支付金额"
|
||||
tooltip="用户下单时使用余额实际支付的金额"
|
||||
icon="fa-solid:wallet"
|
||||
icon-color="bg-cyan-100"
|
||||
icon-bg-color="text-cyan-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.walletPayPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.walletPayPrice,
|
||||
trendSummary?.reference?.walletPayPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="6" :sm="12" :xs="24">
|
||||
<SummaryCard
|
||||
title="支付佣金金额"
|
||||
tooltip="后台给推广员支付的推广佣金,以实际支付为准"
|
||||
icon="fa-solid:award"
|
||||
icon-color="bg-yellow-100"
|
||||
icon-bg-color="text-yellow-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.brokerageSettlementPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.brokerageSettlementPrice,
|
||||
trendSummary?.reference?.brokerageSettlementPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="6" :sm="12" :xs="24">
|
||||
<SummaryCard
|
||||
title="商品退款金额"
|
||||
tooltip="用户成功退款的商品金额"
|
||||
icon="fa-solid:times-circle"
|
||||
icon-color="bg-blue-100"
|
||||
icon-bg-color="text-blue-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(trendSummary?.value?.afterSaleRefundPrice || 0)"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.afterSaleRefundPrice,
|
||||
trendSummary?.reference?.afterSaleRefundPrice
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 折线图 -->
|
||||
<el-skeleton :loading="trendLoading" animated>
|
||||
<Echart :height="500" :options="lineChartOptions" />
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as TradeStatisticsApi from '@/api/mall/statistics/trade'
|
||||
import TradeStatisticValue from './components/TradeStatisticValue.vue'
|
||||
import SummaryCard from '@/components/SummaryCard/index.vue'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { DataComparisonRespVO } from '@/api/mall/statistics/common'
|
||||
import { TradeSummaryRespVO, TradeTrendSummaryRespVO } from '@/api/mall/statistics/trade'
|
||||
import { calculateRelativeRate, fenToYuan } from '@/utils'
|
||||
import download from '@/utils/download'
|
||||
import { CardTitle } from '@/components/Card'
|
||||
import * as DateUtil from '@/utils/formatTime'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/** 交易统计 */
|
||||
defineOptions({ name: 'TradeStatistics' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const trendLoading = ref(true) // 交易状态加载中
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
const summary = ref<DataComparisonRespVO<TradeSummaryRespVO>>() // 交易统计数据
|
||||
const trendSummary = ref<DataComparisonRespVO<TradeTrendSummaryRespVO>>() // 交易状况统计数据
|
||||
const shortcutDateRangePicker = ref()
|
||||
|
||||
/** 折线图配置 */
|
||||
const lineChartOptions = reactive<EChartsOption>({
|
||||
dataset: {
|
||||
dimensions: ['date', 'turnoverPrice', 'orderPayPrice', 'rechargePrice', 'expensePrice'],
|
||||
source: []
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
top: 80,
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
top: 50
|
||||
},
|
||||
series: [
|
||||
{ name: '营业额', type: 'line', smooth: true },
|
||||
{ name: '商品支付金额', type: 'line', smooth: true },
|
||||
{ name: '充值金额', type: 'line', smooth: true },
|
||||
{ name: '支出金额', type: 'line', smooth: true }
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
// 数据区域缩放
|
||||
dataZoom: {
|
||||
yAxisIndex: false // Y轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '交易状况' } // 保存为图片
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
padding: [5, 10]
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}) as EChartsOption
|
||||
|
||||
/** 处理交易状况查询 */
|
||||
const getTradeTrendData = async () => {
|
||||
trendLoading.value = true
|
||||
// 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
if (DateUtil.isSameDay(times[0], times[1])) {
|
||||
// 前天
|
||||
times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
|
||||
}
|
||||
// 查询数据
|
||||
await Promise.all([getTradeStatisticsAnalyse(), getTradeStatisticsList()])
|
||||
trendLoading.value = false
|
||||
}
|
||||
|
||||
/** 查询交易统计 */
|
||||
const getTradeStatisticsSummary = async () => {
|
||||
summary.value = await TradeStatisticsApi.getTradeStatisticsSummary()
|
||||
}
|
||||
|
||||
/** 查询交易状况数据统计 */
|
||||
const getTradeStatisticsAnalyse = async () => {
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
trendSummary.value = await TradeStatisticsApi.getTradeStatisticsAnalyse({ times })
|
||||
}
|
||||
|
||||
/** 查询交易状况数据列表 */
|
||||
const getTradeStatisticsList = async () => {
|
||||
// 查询数据
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
const list = await TradeStatisticsApi.getTradeStatisticsList({ times })
|
||||
// 处理数据
|
||||
for (let item of list) {
|
||||
item.turnoverPrice = fenToYuan(item.turnoverPrice)
|
||||
item.orderPayPrice = fenToYuan(item.orderPayPrice)
|
||||
item.rechargePrice = fenToYuan(item.rechargePrice)
|
||||
item.expensePrice = fenToYuan(item.expensePrice)
|
||||
}
|
||||
// 更新 Echarts 数据
|
||||
if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
|
||||
lineChartOptions.dataset['source'] = list
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
// 导出的二次确认
|
||||
await message.exportConfirm()
|
||||
// 发起导出
|
||||
exportLoading.value = true
|
||||
const times = shortcutDateRangePicker.value.times
|
||||
const data = await TradeStatisticsApi.exportTradeStatisticsExcel({ times })
|
||||
download.excel(data, '交易状况.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
await getTradeStatisticsSummary()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.summary {
|
||||
.el-col {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user