merge new features

This commit is contained in:
2026-05-27 19:19:11 +08:00
parent 886046a72b
commit 98af110531
37 changed files with 6405 additions and 993 deletions
+144 -37
View File
@@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../presentation/pages/auth/captcha_challenge_page.dart';
@@ -30,6 +32,11 @@ class CaptchaService {
String? _captchaToken;
bool _isLoadingCaptcha = false;
// 代理配置(仅 Windows 桌面端)
CaptchaProxyConfig? _proxyConfig;
bool get _isDesktop => !kIsWeb && (Platform.isWindows || Platform.isLinux);
bool get isLoadingCaptcha => _isLoadingCaptcha;
String? get captchaImage => _captchaImage;
String? get captchaTicket => _captchaTicket;
@@ -103,13 +110,15 @@ class CaptchaService {
Map<String, dynamic> config = <String, dynamic>{};
try {
config = await AuthService.instance.getBasicSiteConfig().timeout(
const Duration(seconds: 10),
);
config = await AuthService.instance
.getBasicSiteConfig()
.timeout(const Duration(seconds: 10));
} catch (_) {}
final captchaType = _normalizeCaptchaType(
(config['captcha_type'] ?? config['captchaType'] ?? config['captcha'])
(config['captcha_type'] ??
config['captchaType'] ??
config['captcha'])
?.toString(),
);
@@ -151,8 +160,7 @@ class CaptchaService {
'capAssetServer',
]);
final isExternalCaptcha =
captchaType == 'turnstile' ||
final isExternalCaptcha = captchaType == 'turnstile' ||
captchaType == 'recaptcha' ||
captchaType == 'cap';
@@ -199,7 +207,7 @@ class CaptchaService {
}
/// 跳转到 Web 验证码页面
Future<void> openCaptchaChallenge(BuildContext context) async {
Future<void> openCaptchaChallenge(BuildContext context, {VoidCallback? onVerified}) async {
final server = ServerService.instance.currentServer;
final config = captchaWebConfig;
@@ -210,14 +218,18 @@ class CaptchaService {
final token = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (_) =>
CaptchaChallengePage(config: config, baseUrl: server.baseUrl),
builder: (_) => CaptchaChallengePage(
config: config,
baseUrl: server.baseUrl,
proxyConfig: _isDesktop ? _proxyConfig : null,
),
),
);
if (token != null && token.isNotEmpty) {
_captchaToken = token;
ToastHelper.success('人机验证完成');
onVerified?.call();
}
}
@@ -241,16 +253,21 @@ class CaptchaService {
Map<String, String> getCaptchaParams() {
if (isWebCaptcha) {
if (_captchaToken == null || _captchaToken!.isEmpty) return {};
return {'captcha': _captchaToken!, 'ticket': _captchaToken!};
return {
'captcha': _captchaToken!,
'ticket': _captchaToken!,
};
}
final userInput = captchaController.text.trim();
if (userInput.isEmpty &&
(_captchaTicket == null || _captchaTicket!.isEmpty)) {
if (userInput.isEmpty && (_captchaTicket == null || _captchaTicket!.isEmpty)) {
return {};
}
return {'captcha': userInput, 'ticket': _captchaTicket ?? ''};
return {
'captcha': userInput,
'ticket': _captchaTicket ?? '',
};
}
/// Web 验证码是否已通过
@@ -263,30 +280,44 @@ class CaptchaService {
final config = captchaWebConfig;
final displayName = config?.displayName ?? '人机验证';
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
OutlinedButton.icon(
onPressed: _isLoadingCaptcha
? null
: () => openCaptchaChallenge(context),
icon: Icon(
_captchaToken == null
? Icons.verified_user_outlined
: Icons.verified,
),
label: Text(
_captchaToken == null
? '点击完成 $displayName'
: '$displayName 已完成,点击重新验证',
),
),
const SizedBox(height: 8),
Text(
'当前验证码类型:$displayName',
style: TextStyle(fontSize: 12, color: Theme.of(context).hintColor),
),
],
return StatefulBuilder(
builder: (context, setState) {
final hasProxy = _proxyConfig != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
OutlinedButton.icon(
onPressed: _isLoadingCaptcha ? null : () async {
await openCaptchaChallenge(context);
setState(() {});
},
onLongPress: _isDesktop && Platform.isWindows
? () => _showProxyDialog(context, setState)
: null,
icon: Icon(
_captchaToken == null
? Icons.verified_user_outlined
: Icons.verified,
),
label: Text(
_captchaToken == null
? '点击完成 $displayName'
: '$displayName 已完成,点击重新验证',
),
),
const SizedBox(height: 8),
Text(
_isDesktop && Platform.isWindows
? '当前验证码类型:$displayName${hasProxy ? ' (代理: $_proxyConfig)' : ''}\n网络问题验证失败可长按上方按钮可以设置代理(仅windows)'
: '当前验证码类型:$displayName',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).hintColor,
),
),
],
);
},
);
}
@@ -392,4 +423,80 @@ class CaptchaService {
}
return null;
}
/// Windows 桌面端长按弹出代理设置对话框
void _showProxyDialog(BuildContext context, StateSetter setState) {
final hostCtrl = TextEditingController(text: _proxyConfig?.host ?? '');
final portCtrl = TextEditingController(text: _proxyConfig?.port.toString() ?? '');
showDialog(
context: context,
builder: (ctx) {
return AlertDialog(
title: const Text('WebView 代理设置'),
content: SizedBox(
width: 320,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'仅支持无认证代理(HTTP/SOCKS5',
style: TextStyle(fontSize: 12, color: Theme.of(ctx).hintColor),
),
const SizedBox(height: 16),
TextFormField(
controller: hostCtrl,
decoration: const InputDecoration(
labelText: '代理地址',
hintText: '127.0.0.1',
prefixIcon: Icon(Icons.dns_outlined),
),
),
const SizedBox(height: 12),
TextFormField(
controller: portCtrl,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '端口',
hintText: '7890',
prefixIcon: Icon(Icons.numbers),
),
),
],
),
),
actions: [
TextButton(
onPressed: () {
_proxyConfig = null;
setState(() {});
Navigator.of(ctx).pop();
ToastHelper.success('已清除代理配置');
},
child: const Text('清除'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
FilledButton(
onPressed: () {
final host = hostCtrl.text.trim();
final port = int.tryParse(portCtrl.text.trim()) ?? 0;
if (host.isEmpty || port <= 0) {
ToastHelper.failure('请输入有效的代理地址和端口');
return;
}
_proxyConfig = CaptchaProxyConfig(host: host, port: port);
setState(() {});
Navigator.of(ctx).pop();
ToastHelper.success('代理已设置: $host:$port');
},
child: const Text('确定'),
),
],
);
},
);
}
}