Files
2026-09-28 20:12:34 +08:00

133 lines
3.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 【QuestionPlanCountDown】单题建议作答时长倒计时
/// (1:1 迁移 Vue classExercises/QuestionPlanCountDown.vue,
/// 底层由 uview up-count-down 换为 Flutter Timer.periodic)
///
/// 对外方法(`GlobalKey<QuestionPlanCountDownState>` 调用):
/// 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<Object?>? onFinish;
final TextStyle? textStyle;
@override
State<QuestionPlanCountDown> createState() => QuestionPlanCountDownState();
}
class QuestionPlanCountDownState extends State<QuestionPlanCountDown> {
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,
),
),
);
}
}