Files
xianyan/lib/features/reading_report/presentation/widgets/trend_chart.dart
Developer ae1df22732 feat: v6.10.3 多语言翻译补全 + 17项功能修复
- 引导页协议多语言支持(languageId传递)
- 登录页双书名号修复 + 注册页协议勾选
- 个人中心页面多语言(18个翻译键)
- 网络断开提示增加关闭/刷新按钮
- 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索
- iOS快捷按钮重复修复(删除Info.plist静态定义)
- 测试账号123456警告提示
- 扫码登录自动跳转(HTTP轮询+WebSocket双通道)
- 登录页老用户按钮改次要色
- Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0)
- macOS标题栏跟随软件夜间模式
- 平台兼容分发渠道弹窗
- 软件著作权图片+交叉水印
- 桌面小部件平台兼容说明默认收起
- iOS/macOS图标更新+名称确认为闲言
- 12个语言文件补全roleNative+7个分发渠道翻译字段
2026-06-02 04:50:32 +08:00

148 lines
4.7 KiB
Dart

/// ============================================================
/// 闲言APP — 阅读报告趋势图
/// 创建时间: 2026-05-02
/// 更新时间: 2026-05-29
/// 作用: 折线图展示阅读/签到/金币趋势
/// 上次更新: 从fl_chart迁移至syncfusion_flutter_charts
/// ============================================================
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../shared/widgets/containers/deferred_builder.dart';
import '../../models/reading_report_models.dart';
class _TrendData {
final int index;
final double value;
final String dateLabel;
const _TrendData(this.index, this.value, this.dateLabel);
}
class TrendChart extends StatelessWidget {
const TrendChart({super.key, required this.trend, required this.showSignins});
final List<TrendPoint> trend;
final bool showSignins;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
if (trend.isEmpty) {
return const SizedBox(height: 200, child: Center(child: Text('暂无趋势数据')));
}
final viewData = trend.asMap().entries.map((e) {
final date = e.value.date;
final parts = date.split('-');
final label = parts.length >= 3 ? '${parts[1]}/${parts[2]}' : date;
return _TrendData(e.key, e.value.views.toDouble(), label);
}).toList();
final signinData = trend.asMap().entries.map((e) {
final date = e.value.date;
final parts = date.split('-');
final label = parts.length >= 3 ? '${parts[1]}/${parts[2]}' : date;
return _TrendData(e.key, e.value.signins.toDouble(), label);
}).toList();
final series = <CartesianSeries<_TrendData, int>>[
LineSeries<_TrendData, int>(
dataSource: viewData,
xValueMapper: (_TrendData d, _) => d.index,
yValueMapper: (_TrendData d, _) => d.value,
color: ext.accent,
width: 2.5,
animationDuration: 0,
name: '浏览',
markerSettings: MarkerSettings(
isVisible: trend.length <= 14,
height: 6,
width: 6,
color: ext.accent,
borderColor: Colors.white,
borderWidth: 1.5,
),
),
if (showSignins)
LineSeries<_TrendData, int>(
dataSource: signinData,
xValueMapper: (_TrendData d, _) => d.index,
yValueMapper: (_TrendData d, _) => d.value,
color: CupertinoColors.activeGreen,
width: 2.5,
animationDuration: 0,
name: '签到',
markerSettings: MarkerSettings(
isVisible: trend.length <= 14,
height: 6,
width: 6,
color: CupertinoColors.activeGreen,
borderColor: Colors.white,
borderWidth: 1.5,
),
),
];
return SizedBox(
height: 220,
child: DeferredBuilder(
builder: (context) => SfCartesianChart(
plotAreaBorderWidth: 0,
primaryXAxis: NumericAxis(
majorGridLines: const MajorGridLines(width: 0),
interval: _calcBottomInterval(),
labelStyle: TextStyle(color: ext.textHint, fontSize: 9),
axisLabelFormatter: (AxisLabelRenderDetails details) {
final idx = details.value.toInt();
if (idx < 0 || idx >= trend.length) {
return ChartAxisLabel('', details.textStyle);
}
final raw = trend[idx].date;
final parts = raw.split('-');
final label = parts.length >= 3 ? '${parts[1]}/${parts[2]}' : raw;
return ChartAxisLabel(label, details.textStyle);
},
),
primaryYAxis: NumericAxis(
majorGridLines: MajorGridLines(
width: 0.5,
color: ext.bgCard.withValues(alpha: 0.5),
),
interval: _calcInterval(),
labelStyle: TextStyle(color: ext.textHint, fontSize: 10),
),
tooltipBehavior: TooltipBehavior(
enable: true,
color: ext.bgCard,
textStyle: TextStyle(
color: ext.accent,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
series: series,
),
),
);
}
double _calcInterval() {
if (trend.isEmpty) return 1;
final maxVal = trend.map((e) => e.views).reduce((a, b) => a > b ? a : b);
if (maxVal <= 5) return 1;
if (maxVal <= 20) return 5;
if (maxVal <= 50) return 10;
return (maxVal / 5).ceilToDouble();
}
double _calcBottomInterval() {
if (trend.length <= 7) return 1;
if (trend.length <= 14) return 2;
if (trend.length <= 30) return 5;
return 10;
}
}