/// ============================================================ /// 【QuestionPlanCountDown】单题建议作答时长倒计时 /// (1:1 迁移 Vue classExercises/QuestionPlanCountDown.vue, /// 底层由 uview up-count-down 换为 Flutter Timer.periodic) /// /// 对外方法(`GlobalKey` 调用): /// start / pause / reset / getRemainTime(毫秒) /// autoStart:initState 后自动开始;到点只触发一次 onFinish(key)。 /// ============================================================ library; import 'dart:async'; import 'package:flutter/material.dart'; class QuestionPlanCountDown extends StatefulWidget { const QuestionPlanCountDown({ super.key, this.questionKey, required this.timeMs, this.format = 'mm:ss', this.onFinish, this.textStyle, }); final Object? questionKey; /// 剩余总时长(毫秒) final int timeMs; /// 'mm:ss'(本页另有 HH:mm:ss 的动态格式,按是否含 HH 切换) final String format; final ValueChanged? onFinish; final TextStyle? textStyle; @override State createState() => QuestionPlanCountDownState(); } class QuestionPlanCountDownState extends State { Timer? _timer; late int _remainMs; bool _finished = false; @override void initState() { super.initState(); _remainMs = widget.timeMs; // autoStart start(); } @override void didUpdateWidget(covariant QuestionPlanCountDown oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.timeMs != widget.timeMs) { reset(); start(); } } @override void dispose() { _timer?.cancel(); _timer = null; super.dispose(); } void start() { if (_timer != null) return; _timer = Timer.periodic(const Duration(milliseconds: 200), (_) { if (!mounted) return; _remainMs -= 200; if (_remainMs <= 0) { _remainMs = 0; pause(); if (!_finished) { _finished = true; widget.onFinish?.call(widget.questionKey); } } setState(() {}); }); } void pause() { _timer?.cancel(); _timer = null; } /// 重置回初始时长并暂停(与 uview reset 行为一致,调用方需再 start) void reset() { pause(); _remainMs = widget.timeMs; _finished = false; if (mounted) setState(() {}); } /// 剩余毫秒 int getRemainTime() => _remainMs < 0 ? 0 : _remainMs; String _two(int v) => v < 10 ? '0$v' : '$v'; String get _display { // 展示向上取整:刚开始显示完整秒 final seconds = (_remainMs / 1000).ceil().clamp(0, 1 << 31); final h = seconds ~/ 3600; final m = (seconds % 3600) ~/ 60; final s = seconds % 60; if (widget.format.contains('HH')) { return '${_two(h)}:${_two(m)}:${_two(s)}'; } return '${_two(m)}:${_two(s)}'; } @override Widget build(BuildContext context) { // 字体的 ascent/descent 度量让文字视觉偏下,整体上移 2px 校正 return Transform.translate( offset: const Offset(0, -2), child: Text( _display, style: widget.textStyle ?? const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), ), ); } }