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,171 @@
/// ============================================================
/// 【课堂习题 · 计时】(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;
}