node_list.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. ],
  102. );
  103. }
  104. }
  105. class _CountrySection extends StatelessWidget {
  106. final LocationList locationList;
  107. final bool expanded;
  108. final ValueChanged<bool> onExpandedChanged;
  109. const _CountrySection({
  110. required this.locationList,
  111. required this.expanded,
  112. required this.onExpandedChanged,
  113. });
  114. @override
  115. Widget build(BuildContext context) {
  116. final countryIcon = locationList.icon ?? '';
  117. final countryName = locationList.name ?? '';
  118. final locations = locationList.locations ?? [];
  119. // 获取 HomeController 并判断当前国家是否有选中的节点
  120. final homeController = Get.find<HomeController>();
  121. final hasSelectedLocation = locations.any(
  122. (loc) => loc.id == homeController.selectedLocation?.id,
  123. );
  124. // 根据展开状态和是否选中设置背景色
  125. final backgroundColor = hasSelectedLocation
  126. ? (expanded
  127. ? Get.reactiveTheme.cardColor
  128. : Get.reactiveTheme.highlightColor)
  129. : Get.reactiveTheme.highlightColor;
  130. return Container(
  131. margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  132. decoration: BoxDecoration(
  133. color: backgroundColor,
  134. borderRadius: BorderRadius.circular(12),
  135. ),
  136. child: Column(
  137. children: [
  138. ClickOpacity(
  139. onTap: () => onExpandedChanged(!expanded),
  140. child: Padding(
  141. padding: EdgeInsets.all(14.w),
  142. child: Row(
  143. children: [
  144. // 国旗图标
  145. ClipRRect(
  146. borderRadius: BorderRadius.circular(4.r),
  147. child: SvgPicture.asset(
  148. Assets.getCountryFlagImage(countryIcon),
  149. width: 32.w,
  150. height: 24.w,
  151. fit: BoxFit.cover,
  152. ),
  153. ),
  154. 10.horizontalSpace,
  155. Text(
  156. countryName,
  157. style: TextStyle(
  158. fontSize: 16.sp,
  159. height: 1.5,
  160. fontWeight: FontWeight.w500,
  161. color: hasSelectedLocation
  162. ? Get.reactiveTheme.primaryColor
  163. : Get.reactiveTheme.textTheme.bodyLarge!.color,
  164. ),
  165. ),
  166. const Spacer(),
  167. // 箭头图标
  168. AnimatedRotation(
  169. turns: expanded ? 0.25 : 0.0,
  170. duration: const Duration(milliseconds: 300),
  171. child: Icon(
  172. IconFont.icon02,
  173. size: 20.w,
  174. color: Get.reactiveTheme.hintColor,
  175. ),
  176. ),
  177. ],
  178. ),
  179. ),
  180. ),
  181. ClipRect(
  182. child: AnimatedAlign(
  183. duration: const Duration(milliseconds: 300),
  184. curve: Curves.easeInOut,
  185. heightFactor: expanded ? 1.0 : 0.0,
  186. alignment: Alignment.topLeft,
  187. child: AnimatedOpacity(
  188. opacity: expanded ? 1.0 : 0.0,
  189. duration: const Duration(milliseconds: 300),
  190. child: Column(
  191. children: locations.map((location) {
  192. final locationName = location.name ?? '';
  193. final locationIcon = location.icon ?? countryIcon;
  194. // 判断当前节点是否被选中
  195. final isSelected =
  196. location.id == homeController.selectedLocation?.id;
  197. return ClickOpacity(
  198. onTap: () {
  199. // 获取 HomeController
  200. final homeController = Get.find<HomeController>();
  201. homeController.selectLocation(location);
  202. homeController.handleConnect(delay: true);
  203. // 返回上一页
  204. Get.back();
  205. },
  206. child: Column(
  207. children: [
  208. Divider(
  209. height: 1,
  210. color: Get.reactiveTheme.dividerColor,
  211. ),
  212. Container(
  213. alignment: Alignment.centerLeft,
  214. margin: EdgeInsets.only(
  215. top: 14.w,
  216. bottom: 14.w,
  217. left: 24.w,
  218. right: 14.w,
  219. ),
  220. child: Row(
  221. children: [
  222. ClipRRect(
  223. borderRadius: BorderRadius.circular(4.r),
  224. child: SvgPicture.asset(
  225. Assets.getCountryFlagImage(locationIcon),
  226. width: 32.w,
  227. height: 24.w,
  228. fit: BoxFit.cover,
  229. ),
  230. ),
  231. 10.horizontalSpace,
  232. Expanded(
  233. child: Text(
  234. locationName,
  235. style: TextStyle(
  236. fontSize: 14.sp,
  237. fontWeight: FontWeight.w500,
  238. color: isSelected
  239. ? Get.reactiveTheme.primaryColor
  240. : Get
  241. .reactiveTheme
  242. .textTheme
  243. .bodyLarge!
  244. .color,
  245. ),
  246. ),
  247. ),
  248. // 显示延迟
  249. if (isSelected)
  250. Icon(
  251. IconFont.icon27,
  252. size: 16.w,
  253. color: Get.reactiveTheme.primaryColor,
  254. ),
  255. ],
  256. ),
  257. ),
  258. ],
  259. ),
  260. );
  261. }).toList(),
  262. ),
  263. ),
  264. ),
  265. ),
  266. ],
  267. ),
  268. );
  269. }
  270. }