feat: 新增运营数据展示组件,优化商城首页数据处理逻辑
- 在商城首页引入 WorkbenchQuickDataShow 组件,展示关键运营数据 - 增加数据获取方法,包括订单、商品和钱包充值数据 - 更新 AnalysisOverview 组件以支持双向绑定 - 优化数据加载逻辑,提升用户体验pull/175/head
parent
e88c17f7e2
commit
5edccd3efe
|
|
@ -2,6 +2,7 @@
|
|||
import type {
|
||||
AnalysisOverviewItem,
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickDataShowItem,
|
||||
WorkbenchQuickNavItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
AnalysisOverview,
|
||||
DocAlert,
|
||||
Page,
|
||||
WorkbenchQuickDataShow,
|
||||
WorkbenchQuickNav,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
|
|
@ -22,8 +24,10 @@ import {
|
|||
} from '@vben/icons';
|
||||
import { isString, openWindow } from '@vben/utils';
|
||||
|
||||
import { getTabsCount } from '#/api/mall/product/spu';
|
||||
import { getUserCountComparison } from '#/api/mall/statistics/member';
|
||||
import { getOrderComparison } from '#/api/mall/statistics/trade';
|
||||
import { getWalletRechargePrice } from '#/api/mall/statistics/pay';
|
||||
import { getOrderComparison, getOrderCount } from '#/api/mall/statistics/trade';
|
||||
|
||||
/** 商城首页 */
|
||||
defineOptions({ name: 'MallHome' });
|
||||
|
|
@ -31,6 +35,18 @@ defineOptions({ name: 'MallHome' });
|
|||
const loading = ref(true); // 加载中
|
||||
const orderComparison = ref(); // 交易对照数据
|
||||
const userComparison = ref(); // 用户对照数据
|
||||
const data = ref({
|
||||
orderUndelivered: 0,
|
||||
orderAfterSaleApply: 0,
|
||||
orderWaitePickUp: 0,
|
||||
withdrawAuditing: 0,
|
||||
productForSale: 0,
|
||||
productInWarehouse: 0,
|
||||
productAlertStock: 0,
|
||||
rechargePrice: 0,
|
||||
});
|
||||
|
||||
const dataShow = ref(false);
|
||||
|
||||
/** 查询交易对照卡片数据 */
|
||||
const getOrder = async () => {
|
||||
|
|
@ -42,43 +58,87 @@ const getUserCount = async () => {
|
|||
userComparison.value = await getUserCountComparison();
|
||||
};
|
||||
|
||||
/** 查询订单数据 */
|
||||
const getOrderData = async () => {
|
||||
const orderCount = await getOrderCount();
|
||||
if (orderCount.undelivered) {
|
||||
data.value.orderUndelivered = orderCount.undelivered;
|
||||
}
|
||||
if (orderCount.afterSaleApply) {
|
||||
data.value.orderAfterSaleApply = orderCount.afterSaleApply;
|
||||
}
|
||||
if (orderCount.pickUp) {
|
||||
data.value.orderWaitePickUp = orderCount.pickUp;
|
||||
}
|
||||
if (orderCount.auditingWithdraw) {
|
||||
data.value.withdrawAuditing = orderCount.auditingWithdraw;
|
||||
}
|
||||
};
|
||||
|
||||
/** 查询商品数据 */
|
||||
const getProductData = async () => {
|
||||
// TODO: @芋艿:这个接口的返回值,是不是用命名字段更好些?
|
||||
const productCount = await getTabsCount();
|
||||
data.value.productForSale = productCount['0'] || 0;
|
||||
data.value.productInWarehouse = productCount['1'] || 0;
|
||||
data.value.productAlertStock = productCount['3'] || 0;
|
||||
};
|
||||
|
||||
/** 查询钱包充值数据 */
|
||||
const getWalletRechargeData = async () => {
|
||||
const paySummary = await getWalletRechargePrice();
|
||||
data.value.rechargePrice = paySummary.rechargePrice;
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
await Promise.all([getOrder(), getUserCount()]);
|
||||
await Promise.all([
|
||||
getOrder(),
|
||||
getUserCount(),
|
||||
getOrderData(),
|
||||
getProductData(),
|
||||
getWalletRechargeData(),
|
||||
]);
|
||||
loading.value = false;
|
||||
dataShow.value = true;
|
||||
loadDataShow();
|
||||
loadOverview();
|
||||
});
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '今日销售额',
|
||||
totalTitle: '昨日数据',
|
||||
totalValue: orderComparison.value?.reference?.orderPayPrice || 0,
|
||||
value: orderComparison.value?.orderPayPrice || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '今日用户访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: userComparison.value?.reference?.visitUserCount || 0,
|
||||
value: userComparison.value?.visitUserCount || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '今日订单量',
|
||||
totalTitle: '总订单量',
|
||||
totalValue: orderComparison.value?.orderPayCount || 0,
|
||||
value: orderComparison.value?.reference?.orderPayCount || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '今日会员注册量',
|
||||
totalTitle: '总会员注册量',
|
||||
totalValue: userComparison.value?.registerUserCount || 0,
|
||||
value: userComparison.value?.reference?.registerUserCount || 0,
|
||||
},
|
||||
];
|
||||
const overviewItems = ref<AnalysisOverviewItem[]>([]);
|
||||
const loadOverview = () => {
|
||||
overviewItems.value = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '今日销售额',
|
||||
totalTitle: '昨日数据',
|
||||
totalValue: orderComparison.value?.reference?.orderPayPrice || 0,
|
||||
value: orderComparison.value?.orderPayPrice || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '今日用户访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: userComparison.value?.reference?.visitUserCount || 0,
|
||||
value: userComparison.value?.visitUserCount || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '今日订单量',
|
||||
totalTitle: '总订单量',
|
||||
totalValue: orderComparison.value?.orderPayCount || 0,
|
||||
value: orderComparison.value?.reference?.orderPayCount || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '今日会员注册量',
|
||||
totalTitle: '总会员注册量',
|
||||
totalValue: userComparison.value?.registerUserCount || 0,
|
||||
value: userComparison.value?.reference?.registerUserCount || 0,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
|
|
@ -138,6 +198,69 @@ const quickNavItems: WorkbenchQuickNavItem[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const quickDataShowItems = ref<WorkbenchQuickDataShowItem[]>();
|
||||
|
||||
const loadDataShow = () => {
|
||||
quickDataShowItems.value = [
|
||||
{
|
||||
name: '待发货订单',
|
||||
value: data.value.orderUndelivered,
|
||||
prefix: '',
|
||||
decimals: 0,
|
||||
routerName: 'TradeOrder',
|
||||
},
|
||||
{
|
||||
name: '退款中订单',
|
||||
value: data.value.orderAfterSaleApply,
|
||||
prefix: '',
|
||||
decimals: 0,
|
||||
routerName: 'TradeAfterSale',
|
||||
},
|
||||
{
|
||||
name: '待核销订单',
|
||||
value: data.value.orderWaitePickUp,
|
||||
routerName: 'TradeOrder',
|
||||
prefix: '',
|
||||
decimals: 0,
|
||||
},
|
||||
{
|
||||
name: '库存预警',
|
||||
value: data.value.productAlertStock,
|
||||
routerName: 'ProductSpu',
|
||||
prefix: '',
|
||||
decimals: 0,
|
||||
},
|
||||
{
|
||||
name: '上架商品',
|
||||
value: data.value.productForSale,
|
||||
routerName: 'ProductSpu',
|
||||
prefix: '',
|
||||
decimals: 0,
|
||||
},
|
||||
{
|
||||
name: '仓库商品',
|
||||
value: data.value.productInWarehouse,
|
||||
routerName: 'ProductSpu',
|
||||
prefix: '',
|
||||
decimals: 0,
|
||||
},
|
||||
{
|
||||
name: '提现待审核',
|
||||
value: data.value.withdrawAuditing,
|
||||
routerName: 'TradeBrokerageWithdraw',
|
||||
prefix: '',
|
||||
decimals: 0,
|
||||
},
|
||||
{
|
||||
name: '账户充值',
|
||||
value: data.value.rechargePrice,
|
||||
prefix: '¥',
|
||||
decimals: 2,
|
||||
routerName: 'PayWalletRecharge',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
|
|
@ -164,14 +287,20 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|||
url="https://doc.iocoder.cn/mall/build/"
|
||||
/>
|
||||
</template>
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<div class="mt-5 w-full lg:w-2/5">
|
||||
<AnalysisOverview v-model:model-value="overviewItems" />
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
class="mt-5 md:mr-4 md:mt-0 md:w-1/2"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchQuickDataShow
|
||||
v-if="dataShow"
|
||||
v-model:model-value="quickDataShowItems"
|
||||
title="运营数据"
|
||||
class="mt-5 md:mr-4 md:mt-0 md:w-1/2"
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import type { AnalysisOverviewItem } from '../typing';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -13,20 +15,29 @@ import {
|
|||
|
||||
interface Props {
|
||||
items?: AnalysisOverviewItem[];
|
||||
modelValue?: AnalysisOverviewItem[];
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AnalysisOverview',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
modelValue: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const itemsData = computed({
|
||||
get: () => (props.modelValue?.length ? props.modelValue : props.items),
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<template v-for="item in items" :key="item.title">
|
||||
<template v-for="item in itemsData" :key="item.title">
|
||||
<Card :title="item.title" class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-xl">{{ item.title }}</CardTitle>
|
||||
|
|
|
|||
|
|
@ -39,9 +39,18 @@ interface WorkbenchQuickNavItem {
|
|||
url?: string;
|
||||
}
|
||||
|
||||
interface WorkbenchQuickDataShowItem {
|
||||
name: string;
|
||||
value: number;
|
||||
prefix: string;
|
||||
decimals: number;
|
||||
routerName: string;
|
||||
}
|
||||
|
||||
export type {
|
||||
AnalysisOverviewItem,
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickDataShowItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export { default as WorkbenchHeader } from './workbench-header.vue';
|
||||
export { default as WorkbenchProject } from './workbench-project.vue';
|
||||
export { default as WorkbenchQuickDataShow } from './workbench-quick-data-show.vue';
|
||||
export { default as WorkbenchQuickNav } from './workbench-quick-nav.vue';
|
||||
export { default as WorkbenchTodo } from './workbench-todo.vue';
|
||||
export { default as WorkbenchTrends } from './workbench-trends.vue';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import type { WorkbenchQuickDataShowItem } from '../typing';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { CountTo } from '@vben/common-ui';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
items?: WorkbenchQuickDataShowItem[];
|
||||
modelValue?: WorkbenchQuickDataShowItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkbenchQuickDataShow',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
modelValue: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 使用计算属性实现双向绑定
|
||||
const itemsData = computed({
|
||||
get: () => (props.modelValue?.length ? props.modelValue : props.items),
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader class="py-4">
|
||||
<CardTitle class="text-lg">{{ title }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-wrap p-0">
|
||||
<template v-for="(item, index) in itemsData" :key="item.name">
|
||||
<div
|
||||
:class="{
|
||||
'border-r-0': index % 4 === 3,
|
||||
'border-b-0': index < 4,
|
||||
'pb-4': index > 4,
|
||||
'rounded-bl-xl': index === itemsData.length - 4,
|
||||
'rounded-br-xl': index === itemsData.length - 1,
|
||||
}"
|
||||
class="flex-col-center group w-1/4 cursor-pointer py-9"
|
||||
>
|
||||
<div class="mb-2 flex justify-center">
|
||||
<CountTo
|
||||
:prefix="item.prefix || ''"
|
||||
:end-val="Number(item.value)"
|
||||
:decimals="item.decimals || 0"
|
||||
class="text-4xl font-normal"
|
||||
/>
|
||||
</div>
|
||||
<span class="truncate text-base text-gray-500">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
Loading…
Reference in New Issue