统计:增加商品统计

(cherry picked from commit ca35e1a4af)
pull/420/head
owen 2023-12-16 23:48:09 +08:00 committed by shizhong
parent 96fae4f3ea
commit cae1d7cb1a
8 changed files with 492 additions and 25 deletions

View File

@ -0,0 +1,52 @@
import request from '@/config/axios'
import { DataComparisonRespVO } from '@/api/mall/statistics/common'
export interface ProductStatisticsVO {
id: number
day: string
spuId: number
spuName: string
spuPicUrl: string
browseCount: number
browseUserCount: number
favoriteCount: number
cartCount: number
orderCount: number
orderPayCount: number
orderPayPrice: number
afterSaleCount: number
afterSaleRefundPrice: number
browseConvertPercent: number
}
// 商品统计 API
export const ProductStatisticsApi = {
// 获得商品统计分析
getProductStatisticsAnalyse: (params: any) => {
return request.get<DataComparisonRespVO<ProductStatisticsVO>>({
url: '/statistics/product/analyse',
params
})
},
// 获得商品状况明细
getProductStatisticsList: (params: any) => {
return request.get<ProductStatisticsVO[]>({
url: '/statistics/product/list',
params
})
},
// 导出获得商品状况明细 Excel
exportProductStatisticsExcel: (params: any) => {
return request.download({
url: '/statistics/product/export-excel',
params
})
},
// 获得商品排行榜分页
getProductStatisticsRankPage: async (params: any) => {
return await request.get({
url: `/statistics/product/rank-page`,
params
})
}
}

View File

@ -66,9 +66,9 @@ export const getTradeStatisticsSummary = () => {
}
// 获得交易状况统计
export const getTradeTrendSummary = (params: TradeTrendReqVO) => {
export const getTradeStatisticsAnalyse = (params: TradeTrendReqVO) => {
return request.get<DataComparisonRespVO<TradeTrendSummaryRespVO>>({
url: '/statistics/trade/trend/summary',
url: '/statistics/trade/analyse',
params: formatDateParam(params)
})
}

View File

@ -70,27 +70,11 @@ service.interceptors.request.use(
}
// get参数编码
if (config.method?.toUpperCase() === 'GET' && params) {
let url = config.url + '?'
for (const propName of Object.keys(params)) {
const value = params[propName]
if (value !== void 0 && value !== null && typeof value !== 'undefined') {
if (typeof value === 'object') {
for (const val of Object.keys(value)) {
const params = propName + '[' + val + ']'
const subPart = encodeURIComponent(params) + '='
url += subPart + encodeURIComponent(value[val]) + '&'
}
} else {
url += `${propName}=${encodeURIComponent(value)}&`
}
}
}
// 给 get 请求加上时间戳参数,避免从缓存中拿数据
// const now = new Date().getTime()
// params = params.substring(0, url.length - 1) + `?_t=${now}`
url = url.slice(0, -1)
config.params = {}
config.url = url
const paramsStr = qs.stringify(params, { allowDots: true })
if (paramsStr) {
config.url = config.url + '?' + paramsStr
}
}
return config
},

View File

@ -286,6 +286,15 @@ export const getUrlNumberValue = (key: string, urlStr: string = location.href):
return toNumber(getUrlValue(key, urlStr))
}
/**
*
* @param prop
* @param order
*/
export const buildSortingField = ({ prop, order }) => {
return { field: prop, order: order === 'ascending' ? 'asc' : 'desc' }
}
export const treeFormatter = (ary: any, val: string, valueField = 'value', nameField = 'label') => {
let o = ''
if (ary != null) {

View File

@ -0,0 +1,104 @@
<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" />
<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'
/** 商品排行 */
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>

View 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="2"
:value="fenToYuan(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="2"
:value="fenToYuan(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="2"
:value="fenToYuan(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="2"
:value="fenToYuan(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>

View File

@ -0,0 +1,14 @@
<template>
<!-- 商品概览 -->
<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>

View File

@ -298,7 +298,7 @@ const getTradeTrendData = async () => {
times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
}
//
await Promise.all([getTradeTrendSummary(), getTradeStatisticsList()])
await Promise.all([getTradeStatisticsAnalyse(), getTradeStatisticsList()])
trendLoading.value = false
}
@ -308,9 +308,9 @@ const getTradeStatisticsSummary = async () => {
}
/** 查询交易状况数据统计 */
const getTradeTrendSummary = async () => {
const getTradeStatisticsAnalyse = async () => {
const times = shortcutDateRangePicker.value.times
trendSummary.value = await TradeStatisticsApi.getTradeTrendSummary({ times })
trendSummary.value = await TradeStatisticsApi.getTradeStatisticsAnalyse({ times })
}
/** 查询交易状况数据列表 */