addanalysis

This commit is contained in:
2025-12-27 13:51:08 +08:00
parent a718ac7874
commit b392f9cf0d
7 changed files with 265 additions and 126 deletions

View File

@@ -130,6 +130,12 @@ export namespace AgentApi {
withdraw_no?: string; withdraw_no?: string;
} }
// 提现统计数据
export interface WithdrawalStatistics {
total_withdrawal_amount: number;
today_withdrawal_amount: number;
}
// 代理上级抽佣相关接口 // 代理上级抽佣相关接口
export interface AgentCommissionDeductionListItem { export interface AgentCommissionDeductionListItem {
id: number; id: number;
@@ -446,6 +452,15 @@ async function updateAgentMembershipConfig(
); );
} }
/**
* 获取提现统计数据
*/
async function getWithdrawalStatistics() {
return requestClient.get<AgentApi.WithdrawalStatistics>(
'/agent/agent-withdrawal/statistics',
);
}
export { export {
getAgentCommissionDeductionList, getAgentCommissionDeductionList,
getAgentCommissionList, getAgentCommissionList,
@@ -457,6 +472,7 @@ export {
getAgentRewardList, getAgentRewardList,
getAgentWithdrawalList, getAgentWithdrawalList,
getMembershipRechargeOrderList, getMembershipRechargeOrderList,
getWithdrawalStatistics,
updateAgentMembershipConfig, updateAgentMembershipConfig,
updateAgentProductionConfig, updateAgentProductionConfig,
}; };

View File

@@ -11,6 +11,7 @@ export namespace OrderApi {
payment_platform: 'alipay' | 'appleiap' | 'wechat'; payment_platform: 'alipay' | 'appleiap' | 'wechat';
payment_scene: 'app' | 'h5' | 'mini_program' | 'public_account'; payment_scene: 'app' | 'h5' | 'mini_program' | 'public_account';
amount: number; amount: number;
sales_cost: number;
status: 'closed' | 'failed' | 'paid' | 'pending' | 'refunded'; status: 'closed' | 'failed' | 'paid' | 'pending' | 'refunded';
query_state: 'cleaned' | 'failed' | 'pending' | 'processing' | 'success'; query_state: 'cleaned' | 'failed' | 'pending' | 'processing' | 'success';
create_time: string; create_time: string;
@@ -34,6 +35,20 @@ export namespace OrderApi {
refund_no: string; refund_no: string;
amount: number; amount: number;
} }
// 退款统计数据
export interface RefundStatistics {
total_refund_amount: number;
today_refund_amount: number;
}
// 收入统计数据
export interface IncomeStatistics {
total_revenue_amount: number;
today_revenue_amount: number;
total_profit_amount: number;
today_profit_amount: number;
}
} }
/** /**
@@ -57,4 +72,18 @@ async function refundOrder(id: number, data: OrderApi.RefundOrderRequest) {
); );
} }
export { getOrderList, refundOrder }; /**
* 获取退款统计数据
*/
async function getRefundStatistics() {
return requestClient.get<OrderApi.RefundStatistics>('/order/refund-statistics');
}
/**
* 获取收入统计数据
*/
async function getIncomeStatistics() {
return requestClient.get<OrderApi.IncomeStatistics>('/order/revenue-statistics');
}
export { getOrderList, refundOrder, getRefundStatistics, getIncomeStatistics };

View File

