feat: 添加交易状况组件并优化统计数据展示,支持环比增长率显示

pull/175/head
lrl 2025-07-18 15:06:27 +08:00
parent a442eab9ea
commit 27a7e84def
8 changed files with 362 additions and 54 deletions

View File

@ -1,6 +1,6 @@
import type { MallDataComparisonResp } from './common'; import type { MallDataComparisonResp } from './common';
import { formatDate, formatDate2 } from '@vben/utils'; import { formatDate2 } from '@vben/utils';
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
@ -15,7 +15,7 @@ export namespace MallTradeStatisticsApi {
/** 交易状况 Request */ /** 交易状况 Request */
export interface TradeTrendReq { export interface TradeTrendReq {
times: [Date, Date]; times: Date[];
} }
/** 交易状况统计 Response */ /** 交易状况统计 Response */
@ -64,8 +64,11 @@ export namespace MallTradeStatisticsApi {
/** 时间参数需要格式化, 确保接口能识别 */ /** 时间参数需要格式化, 确保接口能识别 */
const formatDateParam = (params: MallTradeStatisticsApi.TradeTrendReq) => { const formatDateParam = (params: MallTradeStatisticsApi.TradeTrendReq) => {
return { return {
times: [formatDate(params.times[0]), formatDate(params.times[1])], times: [
} as MallTradeStatisticsApi.TradeTrendReq; formatDate2(params.times[0] || new Date()),
formatDate2(params.times[1] || new Date()),
],
};
}; };
/** 查询交易统计 */ /** 查询交易统计 */

View File

@ -120,6 +120,7 @@ const loadOverview = () => {
totalTitle: '昨日数据', totalTitle: '昨日数据',
totalValue: orderComparison.value?.reference?.orderPayPrice || 0, totalValue: orderComparison.value?.reference?.orderPayPrice || 0,
value: orderComparison.value?.orderPayPrice || 0, value: orderComparison.value?.orderPayPrice || 0,
showGrowthRate: true,
}, },
{ {
icon: SvgCakeIcon, icon: SvgCakeIcon,
@ -127,6 +128,7 @@ const loadOverview = () => {
totalTitle: '总访问量', totalTitle: '总访问量',
totalValue: userComparison.value?.reference?.visitUserCount || 0, totalValue: userComparison.value?.reference?.visitUserCount || 0,
value: userComparison.value?.visitUserCount || 0, value: userComparison.value?.visitUserCount || 0,
showGrowthRate: true,
}, },
{ {
icon: SvgDownloadIcon, icon: SvgDownloadIcon,
@ -134,6 +136,7 @@ const loadOverview = () => {
totalTitle: '总订单量', totalTitle: '总订单量',
totalValue: orderComparison.value?.orderPayCount || 0, totalValue: orderComparison.value?.orderPayCount || 0,
value: orderComparison.value?.reference?.orderPayCount || 0, value: orderComparison.value?.reference?.orderPayCount || 0,
//
}, },
{ {
icon: SvgBellIcon, icon: SvgBellIcon,
@ -141,6 +144,7 @@ const loadOverview = () => {
totalTitle: '总会员注册量', totalTitle: '总会员注册量',
totalValue: userComparison.value?.registerUserCount || 0, totalValue: userComparison.value?.registerUserCount || 0,
value: userComparison.value?.reference?.registerUserCount || 0, value: userComparison.value?.reference?.registerUserCount || 0,
showGrowthRate: true,
}, },
]; ];
}; };

View File

