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 行重复代码。
This commit is contained in:
Bad0RANG3 committed 2026-09-28 14:50:51 +08:00
1 parent 0808e367f7
commit 45214b11a0
21 files changed
+1196 -2176

No files matched your search

+49 -251
View File
@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
@@ -8,6 +7,10 @@ import '../config/title_server_config.dart';
import '../models/user_kaleidx_scope.dart';
import '../services/title_api_service.dart';
import '../services/user_all_payload_builder.dart';
import '../widgets/app_card.dart';
import '../widgets/app_notice.dart';
import '../widgets/cooldown_mixin.dart';
import '../widgets/step_progress.dart';
/// 万花筒专区:发现新的宿命之门 + 获取门的钥匙。
///
@@ -34,12 +37,8 @@ class KaleidxScopePage extends StatefulWidget {
State<KaleidxScopePage> createState() => _KaleidxScopePageState();
}
enum _Step { idle, fetchData, upload, logout, complete, failed }
class _KaleidxScopePageState extends State<KaleidxScopePage> {
/// 与其它高危功能一致:不写真实成绩,playlog 用同一条占位记录。
static const int _placeholderMusicId = 11538;
class _KaleidxScopePageState extends State<KaleidxScopePage>
with CooldownMixin<KaleidxScopePage> {
final _gateIdController = TextEditingController();
final List<int> _pending = [];
@@ -52,65 +51,19 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
bool _loading = false;
bool _running = false;
bool _autoLogout = true;
_Step _step = _Step.idle;
RiskStep _step = RiskStep.idle;
String _stepMessage = '';
Timer? _cooldownTimer;
int _cooldownRemaining = 0;
bool get _loggedIn => widget.loginDateTime != null;
@override
void initState() {
super.initState();
_syncCooldown();
}
@override
void didUpdateWidget(covariant KaleidxScopePage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.loginDateTime != widget.loginDateTime) {
_syncCooldown();
}
}
int? get cooldownLoginDateTime => widget.loginDateTime;
@override
void dispose() {
_cooldownTimer?.cancel();
_gateIdController.dispose();
super.dispose();
}
void _syncCooldown() {
_cooldownTimer?.cancel();
final loginDateTime = widget.loginDateTime;
if (loginDateTime == null) {
setState(() => _cooldownRemaining = 0);
return;
}
setState(() => _cooldownRemaining = _computeRemaining(loginDateTime));
if (_cooldownRemaining <= 0) return;
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) return;
final remaining = _computeRemaining(loginDateTime);
setState(() => _cooldownRemaining = remaining);
if (remaining <= 0) {
_cooldownTimer?.cancel();
_cooldownTimer = null;
}
});
}
int _computeRemaining(int loginDateTime) {
final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final remaining =
AppStrings.ticketCooldownSeconds - (nowSec - loginDateTime);
return remaining < 0 ? 0 : remaining;
}
void _snack(String message) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
void _snack(String message) => context.showSnack(message);
void _addPending() {
final id = int.tryParse(_gateIdController.text.trim()) ?? -1;
@@ -149,8 +102,7 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
setState(() => _loading = true);
try {
final config = TitleServerConfigHolder().config!;
final service = TitleApiService(config, cookies: widget.cookies);
final service = TitleApiService.fromHolder(cookies: widget.cookies)!;
final results = await Future.wait([
service.fetchUserAllData(widget.userId),
service.getUserKaleidxScopes(widget.userId),
@@ -179,8 +131,8 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
_snack(AppStrings.kaleidxNotLoggedIn);
return;
}
if (_cooldownRemaining > 0) {
_snack(AppStrings.kaleidxCooldownNotice(_cooldownRemaining));
if (cooldownRemaining > 0) {
_snack(AppStrings.kaleidxCooldownNotice(cooldownRemaining));
return;
}
if (_pending.isEmpty) {
@@ -195,13 +147,12 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
setState(() => _running = true);
try {
final config = TitleServerConfigHolder().config!;
final service = TitleApiService(config, cookies: widget.cookies);
final builder = UserAllPayloadBuilder(config);
final service = TitleApiService.fromHolder(cookies: widget.cookies)!;
final builder = UserAllPayloadBuilder(service.config);
var data = _userAllData;
if (data.isEmpty || _serverScopes.isEmpty) {
_updateStep(_Step.fetchData);
_updateStep(RiskStep.fetchData);
final results = await Future.wait([
service.fetchUserAllData(widget.userId),
service.getUserKaleidxScopes(widget.userId),
@@ -215,12 +166,12 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
}
}
_updateStep(_Step.upload);
_updateStep(RiskStep.upload);
final packet = builder.build(
userId: widget.userId,
loginId: loginId,
loginDateTime: loginDateTime,
musicData: _placeholderMusicData(),
musicData: UserAllPayloadBuilder.placeholderMusicData(),
generalUserInfo: data,
);
@@ -236,18 +187,18 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
);
await service.upsertUserAll(packet, widget.userId);
_updateStep(_Step.complete, await _verify(service));
_updateStep(RiskStep.complete, await _verify(service));
if (_autoLogout && widget.onExitToTitle != null) {
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
_updateStep(_Step.logout, AppStrings.exitingToTitle);
_updateStep(RiskStep.logout, AppStrings.exitingToTitle);
await widget.onExitToTitle!();
}
} on TitleApiException catch (e) {
_updateStep(_Step.failed, e.message);
_updateStep(RiskStep.failed, e.message);
} catch (e) {
_updateStep(_Step.failed, '$e');
_updateStep(RiskStep.failed, '$e');
} finally {
if (mounted) setState(() => _running = false);
}
@@ -273,19 +224,7 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
return AppStrings.kaleidxVerified(saved.length);
}
Map<String, dynamic> _placeholderMusicData() => {
'musicId': _placeholderMusicId,
'level': 0,
'playCount': 1,
'achievement': 0,
'comboStatus': 0,
'syncStatus': 0,
'deluxscoreMax': 0,
'scoreRank': 0,
'extNum1': 0,
};
void _updateStep(_Step step, [String? message]) {
void _updateStep(RiskStep step, [String? message]) {
if (!mounted) return;
setState(() {
_step = step;
@@ -306,22 +245,19 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!_loggedIn)
_notice(theme, AppStrings.kaleidxNotLoggedIn, error: true),
if (_loggedIn && _cooldownRemaining > 0)
_notice(
theme,
AppStrings.kaleidxCooldownNotice(_cooldownRemaining),
if (!isLoggedIn)
const AppNotice(AppStrings.kaleidxNotLoggedIn, error: true),
if (isLoggedIn && cooldownRemaining > 0)
AppNotice(
AppStrings.kaleidxCooldownNotice(cooldownRemaining),
),
_card(
theme,
AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_titleRow(
theme,
Icons.science_outlined,
AppStrings.kaleidxFeatureTitle,
const SectionTitle(
icon: Icons.science_outlined,
title: AppStrings.kaleidxFeatureTitle,
),
const SizedBox(height: 12),
Text(
@@ -339,12 +275,24 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
const SizedBox(height: 12),
_fetchCard(theme),
const SizedBox(height: 12),
_autoLogoutToggle(theme),
AutoLogoutToggle(
value: _autoLogout,
enabled: widget.onExitToTitle != null && !_running,
onChanged: (v) => setState(() => _autoLogout = v),
),
const SizedBox(height: 12),
_runButton(theme),
if (_step != _Step.idle) ...[
if (_step != RiskStep.idle) ...[
const SizedBox(height: 16),
_progressCard(theme),
StepProgressCard(
step: _step,
message: _stepMessage,
steps: [
(RiskStep.fetchData, AppStrings.kaleidxStepFetch),
(RiskStep.upload, AppStrings.kaleidxStepUpload),
if (_autoLogout) (RiskStep.logout, AppStrings.stepLogout),
],
),
],
],
),
@@ -353,59 +301,6 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
);
}
Widget _notice(ThemeData theme, String text, {bool error = false}) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: error
? theme.colorScheme.errorContainer
: theme.colorScheme.tertiaryContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(10),
),
child: Text(
text,
style: theme.textTheme.bodySmall?.copyWith(
color: error
? theme.colorScheme.onErrorContainer
: theme.colorScheme.onTertiaryContainer,
),
),
);
}
Widget _card(ThemeData theme, {required Widget child}) {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Padding(padding: const EdgeInsets.all(20), child: child),
);
}
Widget _titleRow(ThemeData theme, IconData icon, String title) {
return Row(
children: [
Icon(icon, size: 18, color: theme.colorScheme.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _checkRow({
required String label,
required bool value,
@@ -429,8 +324,7 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
Widget _inputCard(ThemeData theme) {
final enabled = !_running;
return _card(
theme,
return AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -521,8 +415,7 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
Widget _fetchCard(ThemeData theme) {
final hasData = _userAllData.isNotEmpty;
return _card(
theme,
return AppCard(
child: Row(
children: [
Icon(
@@ -573,38 +466,11 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
);
}
Widget _autoLogoutToggle(ThemeData theme) {
final enabled = widget.onExitToTitle != null && !_running;
return _card(
theme,
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: enabled ? () => setState(() => _autoLogout = !_autoLogout) : null,
child: Row(
children: [
Checkbox(
value: _autoLogout,
onChanged: enabled
? (v) => setState(() => _autoLogout = v ?? false)
: null,
),
Expanded(
child: Text(
AppStrings.autoLogoutAndExit,
style: theme.textTheme.bodyMedium,
),
),
],
),
),
);
}
Widget _runButton(ThemeData theme) {
return SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _running || !_loggedIn ? null : _run,
onPressed: _running || !isLoggedIn ? null : _run,
icon: _running
? const SizedBox(
width: 20,
@@ -624,72 +490,4 @@ class _KaleidxScopePageState extends State<KaleidxScopePage> {
),
);
}
Widget _progressCard(ThemeData theme) {
final isFailed = _step == _Step.failed;
return _card(
theme,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_stepRow(theme, _Step.fetchData, AppStrings.kaleidxStepFetch),
_stepRow(theme, _Step.upload, AppStrings.kaleidxStepUpload),
if (_autoLogout) _stepRow(theme, _Step.logout, AppStrings.stepLogout),
if (_stepMessage.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isFailed
? theme.colorScheme.errorContainer
: theme.colorScheme.surfaceContainerHighest
.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_stepMessage,
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
color: isFailed
? theme.colorScheme.onErrorContainer
: theme.colorScheme.onSurface,
),
),
),
],
],
),
);
}
Widget _stepRow(ThemeData theme, _Step step, String label) {
final order = [_Step.fetchData, _Step.upload, _Step.logout, _Step.complete];
final current = _step == _Step.failed ? _Step.upload : _step;
final done = order.indexOf(current) > order.indexOf(step);
final active = _step == step;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(
done
? Icons.check_circle
: active
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
size: 18,
color: done
? Colors.green
: active
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 10),
Expanded(child: Text(label, style: theme.textTheme.bodySmall)),
],
),
);
}
}