import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; import '../../widgets/desktop_constrained.dart'; enum _NetworkTestTab { network, service, custom } class NetworkTestPage extends StatefulWidget { const NetworkTestPage({super.key}); @override State createState() => _NetworkTestPageState(); } class _NetworkTestPageState extends State { static final _panUri = Uri.parse('https://pan.gongyun.org'); static final _infoUri = Uri.parse('https://www.gongyun.org/appnetinfo.html'); _NetworkTestTab _tab = _NetworkTestTab.network; bool _networkLoading = false; bool _serviceLoading = false; _NetworkDiagnostics? _networkDiagnostics; _ServiceDiagnostics? _serviceDiagnostics; String? _networkError; String? _serviceError; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _runNetworkTest()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('网络测试'), actions: [ IconButton( tooltip: '说明', icon: const Icon(Icons.info_outline), onPressed: () => launchUrl(_infoUri, mode: LaunchMode.externalApplication), ), ], ), body: DesktopConstrained( child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: SizedBox( width: double.infinity, child: SegmentedButton<_NetworkTestTab>( segments: const [ ButtonSegment( value: _NetworkTestTab.network, icon: Icon(Icons.public), label: Text('网络'), ), ButtonSegment( value: _NetworkTestTab.service, icon: Icon(Icons.dns_outlined), label: Text('服务'), ), ButtonSegment( value: _NetworkTestTab.custom, icon: Icon(Icons.route_outlined), label: Text('自定义'), ), ], selected: {_tab}, onSelectionChanged: (selection) { final selected = selection.first; if (selected == _NetworkTestTab.custom) { _showCustomUnavailableDialog(); return; } setState(() => _tab = selected); if (selected == _NetworkTestTab.network && _networkDiagnostics == null && !_networkLoading) { _runNetworkTest(); } if (selected == _NetworkTestTab.service && _serviceDiagnostics == null && !_serviceLoading) { _runServiceTest(); } }, ), ), ), Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 180), child: switch (_tab) { _NetworkTestTab.network => _NetworkPanel( key: const ValueKey('network'), loading: _networkLoading, error: _networkError, diagnostics: _networkDiagnostics, onRefresh: _runNetworkTest, ), _NetworkTestTab.service => _ServicePanel( key: const ValueKey('service'), loading: _serviceLoading, error: _serviceError, diagnostics: _serviceDiagnostics, onRefresh: _runServiceTest, ), _NetworkTestTab.custom => const SizedBox.shrink(), }, ), ), ], ), ), ); } Future _showCustomUnavailableDialog() async { await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('自定义线路'), content: const Text('此功能暂不可用'), actions: [ FilledButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('好'), ), ], ), ); } Future _runNetworkTest() async { setState(() { _networkLoading = true; _networkError = null; }); try { final ipFuture = _fetchIpInfo(); final panProbe = await _probeUri(_panUri); final stability = await _testStability(_panUri); final ipInfo = await ipFuture; if (!mounted) return; setState(() { _networkDiagnostics = _NetworkDiagnostics( panReachable: panProbe.reachable, latencyMs: panProbe.latencyMs, ipInfo: ipInfo, stable: stability.stable, sampleCount: stability.sampleCount, successCount: stability.successCount, ); }); } catch (e) { if (mounted) setState(() => _networkError = e.toString()); } finally { if (mounted) setState(() => _networkLoading = false); } } Future _runServiceTest() async { setState(() { _serviceLoading = true; _serviceError = null; }); try { final monitorHeartbeats = await _fetchMonitorHeartbeats('gongyun'); _ServiceItem monitor(String label, int monitorId) { return _monitorItem( label: label, monitorId: monitorId, heartbeats: monitorHeartbeats, ); } final telegramBot = await _probeItem( label: 'Telegram bot', uri: Uri.parse('https://bot.mygongyun.com'), healthyStatusCodes: const {200, 401}, ); final business = <_ServiceItem>[ monitor('官网', 3), monitor('网盘', 2), monitor('短链', 37), monitor('SSO', 39), telegramBot, ]; final dc = <_ServiceItem>[ monitor('DC3-CN-SZ', 13), monitor('DC3-CN-MIAMI', 11), monitor('DC3-CN-NY', 12), monitor('DC3-CN-LV', 10), ]; final backend = <_ServiceItem>[monitor('数据库', 32), monitor('键值对数据库', 36)]; final storage = <_ServiceItem>[ monitor('DC-S-NA-1', 5), monitor('DC-S-NA-2', 6), monitor('DC-S-EU', 7), monitor('DC-S-EP', 8), ]; final acceleration = <_ServiceItem>[ monitor('香港', 18), monitor('香港', 19), monitor('新加坡', 22), monitor('洛杉矶', 24), monitor('蒙特利尔', 21), ]; if (!mounted) return; setState(() { _serviceDiagnostics = _ServiceDiagnostics( fetchedAt: DateTime.now(), business: business, dc: dc, backend: backend, storage: storage, acceleration: acceleration, ); }); } catch (e) { if (mounted) setState(() => _serviceError = e.toString()); } finally { if (mounted) setState(() => _serviceLoading = false); } } Future<_IpInfo> _fetchIpInfo() async { try { final response = await http .get(Uri.parse('https://ipwho.is/')) .timeout(const Duration(seconds: 8)); if (response.statusCode >= 200 && response.statusCode < 300) { final data = jsonDecode(response.body) as Map; if (data['success'] != false) { final connection = data['connection']; final connectionMap = connection is Map ? connection : {}; return _IpInfo( ip: data['ip']?.toString() ?? '未知', asn: connectionMap['asn']?.toString() ?? '未知', operatorName: connectionMap['isp']?.toString().isNotEmpty == true ? connectionMap['isp'].toString() : (connectionMap['org']?.toString() ?? '未知'), ); } } } catch (_) { // Fall back to the next public endpoint. } try { final response = await http .get(Uri.parse('https://ipapi.co/json/')) .timeout(const Duration(seconds: 8)); if (response.statusCode >= 200 && response.statusCode < 300) { final data = jsonDecode(response.body) as Map; return _IpInfo( ip: data['ip']?.toString() ?? '未知', asn: data['asn']?.toString() ?? '未知', operatorName: data['org']?.toString() ?? '未知', ); } } catch (_) { // Handled by the default value below. } return const _IpInfo(ip: '未知', asn: '未知', operatorName: '未知'); } _ServiceItem _monitorItem({ required String label, required int monitorId, required Map heartbeats, }) { final heartbeat = heartbeats[monitorId]; return _ServiceItem( label: label, online: heartbeat?.online ?? false, latencyMs: heartbeat?.pingMs, ); } Future> _fetchMonitorHeartbeats( String pageId, ) async { try { final response = await http .get( Uri.parse('https://status.gongyun.org/api/monitor?pageId=$pageId'), headers: const {'Cache-Control': 'no-cache'}, ) .timeout(const Duration(seconds: 10)); if (response.statusCode < 200 || response.statusCode >= 300) { return const {}; } final decoded = jsonDecode(response.body); if (decoded is! Map) return const {}; final data = decoded['data']; if (data is! Map) return const {}; final heartbeatList = data['heartbeatList']; if (heartbeatList is! Map) return const {}; final result = {}; for (final entry in heartbeatList.entries) { final monitorId = int.tryParse(entry.key); final records = entry.value; if (monitorId == null || records is! List || records.isEmpty) continue; final latest = records.last; if (latest is! Map) continue; final status = _asInt(latest['status']); final ping = _asInt(latest['ping']); result[monitorId] = _MonitorHeartbeat( online: status == 1, pingMs: ping, ); } return result; } catch (_) { return const {}; } } int? _asInt(Object? value) { if (value is int) return value; if (value is num) return value.round(); return int.tryParse(value?.toString() ?? ''); } Future<_ServiceItem> _probeItem({ required String label, required Uri uri, Set healthyStatusCodes = const {}, }) async { final probe = await _probeUri(uri, healthyStatusCodes: healthyStatusCodes); return _ServiceItem( label: label, online: probe.reachable, latencyMs: probe.latencyMs, ); } Future<_ProbeResult> _probeUri( Uri uri, { Set healthyStatusCodes = const {}, }) async { final stopwatch = Stopwatch()..start(); try { final response = await http .get(uri, headers: const {'Cache-Control': 'no-cache'}) .timeout(const Duration(seconds: 8)); stopwatch.stop(); final isHealthy = healthyStatusCodes.isEmpty ? response.statusCode >= 200 && response.statusCode < 500 : healthyStatusCodes.contains(response.statusCode); return _ProbeResult( reachable: isHealthy, latencyMs: stopwatch.elapsedMilliseconds, ); } catch (_) { stopwatch.stop(); return const _ProbeResult(reachable: false); } } Future<_StabilityResult> _testStability(Uri uri) async { final latencies = []; var successCount = 0; const sampleCount = 3; for (var i = 0; i < sampleCount; i++) { final probe = await _probeUri(uri); if (probe.reachable) { successCount += 1; if (probe.latencyMs != null) latencies.add(probe.latencyMs!); } if (i < sampleCount - 1) { await Future.delayed(const Duration(milliseconds: 350)); } } final jitter = latencies.isEmpty ? 999999 : latencies.reduce(max) - latencies.reduce(min); return _StabilityResult( stable: successCount == sampleCount && jitter < 1500, sampleCount: sampleCount, successCount: successCount, ); } } class _NetworkPanel extends StatelessWidget { final bool loading; final String? error; final _NetworkDiagnostics? diagnostics; final VoidCallback onRefresh; const _NetworkPanel({ super.key, required this.loading, required this.error, required this.diagnostics, required this.onRefresh, }); @override Widget build(BuildContext context) { final data = diagnostics; final errorColor = Theme.of(context).colorScheme.error; return ListView( padding: const EdgeInsets.fromLTRB(16, 4, 16, 18), children: [ _SectionCard( title: '网络', children: [ if (loading) const LinearProgressIndicator(), if (error != null) _ErrorText(error!), _InfoRow( label: '公云存储官网访问测试', value: data == null ? '检测中' : (data.panReachable ? '正常' : '离线'), valueColor: _statusColor(context, data?.panReachable), ), _InfoRow(label: '当前网络 IP 地址', value: data?.ipInfo.ip ?? '检测中'), _InfoRow(label: 'IP 归属 AS', value: data?.ipInfo.asn ?? '检测中'), _InfoRow(label: '运营商', value: data?.ipInfo.operatorName ?? '检测中'), _InfoRow( label: '连接时延', value: data?.latencyMs == null ? '检测中' : '${data!.latencyMs} ms', ), _InfoRow( label: '长期连接稳定性', value: data == null ? '检测中' : '${data.stable ? '稳定' : '不稳定'} ' '(${data.successCount}/${data.sampleCount})', valueColor: _statusColor(context, data?.stable), ), ], ), _SectionCard( title: '说明', children: const [ _DescriptionLine('公云存储作为国际化云存储产品,已接入Cloudflare CDN作为全球加速引擎。'), _DescriptionLine( '由于部分国家/地区有法律法规限制与审查,可能会出现包括但不限于网络波动、网络丢包以及网络连接不稳定现象', ), _DescriptionLine( '如诺出现网络连接问题您可将本页面内容向公云存储团队进行报告,团队会根据相关诊断线索进行排查与优化。', ), ], ), _SectionCard( title: '警告', titleColor: errorColor, children: [ Text( '本页面内容涉及你本人敏感信息请勿提供给非公云存储团队人员,以避免你本人相关信息泄露!', style: TextStyle(color: errorColor, fontWeight: FontWeight.w700), ), ], ), const SizedBox(height: 8), FilledButton.icon( onPressed: loading ? null : onRefresh, icon: const Icon(Icons.refresh), label: const Text('重新检测'), ), ], ); } } class _ServicePanel extends StatelessWidget { final bool loading; final String? error; final _ServiceDiagnostics? diagnostics; final VoidCallback onRefresh; const _ServicePanel({ super.key, required this.loading, required this.error, required this.diagnostics, required this.onRefresh, }); @override Widget build(BuildContext context) { final data = diagnostics; return ListView( padding: const EdgeInsets.fromLTRB(16, 4, 16, 18), children: [ _SectionCard( title: '业务状态', children: [ if (loading) const LinearProgressIndicator(), if (error != null) _ErrorText(error!), ..._itemsOrLoading(data?.business), ], ), _SectionCard(title: '离线下载与计算节点', children: _itemsOrLoading(data?.dc)), _SectionCard(title: '后端', children: _itemsOrLoading(data?.backend)), _SectionCard(title: '存储节点', children: _itemsOrLoading(data?.storage)), _SectionCard( title: '加速线路', children: _itemsOrLoading(data?.acceleration, showLatency: true), ), const SizedBox(height: 8), FilledButton.icon( onPressed: loading ? null : onRefresh, icon: const Icon(Icons.refresh), label: const Text('重新检测'), ), ], ); } List _itemsOrLoading( List<_ServiceItem>? items, { bool showLatency = false, }) { if (items == null) { return const [_InfoRow(label: '状态', value: '检测中')]; } return items .map( (item) => _InfoRow( label: item.label, value: showLatency ? item.latencyLabel : (item.online ? '正常' : '离线'), valueColor: item.online ? Colors.green : Colors.red, ), ) .toList(); } } class _SectionCard extends StatelessWidget { final String title; final List children; final Color? titleColor; const _SectionCard({ required this.title, required this.children, this.titleColor, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Card( child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: titleColor, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 10), ...children, ], ), ), ), ); } } class _InfoRow extends StatelessWidget { final String label; final String value; final Color? valueColor; const _InfoRow({required this.label, required this.value, this.valueColor}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Text(label, style: theme.textTheme.bodyMedium)), const SizedBox(width: 16), Flexible( child: Text( value, textAlign: TextAlign.right, style: theme.textTheme.bodyMedium?.copyWith( color: valueColor, fontWeight: FontWeight.w700, ), ), ), ], ), ); } } class _DescriptionLine extends StatelessWidget { final String text; const _DescriptionLine(this.text); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Text(text), ); } } class _ErrorText extends StatelessWidget { final String error; const _ErrorText(this.error); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( '检测失败:$error', style: TextStyle(color: Theme.of(context).colorScheme.error), ), ); } } Color? _statusColor(BuildContext context, bool? value) { if (value == null) return null; return value ? Colors.green : Theme.of(context).colorScheme.error; } class _NetworkDiagnostics { final bool panReachable; final int? latencyMs; final _IpInfo ipInfo; final bool stable; final int sampleCount; final int successCount; const _NetworkDiagnostics({ required this.panReachable, required this.latencyMs, required this.ipInfo, required this.stable, required this.sampleCount, required this.successCount, }); } class _IpInfo { final String ip; final String asn; final String operatorName; const _IpInfo({ required this.ip, required this.asn, required this.operatorName, }); } class _ServiceDiagnostics { final DateTime fetchedAt; final List<_ServiceItem> business; final List<_ServiceItem> dc; final List<_ServiceItem> backend; final List<_ServiceItem> storage; final List<_ServiceItem> acceleration; const _ServiceDiagnostics({ required this.fetchedAt, required this.business, required this.dc, required this.backend, required this.storage, required this.acceleration, }); } class _ServiceItem { final String label; final bool online; final int? latencyMs; const _ServiceItem({ required this.label, required this.online, this.latencyMs, }); String get latencyLabel { if (!online) return '离线'; if (latencyMs == null) return '正常 / -- ms'; return '正常 / $latencyMs ms'; } } class _MonitorHeartbeat { final bool online; final int? pingMs; const _MonitorHeartbeat({required this.online, this.pingMs}); } class _ProbeResult { final bool reachable; final int? latencyMs; const _ProbeResult({required this.reachable, this.latencyMs}); } class _StabilityResult { final bool stable; final int sampleCount; final int successCount; const _StabilityResult({ required this.stable, required this.sampleCount, required this.successCount, }); }