personal_rank.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import 'package:common_pub/logger.dart';
  2. import 'package:common_pub/model/distance.dart';
  3. import 'package:common_pub/model/pace.dart';
  4. import 'package:common_pub/utils.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:get/get.dart';
  7. import '../../../widget/title_point.dart';
  8. import '../../service/map_watch.dart';
  9. abstract class PersonalRankController extends GetxController {
  10. @override
  11. void onInit() {
  12. super.onInit();
  13. final now = DateTime.now();
  14. filterStartAt.value = DateTime(now.year, now.month, now.day);
  15. workFlushData();
  16. }
  17. Future<void> workFlushData() async {
  18. while (!isClosed) {
  19. final map = mapWatch;
  20. if (map == null) {
  21. activeList.clear();
  22. await 3.seconds.delay();
  23. continue;
  24. }
  25. try {
  26. final out = await getRankList(map.id, filterStartAt.value);
  27. for (final one in out) {
  28. final oldEvent = map.getEventById(one.id);
  29. if (oldEvent != null) {
  30. for (final user in one.userList) {
  31. final oldUser = oldEvent.getUserById(user.id);
  32. if (oldUser != null) {
  33. user.flag = oldUser.flag.value;
  34. }
  35. }
  36. }
  37. }
  38. activeList.value = out;
  39. } catch (_) {}
  40. await 3.seconds.delay();
  41. }
  42. }
  43. final filterStartAt = DateTime.now().obs;
  44. final activeList = <RankActiveInfo>[].obs;
  45. final Rx<RankActiveInfo?> selectActive = Rx(null);
  46. MapWatch? get mapWatch => Get.find<MapWatchService>().instance;
  47. Future<List<RankActiveInfo>> getRankList(int mapId, DateTime startAt);
  48. }
  49. class PersonalRankPage extends GetView<PersonalRankController> {
  50. const PersonalRankPage({super.key});
  51. @override
  52. Widget build(BuildContext context) {
  53. return Container(
  54. width: double.infinity,
  55. height: double.infinity,
  56. margin: const EdgeInsets.all(20),
  57. padding: const EdgeInsets.fromLTRB(12, 17, 12, 17),
  58. decoration: BoxDecoration(
  59. color: Colors.transparent, borderRadius: BorderRadius.circular(16)),
  60. child: DefaultTextStyle(
  61. style: const TextStyle(color: Colors.white),
  62. child: Row(
  63. children: [
  64. SizedBox(
  65. width: 260,
  66. height: double.infinity,
  67. child: Obx(() => eActiveList(context, controller))),
  68. const SizedBox(width: 20),
  69. Expanded(child: Obx(() => eUserList(context, controller)))
  70. ],
  71. )),
  72. );
  73. }
  74. Future<void> _pickDate(BuildContext context, PersonalRankController c) async {
  75. final now = c.filterStartAt.value;
  76. final time = await showTimePicker(
  77. context: context, initialTime: TimeOfDay.fromDateTime(now));
  78. if (time != null) {
  79. c.filterStartAt.value =
  80. DateTime(now.year, now.month, now.day, time.hour, time.minute);
  81. info('time: ${c.filterStartAt.value}');
  82. c.selectActive.value = null;
  83. c.activeList.clear();
  84. }
  85. // final date = await showDatePicker(
  86. // context: context,
  87. // initialDate: c.filterStartAt.value,
  88. // firstDate: DateTime(now.year - 1),
  89. // lastDate: DateTime(now.year, now.month, now.day + 1),
  90. // );
  91. //
  92. // if (date != null) {
  93. // c.filterStartAt.value = DateTime(date.year, date.month, date.day);
  94. // }
  95. }
  96. Widget titlePoint() {
  97. return const TitlePoint(color: Color(0xff98d8ff));
  98. }
  99. Widget eActiveList(BuildContext context, PersonalRankController c) {
  100. return Column(
  101. children: [
  102. Row(
  103. children: [
  104. Padding(padding: const EdgeInsets.all(8), child: titlePoint()),
  105. Text('活动列表',
  106. style: context.textTheme.titleLarge
  107. ?.copyWith(color: Colors.white)),
  108. const Spacer(),
  109. Container(
  110. decoration: BoxDecoration(
  111. border: Border.all(color: const Color(0xffe3e3e3))),
  112. child: TextButton(
  113. onPressed: () => _pickDate(context, c),
  114. child: Text(TimeOfDay.fromDateTime(c.filterStartAt.value)
  115. .format(context))))
  116. ],
  117. ),
  118. const SizedBox(height: 20),
  119. Expanded(
  120. child: Container(
  121. padding: const EdgeInsets.all(12),
  122. decoration: BoxDecoration(
  123. color: const Color(0xff003656),
  124. borderRadius: BorderRadius.circular(9)),
  125. child: ListView(
  126. children: c.activeList
  127. .map((e) => wActiveCard(
  128. context, e, c.selectActive.value?.id == e.id, () {
  129. c.selectActive.value = e;
  130. }))
  131. .toList(),
  132. ))),
  133. ],
  134. );
  135. }
  136. Widget wActiveCard(BuildContext context, RankActiveInfo active, bool selected,
  137. VoidCallback onTap) {
  138. return GestureDetector(
  139. onTap: onTap,
  140. child: Container(
  141. decoration: const BoxDecoration(color: Color(0xff00a0ff), boxShadow: [
  142. BoxShadow(color: Color(0x4d000000), blurRadius: 3.5)
  143. ]),
  144. height: _cardHeight,
  145. width: double.infinity,
  146. margin: const EdgeInsets.only(top: 7),
  147. padding: const EdgeInsets.only(right: 11),
  148. child: Row(
  149. children: [
  150. Container(
  151. margin: const EdgeInsets.only(right: 10),
  152. height: double.infinity,
  153. width: 6.4,
  154. color: selected ? const Color(0xffff870d) : Colors.transparent,
  155. ),
  156. Expanded(child: Text(active.name)),
  157. const SizedBox(width: 8),
  158. Text(active.userList.length.toString())
  159. ],
  160. ),
  161. ));
  162. }
  163. Widget eUserList(BuildContext context, PersonalRankController c) {
  164. final active = c.selectActive.value;
  165. if (active == null) {
  166. return const SizedBox();
  167. }
  168. final userList = c.selectActive.value?.userList ?? <RankUserInfo>[];
  169. return Column(
  170. children: [
  171. Row(
  172. children: [
  173. const Padding(padding: EdgeInsets.all(8), child: TitlePoint()),
  174. Text('个人排名',
  175. style: context.textTheme.titleLarge
  176. ?.copyWith(color: Colors.white)),
  177. Text(' (${active.name})',
  178. style: context.textTheme.titleLarge
  179. ?.copyWith(color: const Color(0xffffcb00))),
  180. ],
  181. ),
  182. Expanded(
  183. child: Padding(
  184. padding: const EdgeInsets.all(18),
  185. child: Column(
  186. children: [
  187. eUserListTitle(context),
  188. Expanded(
  189. child: ListView(
  190. children: userList.indexed.map<Widget>((t) {
  191. return eUserCard(context, c, t.$1 + 1, t.$2);
  192. }).toList(),
  193. ))
  194. ],
  195. )))
  196. ],
  197. );
  198. }
  199. Widget eUserListTitle(BuildContext context) {
  200. return DefaultTextStyle(
  201. style: const TextStyle(
  202. color: Color(0xff98d8ff),
  203. fontSize: 14.22,
  204. fontWeight: FontWeight.w700),
  205. child: Row(
  206. children: [
  207. const SizedBox(
  208. width: _userIndexWidth,
  209. child: Text('排名', textAlign: TextAlign.center)),
  210. const SizedBox(width: 4),
  211. const SizedBox(
  212. width: _userNameWidth,
  213. child: Text('用户名', textAlign: TextAlign.center)),
  214. verticalDivider(show: false),
  215. const SizedBox(
  216. width: _userPhoneWidth,
  217. child: Text(
  218. '手机号',
  219. textAlign: TextAlign.center,
  220. ),
  221. ),
  222. verticalDivider(show: false),
  223. const Expanded(
  224. flex: 5, child: Text('路线ID', textAlign: TextAlign.center)),
  225. verticalDivider(show: false),
  226. const SizedBox(
  227. width: _userTimeWidth,
  228. child: Text('总时间', textAlign: TextAlign.center)),
  229. verticalDivider(show: false),
  230. const Expanded(
  231. flex: 3, child: Text('总里程', textAlign: TextAlign.center)),
  232. verticalDivider(show: false),
  233. const Expanded(
  234. flex: 4, child: Text('配速', textAlign: TextAlign.center)),
  235. verticalDivider(show: false),
  236. const SizedBox(
  237. width: _userResultWidth,
  238. child: Text('状态', textAlign: TextAlign.center)),
  239. // verticalDivider(show: false),
  240. // const SizedBox(
  241. // width: _userFlagWidth,
  242. // child: Text('分组', textAlign: TextAlign.center)),
  243. // verticalDivider(show: false),
  244. ],
  245. ));
  246. }
  247. Widget eUserCard(BuildContext context, PersonalRankController c, int index,
  248. RankUserInfo data) {
  249. return DefaultTextStyle(
  250. style: context.textTheme.bodyMedium!.copyWith(color: Colors.white),
  251. child: Container(
  252. height: _cardHeight,
  253. width: double.infinity,
  254. margin: const EdgeInsets.only(top: 3),
  255. child: Row(
  256. children: [
  257. Container(
  258. width: _userIndexWidth,
  259. height: double.infinity,
  260. decoration: const BoxDecoration(
  261. color: Color(0xffff870d),
  262. borderRadius: BorderRadius.only(
  263. topLeft: Radius.circular(6),
  264. bottomLeft: Radius.circular(6))),
  265. alignment: Alignment.center,
  266. child: Text(
  267. index.toString(),
  268. textAlign: TextAlign.center,
  269. style: const TextStyle(
  270. fontSize: 34,
  271. fontWeight: FontWeight.w700,
  272. fontStyle: FontStyle.italic),
  273. )),
  274. const SizedBox(width: 4),
  275. Expanded(
  276. child: Container(
  277. height: double.infinity,
  278. decoration: const BoxDecoration(
  279. color: Color(0xff003656),
  280. borderRadius: BorderRadius.only(
  281. topRight: Radius.circular(6),
  282. bottomRight: Radius.circular(6))),
  283. child: Row(
  284. children: [
  285. SizedBox(
  286. width: _userNameWidth,
  287. child: Text(
  288. data.name,
  289. maxLines: 1,
  290. textAlign: TextAlign.center,
  291. )),
  292. verticalDivider(),
  293. SizedBox(
  294. width: _userPhoneWidth,
  295. child: Text(
  296. data.phone,
  297. textAlign: TextAlign.center,
  298. )),
  299. verticalDivider(),
  300. Expanded(
  301. flex: 5,
  302. child: Text(
  303. data.routeName,
  304. textAlign: TextAlign.center,
  305. )),
  306. verticalDivider(),
  307. SizedBox(
  308. width: _userTimeWidth,
  309. child: Text(
  310. data.duration.toMinSecondString(),
  311. textAlign: TextAlign.center,
  312. )),
  313. verticalDivider(),
  314. Expanded(
  315. flex: 3,
  316. child: Text(
  317. data.distance.toString(),
  318. textAlign: TextAlign.center,
  319. )),
  320. verticalDivider(),
  321. Expanded(
  322. flex: 4,
  323. child: Container(
  324. margin: const EdgeInsets.only(left: 8, right: 8),
  325. alignment: Alignment.center,
  326. height: 18,
  327. decoration: BoxDecoration(
  328. color: data.pace.color,
  329. borderRadius: BorderRadius.circular(9)),
  330. child: Text(
  331. data.pace.toString(),
  332. textAlign: TextAlign.center,
  333. ))),
  334. // verticalDivider(),
  335. verticalDivider(),
  336. SizedBox(
  337. width: _userResultWidth,
  338. child: Text(
  339. switch (data.state) {
  340. GameState.processing => '进行中',
  341. GameState.finish => '完赛',
  342. GameState.unFinish => '退赛'
  343. },
  344. textAlign: TextAlign.center,
  345. )),
  346. // Container(
  347. // alignment: Alignment.center,
  348. // width: _userFlagWidth,
  349. // child: Icon(Icons.flag, color: data.flag.color)),
  350. ],
  351. ),
  352. ))
  353. ],
  354. )));
  355. }
  356. Widget verticalDivider({bool show = true}) {
  357. return VerticalDivider(
  358. width: 3,
  359. indent: 14,
  360. endIndent: 14,
  361. color: show ? Colors.white : Colors.transparent,
  362. );
  363. }
  364. static const _userIndexWidth = 56.0;
  365. static const _userNameWidth = 70.0;
  366. static const _userPhoneWidth = 94.0;
  367. static const _userResultWidth = 62.0;
  368. static const _userTimeWidth = 62.0;
  369. static const _cardHeight = 45.0;
  370. // static const _userFlagWidth = 52.0;
  371. }
  372. enum GameState {
  373. processing,
  374. finish,
  375. unFinish,
  376. }
  377. class RankUserInfo {
  378. var id = 0;
  379. var name = '';
  380. var routeName = '';
  381. var state = GameState.processing;
  382. var duration = 0.seconds;
  383. var distance = 0.meter;
  384. var startAt = DateTime(2000);
  385. var phone = '';
  386. Pace get pace => Pace(distance, duration);
  387. var flag = Flag.red;
  388. }
  389. class RankActiveInfo {
  390. var id = 0;
  391. var name = '';
  392. var userList = <RankUserInfo>[];
  393. }