feat:【mall 商城】交易统计、商品统计【antd】10%:初始化
This commit is contained in:
@@ -1,32 +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 { Button } from 'ant-design-vue';
|
import ProductRankCard from './modules/product-rank-card.vue';
|
||||||
|
import ProductSummaryCard from './modules/product-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/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<div class="flex flex-col gap-4">
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<!-- 商品概览 -->
|
||||||
>
|
<ProductSummaryCard />
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<!-- 商品排行 -->
|
||||||
</Button>
|
<ProductRankCard />
|
||||||
<br />
|
</div>
|
||||||
<Button
|
|
||||||
type="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 贡献给我们!
|
|
||||||
</Button>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
|
||||||
|
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { formatToFraction } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Card, Image, Pagination, Table } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 loading = ref(false); // 列表的加载中
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const list = ref<MallProductStatisticsApi.ProductStatistics[]>([]); // 列表的数据
|
||||||
|
const shortcutDateRangePicker = ref();
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
times: undefined as string[] | undefined,
|
||||||
|
sortingFields: [] as any[],
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 格式化:访客-支付转化率 */
|
||||||
|
const formatConvertRate = (value: number) => {
|
||||||
|
return `${value || 0}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 格式化金额(分转元)*/
|
||||||
|
const formatPrice = (value: number) => {
|
||||||
|
return formatToFraction(value / 100, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理排序变化 */
|
||||||
|
const handleSortChange = (sorter: any) => {
|
||||||
|
queryParams.sortingFields =
|
||||||
|
sorter && sorter.field
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
field: sorter.field,
|
||||||
|
order: sorter.order === 'ascend' ? 'ASC' : 'DESC',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
getSpuList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理日期范围变化 */
|
||||||
|
const handleDateRangeChange = (times?: [Dayjs, Dayjs]) => {
|
||||||
|
queryParams.times = times
|
||||||
|
? [
|
||||||
|
times[0].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
times[1].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
getSpuList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询商品列表 */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分页变化 */
|
||||||
|
const handlePaginationChange = (page: number, pageSize: number) => {
|
||||||
|
queryParams.pageNo = page;
|
||||||
|
queryParams.pageSize = pageSize;
|
||||||
|
getSpuList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '商品 ID',
|
||||||
|
dataIndex: 'spuId',
|
||||||
|
key: 'spuId',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商品图片',
|
||||||
|
dataIndex: 'spuPicUrl',
|
||||||
|
key: 'spuPicUrl',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商品名称',
|
||||||
|
dataIndex: 'spuName',
|
||||||
|
key: 'spuName',
|
||||||
|
ellipsis: true,
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '浏览量',
|
||||||
|
dataIndex: 'browseCount',
|
||||||
|
key: 'browseCount',
|
||||||
|
sorter: true,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '访客数',
|
||||||
|
dataIndex: 'browseUserCount',
|
||||||
|
key: 'browseUserCount',
|
||||||
|
sorter: true,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '加购件数',
|
||||||
|
dataIndex: 'cartCount',
|
||||||
|
key: 'cartCount',
|
||||||
|
sorter: true,
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '下单件数',
|
||||||
|
dataIndex: 'orderCount',
|
||||||
|
key: 'orderCount',
|
||||||
|
sorter: true,
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '支付件数',
|
||||||
|
dataIndex: 'orderPayCount',
|
||||||
|
key: 'orderPayCount',
|
||||||
|
sorter: true,
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '支付金额',
|
||||||
|
dataIndex: 'orderPayPrice',
|
||||||
|
key: 'orderPayPrice',
|
||||||
|
sorter: true,
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收藏数',
|
||||||
|
dataIndex: 'favoriteCount',
|
||||||
|
key: 'favoriteCount',
|
||||||
|
sorter: true,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '访客-支付转化率(%)',
|
||||||
|
dataIndex: 'browseConvertPercent',
|
||||||
|
key: 'browseConvertPercent',
|
||||||
|
sorter: true,
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getSpuList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" title="商品排行" class="h-full">
|
||||||
|
<template #extra>
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<ShortcutDateRangePicker
|
||||||
|
ref="shortcutDateRangePicker"
|
||||||
|
@change="handleDateRangeChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 排行列表 -->
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="list"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="false"
|
||||||
|
@change="handleSortChange"
|
||||||
|
row-key="spuId"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- 商品图片 -->
|
||||||
|
<template v-if="column.key === 'spuPicUrl'">
|
||||||
|
<Image
|
||||||
|
:src="record.spuPicUrl"
|
||||||
|
:width="30"
|
||||||
|
:height="30"
|
||||||
|
:preview="true"
|
||||||
|
fallback="data:image/png;base64,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"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 商品名称 -->
|
||||||
|
<template v-else-if="column.key === 'spuName'">
|
||||||
|
<span class="break-words">{{ record.spuName }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 支付金额 -->
|
||||||
|
<template v-else-if="column.key === 'orderPayPrice'">
|
||||||
|
¥{{ formatPrice(record.orderPayPrice) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 访客-支付转化率 -->
|
||||||
|
<template v-else-if="column.key === 'browseConvertPercent'">
|
||||||
|
{{ formatConvertRate(record.browseConvertPercent) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<Pagination
|
||||||
|
:current="queryParams.pageNo"
|
||||||
|
:page-size="queryParams.pageSize"
|
||||||
|
:total="total"
|
||||||
|
show-size-changer
|
||||||
|
show-quick-jumper
|
||||||
|
@change="handlePaginationChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
<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 { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { SummaryCard } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import { fenToYuan } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, Card, Col, message, Row, Skeleton } from 'ant-design-vue';
|
||||||
|
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 './product-summary-chart-options';
|
||||||
|
|
||||||
|
/** 商品概况 */
|
||||||
|
defineOptions({ name: 'ProductSummaryCard' });
|
||||||
|
|
||||||
|
// 消息弹窗
|
||||||
|
|
||||||
|
const trendLoading = ref(true); // 商品状态加载中
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
const trendSummary =
|
||||||
|
ref<DataComparisonRespVO<MallProductStatisticsApi.ProductStatistics>>(); // 商品状况统计数据
|
||||||
|
const shortcutDateRangePicker = ref();
|
||||||
|
|
||||||
|
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 getProductTrendData = async (times?: [Dayjs, Dayjs]) => {
|
||||||
|
trendLoading.value = true;
|
||||||
|
try {
|
||||||
|
let queryTimes = times;
|
||||||
|
if (!queryTimes && shortcutDateRangePicker.value?.times) {
|
||||||
|
queryTimes = shortcutDateRangePicker.value.times;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
|
||||||
|
if (queryTimes && isSameDay(queryTimes[0], queryTimes[1])) {
|
||||||
|
// 前天
|
||||||
|
queryTimes[0] = dayjs(queryTimes[0]).subtract(1, 'd');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
await Promise.all([
|
||||||
|
getProductTrendSummary(queryTimes),
|
||||||
|
getProductStatisticsList(queryTimes),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
trendLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 判断是否同一天 */
|
||||||
|
const isSameDay = (date1: Dayjs, date2: Dayjs): boolean => {
|
||||||
|
return date1.format('YYYY-MM-DD') === date2.format('YYYY-MM-DD');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询商品状况数据统计 */
|
||||||
|
const getProductTrendSummary = async (times?: [Dayjs, Dayjs]) => {
|
||||||
|
const queryTimes = times
|
||||||
|
? [
|
||||||
|
times[0].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
times[1].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
trendSummary.value = await ProductStatisticsApi.getProductStatisticsAnalyse({
|
||||||
|
times: queryTimes,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询商品状况数据列表 */
|
||||||
|
const getProductStatisticsList = async (times?: [Dayjs, Dayjs]) => {
|
||||||
|
// 查询数据
|
||||||
|
const queryTimes = times
|
||||||
|
? [
|
||||||
|
times[0].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
times[1].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
const list: MallProductStatisticsApi.ProductStatistics[] =
|
||||||
|
await ProductStatisticsApi.getProductStatisticsList({ times: queryTimes });
|
||||||
|
|
||||||
|
// 处理数据
|
||||||
|
const processedList = list.map((item) => ({
|
||||||
|
...item,
|
||||||
|
orderPayPrice: Number(fenToYuan(item.orderPayPrice)),
|
||||||
|
afterSaleRefundPrice: Number(fenToYuan(item.afterSaleRefundPrice)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 更新 Echarts 数据
|
||||||
|
await renderEcharts(getProductSummaryChartOptions(processedList));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
// 导出的二次确认
|
||||||
|
await message.confirm({
|
||||||
|
content: '确认导出商品状况数据吗?',
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
});
|
||||||
|
// 发起导出
|
||||||
|
exportLoading.value = true;
|
||||||
|
const times = shortcutDateRangePicker.value?.times;
|
||||||
|
const queryTimes = times
|
||||||
|
? [
|
||||||
|
times[0].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
times[1].format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
const data = await ProductStatisticsApi.exportProductStatisticsExcel({
|
||||||
|
times: queryTimes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理下载
|
||||||
|
const blob = new Blob([data], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = '商品状况.xlsx';
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch {
|
||||||
|
// 用户取消导出
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getProductTrendData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" title="商品概况" class="h-full">
|
||||||
|
<template #extra>
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ShortcutDateRangePicker
|
||||||
|
ref="shortcutDateRangePicker"
|
||||||
|
@change="getProductTrendData"
|
||||||
|
>
|
||||||
|
<Button class="ml-4" @click="handleExport" :loading="exportLoading">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="lucide:download" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</ShortcutDateRangePicker>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 统计值 -->
|
||||||
|
<Row :gutter="16" class="mb-4">
|
||||||
|
<Col :xl="4" :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :xl="4" :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :xl="4" :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :xl="4" :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :xl="4" :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :xl="4" :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- 折线图 -->
|
||||||
|
<Skeleton :loading="trendLoading" :active="true">
|
||||||
|
<EchartsUI ref="chartRef" class="h-[500px]" />
|
||||||
|
</Skeleton>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import type { EChartsOption } from 'echarts';
|
||||||
|
|
||||||
|
/** 商品统计折线图配置 */
|
||||||
|
export function getProductSummaryChartOptions(data: any[]): EChartsOption {
|
||||||
|
return {
|
||||||
|
dataset: {
|
||||||
|
dimensions: [
|
||||||
|
'time',
|
||||||
|
'browseCount',
|
||||||
|
'browseUserCount',
|
||||||
|
'orderPayPrice',
|
||||||
|
'afterSaleRefundPrice',
|
||||||
|
],
|
||||||
|
source: data,
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,32 +1,119 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { DataComparisonRespVO } from '#/api/mall/statistics/common';
|
||||||
|
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
import { fenToYuan } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Col, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||||
|
|
||||||
|
import TradeStatisticCard from './modules/trade-statistic-card.vue';
|
||||||
|
import TradeTrendCard from './modules/trade-trend-card.vue';
|
||||||
|
|
||||||
|
/** 交易统计 */
|
||||||
|
defineOptions({ name: 'TradeStatistics' });
|
||||||
|
|
||||||
|
const loading = ref(true); // 加载中
|
||||||
|
const summary =
|
||||||
|
ref<DataComparisonRespVO<MallTradeStatisticsApi.TradeSummary>>(); // 交易统计数据
|
||||||
|
|
||||||
|
/** 计算环比百分比 */
|
||||||
|
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 getTradeStatisticsSummary = async () => {
|
||||||
|
summary.value = await TradeStatisticsApi.getTradeStatisticsSummary();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
await getTradeStatisticsSummary();
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
</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/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<div class="flex flex-col gap-4">
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<!-- 交易概览卡片 -->
|
||||||
>
|
<Row :gutter="16">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<Col :sm="6" :xs="12">
|
||||||
</Button>
|
<TradeStatisticCard
|
||||||
<br />
|
tooltip="昨日订单数量"
|
||||||
<Button
|
title="昨日订单数量"
|
||||||
type="link"
|
:value="summary?.value?.yesterdayOrderCount || 0"
|
||||||
target="_blank"
|
:percent="
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/trade/index"
|
calculateRelativeRate(
|
||||||
>
|
summary?.value?.yesterdayOrderCount,
|
||||||
可参考
|
summary?.reference?.yesterdayOrderCount,
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/trade/index
|
)
|
||||||
代码,pull request 贡献给我们!
|
"
|
||||||
</Button>
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :sm="6" :xs="12">
|
||||||
|
<TradeStatisticCard
|
||||||
|
tooltip="本月订单数量"
|
||||||
|
title="本月订单数量"
|
||||||
|
:value="summary?.value?.monthOrderCount || 0"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
summary?.value?.monthOrderCount,
|
||||||
|
summary?.reference?.monthOrderCount,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :sm="6" :xs="12">
|
||||||
|
<TradeStatisticCard
|
||||||
|
tooltip="昨日支付金额"
|
||||||
|
title="昨日支付金额"
|
||||||
|
prefix="¥"
|
||||||
|
:decimals="2"
|
||||||
|
:value="Number(fenToYuan(summary?.value?.yesterdayPayPrice || 0))"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
summary?.value?.yesterdayPayPrice,
|
||||||
|
summary?.reference?.yesterdayPayPrice,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :sm="6" :xs="12">
|
||||||
|
<TradeStatisticCard
|
||||||
|
tooltip="本月支付金额"
|
||||||
|
title="本月支付金额"
|
||||||
|
prefix="¥"
|
||||||
|
:decimals="2"
|
||||||
|
:value="Number(fenToYuan(summary?.value?.monthPayPrice || 0))"
|
||||||
|
:percent="
|
||||||
|
calculateRelativeRate(
|
||||||
|
summary?.value?.monthPayPrice,
|
||||||
|
summary?.reference?.monthPayPrice,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- 交易趋势 -->
|
||||||
|
<TradeTrendCard />
|
||||||
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { VbenCountToAnimator } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Card, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
/** 交易统计值组件 */
|
||||||
|
defineOptions({ name: 'TradeStatisticCard' });
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
tooltip: '',
|
||||||
|
title: '',
|
||||||
|
prefix: '',
|
||||||
|
value: 0,
|
||||||
|
decimals: 0,
|
||||||
|
percent: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tooltip?: string;
|
||||||
|
title?: string;
|
||||||
|
prefix?: string;
|
||||||
|
value?: number | string;
|
||||||
|
decimals?: number;
|
||||||
|
percent?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算环比百分比 */
|
||||||
|
const percentValue = computed(() => {
|
||||||
|
return Number(props.percent);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 格式化数值 */
|
||||||
|
const formattedValue = computed(() => {
|
||||||
|
return Number(props.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" class="h-full">
|
||||||
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
<div class="flex items-center justify-between text-gray-500">
|
||||||
|
<span class="text-sm">{{ title }}</span>
|
||||||
|
<Tooltip v-if="tooltip" :title="tooltip" placement="top">
|
||||||
|
<IconifyIcon
|
||||||
|
icon="lucide:circle-alert"
|
||||||
|
class="size-4 cursor-help text-gray-400 hover:text-gray-600"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4 text-3xl font-medium">
|
||||||
|
<VbenCountToAnimator
|
||||||
|
:prefix="prefix"
|
||||||
|
:end-val="formattedValue"
|
||||||
|
:decimals="decimals"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-1 text-sm">
|
||||||
|
<span class="text-gray-500">环比</span>
|
||||||
|
<span
|
||||||
|
:class="percentValue > 0 ? 'text-red-500' : 'text-green-500'"
|
||||||
|
class="flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
{{ Math.abs(percentValue).toFixed(2) }}%
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="
|
||||||
|
percentValue > 0 ? 'lucide:trending-up' : 'lucide:trending-down'
|
||||||
|
"
|
||||||
|
class="size-3"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<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 { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { SummaryCard } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import { fenToYuan } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, Card, Col, message, Row, Skeleton } from 'ant-design-vue';
|
||||||
|
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 './trade-trend-chart-options';
|
||||||
|
|
||||||
|
/** 交易趋势 */
|
||||||
|
defineOptions({ name: 'TradeTrendCard' });
|
||||||
|
|
||||||
|
const trendLoading = ref(true); // 交易状态加载中
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
const trendSummary =
|
||||||
|
ref<DataComparisonRespVO<MallTradeStatisticsApi.TradeTrendSummary>>(); // 交易状况统计数据
|
||||||
|
const shortcutDateRangePicker = ref();
|
||||||
|
|
||||||
|
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 getTradeTrendData = async (times?: [Dayjs, Dayjs]) => {
|
||||||
|
trendLoading.value = true;
|
||||||
|
try {
|
||||||
|
let queryTimes = times;
|
||||||
|
if (!queryTimes && shortcutDateRangePicker.value?.times) {
|
||||||
|
queryTimes = shortcutDateRangePicker.value.times;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
|
||||||
|
if (queryTimes && isSameDay(queryTimes[0], queryTimes[1])) {
|
||||||
|
// 前天
|
||||||
|
queryTimes[0] = dayjs(queryTimes[0]).subtract(1, 'd');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
await Promise.all([
|
||||||
|
getTradeStatisticsAnalyse(queryTimes),
|
||||||
|
getTradeStatisticsList(queryTimes),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
trendLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 判断是否同一天 */
|
||||||
|
const isSameDay = (date1: Dayjs, date2: Dayjs): boolean => {
|
||||||
|
return date1.format('YYYY-MM-DD') === date2.format('YYYY-MM-DD');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询交易状况数据统计 */
|
||||||
|
const getTradeStatisticsAnalyse = async (times?: [Dayjs, Dayjs]) => {
|
||||||
|
const queryTimes = times
|
||||||
|
? { times: [times[0].toDate(), times[1].toDate()] }
|
||||||
|
: undefined;
|
||||||
|
trendSummary.value = await TradeStatisticsApi.getTradeStatisticsAnalyse(
|
||||||
|
queryTimes as any,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询交易状况数据列表 */
|
||||||
|
const getTradeStatisticsList = async (times?: [Dayjs, Dayjs]) => {
|
||||||
|
// 查询数据
|
||||||
|
const queryTimes = times
|
||||||
|
? { times: [times[0].toDate(), times[1].toDate()] }
|
||||||
|
: undefined;
|
||||||
|
const list: MallTradeStatisticsApi.TradeTrendSummary[] =
|
||||||
|
await TradeStatisticsApi.getTradeStatisticsList(queryTimes as any);
|
||||||
|
|
||||||
|
// 处理数据
|
||||||
|
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)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 更新 Echarts 数据
|
||||||
|
await renderEcharts(getTradeTrendChartOptions(processedList));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
// 导出的二次确认
|
||||||
|
await message.confirm({
|
||||||
|
content: '确认导出交易状况数据吗?',
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
});
|
||||||
|
// 发起导出
|
||||||
|
exportLoading.value = true;
|
||||||
|
const times = shortcutDateRangePicker.value?.times;
|
||||||
|
const queryTimes = times
|
||||||
|
? { times: [times[0].toDate(), times[1].toDate()] }
|
||||||
|
: undefined;
|
||||||
|
const data = await TradeStatisticsApi.exportTradeStatisticsExcel(
|
||||||
|
queryTimes as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理下载
|
||||||
|
const blob = new Blob([data], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = '交易状况.xlsx';
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch {
|
||||||
|
// 用户取消导出
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getTradeTrendData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" title="交易状况" class="h-full">
|
||||||
|
<template #extra>
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ShortcutDateRangePicker
|
||||||
|
ref="shortcutDateRangePicker"
|
||||||
|
@change="getTradeTrendData"
|
||||||
|
>
|
||||||
|
<Button class="ml-4" @click="handleExport" :loading="exportLoading">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="lucide:download" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</ShortcutDateRangePicker>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 统计值 -->
|
||||||
|
<Row :gutter="16" class="mb-4">
|
||||||
|
<Col :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :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,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- 折线图 -->
|
||||||
|
<Skeleton :loading="trendLoading" :active="true">
|
||||||
|
<EchartsUI ref="chartRef" class="h-[500px]" />
|
||||||
|
</Skeleton>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { EChartsOption } from 'echarts';
|
||||||
|
|
||||||
|
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
|
||||||
|
|
||||||
|
/** 交易趋势折线图配置 */
|
||||||
|
export function getTradeTrendChartOptions(
|
||||||
|
data: MallTradeStatisticsApi.TradeTrendSummary[],
|
||||||
|
): EChartsOption {
|
||||||
|
return {
|
||||||
|
dataset: {
|
||||||
|
dimensions: [
|
||||||
|
'time',
|
||||||
|
'turnoverPrice',
|
||||||
|
'orderPayPrice',
|
||||||
|
'rechargePrice',
|
||||||
|
'expensePrice',
|
||||||
|
],
|
||||||
|
source: data,
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '¥{value}',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#F5F7F9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user