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

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

View File

@ -156,7 +156,7 @@ async function initComponentAdapter() {
'select',
{
component: TreeSelect,
props: { label: 'label', value: 'value', children: 'children' },
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',

View File

@ -212,13 +212,6 @@ async function initComponentAdapter() {
'select',
{
component: ElCascader,
props: {
props: {
label: 'label',
value: 'value',
children: 'children',
},
},
},
),
ApiTreeSelect: withDefaultPlaceholder(
@ -229,7 +222,6 @@ async function initComponentAdapter() {
'select',
{
component: ElTreeSelect,
props: { label: 'label', children: 'children' },
nodeKey: 'value',
loadingSlot: 'loading',
optionsPropName: 'data',

View File

@ -201,6 +201,16 @@ export const PayOrderStatusEnum = {
};
// ========== MALL - 商品模块 ==========
/**
*
*/
export enum TimeRangeTypeEnum {
DAY30 = 1,
MONTH = 30,
WEEK = 7,
YEAR = 365,
}
/**
* SPU
*/

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

@ -1,12 +1,10 @@
<script setup lang="ts">
import type { WorkbenchQuickDataShowItem } from '../typing';
import type { WorkbenchQuickDataShowItem } from './data';
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[];
@ -34,13 +32,16 @@ const itemsData = computed({
</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">
<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,
@ -60,7 +61,12 @@ const itemsData = computed({
</div>
<span class="truncate text-base text-gray-500">{{ item.name }}</span>
</div>
</template>
</CardContent>
</Card>
</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,

View File

@ -1,14 +1,18 @@
<script lang="ts" setup>
import { watch } from 'vue';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import * as ExpressTemplateApi from '#/api/mall/trade/delivery/expressTemplate';
import { watch } from 'vue';
import { ElMessage } from 'element-plus';
import { DICT_TYPE, getIntDictOptions, DeliveryTypeEnum } from '#/utils';
import { DeliveryTypeEnum, DICT_TYPE, getIntDictOptions } from '#/utils';
const props = defineProps<{
propFormData: Object;
}>();
const emit = defineEmits(['update:activeName']);
/** 将传进来的值赋值给 formData */
watch(
() => props.propFormData,
@ -20,7 +24,6 @@ watch(
},
);
const emit = defineEmits(['update:activeName']);
const validate = async () => {
const { valid } = await formApi.validate();
if (!valid) {
@ -29,10 +32,10 @@ const validate = async () => {
try {
//
Object.assign(props.propFormData, formApi.getValues());
} catch (e) {
} catch (error) {
ElMessage.error('【物流设置】不完善,请填写相关信息');
emit('update:activeName', 'delivery');
throw e; //
throw error; //
}
};
defineExpose({ validate });
@ -62,11 +65,8 @@ const [Form, formApi] = useVbenForm({
component: 'ApiSelect',
componentProps: {
api: ExpressTemplateApi.getSimpleTemplateList,
props: {
label: 'name',
value: 'id',
children: 'children',
},
labelField: 'name',
valueField: 'id',
},
rules: 'required',
dependencies: {

View File

@ -1,20 +1,25 @@
<script lang="ts" setup>
import { useVbenForm } from '#/adapter/form';
import { handleTree } from '@vben/utils';
import * as ProductCategoryApi from '#/api/mall/product/category';
import * as ProductBrandApi from '#/api/mall/product/brand';
import { watch } from 'vue';
import { handleTree } from '@vben/utils';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import * as ProductBrandApi from '#/api/mall/product/brand';
import * as ProductCategoryApi from '#/api/mall/product/category';
const props = defineProps<{
propFormData: Object;
}>();
const emit = defineEmits(['update:activeName']);
const getCategoryList = async () => {
const data = await ProductCategoryApi.getCategorySimpleList();
return handleTree(data, 'id');
};
const props = defineProps<{
propFormData: Object;
}>();
/** 将传进来的值赋值给 formData */
watch(
() => props.propFormData,
@ -26,7 +31,6 @@ watch(
},
);
const emit = defineEmits(['update:activeName']);
const validate = async () => {
const { valid } = await formApi.validate();
if (!valid) {
@ -35,10 +39,10 @@ const validate = async () => {
try {
//
Object.assign(props.propFormData, formApi.getValues());
} catch (e) {
} catch (error) {
ElMessage.error('【基础设置】不完善,请填写相关信息');
emit('update:activeName', 'info');
throw e; //
throw error; //
}
};
defineExpose({ validate });
@ -68,11 +72,9 @@ const [Form, formApi] = useVbenForm({
component: 'ApiCascader',
componentProps: {
api: getCategoryList,
props: {
label: 'name',
value: 'id',
children: 'children',
},
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
rules: 'required',
},

View File

@ -24,7 +24,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
const res = await getCategoryList({});
return handleTree(res, 'id', 'parentId', 'children');
},
props: { label: 'name', value: 'id', children: 'children' },
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
},
{

View File

@ -1,14 +1,14 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { MallMemberStatisticsApi } from '#/api/mall/statistics/member'; //
import type { AnalysisOverviewIconItem } from '#/views/mall/home/components/data';
import { onMounted, ref } from 'vue';
import { AnalysisOverview, DocAlert, Page } from '@vben/common-ui';
import { SvgCakeIcon, SvgCardIcon } from '@vben/icons';
import { DocAlert, Page } from '@vben/common-ui';
import { fenToYuan } from '@vben/utils';
import * as MemberStatisticsApi from '#/api/mall/statistics/member'; //
import AnalysisOverviewIcon from '#/views/mall/home/components/analysis-overview-icon.vue';
import MemberFunnelCard from '#/views/mall/home/components/member-funnel-card.vue';
import MemberTerminalCard from '#/views/mall/home/components/member-terminal-card.vue';
@ -17,29 +17,41 @@ import MemberSexCard from './components/member-sex-card.vue';
const summary = ref<MallMemberStatisticsApi.Summary>();
const overviewItems = ref<AnalysisOverviewItem[]>([]);
const overviewItems = ref<AnalysisOverviewIconItem[]>([]);
const loadOverview = async () => {
summary.value = await MemberStatisticsApi.getMemberSummary();
overviewItems.value = [
{
icon: SvgCakeIcon, // -
icon: 'fa-solid:users', // -
title: '累计会员数',
value: summary.value?.userCount || 0,
iconBgColor: 'text-blue-500',
iconColor: 'bg-blue-100',
},
{
icon: SvgCardIcon, // -
icon: 'fa-solid:user', // -
title: '累计充值人数',
value: summary.value?.rechargeUserCount || 0,
iconBgColor: 'text-purple-500',
iconColor: 'bg-purple-100',
},
{
icon: SvgCardIcon, // -
icon: 'fa-solid:money-check-alt', // -
title: '累计充值金额',
value: summary.value?.rechargePrice || 0,
value: Number(fenToYuan(summary.value?.rechargePrice || 0)),
iconBgColor: 'text-yellow-500',
iconColor: 'bg-yellow-100',
prefix: '¥',
decimals: 2,
},
{
icon: SvgCakeIcon, // -
title: '今日会员注册量',
value: summary.value?.expensePrice || 0,
icon: 'fa-solid:yen-sign', // -
title: '累计消费金额',
value: Number(fenToYuan(summary.value?.expensePrice || 0)),
iconBgColor: 'text-green-500',
iconColor: 'bg-green-100',
prefix: '¥',
decimals: 2,
},
];
};
@ -56,7 +68,7 @@ onMounted(async () => {
url="https://doc.iocoder.cn/mall/statistics/"
/>
<div class="mt-5 w-full md:flex">
<AnalysisOverview
<AnalysisOverviewIcon
v-model:model-value="overviewItems"
class="mt-5 md:mr-4 md:mt-0 md:w-full"
/>

View File

@ -1,16 +1,16 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
import type { AnalysisOverviewIconItem } from '#/views/mall/home/components/data';
import { reactive, ref } from 'vue';
import { AnalysisChartCard, AnalysisOverview, confirm } from '@vben/common-ui';
import { SvgCakeIcon, SvgCardIcon, SvgEyeIcon } from '@vben/icons';
import { confirm } from '@vben/common-ui';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import {
calculateRelativeRate,
downloadFileFromBlobPart,
fenToYuan,
formatDate,
@ -20,6 +20,8 @@ import {
import dayjs from 'dayjs';
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
import AnalysisChartCard from '#/views/mall/home/components/analysis-chart-card.vue';
import AnalysisOverviewIcon from '#/views/mall/home/components/analysis-overview-icon.vue';
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
/** 商品概况 */
@ -210,64 +212,88 @@ const handleExport = async () => {
}
};
const overviewItems = ref<AnalysisOverviewItem[]>();
const overviewItems = ref<AnalysisOverviewIconItem[]>();
const loadOverview = () => {
overviewItems.value = [
{
icon: SvgEyeIcon,
icon: 'ep:view',
title: '商品浏览量',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.browseCount || 0,
value: trendSummary.value?.value?.browseCount || 0,
iconColor: 'bg-blue-100',
iconBgColor: 'text-blue-500',
tooltip:
'在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次',
showGrowthRate: true,
percent: calculateRelativeRate(
trendSummary?.value?.value?.browseCount,
trendSummary.value?.reference?.browseCount,
),
},
{
icon: SvgCakeIcon,
icon: 'ep:user-filled',
title: '商品访客数',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.browseUserCount || 0,
value: trendSummary.value?.value?.browseUserCount || 0,
iconColor: 'bg-purple-100',
iconBgColor: 'text-purple-500',
tooltip:
'在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个',
showGrowthRate: true,
percent: calculateRelativeRate(
trendSummary?.value?.value?.browseUserCount,
trendSummary.value?.reference?.browseUserCount,
),
},
{
icon: SvgCakeIcon,
icon: 'fa-solid:money-check-alt',
title: '支付件数',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.orderPayCount || 0,
iconColor: 'bg-yellow-100',
iconBgColor: 'text-yellow-500',
value: trendSummary.value?.value?.orderPayCount || 0,
tooltip: '在选定条件下,成功付款订单的商品件数之和',
showGrowthRate: true,
percent: calculateRelativeRate(
trendSummary?.value?.value?.orderPayCount,
trendSummary.value?.reference?.orderPayCount,
),
},
{
icon: SvgCardIcon,
icon: 'ep:warning-filled',
title: '支付金额',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
value: trendSummary.value?.value?.orderPayPrice || 0,
iconColor: 'bg-green-100',
iconBgColor: 'text-green-500',
prefix: '¥',
value: Number(fenToYuan(trendSummary.value?.value?.orderPayPrice || 0)),
tooltip: '在选定条件下,成功付款订单的商品金额之和',
showGrowthRate: true,
decimals: 2,
percent: calculateRelativeRate(
trendSummary?.value?.value?.orderPayPrice,
trendSummary.value?.reference?.orderPayPrice,
),
},
{
icon: SvgCakeIcon,
icon: 'fa-solid:wallet',
title: '退款件数',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleCount || 0,
iconColor: 'bg-cyan-100',
iconBgColor: 'text-cyan-500',
value: trendSummary.value?.value?.afterSaleCount || 0,
tooltip: '在选定条件下,成功退款的商品件数之和',
showGrowthRate: true,
percent: calculateRelativeRate(
trendSummary?.value?.value?.afterSaleCount,
trendSummary.value?.reference?.afterSaleCount,
),
},
{
icon: SvgCardIcon,
icon: 'fa-solid:award',
title: '退款金额',
totalTitle: '昨日数据',
totalValue: trendSummary.value?.reference?.afterSaleRefundPrice || 0,
value: trendSummary.value?.value?.afterSaleRefundPrice || 0,
iconColor: 'bg-yellow-100',
iconBgColor: 'text-yellow-500',
prefix: '¥',
decimals: 2,
value: Number(
fenToYuan(trendSummary.value?.value?.afterSaleRefundPrice || 0),
),
tooltip: '在选定条件下,成功退款的商品金额之和',
showGrowthRate: true,
percent: calculateRelativeRate(
trendSummary?.value?.value?.afterSaleRefundPrice,
trendSummary.value?.reference?.afterSaleRefundPrice,
),
},
];
};
@ -291,7 +317,7 @@ const loadOverview = () => {
</ShortcutDateRangePicker>
</template>
<!-- 统计值 -->
<AnalysisOverview
<AnalysisOverviewIcon
v-model:model-value="overviewItems"
:columns-number="6"
class="mt-5 md:mr-4 md:mt-0 md:w-full"

View File

@ -11,7 +11,11 @@ import ProductSummary from './components/product-summary.vue';
title="【统计】会员、商品、交易统计"
url="https://doc.iocoder.cn/mall/statistics/"
/>
<ProductSummary class="md:w-3/3 mt-5 md:mr-4 md:mt-0" />
<ProductRank class="md:w-3/3 mt-5 md:mr-4 md:mt-0" />
<div class="mt-5 w-full">
<ProductSummary class="mt-5 md:mr-4 md:mt-0" />
</div>
<div class="mt-5 w-full">
<ProductRank class="mt-5 md:mr-4 md:mt-0" />
</div>
</Page>
</template>

View File

@ -1,16 +1,15 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
import type { AnalysisOverviewIconItem } from '#/views/mall/home/components/data';
import { reactive, ref } from 'vue';
import { AnalysisChartCard, AnalysisOverview } from '@vben/common-ui';
import { SvgCakeIcon, SvgCardIcon } from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import {
calculateRelativeRate,
downloadFileFromBlobPart,
fenToYuan,
formatDate,
@ -21,11 +20,13 @@ import dayjs from 'dayjs';
import { ElMessageBox } from 'element-plus';
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
import AnalysisChartCard from '#/views/mall/home/components/analysis-chart-card.vue';
import AnalysisOverviewIcon from '#/views/mall/home/components/analysis-overview-icon.vue';
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const overviewItems = ref<AnalysisOverviewItem[]>();
const overviewItems = ref<AnalysisOverviewIconItem[]>();
const summary =
ref<MallDataComparisonResp<MallTradeStatisticsApi.TradeTrendSummary>>();
const shortcutDateRangePicker = ref();
@ -34,61 +35,105 @@ const trendLoading = ref(true); // 交易状态加载中
const loadOverview = () => {
overviewItems.value = [
{
icon: SvgCakeIcon,
icon: 'fa-solid:yen-sign',
title: '营业额',
value: summary?.value?.value.turnoverPrice || 0,
value: Number(fenToYuan(summary?.value?.value.turnoverPrice || 0)),
tooltip: '商品支付金额、充值金额',
totalValue: summary?.value?.reference?.turnoverPrice || 0,
showGrowthRate: true,
iconColor: 'bg-blue-100',
iconBgColor: 'text-blue-500',
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.turnoverPrice,
summary?.value?.reference?.turnoverPrice,
),
},
{
icon: SvgCakeIcon,
icon: 'fa-solid:shopping-cart',
title: '商品支付金额',
value: summary.value?.value?.orderPayPrice || 0,
value: Number(fenToYuan(summary.value?.value?.orderPayPrice || 0)),
tooltip:
'用户购买商品的实际支付金额,包括微信支付、余额支付、支付宝支付、线下支付金额(拼团商品在成团之后计入,线下支付订单在后台确认支付后计入)',
totalValue: summary?.value?.reference?.orderPayPrice || 0,
showGrowthRate: true,
iconColor: 'bg-purple-100',
iconBgColor: 'text-purple-500',
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.orderPayPrice,
summary?.value?.reference?.orderPayPrice,
),
},
{
icon: SvgCardIcon,
icon: 'fa-solid:money-check-alt',
title: '充值金额',
value: summary.value?.value?.rechargePrice || 0,
value: Number(fenToYuan(summary.value?.value?.rechargePrice || 0)),
tooltip: '用户成功充值的金额',
totalValue: summary?.value?.reference?.rechargePrice || 0,
showGrowthRate: true,
iconColor: 'bg-yellow-100',
iconBgColor: 'text-yellow-500',
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.rechargePrice,
summary?.value?.reference?.rechargePrice,
),
},
{
icon: SvgCardIcon,
icon: 'ep:warning-filled',
title: '支出金额',
value: summary.value?.value?.expensePrice || 0,
value: Number(fenToYuan(summary.value?.value?.expensePrice || 0)),
tooltip: '余额支付金额、支付佣金金额、商品退款金额',
totalValue: summary?.value?.reference?.expensePrice || 0,
showGrowthRate: true,
iconColor: 'bg-green-100',
iconBgColor: 'text-green-500',
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.expensePrice,
summary?.value?.reference?.expensePrice,
),
},
{
icon: SvgCardIcon,
icon: 'fa-solid:wallet',
title: '余额支付金额',
value: summary.value?.value?.walletPayPrice || 0,
value: Number(fenToYuan(summary.value?.value?.walletPayPrice || 0)),
tooltip: '余额支付金额、支付佣金金额、商品退款金额',
totalValue: summary?.value?.reference?.walletPayPrice || 0,
showGrowthRate: true,
iconColor: 'bg-cyan-100',
iconBgColor: 'text-cyan-500',
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.walletPayPrice,
summary?.value?.reference?.walletPayPrice,
),
},
{
icon: SvgCardIcon,
icon: 'fa-solid:award',
title: '支付佣金金额',
value: summary.value?.value?.brokerageSettlementPrice || 0,
value: Number(
fenToYuan(summary.value?.value?.brokerageSettlementPrice || 0),
),
tooltip: '后台给推广员支付的推广佣金,以实际支付为准',
totalValue: summary?.value?.reference?.brokerageSettlementPrice || 0,
showGrowthRate: true,
iconColor: 'bg-yellow-100',
iconBgColor: 'text-yellow-500',
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.brokerageSettlementPrice,
summary?.value?.reference?.brokerageSettlementPrice,
),
},
{
icon: SvgCardIcon,
icon: 'fa-solid:times-circle',
title: '商品退款金额',
value: summary.value?.value?.afterSaleRefundPrice || 0,
value: Number(fenToYuan(summary.value?.value?.afterSaleRefundPrice || 0)),
tooltip: '用户成功退款的商品金额',
totalValue: summary?.value?.reference?.afterSaleRefundPrice || 0,
showGrowthRate: true,
iconColor: 'bg-blue-100',
iconBgColor: 'text-blue-500',
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.afterSaleRefundPrice,
summary?.value?.reference?.afterSaleRefundPrice,
),
},
];
};
@ -229,7 +274,7 @@ const lineChartOptions = reactive({
</el-button>
</ShortcutDateRangePicker>
</template>
<AnalysisOverview v-model:model-value="overviewItems" />
<AnalysisOverviewIcon v-model:model-value="overviewItems" />
<EchartsUI height="500px" ref="chartRef" />
</AnalysisChartCard>
</template>

View File

@ -1,54 +1,62 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { MallDataComparisonResp } from '#/api/mall/statistics/common';
import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
import type { AnalysisOverviewTradeItem } from '#/views/mall/home/components/data';
import { onMounted, ref } from 'vue';
import { AnalysisOverview, DocAlert, Page } from '@vben/common-ui';
import { SvgCakeIcon, SvgCardIcon } from '@vben/icons';
import { DocAlert, Page } from '@vben/common-ui';
import { calculateRelativeRate, fenToYuan } from '@vben/utils';
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
import analysisTradeOverview from '#/views/mall/home/components/analysis-trade-overview.vue';
import TradeTransactionCard from './components/trade-transaction-card.vue';
const overviewItems = ref<AnalysisOverviewItem[]>();
const overviewItems = ref<AnalysisOverviewTradeItem[]>();
const summary =
ref<MallDataComparisonResp<MallTradeStatisticsApi.TradeSummary>>();
const loadOverview = () => {
overviewItems.value = [
{
icon: SvgCakeIcon,
title: '昨日订单数量',
value: summary.value?.value?.yesterdayOrderCount || 0,
tooltip: '昨日订单数量',
totalValue: summary?.value?.reference?.yesterdayOrderCount,
showGrowthRate: true,
percent: calculateRelativeRate(
summary?.value?.value?.yesterdayOrderCount,
summary.value?.reference?.yesterdayOrderCount,
),
},
{
icon: SvgCakeIcon,
title: '本月订单数量',
value: summary.value?.value?.monthOrderCount || 0,
tooltip: '本月订单数量',
totalValue: summary?.value?.reference?.monthOrderCount,
showGrowthRate: true,
percent: calculateRelativeRate(
summary?.value?.value?.monthOrderCount,
summary.value?.reference?.monthOrderCount,
),
},
{
icon: SvgCardIcon,
title: '昨日支付金额',
value: summary.value?.value?.yesterdayPayPrice || 0,
value: Number(fenToYuan(summary.value?.value?.yesterdayPayPrice || 0)),
tooltip: '昨日支付金额',
totalValue: summary?.value?.reference?.yesterdayPayPrice,
showGrowthRate: true,
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.yesterdayPayPrice,
summary.value?.reference?.yesterdayPayPrice,
),
},
{
icon: SvgCardIcon,
title: '本月支付金额',
value: summary.value?.value?.monthPayPrice || 0,
tooltip: '本月支付金额',
totalValue: summary?.value?.reference?.monthPayPrice,
showGrowthRate: true,
prefix: '¥',
decimals: 2,
percent: calculateRelativeRate(
summary?.value?.value?.monthPayPrice,
summary.value?.reference?.monthPayPrice,
),
},
];
};
@ -73,7 +81,7 @@ onMounted(async () => {
/>
<!-- 统计值 -->
<div class="mb-4 mt-5 w-full md:flex">
<AnalysisOverview
<analysisTradeOverview
v-model:model-value="overviewItems"
class="mt-5 md:mr-4 md:mt-0 md:w-full"
/>

View File

@ -54,10 +54,8 @@ const [Form, formApi] = useVbenForm({
component: 'ApiSelect',
componentProps: {
api: getSimpleDeliveryExpressList,
props: {
label: 'name',
value: 'id',
},
labelField: 'name',
valueField: 'id',
},
dependencies: {
triggerFields: ['expressType'],

View File

@ -35,10 +35,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: getSimpleDeliveryPickUpStoreList,
props: {
label: 'name',
value: 'id',
},
labelField: 'name',
valueField: 'id',
},
dependencies: {
triggerFields: ['deliveryType'],

View File

@ -51,7 +51,9 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiTreeSelect',
componentProps: {
api: () => getAreaTree(),
props: { label: 'name', value: 'id', children: 'children' },
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
},
{
@ -130,7 +132,8 @@ export function useBindFormSchema(): VbenFormSchema[] {
rules: 'required',
componentProps: {
api: () => getSimpleUserList(),
props: { label: 'nickname', value: 'id' },
labelField: 'nickname',
valueField: 'id',
mode: 'tags',
allowClear: true,
},

View File

@ -54,10 +54,8 @@ const [Form, formApi] = useVbenForm({
component: 'ApiSelect',
componentProps: {
api: getSimpleDeliveryExpressList,
props: {
label: 'name',
value: 'id',
},
labelField: 'name',
valueField: 'id',
},
dependencies: {
triggerFields: ['expressType'],

View File

@ -57,11 +57,9 @@ const [Form, formApi] = useVbenForm({
component: 'ApiTreeSelect',
componentProps: {
api: () => getAreaTree(),
props: {
label: 'name',
value: 'id',
children: 'children',
},
labelField: 'name',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择收件人所在地',
treeDefaultExpandAll: true,
},

View File

@ -86,7 +86,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '所在地',
componentProps: {
api: () => getAreaTree(),
props: { label: 'name', value: 'id', children: 'children' },
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
},
{
@ -95,7 +97,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: '用户标签',
componentProps: {
api: () => getSimpleTagList(),
props: { label: 'name', value: 'id' },
labelField: 'name',
valueField: 'id',
mode: 'multiple',
},
},
@ -105,7 +108,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: '用户分组',
componentProps: {
api: () => getSimpleGroupList(),
props: { label: 'name', value: 'id' },
labelField: 'name',
valueField: 'id',
},
},
{
@ -151,7 +155,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: () => getSimpleTagList(),
props: { label: 'name', value: 'id' },
labelField: 'name',
valueField: 'id',
mode: 'multiple',
},
},
@ -161,7 +166,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: () => getSimpleLevelList(),
props: { label: 'name', value: 'id' },
labelField: 'name',
valueField: 'id',
},
},
{
@ -170,7 +176,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: () => getSimpleGroupList(),
props: { label: 'name', value: 'id' },
labelField: 'name',
valueField: 'id',
},
},
];
@ -290,7 +297,8 @@ export function useLeavelFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: () => getSimpleLevelList(),
props: { label: 'name', value: 'id' },
labelField: 'name',
valueField: 'id',
},
},
{

View File

@ -15,10 +15,7 @@ withDefaults(defineProps<Props>(), {});
<template>
<Card>
<CardHeader>
<div class="my--1.5 flex flex-row items-center justify-between">
<CardTitle class="text-xl">{{ title }}</CardTitle>
<slot name="header-suffix"></slot>
</div>
<CardTitle class="text-xl">{{ title }}</CardTitle>
</CardHeader>
<CardContent>
<slot></slot>

View File

@ -1,141 +1,35 @@
<script setup lang="ts">
import type { AnalysisOverviewItem } from '../typing';
import { computed } from 'vue';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
VbenCountToAnimator,
VbenIcon,
} from '@vben-core/shadcn-ui';
interface Props {
items?: AnalysisOverviewItem[];
modelValue?: AnalysisOverviewItem[];
columnsNumber?: number;
}
defineOptions({
name: 'AnalysisOverview',
});
const props = withDefaults(defineProps<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">
<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">
<Card :title="item.title" class="w-full">
<CardHeader>
<CardTitle class="text-xl">
<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">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<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>
</TooltipTrigger>
<TooltipContent>{{ item.tooltip }}</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
</div>
<!-- 环比增长率显示在右上角 -->
<div
v-if="item.showGrowthRate && item.totalValue !== undefined"
class="flex items-center space-x-1"
>
<VbenIcon
:icon="
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'lucide:trending-up'
: 'lucide:trending-down'
"
class="size-4"
:class="[
calculateGrowthRate(item.value, item.totalValue).isPositive
? 'text-green-500'
: 'text-red-500',
]"
/>
<span
class="text-sm font-medium"
: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>
</CardTitle>
<CardTitle class="text-xl">{{ item.title }}</CardTitle>
</CardHeader>
<CardContent class="flex items-center justify-between">
@ -147,7 +41,7 @@ const formatGrowthRate = (rate: number): string => {
/>
<VbenIcon :icon="item.icon" class="size-8 flex-shrink-0" />
</CardContent>
<CardFooter v-if="item.totalTitle" class="justify-between">
<CardFooter class="justify-between">
<span>{{ item.totalTitle }}</span>
<VbenCountToAnimator
:end-val="item.totalValue"

View File

@ -3,12 +3,9 @@ import type { Component } from 'vue';
interface AnalysisOverviewItem {
icon: Component | string;
title: string;
totalTitle?: string;
totalValue?: number;
totalTitle: string;
totalValue: number;
value: number;
tooltip?: string;
// 环比增长相关字段
showGrowthRate?: boolean; // 是否显示环比增长率默认为false
}
interface WorkbenchProjectItem {
@ -42,18 +39,9 @@ interface WorkbenchQuickNavItem {
url?: string;
}
interface WorkbenchQuickDataShowItem {
name: string;
value: number;
prefix: string;
decimals: number;
routerName: string;
}
export type {
AnalysisOverviewItem,
WorkbenchProjectItem,
WorkbenchQuickDataShowItem,
WorkbenchQuickNavItem,
WorkbenchTodoItem,
WorkbenchTrendItem,

View File

@ -1,6 +1,5 @@
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';

View File

@ -1,33 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
<!-- 样式定义 -->
<style>
.eye-outline { fill: #0D47A1; }
.eye-white { fill: #BBDEFB; }
.eye-iris { fill: #2196F3; }
.eye-pupil { fill: #000000; }
.eye-highlight { fill: #FFFFFF; }
.eye-shadow { fill: #1565C0; }
</style>
<!-- 眼睛外轮廓 -->
<path class="eye-outline" d="M200,250c-80,0-160-60-200-100c40-40,120-100,200-100s160,60,200,100C360,190,280,250,200,250z"/>
<!-- 眼睛白色部分 -->
<path class="eye-white" d="M200,70c70,0,140,50,180,80c-40,30-110,80-180,80s-140-50-180-80C60,120,130,70,200,70z"/>
<!-- 眼睑阴影 -->
<path class="eye-shadow" d="M200,90c-60,0-120,40-160,60c40,20,100,60,160,60s120-40,160-60C320,130,260,90,200,90z"/>
<!-- 虹膜 -->
<circle class="eye-iris" cx="200" cy="150" r="60"/>
<!-- 瞳孔 - 确保是明显的黑色圆形 -->
<circle class="eye-pupil" cx="200" cy="150" r="25"/>
<!-- 高光 -->
<circle class="eye-highlight" cx="180" cy="130" r="12"/>
<!-- 装饰线条 -->
<path class="eye-highlight" d="M100,110c10-5,30-15,40-20c3-1,2-5-1-4c-10,5-30,15-40,20C96,107,97,111,100,110z"/>
<path class="eye-highlight" d="M300,190c2-5,5-10,10-15c10-10,20-20,30-25c2-1,0-5-2-4c-15,10-30,30-40,45C297,193,299,195,300,190z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -10,7 +10,6 @@ const SvgDownloadIcon = createIconifyIcon('svg:download');
const SvgCardIcon = createIconifyIcon('svg:card');
const SvgBellIcon = createIconifyIcon('svg:bell');
const SvgCakeIcon = createIconifyIcon('svg:cake');
const SvgEyeIcon = createIconifyIcon('svg:eye');
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
/** AI */
@ -45,7 +44,6 @@ export {
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
SvgEyeIcon,
SvgGptIcon,
SvgMockIcon,
SvgWalletIcon,