import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import '../models/common_model.dart'; class StatsCard extends StatelessWidget { final String title; final String value; final String unit; final IconData icon; const StatsCard( {super.key, required this.title, required this.value, required this.unit, required this.icon}); @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.primary, shape: BoxShape.circle, ), child: Icon(icon, size: 26, color: colors.surface), ), SizedBox(width: 6), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( title, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( value, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(width: 4), Text( unit, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ], ), ), ], ); } } class LineChart extends StatelessWidget { final String title; final String xAxisName; final String yAxisName; final String unit; final List data; const LineChart( {super.key, required this.title, required this.xAxisName, required this.yAxisName, required this.unit, required this.data}); @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Column( children: [ _buildChartTitle(context, title, Icons.show_chart), const SizedBox(height: 3), _buildChartDivider(context), const SizedBox(height: 3), Expanded( child: SfCartesianChart( // 图表标题 // title: ChartTitle(text: ''), // X轴配置(类别轴) primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), labelIntersectAction: AxisLabelIntersectAction.hide, interval: (data.length > 6) ? 3 : 1, // 根据数据量动态算 edgeLabelPlacement: EdgeLabelPlacement.shift, ), // Y轴配置(数值轴) // primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), // 启用图例 legend: Legend(isVisible: true, position: LegendPosition.top), // 启用交互提示(点击数据点显示详情) tooltipBehavior: TooltipBehavior( enable: true, format: 'point.x: point.y $unit', ), // 折线图数据系列 series: [ LineSeries( dataSource: data, // X轴数据映射 xValueMapper: (ChartData chart, _) => chart.name, // Y轴数据映射 yValueMapper: (ChartData chart, _) => chart.value, // 线条颜色 color: colors.primary, // 线条宽度 width: 3, // 数据点样式 markerSettings: const MarkerSettings( isVisible: true, color: Colors.white, shape: DataMarkerType.circle, height: 6, width: 6, ), // 折线名称(会显示在图例中) name: yAxisName, // 启用数据标签(直接显示数值) dataLabelSettings: const DataLabelSettings( isVisible: true, color: Colors.white, opacity: 0, ), // 动画效果 animationDuration: 2000, // 动画时长(毫秒) ), ], )), ], ); } } class BarChart extends StatelessWidget { final String title; final String xAxisName; final String yAxisName; final String unit; final List data; const BarChart( {super.key, required this.title, required this.xAxisName, required this.yAxisName, required this.unit, required this.data}); @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Column(children: [ _buildChartTitle(context, title, Icons.bar_chart), const SizedBox(height: 3), _buildChartDivider(context), const SizedBox(height: 3), Expanded( child: SfCartesianChart( // X轴配置(类别轴) primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)), // Y轴配置(数值轴) // primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), // 启用交互提示 tooltipBehavior: TooltipBehavior( enable: true, format: 'point.x: point.y $unit', ), // 柱状图数据系列 series: [ ColumnSeries( dataSource: data, // X轴数据映射 xValueMapper: (ChartData chart, _) => chart.name, // Y轴数据映射 yValueMapper: (ChartData chart, _) => chart.value, // 名称 name: yAxisName, // 柱子颜色 color: colors.primary, // 柱子宽度(0-1之间,1表示占满类别间隔) width: 0.6, // 柱子边框 borderWidth: 1, borderColor: Colors.black12, // 数据标签 dataLabelSettings: const DataLabelSettings( isVisible: true, color: Colors.white, opacity: 0, alignment: ChartAlignment.center, ), // 动画效果 animationDuration: 2000, ), ], )) ]); } } class DoubleBarChart extends StatelessWidget { final String title; final String xAxisName; final String yAxisName; final String unit; final List data1; final List data2; final String series1Name; final String series2Name; const DoubleBarChart( {super.key, required this.title, required this.xAxisName, required this.yAxisName, required this.unit, required this.data1, required this.data2, required this.series1Name, required this.series2Name}); @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Column(children: [ _buildChartTitle(context, title, Icons.bar_chart), const SizedBox(height: 3), _buildChartDivider(context), const SizedBox(height: 3), Expanded( child: SfCartesianChart( // X轴配置(类别轴) primaryXAxis: CategoryAxis(majorGridLines: const MajorGridLines(width: 0)), // Y轴配置(数值轴) // primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), // 图例配置 legend: Legend( isVisible: true, position: LegendPosition.top, overflowMode: LegendItemOverflowMode.wrap, ), // 启用交互提示 tooltipBehavior: TooltipBehavior( enable: true, format: 'series.name: point.y $unit', ), // 双柱状图数据系列 series: >[ ColumnSeries( dataSource: data1, // X轴数据映射 xValueMapper: (ChartData chart, _) => chart.name, // Y轴数据映射 yValueMapper: (ChartData chart, _) => chart.value, // 系列名称 name: series1Name, // 柱子颜色 color: colors.primary, // 柱子宽度 width: 0.3, // 柱子边框 borderWidth: 1, borderColor: Colors.black12, // 数据标签 dataLabelSettings: const DataLabelSettings( isVisible: true, color: Colors.white, opacity: 0, alignment: ChartAlignment.center, ), // 动画效果 animationDuration: 2000, ), ColumnSeries( dataSource: data2, // X轴数据映射 xValueMapper: (ChartData chart, _) => chart.name, // Y轴数据映射 yValueMapper: (ChartData chart, _) => chart.value, // 系列名称 name: series2Name, // 柱子颜色 color: colors.inversePrimary, // 柱子宽度 width: 0.3, // 柱子边框 borderWidth: 1, borderColor: Colors.black12, // 数据标签 dataLabelSettings: const DataLabelSettings( isVisible: true, color: Colors.white, opacity: 0, alignment: ChartAlignment.center, ), // 动画效果 animationDuration: 2000, ), ], )) ]); } } class PieChart extends StatelessWidget { final String title; final String unit; final List data; const PieChart( {super.key, required this.title, required this.unit, required this.data}); @override Widget build(BuildContext context) { // 计算 value 的总和 double sumValue = data.fold(0.0, (sum, item) => sum + item.value); return Column(children: [ _buildChartTitle(context, title, Icons.pie_chart), const SizedBox(height: 3), _buildChartDivider(context), const SizedBox(height: 3), Expanded( child: SfCircularChart( // 饼图标题 // title: ChartTitle(text: ''), // 启用图例 legend: const Legend(isVisible: true, position: LegendPosition.right), // 启用交互提示(点击扇区显示详情) tooltipBehavior: TooltipBehavior( enable: true, format: 'point.x: point.y $unit', ), // 饼图系列配置 series: [ PieSeries( dataSource: data, // 类别映射(饼图扇区名称) xValueMapper: (ChartData data, _) => data.name, // 数值映射(扇区大小占比) yValueMapper: (ChartData data, _) => data.value, // 扇区半径(0-1之间,1表示充满容器) // radius: '50%', // 启用扇区分离效果 explode: true, // 指定分离的扇区索引(这里分离第一个扇区) explodeIndex: 0, // 分离距离 explodeOffset: '5%', dataLabelMapper: (ChartData data, _) { final percentage = (data.value / sumValue * 100).toStringAsFixed(0); return '$percentage%'; }, // 数据标签(显示在扇区上的文本) dataLabelSettings: DataLabelSettings(isVisible: true), // 动画效果 animationDuration: 2000, ), ], )) ]); } } class RankChart extends StatelessWidget { final String title; final String unit; final List data; const RankChart( {super.key, required this.title, required this.unit, required this.data}); @override Widget build(BuildContext context) { return Column(children: [ _buildChartTitle(context, title, Icons.bar_chart), const SizedBox(height: 3), _buildChartDivider(context), const SizedBox(height: 3), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: data.length, itemBuilder: (context, index) { final item = data[index]; final rank = index + 1; return SizedBox( height: 35, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( width: 25, alignment: Alignment.center, child: Text( '$rank.', style: TextStyle( fontWeight: FontWeight.bold, color: _getRankColor(context, index), ), ), ), Text( item.name, style: TextStyle( fontWeight: index < 3 ? FontWeight.bold : FontWeight.normal, color: _getRankColor(context, index), ), ), ], ), Text( '${item.value.toStringAsFixed(0)} $unit', style: TextStyle( fontWeight: index < 3 ? FontWeight.bold : FontWeight.normal, color: _getRankColor(context, index), ), ), ], ), ); }, ) ]); } } Widget _buildChartTitle(BuildContext context, String title, IconData icon) { return Row( children: [ Icon(icon, color: Theme.of(context).colorScheme.primary), SizedBox(width: 4), Text(title, style: Theme.of(context).textTheme.titleMedium), ], ); } Widget _buildChartDivider(BuildContext context) { return Divider( height: 1, thickness: 1, color: Theme.of(context).colorScheme.primary, indent: 0, endIndent: 0, ); } Color _getRankColor(BuildContext context, int index) { switch (index) { case 0: // 金牌 return const Color(0xFFFFD700); // 标准金色 case 1: // 银牌 return const Color(0xFFC0C0C0); // 标准银色 case 2: // 铜牌 return const Color(0xFFCD7F32); // 标准铜色 default: return Theme.of(context).colorScheme.onSurface; // 普通颜色 } }