feat: 新增商品统计组件和优化数据处理逻辑
- 引入商品排行和商品概况组件,展示商品相关统计信息 - 更新商品统计 API,支持时间范围查询和数据格式化 - 优化数据加载逻辑,提升用户体验 - 添加日期范围选择器,增强统计数据的灵活性pull/175/head
parent
73a73ac312
commit
4620ede9b9
|
@ -2,6 +2,8 @@ import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import type { MallDataComparisonResp } from './common';
|
import type { MallDataComparisonResp } from './common';
|
||||||
|
|
||||||
|
import { formatDate2 } from '@vben/utils';
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace MallProductStatisticsApi {
|
export namespace MallProductStatisticsApi {
|
||||||
|
@ -38,26 +40,58 @@ export namespace MallProductStatisticsApi {
|
||||||
/** 浏览转化率 */
|
/** 浏览转化率 */
|
||||||
browseConvertPercent: number;
|
browseConvertPercent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 会员分析 Request */
|
||||||
|
export interface ProductStatisticsReq {
|
||||||
|
times: Date[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获得商品统计分析 */
|
/** 获得商品统计分析 */
|
||||||
export function getProductStatisticsAnalyse(params: PageParam) {
|
export function getProductStatisticsAnalyse(
|
||||||
|
params: MallProductStatisticsApi.ProductStatisticsReq,
|
||||||
|
) {
|
||||||
return requestClient.get<
|
return requestClient.get<
|
||||||
MallDataComparisonResp<MallProductStatisticsApi.ProductStatistics>
|
MallDataComparisonResp<MallProductStatisticsApi.ProductStatistics>
|
||||||
>('/statistics/product/analyse', { params });
|
>('/statistics/product/analyse', {
|
||||||
|
params: {
|
||||||
|
times: [
|
||||||
|
formatDate2(params.times[0] || new Date()),
|
||||||
|
formatDate2(params.times[1] || new Date()),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获得商品状况明细 */
|
/** 获得商品状况明细 */
|
||||||
export function getProductStatisticsList(params: PageParam) {
|
export function getProductStatisticsList(
|
||||||
|
params: MallProductStatisticsApi.ProductStatisticsReq,
|
||||||
|
) {
|
||||||
return requestClient.get<MallProductStatisticsApi.ProductStatistics[]>(
|
return requestClient.get<MallProductStatisticsApi.ProductStatistics[]>(
|
||||||
'/statistics/product/list',
|
'/statistics/product/list',
|
||||||
{ params },
|
{
|
||||||
|
params: {
|
||||||
|
times: [
|
||||||
|
formatDate2(params.times[0] || new Date()),
|
||||||
|
formatDate2(params.times[1] || new Date()),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 导出获得商品状况明细 Excel */
|
/** 导出获得商品状况明细 Excel */
|
||||||
export function exportProductStatisticsExcel(params: PageParam) {
|
export function exportProductStatisticsExcel(
|
||||||
return requestClient.download('/statistics/product/export-excel', { params });
|
params: MallProductStatisticsApi.ProductStatisticsReq,
|
||||||
|
) {
|
||||||
|
return requestClient.download('/statistics/product/export-excel', {
|
||||||
|
params: {
|
||||||
|
times: [
|
||||||
|
formatDate2(params.times[0] || new Date()),
|
||||||
|
formatDate2(params.times[1] || new Date()),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获得商品排行榜分页 */
|
/** 获得商品排行榜分页 */
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
|
||||||
|
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AnalysisChartCard } from '@vben/common-ui';
|
||||||
|
import { buildSortingField, floatToFixed2 } from '@vben/utils';
|
||||||
|
|
||||||
|
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
|
||||||
|
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
|
||||||
|
|
||||||
|
/** 商品排行 */
|
||||||
|
defineOptions({ name: 'ProductRank' });
|
||||||
|
|
||||||
|
// 格式化:访客-支付转化率
|
||||||
|
const formatConvertRate = (row: MallProductStatisticsApi.ProductStatistics) => {
|
||||||
|
return `${row.browseConvertPercent}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSortChange = (params: any) => {
|
||||||
|
queryParams.sortingFields = [buildSortingField(params)];
|
||||||
|
getSpuList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (times: any[]) => {
|
||||||
|
queryParams.times = times as [];
|
||||||
|
getSpuList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const shortcutDateRangePicker = ref();
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
times: [],
|
||||||
|
sortingFields: {},
|
||||||
|
});
|
||||||
|
const loading = ref(false); // 列表的加载中
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const list = ref<MallProductStatisticsApi.ProductStatistics[]>([]); // 列表的数据
|
||||||
|
|
||||||
|
/** 查询商品列表 */
|
||||||
|
const getSpuList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data =
|
||||||
|
await ProductStatisticsApi.getProductStatisticsRankPage(queryParams);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化金额【分转元】
|
||||||
|
// @ts-ignore
|
||||||
|
const fenToYuanFormat = (_, __, cellValue: any, ___) => {
|
||||||
|
return `¥${floatToFixed2(cellValue)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getSpuList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<AnalysisChartCard title="商品排行">
|
||||||
|
<template #header-suffix>
|
||||||
|
<ShortcutDateRangePicker
|
||||||
|
ref="shortcutDateRangePicker"
|
||||||
|
@change="handleDateRangeChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- 排行列表 -->
|
||||||
|
<el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
|
||||||
|
<el-table-column label="商品 ID" prop="spuId" min-width="70" />
|
||||||
|
<el-table-column label="商品图片" align="center" prop="picUrl" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
:src="row.picUrl"
|
||||||
|
:preview-src-list="[row.picUrl]"
|
||||||
|
class="h-30px w-30px"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="商品名称"
|
||||||
|
prop="name"
|
||||||
|
min-width="200"
|
||||||
|
:show-overflow-tooltip="true"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="浏览量"
|
||||||
|
prop="browseCount"
|
||||||
|
min-width="90"
|
||||||
|
sortable="custom"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="访客数"
|
||||||
|
prop="browseUserCount"
|
||||||
|
min-width="90"
|
||||||
|
sortable="custom"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="加购件数"
|
||||||
|
prop="cartCount"
|
||||||
|
min-width="105"
|
||||||
|
sortable="custom"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="下单件数"
|
||||||
|
prop="orderCount"
|
||||||
|
min-width="105"
|
||||||
|
sortable="custom"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="支付件数"
|
||||||
|
prop="orderPayCount"
|
||||||
|
min-width="105"
|
||||||
|
sortable="custom"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="支付金额"
|
||||||
|
prop="orderPayPrice"
|
||||||
|
min-width="105"
|
||||||
|
sortable="custom"
|
||||||
|
:formatter="fenToYuanFormat"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="收藏数"
|
||||||
|
prop="favoriteCount"
|
||||||
|
min-width="90"
|
||||||
|
sortable="custom"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="访客-支付转化率(%)"
|
||||||
|
prop="browseConvertPercent"
|
||||||
|
min-width="180"
|
||||||
|
sortable="custom"
|
||||||
|
:formatter="formatConvertRate"
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getSpuList"
|
||||||
|
/>
|
||||||
|
</AnalysisChartCard>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,300 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
|
||||||
|
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
|
||||||
|
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AnalysisChartCard, AnalysisOverview, confirm } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
SvgBellIcon,
|
||||||
|
SvgCakeIcon,
|
||||||
|
SvgDownloadIcon,
|
||||||
|
SvgEyeIcon,
|
||||||
|
} from '@vben/icons';
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import {
|
||||||
|
downloadFileFromBlobPart,
|
||||||
|
fenToYuan,
|
||||||
|
formatDate,
|
||||||
|
isSameDay,
|
||||||
|
} from '@vben/utils';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
|
||||||
|
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
|
||||||
|
|
||||||
|
/** 商品概况 */
|
||||||
|
defineOptions({ name: 'ProductSummary' });
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
const trendLoading = ref(true); // 商品状态加载中
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
const trendSummary =
|
||||||
|
ref<MallDataComparisonResp<MallProductStatisticsApi.ProductStatistics>>(); // 商品状况统计数据
|
||||||
|
const shortcutDateRangePicker = ref();
|
||||||
|
|
||||||
|
/** 折线图配置 */
|
||||||
|
const lineChartOptions = reactive({
|
||||||
|
dataset: {
|
||||||
|
dimensions: [
|
||||||
|
'time',
|
||||||
|
'browseCount',
|
||||||
|
'browseUserCount',
|
||||||
|
'orderPayPrice',
|
||||||
|
'afterSaleRefundPrice',
|
||||||
|
],
|
||||||
|
source: [] as MallProductStatisticsApi.ProductStatistics[],
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
bottom: 20,
|
||||||
|
top: 80,
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 50,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '商品浏览量',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
itemStyle: { color: '#B37FEB' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '商品访客数',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
itemStyle: { color: '#FFAB2B' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '支付金额',
|
||||||
|
type: 'bar',
|
||||||
|
smooth: true,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
itemStyle: { color: '#1890FF' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '退款金额',
|
||||||
|
type: 'bar',
|
||||||
|
smooth: true,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
itemStyle: { color: '#00C050' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
// 数据区域缩放
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: false, // Y轴不缩放
|
||||||
|
},
|
||||||
|
brush: {
|
||||||
|
type: ['lineX', 'clear'] as const, // 区域缩放按钮、还原按钮
|
||||||
|
},
|
||||||
|
saveAsImage: { show: true, name: '商品状况' }, // 保存为图片
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
},
|
||||||
|
padding: [5, 10],
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category' as const,
|
||||||
|
boundaryGap: true,
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value' as const,
|
||||||
|
name: '金额',
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#7F8B9C',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#F5F7F9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value' as const,
|
||||||
|
name: '数量',
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#7F8B9C',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#F5F7F9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 处理商品状况查询 */
|
||||||
|
const getProductTrendData = async () => {
|
||||||
|
trendLoading.value = true;
|
||||||
|
// 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
|
||||||
|
const times = shortcutDateRangePicker.value.times;
|
||||||
|
if (isSameDay(times[0], times[1])) {
|
||||||
|
// 前天
|
||||||
|
times[0] = formatDate(dayjs(times[0]).subtract(1, 'd').toDate());
|
||||||
|
}
|
||||||
|
// 查询数据
|
||||||
|
await Promise.all([getProductTrendSummary(), getProductStatisticsList()]);
|
||||||
|
renderEcharts(lineChartOptions as unknown as echarts.EChartsOption);
|
||||||
|
loadOverview();
|
||||||
|
trendLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询商品状况数据统计 */
|
||||||
|
const getProductTrendSummary = async () => {
|
||||||
|
const times = shortcutDateRangePicker.value.times;
|
||||||
|
trendSummary.value = await ProductStatisticsApi.getProductStatisticsAnalyse({
|
||||||
|
times,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询商品状况数据列表 */
|
||||||
|
const getProductStatisticsList = async () => {
|
||||||
|
// 查询数据
|
||||||
|
const times = shortcutDateRangePicker.value.times;
|
||||||
|
const list: MallProductStatisticsApi.ProductStatistics[] =
|
||||||
|
await ProductStatisticsApi.getProductStatisticsList({ times });
|
||||||
|
// 处理数据
|
||||||
|
for (const item of list) {
|
||||||
|
item.orderPayPrice = Number(fenToYuan(item.orderPayPrice));
|
||||||
|
item.afterSaleRefundPrice = Number(fenToYuan(item.afterSaleRefundPrice));
|
||||||
|
}
|
||||||
|
// 更新 Echarts 数据
|
||||||
|
if (lineChartOptions.dataset && lineChartOptions.dataset.source) {
|
||||||
|
lineChartOptions.dataset.source = list;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
// 导出的二次确认
|
||||||
|
await confirm('确定要导出商品状况吗?');
|
||||||
|
// 发起导出
|
||||||
|
exportLoading.value = true;
|
||||||
|
const times = shortcutDateRangePicker.value.times;
|
||||||
|
const data = await ProductStatisticsApi.exportProductStatisticsExcel({
|
||||||
|
times,
|
||||||
|
});
|
||||||
|
downloadFileFromBlobPart({ fileName: '商品状况.xls', source: data });
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const overviewItems = ref<AnalysisOverviewItem[]>();
|
||||||
|
const loadOverview = () => {
|
||||||
|
overviewItems.value = [
|
||||||
|
{
|
||||||
|
icon: SvgEyeIcon,
|
||||||
|
title: '商品浏览量',
|
||||||
|
totalTitle: '昨日数据',
|
||||||
|
totalValue: trendSummary.value?.reference?.browseCount,
|
||||||
|
value: trendSummary?.value?.browseUserCount || 0,
|
||||||
|
tooltip:
|
||||||
|
'在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgCakeIcon,
|
||||||
|
title: '商品访客数',
|
||||||
|
totalTitle: '昨日数据',
|
||||||
|
totalValue: trendSummary.value?.reference?.browseUserCount || 0,
|
||||||
|
value: trendSummary?.value?.browseUserCount || 0,
|
||||||
|
tooltip:
|
||||||
|
'在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgDownloadIcon,
|
||||||
|
title: '支付件数',
|
||||||
|
totalTitle: '昨日数据',
|
||||||
|
totalValue: trendSummary.value?.reference?.orderPayCount || 0,
|
||||||
|
value: trendSummary?.value?.orderPayCount || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgBellIcon,
|
||||||
|
title: '支付金额',
|
||||||
|
totalTitle: '昨日数据',
|
||||||
|
totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
|
||||||
|
value: trendSummary?.value?.orderPayPrice || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgBellIcon,
|
||||||
|
title: '退款件数',
|
||||||
|
totalTitle: '昨日数据',
|
||||||
|
totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
|
||||||
|
value: trendSummary?.value?.afterSaleCount || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgBellIcon,
|
||||||
|
title: '退款金额',
|
||||||
|
totalTitle: '昨日数据',
|
||||||
|
totalValue: trendSummary.value?.reference?.afterSaleRefundPrice || 0,
|
||||||
|
value: trendSummary?.value?.afterSaleRefundPrice || 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<AnalysisChartCard title="商品概况">
|
||||||
|
<template #header-suffix>
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<ShortcutDateRangePicker
|
||||||
|
ref="shortcutDateRangePicker"
|
||||||
|
@change="getProductTrendData"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
class="ml-4"
|
||||||
|
@click="handleExport"
|
||||||
|
:loading="exportLoading"
|
||||||
|
v-hasPermi="['statistics:product:export']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:download" class="mr-1" />导出
|
||||||
|
</el-button>
|
||||||
|
</ShortcutDateRangePicker>
|
||||||
|
</template>
|
||||||
|
<!-- 统计值 -->
|
||||||
|
<AnalysisOverview
|
||||||
|
v-model:model-value="overviewItems"
|
||||||
|
:columns-number="6"
|
||||||
|
class="mt-5 md:mr-4 md:mt-0 md:w-full"
|
||||||
|
/>
|
||||||
|
<!-- 折线图 -->
|
||||||
|
<el-skeleton :loading="trendLoading" animated>
|
||||||
|
<EchartsUI ref="chartRef" height="500px" />
|
||||||
|
</el-skeleton>
|
||||||
|
</AnalysisChartCard>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -1,7 +1,8 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { ElButton } from 'element-plus';
|
import ProductRank from './components/product-rank.vue';
|
||||||
|
import ProductSummary from './components/product-summary.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -10,25 +11,7 @@ import { ElButton } from 'element-plus';
|
||||||
title="【统计】会员、商品、交易统计"
|
title="【统计】会员、商品、交易统计"
|
||||||
url="https://doc.iocoder.cn/mall/statistics/"
|
url="https://doc.iocoder.cn/mall/statistics/"
|
||||||
/>
|
/>
|
||||||
<ElButton
|
<ProductSummary class="md:w-3/3 mt-5 md:mr-4 md:mt-0" />
|
||||||
danger
|
<ProductRank class="md:w-3/3 mt-5 md:mr-4 md:mt-0" />
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
|
||||||
</ElButton>
|
|
||||||
<br />
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/product/index"
|
|
||||||
>
|
|
||||||
可参考
|
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/product/index
|
|
||||||
代码,pull request 贡献给我们!
|
|
||||||
</ElButton>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -22,3 +22,18 @@ export { default as cloneDeep } from 'lodash.clonedeep';
|
||||||
export { default as get } from 'lodash.get';
|
export { default as get } from 'lodash.get';
|
||||||
export { default as isEqual } from 'lodash.isequal';
|
export { default as isEqual } from 'lodash.isequal';
|
||||||
export { default as set } from 'lodash.set';
|
export { default as set } from 'lodash.set';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建排序字段
|
||||||
|
* @param prop 字段名称
|
||||||
|
* @param order 顺序
|
||||||
|
*/
|
||||||
|
export const buildSortingField = ({
|
||||||
|
prop,
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
order: 'ascending' | 'descending';
|
||||||
|
prop: string;
|
||||||
|
}) => {
|
||||||
|
return { field: prop, order: order === 'ascending' ? 'asc' : 'desc' };
|
||||||
|
};
|
||||||
|
|
|
@ -9,6 +9,10 @@ import {
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
VbenCountToAnimator,
|
VbenCountToAnimator,
|
||||||
VbenIcon,
|
VbenIcon,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
@ -16,6 +20,7 @@ import {
|
||||||
interface Props {
|
interface Props {
|
||||||
items?: AnalysisOverviewItem[];
|
items?: AnalysisOverviewItem[];
|
||||||
modelValue?: AnalysisOverviewItem[];
|
modelValue?: AnalysisOverviewItem[];
|
||||||
|
columnsNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
@ -25,6 +30,7 @@ defineOptions({
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
items: () => [],
|
items: () => [],
|
||||||
modelValue: () => [],
|
modelValue: () => [],
|
||||||
|
columnsNumber: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
@ -33,14 +39,45 @@ const itemsData = computed({
|
||||||
get: () => (props.modelValue?.length ? props.modelValue : props.items),
|
get: () => (props.modelValue?.length ? props.modelValue : props.items),
|
||||||
set: (value) => emit('update:modelValue', value),
|
set: (value) => emit('update:modelValue', value),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算动态的grid列数类名
|
||||||
|
const gridColumnsClass = computed(() => {
|
||||||
|
const colNum = props.columnsNumber;
|
||||||
|
return {
|
||||||
|
'lg:grid-cols-1': colNum === 1,
|
||||||
|
'lg:grid-cols-2': colNum === 2,
|
||||||
|
'lg:grid-cols-3': colNum === 3,
|
||||||
|
'lg:grid-cols-4': colNum === 4,
|
||||||
|
'lg:grid-cols-5': colNum === 5,
|
||||||
|
'lg:grid-cols-6': colNum === 6,
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2" :class="gridColumnsClass">
|
||||||
<template v-for="item in itemsData" :key="item.title">
|
<template v-for="item in itemsData" :key="item.title">
|
||||||
<Card :title="item.title" class="w-full">
|
<Card :title="item.title" class="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-xl">{{ item.title }}</CardTitle>
|
<CardTitle class="text-xl">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<span v-if="item.tooltip" class="ml-1 inline-block">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div
|
||||||
|
class="inline-flex h-4 w-4 translate-y-[-3px] items-center justify-center rounded-full bg-gray-200 text-xs font-bold text-gray-600"
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ item.tooltip }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent class="flex items-center justify-between">
|
<CardContent class="flex items-center justify-between">
|
||||||
|
|
|
@ -6,6 +6,7 @@ interface AnalysisOverviewItem {
|
||||||
totalTitle?: string;
|
totalTitle?: string;
|
||||||
totalValue?: number;
|
totalValue?: number;
|
||||||
value: number;
|
value: number;
|
||||||
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkbenchProjectItem {
|
interface WorkbenchProjectItem {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
|
||||||
|
<!-- 样式定义 -->
|
||||||
|
<style>
|
||||||
|
.eye-outline { fill: #0D47A1; }
|
||||||
|
.eye-white { fill: #BBDEFB; }
|
||||||
|
.eye-iris { fill: #2196F3; }
|
||||||
|
.eye-pupil { fill: #000000; }
|
||||||
|
.eye-highlight { fill: #FFFFFF; }
|
||||||
|
.eye-shadow { fill: #1565C0; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 眼睛外轮廓 -->
|
||||||
|
<path class="eye-outline" d="M200,250c-80,0-160-60-200-100c40-40,120-100,200-100s160,60,200,100C360,190,280,250,200,250z"/>
|
||||||
|
|
||||||
|
<!-- 眼睛白色部分 -->
|
||||||
|
<path class="eye-white" d="M200,70c70,0,140,50,180,80c-40,30-110,80-180,80s-140-50-180-80C60,120,130,70,200,70z"/>
|
||||||
|
|
||||||
|
<!-- 眼睑阴影 -->
|
||||||
|
<path class="eye-shadow" d="M200,90c-60,0-120,40-160,60c40,20,100,60,160,60s120-40,160-60C320,130,260,90,200,90z"/>
|
||||||
|
|
||||||
|
<!-- 虹膜 -->
|
||||||
|
<circle class="eye-iris" cx="200" cy="150" r="60"/>
|
||||||
|
|
||||||
|
<!-- 瞳孔 - 确保是明显的黑色圆形 -->
|
||||||
|
<circle class="eye-pupil" cx="200" cy="150" r="25"/>
|
||||||
|
|
||||||
|
<!-- 高光 -->
|
||||||
|
<circle class="eye-highlight" cx="180" cy="130" r="12"/>
|
||||||
|
|
||||||
|
<!-- 装饰线条 -->
|
||||||
|
<path class="eye-highlight" d="M100,110c10-5,30-15,40-20c3-1,2-5-1-4c-10,5-30,15-40,20C96,107,97,111,100,110z"/>
|
||||||
|
<path class="eye-highlight" d="M300,190c2-5,5-10,10-15c10-10,20-20,30-25c2-1,0-5-2-4c-15,10-30,30-40,45C297,193,299,195,300,190z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -10,6 +10,7 @@ const SvgDownloadIcon = createIconifyIcon('svg:download');
|
||||||
const SvgCardIcon = createIconifyIcon('svg:card');
|
const SvgCardIcon = createIconifyIcon('svg:card');
|
||||||
const SvgBellIcon = createIconifyIcon('svg:bell');
|
const SvgBellIcon = createIconifyIcon('svg:bell');
|
||||||
const SvgCakeIcon = createIconifyIcon('svg:cake');
|
const SvgCakeIcon = createIconifyIcon('svg:cake');
|
||||||
|
const SvgEyeIcon = createIconifyIcon('svg:eye');
|
||||||
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
|
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
|
||||||
|
|
||||||
/** AI */
|
/** AI */
|
||||||
|
@ -44,6 +45,7 @@ export {
|
||||||
SvgCakeIcon,
|
SvgCakeIcon,
|
||||||
SvgCardIcon,
|
SvgCardIcon,
|
||||||
SvgDownloadIcon,
|
SvgDownloadIcon,
|
||||||
|
SvgEyeIcon,
|
||||||
SvgGptIcon,
|
SvgGptIcon,
|
||||||
SvgMockIcon,
|
SvgMockIcon,
|
||||||
SvgWalletIcon,
|
SvgWalletIcon,
|
||||||
|
|
Loading…
Reference in New Issue