商城:增加商城首页

This commit is contained in:
owen
2023-10-16 09:51:19 +08:00
parent 532b237712
commit 3802fee661
15 changed files with 1038 additions and 275 deletions

View File

@@ -0,0 +1,119 @@
<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?.userCount || 0 }}
</div>
<div class="mt-2 text-3.5">
环比增长率{{
calculateRelativeRate(
analyseData?.comparison?.value?.userCount,
analyseData?.comparison?.reference?.userCount
)
}}%
</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?.visitorCount || 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?.activeUserCount || 0 }}
</div>
<div class="mt-2 text-3.5">
环比增长率{{
calculateRelativeRate(
analyseData?.comparison?.value?.activeUserCount,
analyseData?.comparison?.reference?.activeUserCount
)
}}%
</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>

View File

@@ -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>

View File

@@ -44,118 +44,20 @@
</el-row>
<el-row :gutter="16" class="mb-4">
<el-col :md="18" :sm="24">
<el-card shadow="never">
<template #header>
<div class="flex flex-row items-center justify-between">
<span>会员概览</span>
<!-- 查询条件 -->
<div class="my--2 flex flex-row items-center gap-2">
<el-radio-group v-model="shortcutDays" @change="handleDateTypeChange">
<el-radio-button :label="1">昨天</el-radio-button>
<el-radio-button :label="7">最近7天</el-radio-button>
<el-radio-button :label="30">最近30天</el-radio-button>
</el-radio-group>
<el-date-picker
v-model="queryParams.times"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="shortcuts"
class="!w-240px"
@change="getMemberAnalyse"
/>
</div>
</div>
</template>
<div class="min-w-225 py-1.75" v-loading="analyseLoading">
<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?.userCount || 0 }}
</div>
<div class="mt-2 text-3.5">
环比增长率{{
calculateRelativeRate(
analyseData?.comparison?.value?.userCount,
analyseData?.comparison?.reference?.userCount
)
}}%
</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?.visitorCount || 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?.activeUserCount || 0 }}
</div>
<div class="mt-2 text-3.5">
环比增长率{{
calculateRelativeRate(
analyseData?.comparison?.value?.activeUserCount,
analyseData?.comparison?.reference?.activeUserCount
)
}}%
</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>
<!-- 会员概览 -->
<MemberFunnelCard />
</el-col>
<el-col :md="6" :sm="24">
<el-card shadow="never" header="会员终端" v-loading="loading">
<Echart :height="300" :options="terminalChartOptions" />
</el-card>
<!-- 会员终端 -->
<MemberTerminalCard />
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :md="18" :sm="24">
<el-card shadow="never" header="会员地域分布">
<el-card shadow="never">
<template #header>
<CardTitle title="会员地域分布" />
</template>
<el-row v-loading="loading">
<el-col :span="10">
<Echart :height="300" :options="areaChartOptions" />
@@ -206,7 +108,10 @@
</el-card>
</el-col>
<el-col :md="6" :sm="24">
<el-card shadow="never" header="会员性别比例" v-loading="loading">
<el-card shadow="never" v-loading="loading">
<template #header>
<CardTitle title="会员性别比例" />
</template>
<Echart :height="300" :options="sexChartOptions" />
</el-card>
</el-col>
@@ -214,63 +119,34 @@
</div>
</template>
<script lang="ts" setup>
import * as TradeMemberApi from '@/api/mall/statistics/member'
import * as MemberStatisticsApi from '@/api/mall/statistics/member'
import TradeTrendValue from '../trade/components/TradeTrendValue.vue'
import { EChartsOption } from 'echarts'
import china from '@/assets/map/json/china.json'
import dayjs from 'dayjs'
import { fenToYuan } from '@/utils'
import * as DateUtil from '@/utils/formatTime'
import {
MemberAnalyseRespVO,
MemberAreaStatisticsRespVO,
MemberSexStatisticsRespVO,
MemberAnalyseReqVO,
MemberSummaryRespVO,
MemberTerminalStatisticsRespVO
} from '@/api/mall/statistics/member'
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 analyseLoading = ref(true) // 会员概览加载中
const queryParams = reactive<MemberAnalyseReqVO>({ times: ['', ''] }) // 会员概览查询参数
const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
const summary = ref<MemberSummaryRespVO>() // 会员统计数据
const analyseData = ref<MemberAnalyseRespVO>() // 会员分析数据
const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 省份会员统计
// 注册地图
echarts?.registerMap('china', china!)
/** 日期快捷选择 */
const shortcuts = [
{
text: '昨天',
value: () => DateUtil.getDayRange(new Date(), -1)
},
{
text: '最近7天',
value: () => DateUtil.getLast7Days()
},
{
text: '本月',
value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')]
},
{
text: '最近30天',
value: () => DateUtil.getLast30Days()
},
{
text: '最近1年',
value: () => DateUtil.getLast1Year()
}
]
/** 会员终端统计图配置 */
const terminalChartOptions = reactive<EChartsOption>({
tooltip: {
@@ -357,37 +233,14 @@ const areaChartOptions = reactive<EChartsOption>({
]
}) as EChartsOption
/** 计算环比 */
const calculateRelativeRate = (value?: number, reference?: number) => {
// 防止除0
if (!reference) return 0
return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
}
/** 设置时间范围 */
function setTimes() {
const beginDate = dayjs().subtract(shortcutDays.value, 'd')
const yesterday = dayjs().subtract(1, 'd')
queryParams.times = DateUtil.getDateRange(beginDate, yesterday)
}
/** 处理会员概览查询(日期单选按钮组选择后) */
const handleDateTypeChange = async () => {
// 设置时间范围
setTimes()
// 查询数据
await getMemberAnalyse()
}
/** 查询会员统计 */
const getMemberSummary = async () => {
summary.value = await TradeMemberApi.getMemberSummary()
summary.value = await MemberStatisticsApi.getMemberSummary()
}
/** 按照省份,查询会员统计列表 */
const getMemberAreaStatisticsList = async () => {
const list = await TradeMemberApi.getMemberAreaStatisticsList()
const list = await MemberStatisticsApi.getMemberAreaStatisticsList()
areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
return {
...item,
@@ -412,7 +265,7 @@ const getMemberAreaStatisticsList = async () => {
/** 按照性别,查询会员统计列表 */
const getMemberSexStatisticsList = async () => {
const list = await TradeMemberApi.getMemberSexStatisticsList()
const list = await MemberStatisticsApi.getMemberSexStatisticsList()
const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
sexChartOptions.series[0].data = dictDataList.map((dictData: DictDataType) => {
const userCount = list.find((item: MemberSexStatisticsRespVO) => item.sex === dictData.value)
@@ -426,7 +279,7 @@ const getMemberSexStatisticsList = async () => {
/** 按照终端,查询会员统计列表 */
const getMemberTerminalStatisticsList = async () => {
const list = await TradeMemberApi.getMemberTerminalStatisticsList()
const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
const userCount = list.find(
@@ -439,20 +292,6 @@ const getMemberTerminalStatisticsList = async () => {
})
}
/** 查询会员概览数据列表 */
const getMemberAnalyse = async () => {
analyseLoading.value = true
const times = queryParams.times
// 开始与截止在同一天的, 环比出不来, 需要延长一天
if (DateUtil.isSameDay(times[0], times[1])) {
// 前天
times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
}
// 查询数据
analyseData.value = await TradeMemberApi.getMemberAnalyse({ times })
analyseLoading.value = false
}
/** 初始化 **/
onMounted(async () => {
loading.value = true
@@ -460,8 +299,7 @@ onMounted(async () => {
getMemberSummary(),
getMemberTerminalStatisticsList(),
getMemberAreaStatisticsList(),
getMemberSexStatisticsList(),
handleDateTypeChange()
getMemberSexStatisticsList()
])
loading.value = false
})
@@ -472,13 +310,4 @@ onMounted(async () => {
margin-bottom: 1rem;
}
}
.trapezoid1 {
transform: perspective(5em) rotateX(-11deg);
}
.trapezoid2 {
transform: perspective(7em) rotateX(-20deg);
}
.trapezoid3 {
transform: perspective(3em) rotateX(-13deg);
}
</style>

View File

@@ -59,25 +59,9 @@
<template #header>
<!-- 标题 -->
<div class="flex flex-row items-center justify-between">
<span>交易状况</span>
<CardTitle title="交易状况" />
<!-- 查询条件 -->
<div class="flex flex-row items-center gap-2">
<el-radio-group v-model="shortcutDays" @change="handleDateTypeChange">
<el-radio-button :label="1">昨天</el-radio-button>
<el-radio-button :label="7">最近7天</el-radio-button>
<el-radio-button :label="30">最近30天</el-radio-button>
</el-radio-group>
<el-date-picker
v-model="queryParams.times"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="shortcuts"
class="!w-240px"
@change="getTradeTrendData"
/>
<ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getTradeTrendData">
<el-button
class="ml-4"
@click="handleExport"
@@ -86,7 +70,7 @@
>
<Icon icon="ep:download" class="mr-1" />导出
</el-button>
</div>
</ShortcutDateRangePicker>
</div>
</template>
<!-- 统计值 -->
@@ -233,13 +217,11 @@ import { EChartsOption } from 'echarts'
import {
TradeStatisticsComparisonRespVO,
TradeSummaryRespVO,
TradeTrendReqVO,
TradeTrendSummaryRespVO
} from '@/api/mall/statistics/trade'
import dayjs from 'dayjs'
import { fenToYuan } from '@/utils'
import * as DateUtil from '@/utils/formatTime'
import { calculateRelativeRate, fenToYuan } from '@/utils'
import download from '@/utils/download'
import { CardTitle } from '@/components/Card'
/** 交易统计 */
defineOptions({ name: 'TradeStatistics' })
@@ -249,34 +231,9 @@ const message = useMessage() // 消息弹窗
const loading = ref(true) // 加载中
const trendLoading = ref(true) // 交易状态加载中
const exportLoading = ref(false) // 导出的加载中
const queryParams = reactive<TradeTrendReqVO>({ times: ['', ''] }) // 交易状况查询参数
const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
const summary = ref<TradeStatisticsComparisonRespVO<TradeSummaryRespVO>>() // 交易统计数据
const trendSummary = ref<TradeStatisticsComparisonRespVO<TradeTrendSummaryRespVO>>() // 交易状况统计数据
/** 日期快捷选择 */
const shortcuts = [
{
text: '昨天',
value: () => DateUtil.getDayRange(new Date(), -1)
},
{
text: '最近7天',
value: () => DateUtil.getLast7Days()
},
{
text: '本月',
value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')]
},
{
text: '最近30天',
value: () => DateUtil.getLast30Days()
},
{
text: '最近1年',
value: () => DateUtil.getLast1Year()
}
]
const shortcutDateRangePicker = ref()
/** 折线图配置 */
const lineChartOptions = reactive<EChartsOption>({
@@ -333,29 +290,6 @@ const lineChartOptions = reactive<EChartsOption>({
}
}) as EChartsOption
/** 计算环比 */
const calculateRelativeRate = (value?: number, reference?: number) => {
// 防止除0
if (!reference) return 0
return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
}
/** 设置时间范围 */
function setTimes() {
const beginDate = dayjs().subtract(shortcutDays.value, 'd')
const yesterday = dayjs().subtract(1, 'd')
queryParams.times = DateUtil.getDateRange(beginDate, yesterday)
}
/** 处理交易状况查询(日期单选按钮组选择后) */
const handleDateTypeChange = async () => {
// 设置时间范围
setTimes()
// 查询数据
await getTradeTrendData()
}
/** 处理交易状况查询 */
const getTradeTrendData = async () => {
trendLoading.value = true
@@ -370,20 +304,14 @@ const getTradeStatisticsSummary = async () => {
/** 查询交易状况数据统计 */
const getTradeTrendSummary = async () => {
loading.value = true
trendSummary.value = await TradeStatisticsApi.getTradeTrendSummary(queryParams)
loading.value = false
const times = shortcutDateRangePicker.value.times
trendSummary.value = await TradeStatisticsApi.getTradeTrendSummary({ times })
}
/** 查询交易状况数据列表 */
const getTradeTrendList = async () => {
const times = queryParams.times
// 开始与截止在同一天的, 折线图出不来, 需要延长一天
if (DateUtil.isSameDay(times[0], times[1])) {
// 前天
times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
}
// 查询数据
const times = shortcutDateRangePicker.value.times
const list = await TradeStatisticsApi.getTradeTrendList({ times })
// 处理数据
for (let item of list) {
@@ -405,7 +333,8 @@ const handleExport = async () => {
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await TradeStatisticsApi.exportTradeTrend(queryParams)
const times = shortcutDateRangePicker.value.times
const data = await TradeStatisticsApi.exportTradeTrend({ times })
download.excel(data, '交易状况.xls')
} catch {
} finally {
@@ -416,7 +345,6 @@ const handleExport = async () => {
/** 初始化 **/
onMounted(async () => {
await getTradeStatisticsSummary()
await handleDateTypeChange()
})
</script>
<style lang="scss" scoped>