feat: 新增商品统计组件和优化数据处理逻辑

- 引入商品排行和商品概况组件,展示商品相关统计信息
- 更新商品统计 API,支持时间范围查询和数据格式化
- 优化数据加载逻辑,提升用户体验
- 添加日期范围选择器,增强统计数据的灵活性
This commit is contained in:
lrl
2025-07-17 09:53:04 +08:00
parent 73a73ac312
commit 4620ede9b9
9 changed files with 587 additions and 29 deletions

View File

@@ -2,6 +2,8 @@ import type { PageParam, PageResult } from '@vben/request';
import type { MallDataComparisonResp } from './common';
import { formatDate2 } from '@vben/utils';
import { requestClient } from '#/api/request';
export namespace MallProductStatisticsApi {
@@ -38,26 +40,58 @@ export namespace MallProductStatisticsApi {
/** 浏览转化率 */
browseConvertPercent: number;
}
/** 会员分析 Request */
export interface ProductStatisticsReq {
times: Date[];
}
}
/** 获得商品统计分析 */
export function getProductStatisticsAnalyse(params: PageParam) {
export function getProductStatisticsAnalyse(
params: MallProductStatisticsApi.ProductStatisticsReq,
) {
return requestClient.get<
MallDataComparisonResp<MallProductStatisticsApi.ProductStatistics>
>('/statistics/product/analyse', { params });
>('/statistics/product/analyse', {
params: {
times: [
formatDate2(params.times[0] || new Date()),
formatDate2(params.times[1] || new Date()),
],
},
});
}
/** 获得商品状况明细 */
export function getProductStatisticsList(params: PageParam) {
export function getProductStatisticsList(
params: MallProductStatisticsApi.ProductStatisticsReq,
) {
return requestClient.get<MallProductStatisticsApi.ProductStatistics[]>(
'/statistics/product/list',
{ params },
{
params: {
times: [
formatDate2(params.times[0] || new Date()),
formatDate2(params.times[1] || new Date()),
],
},
},
);
}
/** 导出获得商品状况明细 Excel */
export function exportProductStatisticsExcel(params: PageParam) {
return requestClient.download('/statistics/product/export-excel', { params });
export function exportProductStatisticsExcel(
params: MallProductStatisticsApi.ProductStatisticsReq,
) {
return requestClient.download('/statistics/product/export-excel', {
params: {
times: [
formatDate2(params.times[0] || new Date()),
formatDate2(params.times[1] || new Date()),
],
},
});
}
/** 获得商品排行榜分页 */

View File

@@ -0,0 +1,153 @@
<script lang="ts" setup>
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
import { onMounted, reactive, ref } from 'vue';
import { AnalysisChartCard } from '@vben/common-ui';
import { buildSortingField, floatToFixed2 } from '@vben/utils';
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
/** 商品排行 */
defineOptions({ name: 'ProductRank' });
// 格式化:访客-支付转化率
const formatConvertRate = (row: MallProductStatisticsApi.ProductStatistics) => {
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<MallProductStatisticsApi.ProductStatistics[]>([]); // 列表的数据
/** 查询商品列表 */
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;
}
};
// 格式化金额【分转元】
// @ts-ignore
const fenToYuanFormat = (_, __, cellValue: any, ___) => {
return `${floatToFixed2(cellValue)}`;
};
/** 初始化 */
onMounted(async () => {
await getSpuList();
});
</script>
<template>
<AnalysisChartCard title="商品排行">
<template #header-suffix>
<ShortcutDateRangePicker
ref="shortcutDateRangePicker"
@change="handleDateRangeChange"
/>
</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"
/>
</AnalysisChartCard>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,300 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
import { reactive, ref } from 'vue';
import { AnalysisChartCard, AnalysisOverview, confirm } from '@vben/common-ui';
import {
SvgBellIcon,
SvgCakeIcon,
SvgDownloadIcon,
SvgEyeIcon,
} from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import {
downloadFileFromBlobPart,
fenToYuan,
formatDate,
isSameDay,
} from '@vben/utils';
import dayjs from 'dayjs';
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
/** 商品概况 */
defineOptions({ name: 'ProductSummary' });
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const trendLoading = ref(true); // 商品状态加载中
const exportLoading = ref(false); // 导出的加载中
const trendSummary =
ref<MallDataComparisonResp<MallProductStatisticsApi.ProductStatistics>>(); // 商品状况统计数据
const shortcutDateRangePicker = ref();
/** 折线图配置 */
const lineChartOptions = reactive({
dataset: {
dimensions: [
'time',
'browseCount',
'browseUserCount',
'orderPayPrice',
'afterSaleRefundPrice',
],
source: [] as MallProductStatisticsApi.ProductStatistics[],
},
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'] as const, // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '商品状况' }, // 保存为图片
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
padding: [5, 10],
},
xAxis: {
type: 'category' as const,
boundaryGap: true,
axisTick: {
show: false,
},
},
yAxis: [
{
type: 'value' as const,
name: '金额',
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: '#7F8B9C',
},
splitLine: {
show: true,
lineStyle: {
color: '#F5F7F9',
},
},
},
{
type: 'value' as const,
name: '数量',
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: '#7F8B9C',
},
splitLine: {
show: true,
lineStyle: {
color: '#F5F7F9',
},
},
},
],
});
/** 处理商品状况查询 */
const getProductTrendData = async () => {
trendLoading.value = true;
// 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
const times = shortcutDateRangePicker.value.times;
if (isSameDay(times[0], times[1])) {
// 前天
times[0] = formatDate(dayjs(times[0]).subtract(1, 'd').toDate());
}
// 查询数据
await Promise.all([getProductTrendSummary(), getProductStatisticsList()]);
renderEcharts(lineChartOptions as unknown as echarts.EChartsOption);
loadOverview();
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: MallProductStatisticsApi.ProductStatistics[] =
await ProductStatisticsApi.getProductStatisticsList({ times });
// 处理数据
for (const item of list) {
item.orderPayPrice = Number(fenToYuan(item.orderPayPrice));
item.afterSaleRefundPrice = Number(fenToYuan(item.afterSaleRefundPrice));
}
// 更新 Echarts 数据
if (lineChartOptions.dataset && lineChartOptions.dataset.source) {
lineChartOptions.dataset.source = list;
}
};
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await confirm('确定要导出商品状况吗?');
// 发起导出
exportLoading.value = true;
const times = shortcutDateRangePicker.value.times;
const data = await ProductStatisticsApi.exportProductStatisticsExcel({
times,
});
downloadFileFromBlobPart({ fileName: '商品状况.xls', source: data });
} finally {
exportLoading.value = false;
}
};
const overviewItems = ref<AnalysisOverviewItem[]>();
const loadOverview = () => {
overviewItems.value = [
{
icon: SvgEyeIcon,
title: '商品浏览量',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.browseCount,
value: trendSummary?.value?.browseUserCount || 0,
tooltip:
'在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次',
},
{
icon: SvgCakeIcon,
title: '商品访客数',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.browseUserCount || 0,
value: trendSummary?.value?.browseUserCount || 0,
tooltip:
'在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个',
},
{
icon: SvgDownloadIcon,
title: '支付件数',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.orderPayCount || 0,
value: trendSummary?.value?.orderPayCount || 0,
},
{
icon: SvgBellIcon,
title: '支付金额',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
value: trendSummary?.value?.orderPayPrice || 0,
},
{
icon: SvgBellIcon,
title: '退款件数',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
value: trendSummary?.value?.afterSaleCount || 0,
},
{
icon: SvgBellIcon,
title: '退款金额',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleRefundPrice || 0,
value: trendSummary?.value?.afterSaleRefundPrice || 0,
},
];
};
</script>
<template>
<AnalysisChartCard title="商品概况">
<template #header-suffix>
<!-- 查询条件 -->
<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>
</template>
<!-- 统计值 -->
<AnalysisOverview
v-model:model-value="overviewItems"
:columns-number="6"
class="mt-5 md:mr-4 md:mt-0 md:w-full"
/>
<!-- 折线图 -->
<el-skeleton :loading="trendLoading" animated>
<EchartsUI ref="chartRef" height="500px" />
</el-skeleton>
</AnalysisChartCard>
</template>
<style lang="scss" scoped></style>

View File

@@ -1,7 +1,8 @@
<script lang="ts" setup>
import { DocAlert, Page } from '@vben/common-ui';
import { ElButton } from 'element-plus';
import ProductRank from './components/product-rank.vue';
import ProductSummary from './components/product-summary.vue';
</script>
<template>
@@ -10,25 +11,7 @@ import { ElButton } from 'element-plus';
title="【统计】会员、商品、交易统计"
url="https://doc.iocoder.cn/mall/statistics/"
/>
<ElButton
danger
type="primary"
link
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</ElButton>
<br />
<ElButton
type="primary"
link
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/product/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/product/index
代码pull request 贡献给我们
</ElButton>
<ProductSummary class="md:w-3/3 mt-5 md:mr-4 md:mt-0" />
<ProductRank class="md:w-3/3 mt-5 md:mr-4 md:mt-0" />
</Page>
</template>