diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts index ff4faba3..a68e4968 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts @@ -29,10 +29,11 @@ export class PriceSnapshotRepository { } /** - * 获取最早的价格快照(上线首日价格) + * 获取最早的非零价格快照(上线首日价格) */ async getFirstSnapshot(): Promise { const record = await this.prisma.priceSnapshot.findFirst({ + where: { price: { gt: 0 } }, orderBy: { snapshotTime: 'asc' }, }); if (!record) { diff --git a/frontend/mining-app/lib/core/utils/format_utils.dart b/frontend/mining-app/lib/core/utils/format_utils.dart index f996eb7b..0b2318c2 100644 --- a/frontend/mining-app/lib/core/utils/format_utils.dart +++ b/frontend/mining-app/lib/core/utils/format_utils.dart @@ -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) { diff --git a/frontend/mining-app/lib/presentation/pages/profile/mining_records_page.dart b/frontend/mining-app/lib/presentation/pages/profile/mining_records_page.dart index b5410db7..e16d5921 100644 --- a/frontend/mining-app/lib/presentation/pages/profile/mining_records_page.dart +++ b/frontend/mining-app/lib/presentation/pages/profile/mining_records_page.dart @@ -302,7 +302,24 @@ class _MiningRecordsListPageState extends ConsumerState { 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; } diff --git a/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart b/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart index dcd6dad2..1262d6c5 100644 --- a/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart +++ b/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart @@ -511,7 +511,24 @@ class _TradingRecordsPageState extends ConsumerState 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; } 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 2272b914..6692a582 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 @@ -730,7 +730,22 @@ class _KlineChartWidgetState extends State { 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 { } 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); 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 cb2585bb..2e91375a 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 @@ -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