Files
Ichino/lib/widgets/step_progress.dart
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

134 lines
3.9 KiB
Dart

import 'package:flutter/material.dart';
import 'app_card.dart';
/// 高危操作共用的执行阶段。
///
/// 六个功能页原来各自定义 `_Step` / `TicketStep` / `MusicRiskStep`,值完全一样
/// (功能票用 `chargeTicket` 表示「发包」,语义上就是 [upload])。
enum RiskStep { idle, fetchData, upload, logout, complete, failed }
/// 执行进度卡:按顺序展示各阶段,前/当前/后三种状态。
///
/// 说明文字 [message] 用错误色高亮;[error] 用于额外展示底层异常文本。
class StepProgressCard extends StatelessWidget {
final RiskStep step;
final String message;
final String? error;
/// 要展示的 (阶段, 文案) 列表,按执行顺序给出。不含 [RiskStep.idle] / [RiskStep.failed]。
final List<(RiskStep, String)> steps;
const StepProgressCard({
super.key,
required this.step,
required this.steps,
this.message = '',
this.error,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isFailed = step == RiskStep.failed;
final isDone = step == RiskStep.complete;
return AppCard(
borderColor: isFailed
? theme.colorScheme.error.withValues(alpha: 0.5)
: isDone
? Colors.green.withValues(alpha: 0.5)
: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final (stage, label) in steps) _row(theme, stage, label),
if (message.isNotEmpty) ...[
const SizedBox(height: 12),
_banner(
theme,
message,
background: isFailed
? theme.colorScheme.errorContainer
: isDone
? Colors.green.withValues(alpha: 0.1)
: theme.colorScheme.surfaceContainerHighest,
foreground: isFailed
? theme.colorScheme.onErrorContainer
: theme.colorScheme.onSurface,
),
],
if (error != null) ...[
const SizedBox(height: 12),
_banner(
theme,
error!,
background: theme.colorScheme.errorContainer,
foreground: theme.colorScheme.onErrorContainer,
),
],
],
),
);
}
Widget _banner(
ThemeData theme,
String text, {
required Color background,
required Color foreground,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(8),
),
child: Text(
text,
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
color: foreground,
),
),
);
}
Widget _row(ThemeData theme, RiskStep stage, String label) {
// failed 卡在发包阶段,按原逻辑回退到 upload。
final current = step == RiskStep.failed ? RiskStep.upload : step;
const order = [
RiskStep.fetchData,
RiskStep.upload,
RiskStep.logout,
RiskStep.complete,
];
final done = order.indexOf(current) > order.indexOf(stage);
final active = step == stage;
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)),
],
),
);
}
}