stress_progress_bar.dart 5.84 KB
import 'package:flutter/material.dart';

/// 压力状态进度条(四段式)
///
/// 对应 Figma node 1361-44236(轨道)和 1361-44489(indicator)。
///
/// 传入 [score](0~100 压力分):
/// - null   → 加载态:四段等宽 20px,无 indicator
/// - 0~20   → 优秀·绿段(status 3)
/// - 21~60  → 正常·蓝段(status 2)
/// - 61~80  → 注意压力·橙段(status 1)
/// - 81~100 → 压力过载·红段(status 0)
///
/// indicator 在命中段内的横向位置由分数在该区间的位置决定:
///   分数越低(越优秀)→ 指针越靠右;分数越高(越差)→ 越靠左。
///
/// 段间距 4px,轨道高 14px,indicator 24×24px(6px stroke)。
class StressProgressBar extends StatelessWidget {
  const StressProgressBar({super.key, required this.score})
      : assert(score == null || (score >= 0 && score <= 100));

  /// null = 加载中;0~100 = 压力分
  final int? score;

  // ── Figma 图表色/1-4(index 0~3 对应 过载→活力)────────
  static const _colors = [
    Color(0xFFFF5279), // 0 压力过载·红
    Color(0xFFFF9A6E), // 1 注意压力·橙
    Color(0xFF7B9BFB), // 2 状态正常·蓝
    Color(0xFF3BD49D), // 3 活力满满·绿
  ];

  // ── 尺寸常量(来自 Figma)────────────────────────────
  static const _defaultW = 20.0;
  static const _activeW = 148.0;
  static const _trackH = 14.0;
  static const _gap = 4.0;
  static const _dotSize = 24.0;
  static const _dotBorder = 6.0;

  // ── 动画参数 ──────────────────────────────────────────
  static const _dur = Duration(milliseconds: 400);
  static const _curve = Curves.easeInOut;

  // ── 分数区间边界(左闭右闭)───────────────────────────
  // status 3 (绿): 0~20   status 2 (蓝): 21~60
  // status 1 (橙): 61~80  status 0 (红): 81~100
  static const _ranges = [
    (lo: 81, hi: 100), // status 0
    (lo: 61, hi: 80), // status 1
    (lo: 21, hi: 60), // status 2
    (lo: 0, hi: 20), // status 3
  ];

  /// 由压力分计算命中段(0=过载, 1=注意, 2=正常, 3=活力)
  static int _statusFromScore(int s) {
    if (s <= 20) return 3;
    if (s <= 60) return 2;
    if (s <= 80) return 1;
    return 0;
  }

  /// 在命中段内的归一化位置(0.0=最左/最差, 1.0=最右/最优)
  ///
  /// 分数越低越优秀 → 越靠右,所以 progress = (hi - score) / (hi - lo)
  static double _segProgress(int s) {
    final st = _statusFromScore(s);
    final r = _ranges[st];
    if (r.hi == r.lo) return 0.5;
    return ((r.hi - s) / (r.hi - r.lo)).clamp(0.0, 1.0);
  }

  // ── 布局计算 ──────────────────────────────────────────

  int? get _status => score != null ? _statusFromScore(score!) : null;

  /// 第 [i] 段的当前宽度
  double _w(int i) => (_status == i) ? _activeW : _defaultW;

  /// 总宽度(含三个间距)
  double get _totalW => _w(0) + _w(1) + _w(2) + _w(3) + 3 * _gap;

  /// indicator 左边缘 X(在命中段内按 segProgress 定位)
  double _dotLeft(int s) {
    final prog = score != null ? _segProgress(score!) : 0.5;
    double x = 0;
    for (int i = 0; i < s; i++) {
      x += _w(i) + _gap;
    }
    // 在命中段宽内按 progress 定位,并保证圆点不超出段的边缘
    x += (_w(s) * prog).clamp(_dotSize / 2, _w(s) - _dotSize / 2);
    return x - _dotSize / 2;
  }

  int _indexOfStatus(int score) =>
      _ranges.indexWhere((r) => score >= r.lo && score <= r.hi);

  @override
  Widget build(BuildContext context) {
    final s = _status;
    final scoreIndex = _indexOfStatus(score ?? -1);

    return AnimatedContainer(
      duration: _dur,
      curve: _curve,
      width: _totalW,
      height: _dotSize,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          // ── 轨道:四段色块 ─────────────────────────────
          Positioned(
            top: (_dotSize - _trackH) / 2,
            left: 0,
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                for (int i = 0; i < 4; i++) ...[
                  Opacity(
                    opacity: scoreIndex == i ? 1.0 : 0.5,
                    child:
                  AnimatedContainer(
                    duration: _dur,
                    curve: _curve,
                    width: _w(i),
                    height: _trackH,
                    decoration: BoxDecoration(
                      color: _colors[i],
                      borderRadius: BorderRadius.circular(_trackH / 2),
                    ),
                    ),
                  ),
                  if (i < 3) const SizedBox(width: _gap),
                ],
              ],
            ),
          ),

          // ── Indicator:白底 + 状态色描边 ────────────────
          AnimatedPositioned(
            duration: _dur,
            curve: _curve,
            top: 0,
            left: s != null ? _dotLeft(s) : 0,
            child: AnimatedOpacity(
              opacity: s != null ? 1.0 : 0.0,
              duration: _dur,
              curve: _curve,
              child: Container(
                width: _dotSize,
                height: _dotSize,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: Colors.white,
                  border: Border.all(
                    color: _colors[(s ?? 0).clamp(0, 3)],
                    width: _dotBorder,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}