feat:【mall 商城】交易统计【ele】100%
This commit is contained in:
@@ -219,7 +219,6 @@ async function handleExport() {
|
||||
"
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col :xl="8" :md="8" :sm="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="退款金额"
|
||||
|
||||
@@ -91,19 +91,8 @@ async function getTradeStatisticsList() {
|
||||
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||
});
|
||||
|
||||
// 处理数据
|
||||
// TODO @芋艿:搞到 getTradeTrendChartOptions 里;
|
||||
// TODO @芋艿:貌似和 vue3 + element-plus 数据不一致;
|
||||
const processedList = list.map((item) => ({
|
||||
...item,
|
||||
turnoverPrice: Number(fenToYuan(item.turnoverPrice)),
|
||||
orderPayPrice: Number(fenToYuan(item.orderPayPrice)),
|
||||
rechargePrice: Number(fenToYuan(item.rechargePrice)),
|
||||
expensePrice: Number(fenToYuan(item.expensePrice)),
|
||||
}));
|
||||
|
||||
// 渲染图表
|
||||
await renderEcharts(getTradeTrendChartOptions(processedList));
|
||||
await renderEcharts(getTradeTrendChartOptions(list));
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
@@ -120,8 +109,6 @@ async function handleExport() {
|
||||
});
|
||||
// 处理下载
|
||||
downloadFileFromBlobPart({ fileName: '交易状况.xlsx', source: data });
|
||||
} catch {
|
||||
// 用户取消导出
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
|
||||
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
/** 交易趋势折线图配置 */
|
||||
export function getTradeTrendChartOptions(
|
||||
data: MallTradeStatisticsApi.TradeTrendSummary[],
|
||||
): any {
|
||||
// 处理数据:将分转换为元
|
||||
const processedData = data.map((item) => ({
|
||||
...item,
|
||||
turnoverPrice: Number(fenToYuan(item.turnoverPrice)),
|
||||
orderPayPrice: Number(fenToYuan(item.orderPayPrice)),
|
||||
rechargePrice: Number(fenToYuan(item.rechargePrice)),
|
||||
expensePrice: Number(fenToYuan(item.expensePrice)),
|
||||
}));
|
||||
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: [
|
||||
'time',
|
||||
'date',
|
||||
'turnoverPrice',
|
||||
'orderPayPrice',
|
||||
'rechargePrice',
|
||||
'expensePrice',
|
||||
],
|
||||
source: data,
|
||||
source: processedData,
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
|
||||
@@ -153,7 +153,7 @@ export function useValueGridFormSchema(): VbenFormSchema[] {
|
||||
label: '属性项',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getPropertySimpleList(),
|
||||
api: getPropertySimpleList,
|
||||
placeholder: '请选择属性项',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
|
||||
@@ -228,7 +228,6 @@ async function handleExport() {
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xl="8" :md="8" :sm="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="退款金额"
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
|
||||
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
|
||||
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 TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||
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';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
const overviewItems = ref<AnalysisOverviewIconItem[]>();
|
||||
const summary =
|
||||
ref<MallDataComparisonResp<MallTradeStatisticsApi.TradeTrendSummary>>();
|
||||
const shortcutDateRangePicker = ref();
|
||||
const exportLoading = ref(false); // 导出的加载中
|
||||
const trendLoading = ref(true); // 交易状态加载中
|
||||
const loadOverview = () => {
|
||||
overviewItems.value = [
|
||||
{
|
||||
icon: 'fa-solid:yen-sign',
|
||||
title: '营业额',
|
||||
value: Number(fenToYuan(summary?.value?.value.turnoverPrice || 0)),
|
||||
tooltip: '商品支付金额、充值金额',
|
||||
iconColor: 'bg-blue-100',
|
||||
iconBgColor: 'text-blue-500',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.turnoverPrice,
|
||||
summary?.value?.reference?.turnoverPrice,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'fa-solid:shopping-cart',
|
||||
title: '商品支付金额',
|
||||
value: Number(fenToYuan(summary.value?.value?.orderPayPrice || 0)),
|
||||
tooltip:
|
||||
'用户购买商品的实际支付金额,包括微信支付、余额支付、支付宝支付、线下支付金额(拼团商品在成团之后计入,线下支付订单在后台确认支付后计入)',
|
||||
iconColor: 'bg-purple-100',
|
||||
iconBgColor: 'text-purple-500',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.orderPayPrice,
|
||||
summary?.value?.reference?.orderPayPrice,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'fa-solid:money-check-alt',
|
||||
title: '充值金额',
|
||||
value: Number(fenToYuan(summary.value?.value?.rechargePrice || 0)),
|
||||
tooltip: '用户成功充值的金额',
|
||||
iconColor: 'bg-yellow-100',
|
||||
iconBgColor: 'text-yellow-500',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.rechargePrice,
|
||||
summary?.value?.reference?.rechargePrice,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'ep:warning-filled',
|
||||
title: '支出金额',
|
||||
value: Number(fenToYuan(summary.value?.value?.expensePrice || 0)),
|
||||
tooltip: '余额支付金额、支付佣金金额、商品退款金额',
|
||||
iconColor: 'bg-green-100',
|
||||
iconBgColor: 'text-green-500',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.expensePrice,
|
||||
summary?.value?.reference?.expensePrice,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'fa-solid:wallet',
|
||||
title: '余额支付金额',
|
||||
value: Number(fenToYuan(summary.value?.value?.walletPayPrice || 0)),
|
||||
tooltip: '余额支付金额、支付佣金金额、商品退款金额',
|
||||
iconColor: 'bg-cyan-100',
|
||||
iconBgColor: 'text-cyan-500',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.walletPayPrice,
|
||||
summary?.value?.reference?.walletPayPrice,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'fa-solid:award',
|
||||
title: '支付佣金金额',
|
||||
value: Number(
|
||||
fenToYuan(summary.value?.value?.brokerageSettlementPrice || 0),
|
||||
),
|
||||
tooltip: '后台给推广员支付的推广佣金,以实际支付为准',
|
||||
iconColor: 'bg-yellow-100',
|
||||
iconBgColor: 'text-yellow-500',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.brokerageSettlementPrice,
|
||||
summary?.value?.reference?.brokerageSettlementPrice,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'fa-solid:times-circle',
|
||||
title: '商品退款金额',
|
||||
value: Number(fenToYuan(summary.value?.value?.afterSaleRefundPrice || 0)),
|
||||
tooltip: '用户成功退款的商品金额',
|
||||
iconColor: 'bg-blue-100',
|
||||
iconBgColor: 'text-blue-500',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.afterSaleRefundPrice,
|
||||
summary?.value?.reference?.afterSaleRefundPrice,
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
// 导出的二次确认
|
||||
await 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-access:code="['statistics:trade:export']"
|
||||
>
|
||||
<IconifyIcon icon="ep:download" class="mr-1" />导出
|
||||
</el-button>
|
||||
</ShortcutDateRangePicker>
|
||||
</template>
|
||||
<AnalysisOverviewIcon v-model:model-value="overviewItems" />
|
||||
<EchartsUI height="500px" ref="chartRef" />
|
||||
</AnalysisChartCard>
|
||||
</template>
|
||||
@@ -1,93 +1,120 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
|
||||
import type { DataComparisonRespVO } from '#/api/mall/statistics/common';
|
||||
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
|
||||
import type { AnalysisOverviewTradeItem } from '#/views/mall/home/components/data';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { calculateRelativeRate, fenToYuan } from '@vben/utils';
|
||||
import { DocAlert, Page, StatisticCard } from '@vben/common-ui';
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { ElCol, ElRow } from 'element-plus';
|
||||
|
||||
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||
import analysisTradeOverview from '#/views/mall/home/components/analysis-trade-overview.vue';
|
||||
|
||||
import TradeTransactionCard from './components/trade-transaction-card.vue';
|
||||
import TradeTrendCard from './modules/trend-card.vue';
|
||||
|
||||
const overviewItems = ref<AnalysisOverviewTradeItem[]>();
|
||||
/** 交易统计 */
|
||||
defineOptions({ name: 'TradeStatistics' });
|
||||
|
||||
const loading = ref(true); // 加载中
|
||||
const summary =
|
||||
ref<MallDataComparisonResp<MallTradeStatisticsApi.TradeSummary>>();
|
||||
const loadOverview = () => {
|
||||
overviewItems.value = [
|
||||
{
|
||||
title: '昨日订单数量',
|
||||
value: summary.value?.value?.yesterdayOrderCount || 0,
|
||||
tooltip: '昨日订单数量',
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.yesterdayOrderCount,
|
||||
summary.value?.reference?.yesterdayOrderCount,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '本月订单数量',
|
||||
value: summary.value?.value?.monthOrderCount || 0,
|
||||
tooltip: '本月订单数量',
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.monthOrderCount,
|
||||
summary.value?.reference?.monthOrderCount,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '昨日支付金额',
|
||||
value: Number(fenToYuan(summary.value?.value?.yesterdayPayPrice || 0)),
|
||||
tooltip: '昨日支付金额',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.yesterdayPayPrice,
|
||||
summary.value?.reference?.yesterdayPayPrice,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '本月支付金额',
|
||||
value: summary.value?.value?.monthPayPrice || 0,
|
||||
tooltip: '本月支付金额',
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
percent: calculateRelativeRate(
|
||||
summary?.value?.value?.monthPayPrice,
|
||||
summary.value?.reference?.monthPayPrice,
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
ref<DataComparisonRespVO<MallTradeStatisticsApi.TradeSummary>>(); // 交易统计数据
|
||||
|
||||
/** 计算环比百分比 */
|
||||
function 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 getTradeStatisticsSummary = async () => {
|
||||
async function getTradeStatisticsSummary() {
|
||||
summary.value = await TradeStatisticsApi.getTradeStatisticsSummary();
|
||||
};
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
await getTradeStatisticsSummary();
|
||||
loadOverview();
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【统计】会员、商品、交易统计"
|
||||
url="https://doc.iocoder.cn/mall/statistics/"
|
||||
/>
|
||||
<!-- 统计值 -->
|
||||
<div class="mb-4 mt-5 w-full md:flex">
|
||||
<analysisTradeOverview
|
||||
v-model:model-value="overviewItems"
|
||||
class="mt-5 md:mr-4 md:mt-0 md:w-full"
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【统计】会员、商品、交易统计"
|
||||
url="https://doc.iocoder.cn/mall/statistics/"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4 mt-5 w-full md:flex">
|
||||
<TradeTransactionCard class="mt-5 md:mr-4 md:mt-0 md:w-full" />
|
||||
</template>
|
||||
|
||||
<!-- 交易概览卡片 -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :sm="6" :xs="12">
|
||||
<StatisticCard
|
||||
tooltip="昨日订单数量"
|
||||
title="昨日订单数量"
|
||||
:value="summary?.value?.yesterdayOrderCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
summary?.value?.yesterdayOrderCount,
|
||||
summary?.reference?.yesterdayOrderCount,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :sm="6" :xs="12">
|
||||
<StatisticCard
|
||||
tooltip="本月订单数量"
|
||||
title="本月订单数量"
|
||||
:value="summary?.value?.monthOrderCount || 0"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
summary?.value?.monthOrderCount,
|
||||
summary?.reference?.monthOrderCount,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :sm="6" :xs="12">
|
||||
<StatisticCard
|
||||
tooltip="昨日支付金额"
|
||||
title="昨日支付金额"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(summary?.value?.yesterdayPayPrice || 0))"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
summary?.value?.yesterdayPayPrice,
|
||||
summary?.reference?.yesterdayPayPrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :sm="6" :xs="12">
|
||||
<StatisticCard
|
||||
tooltip="本月支付金额"
|
||||
title="本月支付金额"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(summary?.value?.monthPayPrice || 0))"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
summary?.value?.monthPayPrice,
|
||||
summary?.reference?.monthPayPrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 交易趋势 -->
|
||||
<TradeTrendCard />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
<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 { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
|
||||
|
||||
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 { ElButton, ElCard, ElCol, ElRow } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||
|
||||
import { getTradeTrendChartOptions } from './trend-chart-options';
|
||||
|
||||
/** 交易趋势 */
|
||||
defineOptions({ name: 'TradeTrendCard' });
|
||||
|
||||
const trendLoading = ref(true); // 交易状态加载中
|
||||
const exportLoading = ref(false); // 导出的加载中
|
||||
const trendSummary =
|
||||
ref<DataComparisonRespVO<MallTradeStatisticsApi.TradeTrendSummary>>(); // 交易状况统计数据
|
||||
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) {
|
||||
getTradeTrendData();
|
||||
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,
|
||||
];
|
||||
|
||||
// 查询数据
|
||||
getTradeTrendData();
|
||||
};
|
||||
|
||||
/** 处理交易状况查询 */
|
||||
async function getTradeTrendData() {
|
||||
trendLoading.value = true;
|
||||
try {
|
||||
await Promise.all([getTradeStatisticsAnalyse(), getTradeStatisticsList()]);
|
||||
} finally {
|
||||
trendLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询交易状况数据统计 */
|
||||
async function getTradeStatisticsAnalyse() {
|
||||
trendSummary.value = await TradeStatisticsApi.getTradeStatisticsAnalyse({
|
||||
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询交易状况数据列表 */
|
||||
async function getTradeStatisticsList() {
|
||||
const list = await TradeStatisticsApi.getTradeStatisticsList({
|
||||
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||
});
|
||||
|
||||
// 渲染图表
|
||||
await renderEcharts(getTradeTrendChartOptions(list));
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
async function handleExport() {
|
||||
try {
|
||||
// 导出的二次确认
|
||||
await confirm({
|
||||
content: '确认导出交易状况数据吗?',
|
||||
});
|
||||
// 发起导出
|
||||
exportLoading.value = true;
|
||||
const data = await TradeStatisticsApi.exportTradeStatisticsExcel({
|
||||
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||
});
|
||||
// 处理下载
|
||||
downloadFileFromBlobPart({ fileName: '交易状况.xlsx', source: data });
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard :bordered="false" shadow="never" class="h-full">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-base font-medium">交易状况</span>
|
||||
<!-- 查询条件 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<ShortcutDateRangePicker @change="handleDateRangeChange">
|
||||
<ElButton class="ml-4" @click="handleExport" :loading="exportLoading">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:download" />
|
||||
</template>
|
||||
导出
|
||||
</ElButton>
|
||||
</ShortcutDateRangePicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 统计值 -->
|
||||
<ElRow :gutter="16" class="mb-4">
|
||||
<ElCol :md="6" :sm="12" :xs="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="营业额"
|
||||
tooltip="商品支付金额、充值金额"
|
||||
icon="lucide:banknote"
|
||||
icon-color="text-blue-500"
|
||||
icon-bg-color="bg-blue-100"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(trendSummary?.value?.turnoverPrice || 0))"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.turnoverPrice,
|
||||
trendSummary?.reference?.turnoverPrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :md="6" :sm="12" :xs="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="商品支付金额"
|
||||
tooltip="用户购买商品的实际支付金额,包括微信支付、余额支付、支付宝支付、线下支付金额(拼团商品在成团之后计入,线下支付订单在后台确认支付后计入)"
|
||||
icon="lucide:shopping-cart"
|
||||
icon-color="text-purple-500"
|
||||
icon-bg-color="bg-purple-100"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(trendSummary?.value?.orderPayPrice || 0))"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.orderPayPrice,
|
||||
trendSummary?.reference?.orderPayPrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :md="6" :sm="12" :xs="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="充值金额"
|
||||
tooltip="用户成功充值的金额"
|
||||
icon="lucide:credit-card"
|
||||
icon-color="text-yellow-500"
|
||||
icon-bg-color="bg-yellow-100"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(trendSummary?.value?.rechargePrice || 0))"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.rechargePrice,
|
||||
trendSummary?.reference?.rechargePrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :md="6" :sm="12" :xs="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="支出金额"
|
||||
tooltip="余额支付金额、支付佣金金额、商品退款金额"
|
||||
icon="lucide:trending-down"
|
||||
icon-color="text-green-500"
|
||||
icon-bg-color="bg-green-100"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(trendSummary?.value?.expensePrice || 0))"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.expensePrice,
|
||||
trendSummary?.reference?.expensePrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :md="6" :sm="12" :xs="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="余额支付金额"
|
||||
tooltip="用户下单时使用余额实际支付的金额"
|
||||
icon="lucide:wallet"
|
||||
icon-color="text-cyan-500"
|
||||
icon-bg-color="bg-cyan-100"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(trendSummary?.value?.walletPayPrice || 0))"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.walletPayPrice,
|
||||
trendSummary?.reference?.walletPayPrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :md="6" :sm="12" :xs="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="支付佣金金额"
|
||||
tooltip="后台给推广员支付的推广佣金,以实际支付为准"
|
||||
icon="lucide:gift"
|
||||
icon-color="text-orange-500"
|
||||
icon-bg-color="bg-orange-100"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="
|
||||
Number(
|
||||
fenToYuan(trendSummary?.value?.brokerageSettlementPrice || 0),
|
||||
)
|
||||
"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.brokerageSettlementPrice,
|
||||
trendSummary?.reference?.brokerageSettlementPrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :md="6" :sm="12" :xs="24" class="mb-4">
|
||||
<SummaryCard
|
||||
title="商品退款金额"
|
||||
tooltip="用户成功退款的商品金额"
|
||||
icon="lucide:undo-2"
|
||||
icon-color="text-red-500"
|
||||
icon-bg-color="bg-red-100"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="
|
||||
Number(fenToYuan(trendSummary?.value?.afterSaleRefundPrice || 0))
|
||||
"
|
||||
:percent="
|
||||
calculateRelativeRate(
|
||||
trendSummary?.value?.afterSaleRefundPrice,
|
||||
trendSummary?.reference?.afterSaleRefundPrice,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 折线图 -->
|
||||
<div v-loading="trendLoading">
|
||||
<EchartsUI ref="chartRef" />
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
|
||||
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
/** 交易趋势折线图配置 */
|
||||
export function getTradeTrendChartOptions(
|
||||
data: MallTradeStatisticsApi.TradeTrendSummary[],
|
||||
): any {
|
||||
// 处理数据:将分转换为元
|
||||
const processedData = data.map((item) => ({
|
||||
...item,
|
||||
turnoverPrice: Number(fenToYuan(item.turnoverPrice)),
|
||||
orderPayPrice: Number(fenToYuan(item.orderPayPrice)),
|
||||
rechargePrice: Number(fenToYuan(item.rechargePrice)),
|
||||
expensePrice: Number(fenToYuan(item.expensePrice)),
|
||||
}));
|
||||
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: [
|
||||
'date',
|
||||
'turnoverPrice',
|
||||
'orderPayPrice',
|
||||
'rechargePrice',
|
||||
'expensePrice',
|
||||
],
|
||||
source: processedData,
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
top: 80,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '营业额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#1890FF' },
|
||||
},
|
||||
{
|
||||
name: '商品支付金额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#722ED1' },
|
||||
},
|
||||
{
|
||||
name: '充值金额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#FAAD14' },
|
||||
},
|
||||
{
|
||||
name: '支出金额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#52C41A' },
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
// 数据区域缩放
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // Y轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: {
|
||||
show: true,
|
||||
name: '交易状况',
|
||||
}, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
padding: [5, 10],
|
||||
formatter(params: any) {
|
||||
let result = `<div><strong>${params[0].data.time}</strong></div>`;
|
||||
params.forEach((item: any) => {
|
||||
result += `<div style="margin: 4px 0;">
|
||||
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${item.color};"></span>
|
||||
${item.seriesName}: ¥${item.data[item.dimensionNames[item.encode.y[0]]]}
|
||||
</div>`;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: '¥{value}',
|
||||
color: '#7F8B9C',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#F5F7F9',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user