Files
Ichino/lib/widgets/cooldown_mixin.dart
T
Bad0RANG3 45214b11a0 refactor: 抽取高危功能页共享脚手架与传输层日志
诊断:功能票 / 解锁歌曲 / 收藏品 / 旅行伙伴 / 数值修改 / 一键跑图 /
万花筒 / 高危 hub 这些页面各自抄了一份同样的脚手架——冷却计时器、_Step
枚举、自动退登开关、卡片/提示/进度卡;TitleApiService.getUserPreview 还
重复实现了整套传输层;服务层有 37 处裸 print 会把 token / JSESSIONID
打进日志。

- 新增共享原语,7 个功能页切换到:
  * widgets/cooldown_mixin.dart  CooldownMixin(登录后冷却倒计时)
  * widgets/step_progress.dart   RiskStep + StepProgressCard
  * widgets/app_card.dart        AppCard / SectionTitle / AutoLogoutToggle
  * widgets/app_notice.dart      AppNotice + context.showSnack
- TitleApiService: getUserPreview 收敛进通用 _callApi;新增 fromHolder()
  工厂与 config getter,消除散落的 isConfigured + config!
- 传输层 print -> ApiLog(仅 debug 输出),删除全部 avoid_print 忽略
- UserAllPayloadBuilder 新增 placeholderMusicData()(原 5 处重复)
- LoginPage 从 main.dart 抽到 pages/login_page.dart,入口只留 main + MyApp
- 更新 README / CLAUDE.md 目录结构与已过时的 web/依赖说明

验证:flutter analyze 0 issue(含 info);flutter test 96/96 通过。
纯结构重构,行为保持不变;仅有意统一了功能票 / 音乐风险 / hub 的
卡片与步骤视觉样式。

净删除约 2176 行重复代码。
2026-09-28 14:50:51 +08:00

93 lines
2.4 KiB
Dart

import 'dart:async';
import 'package:flutter/widgets.dart';
import '../config/strings.dart';
/// 功能页共用的「登录后冷却」计时。
///
/// 六个功能页(功能票 / 数值修改 / 旅行伙伴 / 一键跑图 / 万花筒 / 解锁)原本
/// 各自抄了一份 `Timer.periodic` + `_computeRemaining`,这里收口成一个 mixin。
///
/// 用法:
/// ```dart
/// class _FooState extends State<Foo> with CooldownMixin<Foo> {
/// @override
/// int? get cooldownLoginDateTime => widget.loginDateTime;
/// }
/// ```
/// 页面**不要**再自己覆写 `initState` / `didUpdateWidget` / `dispose` 里与冷却
/// 相关的逻辑;如需覆写,记得调用 `super`。
mixin CooldownMixin<T extends StatefulWidget> on State<T> {
Timer? _cooldownTimer;
int _cooldownRemaining = 0;
int? _appliedLoginDateTime;
/// 当前会话的 `UserLoginApi` 时间戳;`null` 表示没有有效登录态。
int? get cooldownLoginDateTime;
bool get isLoggedIn => cooldownLoginDateTime != null;
int get cooldownRemaining => _cooldownRemaining;
@override
void initState() {
super.initState();
_applyCooldown();
}
@override
void didUpdateWidget(covariant T oldWidget) {
super.didUpdateWidget(oldWidget);
if (cooldownLoginDateTime != _appliedLoginDateTime) {
_applyCooldown();
}
}
@override
void dispose() {
_cooldownTimer?.cancel();
_cooldownTimer = null;
super.dispose();
}
void _applyCooldown() {
_cooldownTimer?.cancel();
_cooldownTimer = null;
_appliedLoginDateTime = cooldownLoginDateTime;
final loginDateTime = _appliedLoginDateTime;
if (loginDateTime == null) {
_setRemaining(0);
return;
}
_setRemaining(_computeRemaining(loginDateTime));
if (_cooldownRemaining <= 0) return;
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) return;
_setRemaining(_computeRemaining(loginDateTime));
if (_cooldownRemaining <= 0) {
_cooldownTimer?.cancel();
_cooldownTimer = null;
}
});
}
void _setRemaining(int value) {
if (!mounted) {
_cooldownRemaining = value;
return;
}
setState(() => _cooldownRemaining = value);
}
int _computeRemaining(int loginDateTime) {
final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final remaining =
AppStrings.ticketCooldownSeconds - (nowSec - loginDateTime);
return remaining < 0 ? 0 : remaining;
}
}