fix(trading): K线Y轴动态缩放 + 价格显示格式统一 + 首日涨幅修复

- K线图Y轴改为只基于可见K线计算范围,实现动态缩放(类似TradingView)
- 价格显示统一使用 0.0{n}xxx 格式替代科学计数法,覆盖全局 formatPrice、
  K线图、交易记录页、挖矿记录页
- 修复"较上线首日"百分比始终显示 +0.00% 的问题:getFirstSnapshot 过滤
  price=0 的早期快照

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-04 18:36:26 -08:00
parent f13814e577
commit c24f383501
6 changed files with 133 additions and 18 deletions

View File

@ -29,10 +29,11 @@ export class PriceSnapshotRepository {
}
/**
* 线
* 线
*/
async getFirstSnapshot(): Promise<PriceSnapshotEntity | null> {
const record = await this.prisma.priceSnapshot.findFirst({
where: { price: { gt: 0 } },
orderBy: { snapshotTime: 'asc' },
});
if (!record) {

View File

@ -44,7 +44,30 @@ String formatPercent(String? value, [int precision = 2]) {
}
String formatPrice(String? value) {
return formatDecimal(value, 8);
if (value == null || value.isEmpty) return '0';
try {
final decimal = Decimal.parse(value);
if (decimal >= Decimal.one) return decimal.toStringAsFixed(4);
if (decimal >= Decimal.parse('0.0001')) return decimal.toStringAsFixed(6);
if (decimal <= Decimal.zero) return '0';
// 0.00000980 0.0{5}980
final str = decimal.toStringAsFixed(18);
final dotIndex = str.indexOf('.');
int zeroCount = 0;
for (int i = dotIndex + 1; i < str.length; i++) {
if (str[i] == '0') {
zeroCount++;
} else {
break;
}
}
final sigStart = dotIndex + 1 + zeroCount;
final sigEnd = sigStart + 3 > str.length ? str.length : sigStart + 3;
final significant = str.substring(sigStart, sigEnd);
return '0.0{$zeroCount}$significant';
} catch (e) {
return '0';
}
}
String formatAmount(String? value) {

View File

@ -302,7 +302,24 @@ class _MiningRecordsListPageState extends ConsumerState<MiningRecordsListPage> {
String _formatPrice(String price) {
try {
final value = double.parse(price);
return value.toStringAsFixed(8);
if (value >= 1) return value.toStringAsFixed(4);
if (value >= 0.0001) return value.toStringAsFixed(6);
if (value <= 0) return '0';
// 0.00000980 0.0{5}980
final str = value.toStringAsFixed(18);
final dotIndex = str.indexOf('.');
int zeroCount = 0;
for (int i = dotIndex + 1; i < str.length; i++) {
if (str[i] == '0') {
zeroCount++;
} else {
break;
}
}
final sigStart = dotIndex + 1 + zeroCount;
final sigEnd = sigStart + 3 > str.length ? str.length : sigStart + 3;
final significant = str.substring(sigStart, sigEnd);
return '0.0{$zeroCount}$significant';
} catch (e) {
return price;
}

View File

@ -511,7 +511,24 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> with Si
String _formatPrice(String price) {
try {
final value = double.parse(price);
return value.toStringAsFixed(4);
if (value >= 1) return value.toStringAsFixed(4);
if (value >= 0.0001) return value.toStringAsFixed(6);
if (value <= 0) return '0';
// 0.00000980 0.0{5}980
final str = value.toStringAsFixed(18);
final dotIndex = str.indexOf('.');
int zeroCount = 0;
for (int i = dotIndex + 1; i < str.length; i++) {
if (str[i] == '0') {
zeroCount++;
} else {
break;
}
}
final sigStart = dotIndex + 1 + zeroCount;
final sigEnd = sigStart + 3 > str.length ? str.length : sigStart + 3;
final significant = str.substring(sigStart, sigEnd);
return '0.0{$zeroCount}$significant';
} catch (e) {
return price;
}

View File

@ -730,7 +730,22 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
String _formatPrice(double price) {
if (price >= 1) return price.toStringAsFixed(4);
if (price >= 0.0001) return price.toStringAsFixed(6);
return price.toStringAsExponential(2);
if (price <= 0) return '0';
// 0.00000980 0.0{5}980
final str = price.toStringAsFixed(18);
final dotIndex = str.indexOf('.');
int zeroCount = 0;
for (int i = dotIndex + 1; i < str.length; i++) {
if (str[i] == '0') {
zeroCount++;
} else {
break;
}
}
final sigStart = dotIndex + 1 + zeroCount;
final sigEnd = math.min(sigStart + 3, str.length);
final significant = str.substring(sigStart, sigEnd);
return '0.0{$zeroCount}$significant';
}
String _formatVolume(double volume) {
@ -746,17 +761,30 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
}
double _calcPriceY(double price, double chartHeight) {
if (widget.klines.isEmpty) return chartHeight / 2;
if (widget.klines.isEmpty || _chartWidth == 0) return chartHeight / 2;
// KlinePainter K线计算Y轴范围
const leftPadding = 8.0;
const rightPadding = 50.0;
final drawableWidth = _chartWidth - leftPadding - rightPadding;
final visibleStart = math.max(0, (_scrollX / _candleWidth - 2).floor());
final visibleEnd = math.min(widget.klines.length - 1, ((_scrollX + drawableWidth) / _candleWidth + 1).ceil());
double minPrice = double.infinity;
double maxPrice = double.negativeInfinity;
for (final kline in widget.klines) {
for (int i = visibleStart; i <= visibleEnd; i++) {
final kline = widget.klines[i];
final low = double.tryParse(kline.low) ?? 0;
final high = double.tryParse(kline.high) ?? 0;
if (low < minPrice) minPrice = low;
if (high > maxPrice) maxPrice = high;
}
if (minPrice == double.infinity || maxPrice == double.negativeInfinity) {
return chartHeight / 2;
}
final range = maxPrice - minPrice;
// range 0 NaN
final safeRange = range > 0 ? range : (maxPrice > 0 ? maxPrice * 0.1 : 1.0);

View File

@ -59,21 +59,31 @@ class KlinePainter extends CustomPainter {
final chartWidth = size.width - leftPadding - rightPadding;
final chartHeight = size.height - topPadding - bottomPadding;
//
// K线宽度
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
final bodyWidth = math.max(actualCandleWidth * 0.7, 2.0);
// K线的索引范围
final visibleStart = math.max(0, (scrollOffset / actualCandleWidth - 2).floor());
final visibleEnd = math.min(klines.length - 1, ((scrollOffset + chartWidth) / actualCandleWidth + 1).ceil());
// K线Y轴动态缩放
double minPrice = double.infinity;
double maxPrice = double.negativeInfinity;
for (final kline in klines) {
for (int i = visibleStart; i <= visibleEnd; i++) {
final kline = klines[i];
final low = double.tryParse(kline.low) ?? 0;
final high = double.tryParse(kline.high) ?? 0;
if (low < minPrice) minPrice = low;
if (high > maxPrice) maxPrice = high;
}
// MA/EMA/BOLL线的范围
// MA/EMA/BOLL线的范围
if (maData != null) {
for (final values in maData!.values) {
for (final v in values) {
for (int i = visibleStart; i <= visibleEnd && i < values.length; i++) {
final v = values[i];
if (v != null) {
if (v < minPrice) minPrice = v;
if (v > maxPrice) maxPrice = v;
@ -84,7 +94,8 @@ class KlinePainter extends CustomPainter {
if (emaData != null) {
for (final values in emaData!.values) {
for (final v in values) {
for (int i = visibleStart; i <= visibleEnd && i < values.length; i++) {
final v = values[i];
if (v != null) {
if (v < minPrice) minPrice = v;
if (v > maxPrice) maxPrice = v;
@ -95,7 +106,8 @@ class KlinePainter extends CustomPainter {
if (bollData != null) {
for (final values in bollData!.values) {
for (final v in values) {
for (int i = visibleStart; i <= visibleEnd && i < values.length; i++) {
final v = values[i];
if (v != null) {
if (v < minPrice) minPrice = v;
if (v > maxPrice) maxPrice = v;
@ -104,6 +116,12 @@ class KlinePainter extends CustomPainter {
}
}
//
if (minPrice == double.infinity || maxPrice == double.negativeInfinity) {
minPrice = 0;
maxPrice = 1;
}
//
final priceRange = maxPrice - minPrice;
// priceRange 0 NaN
@ -122,10 +140,6 @@ class KlinePainter extends CustomPainter {
//
_drawGrid(canvas, size, minPrice, maxPrice, leftPadding, rightPadding, topPadding, chartHeight);
// K线宽度 - 使
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
final bodyWidth = math.max(actualCandleWidth * 0.7, 2.0);
// K线超出边界
canvas.save();
canvas.clipRect(Rect.fromLTRB(leftPadding, 0, size.width - rightPadding, size.height));
@ -540,7 +554,22 @@ class KlinePainter extends CustomPainter {
String _formatPrice(double price) {
if (price >= 1) return price.toStringAsFixed(4);
if (price >= 0.0001) return price.toStringAsFixed(6);
return price.toStringAsExponential(2);
if (price <= 0) return '0';
// 0.00000980 0.0{5}980
final str = price.toStringAsFixed(18);
final dotIndex = str.indexOf('.');
int zeroCount = 0;
for (int i = dotIndex + 1; i < str.length; i++) {
if (str[i] == '0') {
zeroCount++;
} else {
break;
}
}
final sigStart = dotIndex + 1 + zeroCount;
final sigEnd = math.min(sigStart + 3, str.length);
final significant = str.substring(sigStart, sigEnd);
return '0.0{$zeroCount}$significant';
}
@override