This commit is contained in:
chenshuainan
2026-09-28 20:12:34 +08:00
parent 847986f861
commit 5f53bd8f94
60 changed files with 14624 additions and 398 deletions
@@ -0,0 +1,132 @@
/// ============================================================
/// 【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,
),
),
);
}
}