@@ -5,28 +5,14 @@ import { onMounted, ref } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { statsHistory, statsTotal } from '#/api/promotion/analytics'; import { getOrderList } from '#/api/order/order';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);
// 获取30天前的日期
const getDateString = (daysAgo: number) => {
const date = new Date();
date.setDate(date.getDate() - daysAgo);
return date.toISOString().split('T')[0];
};
onMounted(async () => { onMounted(async () => {
try { try {
// 获取趋势数据
const endDate = getDateString(0); // 今天
const startDate = getDateString(29); // 29天前
const trendData = await statsHistory({ start_date: startDate, end_date: endDate });
// 获取统计数据
const statsData = await statsTotal();
// 准备图表数据 // 准备图表数据
const dates = Array.from({ length: 30 }).map((_, index) => { const dates = Array.from({ length: 30 }).map((_, index) => {
const date = new Date(); const date = new Date();
@@ -34,47 +20,37 @@ onMounted(async () => {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}); });
// 如果有历史数据,使用历史数据;否则使用模拟数据 // 准备日期数组,用于查询订单数据
let orderData = Array(30).fill(0); const queryDates = Array.from({ length: 30 }).map((_, index) => {
if (trendData && trendData.length > 0) { const date = new Date();
// 将历史数据按日期排序并映射到数组 date.setDate(date.getDate() - 29 + index);
const sortedData = trendData.sort((a, b) => return date.toISOString().split('T')[0]; // YYYY-MM-DD格式
new Date(a.stats_date).getTime() - new Date(b.stats_date).getTime() });
);
// 获取每日订单数据
sortedData.forEach((item) => { const orderDataPromises = queryDates.map(async (date) => {
const itemDate = new Date(item.stats_date); try {
const today = new Date(); // 获取当天的开始和结束时间
const daysDiff = Math.floor((today.getTime() - itemDate.getTime()) / (1000 * 60 * 60 * 24)); const startTime = `${date} 00:00:00`;
const endTime = `${date} 23:59:59`;
if (daysDiff >= 0 && daysDiff < 30) { // 获取当天的订单数据
// 使用实际日期索引 const response = await getOrderList({
orderData[29 - daysDiff] = item.pay_count || 0; page: 1,
} pageSize: 1,
}); create_time_start: startTime,
} else { create_time_end: endTime
// 没有历史数据时,使用统计数据生成模拟数据 });
const todayPayCount = statsData?.today_pay_count || 0;
const totalPayCount = statsData?.total_pay_count || 0; return response.total || 0;
} catch (error) {
// 简单的线性分布模拟数据 console.error(`获取${date}的订单数据失败:`, error);
for (let i = 0; i < 30; i++) { return 0;
// 最后一天使用今日数据,其他天按比例分布
if (i === 29) {
orderData[i] = todayPayCount;
} else {
// 按指数衰减模拟历史数据
orderData[i] = Math.max(0, Math.floor(todayPayCount * Math.exp(-0.1 * (29 - i))));
}
} }
});
// 确保总和不超过总计数
const sum = orderData.reduce((a, b) => a + b, 0); // 等待所有请求完成
if (sum > totalPayCount && totalPayCount > 0) { const orderData = await Promise.all(orderDataPromises);
const ratio = totalPayCount / sum;
orderData = orderData.map(val => Math.floor(val * ratio));
}
}
// 计算Y轴最大值 // 计算Y轴最大值
const maxValue = Math.max(...orderData) || 10; const maxValue = Math.max(...orderData) || 10;
@@ -89,13 +65,17 @@ onMounted(async () => {
}, },
series: [ series: [
{ {
barMaxWidth: 80,
data: orderData, data: orderData,
type: 'bar', type: 'line',
name: '订单数', name: '订单数',
smooth: true,
itemStyle: { itemStyle: {
color: '#4f9cff', color: '#4f9cff',
}, },
areaStyle: {
opacity: 0.3,
color: '#4f9cff',
},
}, },
], ],
tooltip: { tooltip: {
@@ -113,6 +93,7 @@ onMounted(async () => {
xAxis: { xAxis: {
data: dates, data: dates,
type: 'category', type: 'category',
boundaryGap: false,
}, },
yAxis: { yAxis: {
max: Math.ceil(maxValue * 1.2), // 比最大值大20%作为Y轴上限 max: Math.ceil(maxValue * 1.2), // 比最大值大20%作为Y轴上限
@@ -134,13 +115,17 @@ onMounted(async () => {
}, },
series: [ series: [
{ {
barMaxWidth: 80,
data: Array(30).fill(0), data: Array(30).fill(0),
type: 'bar', type: 'line',
name: '订单数', name: '订单数',
smooth: true,
itemStyle: { itemStyle: {
color: '#4f9cff', color: '#4f9cff',
}, },
areaStyle: {
opacity: 0.3,
color: '#4f9cff',
},
}, },
], ],
tooltip: { tooltip: {
@@ -158,6 +143,7 @@ onMounted(async () => {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}), }),
type: 'category', type: 'category',
boundaryGap: false,
}, },
yAxis: { yAxis: {
max: 10, max: 10,

View File

@@ -22,40 +22,55 @@ import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue'; import AnalyticsVisitsSource from './analytics-visits-source.vue';
import AnalyticsVisits from './analytics-visits.vue'; import AnalyticsVisits from './analytics-visits.vue';
import { getAgentList } from '#/api/agent'; import { getAgentList, getWithdrawalStatistics } from '#/api/agent';
import { getOrderList } from '#/api/order/order'; import { getOrderList, getRefundStatistics, getIncomeStatistics } from '#/api/order/order';
import { getProductList } from '#/api/product-manage/product';
import { getPlatformUserList } from '#/api/platform-user'; import { getPlatformUserList } from '#/api/platform-user';
// 初始化概览数据 // 初始化概览数据
const overviewItems = ref<AnalysisOverviewItem[]>([ const overviewItems = ref<AnalysisOverviewItem[]>([
{ {
icon: SvgCardIcon, icon: SvgCardIcon,
title: '平台用户数', title: '用户数',
totalTitle: '总用户数',
totalValue: 0,
value: 0, value: 0,
todaytitle: '今日新增用户数',
todayValue: 0,
Subtitle: '总代理数',
SubValue: 0,
todaySubtitle: '今日新增代理数',
todaySubValue: 0,
}, },
{ {
icon: SvgCakeIcon, icon: SvgCakeIcon,
title: '推广访问量', title: '总订单数',
totalTitle: '总推广访问量',
totalValue: 0,
value: 0, value: 0,
todaytitle: '今日新增订单数',
todayValue: 0,
Subtitle: '代理总订单量',
SubValue: 0,
todaySubtitle: '今日新增代理订单量',
todaySubValue: 0,
}, },
{ {
icon: SvgDownloadIcon, icon: SvgDownloadIcon,
title: '产品数量', title: '总收入',
totalTitle: '总产品数量',
totalValue: 0,
value: 0, value: 0,
todaytitle: '今日新增收入',
todayValue: 0,
Subtitle: '总利润',
SubValue: 0,
todaySubtitle: '今日新增利润',
todaySubValue: 0,
}, },
{ {
icon: SvgBellIcon, icon: SvgBellIcon,
title: '代理数量', title: '总提现金额',
totalTitle: '总代理数量',
totalValue: 0,
value: 0, value: 0,
todaytitle: '今日新增提现金额',
todayValue: 0,
Subtitle: '总退款金额',
SubValue: 0,
todaySubtitle: '今日新增退款金额',
todaySubValue: 0,
}, },
]); ]);
@@ -73,51 +88,128 @@ const chartTabs: TabOption[] = [
// 获取统计数据 // 获取统计数据
async function fetchStatistics() { async function fetchStatistics() {
try { try {
// 获取平台用户数据 // 获取今日的开始和结束时间
const today = new Date();
// 将时间格式化为后端期望的格式 (YYYY-MM-DD HH:MM:SS)
const startTime = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().replace('T', ' ').substring(0, 19);
const endTime = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1).toISOString().replace('T', ' ').substring(0, 19);
// 获取平台用户数据(总数)
const platformUserResponse = await getPlatformUserList({ page: 1, pageSize: 1 }); const platformUserResponse = await getPlatformUserList({ page: 1, pageSize: 1 });
const platformUserTotal = platformUserResponse.total || 0; const platformUserTotal = platformUserResponse.total || 0;
// 获取今日新增用户数
// 由于平台用户API不支持时间过滤我们需要获取更多数据并在前端过滤
const newUserResponse = await getPlatformUserList({ page: 1, pageSize: 1000 });
const newUserCount = newUserResponse.items?.filter(user => {
const userCreateTime = new Date(user.create_time);
return userCreateTime >= new Date(startTime) && userCreateTime < new Date(endTime);
}).length || 0;
// 获取订单数据 // 获取订单数据
const orderResponse = await getOrderList({ page: 1, pageSize: 1 }); const orderResponse = await getOrderList({ page: 1, pageSize: 1 });
const orderTotal = orderResponse.total || 0; const orderTotal = orderResponse.total || 0;
// 获取产品数据 // 获取代理订单数据
const productResponse = await getProductList({ page: 1, pageSize: 1 }); const agentOrderResponse = await getOrderList({ page: 1, pageSize: 1, is_agent_order: true });
const productTotal = productResponse.total || 0; const agentOrderTotal = agentOrderResponse.total || 0;
// 获取代理数据 // 获取今日新增订单数
const todayOrderResponse = await getOrderList({
page: 1,
pageSize: 1000,
create_time_start: startTime,
create_time_end: endTime
});
const todayOrderTotal = todayOrderResponse.total || 0;
// 获取今日新增代理订单数
const todayAgentOrderResponse = await getOrderList({
page: 1,
pageSize: 1000,
is_agent_order: true,
create_time_start: startTime,
create_time_end: endTime
});
const todayAgentOrderTotal = todayAgentOrderResponse.total || 0;
// Product data is no longer needed for order statistics
// 获取代理数据(总数)
const agentResponse = await getAgentList({ page: 1, pageSize: 1 }); const agentResponse = await getAgentList({ page: 1, pageSize: 1 });
const agentTotal = agentResponse.total || 0; const agentTotal = agentResponse.total || 0;
// 获取今日新增代理数
const newAgentResponse = await getAgentList({
page: 1,
pageSize: 100,
create_time_start: startTime,
create_time_end: endTime
});
const newAgentCount = newAgentResponse.total || 0;
// 获取提现统计数据
const withdrawalStatsResponse = await getWithdrawalStatistics();
const totalWithdrawalAmount = withdrawalStatsResponse.total_withdrawal_amount || 0;
const todayWithdrawalAmount = withdrawalStatsResponse.today_withdrawal_amount || 0;
// 获取退款统计数据
const refundStatsResponse = await getRefundStatistics();
const totalRefundAmount = refundStatsResponse.total_refund_amount || 0;
const todayRefundAmount = refundStatsResponse.today_refund_amount || 0;
// 获取收入统计数据
const incomeStatsResponse = await getIncomeStatistics();
const totalIncome = incomeStatsResponse.total_revenue_amount || 0;
const todayIncome = incomeStatsResponse.today_revenue_amount || 0;
const totalProfit = incomeStatsResponse.total_profit_amount || 0;
const todayProfit = incomeStatsResponse.today_profit_amount || 0;
// 更新概览数据 // 更新概览数据
overviewItems.value = [ overviewItems.value = [
{ {
icon: SvgCardIcon, icon: SvgCardIcon,
title: '平台用户数', title: '用户数',
totalTitle: '总用户数', value: platformUserTotal,
totalValue: platformUserTotal, todaytitle: '今日新增用户数',
value: Math.min(100, platformUserTotal), // 显示最近的100个作为今日新增 todayValue: newUserCount,
Subtitle: '总代理数',
SubValue: agentTotal,
todaySubtitle: '今日新增代理数',
todaySubValue: newAgentCount,
}, },
{ {
icon: SvgCakeIcon, icon: SvgCakeIcon,
title: '推广访问量', title: '总订单数',
totalTitle: '总推广访问量', value: orderTotal,
totalValue: orderTotal * 10, // 假设每个订单平均带来10次访问 todaytitle: '今日新增订单数',
value: Math.min(1000, orderTotal), // 显示最近的1000个作为今日新增 todayValue: todayOrderTotal,
Subtitle: '总代理订单量',
SubValue: agentOrderTotal,
todaySubtitle: '今日新增代理订单量',
todaySubValue: todayAgentOrderTotal,
}, },
{ {
icon: SvgDownloadIcon, icon: SvgDownloadIcon,
title: '产品数量', title: '总收入',
totalTitle: '总产品数量', value: totalIncome,
totalValue: productTotal, todaytitle: '今日新增收入',
value: Math.min(10, productTotal), // 显示最近的10个作为今日新增 todayValue: todayIncome,
Subtitle: '总利润',
SubValue: totalProfit,
todaySubtitle: '今日新增利润',
todaySubValue: todayProfit,
}, },
{ {
icon: SvgBellIcon, icon: SvgBellIcon,
title: '代理数量', title: '总提现金额',
totalTitle: '总代理数量', value: totalWithdrawalAmount,
totalValue: agentTotal, todaytitle: '今日新增提现金额',
value: Math.min(50, agentTotal), // 显示最近的50个作为今日新增 todayValue: todayWithdrawalAmount,
Subtitle: '总退款金额',
SubValue: totalRefundAmount,
todaySubtitle: '今日新增退款金额',
todaySubValue: todayRefundAmount,
}, },
]; ];
} catch (error) { } catch (error) {

View File

@@ -53,6 +53,14 @@ export function useColumns<T = OrderApi.Order>(
return sceneMap[row.payment_scene] || row.payment_scene; return sceneMap[row.payment_scene] || row.payment_scene;
}, },
}, },
{
field: 'sales_cost',
title: '成本价',
width: 120,
formatter: ({ row }) => {
return `¥${row.sales_cost.toFixed(2)}`;
},
},
{ {
field: 'amount', field: 'amount',
title: '金额', title: '金额',

View File

@@ -4,7 +4,6 @@ import type { AnalysisOverviewItem } from '../typing';
import { import {
Card, Card,
CardContent, CardContent,
CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
VbenCountToAnimator, VbenCountToAnimator,
@@ -26,32 +25,38 @@ withDefaults(defineProps<Props>(), {
<template> <template>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"> <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 v-for="(item, index) in items" :key="index" class="relative overflow-hidden">
<Card :title="item.title" class="w-full"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardHeader> <CardTitle class="text-sm font-medium">
<CardTitle class="text-xl">{{ item.title }}</CardTitle> {{ item.title }}
</CardHeader> </CardTitle>
<VbenIcon :icon="item.icon" class="h-4 w-4 text-muted-foreground" />
<CardContent class="flex items-center justify-between"> </CardHeader>
<VbenCountToAnimator <CardContent>
:end-val="item.value" <div class="text-2xl font-bold">
:start-val="1" <VbenCountToAnimator :end-val="item.value" />
class="text-xl" </div>
prefix="" <p class="text-xs text-muted-foreground">
:decimals="item.decimals ?? 0" {{ item.todaytitle }}
/> <span class="font-medium text-foreground">
<VbenIcon :icon="item.icon" class="size-8 flex-shrink-0" /> +<VbenCountToAnimator :end-val="item.todayValue" />
</CardContent> </span>
<CardFooter class="justify-between"> </p>
<span>{{ item.totalTitle }}</span> <div class="mt-3 border-t pt-3">
<VbenCountToAnimator <p class="text-xs text-muted-foreground flex justify-between">
:end-val="item.totalValue" <span>{{ item.Subtitle }}</span>
:start-val="1" <span class="font-medium text-foreground">
prefix="" <VbenCountToAnimator :end-val="item.SubValue" />
:decimals="item.decimals ?? 0" </span>
/> </p>
</CardFooter> <p class="text-xs text-muted-foreground flex justify-between mt-1">
</Card> <span>{{ item.todaySubtitle }}</span>
</template> <span class="font-medium text-foreground">
+<VbenCountToAnimator :end-val="item.todaySubValue" />
</span>
</p>
</div>
</CardContent>
</Card>
</div> </div>
</template> </template>

View File

@@ -3,10 +3,13 @@ import type { Component } from 'vue';
interface AnalysisOverviewItem { interface AnalysisOverviewItem {
icon: Component | string; icon: Component | string;
title: string; title: string;
totalTitle: string;
totalValue: number;
value: number; value: number;
decimals?: number; todaytitle: string;
todayValue: number;
Subtitle: string;
SubValue: number;
todaySubtitle: string;
todaySubValue: number;
} }
interface WorkbenchProjectItem { interface WorkbenchProjectItem {