hrv_trend_view.dart 9.89 KB
import 'package:fl_chart/fl_chart.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../../controllers/trend/trend_types.dart';
import '../../controllers/trend/hrv_controller.dart';
import 'trend_period_tab_bar.dart';
import 'trend_date_range_bar.dart';

/// HRV 趋势内容区(含第2、3层 Tab)
class HrvTrendView extends GetView<HrvController> {
  const HrvTrendView({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 第2层:周/月/年
        TrendPeriodTabBar(controller: controller),
        // 第3层:日期范围
        TrendDateRangeBar(controller: controller),
        // 图表内容
        Expanded(
          child: Obx(() {
            if (controller.isLoading.value) {
              return const Center(
                child: CircularProgressIndicator(
                  valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)),
                ),
              );
            }
            return SingleChildScrollView(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Column(
                children: [
                  const SizedBox(height: 8),
                  const _HrvSummaryCard(),
                  const SizedBox(height: 12),
                  const _HrvChartCard(),
                  const SizedBox(height: 180),
                ],
              ),
            );
          }),
        ),
      ],
    );
  }
}

// ─── 摘要卡片 ────────────────────────────────────────────
class _HrvSummaryCard extends GetView<HrvController> {
  const _HrvSummaryCard();

  static const _h1 = Color(0xFF0F0F11);
  static const _h3 = Color(0xFF999999);
  static const _brandColor = Color(0xFF845EEE);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Obx(() {
        final periodLabel =
            _periodLabel(context, controller.currentPeriod.value);
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Text(
                  context.l10n.hrvTrendTitle,
                  style: const TextStyle(
                    color: _h1,
                    fontSize: 16,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(width: 4),
                const Icon(Icons.info_outline, size: 14, color: _h3),
              ],
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                _StatItem(
                  label: context.l10n.hrvPeriodAverage(periodLabel),
                  value: controller.averageHrv.value,
                  unit: 'ms',
                  color: _brandColor,
                ),
                const SizedBox(width: 24),
                _StatItem(
                  label: context.l10n.hrvComparedPreviousPeriod(periodLabel),
                  value: controller.changeHrv.value,
                  unit: 'ms',
                  color: const Color(0xFF3BD49D),
                ),
              ],
            ),
          ],
        );
      }),
    );
  }

  String _periodLabel(BuildContext context, TrendPeriod period) =>
      switch (period) {
        TrendPeriod.week => context.l10n.reportPeriodWeek,
        TrendPeriod.month => context.l10n.reportPeriodMonth,
        TrendPeriod.year => context.l10n.reportPeriodYear,
      };
}

class _StatItem extends StatelessWidget {
  final String label;
  final String value;
  final String unit;
  final Color color;

  const _StatItem({
    required this.label,
    required this.value,
    required this.unit,
    required this.color,
  });

  static const _h2 = Color(0xFF78787D);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: const TextStyle(color: _h2, fontSize: 12),
        ),
        const SizedBox(height: 4),
        Row(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Text(
              value,
              style: TextStyle(
                color: color,
                fontSize: 32,
                fontWeight: FontWeight.w600,
                height: 1.0,
              ),
            ),
            const SizedBox(width: 2),
            Padding(
              padding: const EdgeInsets.only(bottom: 4),
              child: Text(
                unit,
                style: TextStyle(color: color, fontSize: 14),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

// ─── HRV 折线图卡片 ──────────────────────────────────────
class _HrvChartCard extends GetView<HrvController> {
  const _HrvChartCard();

  static const _h1 = Color(0xFF0F0F11);
  static const _h3 = Color(0xFF999999);
  static const _h5 = Color(0xFFCCCCCC);
  static const _brandColor = Color(0xFF845EEE);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Obx(() {
        final period = controller.currentPeriod.value;
        final periodLabel = _periodLabel(context, period);
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              context.l10n.hrvPeriodTrendChart(periodLabel),
              style: const TextStyle(
                color: _h1,
                fontSize: 14,
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 16),
            SizedBox(
              height: 180,
              child: _buildChart(period),
            ),
          ],
        );
      }),
    );
  }

  String _periodLabel(BuildContext context, TrendPeriod period) =>
      switch (period) {
        TrendPeriod.week => context.l10n.reportPeriodWeek,
        TrendPeriod.month => context.l10n.reportPeriodMonth,
        TrendPeriod.year => context.l10n.reportPeriodYear,
      };

  Widget _buildChart(TrendPeriod period) {
    if (controller.chartData.isEmpty) {
      return const Center(
        child: CircularProgressIndicator(
          valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)),
        ),
      );
    }

    final spots = List.generate(
      controller.chartData.length,
      (i) => FlSpot(i.toDouble(), controller.chartData[i]),
    );

    return LineChart(
      LineChartData(
        minX: 0,
        maxX: (controller.chartData.length - 1).toDouble(),
        minY: 20,
        maxY: 80,
        gridData: FlGridData(
          show: true,
          drawVerticalLine: false,
          horizontalInterval: 20,
          getDrawingHorizontalLine: (_) => const FlLine(
            color: _h5,
            strokeWidth: 1,
            dashArray: [2, 2],
          ),
        ),
        borderData: FlBorderData(show: false),
        titlesData: FlTitlesData(
          topTitles:
              const AxisTitles(sideTitles: SideTitles(showTitles: false)),
          rightTitles:
              const AxisTitles(sideTitles: SideTitles(showTitles: false)),
          leftTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              reservedSize: 30,
              interval: 20,
              getTitlesWidget: (val, _) => Text(
                '${val.toInt()}',
                style: const TextStyle(fontSize: 10, color: _h3),
              ),
            ),
          ),
          bottomTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              reservedSize: 20,
              getTitlesWidget: (val, _) {
                final i = val.toInt();
                if (i < 0 || i >= controller.chartLabels.length) {
                  return const SizedBox.shrink();
                }
                return Text(
                  controller.chartLabels[i],
                  style: const TextStyle(fontSize: 10, color: _h3),
                );
              },
            ),
          ),
        ),
        lineBarsData: [
          LineChartBarData(
            spots: spots,
            isCurved: true,
            curveSmoothness: 0.3,
            color: _brandColor,
            barWidth: 2,
            belowBarData: BarAreaData(
              show: true,
              gradient: LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [
                  _brandColor.withValues(alpha: 0.15),
                  _brandColor.withValues(alpha: 0.0),
                ],
              ),
            ),
            dotData: FlDotData(
              getDotPainter: (spot, percent, barData, index) =>
                  FlDotCirclePainter(
                radius: 4,
                color: Colors.white,
                strokeWidth: 2.5,
                strokeColor: _brandColor,
              ),
            ),
          ),
        ],
        lineTouchData: LineTouchData(
          touchTooltipData: LineTouchTooltipData(
            tooltipRoundedRadius: 8,
            getTooltipColor: (_) => const Color(0xFFF3F3F3),
            getTooltipItems: (spots) => spots.map((s) {
              return LineTooltipItem(
                '${s.y.toInt()}ms',
                const TextStyle(
                  color: _brandColor,
                  fontWeight: FontWeight.w600,
                  fontSize: 12,
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}