完善PR审查后出现的问题

This commit is contained in:
2026-05-31 10:13:36 +08:00
parent 61ad85f6fc
commit 5ee6ba1e28
4 changed files with 161 additions and 50 deletions
@@ -7,6 +7,7 @@ import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import '../../widgets/desktop_constrained.dart';
import '../../widgets/toast_helper.dart';
enum _NetworkTestTab { network, service, custom }
@@ -20,6 +21,13 @@ class NetworkTestPage extends StatefulWidget {
class _NetworkTestPageState extends State<NetworkTestPage> {
static final _panUri = Uri.parse('https://pan.gongyun.org');
static final _infoUri = Uri.parse('https://www.gongyun.org/appnetinfo.html');
static final _primaryIpLookupUri = Uri.parse('https://ipwho.is/');
static final _secondaryIpLookupUri = Uri.parse('https://ipapi.co/json/');
static const _probeHeaders = {'Cache-Control': 'no-cache'};
static const _probeGetFallbackHeaders = {
'Cache-Control': 'no-cache',
'Range': 'bytes=0-0',
};
_NetworkTestTab _tab = _NetworkTestTab.network;
bool _networkLoading = false;
@@ -44,8 +52,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
IconButton(
tooltip: '说明',
icon: const Icon(Icons.info_outline),
onPressed: () =>
launchUrl(_infoUri, mode: LaunchMode.externalApplication),
onPressed: () => _openExternalUrl(_infoUri),
),
],
),
@@ -71,7 +78,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
ButtonSegment(
value: _NetworkTestTab.custom,
icon: Icon(Icons.route_outlined),
label: Text('自定义'),
label: Text('自定义线路'),
),
],
selected: {_tab},
@@ -129,7 +136,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('自定义线路'),
content: const Text('此功能暂不可用'),
content: const Text('自定义线路为会员功能,当前版本暂未开放。'),
actions: [
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(),
@@ -140,7 +147,20 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
);
}
Future<void> _openExternalUrl(Uri uri) async {
try {
final opened = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!opened) {
ToastHelper.failure('无法打开链接: $uri');
}
} catch (e) {
ToastHelper.failure('无法打开链接: $e');
}
}
Future<void> _runNetworkTest() async {
if (_networkLoading) return;
setState(() {
_networkLoading = true;
_networkError = null;
@@ -171,6 +191,8 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
}
Future<void> _runServiceTest() async {
if (_serviceLoading) return;
setState(() {
_serviceLoading = true;
_serviceError = null;
@@ -246,7 +268,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
Future<_IpInfo> _fetchIpInfo() async {
try {
final response = await http
.get(Uri.parse('https://ipwho.is/'))
.get(_primaryIpLookupUri)
.timeout(const Duration(seconds: 8));
if (response.statusCode >= 200 && response.statusCode < 300) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
@@ -270,7 +292,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
try {
final response = await http
.get(Uri.parse('https://ipapi.co/json/'))
.get(_secondaryIpLookupUri)
.timeout(const Duration(seconds: 8));
if (response.statusCode >= 200 && response.statusCode < 300) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
@@ -307,7 +329,7 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
final response = await http
.get(
Uri.parse('https://status.gongyun.org/api/monitor?pageId=$pageId'),
headers: const {'Cache-Control': 'no-cache'},
headers: _probeHeaders,
)
.timeout(const Duration(seconds: 10));
if (response.statusCode < 200 || response.statusCode >= 300) {
@@ -367,13 +389,19 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
}) async {
final stopwatch = Stopwatch()..start();
try {
final response = await http
.get(uri, headers: const {'Cache-Control': 'no-cache'})
var response = await http
.head(uri, headers: _probeHeaders)
.timeout(const Duration(seconds: 8));
if (_shouldRetryProbeWithGet(response.statusCode)) {
response = await http
.get(uri, headers: _probeGetFallbackHeaders)
.timeout(const Duration(seconds: 8));
}
stopwatch.stop();
final isHealthy = healthyStatusCodes.isEmpty
? response.statusCode >= 200 && response.statusCode < 500
: healthyStatusCodes.contains(response.statusCode);
final isHealthy = _isHealthyStatus(
response.statusCode,
healthyStatusCodes,
);
return _ProbeResult(
reachable: isHealthy,
latencyMs: stopwatch.elapsedMilliseconds,
@@ -384,6 +412,17 @@ class _NetworkTestPageState extends State<NetworkTestPage> {
}
}
bool _shouldRetryProbeWithGet(int statusCode) {
return statusCode == 403 || statusCode == 405 || statusCode == 501;
}
bool _isHealthyStatus(int statusCode, Set<int> healthyStatusCodes) {
if (healthyStatusCodes.isNotEmpty) {
return healthyStatusCodes.contains(statusCode);
}
return statusCode >= 200 && statusCode < 500;
}
Future<_StabilityResult> _testStability(Uri uri) async {
final latencies = <int>[];
var successCount = 0;
@@ -438,7 +477,7 @@ class _NetworkPanel extends StatelessWidget {
if (loading) const LinearProgressIndicator(),
if (error != null) _ErrorText(error!),
_InfoRow(
label: '公云存储网访问测试',
label: '公云存储网访问测试',
value: data == null ? '检测中' : (data.panReachable ? '正常' : '离线'),
valueColor: _statusColor(context, data?.panReachable),
),
@@ -463,11 +502,14 @@ class _NetworkPanel extends StatelessWidget {
title: '说明',
children: const [
_DescriptionLine('公云存储作为国际化云存储产品,已接入Cloudflare CDN作为全球加速引擎。'),
_DescriptionLine(
'当前网络 IP 地址、AS 与运营商信息通过第三方服务 ipwho.is 查询;如该服务不可用,将使用 ipapi.co 作为备用查询。',
),
_DescriptionLine(
'由于部分国家/地区有法律法规限制与审查,可能会出现包括但不限于网络波动、网络丢包以及网络连接不稳定现象',
),
_DescriptionLine(
'出现网络连接问题您可将本页面内容向公云存储团队进行报告,团队会根据相关诊断线索进行排查与优化。',
'出现网络连接问题您可将本页面内容向公云存储团队报告,团队会根据相关诊断线索进行排查与优化。',
),
],
),
@@ -167,9 +167,8 @@ class _SettingsPageState extends State<SettingsPage> {
subtitle: const Text('gongyun_app'),
trailing: const Icon(Icons.open_in_new, size: 16),
onTap: () {
launchUrl(
_openExternalUrl(
Uri.parse('https://git.saont.net/gongyun/app'),
mode: LaunchMode.externalApplication,
);
},
),
@@ -179,11 +178,10 @@ class _SettingsPageState extends State<SettingsPage> {
subtitle: const Text('cloudreve4_flutter'),
trailing: const Icon(Icons.open_in_new, size: 16),
onTap: () {
launchUrl(
_openExternalUrl(
Uri.parse(
'https://github.com/LimoYuan/cloudreve4_flutter',
),
mode: LaunchMode.externalApplication,
);
},
),
@@ -405,13 +403,16 @@ class _SettingsPageState extends State<SettingsPage> {
return AnimatedBuilder(
animation: UpdateService.instance,
builder: (context, _) {
final hasUpdate = UpdateService.instance.hasUpdate;
return ListTile(
leading: const Icon(Icons.tag),
leading: Icon(
hasUpdate ? Icons.new_releases_outlined : Icons.tag,
color: hasUpdate ? Theme.of(context).colorScheme.primary : null,
),
title: const Text('版本号'),
subtitle: Text(_appVersion.isEmpty ? '加载中...' : _appVersion),
trailing: UpdateService.instance.hasUpdate
? const _BlinkingUpdateDot()
: null,
onTap: hasUpdate ? () => _openDownloadsPage() : null,
trailing: hasUpdate ? const _UpdateAvailableBadge() : null,
);
},
);
@@ -562,6 +563,25 @@ class _SettingsPageState extends State<SettingsPage> {
}
}
Future<void> _openExternalUrl(Uri uri) async {
try {
final opened = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!opened) {
ToastHelper.failure('无法打开链接: $uri');
}
} catch (e) {
ToastHelper.failure('无法打开链接: $e');
}
}
Future<void> _openDownloadsPage() async {
try {
await UpdateService.instance.openDownloadsPage();
} catch (e) {
ToastHelper.failure('打开下载页面失败: $e');
}
}
Future<void> _openNetworkTest(BuildContext context) async {
final agreed = await showDialog<bool>(
context: context,
@@ -569,13 +589,11 @@ class _SettingsPageState extends State<SettingsPage> {
builder: (dialogContext) => AlertDialog(
title: const Text('会话信息授权'),
content: _NetworkTestConsentText(
onOpenPrivacy: () => launchUrl(
onOpenPrivacy: () => _openExternalUrl(
Uri.parse('https://www.gongyun.org/policy/privacy.html'),
mode: LaunchMode.externalApplication,
),
onOpenTerms: () => launchUrl(
onOpenTerms: () => _openExternalUrl(
Uri.parse('https://www.gongyun.org/policy/terms.html'),
mode: LaunchMode.externalApplication,
),
),
actions: [
@@ -641,14 +659,14 @@ class _SettingsPageState extends State<SettingsPage> {
}
}
class _BlinkingUpdateDot extends StatefulWidget {
const _BlinkingUpdateDot();
class _UpdateAvailableBadge extends StatefulWidget {
const _UpdateAvailableBadge();
@override
State<_BlinkingUpdateDot> createState() => _BlinkingUpdateDotState();
State<_UpdateAvailableBadge> createState() => _UpdateAvailableBadgeState();
}
class _BlinkingUpdateDotState extends State<_BlinkingUpdateDot>
class _UpdateAvailableBadgeState extends State<_UpdateAvailableBadge>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _opacity;
@@ -674,15 +692,31 @@ class _BlinkingUpdateDotState extends State<_BlinkingUpdateDot>
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return FadeTransition(
opacity: _opacity,
child: Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: colorScheme.error,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
'发现新版本',
style: TextStyle(
color: colorScheme.error,
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: 4),
Icon(Icons.chevron_right, size: 18, color: colorScheme.error),
],
),
);
}
@@ -734,9 +768,8 @@ class _NetworkTestConsentTextState extends State<_NetworkTestConsentText> {
fontWeight: FontWeight.w700,
);
return RichText(
text: TextSpan(
style: Theme.of(context).textTheme.bodyMedium,
return Text.rich(
TextSpan(
children: [
const TextSpan(text: '您需同意'),
TextSpan(
@@ -750,7 +783,10 @@ class _NetworkTestConsentTextState extends State<_NetworkTestConsentText> {
style: linkStyle,
recognizer: _termsRecognizer,
),
const TextSpan(text: '方可进入网络测试页。'),
const TextSpan(
text:
'方可进入网络测试页。测试会访问公云存储服务,并通过第三方服务 ipwho.is 查询当前网络 IP、AS 与运营商信息;如 ipwho.is 不可用,将使用 ipapi.co 作为备用查询。',
),
],
),
);