This commit is contained in:
2026-02-08 17:01:33 +08:00
parent 0668eea99b
commit 3211cc32ce
76 changed files with 5054 additions and 2423 deletions

View File

@@ -0,0 +1,57 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace DashboardApi {
export interface OrderStatistics {
today_count: number;
month_count: number;
total_count: number;
yesterday_count: number;
change_rate: number;
}
export interface RevenueStatistics {
today_amount: number;
month_amount: number;
total_amount: number;
yesterday_amount: number;
change_rate: number;
}
export interface AgentStatistics {
total_count: number;
today_new: number;
month_new: number;
}
export interface ProfitStatistics {
today_profit: number;
month_profit: number;
total_profit: number;
today_profit_rate: number;
month_profit_rate: number;
total_profit_rate: number;
}
export interface TrendData {
date: string;
value: number;
}
export interface DashboardStatistics {
order_stats: OrderStatistics;
revenue_stats: RevenueStatistics;
agent_stats: AgentStatistics;
profit_stats: ProfitStatistics;
order_trend: TrendData[];
revenue_trend: TrendData[];
}
}
export async function getDashboardStatistics(): Promise<DashboardApi.DashboardStatistics> {
return await requestClient.get<DashboardApi.DashboardStatistics>(
'/api/v1/admin/dashboard/statistics',
);
}

View File

