diff --git a/src/api/mall/statistics/member.ts b/src/api/mall/statistics/member.ts index d4680d3d..e0d77e40 100644 --- a/src/api/mall/statistics/member.ts +++ b/src/api/mall/statistics/member.ts @@ -54,6 +54,20 @@ export interface MemberTerminalStatisticsRespVO { userCount: number } +/** 会员数量统计 Response VO */ +export interface MemberCountRespVO { + /** 用户访问量 */ + visitUserCount: string + /** 新增用户数量 */ + createUserCount: number +} + +/** 会员注册数量 Response VO */ +export interface MemberRegisterCountRespVO { + date: string + count: number +} + // 查询会员统计 export const getMemberSummary = () => { return request.get<MemberSummaryRespVO>({ @@ -89,3 +103,21 @@ export const getMemberTerminalStatisticsList = () => { url: '/statistics/member/get-terminal-statistics-list' }) } + +// 获得用户数量量对照 +export const getUserCountComparison = () => { + return request.get<TradeStatisticsComparisonRespVO<MemberCountRespVO>>({ + url: '/statistics/member/user-count-comparison' + }) +} + +// 获得会员注册数量列表 +export const getMemberRegisterCountList = ( + beginTime: dayjs.ConfigType, + endTime: dayjs.ConfigType +) => { + return request.get<MemberRegisterCountRespVO[]>({ + url: '/statistics/member/register-count-list', + params: { times: [formatDate(beginTime), formatDate(endTime)] } + }) +} diff --git a/src/api/mall/statistics/pay.ts b/src/api/mall/statistics/pay.ts new file mode 100644 index 00000000..1593f38d --- /dev/null +++ b/src/api/mall/statistics/pay.ts @@ -0,0 +1,6 @@ +import request from '@/config/axios' + +/** 获取钱包充值金额 */ +export const getWalletRechargePrice = async () => { + return await request.get<number>({ url: `/statistics/pay/wallet-recharge-price` }) +} diff --git a/src/api/mall/statistics/trade.ts b/src/api/mall/statistics/trade.ts index f7829ccb..76da08ca 100644 --- a/src/api/mall/statistics/trade.ts +++ b/src/api/mall/statistics/trade.ts @@ -33,6 +33,36 @@ export interface TradeTrendSummaryRespVO { orderRefundPrice: number } +/** 交易订单数量 Response VO */ +export interface TradeOrderCountRespVO { + /** 待发货 */ + undelivered?: number + /** 待核销 */ + pickUp?: number + /** 退款中 */ + afterSaleApply?: number + /** 提现待审核 */ + auditingWithdraw?: number +} + +/** 交易订单统计 Response VO */ +export interface TradeOrderSummaryRespVO { + /** 支付订单商品数 */ + orderPayCount?: number + /** 总支付金额,单位:分 */ + orderPayPrice?: number +} + +/** 订单量趋势统计 Response VO */ +export interface TradeOrderTrendRespVO { + /** 日期 */ + date: string + /** 订单数量 */ + orderPayCount: number + /** 订单支付金额 */ + orderPayPrice: number +} + // 查询交易统计 export const getTradeStatisticsSummary = () => { return request.get<TradeStatisticsComparisonRespVO<TradeSummaryRespVO>>({ @@ -64,6 +94,30 @@ export const exportTradeTrend = (params: TradeTrendReqVO) => { }) } +// 获得交易订单数量 +export const getOrderCount = async () => { + return await request.get<TradeOrderCountRespVO>({ url: `/statistics/trade/order-count` }) +} + +// 获得交易订单数量对照 +export const getOrderComparison = async () => { + return await request.get<TradeStatisticsComparisonRespVO<TradeOrderSummaryRespVO>>({ + url: `/statistics/trade/order-comparison` + }) +} + +// 获得订单量趋势统计 +export const getOrderCountTrendComparison = ( + type: number, + beginTime: dayjs.ConfigType, + endTime: dayjs.ConfigType +) => { + return request.get<TradeStatisticsComparisonRespVO<TradeOrderTrendRespVO>[]>({ + url: '/statistics/trade/order-count-trend', + params: { type, beginTime: formatDate(beginTime), endTime: formatDate(endTime) } + }) +} + /** 时间参数需要格式化, 确保接口能识别 */ const formatDateParam = (params: TradeTrendReqVO) => { return { times: [formatDate(params.times[0]), formatDate(params.times[1])] } as TradeTrendReqVO diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue new file mode 100644 index 00000000..d7fa90cb --- /dev/null +++ b/src/components/ShortcutDateRangePicker/index.vue @@ -0,0 +1,89 @@ +<template> + <div class="flex flex-row items-center gap-2"> + <el-radio-group v-model="shortcutDays" @change="handleShortcutDaysChange"> + <el-radio-button :label="1">昨天</el-radio-button> + <el-radio-button :label="7">最近7天</el-radio-button> + <el-radio-button :label="30">最近30天</el-radio-button> + </el-radio-group> + <el-date-picker + v-model="times" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + :shortcuts="shortcuts" + class="!w-240px" + @change="emitDateRangePicker" + /> + <slot></slot> + </div> +</template> +<script lang="ts" setup> +import dayjs from 'dayjs' +import * as DateUtil from '@/utils/formatTime' + +/** 快捷日期范围选择组件 */ +defineOptions({ name: 'ShortcutDateRangePicker' }) + +const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天 +const times = ref<[dayjs.ConfigType, dayjs.ConfigType]>(['', '']) // 时间范围参数 +defineExpose({ times }) // 暴露时间范围参数 +/** 日期快捷选择 */ +const shortcuts = [ + { + text: '昨天', + value: () => DateUtil.getDayRange(new Date(), -1) + }, + { + text: '最近7天', + value: () => DateUtil.getLast7Days() + }, + { + text: '本月', + value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')] + }, + { + text: '最近30天', + value: () => DateUtil.getLast30Days() + }, + { + text: '最近1年', + value: () => DateUtil.getLast1Year() + } +] + +/** 设置时间范围 */ +function setTimes() { + const beginDate = dayjs().subtract(shortcutDays.value, 'd') + const yesterday = dayjs().subtract(1, 'd') + times.value = DateUtil.getDateRange(beginDate, yesterday) +} + +/** 快捷日期单选按钮选中 */ +const handleShortcutDaysChange = async () => { + // 设置时间范围 + setTimes() + // 发送时间范围选中事件 + await emitDateRangePicker() +} + +/** 触发事件:时间范围选中 */ +const emits = defineEmits<{ + (e: 'change', times: [dayjs.ConfigType, dayjs.ConfigType]): void +}>() +/** 触发时间范围选中事件 */ +const emitDateRangePicker = async () => { + // 开始与截止在同一天的, 折线图出不来, 需要延长一天 + if (DateUtil.isSameDay(times.value[0], times.value[1])) { + // 前天 + times.value[0] = DateUtil.formatDate(dayjs(times.value[0]).subtract(1, 'd')) + } + emits('change', times.value) +} + +/** 初始化 **/ +onMounted(() => { + handleShortcutDaysChange() +}) +</script> diff --git a/src/utils/index.ts b/src/utils/index.ts index 6c9a5df2..e6b3173c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -233,3 +233,16 @@ export const yuanToFen = (amount: string | number): number => { export const fenToYuan = (price: string | number): number => { return formatToFraction(price) } + +/** + * 计算环比 + * + * @param value 当前数值 + * @param reference 对比数值 + */ +export const calculateRelativeRate = (value?: number, reference?: number) => { + // 防止除0 + if (!reference) return 0 + + return ((100 * ((value || 0) - reference)) / reference).toFixed(0) +} diff --git a/src/views/mall/home/components/ComparisonCard.vue b/src/views/mall/home/components/ComparisonCard.vue new file mode 100644 index 00000000..ee1c2f0c --- /dev/null +++ b/src/views/mall/home/components/ComparisonCard.vue @@ -0,0 +1,42 @@ +<template> + <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>{{ title }}</span> + <el-tag>{{ tag }}</el-tag> + </div> + <div class="flex flex-row items-baseline justify-between"> + <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" class="text-3xl" /> + <span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'"> + {{ Math.abs(toNumber(percent)) }}% + <Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" /> + </span> + </div> + <el-divider class="mb-1! mt-2!" /> + <div class="flex flex-row items-center justify-between text-sm"> + <span class="text-gray-500">昨日数据</span> + <span>{{ prefix || '' }}{{ reference }}</span> + </div> + </div> +</template> +<script lang="ts" setup> +import { propTypes } from '@/utils/propTypes' +import { toNumber } from 'lodash-es' +import { calculateRelativeRate } from '@/utils' + +/** 交易对照卡片 */ +defineOptions({ name: 'ComparisonCard' }) + +const props = defineProps({ + title: propTypes.string.def('').isRequired, + tag: propTypes.string.def(''), + prefix: propTypes.string.def(''), + value: propTypes.number.def(0).isRequired, + reference: propTypes.number.def(0).isRequired, + decimals: propTypes.number.def(0) +}) + +// 计算环比 +const percent = computed(() => + calculateRelativeRate(props.value as number, props.reference as number) +) +</script> diff --git a/src/views/mall/home/components/MemberStatisticsCard.vue b/src/views/mall/home/components/MemberStatisticsCard.vue new file mode 100644 index 00000000..2f9d7ab5 --- /dev/null +++ b/src/views/mall/home/components/MemberStatisticsCard.vue @@ -0,0 +1,91 @@ +<template> + <el-card shadow="never"> + <template #header> + <CardTitle title="用户统计" /> + </template> + <!-- 折线图 --> + <Echart :height="300" :options="lineChartOptions" /> + </el-card> +</template> +<script lang="ts" setup> +import dayjs from 'dayjs' +import { EChartsOption } from 'echarts' +import * as MemberStatisticsApi from '@/api/mall/statistics/member' +import { formatDate } from '@/utils/formatTime' +import { CardTitle } from '@/components/Card' + +/** 会员用户统计卡片 */ +defineOptions({ name: 'MemberStatisticsCard' }) + +const loading = ref(true) // 加载中 +/** 折线图配置 */ +const lineChartOptions = reactive<EChartsOption>({ + dataset: { + dimensions: ['date', 'count'], + source: [] + }, + 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) => formatDate(date, 'MM-DD') + } + }, + yAxis: { + axisTick: { + show: false + } + } +}) as EChartsOption + +const getMemberRegisterCountList = async () => { + loading.value = true + // 查询最近一月数据 + const beginTime = dayjs().subtract(30, 'd').startOf('d') + const endTime = dayjs().endOf('d') + const list = await MemberStatisticsApi.getMemberRegisterCountList(beginTime, endTime) + // 更新 Echarts 数据 + if (lineChartOptions.dataset && lineChartOptions.dataset['source']) { + lineChartOptions.dataset['source'] = list + } + loading.value = false +} + +/** 初始化 **/ +onMounted(() => { + getMemberRegisterCountList() +}) +</script> diff --git a/src/views/mall/home/components/OperationDataCard.vue b/src/views/mall/home/components/OperationDataCard.vue new file mode 100644 index 00000000..cae09a3e --- /dev/null +++ b/src/views/mall/home/components/OperationDataCard.vue @@ -0,0 +1,91 @@ +<template> + <el-card shadow="never"> + <template #header> + <CardTitle title="运营数据" /> + </template> + <div class="flex flex-row flex-wrap items-center gap-8 p-4"> + <div + v-for="item in data" + :key="item.name" + class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2" + @click="handleClick(item.routerName)" + > + <CountTo + :prefix="item.prefix" + :end-val="item.value" + :decimals="item.decimals" + class="text-3xl" + /> + <span class="text-center">{{ item.name }}</span> + </div> + </div> + </el-card> +</template> +<script lang="ts" setup> +import * as ProductSpuApi from '@/api/mall/product/spu' +import * as TradeStatisticsApi from '@/api/mall/statistics/trade' +import * as PayStatisticsApi from '@/api/mall/statistics/pay' +import { CardTitle } from '@/components/Card' + +/** 运营数据卡片 */ +defineOptions({ name: 'OperationDataCard' }) + +const router = useRouter() // 路由 + +/** 数据 */ +const data = reactive({ + orderUndelivered: { name: '待发货订单', value: 9, routerName: 'TradeOrder' }, + orderAfterSaleApply: { name: '退款中订单', value: 4, 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() + data.orderUndelivered.value = orderCount.undelivered + data.orderAfterSaleApply.value = orderCount.afterSaleApply + data.orderWaitePickUp.value = orderCount.pickUp + data.withdrawAuditing.value = orderCount.auditingWithdraw +} + +/** 查询商品数据 */ +const getProductData = async () => { + // TODO: @芋艿:这个接口的返回值,是不是用命名字段更好些? + const productCount = await ProductSpuApi.getTabsCount() + data.productForSale.value = productCount['0'] + data.productInWarehouse.value = productCount['1'] + data.productAlertStock.value = productCount['3'] +} + +/** 查询钱包充值数据 */ +const getWalletRechargeData = async () => { + data.rechargePrice.value = await PayStatisticsApi.getWalletRechargePrice() +} + +/** + * 跳转到对应页面 + * + * @param routerName 路由页面组件的名称 + */ +const handleClick = (routerName: string) => { + router.push({ name: routerName }) +} + +/** 初始化 **/ +onMounted(() => { + getOrderData() + getProductData() + getWalletRechargeData() +}) +</script> diff --git a/src/views/mall/home/components/ShortcutCard.vue b/src/views/mall/home/components/ShortcutCard.vue new file mode 100644 index 00000000..9fdd5cd4 --- /dev/null +++ b/src/views/mall/home/components/ShortcutCard.vue @@ -0,0 +1,79 @@ +<template> + <el-card shadow="never"> + <template #header> + <CardTitle title="快捷入口" /> + </template> + <div class="flex flex-row flex-wrap gap-8 p-4"> + <div + v-for="menu in menuList" + :key="menu.name" + class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2" + @click="handleMenuClick(menu.routerName)" + > + <div :class="menu.bgColor" class="rounded p-3 text-white"> + <Icon :icon="menu.icon" class="text-7.5!" /> + </div> + <span>{{ menu.name }}</span> + </div> + </div> + </el-card> +</template> +<script lang="ts" setup> +/** 快捷入口卡片 */ +import { CardTitle } from '@/components/Card' + +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> diff --git a/src/views/mall/home/components/TradeTrendCard.vue b/src/views/mall/home/components/TradeTrendCard.vue new file mode 100644 index 00000000..6aa9fdcc --- /dev/null +++ b/src/views/mall/home/components/TradeTrendCard.vue @@ -0,0 +1,208 @@ +<template> + <el-card shadow="never"> + <template #header> + <div class="flex flex-row items-center justify-between"> + <CardTitle title="交易量趋势" /> + <!-- 查询条件 --> + <div class="flex flex-row items-center gap-2"> + <el-radio-group v-model="timeRangeType" @change="handleTimeRangeTypeChange"> + <el-radio-button v-for="[key, value] in timeRange.entries()" :key="key" :label="key"> + {{ value.name }} + </el-radio-button> + </el-radio-group> + </div> + </div> + </template> + <!-- 折线图 --> + <Echart :height="300" :options="eChartOptions" /> + </el-card> +</template> +<script lang="ts" setup> +import dayjs, { Dayjs } from 'dayjs' +import { EChartsOption } from 'echarts' +import * as TradeStatisticsApi from '@/api/mall/statistics/trade' +import { fenToYuan } from '@/utils' +import { formatDate } from '@/utils/formatTime' +import { CardTitle } from '@/components/Card' + +/** 交易量趋势 */ +defineOptions({ name: 'TradeTrendCard' }) + +enum TimeRangeTypeEnum { + DAY30 = 1, + WEEK = 7, + MONTH = 30, + YEAR = 365 +} // 日期类型 +const timeRangeType = ref(TimeRangeTypeEnum.DAY30) // 日期快捷选择按钮, 默认30天 +const loading = ref(true) // 加载中 +// 时间范围 Map +const timeRange = new Map() + .set(TimeRangeTypeEnum.DAY30, { + name: '30天', + series: [ + { name: '订单金额', type: 'bar', smooth: true, data: [] }, + { name: '订单数量', type: 'line', smooth: true, data: [] } + ] + }) + .set(TimeRangeTypeEnum.WEEK, { + name: '周', + series: [ + { name: '上周金额', type: 'bar', smooth: true, data: [] }, + { name: '本周金额', type: 'bar', smooth: true, data: [] }, + { name: '上周数量', type: 'line', smooth: true, data: [] }, + { name: '本周数量', type: 'line', smooth: true, data: [] } + ] + }) + .set(TimeRangeTypeEnum.MONTH, { + name: '月', + series: [ + { name: '上月金额', type: 'bar', smooth: true, data: [] }, + { name: '本月金额', type: 'bar', smooth: true, data: [] }, + { name: '上月数量', type: 'line', smooth: true, data: [] }, + { name: '本月数量', type: 'line', smooth: true, data: [] } + ] + }) + .set(TimeRangeTypeEnum.YEAR, { + name: '年', + series: [ + { name: '去年金额', type: 'bar', smooth: true, data: [] }, + { name: '今年金额', type: 'bar', smooth: true, data: [] }, + { name: '去年数量', type: 'line', smooth: true, data: [] }, + { name: '今年数量', type: 'line', smooth: true, data: [] } + ] + }) +/** 图表配置 */ +const eChartOptions = reactive<EChartsOption>({ + grid: { + left: 20, + right: 20, + bottom: 20, + top: 80, + containLabel: true + }, + legend: { + top: 50, + data: [] + }, + 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: [], + axisLabel: { + formatter: (date: string) => { + switch (timeRangeType.value) { + case TimeRangeTypeEnum.DAY30: + return formatDate(date, 'MM-DD') + case TimeRangeTypeEnum.WEEK: + let weekDay = formatDate(date, 'ddd') + if (weekDay == '0') weekDay = '日' + return '周' + weekDay + case TimeRangeTypeEnum.MONTH: + return formatDate(date, 'D') + case TimeRangeTypeEnum.YEAR: + return formatDate(date, 'M') + '月' + default: + return date + } + } + } + }, + yAxis: { + axisTick: { + show: false + } + } +}) as EChartsOption + +/** 时间范围类型单选按钮选中 */ +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 + // 查询数据 + const list = await TradeStatisticsApi.getOrderCountTrendComparison( + timeRangeType.value, + beginTime, + endTime + ) + // 处理数据 + const dates: string[] = [] + const series = [...timeRange.get(timeRangeType.value).series] + for (let item of list) { + dates.push(item.value.date) + if (series.length === 2) { + series[0].data.push(fenToYuan(item?.value?.orderPayPrice || 0)) // 当前金额 + series[1].data.push(fenToYuan(item?.value?.orderPayCount || 0)) // 对照数量 + } else { + series[0].data.push(fenToYuan(item?.reference?.orderPayPrice || 0)) // 对照金额 + series[1].data.push(fenToYuan(item?.value?.orderPayPrice || 0)) // 当前金额 + series[2].data.push(item?.reference?.orderPayCount || 0) // 对照数量 + series[3].data.push(item?.value?.orderPayCount || 0) // 当前数量 + } + } + eChartOptions.xAxis!['data'] = dates + eChartOptions.series = series + // legend在4个切换到2个的时候,还是显示成4个,需要手动配置一下 + eChartOptions.legend['data'] = series.map((item) => item.name) + loading.value = false +} + +/** 初始化 **/ +onMounted(() => { + handleTimeRangeTypeChange() +}) +</script> diff --git a/src/views/mall/home/index.vue b/src/views/mall/home/index.vue new file mode 100644 index 00000000..95e2e1da --- /dev/null +++ b/src/views/mall/home/index.vue @@ -0,0 +1,113 @@ +<template> + <div class="flex flex-col"> + <!-- 数据对照 --> + <el-row :gutter="16" class="row"> + <el-col :md="6" :sm="12" :xs="24" :loading="loading"> + <ComparisonCard + tag="今日" + title="销售额" + prefix="¥" + ::decimals="2" + :value="fenToYuan(orderComparison?.value?.orderPayPrice || 0)" + :reference="fenToYuan(orderComparison?.reference?.orderPayPrice || 0)" + /> + </el-col> + <el-col :md="6" :sm="12" :xs="24" :loading="loading"> + <ComparisonCard + tag="今日" + title="用户访问量" + :value="userComparison?.value?.visitUserCount || 0" + :reference="userComparison?.reference?.visitUserCount || 0" + /> + </el-col> + <el-col :md="6" :sm="12" :xs="24" :loading="loading"> + <ComparisonCard + tag="今日" + title="订单量" + :value="fenToYuan(orderComparison?.value?.orderPayCount || 0)" + :reference="fenToYuan(orderComparison?.reference?.orderPayCount || 0)" + /> + </el-col> + <el-col :md="6" :sm="12" :xs="24" :loading="loading"> + <ComparisonCard + tag="今日" + title="新增用户" + :value="userComparison?.value?.createUserCount || 0" + :reference="userComparison?.reference?.createUserCount || 0" + /> + </el-col> + </el-row> + <el-row :gutter="16" class="row"> + <el-col :md="12"> + <!-- 快捷入口 --> + <ShortcutCard /> + </el-col> + <el-col :md="12"> + <!-- 运营数据 --> + <OperationDataCard /> + </el-col> + </el-row> + <el-row :gutter="16" class="mb-4"> + <el-col :md="18" :sm="24"> + <!-- 会员概览 --> + <MemberFunnelCard /> + </el-col> + <el-col :md="6" :sm="24"> + <!-- 会员终端 --> + <MemberTerminalCard /> + </el-col> + </el-row> + <!-- 交易量趋势 --> + <TradeTrendCard class="mb-4" /> + <!-- 会员统计 --> + <MemberStatisticsCard /> + </div> +</template> +<script lang="ts" setup> +import * as TradeStatisticsApi from '@/api/mall/statistics/trade' +import * as MemberStatisticsApi from '@/api/mall/statistics/member' +import { + TradeOrderSummaryRespVO, + TradeStatisticsComparisonRespVO +} from '@/api/mall/statistics/trade' +import { MemberCountRespVO } from '@/api/mall/statistics/member' +import { fenToYuan } from '@/utils' +import ComparisonCard from './components/ComparisonCard.vue' +import MemberStatisticsCard from './components/MemberStatisticsCard.vue' +import OperationDataCard from './components/OperationDataCard.vue' +import ShortcutCard from './components/ShortcutCard.vue' +import TradeTrendCard from './components/TradeTrendCard.vue' +import MemberTerminalCard from '@/views/mall/statistics/member/components/MemberTerminalCard.vue' +import MemberFunnelCard from '@/views/mall/statistics/member/components/MemberFunnelCard.vue' + +/** 商城首页 */ +defineOptions({ name: 'MallHome' }) + +const loading = ref(true) // 加载中 +const orderComparison = ref<TradeStatisticsComparisonRespVO<TradeOrderSummaryRespVO>>() // 交易对照数据 +const userComparison = ref<TradeStatisticsComparisonRespVO<MemberCountRespVO>>() // 用户对照数据 + +/** 查询交易对照卡片数据 */ +const getOrderComparison = async () => { + orderComparison.value = await TradeStatisticsApi.getOrderComparison() +} + +/** 查询会员用户数量对照卡片数据 */ +const getUserCountComparison = async () => { + userComparison.value = await MemberStatisticsApi.getUserCountComparison() +} + +/** 初始化 **/ +onMounted(async () => { + loading.value = true + await Promise.all([getOrderComparison(), getUserCountComparison()]) + loading.value = false +}) +</script> +<style lang="scss" scoped> +.row { + .el-col { + margin-bottom: 1rem; + } +} +</style> diff --git a/src/views/mall/statistics/member/components/MemberFunnelCard.vue b/src/views/mall/statistics/member/components/MemberFunnelCard.vue new file mode 100644 index 00000000..15c954e1 --- /dev/null +++ b/src/views/mall/statistics/member/components/MemberFunnelCard.vue @@ -0,0 +1,119 @@ +<template> + <el-card shadow="never"> + <template #header> + <div class="my--1.5 flex flex-row items-center justify-between"> + <CardTitle title="会员概览" /> + <!-- 查询条件 --> + <ShortcutDateRangePicker @change="handleTimeRangeChange" /> + </div> + </template> + <div class="min-w-225 py-1.75" v-loading="loading"> + <div class="relative h-24 flex"> + <div class="h-full w-75% bg-blue-50 <lg:w-35% <xl:w-55%"> + <div class="ml-15 h-full flex flex-col justify-center"> + <div class="font-bold"> + 注册用户数量:{{ analyseData?.comparison?.value?.userCount || 0 }} + </div> + <div class="mt-2 text-3.5"> + 环比增长率:{{ + calculateRelativeRate( + analyseData?.comparison?.value?.userCount, + analyseData?.comparison?.reference?.userCount + ) + }}% + </div> + </div> + </div> + <div + class="trapezoid1 ml--38.5 mt-1.5 h-full w-77 flex flex-col items-center justify-center bg-blue-5 text-3.5 text-white" + > + <span class="text-6 font-bold">{{ analyseData?.visitorCount || 0 }}</span> + <span>访客</span> + </div> + </div> + <div class="relative h-24 flex"> + <div class="h-full w-75% flex bg-cyan-50 <lg:w-35% <xl:w-55%"> + <div class="ml-15 h-full flex flex-col justify-center"> + <div class="font-bold"> + 活跃用户数量:{{ analyseData?.comparison?.value?.activeUserCount || 0 }} + </div> + <div class="mt-2 text-3.5"> + 环比增长率:{{ + calculateRelativeRate( + analyseData?.comparison?.value?.activeUserCount, + analyseData?.comparison?.reference?.activeUserCount + ) + }}% + </div> + </div> + </div> + <div + class="trapezoid2 ml--28 mt-1.7 h-25 w-56 flex flex-col items-center justify-center bg-cyan-5 text-3.5 text-white" + > + <span class="text-6 font-bold">{{ analyseData?.orderUserCount || 0 }}</span> + <span>下单</span> + </div> + </div> + <div class="relative h-24 flex"> + <div class="w-75% flex bg-slate-50 <lg:w-35% <xl:w-55%"> + <div class="ml-15 h-full flex 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-3.5"> + 环比增长率:{{ + calculateRelativeRate( + analyseData?.comparison?.value?.rechargeUserCount, + analyseData?.comparison?.reference?.rechargeUserCount + ) + }}% + </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--18 mt-3.25 h-23 w-36 flex flex-col items-center justify-center bg-slate-5 text-3.5 text-white" + > + <span class="text-6 font-bold">{{ analyseData?.payUserCount || 0 }}</span> + <span>成交用户</span> + </div> + </div> + </div> + </el-card> +</template> +<script lang="ts" setup> +import * as MemberStatisticsApi from '@/api/mall/statistics/member' +import dayjs from 'dayjs' +import { calculateRelativeRate, fenToYuan } from '@/utils' +import { MemberAnalyseRespVO } from '@/api/mall/statistics/member' +import { CardTitle } from '@/components/Card' + +/** 会员概览卡片 */ +defineOptions({ name: 'MemberFunnelCard' }) + +const loading = ref(true) // 加载中 +const analyseData = ref<MemberAnalyseRespVO>() // 会员分析数据 + +/** 查询会员概览数据列表 */ +const handleTimeRangeChange = async (times: [dayjs.ConfigType, dayjs.ConfigType]) => { + loading.value = true + // 查询数据 + analyseData.value = await MemberStatisticsApi.getMemberAnalyse({ times }) + loading.value = false +} +</script> +<style lang="scss" scoped> +.trapezoid1 { + transform: perspective(5em) rotateX(-11deg); +} +.trapezoid2 { + transform: perspective(7em) rotateX(-20deg); +} +.trapezoid3 { + transform: perspective(3em) rotateX(-13deg); +} +</style> diff --git a/src/views/mall/statistics/member/components/MemberTerminalCard.vue b/src/views/mall/statistics/member/components/MemberTerminalCard.vue new file mode 100644 index 00000000..7bbab76c --- /dev/null +++ b/src/views/mall/statistics/member/components/MemberTerminalCard.vue @@ -0,0 +1,69 @@ +<template> + <el-card shadow="never" v-loading="loading"> + <template #header> + <CardTitle title="会员终端" /> + </template> + <Echart :height="300" :options="terminalChartOptions" /> + </el-card> +</template> +<script lang="ts" setup> +import * as MemberStatisticsApi from '@/api/mall/statistics/member' +import { EChartsOption } from 'echarts' +import { MemberTerminalStatisticsRespVO } from '@/api/mall/statistics/member' +import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict' +import { CardTitle } from '@/components/Card' + +/** 会员终端卡片 */ +defineOptions({ name: 'MemberTerminalCard' }) + +const loading = ref(true) // 加载中 + +/** 会员终端统计图配置 */ +const terminalChartOptions = reactive<EChartsOption>({ + 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: [] + } + ] +}) as EChartsOption + +/** 按照终端,查询会员统计列表 */ +const getMemberTerminalStatisticsList = async () => { + loading.value = true + const list = await MemberStatisticsApi.getMemberTerminalStatisticsList() + const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL) + terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => { + const userCount = list.find( + (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value + )?.userCount + return { + name: dictData.label, + value: userCount || 0 + } + }) + loading.value = false +} + +/** 初始化 **/ +onMounted(() => { + getMemberTerminalStatisticsList() +}) +</script> diff --git a/src/views/mall/statistics/member/index.vue b/src/views/mall/statistics/member/index.vue index e76e861c..713f7776 100644 --- a/src/views/mall/statistics/member/index.vue +++ b/src/views/mall/statistics/member/index.vue @@ -44,118 +44,20 @@ </el-row> <el-row :gutter="16" class="mb-4"> <el-col :md="18" :sm="24"> - <el-card shadow="never"> - <template #header> - <div class="flex flex-row items-center justify-between"> - <span>会员概览</span> - <!-- 查询条件 --> - <div class="my--2 flex flex-row items-center gap-2"> - <el-radio-group v-model="shortcutDays" @change="handleDateTypeChange"> - <el-radio-button :label="1">昨天</el-radio-button> - <el-radio-button :label="7">最近7天</el-radio-button> - <el-radio-button :label="30">最近30天</el-radio-button> - </el-radio-group> - <el-date-picker - v-model="queryParams.times" - value-format="YYYY-MM-DD HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - :shortcuts="shortcuts" - class="!w-240px" - @change="getMemberAnalyse" - /> - </div> - </div> - </template> - <div class="min-w-225 py-1.75" v-loading="analyseLoading"> - <div class="relative h-24 flex"> - <div class="h-full w-75% bg-blue-50 <lg:w-35% <xl:w-55%"> - <div class="ml-15 h-full flex flex-col justify-center"> - <div class="font-bold"> - 注册用户数量:{{ analyseData?.comparison?.value?.userCount || 0 }} - </div> - <div class="mt-2 text-3.5"> - 环比增长率:{{ - calculateRelativeRate( - analyseData?.comparison?.value?.userCount, - analyseData?.comparison?.reference?.userCount - ) - }}% - </div> - </div> - </div> - <div - class="trapezoid1 ml--38.5 mt-1.5 h-full w-77 flex flex-col items-center justify-center bg-blue-5 text-3.5 text-white" - > - <span class="text-6 font-bold">{{ analyseData?.visitorCount || 0 }}</span> - <span>访客</span> - </div> - </div> - <div class="relative h-24 flex"> - <div class="h-full w-75% flex bg-cyan-50 <lg:w-35% <xl:w-55%"> - <div class="ml-15 h-full flex flex-col justify-center"> - <div class="font-bold"> - 活跃用户数量:{{ analyseData?.comparison?.value?.activeUserCount || 0 }} - </div> - <div class="mt-2 text-3.5"> - 环比增长率:{{ - calculateRelativeRate( - analyseData?.comparison?.value?.activeUserCount, - analyseData?.comparison?.reference?.activeUserCount - ) - }}% - </div> - </div> - </div> - <div - class="trapezoid2 ml--28 mt-1.7 h-25 w-56 flex flex-col items-center justify-center bg-cyan-5 text-3.5 text-white" - > - <span class="text-6 font-bold">{{ analyseData?.orderUserCount || 0 }}</span> - <span>下单</span> - </div> - </div> - <div class="relative h-24 flex"> - <div class="w-75% flex bg-slate-50 <lg:w-35% <xl:w-55%"> - <div class="ml-15 h-full flex 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-3.5"> - 环比增长率:{{ - calculateRelativeRate( - analyseData?.comparison?.value?.rechargeUserCount, - analyseData?.comparison?.reference?.rechargeUserCount - ) - }}% - </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--18 mt-3.25 h-23 w-36 flex flex-col items-center justify-center bg-slate-5 text-3.5 text-white" - > - <span class="text-6 font-bold">{{ analyseData?.payUserCount || 0 }}</span> - <span>成交用户</span> - </div> - </div> - </div> - </el-card> + <!-- 会员概览 --> + <MemberFunnelCard /> </el-col> <el-col :md="6" :sm="24"> - <el-card shadow="never" header="会员终端" v-loading="loading"> - <Echart :height="300" :options="terminalChartOptions" /> - </el-card> + <!-- 会员终端 --> + <MemberTerminalCard /> </el-col> </el-row> <el-row :gutter="16"> <el-col :md="18" :sm="24"> - <el-card shadow="never" header="会员地域分布"> + <el-card shadow="never"> + <template #header> + <CardTitle title="会员地域分布" /> + </template> <el-row v-loading="loading"> <el-col :span="10"> <Echart :height="300" :options="areaChartOptions" /> @@ -206,7 +108,10 @@ </el-card> </el-col> <el-col :md="6" :sm="24"> - <el-card shadow="never" header="会员性别比例" v-loading="loading"> + <el-card shadow="never" v-loading="loading"> + <template #header> + <CardTitle title="会员性别比例" /> + </template> <Echart :height="300" :options="sexChartOptions" /> </el-card> </el-col> @@ -214,63 +119,34 @@ </div> </template> <script lang="ts" setup> -import * as TradeMemberApi from '@/api/mall/statistics/member' +import * as MemberStatisticsApi from '@/api/mall/statistics/member' import TradeTrendValue from '../trade/components/TradeTrendValue.vue' import { EChartsOption } from 'echarts' import china from '@/assets/map/json/china.json' -import dayjs from 'dayjs' import { fenToYuan } from '@/utils' -import * as DateUtil from '@/utils/formatTime' import { - MemberAnalyseRespVO, MemberAreaStatisticsRespVO, MemberSexStatisticsRespVO, - MemberAnalyseReqVO, MemberSummaryRespVO, MemberTerminalStatisticsRespVO } from '@/api/mall/statistics/member' import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict' import echarts from '@/plugins/echarts' import { fenToYuanFormat } from '@/utils/formatter' +import MemberFunnelCard from './components/MemberFunnelCard.vue' +import MemberTerminalCard from './components/MemberTerminalCard.vue' +import { CardTitle } from '@/components/Card' /** 会员统计 */ defineOptions({ name: 'MemberStatistics' }) const loading = ref(true) // 加载中 -const analyseLoading = ref(true) // 会员概览加载中 -const queryParams = reactive<MemberAnalyseReqVO>({ times: ['', ''] }) // 会员概览查询参数 -const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天 const summary = ref<MemberSummaryRespVO>() // 会员统计数据 -const analyseData = ref<MemberAnalyseRespVO>() // 会员分析数据 const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 省份会员统计 // 注册地图 echarts?.registerMap('china', china!) -/** 日期快捷选择 */ -const shortcuts = [ - { - text: '昨天', - value: () => DateUtil.getDayRange(new Date(), -1) - }, - { - text: '最近7天', - value: () => DateUtil.getLast7Days() - }, - { - text: '本月', - value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')] - }, - { - text: '最近30天', - value: () => DateUtil.getLast30Days() - }, - { - text: '最近1年', - value: () => DateUtil.getLast1Year() - } -] - /** 会员终端统计图配置 */ const terminalChartOptions = reactive<EChartsOption>({ tooltip: { @@ -357,37 +233,14 @@ const areaChartOptions = reactive<EChartsOption>({ ] }) as EChartsOption -/** 计算环比 */ -const calculateRelativeRate = (value?: number, reference?: number) => { - // 防止除0 - if (!reference) return 0 - - return ((100 * ((value || 0) - reference)) / reference).toFixed(0) -} - -/** 设置时间范围 */ -function setTimes() { - const beginDate = dayjs().subtract(shortcutDays.value, 'd') - const yesterday = dayjs().subtract(1, 'd') - queryParams.times = DateUtil.getDateRange(beginDate, yesterday) -} - -/** 处理会员概览查询(日期单选按钮组选择后) */ -const handleDateTypeChange = async () => { - // 设置时间范围 - setTimes() - // 查询数据 - await getMemberAnalyse() -} - /** 查询会员统计 */ const getMemberSummary = async () => { - summary.value = await TradeMemberApi.getMemberSummary() + summary.value = await MemberStatisticsApi.getMemberSummary() } /** 按照省份,查询会员统计列表 */ const getMemberAreaStatisticsList = async () => { - const list = await TradeMemberApi.getMemberAreaStatisticsList() + const list = await MemberStatisticsApi.getMemberAreaStatisticsList() areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => { return { ...item, @@ -412,7 +265,7 @@ const getMemberAreaStatisticsList = async () => { /** 按照性别,查询会员统计列表 */ const getMemberSexStatisticsList = async () => { - const list = await TradeMemberApi.getMemberSexStatisticsList() + const list = await MemberStatisticsApi.getMemberSexStatisticsList() const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX) sexChartOptions.series[0].data = dictDataList.map((dictData: DictDataType) => { const userCount = list.find((item: MemberSexStatisticsRespVO) => item.sex === dictData.value) @@ -426,7 +279,7 @@ const getMemberSexStatisticsList = async () => { /** 按照终端,查询会员统计列表 */ const getMemberTerminalStatisticsList = async () => { - const list = await TradeMemberApi.getMemberTerminalStatisticsList() + const list = await MemberStatisticsApi.getMemberTerminalStatisticsList() const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL) terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => { const userCount = list.find( @@ -439,20 +292,6 @@ const getMemberTerminalStatisticsList = async () => { }) } -/** 查询会员概览数据列表 */ -const getMemberAnalyse = async () => { - analyseLoading.value = true - const times = queryParams.times - // 开始与截止在同一天的, 环比出不来, 需要延长一天 - if (DateUtil.isSameDay(times[0], times[1])) { - // 前天 - times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd')) - } - // 查询数据 - analyseData.value = await TradeMemberApi.getMemberAnalyse({ times }) - analyseLoading.value = false -} - /** 初始化 **/ onMounted(async () => { loading.value = true @@ -460,8 +299,7 @@ onMounted(async () => { getMemberSummary(), getMemberTerminalStatisticsList(), getMemberAreaStatisticsList(), - getMemberSexStatisticsList(), - handleDateTypeChange() + getMemberSexStatisticsList() ]) loading.value = false }) @@ -472,13 +310,4 @@ onMounted(async () => { margin-bottom: 1rem; } } -.trapezoid1 { - transform: perspective(5em) rotateX(-11deg); -} -.trapezoid2 { - transform: perspective(7em) rotateX(-20deg); -} -.trapezoid3 { - transform: perspective(3em) rotateX(-13deg); -} </style> diff --git a/src/views/mall/statistics/trade/index.vue b/src/views/mall/statistics/trade/index.vue index 8fc2d0cb..e5290dca 100644 --- a/src/views/mall/statistics/trade/index.vue +++ b/src/views/mall/statistics/trade/index.vue @@ -59,25 +59,9 @@ <template #header> <!-- 标题 --> <div class="flex flex-row items-center justify-between"> - <span>交易状况</span> + <CardTitle title="交易状况" /> <!-- 查询条件 --> - <div class="flex flex-row items-center gap-2"> - <el-radio-group v-model="shortcutDays" @change="handleDateTypeChange"> - <el-radio-button :label="1">昨天</el-radio-button> - <el-radio-button :label="7">最近7天</el-radio-button> - <el-radio-button :label="30">最近30天</el-radio-button> - </el-radio-group> - <el-date-picker - v-model="queryParams.times" - value-format="YYYY-MM-DD HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - :shortcuts="shortcuts" - class="!w-240px" - @change="getTradeTrendData" - /> + <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getTradeTrendData"> <el-button class="ml-4" @click="handleExport" @@ -86,7 +70,7 @@ > <Icon icon="ep:download" class="mr-1" />导出 </el-button> - </div> + </ShortcutDateRangePicker> </div> </template> <!-- 统计值 --> @@ -233,13 +217,11 @@ import { EChartsOption } from 'echarts' import { TradeStatisticsComparisonRespVO, TradeSummaryRespVO, - TradeTrendReqVO, TradeTrendSummaryRespVO } from '@/api/mall/statistics/trade' -import dayjs from 'dayjs' -import { fenToYuan } from '@/utils' -import * as DateUtil from '@/utils/formatTime' +import { calculateRelativeRate, fenToYuan } from '@/utils' import download from '@/utils/download' +import { CardTitle } from '@/components/Card' /** 交易统计 */ defineOptions({ name: 'TradeStatistics' }) @@ -249,34 +231,9 @@ const message = useMessage() // 消息弹窗 const loading = ref(true) // 加载中 const trendLoading = ref(true) // 交易状态加载中 const exportLoading = ref(false) // 导出的加载中 -const queryParams = reactive<TradeTrendReqVO>({ times: ['', ''] }) // 交易状况查询参数 -const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天 const summary = ref<TradeStatisticsComparisonRespVO<TradeSummaryRespVO>>() // 交易统计数据 const trendSummary = ref<TradeStatisticsComparisonRespVO<TradeTrendSummaryRespVO>>() // 交易状况统计数据 - -/** 日期快捷选择 */ -const shortcuts = [ - { - text: '昨天', - value: () => DateUtil.getDayRange(new Date(), -1) - }, - { - text: '最近7天', - value: () => DateUtil.getLast7Days() - }, - { - text: '本月', - value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')] - }, - { - text: '最近30天', - value: () => DateUtil.getLast30Days() - }, - { - text: '最近1年', - value: () => DateUtil.getLast1Year() - } -] +const shortcutDateRangePicker = ref() /** 折线图配置 */ const lineChartOptions = reactive<EChartsOption>({ @@ -333,29 +290,6 @@ const lineChartOptions = reactive<EChartsOption>({ } }) as EChartsOption -/** 计算环比 */ -const calculateRelativeRate = (value?: number, reference?: number) => { - // 防止除0 - if (!reference) return 0 - - return ((100 * ((value || 0) - reference)) / reference).toFixed(0) -} - -/** 设置时间范围 */ -function setTimes() { - const beginDate = dayjs().subtract(shortcutDays.value, 'd') - const yesterday = dayjs().subtract(1, 'd') - queryParams.times = DateUtil.getDateRange(beginDate, yesterday) -} - -/** 处理交易状况查询(日期单选按钮组选择后) */ -const handleDateTypeChange = async () => { - // 设置时间范围 - setTimes() - // 查询数据 - await getTradeTrendData() -} - /** 处理交易状况查询 */ const getTradeTrendData = async () => { trendLoading.value = true @@ -370,20 +304,14 @@ const getTradeStatisticsSummary = async () => { /** 查询交易状况数据统计 */ const getTradeTrendSummary = async () => { - loading.value = true - trendSummary.value = await TradeStatisticsApi.getTradeTrendSummary(queryParams) - loading.value = false + const times = shortcutDateRangePicker.value.times + trendSummary.value = await TradeStatisticsApi.getTradeTrendSummary({ times }) } /** 查询交易状况数据列表 */ const getTradeTrendList = async () => { - const times = queryParams.times - // 开始与截止在同一天的, 折线图出不来, 需要延长一天 - if (DateUtil.isSameDay(times[0], times[1])) { - // 前天 - times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd')) - } // 查询数据 + const times = shortcutDateRangePicker.value.times const list = await TradeStatisticsApi.getTradeTrendList({ times }) // 处理数据 for (let item of list) { @@ -405,7 +333,8 @@ const handleExport = async () => { await message.exportConfirm() // 发起导出 exportLoading.value = true - const data = await TradeStatisticsApi.exportTradeTrend(queryParams) + const times = shortcutDateRangePicker.value.times + const data = await TradeStatisticsApi.exportTradeTrend({ times }) download.excel(data, '交易状况.xls') } catch { } finally { @@ -416,7 +345,6 @@ const handleExport = async () => { /** 初始化 **/ onMounted(async () => { await getTradeStatisticsSummary() - await handleDateTypeChange() }) </script> <style lang="scss" scoped>