node_list.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:flutter_sticky_header/flutter_sticky_header.dart';
  4. import 'package:get/get.dart';
  5. import 'package:nomo/app/widgets/click_opacity.dart';
  6. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  7. import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
  8. import '../../../constants/iconfont/iconfont.dart';
  9. import '../../../data/models/launch/groups.dart';
  10. import '../../../widgets/country_icon.dart';
  11. import '../../home/controllers/home_controller.dart';
  12. import '../controllers/node_controller.dart';
  13. class NodeList extends StatefulWidget {
  14. final int tabIndex;
  15. const NodeList({super.key, required this.tabIndex});
  16. @override
  17. State<NodeList> createState() {
  18. return _NodeListState();
  19. }
  20. }
  21. class _NodeListState extends State<NodeList>
  22. with AutomaticKeepAliveClientMixin {
  23. @override
  24. bool get wantKeepAlive => true;
  25. // 获取 Controller
  26. NodeController get controller => Get.find<NodeController>();
  27. /// 获取国家的展开状态
  28. bool _getExpandedState(String countryCode) {
  29. return controller.getExpandedState(widget.tabIndex, countryCode);
  30. }
  31. /// 设置国家的展开状态
  32. void _setExpandedState(String countryCode, bool expanded) {
  33. setState(() {
  34. controller.setExpandedState(widget.tabIndex, countryCode, expanded);
  35. });
  36. }
  37. final RefreshController _refreshController = RefreshController(
  38. initialRefresh: false,
  39. );
  40. void _onRefresh() async {
  41. await Future.delayed(Duration(milliseconds: 2000));
  42. _refreshController.refreshCompleted();
  43. }
  44. @override
  45. Widget build(BuildContext context) {
  46. super.build(context);
  47. // 获取当前 tab 的数据
  48. final data = controller.getDataByTabIndex(widget.tabIndex);
  49. if (data == null || data.tags == null || data.list == null) {
  50. return Center(
  51. child: Text(
  52. '暂无数据',
  53. style: TextStyle(fontSize: 16.sp, color: Get.reactiveTheme.hintColor),
  54. ),
  55. );
  56. }
  57. // 按 tag 分组数据
  58. final groupedData = <int, List<dynamic>>{};
  59. for (var location in data.list!) {
  60. if (location.tag != null) {
  61. groupedData.putIfAbsent(location.tag!, () => []).add(location);
  62. }
  63. }
  64. // 创建 tag 名称映射
  65. final tagNameMap = <int, String>{};
  66. for (var tag in data.tags!) {
  67. if (tag.id != null && tag.name != null) {
  68. tagNameMap[tag.id!] = tag.name!;
  69. }
  70. }
  71. // 按 sort 排序 tags
  72. final sortedTags = data.tags!.toList()
  73. ..sort((a, b) => (a.sort ?? 0).compareTo(b.sort ?? 0));
  74. return SmartRefresher(
  75. enablePullDown: true,
  76. enablePullUp: false,
  77. controller: _refreshController,
  78. onRefresh: _onRefresh,
  79. child: CustomScrollView(
  80. slivers: [
  81. for (var tag in sortedTags)
  82. if (groupedData.containsKey(tag.id) &&
  83. groupedData[tag.id]!.isNotEmpty)
  84. SliverStickyHeader(
  85. header: Container(
  86. color: Get.reactiveTheme.scaffoldBackgroundColor,
  87. padding: const EdgeInsets.all(16),
  88. child: Text(
  89. tag.name ?? '',
  90. style: TextStyle(
  91. color: Get.reactiveTheme.hintColor,
  92. fontSize: 16,
  93. fontWeight: FontWeight.bold,
  94. ),
  95. ),
  96. ),
  97. sliver: SliverList(
  98. delegate: SliverChildBuilderDelegate((context, i) {
  99. final locationList = groupedData[tag.id]![i];
  100. final countryCode = locationList.icon ?? '';
  101. return _CountrySection(
  102. locationList: locationList,
  103. // 传递展开状态
  104. expanded: _getExpandedState(countryCode),
  105. // 展开状态变化回调
  106. onExpandedChanged: (expanded) {
  107. _setExpandedState(countryCode, expanded);
  108. },
  109. );
  110. }, childCount: groupedData[tag.id]!.length),
  111. ),
  112. ),
  113. SliverSafeArea(sliver: SliverToBoxAdapter(child: 0.verticalSpace)),
  114. ],
  115. ),
  116. );
  117. }
  118. }
  119. class _CountrySection extends StatelessWidget {
  120. final LocationList locationList;
  121. final bool expanded;
  122. final ValueChanged<bool> onExpandedChanged;
  123. const _CountrySection({
  124. required this.locationList,
  125. required this.expanded,
  126. required this.onExpandedChanged,
  127. });
  128. @override
  129. Widget build(BuildContext context) {
  130. final countryIcon = locationList.icon ?? '';
  131. final countryName = locationList.name ?? '';
  132. final locations = locationList.locations ?? [];
  133. // 获取 HomeController 并判断当前国家是否有选中的节点
  134. final homeController = Get.find<HomeController>();
  135. final hasSelectedLocation = locations.any(
  136. (loc) => loc.id == homeController.selectedLocation?.id,
  137. );
  138. // 根据展开状态和是否选中设置背景色
  139. final backgroundColor = hasSelectedLocation
  140. ? (expanded
  141. ? Get.reactiveTheme.cardColor
  142. : Get.reactiveTheme.highlightColor)
  143. : Get.reactiveTheme.highlightColor;
  144. return AnimatedContainer(
  145. duration: const Duration(milliseconds: 300),
  146. curve: Curves.easeInOut,
  147. margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
  148. decoration: BoxDecoration(
  149. color: backgroundColor,
  150. borderRadius: BorderRadius.circular(12.r),
  151. ),
  152. child: Column(
  153. children: [
  154. ClickOpacity(
  155. onTap: () => onExpandedChanged(!expanded),
  156. child: Padding(
  157. padding: EdgeInsets.all(14.w),
  158. child: Row(
  159. children: [
  160. // 国旗图标
  161. CountryIcon(
  162. countryCode: countryIcon,
  163. width: 32.w,
  164. height: 24.w,
  165. borderRadius: 4.r,
  166. ),
  167. 10.horizontalSpace,
  168. Text(
  169. countryName,
  170. style: TextStyle(
  171. fontSize: 16.sp,
  172. height: 1.5,
  173. fontWeight: FontWeight.w500,
  174. color: hasSelectedLocation
  175. ? Get.reactiveTheme.primaryColor
  176. : Get.reactiveTheme.textTheme.bodyLarge!.color,
  177. ),
  178. ),
  179. const Spacer(),
  180. // 箭头图标
  181. AnimatedRotation(
  182. turns: expanded ? 0.25 : 0.0,
  183. duration: const Duration(milliseconds: 300),
  184. child: Icon(
  185. IconFont.icon02,
  186. size: 20.w,
  187. color: Get.reactiveTheme.hintColor,
  188. ),
  189. ),
  190. ],
  191. ),
  192. ),
  193. ),
  194. ClipRect(
  195. child: AnimatedAlign(
  196. duration: const Duration(milliseconds: 300),
  197. curve: Curves.easeInOut,
  198. heightFactor: expanded ? 1.0 : 0.0,
  199. alignment: Alignment.topLeft,
  200. child: AnimatedOpacity(
  201. opacity: expanded ? 1.0 : 0.0,
  202. duration: const Duration(milliseconds: 300),
  203. child: Column(
  204. children: locations.map((location) {
  205. final locationName = location.name ?? '';
  206. final locationIcon = location.icon ?? countryIcon;
  207. // 判断当前节点是否被选中
  208. final isSelected =
  209. location.id == homeController.selectedLocation?.id;
  210. return ClickOpacity(
  211. onTap: () {
  212. // 获取 HomeController
  213. final homeController = Get.find<HomeController>();
  214. homeController.selectLocation(location);
  215. homeController.handleConnect(delay: true);
  216. // 返回上一页
  217. Get.back();
  218. },
  219. child: Column(
  220. children: [
  221. Divider(
  222. height: 1,
  223. color: Get.reactiveTheme.dividerColor,
  224. ),
  225. Container(
  226. alignment: Alignment.centerLeft,
  227. margin: EdgeInsets.only(
  228. top: 14.w,
  229. bottom: 14.w,
  230. left: 24.w,
  231. right: 14.w,
  232. ),
  233. child: Row(
  234. children: [
  235. CountryIcon(
  236. countryCode: locationIcon,
  237. width: 32.w,
  238. height: 24.w,
  239. borderRadius: 4.r,
  240. ),
  241. 10.horizontalSpace,
  242. Expanded(
  243. child: Text(
  244. locationName,
  245. style: TextStyle(
  246. fontSize: 14.sp,
  247. fontWeight: FontWeight.w500,
  248. color: isSelected
  249. ? Get.reactiveTheme.primaryColor
  250. : Get
  251. .reactiveTheme
  252. .textTheme
  253. .bodyLarge!
  254. .color,
  255. ),
  256. ),
  257. ),
  258. // 显示延迟
  259. if (isSelected)
  260. Icon(
  261. IconFont.icon27,
  262. size: 16.w,
  263. color: Get.reactiveTheme.primaryColor,
  264. ),
  265. ],
  266. ),
  267. ),
  268. ],
  269. ),
  270. );
  271. }).toList(),
  272. ),
  273. ),
  274. ),
  275. ),
  276. ],
  277. ),
  278. );
  279. }
  280. }