refactor: 拆分主页与公共组件,扫码登录改走 WinHTTP
- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析 - 主页与音乐风险页拆成组件,风险功能共用一套运行框架 - 修复设置页作为主页页签保存时弹掉根路由导致的黑屏 - 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正, 补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对 BoringSSL 的 TLS 指纹拦截(证书校验保持开启) - 补开发预览数据与测试
This commit is contained in:
1 parent
0cb5320290
commit
e9357f5969
55 files changed
+4387
-3392
No files matched your search
+83
-132
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/responsive.dart';
|
||||
import '../config/strings.dart';
|
||||
import '../config/title_server_config.dart';
|
||||
import '../models/session_model.dart';
|
||||
import '../services/title_api_service.dart';
|
||||
import '../widgets/app_card.dart';
|
||||
import '../widgets/app_notice.dart';
|
||||
import '../widgets/cooldown_mixin.dart';
|
||||
import '../widgets/risk/risk_run_mixin.dart';
|
||||
import '../widgets/risk/risk_widgets.dart';
|
||||
import '../widgets/step_progress.dart';
|
||||
|
||||
class TicketPage extends StatefulWidget {
|
||||
@@ -35,19 +37,13 @@ class TicketPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TicketPageState extends State<TicketPage>
|
||||
with CooldownMixin<TicketPage> {
|
||||
with CooldownMixin<TicketPage>, RiskRunMixin<TicketPage> {
|
||||
static const _ticketNameMap = {
|
||||
2: '2倍票',
|
||||
4: '4倍票',
|
||||
5: '5倍票'
|
||||
};
|
||||
|
||||
RiskStep _step = RiskStep.idle;
|
||||
String _stepMessage = '';
|
||||
String? _error;
|
||||
bool _running = false;
|
||||
bool _autoLogout = true;
|
||||
|
||||
List<Map<String, dynamic>>? _tickets;
|
||||
bool _ticketsLoading = false;
|
||||
String? _ticketsError;
|
||||
@@ -70,16 +66,11 @@ class _TicketPageState extends State<TicketPage>
|
||||
return null;
|
||||
}
|
||||
|
||||
void _updateStep(RiskStep step, [String? message]) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_step = step;
|
||||
_stepMessage = message ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadTickets() async {
|
||||
if (!TitleServerConfigHolder().isConfigured) return;
|
||||
final service = SessionModel.instance.createTitleService(
|
||||
cookies: widget.cookies,
|
||||
);
|
||||
if (service == null) return;
|
||||
|
||||
setState(() {
|
||||
_ticketsLoading = true;
|
||||
@@ -89,7 +80,6 @@ class _TicketPageState extends State<TicketPage>
|
||||
});
|
||||
|
||||
try {
|
||||
final service = TitleApiService.fromHolder(cookies: widget.cookies)!;
|
||||
final data = await service.getUserCharge(widget.userId);
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -101,73 +91,67 @@ class _TicketPageState extends State<TicketPage>
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_ticketsError = e.toString();
|
||||
_ticketsError = describeApiError(e);
|
||||
_ticketsLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runTicket() async {
|
||||
if (_selectedTicketId == null) {
|
||||
final ticketId = _selectedTicketId;
|
||||
if (ticketId == null) {
|
||||
_snack(AppStrings.ticketNotSelected);
|
||||
return;
|
||||
}
|
||||
if (!TitleServerConfigHolder().isConfigured) {
|
||||
_snack(AppStrings.ticketNotConfigured);
|
||||
return;
|
||||
}
|
||||
final loginDateTime = widget.loginDateTime;
|
||||
if (loginDateTime == null) {
|
||||
_snack(AppStrings.ticketNotLoggedIn);
|
||||
return;
|
||||
}
|
||||
if (cooldownRemaining > 0) {
|
||||
_snack(AppStrings.ticketCooldownNotice(cooldownRemaining));
|
||||
final guard = riskPreconditionError(
|
||||
loginDateTime: widget.loginDateTime,
|
||||
loginId: null,
|
||||
cooldownRemaining: cooldownRemaining,
|
||||
notLoggedInMessage: AppStrings.ticketNotLoggedIn,
|
||||
cooldownNotice: AppStrings.ticketCooldownNotice,
|
||||
requireLoginId: false,
|
||||
);
|
||||
if (guard != null) {
|
||||
_snack(guard);
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedTicket = _findTicket(_selectedTicketId!);
|
||||
final selectedTicket = _findTicket(ticketId);
|
||||
if (selectedTicket != null &&
|
||||
(selectedTicket['stock'] as num?)?.toInt() != 0) {
|
||||
_snack(AppStrings.ticketStockNotEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
final service = TitleApiService.fromHolder(cookies: widget.cookies)!;
|
||||
final ticketId = _selectedTicketId!;
|
||||
|
||||
setState(() {
|
||||
_running = true;
|
||||
_error = null;
|
||||
});
|
||||
final service = SessionModel.instance.createTitleService(
|
||||
cookies: widget.cookies,
|
||||
)!;
|
||||
setRiskRunning(true);
|
||||
clearRiskError();
|
||||
|
||||
try {
|
||||
_updateStep(RiskStep.upload);
|
||||
updateRiskStep(RiskStep.upload);
|
||||
await service.upsertUserChargeLog(
|
||||
userId: widget.userId,
|
||||
ticketId: ticketId,
|
||||
loginDateTime: loginDateTime,
|
||||
loginDateTime: widget.loginDateTime!,
|
||||
playerRating: widget.playerRating,
|
||||
);
|
||||
|
||||
if (_autoLogout && widget.onLogoutRequested != null) {
|
||||
_updateStep(RiskStep.logout);
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
if (riskAutoLogout && widget.onLogoutRequested != null) {
|
||||
updateRiskStep(RiskStep.logout);
|
||||
await Future<void>.delayed(const Duration(seconds: 5));
|
||||
await widget.onLogoutRequested!();
|
||||
}
|
||||
|
||||
_updateStep(
|
||||
updateRiskStep(
|
||||
RiskStep.complete,
|
||||
'${AppStrings.ticketUsed}: ${_ticketName(ticketId)}',
|
||||
);
|
||||
} on TitleApiException catch (e) {
|
||||
_updateStep(RiskStep.failed, e.message);
|
||||
setState(() => _error = e.message);
|
||||
} catch (e) {
|
||||
_updateStep(RiskStep.failed, e.toString());
|
||||
setState(() => _error = e.toString());
|
||||
reportRiskError(e);
|
||||
} finally {
|
||||
if (mounted) setState(() => _running = false);
|
||||
setRiskRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,63 +160,46 @@ class _TicketPageState extends State<TicketPage>
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (!TitleServerConfigHolder().isConfigured) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.settings_ethernet, size: 48, color: theme.colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(AppStrings.ticketNotConfigured, style: theme.textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
return const RiskConfigMissingView(
|
||||
message: AppStrings.ticketNotConfigured,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: responsiveBody(
|
||||
context,
|
||||
child: Column(
|
||||
children: [
|
||||
if (!isLoggedIn)
|
||||
const AppNotice(
|
||||
AppStrings.ticketNotLoggedIn,
|
||||
error: true,
|
||||
icon: Icons.warning_amber_rounded,
|
||||
),
|
||||
if (isLoggedIn && cooldownRemaining > 0)
|
||||
AppNotice(
|
||||
AppStrings.ticketCooldownNotice(cooldownRemaining),
|
||||
icon: Icons.timer_outlined,
|
||||
),
|
||||
_buildTicketListCard(theme),
|
||||
const SizedBox(height: 16),
|
||||
AutoLogoutToggle(
|
||||
value: _autoLogout,
|
||||
enabled: widget.onLogoutRequested != null && !_running,
|
||||
onChanged: (v) => setState(() => _autoLogout = v),
|
||||
label: AppStrings.ticketAutoLogout,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildRunButton(theme),
|
||||
if (_step != RiskStep.idle) ...[
|
||||
const SizedBox(height: 16),
|
||||
StepProgressCard(
|
||||
step: _step,
|
||||
message: _stepMessage,
|
||||
error: _error,
|
||||
steps: [
|
||||
(RiskStep.upload, AppStrings.stepChargeTicket),
|
||||
if (_autoLogout) (RiskStep.logout, AppStrings.stepLogout),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
return _buildBody(theme);
|
||||
}
|
||||
|
||||
Widget _buildBody(ThemeData theme) {
|
||||
return RiskPageBody(
|
||||
children: [
|
||||
RiskStatusNotices(
|
||||
isLoggedIn: isLoggedIn,
|
||||
cooldownRemaining: cooldownRemaining,
|
||||
notLoggedInText: AppStrings.ticketNotLoggedIn,
|
||||
cooldownText: AppStrings.ticketCooldownNotice,
|
||||
),
|
||||
),
|
||||
_buildTicketListCard(theme),
|
||||
const SizedBox(height: 16),
|
||||
AutoLogoutToggle(
|
||||
value: riskAutoLogout,
|
||||
enabled: widget.onLogoutRequested != null && !riskRunning,
|
||||
onChanged: setRiskAutoLogout,
|
||||
label: AppStrings.ticketAutoLogout,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildRunButton(theme),
|
||||
if (riskStep != RiskStep.idle) ...[
|
||||
const SizedBox(height: 16),
|
||||
StepProgressCard(
|
||||
step: riskStep,
|
||||
message: riskStepMessage,
|
||||
error: riskError,
|
||||
steps: [
|
||||
(RiskStep.upload, AppStrings.stepChargeTicket),
|
||||
if (riskAutoLogout) (RiskStep.logout, AppStrings.stepLogout),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -327,7 +294,7 @@ class _TicketPageState extends State<TicketPage>
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: _running
|
||||
onTap: riskRunning
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
@@ -415,7 +382,7 @@ class _TicketPageState extends State<TicketPage>
|
||||
? _ticketName(_selectedTicketId!)
|
||||
: null;
|
||||
final onCooldown = cooldownRemaining > 0;
|
||||
final canRun = widget.loginDateTime != null && !_running && !onCooldown;
|
||||
final canRun = widget.loginDateTime != null && !onCooldown;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -437,34 +404,18 @@ class _TicketPageState extends State<TicketPage>
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: canRun ? _runTicket : null,
|
||||
icon: _running
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: Icon(onCooldown ? Icons.timer_outlined : Icons.play_arrow, size: 20),
|
||||
label: Text(
|
||||
_running
|
||||
? AppStrings.runningTicket
|
||||
: onCooldown
|
||||
? AppStrings.ticketCooldownCountdown(cooldownRemaining)
|
||||
: AppStrings.runTicket,
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
RiskRunButton(
|
||||
running: riskRunning,
|
||||
enabled: canRun,
|
||||
icon: onCooldown ? Icons.timer_outlined : Icons.play_arrow,
|
||||
label: AppStrings.runTicket,
|
||||
runningLabel: AppStrings.runningTicket,
|
||||
disabledLabel: onCooldown
|
||||
? AppStrings.ticketCooldownCountdown(cooldownRemaining)
|
||||
: null,
|
||||
onPressed: _runTicket,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user