@@ -0,0 +1,2 @@
export * from './dashboard';

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const updateChart = () => {
if (!props.data || props.data.length === 0) {
return;
}
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2%',
},
series: [
{
areaStyle: {},
data: values,
itemStyle: {
color: '#019680',
},
smooth: true,
type: 'line',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
width: 1,
},
},
trigger: 'axis',
formatter: (params: any) => {
const param = params[0];
return `${param.name}<br/>${param.seriesName}: ¥${param.value.toFixed(2)}`;
},
},
xAxis: {
axisTick: {
show: false,
},
boundaryGap: false,
data: dates,
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category',
},
yAxis: [
{
axisTick: {
show: false,
},
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value',
axisLabel: {
formatter: (value: number) => {
if (value >= 10000) {
return `${(value / 10000).toFixed(1)}`;
}
return value.toString();
},
},
},
],
});
};
watch(
() => props.data,
() => {
updateChart();
},
{ deep: true },
);
onMounted(() => {
updateChart();
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -1,72 +1,62 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
const updateChart = () => {
if (!props.data || props.data.length === 0) {
return;
}
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2 %',
top: '2%',
},
series: [
{
areaStyle: {},
data: [
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
111,
],
data: values,
itemStyle: {
color: '#5ab1ef',
},
smooth: true,
type: 'line',
},
{
areaStyle: {},
data: [
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
],
itemStyle: {
color: '#019680',
},
smooth: true,
type: 'line',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
color: '#5ab1ef',
width: 1,
},
},
trigger: 'axis',
},
// xAxis: {
// axisTick: {
// show: false,
// },
// boundaryGap: false,
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
// type: 'category',
// },
xAxis: {
axisTick: {
show: false,
},
boundaryGap: false,
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
data: dates,
splitLine: {
lineStyle: {
type: 'solid',
@@ -81,7 +71,6 @@ onMounted(() => {
axisTick: {
show: false,
},
max: 80_000,
splitArea: {
show: true,
},
@@ -90,6 +79,18 @@ onMounted(() => {
},
],
});
};
watch(
() => props.data,
() => {
updateChart();
},
{ deep: true },
);
onMounted(() => {
updateChart();
});
</script>

View File

@@ -2,6 +2,8 @@
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { TabOption } from '@vben/types';
import { computed, onMounted, ref } from 'vue';
import {
AnalysisChartCard,
AnalysisChartsTabs,
@@ -13,78 +15,211 @@ import {
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import { message } from 'ant-design-vue';
import { getDashboardStatistics } from '#/api/dashboard';
import type { DashboardApi } from '#/api/dashboard';
import AnalyticsTrends from './analytics-trends.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue';
import AnalyticsVisits from './analytics-visits.vue';
import AnalyticsRevenueTrend from './analytics-revenue-trend.vue';
const overviewItems: AnalysisOverviewItem[] = [
{
icon: SvgCardIcon,
title: '用户量',
totalTitle: '总用户量',
totalValue: 120_000,
value: 2000,
},
{
icon: SvgCakeIcon,
title: '访问量',
totalTitle: '总访问量',
totalValue: 500_000,
value: 20_000,
},
{
icon: SvgDownloadIcon,
title: '下载量',
totalTitle: '总下载量',
totalValue: 120_000,
value: 8000,
},
{
icon: SvgBellIcon,
title: '使用量',
totalTitle: '总使用量',
totalValue: 50_000,
value: 5000,
},
];
const loading = ref(false);
const statistics = ref<DashboardApi.DashboardStatistics | null>(null);
// 格式化金额
const formatAmount = (amount: number) => {
if (amount >= 10000) {
return `${(amount / 10000).toFixed(2)}`;
}
return amount.toFixed(2);
};
// 格式化数字
const formatNumber = (num: number) => {
if (num >= 10000) {
return `${(num / 10000).toFixed(2)}`;
}
return num.toString();
};
// 加载统计数据
const loadStatistics = async () => {
loading.value = true;
try {
const data = await getDashboardStatistics();
statistics.value = data;
} catch (error) {
message.error('加载统计数据失败');
} finally {
loading.value = false;
}
};
// 计算概览卡片数据
const overviewItems = computed<AnalysisOverviewItem[]>(() => {
if (!statistics.value) {
return [];
}
const { order_stats, revenue_stats, agent_stats, profit_stats } =
statistics.value;
return [
{
icon: SvgCardIcon,
title: '今日订单',
totalTitle: '总订单',
totalValue: order_stats.total_count,
value: order_stats.today_count,
suffix: order_stats.change_rate
? ` ${order_stats.change_rate > 0 ? '↑' : '↓'} ${Math.abs(
order_stats.change_rate,
).toFixed(1)}%`
: '',
},
{
icon: SvgCakeIcon,
title: '今日营收',
totalTitle: '总营收',
totalValue: revenue_stats.total_amount,
value: revenue_stats.today_amount,
suffix: revenue_stats.change_rate
? ` ${revenue_stats.change_rate > 0 ? '↑' : '↓'} ${Math.abs(
revenue_stats.change_rate,
).toFixed(1)}%`
: '',
},
{
icon: SvgBellIcon,
title: '代理总数',
totalTitle: '代理总数',
totalValue: agent_stats.total_count,
value: agent_stats.today_new,
suffix: ` 今日新增: ${agent_stats.today_new}`,
},
{
icon: SvgDownloadIcon,
title: '今日利润',
totalTitle: '总利润',
totalValue: profit_stats.total_profit,
value: profit_stats.today_profit,
suffix: ` 利润率: ${profit_stats.today_profit_rate.toFixed(1)}%`,
},
];
});
const chartTabs: TabOption[] = [
{
label: '流量趋势',
value: 'trends',
label: '订单趋势',
value: 'order',
},
{
label: '月访问量',
value: 'visits',
label: '营收趋势',
value: 'revenue',
},
];
onMounted(() => {
loadStatistics();
});
</script>
<template>
<div class="p-5">
<AnalysisOverview :items="overviewItems" />
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<AnalyticsTrends />
</template>
<template #visits>
<AnalyticsVisits />
</template>
</AnalysisChartsTabs>
<a-spin :spinning="loading">
<div v-if="statistics">
<!-- 统计卡片 -->
<AnalysisOverview :items="overviewItems" />
<div class="mt-5 w-full md:flex">
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
<AnalyticsVisitsData />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSales />
</AnalysisChartCard>
</div>
<!-- 趋势图表 -->
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #order>
<AnalyticsTrends :data="statistics.order_trend" />
</template>
<template #revenue>
<AnalyticsRevenueTrend :data="statistics.revenue_trend" />
</template>
</AnalysisChartsTabs>
<!-- 详细统计信息 -->
<div class="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- 订单统计卡片 -->
<AnalysisChartCard title="订单统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">今日订单:</span>
<span class="font-semibold">{{ statistics.order_stats.today_count }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月订单:</span>
<span class="font-semibold">{{ formatNumber(statistics.order_stats.month_count) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">总订单:</span>
<span class="font-semibold">{{ formatNumber(statistics.order_stats.total_count) }}</span>
</div>
</div>
</AnalysisChartCard>
<!-- 营收统计卡片 -->
<AnalysisChartCard title="营收统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">今日营收:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.revenue_stats.today_amount) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月营收:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.revenue_stats.month_amount) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">总营收:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.revenue_stats.total_amount) }}</span>
</div>
</div>
</AnalysisChartCard>
<!-- 代理统计卡片 -->
<AnalysisChartCard title="代理统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">代理总数:</span>
<span class="font-semibold">{{ statistics.agent_stats.total_count }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">今日新增:</span>
<span class="font-semibold">{{ statistics.agent_stats.today_new }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月新增:</span>
<span class="font-semibold">{{ statistics.agent_stats.month_new }}</span>
</div>
</div>
</AnalysisChartCard>
<!-- 利润统计卡片 -->
<AnalysisChartCard title="利润统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">今日利润:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.profit_stats.today_profit) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月利润:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.profit_stats.month_profit) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">总利润:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.profit_stats.total_profit) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">利润率:</span>
<span class="font-semibold">{{ statistics.profit_stats.total_profit_rate.toFixed(1) }}%</span>
</div>
</div>
</AnalysisChartCard>
</div>
</div>
</a-spin>
</div>
</template>