From d5e5bf642cf5b789cb31b20cc44ae8051d1b86d9 Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 16 Jan 2026 09:55:49 -0800 Subject: [PATCH] fix(kline-chart): prevent overflow in indicator selector and legend - Wrap indicator selector Row in SingleChildScrollView for horizontal scrolling - Add maxX boundary checks in _drawLegend to stop drawing when exceeding available space - Prevents text overflow on narrow screens or when displaying many indicators Co-Authored-By: Claude Opus 4.5 --- .../kline_chart/kline_chart_widget.dart | 63 ++++++++++--------- .../widgets/kline_chart/kline_painter.dart | 15 +++-- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart b/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart index 1fa13388..12124d7f 100644 --- a/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart +++ b/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart @@ -232,36 +232,39 @@ class _KlineChartWidgetState extends State { decoration: BoxDecoration( border: Border(bottom: BorderSide(color: _borderGray)), ), - child: Row( - children: [ - const Text('主图:', style: TextStyle(fontSize: 12, color: _grayText)), - const SizedBox(width: 8), - _buildIndicatorChip('MA', _selectedMainIndicator == 0, () { - setState(() => _selectedMainIndicator = 0); - }), - const SizedBox(width: 4), - _buildIndicatorChip('EMA', _selectedMainIndicator == 1, () { - setState(() => _selectedMainIndicator = 1); - }), - const SizedBox(width: 4), - _buildIndicatorChip('BOLL', _selectedMainIndicator == 2, () { - setState(() => _selectedMainIndicator = 2); - }), - const SizedBox(width: 16), - const Text('副图:', style: TextStyle(fontSize: 12, color: _grayText)), - const SizedBox(width: 8), - _buildIndicatorChip('MACD', _selectedSubIndicator == 0, () { - setState(() => _selectedSubIndicator = 0); - }), - const SizedBox(width: 4), - _buildIndicatorChip('KDJ', _selectedSubIndicator == 1, () { - setState(() => _selectedSubIndicator = 1); - }), - const SizedBox(width: 4), - _buildIndicatorChip('RSI', _selectedSubIndicator == 2, () { - setState(() => _selectedSubIndicator = 2); - }), - ], + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + const Text('主图:', style: TextStyle(fontSize: 12, color: _grayText)), + const SizedBox(width: 8), + _buildIndicatorChip('MA', _selectedMainIndicator == 0, () { + setState(() => _selectedMainIndicator = 0); + }), + const SizedBox(width: 4), + _buildIndicatorChip('EMA', _selectedMainIndicator == 1, () { + setState(() => _selectedMainIndicator = 1); + }), + const SizedBox(width: 4), + _buildIndicatorChip('BOLL', _selectedMainIndicator == 2, () { + setState(() => _selectedMainIndicator = 2); + }), + const SizedBox(width: 16), + const Text('副图:', style: TextStyle(fontSize: 12, color: _grayText)), + const SizedBox(width: 8), + _buildIndicatorChip('MACD', _selectedSubIndicator == 0, () { + setState(() => _selectedSubIndicator = 0); + }), + const SizedBox(width: 4), + _buildIndicatorChip('KDJ', _selectedSubIndicator == 1, () { + setState(() => _selectedSubIndicator = 1); + }), + const SizedBox(width: 4), + _buildIndicatorChip('RSI', _selectedSubIndicator == 2, () { + setState(() => _selectedSubIndicator = 2); + }), + ], + ), ), ); } diff --git a/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_painter.dart b/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_painter.dart index a2456b41..b20ea133 100644 --- a/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_painter.dart +++ b/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_painter.dart @@ -370,10 +370,12 @@ class KlinePainter extends CustomPainter { final textPainter = TextPainter(textDirection: ui.TextDirection.ltr); double x = leftPadding; final y = 4.0; + final maxX = size.width - 60; // 留出右侧价格标签空间 if (maData != null) { int colorIndex = 0; for (final entry in maData!.entries) { + if (x > maxX) break; // 超出边界则停止绘制 final lastValue = entry.value.lastWhere((v) => v != null, orElse: () => null); if (lastValue != null) { textPainter.text = TextSpan( @@ -381,6 +383,7 @@ class KlinePainter extends CustomPainter { style: TextStyle(color: _maColors[colorIndex % _maColors.length], fontSize: 9), ); textPainter.layout(); + if (x + textPainter.width > maxX) break; textPainter.paint(canvas, Offset(x, y)); x += textPainter.width + 8; } @@ -391,6 +394,7 @@ class KlinePainter extends CustomPainter { if (emaData != null) { int colorIndex = 0; for (final entry in emaData!.entries) { + if (x > maxX) break; final lastValue = entry.value.lastWhere((v) => v != null, orElse: () => null); if (lastValue != null) { textPainter.text = TextSpan( @@ -398,6 +402,7 @@ class KlinePainter extends CustomPainter { style: TextStyle(color: _maColors[colorIndex % _maColors.length], fontSize: 9), ); textPainter.layout(); + if (x + textPainter.width > maxX) break; textPainter.paint(canvas, Offset(x, y)); x += textPainter.width + 8; } @@ -405,10 +410,8 @@ class KlinePainter extends CustomPainter { } } - if (bollData != null) { + if (bollData != null && x <= maxX) { final middle = bollData!['middle']?.lastWhere((v) => v != null, orElse: () => null); - final upper = bollData!['upper']?.lastWhere((v) => v != null, orElse: () => null); - final lower = bollData!['lower']?.lastWhere((v) => v != null, orElse: () => null); if (middle != null) { textPainter.text = TextSpan( @@ -416,8 +419,10 @@ class KlinePainter extends CustomPainter { style: const TextStyle(color: _bollMiddleColor, fontSize: 9), ); textPainter.layout(); - textPainter.paint(canvas, Offset(x, y)); - x += textPainter.width + 8; + if (x + textPainter.width <= maxX) { + textPainter.paint(canvas, Offset(x, y)); + x += textPainter.width + 8; + } } } }