ss
Some checks failed
Deploy Website on push / Deploy Push Element Ftp (push) Waiting to run
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Some checks failed
Deploy Website on push / Deploy Push Element Ftp (push) Waiting to run
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This commit is contained in:
216
apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue
Normal file
216
apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { statsHistory, statsTotal } from '#/api/promotion/analytics';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
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 () => {
|
||||
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 date = new Date();
|
||||
date.setDate(date.getDate() - 29 + index);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
});
|
||||
|
||||
// 如果有历史数据,使用历史数据;否则使用模拟数据
|
||||
let clickData = Array(30).fill(0);
|
||||
if (trendData && trendData.length > 0) {
|
||||
// 将历史数据按日期排序并映射到数组
|
||||
const sortedData = trendData.sort((a, b) =>
|
||||
new Date(a.stats_date).getTime() - new Date(b.stats_date).getTime()
|
||||
);
|
||||
|
||||
sortedData.forEach((item) => {
|
||||
const itemDate = new Date(item.stats_date);
|
||||
const today = new Date();
|
||||
const daysDiff = Math.floor((today.getTime() - itemDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff >= 0 && daysDiff < 30) {
|
||||
// 使用实际日期索引
|
||||
clickData[29 - daysDiff] = item.click_count || 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 没有历史数据时,使用统计数据生成模拟数据
|
||||
const todayClickCount = statsData?.today_click_count || 0;
|
||||
const totalClickCount = statsData?.total_click_count || 0;
|
||||
|
||||
// 简单的线性分布模拟数据
|
||||
for (let i = 0; i < 30; i++) {
|
||||
// 最后一天使用今日数据,其他天按比例分布
|
||||
if (i === 29) {
|
||||
clickData[i] = todayClickCount;
|
||||
} else {
|
||||
// 按指数衰减模拟历史数据
|
||||
clickData[i] = Math.max(0, Math.floor(todayClickCount * Math.exp(-0.05 * (29 - i))));
|
||||
}
|
||||
}
|
||||
|
||||
// 确保总和不超过总计数
|
||||
const sum = clickData.reduce((a, b) => a + b, 0);
|
||||
if (sum > totalClickCount && totalClickCount > 0) {
|
||||
const ratio = totalClickCount / sum;
|
||||
clickData = clickData.map(val => Math.floor(val * ratio));
|
||||
}
|
||||
}
|
||||
|
||||
// 计算Y轴最大值
|
||||
const maxValue = Math.max(...clickData) || 10;
|
||||
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: clickData,
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
name: '推广访问量',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#5ab1ef',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `${param.axisValue}<br/>${param.seriesName}: ${param.value}`;
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: Math.ceil(maxValue * 1.2), // 比最大值大20%作为Y轴上限
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取推广趋势数据失败:', error);
|
||||
|
||||
// 发生错误时显示默认图表
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: Array(30).fill(0),
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
name: '推广访问量',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#5ab1ef',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 30 }).map((_, index) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 29 + index);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
}),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 10,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['渠道A', '渠道B', '渠道C', '渠道D'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '渠道A' },
|
||||
{ name: '渠道B' },
|
||||
{ name: '渠道C' },
|
||||
{ name: '渠道D' },
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: { color: '#b6a2de' },
|
||||
name: '渠道A',
|
||||
value: [90, 50, 86, 40],
|
||||
},
|
||||
{
|
||||
itemStyle: { color: '#5ab1ef' },
|
||||
name: '渠道B',
|
||||
value: [70, 75, 70, 76],
|
||||
},
|
||||
{
|
||||
itemStyle: { color: '#67e0e3' },
|
||||
name: '渠道C',
|
||||
value: [60, 65, 60, 66],
|
||||
},
|
||||
{
|
||||
itemStyle: { color: '#2ec7c9' },
|
||||
name: '渠道D',
|
||||
value: [50, 55, 50, 56],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3'],
|
||||
data: [
|
||||
{ name: '佣金', value: 500 },
|
||||
{ name: '奖励', value: 310 },
|
||||
{ name: '提现', value: 274 },
|
||||
],
|
||||
name: '金额占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { getOrderSourceStatistics } from '#/api/order/order';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
const loading = ref(false);
|
||||
|
||||
// 获取订单来源统计数据
|
||||
async function fetchOrderSourceStatistics() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await getOrderSourceStatistics();
|
||||
|
||||
// 提取产品名称和订单数量
|
||||
const data = response.items.map(item => ({
|
||||
name: item.product_name,
|
||||
value: item.order_count,
|
||||
}));
|
||||
|
||||
// 如果有数据,则渲染图表
|
||||
if (data && data.length > 0) {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
data: data.map(item => item.name),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: [
|
||||
'#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9',
|
||||
'#ffb980', '#d87a80', '#8d98b3', '#e5cf0d',
|
||||
'#97b552', '#95706d', '#dc69aa', '#07a2a4',
|
||||
'#9a7fd1', '#588dd5', '#f5994e', '#c05050'
|
||||
],
|
||||
data: data,
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '订单来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单来源统计数据失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrderSourceStatistics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="flex justify-center items-center h-64">
|
||||
加载中...
|
||||
</div>
|
||||
<EchartsUI v-else ref="chartRef" />
|
||||
</template>
|
||||
262
apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue
Normal file
262
apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { getOrderStatistics } from '#/api/order/order-statistics';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
// 时间维度状态
|
||||
const timeDimension = ref<'day' | 'month' | 'year' | 'all'>('day');
|
||||
|
||||
// 获取订单统计数据
|
||||
async function fetchOrderStatistics() {
|
||||
try {
|
||||
console.log('Fetching order statistics with dimension:', timeDimension.value);
|
||||
|
||||
// 使用后端API获取数据
|
||||
const response = await getOrderStatistics(timeDimension.value);
|
||||
console.log('Order statistics response:', response);
|
||||
|
||||
let items = response.items || [];
|
||||
|
||||
// 如果后端返回空数据,显示空状态
|
||||
if (items.length === 0) {
|
||||
console.log('No data from backend, showing empty chart');
|
||||
items = [];
|
||||
}
|
||||
|
||||
console.log('Items for chart:', items);
|
||||
|
||||
// 按日期排序
|
||||
items.sort((a: any, b: any) => a.date.localeCompare(b.date));
|
||||
|
||||
// 提取日期和数量
|
||||
const dates = items.map((item: any) => {
|
||||
// 根据时间维度格式化日期显示
|
||||
const date = new Date(item.date);
|
||||
if (timeDimension.value === 'year') {
|
||||
return `${date.getFullYear()}`;
|
||||
} else if (timeDimension.value === 'month') {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${date.getMonth() + 1}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
});
|
||||
|
||||
const orderData = items.map((item: any) => item.count);
|
||||
const amountData = items.map((item: any) => item.amount);
|
||||
|
||||
// 计算Y轴最大值
|
||||
const maxOrderValue = Math.max(...orderData) || 10;
|
||||
const maxAmountValue = Math.max(...amountData) || 10;
|
||||
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '10%',
|
||||
},
|
||||
legend: {
|
||||
data: ['订单数', '订单金额'],
|
||||
top: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: orderData,
|
||||
type: 'line',
|
||||
name: '订单数',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#4f9cff',
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
color: '#4f9cff',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: amountData,
|
||||
type: 'line',
|
||||
name: '订单金额',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
color: '#52c41a',
|
||||
},
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
let result = `${params[0].axisValue}<br/>`;
|
||||
params.forEach((param: any) => {
|
||||
if (param.seriesName === '订单金额') {
|
||||
result += `${param.seriesName}: ¥${param.value.toFixed(2)}<br/>`;
|
||||
} else {
|
||||
result += `${param.seriesName}: ${param.value}<br/>`;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
data: dates,
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '订单数',
|
||||
position: 'left',
|
||||
max: Math.ceil(maxOrderValue * 1.2),
|
||||
splitNumber: 4,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额(¥)',
|
||||
position: 'right',
|
||||
max: Math.ceil(maxAmountValue * 1.2),
|
||||
splitNumber: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取订单趋势数据失败:', error);
|
||||
|
||||
// 发生错误时显示默认图表
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '10%',
|
||||
},
|
||||
legend: {
|
||||
data: ['订单数', '订单金额'],
|
||||
top: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: Array(30).fill(0),
|
||||
type: 'line',
|
||||
name: '订单数',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#4f9cff',
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
color: '#4f9cff',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: Array(30).fill(0),
|
||||
type: 'line',
|
||||
name: '订单金额',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
color: '#52c41a',
|
||||
},
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 30 }).map((_, index) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 29 + index);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
}),
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '订单数',
|
||||
position: 'left',
|
||||
max: 10,
|
||||
splitNumber: 4,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额(¥)',
|
||||
position: 'right',
|
||||
max: 10,
|
||||
splitNumber: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchOrderStatistics();
|
||||
});
|
||||
|
||||
// 监听时间维度变化
|
||||
watch(timeDimension, () => {
|
||||
fetchOrderStatistics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4 flex justify-end space-x-2">
|
||||
<Button
|
||||
:type="timeDimension === 'day' ? 'primary' : 'default'"
|
||||
@click="timeDimension = 'day'"
|
||||
>
|
||||
日
|
||||
</Button>
|
||||
<Button
|
||||
:type="timeDimension === 'month' ? 'primary' : 'default'"
|
||||
@click="timeDimension = 'month'"
|
||||
>
|
||||
月
|
||||
</Button>
|
||||
<Button
|
||||
:type="timeDimension === 'year' ? 'primary' : 'default'"
|
||||
@click="timeDimension = 'year'"
|
||||
>
|
||||
年
|
||||
</Button>
|
||||
<Button
|
||||
:type="timeDimension === 'all' ? 'primary' : 'default'"
|
||||
@click="timeDimension = 'all'"
|
||||
>
|
||||
全部
|
||||
</Button>
|
||||
</div>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</div>
|
||||
</template>
|
||||
255
apps/web-antd/src/views/dashboard/analytics/index.vue
Normal file
255
apps/web-antd/src/views/dashboard/analytics/index.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
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 { getAgentStatistics, getWithdrawalStatistics, getAgentOrderStatistics } from '#/api/agent';
|
||||
import { getOrderList, getRefundStatistics, getIncomeStatistics } from '#/api/order/order';
|
||||
import { getPlatformUserList } from '#/api/platform-user';
|
||||
|
||||
// 初始化概览数据
|
||||
const overviewItems = ref<AnalysisOverviewItem[]>([
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '总用户数',
|
||||
value: 0,
|
||||
todaytitle: '今日新增用户数',
|
||||
todayValue: 0,
|
||||
Subtitle: '总代理数',
|
||||
SubValue: 0,
|
||||
todaySubtitle: '今日新增代理数',
|
||||
todaySubValue: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '总订单数',
|
||||
value: 0,
|
||||
todaytitle: '今日新增订单数',
|
||||
todayValue: 0,
|
||||
Subtitle: '代理总订单量',
|
||||
SubValue: 0,
|
||||
todaySubtitle: '今日新增代理订单量',
|
||||
todaySubValue: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '总收入流水金额',
|
||||
value: 0,
|
||||
todaytitle: '今日新增收入流水金额',
|
||||
todayValue: 0,
|
||||
Subtitle: '总利润',
|
||||
SubValue: 0,
|
||||
todaySubtitle: '今日新增利润',
|
||||
todaySubValue: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '总提现金额',
|
||||
value: 0,
|
||||
SubValue: 0,
|
||||
todaySubtitle: '总实际到账金额',
|
||||
todaySubValue: 0,
|
||||
extraTitle: '总扣税金额',
|
||||
extraValue: 0,
|
||||
todaytitle: '今日新增提现金额',
|
||||
todayValue: 0,
|
||||
Subtitle: '总退款金额',
|
||||
extra2Title: '今日新增退款金额',
|
||||
extra2Value: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '订单趋势',
|
||||
value: 'visits',
|
||||
},
|
||||
{
|
||||
label: '推广访问趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
];
|
||||
|
||||
// 获取统计数据
|
||||
async function fetchStatistics() {
|
||||
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 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 orderTotal = orderResponse.total || 0;
|
||||
|
||||
// 获取代理订单数据
|
||||
const agentOrderResponse = await getAgentOrderStatistics();
|
||||
const agentOrderTotal = agentOrderResponse.total_agent_order_count || 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 getAgentOrderStatistics();
|
||||
const todayAgentOrderTotal = todayAgentOrderResponse.today_agent_order_count || 0;
|
||||
|
||||
// Product data is no longer needed for order statistics
|
||||
|
||||
// 获取代理统计数据
|
||||
const agentStatsResponse = await getAgentStatistics();
|
||||
const agentTotal = agentStatsResponse.total_agent_count || 0;
|
||||
const newAgentCount = agentStatsResponse.today_agent_count || 0;
|
||||
|
||||
// 获取提现统计数据
|
||||
const withdrawalStatsResponse = await getWithdrawalStatistics();
|
||||
const totalWithdrawalAmount = withdrawalStatsResponse.total_withdrawal_amount || 0;
|
||||
const todayWithdrawalAmount = withdrawalStatsResponse.today_withdrawal_amount || 0;
|
||||
const totalActualAmount = withdrawalStatsResponse.total_actual_amount || 0;
|
||||
const totalTaxAmount = withdrawalStatsResponse.total_tax_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 = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '总用户数',
|
||||
value: platformUserTotal,
|
||||
todaytitle: '今日新增用户数',
|
||||
todayValue: newUserCount,
|
||||
Subtitle: '总代理数',
|
||||
SubValue: agentTotal,
|
||||
todaySubtitle: '今日新增代理数',
|
||||
todaySubValue: newAgentCount,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '总订单数',
|
||||
value: orderTotal,
|
||||
todaytitle: '今日新增订单数',
|
||||
todayValue: todayOrderTotal,
|
||||
Subtitle: '总代理订单量',
|
||||
SubValue: agentOrderTotal,
|
||||
todaySubtitle: '今日新增代理订单量',
|
||||
todaySubValue: todayAgentOrderTotal,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '总收入流水金额',
|
||||
value: totalIncome,
|
||||
todaytitle: '今日新增收入流水金额',
|
||||
todayValue: todayIncome,
|
||||
Subtitle: '总利润',
|
||||
SubValue: totalProfit,
|
||||
todaySubtitle: '今日新增利润',
|
||||
todaySubValue: todayProfit,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '总提现金额',
|
||||
value: totalWithdrawalAmount,
|
||||
todaytitle: '今日新增提现金额',
|
||||
todayValue: todayWithdrawalAmount,
|
||||
extra2Title: '今日新增退款金额',
|
||||
extra2Value: todayRefundAmount,
|
||||
Subtitle: '总退款金额',
|
||||
SubValue: totalRefundAmount,
|
||||
todaySubtitle: '总实际到账金额',
|
||||
todaySubValue: totalActualAmount,
|
||||
extraTitle: '总扣税金额',
|
||||
extraValue: totalTaxAmount,
|
||||
},
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchStatistics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
266
apps/web-antd/src/views/dashboard/workspace/index.vue
Normal file
266
apps/web-antd/src/views/dashboard/workspace/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
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 {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user