feat:【mall 商城】交易统计、商品统计【antd】15%:product-rank-card.vue 完善
This commit is contained in:
@@ -81,7 +81,7 @@ onMounted(() => {
|
||||
:value-format="rangePickerProps.valueFormat"
|
||||
:placeholder="rangePickerProps.placeholder"
|
||||
:presets="rangePickerProps.presets"
|
||||
class="!w-[240px]"
|
||||
class="!w-[235px]"
|
||||
@change="handleDateRangeChange"
|
||||
/>
|
||||
<slot></slot>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import ProductRankCard from './modules/product-rank-card.vue';
|
||||
import ProductSummaryCard from './modules/product-summary-card.vue';
|
||||
import ProductRankCard from './modules/rank-card.vue';
|
||||
|
||||
/** 商品统计 */
|
||||
defineOptions({ name: 'ProductStatistics' });
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
<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,135 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { buildSortingField } from '@vben/request';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Card } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
|
||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||
|
||||
/** 商品排行 */
|
||||
defineOptions({ name: 'ProductRankCard' });
|
||||
|
||||
const searchTimes = ref<string[]>([]);
|
||||
|
||||
/** 处理日期范围变化 */
|
||||
const handleDateRangeChange = (times?: [Dayjs, Dayjs]) => {
|
||||
if (times?.length !== 2) {
|
||||
return;
|
||||
}
|
||||
searchTimes.value = [
|
||||
formatDateTime(times[0]) as string,
|
||||
formatDateTime(times[1]) as string,
|
||||
];
|
||||
gridApi.query();
|
||||
};
|
||||
|
||||
const columns: VxeTableGridOptions['columns'] = [
|
||||
{ field: 'spuId', title: '商品 ID', minWidth: 100 },
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
minWidth: 100,
|
||||
cellRender: { name: 'CellImage' },
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商品名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'browseCount',
|
||||
title: '浏览量',
|
||||
minWidth: 100,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'browseUserCount',
|
||||
title: '访客数',
|
||||
minWidth: 100,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'cartCount',
|
||||
title: '加购件数',
|
||||
minWidth: 110,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'orderCount',
|
||||
title: '下单件数',
|
||||
minWidth: 110,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'orderPayCount',
|
||||
title: '支付件数',
|
||||
minWidth: 110,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'orderPayPrice',
|
||||
title: '支付金额(元)',
|
||||
minWidth: 120,
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'favoriteCount',
|
||||
title: '收藏数',
|
||||
minWidth: 100,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'browseConvertPercent',
|
||||
title: '访客-支付转化率(%)',
|
||||
minWidth: 160,
|
||||
sortable: true,
|
||||
formatter: ({ cellValue }) => `${cellValue || 0}%`,
|
||||
},
|
||||
];
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns,
|
||||
height: 400,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sorts }) => {
|
||||
return await ProductStatisticsApi.getProductStatisticsRankPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
times: searchTimes.value.length > 0 ? searchTimes.value : undefined,
|
||||
...buildSortingField(sorts),
|
||||
});
|
||||
},
|
||||
},
|
||||
sort: true,
|
||||
},
|
||||
sortConfig: {
|
||||
remote: true,
|
||||
multiple: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :bordered="false" title="商品排行">
|
||||
<template #extra>
|
||||
<ShortcutDateRangePicker @change="handleDateRangeChange" />
|
||||
</template>
|
||||
<Grid />
|
||||
</Card>
|
||||
</template>
|
||||
@@ -102,7 +102,7 @@ onMounted(() => {
|
||||
:start-placeholder="rangePickerProps.startPlaceholder"
|
||||
:end-placeholder="rangePickerProps.endPlaceholder"
|
||||
:default-time="rangePickerProps.defaultTime as any"
|
||||
class="!w-[360px]"
|
||||
class="!w-[215px]"
|
||||
@change="handleDateRangeChange"
|
||||
/>
|
||||
<slot></slot>
|
||||
|
||||
@@ -23,19 +23,4 @@ export * from './window';
|
||||
export { default as cloneDeep } from 'lodash.clonedeep';
|
||||
export { default as get } from 'lodash.get';
|
||||
export { default as isEqual } from 'lodash.isequal';
|
||||
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' };
|
||||
};
|
||||
export { default as set } from 'lodash.set';
|
||||
@@ -162,4 +162,21 @@ class RequestClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建排序字段,处理 vxe 排序条件
|
||||
*
|
||||
* add by 芋艿
|
||||
*/
|
||||
export const buildSortingField = (sorts: any[]) => {
|
||||
if (!sorts || sorts.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const result: Record<string, any> = {};
|
||||
sorts.forEach((sort: any, index: number) => {
|
||||
result[`sortingFields[${index}].field`] = sort.field;
|
||||
result[`sortingFields[${index}].order`] = sort.order;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export { RequestClient };
|
||||
|
||||
Reference in New Issue
Block a user