@ -6,12 +6,7 @@ import type { MallMemberStatisticsApi } from '#/api/mall/statistics/member'; //
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { AnalysisOverview, DocAlert, Page } from '@vben/common-ui'; import { AnalysisOverview, DocAlert, Page } from '@vben/common-ui';
import { import { SvgCakeIcon, SvgCardIcon } from '@vben/icons';
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import * as MemberStatisticsApi from '#/api/mall/statistics/member'; // import * as MemberStatisticsApi from '#/api/mall/statistics/member'; //
import MemberFunnelCard from '#/views/mall/home/components/member-funnel-card.vue'; import MemberFunnelCard from '#/views/mall/home/components/member-funnel-card.vue';
@ -27,22 +22,22 @@ const loadOverview = async () => {
summary.value = await MemberStatisticsApi.getMemberSummary(); summary.value = await MemberStatisticsApi.getMemberSummary();
overviewItems.value = [ overviewItems.value = [
{ {
icon: SvgCardIcon, icon: SvgCakeIcon, // -
title: '累计会员数', title: '累计会员数',
value: summary.value?.userCount || 0, value: summary.value?.userCount || 0,
}, },
{ {
icon: SvgCakeIcon, icon: SvgCardIcon, // -
title: '累计充值人数', title: '累计充值人数',
value: summary.value?.rechargeUserCount || 0, value: summary.value?.rechargeUserCount || 0,
}, },
{ {
icon: SvgDownloadIcon, icon: SvgCardIcon, // -
title: '累计充值金额', title: '累计充值金额',
value: summary.value?.rechargePrice || 0, value: summary.value?.rechargePrice || 0,
}, },
{ {
icon: SvgBellIcon, icon: SvgCakeIcon, // -
title: '今日会员注册量', title: '今日会员注册量',
value: summary.value?.expensePrice || 0, value: summary.value?.expensePrice || 0,
}, },

View File

@ -8,12 +8,7 @@ import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { AnalysisChartCard, AnalysisOverview, confirm } from '@vben/common-ui'; import { AnalysisChartCard, AnalysisOverview, confirm } from '@vben/common-ui';
import { import { SvgCakeIcon, SvgCardIcon, SvgEyeIcon } from '@vben/icons';
SvgBellIcon,
SvgCakeIcon,
SvgDownloadIcon,
SvgEyeIcon,
} from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { import {
downloadFileFromBlobPart, downloadFileFromBlobPart,
@ -222,10 +217,11 @@ const loadOverview = () => {
icon: SvgEyeIcon, icon: SvgEyeIcon,
title: '商品浏览量', title: '商品浏览量',
totalTitle: '昨日数据', totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.browseCount, totalValue: trendSummary.value?.reference?.browseCount || 0,
value: trendSummary.value?.value?.browseCount || 0, value: trendSummary.value?.value?.browseCount || 0,
tooltip: tooltip:
'在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次', '在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次',
showGrowthRate: true,
}, },
{ {
icon: SvgCakeIcon, icon: SvgCakeIcon,
@ -235,38 +231,43 @@ const loadOverview = () => {
value: trendSummary.value?.value?.browseUserCount || 0, value: trendSummary.value?.value?.browseUserCount || 0,
tooltip: tooltip:
'在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个', '在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个',
showGrowthRate: true,
}, },
{ {
icon: SvgDownloadIcon, icon: SvgCakeIcon,
title: '支付件数', title: '支付件数',
totalTitle: '昨日数据', totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.orderPayCount || 0, totalValue: trendSummary.value?.reference?.orderPayCount || 0,
value: trendSummary.value?.value?.orderPayCount || 0, value: trendSummary.value?.value?.orderPayCount || 0,
tooltip: '在选定条件下,成功付款订单的商品件数之和', tooltip: '在选定条件下,成功付款订单的商品件数之和',
showGrowthRate: true,
}, },
{ {
icon: SvgBellIcon, icon: SvgCardIcon,
title: '支付金额', title: '支付金额',
totalTitle: '昨日数据', totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleCount || 0, totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
value: trendSummary.value?.value?.orderPayPrice || 0, value: trendSummary.value?.value?.orderPayPrice || 0,
tooltip: '在选定条件下,成功付款订单的商品金额之和', tooltip: '在选定条件下,成功付款订单的商品金额之和',
showGrowthRate: true,
}, },
{ {
icon: SvgBellIcon, icon: SvgCakeIcon,
title: '退款件数', title: '退款件数',
totalTitle: '昨日数据', totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleCount || 0, totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
value: trendSummary.value?.value?.afterSaleCount || 0, value: trendSummary.value?.value?.afterSaleCount || 0,
tooltip: '在选定条件下,成功退款的商品件数之和', tooltip: '在选定条件下,成功退款的商品件数之和',
showGrowthRate: true,
}, },
{ {
icon: SvgBellIcon, icon: SvgCardIcon,
title: '退款金额', title: '退款金额',
totalTitle: '昨日数据', totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleRefundPrice || 0, totalValue: trendSummary.value?.reference?.afterSaleRefundPrice || 0,
value: trendSummary.value?.value?.afterSaleRefundPrice || 0, value: trendSummary.value?.value?.afterSaleRefundPrice || 0,
tooltip: '在选定条件下,成功退款的商品金额之和', tooltip: '在选定条件下,成功退款的商品金额之和',
showGrowthRate: true,
}, },
]; ];
}; };

View File

@ -0,0 +1,235 @@
<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 { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
import { reactive, ref } from 'vue';
import { AnalysisChartCard, AnalysisOverview } from '@vben/common-ui';
import { SvgCakeIcon, SvgCardIcon } from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import {
downloadFileFromBlobPart,
fenToYuan,
formatDate,
isSameDay,
} from '@vben/utils';
import dayjs from 'dayjs';
import { ElMessageBox } from 'element-plus';
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const overviewItems = ref<AnalysisOverviewItem[]>();
const summary =
ref<MallDataComparisonResp<MallTradeStatisticsApi.TradeTrendSummary>>();
const shortcutDateRangePicker = ref();
const exportLoading = ref(false); //
const trendLoading = ref(true); //
const loadOverview = () => {
overviewItems.value = [
{
icon: SvgCakeIcon,
title: '营业额',
value: summary?.value?.value.turnoverPrice || 0,
tooltip: '商品支付金额、充值金额',
totalValue: summary?.value?.reference?.turnoverPrice || 0,
showGrowthRate: true,
},
{
icon: SvgCakeIcon,
title: '商品支付金额',
value: summary.value?.value?.orderPayPrice || 0,
tooltip:
'用户购买商品的实际支付金额,包括微信支付、余额支付、支付宝支付、线下支付金额(拼团商品在成团之后计入,线下支付订单在后台确认支付后计入)',
totalValue: summary?.value?.reference?.orderPayPrice || 0,
showGrowthRate: true,
},
{
icon: SvgCardIcon,
title: '充值金额',
value: summary.value?.value?.rechargePrice || 0,
tooltip: '用户成功充值的金额',
totalValue: summary?.value?.reference?.rechargePrice || 0,
showGrowthRate: true,
},
{
icon: SvgCardIcon,
title: '支出金额',
value: summary.value?.value?.expensePrice || 0,
tooltip: '余额支付金额、支付佣金金额、商品退款金额',
totalValue: summary?.value?.reference?.expensePrice || 0,
showGrowthRate: true,
},
{
icon: SvgCardIcon,
title: '余额支付金额',
value: summary.value?.value?.walletPayPrice || 0,
tooltip: '余额支付金额、支付佣金金额、商品退款金额',
totalValue: summary?.value?.reference?.walletPayPrice || 0,
showGrowthRate: true,
},
{
icon: SvgCardIcon,
title: '支付佣金金额',
value: summary.value?.value?.brokerageSettlementPrice || 0,
tooltip: '后台给推广员支付的推广佣金,以实际支付为准',
totalValue: summary?.value?.reference?.brokerageSettlementPrice || 0,
showGrowthRate: true,
},
{
icon: SvgCardIcon,
title: '商品退款金额',
value: summary.value?.value?.afterSaleRefundPrice || 0,
tooltip: '用户成功退款的商品金额',
totalValue: summary?.value?.reference?.afterSaleRefundPrice || 0,
showGrowthRate: true,
},
];
};
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await ElMessageBox.confirm('确定要导出交易状况吗?');
//
exportLoading.value = true;
const times = shortcutDateRangePicker.value.times;
const data = await TradeStatisticsApi.exportTradeStatisticsExcel({ times });
downloadFileFromBlobPart({ fileName: '交易状况.xls', source: data });
} finally {
exportLoading.value = false;
}
};
const getTradeTrendData = 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([getTradeStatisticsAnalyse(), getTradeStatisticsList()]);
trendLoading.value = false;
loadOverview();
renderEcharts(lineChartOptions as any);
};
/** 查询交易状况数据统计 */
const getTradeStatisticsAnalyse = async () => {
const times = shortcutDateRangePicker.value.times;
summary.value = await TradeStatisticsApi.getTradeStatisticsAnalyse({
times,
});
};
/** 查询交易状况数据列表 */
const getTradeStatisticsList = async () => {
//
const times = shortcutDateRangePicker.value.times;
const list = await TradeStatisticsApi.getTradeStatisticsList({ times });
//
for (const item of list) {
item.turnoverPrice = Number(fenToYuan(item.turnoverPrice));
item.orderPayPrice = Number(fenToYuan(item.orderPayPrice));
item.rechargePrice = Number(fenToYuan(item.rechargePrice));
item.expensePrice = Number(fenToYuan(item.expensePrice));
}
// Echarts
if (lineChartOptions.dataset && lineChartOptions.dataset.source) {
lineChartOptions.dataset.source = list;
}
};
/** 折线图配置 */
const lineChartOptions = reactive({
dataset: {
dimensions: [
'date',
'turnoverPrice',
'orderPayPrice',
'rechargePrice',
'expensePrice',
],
source: [] as MallTradeStatisticsApi.TradeTrendSummary[],
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 80,
containLabel: true,
},
legend: {
top: 50,
},
series: [
{ name: '营业额', type: 'line', smooth: true },
{ name: '商品支付金额', type: 'line', smooth: true },
{ name: '充值金额', type: 'line', smooth: true },
{ name: '支出金额', type: 'line', smooth: true },
],
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: false,
axisTick: {
show: false,
},
},
yAxis: {
axisTick: {
show: false,
},
},
});
</script>
<template>
<AnalysisChartCard title="交易状况">
<template #header-suffix>
<!-- 查询条件 -->
<ShortcutDateRangePicker
ref="shortcutDateRangePicker"
@change="getTradeTrendData"
>
<el-button
class="ml-4"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['statistics:trade:export']"
>
<Icon icon="ep:download" class="mr-1" />导出
</el-button>
</ShortcutDateRangePicker>
</template>
<AnalysisOverview v-model:model-value="overviewItems" />
<EchartsUI height="500px" ref="chartRef" />
</AnalysisChartCard>
</template>

