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