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

View File

@ -11,12 +11,15 @@ class KlinePainter extends CustomPainter {
final Map<String, List<double?>>? bollData;
final int crossLineIndex;
final double? candleWidth; // K线宽度
final bool isDark; //
static const Color _green = Color(0xFF10B981);
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线颜色
static const List<Color> _maColors = [
@ -38,6 +41,7 @@ class KlinePainter extends CustomPainter {
this.bollData,
this.crossLineIndex = -1,
this.candleWidth,
this.isDark = false,
});
@override
@ -248,7 +252,7 @@ class KlinePainter extends CustomPainter {
final price = maxPrice - priceStep * i;
textPainter.text = TextSpan(
text: _formatPrice(price),
style: const TextStyle(color: _textColor, fontSize: 9),
style: TextStyle(color: _textColor, fontSize: 9),
);
textPainter.layout();
textPainter.paint(canvas, Offset(size.width - rightPadding + 4, y - textPainter.height / 2));
@ -344,12 +348,16 @@ class KlinePainter extends CustomPainter {
//
final textPainter = TextPainter(
text: TextSpan(
text: _formatPrice(close),
style: const TextStyle(color: Colors.white, fontSize: 9),
text: const TextSpan(
text: '',
style: TextStyle(color: Colors.white, fontSize: 9),
),
textDirection: ui.TextDirection.ltr,
);
textPainter.text = TextSpan(
text: _formatPrice(close),
style: const TextStyle(color: Colors.white, fontSize: 9),
);
textPainter.layout();
final labelRect = RRect.fromRectAndRadius(
@ -443,6 +451,7 @@ class KlinePainter extends CustomPainter {
oldDelegate.emaData != emaData ||
oldDelegate.bollData != bollData ||
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 int crossLineIndex;
final double? candleWidth;
final bool isDark; //
static const Color _green = Color(0xFF10B981);
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({
required this.klines,
this.crossLineIndex = -1,
this.candleWidth,
this.isDark = false,
});
@override
@ -55,7 +60,7 @@ class KlineVolumePainter extends CustomPainter {
final textPainter = TextPainter(
text: TextSpan(
text: 'VOL',
style: const TextStyle(color: _textColor, fontSize: 9),
style: TextStyle(color: _textColor, fontSize: 9),
),
textDirection: ui.TextDirection.ltr,
);
@ -66,7 +71,7 @@ class KlineVolumePainter extends CustomPainter {
final maxVolText = TextPainter(
text: TextSpan(
text: _formatVolume(maxVolume),
style: const TextStyle(color: _textColor, fontSize: 8),
style: TextStyle(color: _textColor, fontSize: 8),
),
textDirection: ui.TextDirection.ltr,
);
@ -106,7 +111,7 @@ class KlineVolumePainter extends CustomPainter {
Offset(x, 0),
Offset(x, size.height),
Paint()
..color = const Color(0xFF9CA3AF)
..color = _crossLineColor
..strokeWidth = 0.5,
);
@ -115,7 +120,7 @@ class KlineVolumePainter extends CustomPainter {
final volText = TextPainter(
text: TextSpan(
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,
);
@ -135,6 +140,7 @@ class KlineVolumePainter extends CustomPainter {
bool shouldRepaint(covariant KlineVolumePainter oldDelegate) {
return oldDelegate.klines != klines ||
oldDelegate.crossLineIndex != crossLineIndex ||
oldDelegate.candleWidth != candleWidth;
oldDelegate.candleWidth != candleWidth ||
oldDelegate.isDark != isDark;
}
}