/// ============================================================ /// 【课堂习题 · 计时】(1:1 迁移 Vue timing.js / exerciseTiming.js) /// /// - formatElapsed / formatTime / formatDuration 与 Vue 纯函数一致; /// - ExerciseTiming 对应 createTiming(每秒一次 onTick); /// - getDisplayPlanTimeSecond 对应 exerciseTiming.js 的同名导出: /// 优先用本地缓存的剩余秒数 NewPlanTimeSecond,否则用 /// planTimeSecond - answerTimeSecond;<=0 时置 ISFinishPlanTime=true。 /// ============================================================ library; import 'dart:async'; import 'package:new_learn/features/conquerPackage/classExercises/models/question_model.dart'; String _pad2(int n) => n < 10 ? '0$n' : '$n'; /// mm:ss(不足 1 小时)/ hh:mm:ss(达到 1 小时) String formatElapsed(int ms) { final safeMs = ms < 0 ? 0 : ms; final totalSeconds = safeMs ~/ 1000; if (totalSeconds < 3600) { final mm = totalSeconds ~/ 60; final ss = totalSeconds % 60; return '${_pad2(mm)}:${_pad2(ss)}'; } final hh = totalSeconds ~/ 3600; final mm = (totalSeconds % 3600) ~/ 60; final ss = totalSeconds % 60; return '${_pad2(hh)}:${_pad2(mm)}:${_pad2(ss)}'; } /// 建议学习时长文案:N秒 / N分钟 / N小时 / Nd String formatTime(int seconds) { final t = seconds < 0 ? 0 : seconds; if (t < 60) return '$t秒'; if (t < 3600) return '${t ~/ 60}分钟'; if (t < 86400) return '${t ~/ 3600}小时'; return '${t ~/ 86400}d'; } /// 已学习时长文案:N秒 / N分钟N秒 / N小时N分钟 / Nd String formatDuration(int seconds) { final t = seconds < 0 ? 0 : seconds; if (t < 60) return '$t秒'; if (t < 3600) { final m = t ~/ 60; final s = t % 60; return '$m分钟$s秒'; } if (t < 86400) { final h = t ~/ 3600; final m = (t % 3600) ~/ 60; return '$h小时$m分钟'; } return '${t ~/ 86400}d'; } /// 计时回调载荷(对标 JS {elapsedMs,text,running}) class TimingTick { const TimingTick({ required this.elapsedMs, required this.text, required this.running, }); final int elapsedMs; final String text; final bool running; } /// 页面计时器(对应 createTiming)。 /// /// 注意与 JS 保持一致的几个细节: /// - pause() 不取消周期定时器,仅冻结累计值(running=false), /// 周期回调继续按冻结值触发; /// - start() 已在计时时直接返回; /// - stop() 才真正取消定时器; /// - reset() 不清运行状态,只把累计值归零。 class ExerciseTiming { ExerciseTiming({this.onTick, this.intervalMs = 1000}); final void Function(TimingTick tick)? onTick; final int intervalMs; Timer? _timer; bool _running = false; int _lastStartAt = 0; int _elapsedMs = 0; void _emit() { final cb = onTick; if (cb == null) return; final now = DateTime.now().millisecondsSinceEpoch; final current = _running ? _elapsedMs + (now - _lastStartAt) : _elapsedMs; cb(TimingTick( elapsedMs: current, text: formatElapsed(current), running: _running, )); } void start() { if (_timer != null) return; _running = true; _lastStartAt = DateTime.now().millisecondsSinceEpoch; _emit(); _timer = Timer.periodic(Duration(milliseconds: intervalMs), (_) => _emit()); } void pause() { if (!_running) return; _elapsedMs += DateTime.now().millisecondsSinceEpoch - _lastStartAt; _running = false; _emit(); } void resume() { if (_running) return; _running = true; _lastStartAt = DateTime.now().millisecondsSinceEpoch; _emit(); } void toggle() { if (_running) { pause(); } else { resume(); } } void stop() { _timer?.cancel(); _timer = null; _running = false; } void reset() { _elapsedMs = 0; _lastStartAt = DateTime.now().millisecondsSinceEpoch; _emit(); } int getElapsedMs() { final now = DateTime.now().millisecondsSinceEpoch; return _running ? _elapsedMs + (now - _lastStartAt) : _elapsedMs; } bool isRunning() => _running; } /// 单题倒计时展示秒数(对应 getDisplayPlanTimeSecond)。 /// /// NewPlanTimeSecond 是切题/暂停前本地缓存的“剩余秒数”,不是总时长; /// 首次进入题目没有缓存时,用总时长 - 后端累计作答时长。 /// 结果 <=0 时直接视为倒计时结束并回写 ISFinishPlanTime。 int getDisplayPlanTimeSecond(QuestionModel? q) { if (q == null) return 0; final hasLocalRemain = q.m['NewPlanTimeSecond'] != null; final remain = hasLocalRemain ? (q.newPlanTimeSecond ?? 0) : q.planTimeSecond - q.answerTimeSecond; if (remain <= 0) { q.isFinishPlanTime = true; return 0; } return remain; }