feat: 新增运营数据展示组件,优化商城首页数据处理逻辑
- 在商城首页引入 WorkbenchQuickDataShow 组件,展示关键运营数据 - 增加数据获取方法,包括订单、商品和钱包充值数据 - 更新 AnalysisOverview 组件以支持双向绑定 - 优化数据加载逻辑,提升用户体验pull/175/head
parent
e88c17f7e2
commit
5edccd3efe
|
|
@ -2,6 +2,7 @@
|
||||||
import type {
|
import type {
|
||||||
AnalysisOverviewItem,
|
AnalysisOverviewItem,
|
||||||
WorkbenchProjectItem,
|
WorkbenchProjectItem,
|
||||||
|
WorkbenchQuickDataShowItem,
|
||||||
WorkbenchQuickNavItem,
|
WorkbenchQuickNavItem,
|
||||||
} from '@vben/common-ui';
|
} from '@vben/common-ui';
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ import {
|
||||||
AnalysisOverview,
|
AnalysisOverview,
|
||||||
DocAlert,
|
DocAlert,
|
||||||
Page,
|
Page,
|
||||||
|
WorkbenchQuickDataShow,
|
||||||
WorkbenchQuickNav,
|
WorkbenchQuickNav,
|
||||||
} from '@vben/common-ui';
|
} from '@vben/common-ui';
|
||||||
import {
|
import {
|
||||||
|
|
@ -22,8 +24,10 @@ import {
|
||||||
} from '@vben/icons';
|
} from '@vben/icons';
|
||||||
import { isString, openWindow } from '@vben/utils';
|
import { isString, openWindow } from '@vben/utils';
|
||||||
|
|
||||||
|
import { getTabsCount } from '#/api/mall/product/spu';
|
||||||
import { getUserCountComparison } from '#/api/mall/statistics/member';
|
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' });
|
defineOptions({ name: 'MallHome' });
|
||||||
|
|
@ -31,6 +35,18 @@ defineOptions({ name: 'MallHome' });
|
||||||
const loading = ref(true); // 加载中
|
const loading = ref(true); // 加载中
|
||||||
const orderComparison = ref(); // 交易对照数据
|
const orderComparison = ref(); // 交易对照数据
|
||||||
const userComparison = 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 () => {
|
const getOrder = async () => {
|
||||||
|
|
@ -42,14 +58,57 @@ const getUserCount = async () => {
|
||||||
userComparison.value = await getUserCountComparison();
|
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 () => {
|
onMounted(async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await Promise.all([getOrder(), getUserCount()]);
|
await Promise.all([
|
||||||
|
getOrder(),
|
||||||
|
getUserCount(),
|
||||||
|
getOrderData(),
|
||||||
|
getProductData(),
|
||||||
|
getWalletRechargeData(),
|
||||||
|
]);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
dataShow.value = true;
|
||||||
|
loadDataShow();
|
||||||
|
loadOverview();
|
||||||
});
|
});
|
||||||
|
|
||||||
const overviewItems: AnalysisOverviewItem[] = [
|
const overviewItems = ref<AnalysisOverviewItem[]>([]);
|
||||||
|
const loadOverview = () => {
|
||||||
|
overviewItems.value = [
|
||||||
{
|
{
|
||||||
icon: SvgCardIcon,
|
icon: SvgCardIcon,
|
||||||
title: '今日销售额',
|
title: '今日销售额',
|
||||||
|
|
@ -79,6 +138,7 @@ const overviewItems: AnalysisOverviewItem[] = [
|
||||||
value: userComparison.value?.reference?.registerUserCount || 0,
|
value: userComparison.value?.reference?.registerUserCount || 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
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();
|
const router = useRouter();
|
||||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||||
if (nav.url?.startsWith('http')) {
|
if (nav.url?.startsWith('http')) {
|
||||||
|
|
@ -164,14 +287,20 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||||
url="https://doc.iocoder.cn/mall/build/"
|
url="https://doc.iocoder.cn/mall/build/"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<AnalysisOverview :items="overviewItems" />
|
<AnalysisOverview v-model:model-value="overviewItems" />
|
||||||
<div class="mt-5 w-full lg:w-2/5">
|
<div class="mt-5 w-full md:flex">
|
||||||
<WorkbenchQuickNav
|
<WorkbenchQuickNav
|
||||||
:items="quickNavItems"
|
:items="quickNavItems"
|
||||||
class="mt-5 lg:mt-0"
|
class="mt-5 md:mr-4 md:mt-0 md:w-1/2"
|
||||||
title="快捷导航"
|
title="快捷导航"
|
||||||
@click="navTo"
|
@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>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AnalysisOverviewItem } from '../typing';
|
import type { AnalysisOverviewItem } from '../typing';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
|
@ -13,20 +15,29 @@ import {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items?: AnalysisOverviewItem[];
|
items?: AnalysisOverviewItem[];
|
||||||
|
modelValue?: AnalysisOverviewItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AnalysisOverview',
|
name: 'AnalysisOverview',
|
||||||
});
|
});
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
items: () => [],
|
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>
|
</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 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">
|
<Card :title="item.title" class="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-xl">{{ item.title }}</CardTitle>
|
<CardTitle class="text-xl">{{ item.title }}</CardTitle>
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,18 @@ interface WorkbenchQuickNavItem {
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WorkbenchQuickDataShowItem {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
prefix: string;
|
||||||
|
decimals: number;
|
||||||
|
routerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AnalysisOverviewItem,
|
AnalysisOverviewItem,
|
||||||
WorkbenchProjectItem,
|
WorkbenchProjectItem,
|
||||||
|
WorkbenchQuickDataShowItem,
|
||||||
WorkbenchQuickNavItem,
|
WorkbenchQuickNavItem,
|
||||||
WorkbenchTodoItem,
|
WorkbenchTodoItem,
|
||||||
WorkbenchTrendItem,
|
WorkbenchTrendItem,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export { default as WorkbenchHeader } from './workbench-header.vue';
|
export { default as WorkbenchHeader } from './workbench-header.vue';
|
||||||
export { default as WorkbenchProject } from './workbench-project.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 WorkbenchQuickNav } from './workbench-quick-nav.vue';
|
||||||
export { default as WorkbenchTodo } from './workbench-todo.vue';
|
export { default as WorkbenchTodo } from './workbench-todo.vue';
|
||||||
export { default as WorkbenchTrends } from './workbench-trends.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