View File

@ -7,43 +7,48 @@ import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { AnalysisOverview, DocAlert, Page } from '@vben/common-ui'; import { AnalysisOverview, DocAlert, Page } from '@vben/common-ui';
import { import { SvgCakeIcon, SvgCardIcon } from '@vben/icons';
SvgBellIcon,
SvgCakeIcon,
SvgDownloadIcon,
SvgEyeIcon,
} from '@vben/icons';
import * as TradeStatisticsApi from '#/api/mall/statistics/trade'; import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
import TradeTransactionCard from './components/trade-transaction-card.vue';
const overviewItems = ref<AnalysisOverviewItem[]>(); const overviewItems = ref<AnalysisOverviewItem[]>();
const summary = const summary =
ref<MallDataComparisonResp<MallTradeStatisticsApi.TradeSummary>>(); ref<MallDataComparisonResp<MallTradeStatisticsApi.TradeSummary>>();
const loadOverview = () => { const loadOverview = () => {
overviewItems.value = [ overviewItems.value = [
{ {
icon: SvgEyeIcon, icon: SvgCakeIcon,
title: '昨日订单数量', title: '昨日订单数量',
value: summary.value?.value?.yesterdayOrderCount || 0, value: summary.value?.value?.yesterdayOrderCount || 0,
tooltip: '昨日订单数量', tooltip: '昨日订单数量',
totalValue: summary?.value?.reference?.yesterdayOrderCount,
showGrowthRate: true,
}, },
{ {
icon: SvgCakeIcon, icon: SvgCakeIcon,
title: '本月订单数量', title: '本月订单数量',
value: summary.value?.value?.monthOrderCount || 0, value: summary.value?.value?.monthOrderCount || 0,
tooltip: '本月订单数量', tooltip: '本月订单数量',
totalValue: summary?.value?.reference?.monthOrderCount,
showGrowthRate: true,
}, },
{ {
icon: SvgDownloadIcon, icon: SvgCardIcon,
title: '昨日支付金额', title: '昨日支付金额',
value: summary.value?.value?.yesterdayPayPrice || 0, value: summary.value?.value?.yesterdayPayPrice || 0,
tooltip: '昨日支付金额', tooltip: '昨日支付金额',
totalValue: summary?.value?.reference?.yesterdayPayPrice,
showGrowthRate: true,
}, },
{ {
icon: SvgBellIcon, icon: SvgCardIcon,
title: '本月支付金额', title: '本月支付金额',
value: summary.value?.value?.monthPayPrice || 0, value: summary.value?.value?.monthPayPrice || 0,
tooltip: '本月支付金额', tooltip: '本月支付金额',
totalValue: summary?.value?.reference?.monthPayPrice,
showGrowthRate: true,
}, },
]; ];
}; };
@ -67,9 +72,14 @@ onMounted(async () => {
url="https://doc.iocoder.cn/mall/statistics/" url="https://doc.iocoder.cn/mall/statistics/"
/> />
<!-- 统计值 --> <!-- 统计值 -->
<div class="mb-4 mt-5 w-full md:flex">
<AnalysisOverview <AnalysisOverview
v-model:model-value="overviewItems" v-model:model-value="overviewItems"
class="mt-5 md:mr-4 md:mt-0 md:w-full" class="mt-5 md:mr-4 md:mt-0 md:w-full"
/> />
</div>
<div class="mb-4 mt-5 w-full md:flex">
<TradeTransactionCard class="mt-5 md:mr-4 md:mt-0 md:w-full" />
</div>
</Page> </Page>
</template> </template>

View File

@ -52,6 +52,24 @@ const gridColumnsClass = computed(() => {
'lg:grid-cols-6': colNum === 6, 'lg:grid-cols-6': colNum === 6,
}; };
}); });
//
const calculateGrowthRate = (
currentValue: number,
previousValue: number,
): { isPositive: boolean; rate: number } => {
if (previousValue === 0) {
return { rate: currentValue > 0 ? 100 : 0, isPositive: currentValue >= 0 };
}
const rate = ((currentValue - previousValue) / previousValue) * 100;
return { rate: Math.abs(rate), isPositive: rate >= 0 };
};
//
const formatGrowthRate = (rate: number): string => {
return rate.toFixed(1);
};
</script> </script>
<template> <template>
@ -60,6 +78,7 @@ const gridColumnsClass = computed(() => {
<Card :title="item.title" class="w-full"> <Card :title="item.title" class="w-full">
<CardHeader> <CardHeader>
<CardTitle class="text-xl"> <CardTitle class="text-xl">
<div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<span>{{ item.title }}</span> <span>{{ item.title }}</span>
<span v-if="item.tooltip" class="ml-1 inline-block"> <span v-if="item.tooltip" class="ml-1 inline-block">
@ -77,6 +96,45 @@ const gridColumnsClass = computed(() => {
</TooltipProvider> </TooltipProvider>
</span> </span>
</div> </div>
<!-- 环比增长率显示在右上角 -->
<div
v-if="item.showGrowthRate && item.totalValue !== undefined"
class="flex items-center space-x-1"
>
<VbenIcon
:icon="
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'lucide:trending-up'
: 'lucide:trending-down'
"
class="size-4"
:class="[
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'text-green-500'
: 'text-red-500',
]"
/>
<span
class="text-sm font-medium"
:class="[
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'text-green-500'
: 'text-red-500',
]"
>
{{
calculateGrowthRate(item.value, item.totalValue).isPositive
? '+'
: '-'
}}{{
formatGrowthRate(
calculateGrowthRate(item.value, item.totalValue).rate,
)
}}%
</span>
</div>
</div>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@ -7,6 +7,8 @@ interface AnalysisOverviewItem {
totalValue?: number; totalValue?: number;
value: number; value: number;
tooltip?: string; tooltip?: string;
// 环比增长相关字段
showGrowthRate?: boolean; // 是否显示环比增长率默认为false
} }
interface WorkbenchProjectItem { interface WorkbenchProjectItem {