refactor: 重构商场首页和统计页面组件

- 新等组件
- 优化 Work增 AnalysisOverview、AnalysisOverviewIconbenchQuickDataShow 组件的使用
- 更新图标使用方式,移除自定义 SVG 图标
-提升页面视觉效果 调整布局和样式,
This commit is contained in:
lrl
2025-07-23 10:51:13 +08:00
parent 27a7e84def
commit 992f0bd2f0
33 changed files with 726 additions and 367 deletions

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
interface Props {
title: string;
}
defineOptions({
name: 'AnalysisChartCard',
});
withDefaults(defineProps<Props>(), {});
</script>
<template>
<el-card>
<template #header>
<div class="my--1.5 flex flex-row items-center justify-between">
<div class="text-xl">{{ title }}</div>
<slot name="header-suffix"></slot>
</div>
</template>
<template #default>
<slot></slot>
</template>
</el-card>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import type { AnalysisOverviewIconItem } from './data';
import { computed } from 'vue';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
interface Props {
items?: AnalysisOverviewIconItem[];
modelValue?: AnalysisOverviewIconItem[];
columnsNumber?: number;
}
defineOptions({
name: 'AnalysisOverview',
});
const props = withDefaults(defineProps<Props>(), {
items: () => [],
modelValue: () => [],
columnsNumber: 4,
});
const emit = defineEmits(['update:modelValue']);
const itemsData = computed({
get: () => (props.modelValue?.length ? props.modelValue : props.items),
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>
<template>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2" :class="gridColumnsClass">
<template v-for="item in itemsData" :key="item.title">
<div
class="flex flex-row items-center gap-3 rounded bg-[var(--el-bg-color-overlay)] p-4"
>
<div
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded"
:class="`${item.iconColor} ${item.iconBgColor}`"
>
<IconifyIcon :icon="item.icon" class="text-2xl" />
</div>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-1 text-gray-500">
<span class="text-sm">{{ item.title }}</span>
<el-tooltip
:content="item.tooltip"
placement="top-start"
v-if="item.tooltip"
>
<IconifyIcon
icon="ep:warning"
class="flex items-center text-sm"
/>
</el-tooltip>
</div>
<div class="flex flex-row items-baseline gap-2">
<div class="text-3xl">
<CountTo
:prefix="item.prefix"
:end-val="item.value"
:decimals="item.decimals"
/>
</div>
<span
v-if="item.percent !== undefined"
:class="
Number(item.percent) > 0 ? 'text-red-500' : 'text-green-500'
"
class="flex items-center whitespace-nowrap"
>
<span class="text-sm">{{ Math.abs(Number(item.percent)) }}%</span>
<IconifyIcon
:icon="
Number(item.percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'
"
class="ml-0.5 text-sm"
/>
</span>
</div>
</div>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,174 @@
<script setup lang="ts">
import type { AnalysisOverviewItem } from './data';
import { computed } from 'vue';
import { VbenCountToAnimator } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
interface Props {
items?: AnalysisOverviewItem[];
modelValue?: AnalysisOverviewItem[];
columnsNumber?: number;
}
defineOptions({
name: 'AnalysisOverview',
});
const props = withDefaults(defineProps<Props>(), {
items: () => [],
modelValue: () => [],
columnsNumber: 4,
});
const emit = defineEmits(['update:modelValue']);
const itemsData = computed({
get: () => (props.modelValue?.length ? props.modelValue : props.items),
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,
};
});
// 计算环比增长率
const calculateGrowthRate = (
currentValue: number,
previousValue: number,
): { isPositive: boolean; rate: number } => {
if (previousValue === 0) {
return { rate: currentValue > 0 ? 100 : 0, isPositive: currentValue >= 0 };
}
const rate = ((currentValue - previousValue) / previousValue) * 100;
return { rate: Math.abs(rate), isPositive: rate >= 0 };
};
// 格式化增长率显示
const formatGrowthRate = (rate: number): string => {
return rate.toFixed(1);
};
</script>
<template>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2" :class="gridColumnsClass">
<template v-for="item in itemsData" :key="item.title">
<el-card :title="item.title" class="w-full">
<template #header>
<div class="text-lg font-semibold">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span>{{ item.title }}</span>
<span v-if="item.tooltip" class="ml-1 inline-block">
<el-tooltip>
<template #default>
<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>
</template>
<template #content>
{{ item.tooltip }}
</template>
</el-tooltip>
</span>
</div>
<el-tag>今日</el-tag>
</div>
</div>
</template>
<template #default>
<!-- 左右布局左边数字右边图标 -->
<div class="flex items-center justify-between">
<!-- 左侧数字显示 -->
<div class="flex-1">
<div class="flex items-baseline">
<!-- prefix 前缀 -->
<span
v-if="item.prefix"
class="mr-1 text-3xl font-medium text-gray-600"
>
{{ item.prefix }}
</span>
<!-- 数字动画 -->
<VbenCountToAnimator
:end-val="item.value"
:start-val="1"
class="text-3xl font-bold text-gray-900"
prefix=""
/>
</div>
</div>
<!-- 右侧环比增长率图标和数值 -->
<div
v-if="item.showGrowthRate && item.totalValue !== undefined"
class="flex items-center space-x-2 rounded-lg bg-gray-50 px-3 py-2"
>
<IconifyIcon
:icon="
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'lucide:trending-up'
: 'lucide:trending-down'
"
class="size-5"
:class="[
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'text-green-500'
: 'text-red-500',
]"
/>
<span
class="text-sm font-semibold"
:class="[
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'text-green-500'
: 'text-red-500',
]"
>
{{
calculateGrowthRate(item.value, item.totalValue).isPositive
? '+'
: '-'
}}{{
formatGrowthRate(
calculateGrowthRate(item.value, item.totalValue).rate,
)
}}%
</span>
</div>
</div>
</template>
<template #footer v-if="item.totalTitle">
<div class="flex items-center justify-between">
<span>{{ item.totalTitle }}</span>
<VbenCountToAnimator
:end-val="item.totalValue"
:start-val="1"
prefix=""
/>
</div>
</template>
</el-card>
</template>
</div>
</template>
<style lang="scss" scoped>
/* 移除 el-card header 的下边框 */
:deep(.el-card__header) {
padding-bottom: 16px;
border-bottom: none !important;
}
</style>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import type { AnalysisOverviewTradeItem } from './data';
import { computed } from 'vue';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
interface Props {
items?: AnalysisOverviewTradeItem[];
modelValue?: AnalysisOverviewTradeItem[];
columnsNumber?: number;
}
defineOptions({
name: 'AnalysisOverview',
});
const props = withDefaults(defineProps<Props>(), {
items: () => [],
modelValue: () => [],
columnsNumber: 4,
});
const emit = defineEmits(['update:modelValue']);
const itemsData = computed({
get: () => (props.modelValue?.length ? props.modelValue : props.items),
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>
<template>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2" :class="gridColumnsClass">
<template v-for="item in itemsData" :key="item.title">
<div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
<div class="flex items-center justify-between text-gray-500">
<span>{{ item.title }}</span>
<el-tooltip
:content="item.tooltip"
placement="top-start"
v-if="item.tooltip"
>
<IconifyIcon icon="ep:warning" />
</el-tooltip>
</div>
<div class="mb-4 text-3xl">
<CountTo
:prefix="item.prefix"
:end-val="item.value"
:decimals="item.decimals"
/>
</div>
<div class="flex flex-row gap-1 text-sm">
<span class="text-gray-500">环比</span>
<span
class="flex items-center gap-0.5 whitespace-nowrap"
:class="
Number(item.percent) > 0 ? 'text-red-500' : 'text-green-500'
"
>
<span>{{ Math.abs(Number(item.percent)) }}%</span>
<IconifyIcon
:icon="
Number(item.percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'
"
class="flex-shrink-0 !text-sm"
/>
</span>
</div>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,39 @@
export interface WorkbenchQuickDataShowItem {
name: string;
value: number;
prefix: string;
decimals: number;
routerName: string;
}
export interface AnalysisOverviewItem {
title: string;
totalTitle?: string;
totalValue?: number;
value: number;
prefix?: string;
tooltip?: string;
// 环比增长相关字段
showGrowthRate?: boolean; // 是否显示环比增长率默认为false
}
export interface AnalysisOverviewIconItem {
icon: string;
title: string;
value: number;
prefix?: string;
iconBgColor: string;
iconColor: string;
tooltip?: string;
decimals?: number;
percent?: number;
}
export interface AnalysisOverviewTradeItem {
title: string;
value: number;
prefix?: string;
decimals?: number;
percent?: number;
tooltip?: string;
}

View File

@@ -3,13 +3,13 @@ import type { MallMemberStatisticsApi } from '#/api/mall/statistics/member';
import { ref } from 'vue';
import { AnalysisChartCard } from '@vben/common-ui';
import { calculateRelativeRate, fenToYuan } from '@vben/utils';
import dayjs from 'dayjs';
import * as MemberStatisticsApi from '#/api/mall/statistics/member';
import AnalysisChartCard from './analysis-chart-card.vue';
import ShortcutDateRangePicker from './shortcut-date-range-picker.vue';
/** 会员概览卡片 */

View File

@@ -10,8 +10,7 @@ import { fenToYuan, formatDate } from '@vben/utils';
import dayjs, { Dayjs } from 'dayjs';
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
import { TimeRangeTypeEnum } from '../data';
import { TimeRangeTypeEnum } from '#/utils/constants';
/** 交易量趋势 */
defineOptions({ name: 'TradeTrendCard' });

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import type { WorkbenchQuickDataShowItem } from './data';
import { computed } from 'vue';
import { CountTo } from '@vben/common-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>
<el-card>
<template #header>
<!-- <CardTitle class="text-lg " >{{ title }}</CardTitle>-->
<div class="text-lg font-semibold">{{ title }}</div>
</template>
<template #default>
<div class="flex flex-wrap p-0">
<div
v-for="(item, index) in itemsData"
:key="item.name"
: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>
</div>
</template>
<!-- <CardContent class="flex flex-wrap p-0">-->
<!-- <template>-->
<!-- -->
<!-- </template>-->
<!-- </CardContent>-->
</el-card>
</template>

View File

@@ -1,6 +0,0 @@
export enum TimeRangeTypeEnum {
DAY30 = 1,
MONTH = 30,
WEEK = 7,
YEAR = 365,
} // 日期类型

View File

@@ -1,27 +1,17 @@
<script lang="ts" setup>
import type {
AnalysisOverviewItem,
WorkbenchProjectItem,
WorkbenchQuickDataShowItem,
WorkbenchQuickNavItem,
} from '@vben/common-ui';
import type { AnalysisOverviewItem } from './components/data';
import type { WorkbenchQuickDataShowItem } from '#/views/mall/home/components/data';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import {
AnalysisOverview,
DocAlert,
Page,
WorkbenchQuickDataShow,
WorkbenchQuickNav,
} from '@vben/common-ui';
import {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import { DocAlert, Page, WorkbenchQuickNav } from '@vben/common-ui';
import { isString, openWindow } from '@vben/utils';
import { getTabsCount } from '#/api/mall/product/spu';
@@ -29,10 +19,12 @@ import { getUserCountComparison } from '#/api/mall/statistics/member';
import { getWalletRechargePrice } from '#/api/mall/statistics/pay';
import { getOrderComparison, getOrderCount } from '#/api/mall/statistics/trade';
import AnalysisOverview from './components/analysis-overview.vue';
import MemberFunnelCard from './components/member-funnel-card.vue';
import MemberStatisticsCard from './components/member-statistics-card.vue';
import MemberTerminalCard from './components/member-terminal-card.vue';
import TradeTrendCard from './components/trade-trend-card.vue';
import WorkbenchQuickDataShow from './components/workbench-quick-data-show.vue';
/** 商城首页 */
defineOptions({ name: 'MallHome' });
@@ -115,7 +107,6 @@ const overviewItems = ref<AnalysisOverviewItem[]>([]);
const loadOverview = () => {
overviewItems.value = [
{
icon: SvgCardIcon,
title: '今日销售额',
totalTitle: '昨日数据',
totalValue: orderComparison.value?.reference?.orderPayPrice || 0,
@@ -123,7 +114,6 @@ const loadOverview = () => {
showGrowthRate: true,
},
{
icon: SvgCakeIcon,
title: '今日用户访问量',
totalTitle: '总访问量',
totalValue: userComparison.value?.reference?.visitUserCount || 0,
@@ -131,15 +121,13 @@ const loadOverview = () => {
showGrowthRate: true,
},
{
icon: SvgDownloadIcon,
title: '今日订单量',
totalTitle: '总订单量',
totalValue: orderComparison.value?.orderPayCount || 0,
value: orderComparison.value?.reference?.orderPayCount || 0,
// 不显示环比增长率
showGrowthRate: true,
},
{
icon: SvgBellIcon,
title: '今日会员注册量',
totalTitle: '总会员注册量',
totalValue: userComparison.value?.registerUserCount || 0,