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

172 lines
4.7 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.
/// ============================================================
/// 【课堂习题 · 计时】(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;
}