f
This commit is contained in:
57
playground/src/api/dashboard/dashboard.ts
Normal file
57
playground/src/api/dashboard/dashboard.ts
Normal 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',
|
||||
);
|
||||
}
|
||||
|
||||
2
playground/src/api/dashboard/index.ts
Normal file
2
playground/src/api/dashboard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './dashboard';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user