parent
96fae4f3ea
commit
cae1d7cb1a
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 })
|
||||
}
|
||||
|
||||
/** 查询交易状况数据列表 */
|
||||
|
|
Loading…
Reference in New Issue