service_route_screen.dart 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../../core/l10n/app_localizations.dart';
  5. import '../../../core/theme/app_colors.dart';
  6. import '../../../data/models/node/service_node.dart';
  7. import '../../../providers/node_provider.dart';
  8. import '../../widgets/common/app_refresh_indicator.dart';
  9. class ServiceRouteScreen extends ConsumerStatefulWidget {
  10. const ServiceRouteScreen({super.key});
  11. @override
  12. ConsumerState<ServiceRouteScreen> createState() =>
  13. _ServiceRouteScreenState();
  14. }
  15. class _ServiceRouteScreenState extends ConsumerState<ServiceRouteScreen> {
  16. @override
  17. void initState() {
  18. super.initState();
  19. // 进入页面时自动测速
  20. Future.microtask(() => ref.read(nodeProvider.notifier).speedTestAll());
  21. }
  22. @override
  23. Widget build(BuildContext context) {
  24. final cs = Theme.of(context).colorScheme;
  25. final isDark = Theme.of(context).brightness == Brightness.dark;
  26. final state = ref.watch(nodeProvider);
  27. return Scaffold(
  28. appBar: AppBar(
  29. elevation: 0,
  30. leading: IconButton(
  31. icon: const Icon(Icons.chevron_left, size: 28),
  32. onPressed: () => context.pop(),
  33. ),
  34. title: Text(
  35. AppLocalizations.of(context)!.serviceRoute,
  36. style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
  37. ),
  38. centerTitle: true,
  39. ),
  40. body: state.isLoading && state.nodes.isEmpty
  41. ? const Center(child: CircularProgressIndicator())
  42. : state.nodes.isEmpty
  43. ? _EmptyView(
  44. onRetry: () =>
  45. ref.read(nodeProvider.notifier).fetchNodeList(),
  46. )
  47. : AppRefreshIndicator(
  48. onRefresh: () async {
  49. await ref.read(nodeProvider.notifier).fetchNodeList();
  50. await ref.read(nodeProvider.notifier).speedTestAll();
  51. },
  52. child: ListView(
  53. children: [
  54. const SizedBox(height: 16),
  55. Container(
  56. margin: const EdgeInsets.symmetric(horizontal: 16),
  57. decoration: BoxDecoration(
  58. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  59. borderRadius: BorderRadius.circular(12),
  60. ),
  61. child: Column(
  62. children: [
  63. for (int i = 0; i < state.nodes.length; i++) ...[
  64. _NodeItem(
  65. node: state.nodes[i],
  66. latency: state.latencyMap[state.nodes[i].id],
  67. isSelected:
  68. state.currentNode?.id == state.nodes[i].id,
  69. isTesting: state.isSpeedTesting,
  70. isFirst: i == 0,
  71. isLast: i == state.nodes.length - 1,
  72. onTap: () => ref
  73. .read(nodeProvider.notifier)
  74. .selectNode(state.nodes[i]),
  75. ),
  76. if (i < state.nodes.length - 1)
  77. Divider(
  78. height: 1,
  79. indent: 16,
  80. endIndent: 0,
  81. color: cs.outline.withAlpha(40),
  82. ),
  83. ],
  84. ],
  85. ),
  86. ),
  87. const SizedBox(height: 16),
  88. Padding(
  89. padding: const EdgeInsets.symmetric(horizontal: 20),
  90. child: Text(
  91. AppLocalizations.of(context)!.serviceRouteHint,
  92. style: TextStyle(
  93. color: cs.onSurface.withAlpha(102),
  94. fontSize: 12,
  95. ),
  96. ),
  97. ),
  98. const SizedBox(height: 40),
  99. ],
  100. ),
  101. ),
  102. );
  103. }
  104. }
  105. // ── 节点行 ──────────────────────────────────────────────────
  106. class _NodeItem extends StatelessWidget {
  107. const _NodeItem({
  108. required this.node,
  109. required this.latency,
  110. required this.isSelected,
  111. required this.isTesting,
  112. required this.isFirst,
  113. required this.isLast,
  114. required this.onTap,
  115. });
  116. final ServiceNode node;
  117. final int? latency;
  118. final bool isSelected;
  119. final bool isTesting;
  120. final bool isFirst;
  121. final bool isLast;
  122. final VoidCallback onTap;
  123. @override
  124. Widget build(BuildContext context) {
  125. final cs = Theme.of(context).colorScheme;
  126. return InkWell(
  127. onTap: onTap,
  128. borderRadius: BorderRadius.vertical(
  129. top: isFirst ? const Radius.circular(12) : Radius.zero,
  130. bottom: isLast ? const Radius.circular(12) : Radius.zero,
  131. ),
  132. child: Padding(
  133. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  134. child: Row(
  135. children: [
  136. // 线路名称
  137. Expanded(
  138. child: Text(
  139. node.name,
  140. style: TextStyle(
  141. color: cs.onSurface,
  142. fontSize: 15,
  143. fontWeight: FontWeight.w500,
  144. ),
  145. ),
  146. ),
  147. // 信号图标
  148. _SignalBars(latency: latency),
  149. const SizedBox(width: 12),
  150. // 延迟数值
  151. SizedBox(
  152. width: 60,
  153. child: isTesting && latency == null
  154. ? SizedBox(
  155. width: 16,
  156. height: 16,
  157. child: CircularProgressIndicator(
  158. strokeWidth: 2,
  159. color: cs.onSurface.withAlpha(102),
  160. ),
  161. )
  162. : Text(
  163. latency != null ? '${latency}ms' : '--',
  164. style: TextStyle(
  165. color: _latencyColor(latency),
  166. fontSize: 14,
  167. fontWeight: FontWeight.w500,
  168. ),
  169. ),
  170. ),
  171. // 选中标记
  172. SizedBox(
  173. width: 24,
  174. child: isSelected
  175. ? const Icon(Icons.check, color: AppColors.rise, size: 20)
  176. : null,
  177. ),
  178. ],
  179. ),
  180. ),
  181. );
  182. }
  183. }
  184. // ── 信号柱状图标 ────────────────────────────────────────────
  185. class _SignalBars extends StatelessWidget {
  186. const _SignalBars({required this.latency});
  187. final int? latency;
  188. @override
  189. Widget build(BuildContext context) {
  190. final level = _signalLevel(latency);
  191. final color = _latencyColor(latency);
  192. return Row(
  193. mainAxisSize: MainAxisSize.min,
  194. crossAxisAlignment: CrossAxisAlignment.end,
  195. children: [
  196. _bar(6, level >= 1 ? color : color.withAlpha(60)),
  197. const SizedBox(width: 2),
  198. _bar(10, level >= 2 ? color : color.withAlpha(60)),
  199. const SizedBox(width: 2),
  200. _bar(14, level >= 3 ? color : color.withAlpha(60)),
  201. const SizedBox(width: 2),
  202. _bar(18, level >= 4 ? color : color.withAlpha(60)),
  203. ],
  204. );
  205. }
  206. Widget _bar(double height, Color color) {
  207. return Container(
  208. width: 4,
  209. height: height,
  210. decoration: BoxDecoration(
  211. color: color,
  212. borderRadius: BorderRadius.circular(1),
  213. ),
  214. );
  215. }
  216. }
  217. // ── 空状态 ──────────────────────────────────────────────────
  218. class _EmptyView extends StatelessWidget {
  219. const _EmptyView({required this.onRetry});
  220. final VoidCallback onRetry;
  221. @override
  222. Widget build(BuildContext context) {
  223. final cs = Theme.of(context).colorScheme;
  224. return Center(
  225. child: Column(
  226. mainAxisSize: MainAxisSize.min,
  227. children: [
  228. Icon(Icons.cloud_off, size: 48, color: cs.onSurface.withAlpha(80)),
  229. const SizedBox(height: 12),
  230. Text(
  231. AppLocalizations.of(context)!.noAvailableRoute,
  232. style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14),
  233. ),
  234. const SizedBox(height: 16),
  235. ElevatedButton(
  236. onPressed: onRetry,
  237. child: Text(AppLocalizations.of(context)!.retry),
  238. ),
  239. ],
  240. ),
  241. );
  242. }
  243. }
  244. // ── 工具函数 ────────────────────────────────────────────────
  245. /// 信号等级:4=优秀 3=良好 2=一般 1=差 0=不可达
  246. int _signalLevel(int? latency) {
  247. if (latency == null) return 0;
  248. if (latency < 150) return 4;
  249. if (latency < 300) return 3;
  250. if (latency < 500) return 2;
  251. return 1;
  252. }
  253. Color _latencyColor(int? latency) {
  254. if (latency == null) return AppColors.lightTextDisabled;
  255. if (latency < 200) return AppColors.rise;
  256. if (latency < 400) return const Color(0xFFf0b90b);
  257. return AppColors.fall;
  258. }