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