feat(frontend): K线图组件支持深色模式

- kline_chart_widget.dart: 使用 AppColors 动态颜色,传递 isDark 参数
- kline_painter.dart: 添加 isDark 参数,网格/文字/十字线颜色随主题变化
- kline_volume_painter.dart: 添加 isDark 参数,成交量图颜色随主题变化

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-19 19:42:52 -08:00
parent 2745995a1a
commit f51aa44cd9
3 changed files with 60 additions and 42 deletions

View File

@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../../domain/entities/kline.dart'; import '../../../domain/entities/kline.dart';
import '../../../core/constants/app_colors.dart';
import 'kline_painter.dart'; import 'kline_painter.dart';
import 'kline_indicator_painter.dart'; import 'kline_indicator_painter.dart';
import 'kline_volume_painter.dart'; import 'kline_volume_painter.dart';
@ -35,12 +36,10 @@ class KlineChartWidget extends StatefulWidget {
} }
class _KlineChartWidgetState extends State<KlineChartWidget> { class _KlineChartWidgetState extends State<KlineChartWidget> {
// //
static const Color _orange = Color(0xFFFF6B00); static const Color _orange = AppColors.orange;
static const Color _green = Color(0xFF10B981); static const Color _green = Color(0xFF10B981);
static const Color _red = Color(0xFFEF4444); static const Color _red = Color(0xFFEF4444);
static const Color _grayText = Color(0xFF6B7280);
static const Color _borderGray = Color(0xFFE5E7EB);
// - candleWidth的缩放模式 // - candleWidth的缩放模式
double _candleWidth = 8.0; // K线宽度 double _candleWidth = 8.0; // K线宽度
@ -141,7 +140,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
Widget _buildNormalChart() { Widget _buildNormalChart() {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppColors.cardOf(context),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
@ -159,20 +158,20 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
Widget _buildFullScreenChart() { Widget _buildFullScreenChart() {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: AppColors.backgroundOf(context),
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: AppColors.cardOf(context),
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1F2937)), icon: Icon(Icons.close, color: AppColors.textPrimaryOf(context)),
onPressed: widget.onFullScreenToggle, onPressed: widget.onFullScreenToggle,
), ),
title: const Text( title: Text(
'K线图', 'K线图',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF1F2937), color: AppColors.textPrimaryOf(context),
), ),
), ),
centerTitle: true, centerTitle: true,
@ -244,15 +243,16 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
} }
Widget _buildIndicatorChip(String label, bool isSelected, VoidCallback onTap) { Widget _buildIndicatorChip(String label, bool isSelected, VoidCallback onTap) {
final isDark = AppColors.isDark(context);
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? _orange.withOpacity(0.1) : Colors.transparent, color: isSelected ? _orange.withOpacity(isDark ? 0.2 : 0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all( border: Border.all(
color: isSelected ? _orange : _borderGray, color: isSelected ? _orange : AppColors.borderOf(context),
), ),
), ),
child: Text( child: Text(
@ -260,7 +260,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? _orange : _grayText, color: isSelected ? _orange : AppColors.textSecondaryOf(context),
), ),
), ),
), ),
@ -271,13 +271,13 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: _borderGray)), border: Border(bottom: BorderSide(color: AppColors.borderOf(context))),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: [ children: [
const Text('主图:', style: TextStyle(fontSize: 12, color: _grayText)), Text('主图:', style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context))),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildIndicatorChip('MA', _selectedMainIndicator == 0, () { _buildIndicatorChip('MA', _selectedMainIndicator == 0, () {
setState(() => _selectedMainIndicator = 0); setState(() => _selectedMainIndicator = 0);
@ -291,7 +291,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
setState(() => _selectedMainIndicator = 2); setState(() => _selectedMainIndicator = 2);
}), }),
const SizedBox(width: 16), const SizedBox(width: 16),
const Text('副图:', style: TextStyle(fontSize: 12, color: _grayText)), Text('副图:', style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context))),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildIndicatorChip('MACD', _selectedSubIndicator == 0, () { _buildIndicatorChip('MACD', _selectedSubIndicator == 0, () {
setState(() => _selectedSubIndicator = 0); setState(() => _selectedSubIndicator = 0);
@ -342,9 +342,9 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.show_chart, size: 48, color: _grayText.withOpacity(0.5)), Icon(Icons.show_chart, size: 48, color: AppColors.textMutedOf(context)),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text('暂无K线数据', style: TextStyle(color: _grayText)), Text('暂无K线数据', style: TextStyle(color: AppColors.textSecondaryOf(context))),
], ],
), ),
); );
@ -389,6 +389,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
bollData: _selectedMainIndicator == 2 ? _getVisibleBollData(bollData, visibleData.startIndex) : null, bollData: _selectedMainIndicator == 2 ? _getVisibleBollData(bollData, visibleData.startIndex) : null,
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1, crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
candleWidth: visibleData.candleWidth, candleWidth: visibleData.candleWidth,
isDark: AppColors.isDark(context),
), ),
), ),
// 线 // 线
@ -428,6 +429,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
klines: visibleData.klines, klines: visibleData.klines,
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1, crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
candleWidth: visibleData.candleWidth, candleWidth: visibleData.candleWidth,
isDark: AppColors.isDark(context),
), ),
), ),
), ),
@ -469,16 +471,16 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? _orange : Colors.white, color: isSelected ? _orange : AppColors.cardOf(context),
borderRadius: BorderRadius.circular(9999), borderRadius: BorderRadius.circular(9999),
border: isSelected ? null : Border.all(color: _borderGray), border: isSelected ? null : Border.all(color: AppColors.borderOf(context)),
), ),
child: Text( child: Text(
widget.timeRanges[index], widget.timeRanges[index],
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: isSelected ? Colors.white : _grayText, color: isSelected ? Colors.white : AppColors.textSecondaryOf(context),
), ),
), ),
), ),
@ -655,18 +657,19 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
final volume = double.tryParse(kline.volume) ?? 0; final volume = double.tryParse(kline.volume) ?? 0;
final isUp = close >= open; final isUp = close >= open;
final isDark = AppColors.isDark(context);
return Positioned( return Positioned(
left: 8, left: 8,
top: 8, top: 8,
child: Container( child: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95), color: AppColors.cardOf(context).withOpacity(0.95),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: _borderGray), border: Border.all(color: AppColors.borderOf(context)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withOpacity(isDark ? 0.3 : 0.1),
blurRadius: 4, blurRadius: 4,
), ),
], ],
@ -677,7 +680,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
children: [ children: [
Text( Text(
_formatDateTime(kline.time), _formatDateTime(kline.time),
style: const TextStyle(fontSize: 10, color: _grayText), style: TextStyle(fontSize: 10, color: AppColors.textSecondaryOf(context)),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
@ -698,7 +701,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
], ],
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
_buildInfoItem('', _formatVolume(volume), _grayText), _buildInfoItem('', _formatVolume(volume), AppColors.textSecondaryOf(context)),
], ],
), ),
), ),
@ -711,7 +714,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
children: [ children: [
Text( Text(
'$label: ', '$label: ',
style: const TextStyle(fontSize: 10, color: _grayText), style: TextStyle(fontSize: 10, color: AppColors.textSecondaryOf(context)),
), ),
Text( Text(
value, value,

View File

@ -11,12 +11,15 @@ class KlinePainter extends CustomPainter {
final Map<String, List<double?>>? bollData; final Map<String, List<double?>>? bollData;
final int crossLineIndex; final int crossLineIndex;
final double? candleWidth; // K线宽度 final double? candleWidth; // K线宽度
final bool isDark; //
static const Color _green = Color(0xFF10B981); static const Color _green = Color(0xFF10B981);
static const Color _red = Color(0xFFEF4444); static const Color _red = Color(0xFFEF4444);
static const Color _gridColor = Color(0xFFE5E7EB);
static const Color _textColor = Color(0xFF6B7280); //
static const Color _crossLineColor = Color(0xFF9CA3AF); Color get _gridColor => isDark ? const Color(0xFF374151) : const Color(0xFFE5E7EB);
Color get _textColor => isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280);
Color get _crossLineColor => isDark ? const Color(0xFF6B7280) : const Color(0xFF9CA3AF);
// MA线颜色 // MA线颜色
static const List<Color> _maColors = [ static const List<Color> _maColors = [
@ -38,6 +41,7 @@ class KlinePainter extends CustomPainter {
this.bollData, this.bollData,
this.crossLineIndex = -1, this.crossLineIndex = -1,
this.candleWidth, this.candleWidth,
this.isDark = false,
}); });
@override @override
@ -248,7 +252,7 @@ class KlinePainter extends CustomPainter {
final price = maxPrice - priceStep * i; final price = maxPrice - priceStep * i;
textPainter.text = TextSpan( textPainter.text = TextSpan(
text: _formatPrice(price), text: _formatPrice(price),
style: const TextStyle(color: _textColor, fontSize: 9), style: TextStyle(color: _textColor, fontSize: 9),
); );
textPainter.layout(); textPainter.layout();
textPainter.paint(canvas, Offset(size.width - rightPadding + 4, y - textPainter.height / 2)); textPainter.paint(canvas, Offset(size.width - rightPadding + 4, y - textPainter.height / 2));
@ -344,12 +348,16 @@ class KlinePainter extends CustomPainter {
// //
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan( text: const TextSpan(
text: _formatPrice(close), text: '',
style: const TextStyle(color: Colors.white, fontSize: 9), style: TextStyle(color: Colors.white, fontSize: 9),
), ),
textDirection: ui.TextDirection.ltr, textDirection: ui.TextDirection.ltr,
); );
textPainter.text = TextSpan(
text: _formatPrice(close),
style: const TextStyle(color: Colors.white, fontSize: 9),
);
textPainter.layout(); textPainter.layout();
final labelRect = RRect.fromRectAndRadius( final labelRect = RRect.fromRectAndRadius(
@ -443,6 +451,7 @@ class KlinePainter extends CustomPainter {
oldDelegate.emaData != emaData || oldDelegate.emaData != emaData ||
oldDelegate.bollData != bollData || oldDelegate.bollData != bollData ||
oldDelegate.crossLineIndex != crossLineIndex || oldDelegate.crossLineIndex != crossLineIndex ||
oldDelegate.candleWidth != candleWidth; oldDelegate.candleWidth != candleWidth ||
oldDelegate.isDark != isDark;
} }
} }

View File

@ -8,16 +8,21 @@ class KlineVolumePainter extends CustomPainter {
final List<Kline> klines; final List<Kline> klines;
final int crossLineIndex; final int crossLineIndex;
final double? candleWidth; final double? candleWidth;
final bool isDark; //
static const Color _green = Color(0xFF10B981); static const Color _green = Color(0xFF10B981);
static const Color _red = Color(0xFFEF4444); static const Color _red = Color(0xFFEF4444);
static const Color _gridColor = Color(0xFFE5E7EB);
static const Color _textColor = Color(0xFF6B7280); //
Color get _gridColor => isDark ? const Color(0xFF374151) : const Color(0xFFE5E7EB);
Color get _textColor => isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280);
Color get _crossLineColor => isDark ? const Color(0xFF6B7280) : const Color(0xFF9CA3AF);
KlineVolumePainter({ KlineVolumePainter({
required this.klines, required this.klines,
this.crossLineIndex = -1, this.crossLineIndex = -1,
this.candleWidth, this.candleWidth,
this.isDark = false,
}); });
@override @override
@ -55,7 +60,7 @@ class KlineVolumePainter extends CustomPainter {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan( text: TextSpan(
text: 'VOL', text: 'VOL',
style: const TextStyle(color: _textColor, fontSize: 9), style: TextStyle(color: _textColor, fontSize: 9),
), ),
textDirection: ui.TextDirection.ltr, textDirection: ui.TextDirection.ltr,
); );
@ -66,7 +71,7 @@ class KlineVolumePainter extends CustomPainter {
final maxVolText = TextPainter( final maxVolText = TextPainter(
text: TextSpan( text: TextSpan(
text: _formatVolume(maxVolume), text: _formatVolume(maxVolume),
style: const TextStyle(color: _textColor, fontSize: 8), style: TextStyle(color: _textColor, fontSize: 8),
), ),
textDirection: ui.TextDirection.ltr, textDirection: ui.TextDirection.ltr,
); );
@ -106,7 +111,7 @@ class KlineVolumePainter extends CustomPainter {
Offset(x, 0), Offset(x, 0),
Offset(x, size.height), Offset(x, size.height),
Paint() Paint()
..color = const Color(0xFF9CA3AF) ..color = _crossLineColor
..strokeWidth = 0.5, ..strokeWidth = 0.5,
); );
@ -115,7 +120,7 @@ class KlineVolumePainter extends CustomPainter {
final volText = TextPainter( final volText = TextPainter(
text: TextSpan( text: TextSpan(
text: _formatVolume(volume), text: _formatVolume(volume),
style: const TextStyle(color: _textColor, fontSize: 9, fontWeight: FontWeight.bold), style: TextStyle(color: _textColor, fontSize: 9, fontWeight: FontWeight.bold),
), ),
textDirection: ui.TextDirection.ltr, textDirection: ui.TextDirection.ltr,
); );
@ -135,6 +140,7 @@ class KlineVolumePainter extends CustomPainter {
bool shouldRepaint(covariant KlineVolumePainter oldDelegate) { bool shouldRepaint(covariant KlineVolumePainter oldDelegate) {
return oldDelegate.klines != klines || return oldDelegate.klines != klines ||
oldDelegate.crossLineIndex != crossLineIndex || oldDelegate.crossLineIndex != crossLineIndex ||
oldDelegate.candleWidth != candleWidth; oldDelegate.candleWidth != candleWidth ||
oldDelegate.isDark != isDark;
} }
} }