feat:【mall 商城】交易统计、商品统计【ele】60%:初始化
This commit is contained in:
@@ -1,147 +0,0 @@
|
|||||||
<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, fenToYuanFormat } 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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
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>
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
||||||
|
|
||||||
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
|
|
||||||
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
|
|
||||||
import type { AnalysisOverviewIconItem } from '#/views/mall/home/components/data';
|
|
||||||
|
|
||||||
import { reactive, ref } from 'vue';
|
|
||||||
|
|
||||||
import { confirm } from '@vben/common-ui';
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import {
|
|
||||||
calculateRelativeRate,
|
|
||||||
downloadFileFromBlobPart,
|
|
||||||
fenToYuan,
|
|
||||||
formatDate,
|
|
||||||
isSameDay,
|
|
||||||
} from '@vben/utils';
|
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
|
|
||||||
import AnalysisChartCard from '#/views/mall/home/components/analysis-chart-card.vue';
|
|
||||||
import AnalysisOverviewIcon from '#/views/mall/home/components/analysis-overview-icon.vue';
|
|
||||||
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<AnalysisOverviewIconItem[]>();
|
|
||||||
const loadOverview = () => {
|
|
||||||
overviewItems.value = [
|
|
||||||
{
|
|
||||||
icon: 'ep:view',
|
|
||||||
title: '商品浏览量',
|
|
||||||
value: trendSummary.value?.value?.browseCount || 0,
|
|
||||||
iconColor: 'bg-blue-100',
|
|
||||||
iconBgColor: 'text-blue-500',
|
|
||||||
tooltip:
|
|
||||||
'在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次',
|
|
||||||
percent: calculateRelativeRate(
|
|
||||||
trendSummary?.value?.value?.browseCount,
|
|
||||||
trendSummary.value?.reference?.browseCount,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'ep:user-filled',
|
|
||||||
title: '商品访客数',
|
|
||||||
value: trendSummary.value?.value?.browseUserCount || 0,
|
|
||||||
iconColor: 'bg-purple-100',
|
|
||||||
iconBgColor: 'text-purple-500',
|
|
||||||
tooltip:
|
|
||||||
'在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个',
|
|
||||||
percent: calculateRelativeRate(
|
|
||||||
trendSummary?.value?.value?.browseUserCount,
|
|
||||||
trendSummary.value?.reference?.browseUserCount,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-solid:money-check-alt',
|
|
||||||
title: '支付件数',
|
|
||||||
iconColor: 'bg-yellow-100',
|
|
||||||
iconBgColor: 'text-yellow-500',
|
|
||||||
value: trendSummary.value?.value?.orderPayCount || 0,
|
|
||||||
tooltip: '在选定条件下,成功付款订单的商品件数之和',
|
|
||||||
percent: calculateRelativeRate(
|
|
||||||
trendSummary?.value?.value?.orderPayCount,
|
|
||||||
trendSummary.value?.reference?.orderPayCount,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'ep:warning-filled',
|
|
||||||
title: '支付金额',
|
|
||||||
iconColor: 'bg-green-100',
|
|
||||||
iconBgColor: 'text-green-500',
|
|
||||||
prefix: '¥',
|
|
||||||
value: Number(fenToYuan(trendSummary.value?.value?.orderPayPrice || 0)),
|
|
||||||
tooltip: '在选定条件下,成功付款订单的商品金额之和',
|
|
||||||
decimals: 2,
|
|
||||||
percent: calculateRelativeRate(
|
|
||||||
trendSummary?.value?.value?.orderPayPrice,
|
|
||||||
trendSummary.value?.reference?.orderPayPrice,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-solid:wallet',
|
|
||||||
title: '退款件数',
|
|
||||||
iconColor: 'bg-cyan-100',
|
|
||||||
iconBgColor: 'text-cyan-500',
|
|
||||||
value: trendSummary.value?.value?.afterSaleCount || 0,
|
|
||||||
tooltip: '在选定条件下,成功退款的商品件数之和',
|
|
||||||
percent: calculateRelativeRate(
|
|
||||||
trendSummary?.value?.value?.afterSaleCount,
|
|
||||||
trendSummary.value?.reference?.afterSaleCount,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-solid:award',
|
|
||||||
title: '退款金额',
|
|
||||||
iconColor: 'bg-yellow-100',
|
|
||||||
iconBgColor: 'text-yellow-500',
|
|
||||||
prefix: '¥',
|
|
||||||
decimals: 2,
|
|
||||||
value: Number(
|
|
||||||
fenToYuan(trendSummary.value?.value?.afterSaleRefundPrice || 0),
|
|
||||||
),
|
|
||||||
tooltip: '在选定条件下,成功退款的商品金额之和',
|
|
||||||
percent: calculateRelativeRate(
|
|
||||||
trendSummary?.value?.value?.afterSaleRefundPrice,
|
|
||||||
trendSummary.value?.reference?.afterSaleRefundPrice,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<AnalysisChartCard title="商品概况">
|
|
||||||
<template #header-suffix>
|
|
||||||
<!-- 查询条件 -->
|
|
||||||
<ShortcutDateRangePicker
|
|
||||||
ref="shortcutDateRangePicker"
|
|
||||||
@change="getProductTrendData"
|
|
||||||
>
|
|
||||||
<el-button
|
|
||||||
class="ml-4"
|
|
||||||
@click="handleExport"
|
|
||||||
:loading="exportLoading"
|
|
||||||
v-access:code="['statistics:product:export']"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ep:download" class="mr-1" />导出
|
|
||||||
</el-button>
|
|
||||||
</ShortcutDateRangePicker>
|
|
||||||
</template>
|
|
||||||
<!-- 统计值 -->
|
|
||||||
<AnalysisOverviewIcon
|
|
||||||
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,21 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import ProductRank from './components/product-rank.vue';
|
import ProductRankCard from './modules/rank-card.vue';
|
||||||
import ProductSummary from './components/product-summary.vue';
|
import ProductSummaryCard from './modules/summary-card.vue';
|
||||||
|
|
||||||
|
/** 商品统计 */
|
||||||
|
defineOptions({ name: 'ProductStatistics' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【统计】会员、商品、交易统计"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/statistics/"
|
title="【统计】会员、商品、交易统计"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/statistics/"
|
||||||
<div class="mt-5 w-full">
|
/>
|
||||||
<ProductSummary class="mt-5 md:mr-4 md:mt-0" />
|
</template>
|
||||||
</div>
|
|
||||||
<div class="mt-5 w-full">
|
<div class="flex flex-col gap-4">
|
||||||
<ProductRank class="mt-5 md:mr-4 md:mt-0" />
|
<!-- 商品概览 -->
|
||||||
|
<ProductSummaryCard />
|
||||||
|
<!-- 商品排行 -->
|
||||||
|
<ProductRankCard />
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { buildSortingField } from '@vben/request';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
|
||||||
|
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||||
|
|
||||||
|
/** 商品排行 */
|
||||||
|
defineOptions({ name: 'ProductRankCard' });
|
||||||
|
|
||||||
|
const searchTimes = ref<string[]>([]);
|
||||||
|
|
||||||
|
/** 处理日期范围变化 */
|
||||||
|
const handleDateRangeChange = (times?: [Dayjs, Dayjs]) => {
|
||||||
|
if (times?.length !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchTimes.value = [
|
||||||
|
formatDateTime(times[0]) as string,
|
||||||
|
formatDateTime(times[1]) as string,
|
||||||
|
];
|
||||||
|
gridApi.query();
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: VxeTableGridOptions['columns'] = [
|
||||||
|
{ field: 'spuId', title: '商品 ID', minWidth: 100 },
|
||||||
|
{
|
||||||
|
field: 'picUrl',
|
||||||
|
title: '商品图片',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: { name: 'CellImage' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '商品名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'browseCount',
|
||||||
|
title: '浏览量',
|
||||||
|
minWidth: 100,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'browseUserCount',
|
||||||
|
title: '访客数',
|
||||||
|
minWidth: 100,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cartCount',
|
||||||
|
title: '加购件数',
|
||||||
|
minWidth: 110,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'orderCount',
|
||||||
|
title: '下单件数',
|
||||||
|
minWidth: 110,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'orderPayCount',
|
||||||
|
title: '支付件数',
|
||||||
|
minWidth: 110,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'orderPayPrice',
|
||||||
|
title: '支付金额(元)',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: 'formatFenToYuanAmount',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'favoriteCount',
|
||||||
|
title: '收藏数',
|
||||||
|
minWidth: 100,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'browseConvertPercent',
|
||||||
|
title: '访客-支付转化率(%)',
|
||||||
|
minWidth: 160,
|
||||||
|
sortable: true,
|
||||||
|
formatter: ({ cellValue }) => `${cellValue || 0}%`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns,
|
||||||
|
height: 400,
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page, sorts }) => {
|
||||||
|
return await ProductStatisticsApi.getProductStatisticsRankPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||||
|
...buildSortingField(sorts),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: true,
|
||||||
|
},
|
||||||
|
sortConfig: {
|
||||||
|
remote: true,
|
||||||
|
multiple: false,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>商品排行</span>
|
||||||
|
<ShortcutDateRangePicker @change="handleDateRangeChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Grid />
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import type { DataComparisonRespVO } from '#/api/mall/statistics/common';
|
||||||
|
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, SummaryCard } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import {
|
||||||
|
downloadFileFromBlobPart,
|
||||||
|
fenToYuan,
|
||||||
|
formatDateTime,
|
||||||
|
isSameDay,
|
||||||
|
} from '@vben/utils';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
|
||||||
|
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||||
|
|
||||||
|
import { getProductSummaryChartOptions } from './summary-chart-options';
|
||||||
|
|
||||||
|
/** 商品概况 */
|
||||||
|
defineOptions({ name: 'ProductSummaryCard' });
|
||||||
|
|
||||||
|
const trendLoading = ref(true); // 商品状态加载中
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
const trendSummary =
|
||||||
|
ref<DataComparisonRespVO<MallProductStatisticsApi.ProductStatistics>>(); // 商品状况统计数据
|
||||||
|
const searchTimes = ref<string[]>([]);
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
/** 计算环比百分比 */
|
||||||
|
const calculateRelativeRate = (value?: number, reference?: number): string => {
|
||||||
|
const refValue = Number(reference || 0);
|
||||||
|
const curValue = Number(value || 0);
|
||||||
|
if (!refValue || refValue === 0) {
|
||||||
|
return '0.00';
|
||||||
|
}
|
||||||
|
return (((curValue - refValue) / refValue) * 100).toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理日期范围变化 */
|
||||||
|
const handleDateRangeChange = (times?: [Dayjs, Dayjs]) => {
|
||||||
|
if (times?.length !== 2) {
|
||||||
|
getProductTrendData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
|
||||||
|
let adjustedTimes = times;
|
||||||
|
if (isSameDay(times[0], times[1])) {
|
||||||
|
adjustedTimes = [dayjs(times[0]).subtract(1, 'd'), times[1]];
|
||||||
|
}
|
||||||
|
searchTimes.value = [
|
||||||
|
formatDateTime(adjustedTimes[0]) as string,
|
||||||
|
formatDateTime(adjustedTimes[1]) as string,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
getProductTrendData();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理商品状况查询 */
|
||||||
|
const getProductTrendData = async () => {
|
||||||
|
trendLoading.value = true;
|
||||||
|
try {
|
||||||
|
await Promise.all([getProductTrendSummary(), getProductStatisticsList()]);
|
||||||
|
} finally {
|
||||||
|
trendLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询商品状况数据统计 */
|
||||||
|
async function getProductTrendSummary() {
|
||||||
|
trendSummary.value = await ProductStatisticsApi.getProductStatisticsAnalyse({
|
||||||
|
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询商品状况数据列表 */
|
||||||
|
async function getProductStatisticsList() {
|
||||||
|
const list = await ProductStatisticsApi.getProductStatisticsList({
|
||||||
|
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染图表
|
||||||
|
await renderEcharts(getProductSummaryChartOptions(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
async function handleExport() {
|
||||||
|
try {
|
||||||
|
// 导出的二次确认
|
||||||
|
await confirm({
|
||||||
|
content: '确认导出商品状况数据吗?',
|
||||||
|
});
|
||||||
|
// 发起导出
|
||||||
|
exportLoading.value = true;
|
||||||
|
const data = await ProductStatisticsApi.exportProductStatisticsExcel({
|
||||||
|
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||||
|
});
|
||||||
|
// 处理下载
|
||||||
|
downloadFileFromBlobPart({ fileName: '商品状况.xlsx', source: data });
|
||||||
|
} catch {
|
||||||
|
// 用户取消导出
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-card shadow="never" class="h-full">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>商品概况</span>
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ShortcutDateRangePicker @change="handleDateRangeChange">
|
||||||
|
<el-button class="ml-4" @click="handleExport" :loading="exportLoading">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="lucide:download" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</el-button>
|
||||||
|
</ShortcutDateRangePicker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 统计值 -->
|
||||||
|
<el-row :gutter="16" class="mb-4">
|
||||||
|
<el-col :xl="8" :md="8" :sm="24" class="mb-4">
|
||||||
|
<SummaryCard
|
||||||
|
title="商品浏览量"
|
||||||
|
tooltip="在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次"
|
||||||
|
icon="lucide:eye"
|
||||||
|
icon-color="text-blue-500"
|
||||||
|
icon-bg-color="bg-blue-100"
|
||||||
|
:decimals="0"
|
||||||
|
:value="trendSummary?.value?.browseCount || 0"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
trendSummary?.value?.browseCount,
|
||||||
|
trendSummary?.reference?.browseCount,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xl="8" :md="8" :sm="24" class="mb-4">
|
||||||
|
<SummaryCard
|
||||||
|
title="商品访客数"
|
||||||
|
tooltip="在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个"
|
||||||
|
icon="lucide:users"
|
||||||
|
icon-color="text-purple-500"
|
||||||
|
icon-bg-color="bg-purple-100"
|
||||||
|
:decimals="0"
|
||||||
|
:value="trendSummary?.value?.browseUserCount || 0"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
trendSummary?.value?.browseUserCount,
|
||||||
|
trendSummary?.reference?.browseUserCount,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xl="8" :md="8" :sm="24" class="mb-4">
|
||||||
|
<SummaryCard
|
||||||
|
title="支付件数"
|
||||||
|
tooltip="在选定条件下,成功付款订单的商品件数之和"
|
||||||
|
icon="lucide:credit-card"
|
||||||
|
icon-color="text-yellow-500"
|
||||||
|
icon-bg-color="bg-yellow-100"
|
||||||
|
:decimals="0"
|
||||||
|
:value="trendSummary?.value?.orderPayCount || 0"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
trendSummary?.value?.orderPayCount,
|
||||||
|
trendSummary?.reference?.orderPayCount,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xl="8" :md="8" :sm="24" class="mb-4">
|
||||||
|
<SummaryCard
|
||||||
|
title="支付金额"
|
||||||
|
tooltip="在选定条件下,成功付款订单的商品金额之和"
|
||||||
|
icon="lucide:banknote"
|
||||||
|
icon-color="text-green-500"
|
||||||
|
icon-bg-color="bg-green-100"
|
||||||
|
prefix="¥"
|
||||||
|
:decimals="2"
|
||||||
|
:value="Number(fenToYuan(trendSummary?.value?.orderPayPrice || 0))"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
trendSummary?.value?.orderPayPrice,
|
||||||
|
trendSummary?.reference?.orderPayPrice,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xl="8" :md="8" :sm="24" class="mb-4">
|
||||||
|
<SummaryCard
|
||||||
|
title="退款件数"
|
||||||
|
tooltip="在选定条件下,成功退款的商品件数之和"
|
||||||
|
icon="lucide:wallet"
|
||||||
|
icon-color="text-cyan-500"
|
||||||
|
icon-bg-color="bg-cyan-100"
|
||||||
|
:decimals="0"
|
||||||
|
:value="trendSummary?.value?.afterSaleCount || 0"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
trendSummary?.value?.afterSaleCount,
|
||||||
|
trendSummary?.reference?.afterSaleCount,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xl="8" :md="8" :sm="24" class="mb-4">
|
||||||
|
<SummaryCard
|
||||||
|
title="退款金额"
|
||||||
|
tooltip="在选定条件下,成功退款的商品金额之和"
|
||||||
|
icon="lucide:receipt"
|
||||||
|
icon-color="text-orange-500"
|
||||||
|
icon-bg-color="bg-orange-100"
|
||||||
|
prefix="¥"
|
||||||
|
:decimals="2"
|
||||||
|
:value="
|
||||||
|
Number(fenToYuan(trendSummary?.value?.afterSaleRefundPrice || 0))
|
||||||
|
"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
trendSummary?.value?.afterSaleRefundPrice,
|
||||||
|
trendSummary?.reference?.afterSaleRefundPrice,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 折线图 -->
|
||||||
|
<div v-loading="trendLoading">
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/** 商品统计折线图配置 */
|
||||||
|
export function getProductSummaryChartOptions(data: any[]): any {
|
||||||
|
// 处理数据:将金额从分转换为元
|
||||||
|
const processedData = data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
orderPayPrice: Number((item.orderPayPrice / 100).toFixed(2)),
|
||||||
|
afterSaleRefundPrice: Number((item.afterSaleRefundPrice / 100).toFixed(2)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataset: {
|
||||||
|
dimensions: [
|
||||||
|
'time',
|
||||||
|
'browseCount',
|
||||||
|
'browseUserCount',
|
||||||
|
'orderPayPrice',
|
||||||
|
'afterSaleRefundPrice',
|
||||||
|
],
|
||||||
|
source: processedData,
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
color: '#7F8B9C',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#F5F7F9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '数量',
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#7F8B9C',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#F5F7F9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user