feat(planting-service, admin-web): 实现认种趋势图表功能
后端变更 (planting-service): - 添加 getTrendData API 接口支持按时间维度(日/周/月/季度/年)查询趋势数据 - 添加 TrendPeriod 类型和 TrendDataPoint 接口 - 实现 repository 层的趋势数据聚合查询 前端变更 (admin-web): - 添加趋势数据 API 端点和类型定义 - 使用 recharts 实现折线图展示认种棵数和订单数趋势 - 支持日/周/月/季度/年度时间维度切换 - 添加加载状态、错误状态和空数据状态处理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4f3660f05e
commit
fa1931b3b6
|
|
@ -1,13 +1,14 @@
|
||||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
|
ApiQuery,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { PlantingApplicationService } from '../../application/services/planting-application.service';
|
import { PlantingApplicationService, TrendPeriodType } from '../../application/services/planting-application.service';
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
import { GlobalStatsResponseDto } from '../dto/response/planting-stats.response';
|
import { GlobalStatsResponseDto, TrendDataResponseDto } from '../dto/response/planting-stats.response';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认种统计控制器
|
* 认种统计控制器
|
||||||
|
|
@ -69,4 +70,41 @@ export class PlantingStatsController {
|
||||||
data: stats,
|
data: stats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取认种趋势数据(公开接口)
|
||||||
|
* [2026-01-06] 新增
|
||||||
|
*/
|
||||||
|
@Get('trend')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取认种趋势数据',
|
||||||
|
description: '按时间维度聚合认种统计数据,用于绘制趋势图表',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'period',
|
||||||
|
required: false,
|
||||||
|
enum: ['day', 'week', 'month', 'quarter', 'year'],
|
||||||
|
description: '时间维度:day(最近30天)、week(最近12周)、month(最近12月)、quarter(最近8季度)、year(最近5年)',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '趋势数据列表',
|
||||||
|
type: [TrendDataResponseDto],
|
||||||
|
})
|
||||||
|
async getTrendData(
|
||||||
|
@Query('period') period: TrendPeriodType = 'day',
|
||||||
|
): Promise<{ code: number; message: string; data: TrendDataResponseDto[] }> {
|
||||||
|
const validPeriods: TrendPeriodType[] = ['day', 'week', 'month', 'quarter', 'year'];
|
||||||
|
if (!validPeriods.includes(period)) {
|
||||||
|
period = 'day';
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this.plantingService.getTrendData(period);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,3 +75,21 @@ export class GlobalStatsResponseDto {
|
||||||
@ApiProperty({ description: '统计计算时间', example: '2026-01-04T12:00:00.000Z' })
|
@ApiProperty({ description: '统计计算时间', example: '2026-01-04T12:00:00.000Z' })
|
||||||
calculatedAt: string;
|
calculatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 趋势数据响应 DTO
|
||||||
|
* [2026-01-06] 新增
|
||||||
|
*/
|
||||||
|
export class TrendDataResponseDto {
|
||||||
|
@ApiProperty({ description: '时间标签', example: '2026-01-06' })
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '认种棵数', example: 10 })
|
||||||
|
treeCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单数', example: 5 })
|
||||||
|
orderCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '认种金额', example: '1000.00' })
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -535,6 +535,15 @@ export class PlantingApplicationService {
|
||||||
calculatedAt: new Date().toISOString(),
|
calculatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取认种趋势数据
|
||||||
|
* [2026-01-06] 新增
|
||||||
|
*/
|
||||||
|
async getTrendData(period: TrendPeriodType): Promise<TrendDataResult[]> {
|
||||||
|
this.logger.log(`Getting planting trend data for period: ${period}`);
|
||||||
|
return this.orderRepository.getTrendData(period);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 全局统计结果 */
|
/** 全局统计结果 */
|
||||||
|
|
@ -568,3 +577,14 @@ export interface GlobalStatsResult {
|
||||||
/** 统计时间 */
|
/** 统计时间 */
|
||||||
calculatedAt: string;
|
calculatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 趋势时间维度类型 [2026-01-06] 新增 */
|
||||||
|
export type TrendPeriodType = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||||
|
|
||||||
|
/** 趋势数据结果 [2026-01-06] 新增 */
|
||||||
|
export interface TrendDataResult {
|
||||||
|
label: string;
|
||||||
|
treeCount: number;
|
||||||
|
orderCount: number;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,13 @@ export interface IPlantingOrderRepository {
|
||||||
* 按状态获取订单分布统计
|
* 按状态获取订单分布统计
|
||||||
*/
|
*/
|
||||||
getStatusDistribution(): Promise<StatusDistribution>;
|
getStatusDistribution(): Promise<StatusDistribution>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取趋势数据
|
||||||
|
* [2026-01-06] 新增
|
||||||
|
* @param period 时间维度: day(最近30天), week(最近12周), month(最近12月), quarter(最近8季度), year(最近5年)
|
||||||
|
*/
|
||||||
|
getTrendData(period: TrendPeriod): Promise<TrendDataPoint[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 全局认种统计 */
|
/** 全局认种统计 */
|
||||||
|
|
@ -70,4 +77,19 @@ export interface StatusDistribution {
|
||||||
miningEnabled: number;
|
miningEnabled: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 趋势时间维度 [2026-01-06] 新增 */
|
||||||
|
export type TrendPeriod = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||||
|
|
||||||
|
/** 趋势数据点 [2026-01-06] 新增 */
|
||||||
|
export interface TrendDataPoint {
|
||||||
|
/** 时间标签 (如 "2026-01-06", "2026-W01", "2026-01", "2026-Q1", "2026") */
|
||||||
|
label: string;
|
||||||
|
/** 认种棵数 */
|
||||||
|
treeCount: number;
|
||||||
|
/** 订单数 */
|
||||||
|
orderCount: number;
|
||||||
|
/** 认种金额 */
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const PLANTING_ORDER_REPOSITORY = Symbol('IPlantingOrderRepository');
|
export const PLANTING_ORDER_REPOSITORY = Symbol('IPlantingOrderRepository');
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import {
|
||||||
GlobalPlantingStats,
|
GlobalPlantingStats,
|
||||||
DailyPlantingStats,
|
DailyPlantingStats,
|
||||||
StatusDistribution,
|
StatusDistribution,
|
||||||
|
TrendPeriod,
|
||||||
|
TrendDataPoint,
|
||||||
} from '../../../domain/repositories/planting-order.repository.interface';
|
} from '../../../domain/repositories/planting-order.repository.interface';
|
||||||
import { PlantingOrder } from '../../../domain/aggregates/planting-order.aggregate';
|
import { PlantingOrder } from '../../../domain/aggregates/planting-order.aggregate';
|
||||||
import { PlantingOrderStatus } from '../../../domain/value-objects/planting-order-status.enum';
|
import { PlantingOrderStatus } from '../../../domain/value-objects/planting-order-status.enum';
|
||||||
|
|
@ -399,4 +401,93 @@ export class PlantingOrderRepositoryImpl implements IPlantingOrderRepository {
|
||||||
|
|
||||||
return distribution;
|
return distribution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取趋势数据
|
||||||
|
* [2026-01-06] 新增
|
||||||
|
*/
|
||||||
|
async getTrendData(period: TrendPeriod): Promise<TrendDataPoint[]> {
|
||||||
|
const paidStatuses = [
|
||||||
|
PlantingOrderStatus.PAID,
|
||||||
|
PlantingOrderStatus.FUND_ALLOCATED,
|
||||||
|
PlantingOrderStatus.POOL_SCHEDULED,
|
||||||
|
PlantingOrderStatus.POOL_INJECTED,
|
||||||
|
PlantingOrderStatus.MINING_ENABLED,
|
||||||
|
];
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let startDate: Date;
|
||||||
|
let dateFormat: string;
|
||||||
|
let groupByExpression: string;
|
||||||
|
|
||||||
|
// 根据不同维度设置时间范围和分组方式
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
// 最近30天,按天分组
|
||||||
|
startDate = new Date(now);
|
||||||
|
startDate.setDate(startDate.getDate() - 29);
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
dateFormat = 'YYYY-MM-DD';
|
||||||
|
groupByExpression = `TO_CHAR("paid_at", 'YYYY-MM-DD')`;
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
// 最近12周,按周分组
|
||||||
|
startDate = new Date(now);
|
||||||
|
startDate.setDate(startDate.getDate() - 83); // ~12周
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
dateFormat = 'IYYY-IW';
|
||||||
|
groupByExpression = `TO_CHAR("paid_at", 'IYYY-"W"IW')`;
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
// 最近12个月,按月分组
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth() - 11, 1);
|
||||||
|
dateFormat = 'YYYY-MM';
|
||||||
|
groupByExpression = `TO_CHAR("paid_at", 'YYYY-MM')`;
|
||||||
|
break;
|
||||||
|
case 'quarter':
|
||||||
|
// 最近8季度,按季度分组
|
||||||
|
startDate = new Date(now.getFullYear() - 2, 0, 1);
|
||||||
|
dateFormat = 'YYYY-QQ';
|
||||||
|
groupByExpression = `TO_CHAR("paid_at", 'YYYY-"Q"') || EXTRACT(QUARTER FROM "paid_at")`;
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
// 最近5年,按年分组
|
||||||
|
startDate = new Date(now.getFullYear() - 4, 0, 1);
|
||||||
|
dateFormat = 'YYYY';
|
||||||
|
groupByExpression = `TO_CHAR("paid_at", 'YYYY')`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid period: ${period}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用原生SQL进行分组查询
|
||||||
|
const results = await this.prisma.$queryRawUnsafe<
|
||||||
|
Array<{
|
||||||
|
label: string;
|
||||||
|
tree_count: bigint;
|
||||||
|
order_count: bigint;
|
||||||
|
total_amount: bigint | null;
|
||||||
|
}>
|
||||||
|
>(`
|
||||||
|
SELECT
|
||||||
|
${groupByExpression} as label,
|
||||||
|
COALESCE(SUM(tree_count), 0) as tree_count,
|
||||||
|
COUNT(*) as order_count,
|
||||||
|
COALESCE(SUM(total_amount), 0) as total_amount
|
||||||
|
FROM planting_order
|
||||||
|
WHERE status IN (${paidStatuses.map(s => `'${s}'`).join(', ')})
|
||||||
|
AND paid_at >= $1
|
||||||
|
AND paid_at IS NOT NULL
|
||||||
|
GROUP BY ${groupByExpression}
|
||||||
|
ORDER BY label ASC
|
||||||
|
`, startDate);
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
return results.map(r => ({
|
||||||
|
label: r.label,
|
||||||
|
treeCount: Number(r.tree_count),
|
||||||
|
orderCount: Number(r.order_count),
|
||||||
|
amount: (r.total_amount ?? 0).toString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
/**
|
/**
|
||||||
* 数据统计页面
|
* 数据统计页面
|
||||||
* [2026-01-04] 更新:新增系统账户报表Tab
|
* [2026-01-04] 更新:新增系统账户报表Tab
|
||||||
* [2026-01-06] 更新:认种统计改为真实数据,删除不相关的mock功能
|
* [2026-01-06] 更新:认种统计改为真实数据,实现趋势图表
|
||||||
* 回滚方式:删除 SystemAccountsTab 导入及相关 mainTab state 和切换逻辑
|
* 回滚方式:删除 SystemAccountsTab 导入及相关 mainTab state 和切换逻辑
|
||||||
*/
|
*/
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Legend,
|
||||||
|
} from 'recharts';
|
||||||
import { PageContainer } from '@/components/layout';
|
import { PageContainer } from '@/components/layout';
|
||||||
import { cn } from '@/utils/helpers';
|
import { cn } from '@/utils/helpers';
|
||||||
// [2026-01-04] 新增:系统账户报表Tab
|
// [2026-01-04] 新增:系统账户报表Tab
|
||||||
import { SystemAccountsTab } from '@/components/features/system-account-report';
|
import { SystemAccountsTab } from '@/components/features/system-account-report';
|
||||||
import { dashboardService } from '@/services/dashboardService';
|
import { dashboardService } from '@/services/dashboardService';
|
||||||
import type { PlantingGlobalStats } from '@/types';
|
import type { PlantingGlobalStats, PlantingTrendPeriod, PlantingTrendDataPoint } from '@/types';
|
||||||
import styles from './statistics.module.scss';
|
import styles from './statistics.module.scss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,22 +41,53 @@ function formatAmount(amount: string): string {
|
||||||
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
|
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化趋势图标签
|
||||||
|
*/
|
||||||
|
function formatTrendLabel(label: string, period: PlantingTrendPeriod): string {
|
||||||
|
if (!label) return '';
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
// 2026-01-06 -> 01-06
|
||||||
|
return label.slice(5);
|
||||||
|
case 'week':
|
||||||
|
// 2026-W01 -> W01
|
||||||
|
return label.slice(5);
|
||||||
|
case 'month':
|
||||||
|
// 2026-01 -> 01月
|
||||||
|
return label.slice(5) + '月';
|
||||||
|
case 'quarter':
|
||||||
|
// 2026-Q1 -> Q1
|
||||||
|
return label.slice(5);
|
||||||
|
case 'year':
|
||||||
|
// 2026 -> 2026年
|
||||||
|
return label + '年';
|
||||||
|
default:
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据统计页面
|
* 数据统计页面
|
||||||
* [2026-01-04] 更新:新增系统账户报表Tab
|
* [2026-01-04] 更新:新增系统账户报表Tab
|
||||||
* [2026-01-06] 更新:认种统计改为真实数据
|
* [2026-01-06] 更新:认种统计改为真实数据,实现趋势图表
|
||||||
*/
|
*/
|
||||||
export default function StatisticsPage() {
|
export default function StatisticsPage() {
|
||||||
// [2026-01-04] 新增:主Tab切换 - 数据统计 vs 系统账户
|
// [2026-01-04] 新增:主Tab切换 - 数据统计 vs 系统账户
|
||||||
const [mainTab, setMainTab] = useState<'statistics' | 'system-accounts'>('statistics');
|
const [mainTab, setMainTab] = useState<'statistics' | 'system-accounts'>('statistics');
|
||||||
// 趋势图时间维度
|
// 趋势图时间维度
|
||||||
const [trendPeriod, setTrendPeriod] = useState<'day' | 'week' | 'month' | 'quarter' | 'year'>('day');
|
const [trendPeriod, setTrendPeriod] = useState<PlantingTrendPeriod>('day');
|
||||||
|
|
||||||
// [2026-01-06] 新增:认种统计数据状态
|
// [2026-01-06] 新增:认种统计数据状态
|
||||||
const [plantingStats, setPlantingStats] = useState<PlantingGlobalStats | null>(null);
|
const [plantingStats, setPlantingStats] = useState<PlantingGlobalStats | null>(null);
|
||||||
const [statsLoading, setStatsLoading] = useState(true);
|
const [statsLoading, setStatsLoading] = useState(true);
|
||||||
const [statsError, setStatsError] = useState<string | null>(null);
|
const [statsError, setStatsError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// [2026-01-06] 新增:趋势数据状态
|
||||||
|
const [trendData, setTrendData] = useState<PlantingTrendDataPoint[]>([]);
|
||||||
|
const [trendLoading, setTrendLoading] = useState(true);
|
||||||
|
const [trendError, setTrendError] = useState<string | null>(null);
|
||||||
|
|
||||||
// [2026-01-06] 新增:获取认种统计数据
|
// [2026-01-06] 新增:获取认种统计数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchPlantingStats() {
|
async function fetchPlantingStats() {
|
||||||
|
|
@ -69,6 +110,35 @@ export default function StatisticsPage() {
|
||||||
fetchPlantingStats();
|
fetchPlantingStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// [2026-01-06] 新增:获取趋势数据
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchTrendData() {
|
||||||
|
try {
|
||||||
|
setTrendLoading(true);
|
||||||
|
setTrendError(null);
|
||||||
|
const response = await dashboardService.getPlantingTrendData(trendPeriod);
|
||||||
|
if (response.code === 0 && response.data) {
|
||||||
|
setTrendData(response.data);
|
||||||
|
} else {
|
||||||
|
setTrendError(response.message || '获取趋势数据失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取趋势数据失败:', error);
|
||||||
|
setTrendError('获取趋势数据失败');
|
||||||
|
} finally {
|
||||||
|
setTrendLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchTrendData();
|
||||||
|
}, [trendPeriod]);
|
||||||
|
|
||||||
|
// 处理趋势数据用于图表显示
|
||||||
|
const chartData = trendData.map(item => ({
|
||||||
|
name: formatTrendLabel(item.label, trendPeriod),
|
||||||
|
认种棵数: item.treeCount,
|
||||||
|
订单数: item.orderCount,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer title="数据统计">
|
<PageContainer title="数据统计">
|
||||||
<div className={styles.statistics}>
|
<div className={styles.statistics}>
|
||||||
|
|
@ -152,7 +222,7 @@ export default function StatisticsPage() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 榴莲树认种数量趋势 */}
|
{/* 榴莲树认种数量趋势 - [2026-01-06] 实现真实图表 */}
|
||||||
<section className={styles.statistics__card}>
|
<section className={styles.statistics__card}>
|
||||||
<div className={styles.statistics__cardHeader}>
|
<div className={styles.statistics__cardHeader}>
|
||||||
<b className={styles.statistics__cardTitle}>榴莲树认种数量趋势</b>
|
<b className={styles.statistics__cardTitle}>榴莲树认种数量趋势</b>
|
||||||
|
|
@ -168,7 +238,58 @@ export default function StatisticsPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statistics__chartArea}>Chart Placeholder</div>
|
<div className={styles.statistics__chartArea}>
|
||||||
|
{trendLoading ? (
|
||||||
|
<div className={styles.statistics__chartLoading}>加载中...</div>
|
||||||
|
) : trendError ? (
|
||||||
|
<div className={styles.statistics__chartError}>{trendError}</div>
|
||||||
|
) : chartData.length === 0 ? (
|
||||||
|
<div className={styles.statistics__chartEmpty}>暂无数据</div>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
tickLine={{ stroke: '#e5e7eb' }}
|
||||||
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
tickLine={{ stroke: '#e5e7eb' }}
|
||||||
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
|
allowDecimals={false}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="认种棵数"
|
||||||
|
stroke="#10b981"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#10b981', strokeWidth: 2, r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="订单数"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: '#3b82f6', strokeWidth: 2, r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -174,17 +174,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 图表占位区域 */
|
/* 图表区域 [2026-01-06] 更新:支持真实图表 */
|
||||||
.statistics__chartArea {
|
.statistics__chartArea {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
height: 320px;
|
min-height: 320px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #f3f4f6;
|
background-color: #f9fafb;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表加载状态 */
|
||||||
|
.statistics__chartLoading {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表错误状态 */
|
||||||
|
.statistics__chartError {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表空数据状态 */
|
||||||
|
.statistics__chartEmpty {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 龙虎榜与排名统计 */
|
/* 龙虎榜与排名统计 */
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ export const API_ENDPOINTS = {
|
||||||
// 认种统计 (planting-service) - 从订单表实时聚合,数据可靠
|
// 认种统计 (planting-service) - 从订单表实时聚合,数据可靠
|
||||||
PLANTING_STATS: {
|
PLANTING_STATS: {
|
||||||
GLOBAL: '/v1/planting/stats/global',
|
GLOBAL: '/v1/planting/stats/global',
|
||||||
|
// [2026-01-06] 新增:趋势数据接口
|
||||||
|
TREND: '/v1/planting/stats/trend',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 通知管理 (admin-service)
|
// 通知管理 (admin-service)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import type {
|
||||||
DashboardActivity,
|
DashboardActivity,
|
||||||
DashboardStatItem,
|
DashboardStatItem,
|
||||||
PlantingGlobalStats,
|
PlantingGlobalStats,
|
||||||
|
PlantingTrendPeriod,
|
||||||
|
PlantingTrendDataPoint,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
|
||||||
/** 仪表板概览响应 */
|
/** 仪表板概览响应 */
|
||||||
|
|
@ -89,6 +91,17 @@ export const dashboardService = {
|
||||||
async getPlantingStats(): Promise<ApiResponse<PlantingGlobalStats>> {
|
async getPlantingStats(): Promise<ApiResponse<PlantingGlobalStats>> {
|
||||||
return apiClient.get(API_ENDPOINTS.PLANTING_STATS.GLOBAL);
|
return apiClient.get(API_ENDPOINTS.PLANTING_STATS.GLOBAL);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取认种趋势数据
|
||||||
|
* [2026-01-06] 新增
|
||||||
|
* @param period 时间维度:day(最近30天)、week(最近12周)、month(最近12月)、quarter(最近8季度)、year(最近5年)
|
||||||
|
*/
|
||||||
|
async getPlantingTrendData(period: PlantingTrendPeriod = 'day'): Promise<ApiResponse<PlantingTrendDataPoint[]>> {
|
||||||
|
return apiClient.get(API_ENDPOINTS.PLANTING_STATS.TREND, {
|
||||||
|
params: { period },
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default dashboardService;
|
export default dashboardService;
|
||||||
|
|
|
||||||
|
|
@ -116,3 +116,14 @@ export interface PlantingGlobalStats {
|
||||||
monthStats: PlantingPeriodStats;
|
monthStats: PlantingPeriodStats;
|
||||||
calculatedAt: string;
|
calculatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 趋势时间维度 [2026-01-06] 新增 */
|
||||||
|
export type PlantingTrendPeriod = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||||
|
|
||||||
|
/** 趋势数据点 [2026-01-06] 新增 */
|
||||||
|
export interface PlantingTrendDataPoint {
|
||||||
|
label: string;
|
||||||
|
treeCount: number;
|
||||||
|
orderCount: number;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue