node_list.dart 10 KB

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