refactor: 重构商场首页和统计页面组件
- 新等组件 - 优化 Work增 AnalysisOverview、AnalysisOverviewIconbenchQuickDataShow 组件的使用 - 更新图标使用方式,移除自定义 SVG 图标 -提升页面视觉效果 调整布局和样式,
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
39
apps/web-ele/src/views/mall/home/components/data.ts
Normal file
39
apps/web-ele/src/views/mall/home/components/data.ts
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
/** 会员概览卡片 */
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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>
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum TimeRangeTypeEnum {
|
||||
DAY30 = 1,
|
||||
MONTH = 30,
|
||||
WEEK = 7,
|
||||
YEAR = 365,
|
||||
} // 日期类型
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user