sleep_trend_view.dart 8.94 KB
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

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

/// 睡眠报告趋势内容区(含第2、3层 Tab)
class SleepTrendView extends GetView<SleepController> {
  const SleepTrendView({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TrendPeriodTabBar(controller: controller),
        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 _SleepSummaryCard(),
                  const SizedBox(height: 12),
                  const _SleepChartCard(),
                  const SizedBox(height: 180),
                ],
              ),
            );
          }),
        ),
      ],
    );
  }
}

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

  static const _h1 = Color(0xFF0F0F11);
  static const _h3 = Color(0xFF999999);
  static const _sleepColor = Color(0xFF7B9BFB);

  @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 = controller.currentPeriod.value.label;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Text(
                  '睡眠报告趋势',
                  style: 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: '本$periodLabel均睡眠',
                  value: controller.averageSleep.value,
                  unit: 'h',
                  color: _sleepColor,
                ),
                const SizedBox(width: 24),
                _StatItem(
                  label: '深睡占比',
                  value: controller.deepSleepRatio.value,
                  unit: '%',
                  color: const Color(0xFF845EEE),
                ),
              ],
            ),
          ],
        );
      }),
    );
  }
}

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)),
            ),
          ],
        ),
      ],
    );
  }
}

// ─── 睡眠时长折线图卡片 ────────────────────────────────
class _SleepChartCard extends GetView<SleepController> {
  const _SleepChartCard();

  static const _h1 = Color(0xFF0F0F11);
  static const _h3 = Color(0xFF999999);
  static const _h5 = Color(0xFFCCCCCC);
  static const _sleepColor = Color(0xFF7B9BFB);

  @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;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '睡眠时长${period.label}趋势图',
              style: const TextStyle(
                color: _h1,
                fontSize: 14,
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 16),
            SizedBox(
              height: 180,
              child: _buildChart(),
            ),
          ],
        );
      }),
    );
  }

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

    return LineChart(
      LineChartData(
        minX: 0,
        maxX: (controller.chartData.length - 1).toDouble(),
        minY: 4,
        maxY: 10,
        gridData: FlGridData(
          show: true,
          drawVerticalLine: false,
          horizontalInterval: 2,
          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: 2,
              getTitlesWidget: (val, _) => Text(
                '${val.toInt()}h',
                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: List.generate(
              controller.chartData.length,
              (i) => FlSpot(i.toDouble(), controller.chartData[i]),
            ),
            isCurved: true,
            curveSmoothness: 0.3,
            color: _sleepColor,
            barWidth: 2,
            belowBarData: BarAreaData(
              show: true,
              gradient: LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [
                  _sleepColor.withValues(alpha: 0.15),
                  _sleepColor.withValues(alpha: 0.0),
                ],
              ),
            ),
            dotData: FlDotData(
              getDotPainter: (spot, percent, barData, index) =>
                  FlDotCirclePainter(
                radius: 4,
                color: Colors.white,
                strokeWidth: 2.5,
                strokeColor: _sleepColor,
              ),
            ),
          ),
        ],
        lineTouchData: LineTouchData(
          touchTooltipData: LineTouchTooltipData(
            tooltipRoundedRadius: 8,
            getTooltipColor: (_) => const Color(0xFFF3F3F3),
            getTooltipItems: (spots) => spots.map((s) {
              return LineTooltipItem(
                '${s.y}h',
                const TextStyle(
                  color: _sleepColor,
                  fontWeight: FontWeight.w600,
                  fontSize: 12,
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}