feat: 新增商品统计组件和优化数据处理逻辑
- 引入商品排行和商品概况组件,展示商品相关统计信息 - 更新商品统计 API,支持时间范围查询和数据格式化 - 优化数据加载逻辑,提升用户体验 - 添加日期范围选择器,增强统计数据的灵活性
This commit is contained in:
@@ -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()),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 获得商品排行榜分页 */
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user