feat:【mall 商城】商城首页的迁移【antd】10%:初始化
This commit is contained in:
@@ -1,159 +1,45 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type {
|
|
||||||
AnalysisOverviewItem,
|
|
||||||
WorkbenchProjectItem,
|
|
||||||
WorkbenchQuickNavItem,
|
|
||||||
} from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import {
|
import { Col, Row } from 'ant-design-vue';
|
||||||
AnalysisOverview,
|
|
||||||
DocAlert,
|
|
||||||
Page,
|
|
||||||
WorkbenchQuickNav,
|
|
||||||
} from '@vben/common-ui';
|
|
||||||
import {
|
|
||||||
SvgBellIcon,
|
|
||||||
SvgCakeIcon,
|
|
||||||
SvgCardIcon,
|
|
||||||
SvgDownloadIcon,
|
|
||||||
} from '@vben/icons';
|
|
||||||
import { isString, openWindow } from '@vben/utils';
|
|
||||||
|
|
||||||
import { getUserCountComparison } from '#/api/mall/statistics/member';
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
import { getOrderComparison } from '#/api/mall/statistics/trade';
|
import { fenToYuan } from '@vben/utils';
|
||||||
|
|
||||||
|
import * as MemberStatisticsApi from '#/api/mall/statistics/member';
|
||||||
|
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||||
|
|
||||||
|
import ComparisonCard from './modules/comparison-card.vue';
|
||||||
|
import MemberFunnelCard from './modules/member-funnel-card.vue';
|
||||||
|
import MemberStatisticsCard from './modules/member-statistics-card.vue';
|
||||||
|
import MemberTerminalCard from './modules/member-terminal-card.vue';
|
||||||
|
import OperationDataCard from './modules/operation-data-card.vue';
|
||||||
|
import ShortcutCard from './modules/shortcut-card.vue';
|
||||||
|
import TradeTrendCard from './modules/trade-trend-card.vue';
|
||||||
|
|
||||||
/** 商城首页 */
|
/** 商城首页 */
|
||||||
defineOptions({ name: 'MallHome' });
|
defineOptions({ name: 'MallHome' });
|
||||||
|
|
||||||
const loading = ref(true); // 加载中
|
const loading = ref(true); // 加载中
|
||||||
const orderComparison = ref(); // 交易对照数据
|
const orderComparison = ref<any>(); // 交易对照数据
|
||||||
const userComparison = ref(); // 用户对照数据
|
const userComparison = ref<any>(); // 用户对照数据
|
||||||
|
|
||||||
/** 查询交易对照卡片数据 */
|
/** 查询交易对照卡片数据 */
|
||||||
const getOrder = async () => {
|
const getOrderComparison = async () => {
|
||||||
orderComparison.value = await getOrderComparison();
|
orderComparison.value = await TradeStatisticsApi.getOrderComparison();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 查询会员用户数量对照卡片数据 */
|
/** 查询会员用户数量对照卡片数据 */
|
||||||
const getUserCount = async () => {
|
const getUserCountComparison = async () => {
|
||||||
userComparison.value = await getUserCountComparison();
|
userComparison.value = await MemberStatisticsApi.getUserCountComparison();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await Promise.all([getOrder(), getUserCount()]);
|
await Promise.all([getOrderComparison(), getUserCountComparison()]);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
|
||||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
|
||||||
{
|
|
||||||
color: '#1fdaca',
|
|
||||||
icon: 'ep:user-filled',
|
|
||||||
title: '用户管理',
|
|
||||||
url: 'MemberUser',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#ff6b6b',
|
|
||||||
icon: 'fluent-mdl2:product',
|
|
||||||
title: '商品管理',
|
|
||||||
url: 'ProductSpu',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#7c3aed',
|
|
||||||
icon: 'ep:list',
|
|
||||||
title: '订单管理',
|
|
||||||
url: 'TradeOrder',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#3fb27f',
|
|
||||||
icon: 'ri:refund-2-line',
|
|
||||||
title: '售后管理',
|
|
||||||
url: 'TradeAfterSale',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#4daf1bc9',
|
|
||||||
icon: 'fa-solid:project-diagram',
|
|
||||||
title: '分销管理',
|
|
||||||
url: 'TradeBrokerageUser',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#1a73e8',
|
|
||||||
icon: 'ep:ticket',
|
|
||||||
title: '优惠券',
|
|
||||||
url: 'PromotionCoupon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#4daf1bc9',
|
|
||||||
icon: 'fa:group',
|
|
||||||
title: '拼团活动',
|
|
||||||
url: 'PromotionBargainActivity',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#1a73e8',
|
|
||||||
icon: 'vaadin:money-withdraw',
|
|
||||||
title: '佣金提现',
|
|
||||||
url: 'TradeBrokerageWithdraw',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#1a73e8',
|
|
||||||
icon: 'vaadin:money-withdraw',
|
|
||||||
title: '数据统计',
|
|
||||||
url: 'TradeBrokerageWithdraw',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|
||||||
if (nav.url?.startsWith('http')) {
|
|
||||||
openWindow(nav.url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (nav.url?.startsWith('/')) {
|
|
||||||
router.push(nav.url).catch((error) => {
|
|
||||||
console.error('Navigation failed:', error);
|
|
||||||
});
|
|
||||||
} else if (isString(nav.url)) {
|
|
||||||
router.push({ name: nav.url });
|
|
||||||
} else {
|
|
||||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -164,14 +50,71 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|||||||
url="https://doc.iocoder.cn/mall/build/"
|
url="https://doc.iocoder.cn/mall/build/"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<AnalysisOverview :items="overviewItems" />
|
|
||||||
<div class="mt-5 w-full lg:w-2/5">
|
<div class="flex flex-col gap-4">
|
||||||
<WorkbenchQuickNav
|
<!-- 数据对照 -->
|
||||||
:items="quickNavItems"
|
<Row :gutter="16">
|
||||||
class="mt-5 lg:mt-0"
|
<Col :md="6" :sm="12" :xs="24">
|
||||||
title="快捷导航"
|
<ComparisonCard
|
||||||
@click="navTo"
|
tag="今日"
|
||||||
/>
|
title="销售额"
|
||||||
|
prefix="¥"
|
||||||
|
:decimals="2"
|
||||||
|
:value="fenToYuan(orderComparison?.value?.orderPayPrice || 0)"
|
||||||
|
:reference="fenToYuan(orderComparison?.reference?.orderPayPrice || 0)"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :md="6" :sm="12" :xs="24">
|
||||||
|
<ComparisonCard
|
||||||
|
tag="今日"
|
||||||
|
title="用户访问量"
|
||||||
|
:value="userComparison?.value?.visitUserCount || 0"
|
||||||
|
:reference="userComparison?.reference?.visitUserCount || 0"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :md="6" :sm="12" :xs="24">
|
||||||
|
<ComparisonCard
|
||||||
|
tag="今日"
|
||||||
|
title="订单量"
|
||||||
|
:value="orderComparison?.value?.orderPayCount || 0"
|
||||||
|
:reference="orderComparison?.reference?.orderPayCount || 0"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col :md="6" :sm="12" :xs="24">
|
||||||
|
<ComparisonCard
|
||||||
|
tag="今日"
|
||||||
|
title="新增用户"
|
||||||
|
:value="userComparison?.value?.registerUserCount || 0"
|
||||||
|
:reference="userComparison?.reference?.registerUserCount || 0"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- 快捷入口和运营数据 -->
|
||||||
|
<Row :gutter="16">
|
||||||
|
<Col :md="12" :xs="24">
|
||||||
|
<ShortcutCard />
|
||||||
|
</Col>
|
||||||
|
<Col :md="12" :xs="24">
|
||||||
|
<OperationDataCard />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- 会员概览和会员终端 -->
|
||||||
|
<Row :gutter="16">
|
||||||
|
<Col :md="18" :sm="24" :xs="24">
|
||||||
|
<MemberFunnelCard />
|
||||||
|
</Col>
|
||||||
|
<Col :md="6" :sm="24" :xs="24">
|
||||||
|
<MemberTerminalCard />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- 交易量趋势 -->
|
||||||
|
<TradeTrendCard />
|
||||||
|
|
||||||
|
<!-- 会员统计 -->
|
||||||
|
<MemberStatisticsCard />
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { Card, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { erpCalculatePercentage } from '@vben/utils';
|
||||||
|
|
||||||
|
/** 交易对照卡片 */
|
||||||
|
defineOptions({ name: 'ComparisonCard' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
tag?: string;
|
||||||
|
prefix?: string;
|
||||||
|
value: number | string;
|
||||||
|
reference: number | string;
|
||||||
|
decimals?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
tag: '',
|
||||||
|
prefix: '',
|
||||||
|
decimals: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算环比
|
||||||
|
const percent = computed(() => {
|
||||||
|
const refValue = Number(props.reference);
|
||||||
|
const curValue = Number(props.value);
|
||||||
|
if (!refValue || refValue === 0) return 0;
|
||||||
|
return ((curValue - refValue) / refValue) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化数值
|
||||||
|
const formattedValue = computed(() => {
|
||||||
|
const numValue = Number(props.value);
|
||||||
|
return numValue.toFixed(props.decimals);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedReference = computed(() => {
|
||||||
|
const numValue = Number(props.reference);
|
||||||
|
return numValue.toFixed(props.decimals);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" class="comparison-card">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between text-gray-500">
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<Tag v-if="tag">{{ tag }}</Tag>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-baseline justify-between">
|
||||||
|
<div class="text-3xl font-semibold">
|
||||||
|
{{ prefix }}{{ formattedValue }}
|
||||||
|
</div>
|
||||||
|
<span :class="percent > 0 ? 'text-red-500' : 'text-green-500'">
|
||||||
|
{{ Math.abs(percent).toFixed(2) }}%
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="percent > 0 ? 'ep:caret-top' : 'ep:caret-bottom'"
|
||||||
|
class="text-sm"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 border-t border-gray-200 pt-2">
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-500">昨日数据</span>
|
||||||
|
<span>{{ prefix }}{{ formattedReference }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.comparison-card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
151
apps/web-antd/src/views/mall/home/modules/member-funnel-card.vue
Normal file
151
apps/web-antd/src/views/mall/home/modules/member-funnel-card.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Card, DatePicker } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { erpCalculatePercentage, fenToYuan } from '@vben/utils';
|
||||||
|
|
||||||
|
import * as MemberStatisticsApi from '#/api/mall/statistics/member';
|
||||||
|
|
||||||
|
/** 会员概览卡片 */
|
||||||
|
defineOptions({ name: 'MemberFunnelCard' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const analyseData = ref<any>();
|
||||||
|
|
||||||
|
/** 查询会员概览数据列表 */
|
||||||
|
const handleTimeRangeChange = async (times: [Dayjs, Dayjs]) => {
|
||||||
|
if (!times || times.length !== 2) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// 查询数据
|
||||||
|
analyseData.value = await MemberStatisticsApi.getMemberAnalyse({ times });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 计算环比增长率 */
|
||||||
|
const calculateRelativeRate = (value?: number, reference?: number) => {
|
||||||
|
if (!reference || reference === 0) return 0;
|
||||||
|
return (((value || 0) - reference) / reference) * 100;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" :loading="loading">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>会员概览</span>
|
||||||
|
<DatePicker.RangePicker @change="handleTimeRangeChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="min-w-[900px] py-7">
|
||||||
|
<div class="relative flex h-24">
|
||||||
|
<div class="flex h-full w-[75%] bg-blue-50">
|
||||||
|
<div class="ml-15 flex h-full flex-col justify-center">
|
||||||
|
<div class="font-bold">
|
||||||
|
注册用户数量:{{
|
||||||
|
analyseData?.comparison?.value?.registerUserCount || 0
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm">
|
||||||
|
环比增长率:{{
|
||||||
|
calculateRelativeRate(
|
||||||
|
analyseData?.comparison?.value?.registerUserCount,
|
||||||
|
analyseData?.comparison?.reference?.registerUserCount,
|
||||||
|
).toFixed(2)
|
||||||
|
}}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trapezoid1 -ml-[154px] mt-1.5 flex h-full w-[308px] flex-col items-center justify-center bg-blue-500 text-sm text-white"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-bold">{{
|
||||||
|
analyseData?.visitUserCount || 0
|
||||||
|
}}</span>
|
||||||
|
<span>访客</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex h-24">
|
||||||
|
<div class="flex h-full w-[75%] bg-cyan-50">
|
||||||
|
<div class="ml-15 flex h-full flex-col justify-center">
|
||||||
|
<div class="font-bold">
|
||||||
|
活跃用户数量:{{
|
||||||
|
analyseData?.comparison?.value?.visitUserCount || 0
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm">
|
||||||
|
环比增长率:{{
|
||||||
|
calculateRelativeRate(
|
||||||
|
analyseData?.comparison?.value?.visitUserCount,
|
||||||
|
analyseData?.comparison?.reference?.visitUserCount,
|
||||||
|
).toFixed(2)
|
||||||
|
}}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trapezoid2 -ml-[112px] mt-[6.8px] flex h-[100px] w-[224px] flex-col items-center justify-center bg-cyan-500 text-sm text-white"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-bold">{{
|
||||||
|
analyseData?.orderUserCount || 0
|
||||||
|
}}</span>
|
||||||
|
<span>下单</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex h-24">
|
||||||
|
<div class="flex w-[75%] bg-slate-50">
|
||||||
|
<div class="ml-15 flex h-full flex-row gap-x-16">
|
||||||
|
<div class="flex flex-col justify-center">
|
||||||
|
<div class="font-bold">
|
||||||
|
充值用户数量:{{
|
||||||
|
analyseData?.comparison?.value?.rechargeUserCount || 0
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm">
|
||||||
|
环比增长率:{{
|
||||||
|
calculateRelativeRate(
|
||||||
|
analyseData?.comparison?.value?.rechargeUserCount,
|
||||||
|
analyseData?.comparison?.reference?.rechargeUserCount,
|
||||||
|
).toFixed(2)
|
||||||
|
}}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-center">
|
||||||
|
<div class="font-bold">
|
||||||
|
客单价:{{ fenToYuan(analyseData?.atv || 0) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trapezoid3 -ml-[72px] mt-[13px] flex h-[92px] w-[144px] flex-col items-center justify-center bg-slate-500 text-sm text-white"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-bold">{{
|
||||||
|
analyseData?.payUserCount || 0
|
||||||
|
}}</span>
|
||||||
|
<span>成交用户</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.trapezoid1 {
|
||||||
|
transform: perspective(5em) rotateX(-11deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trapezoid2 {
|
||||||
|
transform: perspective(7em) rotateX(-20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trapezoid3 {
|
||||||
|
transform: perspective(3em) rotateX(-13deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Card, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import * as MemberStatisticsApi from '#/api/mall/statistics/member';
|
||||||
|
|
||||||
|
import { getMemberStatisticsChartOptions } from './member-statistics-chart-options';
|
||||||
|
|
||||||
|
/** 会员用户统计卡片 */
|
||||||
|
defineOptions({ name: 'MemberStatisticsCard' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
const getMemberRegisterCountList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// 查询最近一月数据
|
||||||
|
const beginTime = dayjs().subtract(30, 'd').startOf('d');
|
||||||
|
const endTime = dayjs().endOf('d');
|
||||||
|
const list = await MemberStatisticsApi.getMemberRegisterCountList(
|
||||||
|
beginTime,
|
||||||
|
endTime,
|
||||||
|
);
|
||||||
|
// 更新 Echarts 数据
|
||||||
|
await renderEcharts(getMemberStatisticsChartOptions(list));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getMemberRegisterCountList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" title="用户统计">
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
<EchartsUI ref="chartRef" class="h-[300px] w-full" />
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员统计图表配置
|
||||||
|
*/
|
||||||
|
export function getMemberStatisticsChartOptions(list: any[]): any {
|
||||||
|
return {
|
||||||
|
dataset: {
|
||||||
|
dimensions: ['date', 'count'],
|
||||||
|
source: list,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
bottom: 20,
|
||||||
|
top: 80,
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 50,
|
||||||
|
},
|
||||||
|
series: [{ name: '注册量', type: 'line', smooth: true, areaStyle: {} }],
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
// 数据区域缩放
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: false, // Y轴不缩放
|
||||||
|
},
|
||||||
|
brush: {
|
||||||
|
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||||
|
},
|
||||||
|
saveAsImage: { show: true, name: '会员统计' }, // 保存为图片
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
},
|
||||||
|
padding: [5, 10],
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (date: string) => dayjs(date).format('MM-DD'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Card, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import * as MemberStatisticsApi from '#/api/mall/statistics/member';
|
||||||
|
|
||||||
|
import { getTerminalChartOptions } from './member-terminal-chart-options';
|
||||||
|
|
||||||
|
/** 会员终端卡片 */
|
||||||
|
defineOptions({ name: 'MemberTerminalCard' });
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
/** 按照终端,查询会员统计列表 */
|
||||||
|
const getMemberTerminalStatisticsList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await MemberStatisticsApi.getMemberTerminalStatisticsList();
|
||||||
|
const dictDataList = getDictOptions('terminal');
|
||||||
|
const chartData = dictDataList.map((dictData: any) => {
|
||||||
|
const userCount = list.find(
|
||||||
|
(item: any) => item.terminal === dictData.value,
|
||||||
|
)?.userCount;
|
||||||
|
return {
|
||||||
|
name: dictData.label,
|
||||||
|
value: userCount || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await renderEcharts(getTerminalChartOptions(chartData));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getMemberTerminalStatisticsList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" title="会员终端">
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
<EchartsUI ref="chartRef" class="h-[300px] w-full" />
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 会员终端统计图配置
|
||||||
|
*/
|
||||||
|
export function getTerminalChartOptions(data: any[]): any {
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
confine: true,
|
||||||
|
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'right',
|
||||||
|
},
|
||||||
|
roseType: 'area',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '会员终端',
|
||||||
|
type: 'pie',
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted, reactive } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { CountTo } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import * as ProductSpuApi from '#/api/mall/product/spu';
|
||||||
|
import * as PayStatisticsApi from '#/api/mall/statistics/pay';
|
||||||
|
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||||
|
|
||||||
|
/** 运营数据卡片 */
|
||||||
|
defineOptions({ name: 'OperationDataCard' });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** 数据 */
|
||||||
|
const data = reactive({
|
||||||
|
orderUndelivered: { name: '待发货订单', value: 0, routerName: 'TradeOrder' },
|
||||||
|
orderAfterSaleApply: {
|
||||||
|
name: '退款中订单',
|
||||||
|
value: 0,
|
||||||
|
routerName: 'TradeAfterSale',
|
||||||
|
},
|
||||||
|
orderWaitePickUp: { name: '待核销订单', value: 0, routerName: 'TradeOrder' },
|
||||||
|
productAlertStock: { name: '库存预警', value: 0, routerName: 'ProductSpu' },
|
||||||
|
productForSale: { name: '上架商品', value: 0, routerName: 'ProductSpu' },
|
||||||
|
productInWarehouse: { name: '仓库商品', value: 0, routerName: 'ProductSpu' },
|
||||||
|
withdrawAuditing: {
|
||||||
|
name: '提现待审核',
|
||||||
|
value: 0,
|
||||||
|
routerName: 'TradeBrokerageWithdraw',
|
||||||
|
},
|
||||||
|
rechargePrice: {
|
||||||
|
name: '账户充值',
|
||||||
|
value: 0.0,
|
||||||
|
prefix: '¥',
|
||||||
|
decimals: 2,
|
||||||
|
routerName: 'PayWalletRecharge',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查询订单数据 */
|
||||||
|
const getOrderData = async () => {
|
||||||
|
const orderCount = await TradeStatisticsApi.getOrderCount();
|
||||||
|
if (orderCount.undelivered != null) {
|
||||||
|
data.orderUndelivered.value = orderCount.undelivered;
|
||||||
|
}
|
||||||
|
if (orderCount.afterSaleApply != null) {
|
||||||
|
data.orderAfterSaleApply.value = orderCount.afterSaleApply;
|
||||||
|
}
|
||||||
|
if (orderCount.pickUp != null) {
|
||||||
|
data.orderWaitePickUp.value = orderCount.pickUp;
|
||||||
|
}
|
||||||
|
if (orderCount.auditingWithdraw != null) {
|
||||||
|
data.withdrawAuditing.value = orderCount.auditingWithdraw;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询商品数据 */
|
||||||
|
const getProductData = async () => {
|
||||||
|
const productCount = await ProductSpuApi.getTabsCount();
|
||||||
|
data.productForSale.value = productCount['0'];
|
||||||
|
data.productInWarehouse.value = productCount['1'];
|
||||||
|
data.productAlertStock.value = productCount['3'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询钱包充值数据 */
|
||||||
|
const getWalletRechargeData = async () => {
|
||||||
|
const paySummary = await PayStatisticsApi.getWalletRechargePrice();
|
||||||
|
data.rechargePrice.value = paySummary.rechargePrice;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到对应页面
|
||||||
|
*
|
||||||
|
* @param routerName 路由页面组件的名称
|
||||||
|
*/
|
||||||
|
const handleClick = (routerName: string) => {
|
||||||
|
router.push({ name: routerName });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 激活时 */
|
||||||
|
onActivated(() => {
|
||||||
|
getOrderData();
|
||||||
|
getProductData();
|
||||||
|
getWalletRechargeData();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getOrderData();
|
||||||
|
getProductData();
|
||||||
|
getWalletRechargeData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" title="运营数据">
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-8 p-4">
|
||||||
|
<div
|
||||||
|
v-for="item in data"
|
||||||
|
:key="item.name"
|
||||||
|
class="flex h-20 w-[20%] cursor-pointer flex-col items-center justify-center gap-2"
|
||||||
|
@click="handleClick(item.routerName)"
|
||||||
|
>
|
||||||
|
<CountTo
|
||||||
|
:decimals="item.decimals || 0"
|
||||||
|
:end-val="item.value"
|
||||||
|
:prefix="item.prefix || ''"
|
||||||
|
class="text-3xl"
|
||||||
|
/>
|
||||||
|
<span class="text-center">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
95
apps/web-antd/src/views/mall/home/modules/shortcut-card.vue
Normal file
95
apps/web-antd/src/views/mall/home/modules/shortcut-card.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
/** 快捷入口卡片 */
|
||||||
|
defineOptions({ name: 'ShortcutCard' });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** 菜单列表 */
|
||||||
|
const menuList = [
|
||||||
|
{
|
||||||
|
name: '用户管理',
|
||||||
|
icon: 'ep:user-filled',
|
||||||
|
bgColor: 'bg-red-400',
|
||||||
|
routerName: 'MemberUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '商品管理',
|
||||||
|
icon: 'fluent-mdl2:product',
|
||||||
|
bgColor: 'bg-orange-400',
|
||||||
|
routerName: 'ProductSpu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '订单管理',
|
||||||
|
icon: 'ep:list',
|
||||||
|
bgColor: 'bg-yellow-500',
|
||||||
|
routerName: 'TradeOrder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '售后管理',
|
||||||
|
icon: 'ri:refund-2-line',
|
||||||
|
bgColor: 'bg-green-600',
|
||||||
|
routerName: 'TradeAfterSale',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '分销管理',
|
||||||
|
icon: 'fa-solid:project-diagram',
|
||||||
|
bgColor: 'bg-cyan-500',
|
||||||
|
routerName: 'TradeBrokerageUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '优惠券',
|
||||||
|
icon: 'ep:ticket',
|
||||||
|
bgColor: 'bg-blue-500',
|
||||||
|
routerName: 'PromotionCoupon',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '拼团活动',
|
||||||
|
icon: 'fa:group',
|
||||||
|
bgColor: 'bg-purple-500',
|
||||||
|
routerName: 'PromotionBargainActivity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '佣金提现',
|
||||||
|
icon: 'vaadin:money-withdraw',
|
||||||
|
bgColor: 'bg-rose-500',
|
||||||
|
routerName: 'TradeBrokerageWithdraw',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到菜单对应页面
|
||||||
|
*
|
||||||
|
* @param routerName 路由页面组件的名称
|
||||||
|
*/
|
||||||
|
const handleMenuClick = (routerName: string) => {
|
||||||
|
router.push({ name: routerName });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" title="快捷入口">
|
||||||
|
<div class="flex flex-row flex-wrap gap-8 p-4">
|
||||||
|
<div
|
||||||
|
v-for="menu in menuList"
|
||||||
|
:key="menu.name"
|
||||||
|
class="flex h-20 w-[20%] cursor-pointer flex-col items-center justify-center gap-2"
|
||||||
|
@click="handleMenuClick(menu.routerName)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="menu.bgColor"
|
||||||
|
class="flex h-12 w-12 items-center justify-center rounded text-white"
|
||||||
|
>
|
||||||
|
<IconifyIcon :icon="menu.icon" class="text-2xl" />
|
||||||
|
</div>
|
||||||
|
<span>{{ menu.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
203
apps/web-antd/src/views/mall/home/modules/trade-trend-card.vue
Normal file
203
apps/web-antd/src/views/mall/home/modules/trade-trend-card.vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Card, Radio, RadioGroup, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import { fenToYuan } from '@vben/utils';
|
||||||
|
|
||||||
|
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||||
|
|
||||||
|
import { getTradeTrendChartOptions } from './trade-trend-chart-options';
|
||||||
|
|
||||||
|
/** 交易量趋势 */
|
||||||
|
defineOptions({ name: 'TradeTrendCard' });
|
||||||
|
|
||||||
|
enum TimeRangeTypeEnum {
|
||||||
|
DAY30 = 1,
|
||||||
|
WEEK = 7,
|
||||||
|
MONTH = 30,
|
||||||
|
YEAR = 365,
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeRangeType = ref(TimeRangeTypeEnum.DAY30); // 日期快捷选择按钮, 默认30天
|
||||||
|
const loading = ref(false);
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
// 时间范围 Map
|
||||||
|
const timeRangeConfig = {
|
||||||
|
[TimeRangeTypeEnum.DAY30]: {
|
||||||
|
name: '30天',
|
||||||
|
seriesCount: 2,
|
||||||
|
},
|
||||||
|
[TimeRangeTypeEnum.WEEK]: {
|
||||||
|
name: '周',
|
||||||
|
seriesCount: 4,
|
||||||
|
},
|
||||||
|
[TimeRangeTypeEnum.MONTH]: {
|
||||||
|
name: '月',
|
||||||
|
seriesCount: 4,
|
||||||
|
},
|
||||||
|
[TimeRangeTypeEnum.YEAR]: {
|
||||||
|
name: '年',
|
||||||
|
seriesCount: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 时间范围类型单选按钮选中 */
|
||||||
|
const handleTimeRangeTypeChange = async () => {
|
||||||
|
// 设置时间范围
|
||||||
|
let beginTime: Dayjs;
|
||||||
|
let endTime: Dayjs;
|
||||||
|
switch (timeRangeType.value) {
|
||||||
|
case TimeRangeTypeEnum.WEEK: {
|
||||||
|
beginTime = dayjs().startOf('week');
|
||||||
|
endTime = dayjs().endOf('week');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TimeRangeTypeEnum.MONTH: {
|
||||||
|
beginTime = dayjs().startOf('month');
|
||||||
|
endTime = dayjs().endOf('month');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TimeRangeTypeEnum.YEAR: {
|
||||||
|
beginTime = dayjs().startOf('year');
|
||||||
|
endTime = dayjs().endOf('year');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TimeRangeTypeEnum.DAY30:
|
||||||
|
default: {
|
||||||
|
beginTime = dayjs().subtract(30, 'day').startOf('d');
|
||||||
|
endTime = dayjs().endOf('d');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 发送时间范围选中事件
|
||||||
|
await getOrderCountTrendComparison(beginTime, endTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查询订单数量趋势对照数据 */
|
||||||
|
const getOrderCountTrendComparison = async (
|
||||||
|
beginTime: dayjs.ConfigType,
|
||||||
|
endTime: dayjs.ConfigType,
|
||||||
|
) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// 查询数据
|
||||||
|
const list = await TradeStatisticsApi.getOrderCountTrendComparison(
|
||||||
|
timeRangeType.value,
|
||||||
|
beginTime,
|
||||||
|
endTime,
|
||||||
|
);
|
||||||
|
// 处理数据
|
||||||
|
const dates: string[] = [];
|
||||||
|
const series: any[] = [];
|
||||||
|
const config = timeRangeConfig[timeRangeType.value];
|
||||||
|
|
||||||
|
if (config.seriesCount === 2) {
|
||||||
|
const orderPayPriceData: number[] = [];
|
||||||
|
const orderPayCountData: number[] = [];
|
||||||
|
for (const item of list) {
|
||||||
|
dates.push(item.value.date);
|
||||||
|
orderPayPriceData.push(fenToYuan(item?.value?.orderPayPrice || 0));
|
||||||
|
orderPayCountData.push(item?.value?.orderPayCount || 0);
|
||||||
|
}
|
||||||
|
series.push(
|
||||||
|
{ name: '订单金额', type: 'bar', smooth: true, data: orderPayPriceData },
|
||||||
|
{
|
||||||
|
name: '订单数量',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
data: orderPayCountData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const refPriceData: number[] = [];
|
||||||
|
const curPriceData: number[] = [];
|
||||||
|
const refCountData: number[] = [];
|
||||||
|
const curCountData: number[] = [];
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
dates.push(item.value.date);
|
||||||
|
refPriceData.push(fenToYuan(item?.reference?.orderPayPrice || 0));
|
||||||
|
curPriceData.push(fenToYuan(item?.value?.orderPayPrice || 0));
|
||||||
|
refCountData.push(item?.reference?.orderPayCount || 0);
|
||||||
|
curCountData.push(item?.value?.orderPayCount || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLabel =
|
||||||
|
timeRangeType.value === TimeRangeTypeEnum.WEEK
|
||||||
|
? ['上周', '本周']
|
||||||
|
: timeRangeType.value === TimeRangeTypeEnum.MONTH
|
||||||
|
? ['上月', '本月']
|
||||||
|
: ['去年', '今年'];
|
||||||
|
|
||||||
|
series.push(
|
||||||
|
{
|
||||||
|
name: `${timeLabel[0]}金额`,
|
||||||
|
type: 'bar',
|
||||||
|
smooth: true,
|
||||||
|
data: refPriceData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${timeLabel[1]}金额`,
|
||||||
|
type: 'bar',
|
||||||
|
smooth: true,
|
||||||
|
data: curPriceData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${timeLabel[0]}数量`,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
data: refCountData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${timeLabel[1]}数量`,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
data: curCountData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderEcharts(
|
||||||
|
getTradeTrendChartOptions(dates, series, timeRangeType.value),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
handleTimeRangeTypeChange();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>交易量趋势</span>
|
||||||
|
<RadioGroup v-model:value="timeRangeType" @change="handleTimeRangeTypeChange">
|
||||||
|
<Radio
|
||||||
|
v-for="[key, value] in Object.entries(timeRangeConfig)"
|
||||||
|
:key="key"
|
||||||
|
:value="Number(key)"
|
||||||
|
>
|
||||||
|
{{ value.name }}
|
||||||
|
</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
<EchartsUI ref="chartRef" class="h-[300px] w-full" />
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易量趋势图表配置
|
||||||
|
*/
|
||||||
|
export function getTradeTrendChartOptions(
|
||||||
|
dates: string[],
|
||||||
|
series: any[],
|
||||||
|
timeRangeType: number,
|
||||||
|
): any {
|
||||||
|
return {
|
||||||
|
grid: {
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
bottom: 20,
|
||||||
|
top: 80,
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 50,
|
||||||
|
data: series.map((item) => item.name),
|
||||||
|
},
|
||||||
|
series,
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
// 数据区域缩放
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: false, // Y轴不缩放
|
||||||
|
},
|
||||||
|
brush: {
|
||||||
|
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||||
|
},
|
||||||
|
saveAsImage: { show: true, name: '订单量趋势' }, // 保存为图片
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
},
|
||||||
|
padding: [5, 10],
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
inverse: true,
|
||||||
|
boundaryGap: false,
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data: dates,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (date: string) => {
|
||||||
|
switch (timeRangeType) {
|
||||||
|
case 1: // DAY30
|
||||||
|
return dayjs(date).format('MM-DD');
|
||||||
|
case 7: // WEEK
|
||||||
|
{
|
||||||
|
const weekDay = dayjs(date).day();
|
||||||
|
return weekDay === 0 ? '周日' : `周${weekDay}`;
|
||||||
|
}
|
||||||
|
case 30: // MONTH
|
||||||
|
return dayjs(date).format('D');
|
||||||
|
case 365: // YEAR
|
||||||
|
return dayjs(date).format('M') + '月';
|
||||||
|
default:
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user