personal_rank.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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, fontSize: 14.22)),
  108. const Spacer(),
  109. GestureDetector(
  110. onTap: () => _pickDate(context, c),
  111. child: Container(
  112. decoration: BoxDecoration(
  113. border: Border.all(color: const Color(0xffe3e3e3))),
  114. height: 22.04,
  115. alignment: Alignment.center,
  116. padding: const EdgeInsets.symmetric(horizontal: 8),
  117. child: Text(TimeOfDay.fromDateTime(c.filterStartAt.value)
  118. .format(context))))
  119. ],
  120. ),
  121. const SizedBox(height: 20),
  122. Expanded(
  123. child: Container(
  124. padding: const EdgeInsets.all(12),
  125. decoration: BoxDecoration(
  126. color: const Color(0xff003656),
  127. borderRadius: BorderRadius.circular(9)),
  128. child: ListView(
  129. children: c.activeList
  130. .map((e) => wActiveCard(
  131. context, e, c.selectActive.value?.id == e.id, () {
  132. c.selectActive.value = e;
  133. }))
  134. .toList(),
  135. ))),
  136. ],
  137. );
  138. }
  139. Widget wActiveCard(BuildContext context, RankActiveInfo active, bool selected,
  140. VoidCallback onTap) {
  141. return GestureDetector(
  142. onTap: onTap,
  143. child: Container(
  144. decoration: const BoxDecoration(color: Color(0xff00a0ff), boxShadow: [
  145. BoxShadow(color: Color(0x4d000000), blurRadius: 3.5)
  146. ]),
  147. height: _cardHeight,
  148. width: double.infinity,
  149. margin: const EdgeInsets.only(top: 7),
  150. padding: const EdgeInsets.only(right: 11),
  151. child: Row(
  152. children: [
  153. Container(
  154. margin: const EdgeInsets.only(right: 10),
  155. height: double.infinity,
  156. width: 6.4,
  157. color: selected ? const Color(0xffff870d) : Colors.transparent,
  158. ),
  159. Expanded(child: Text(active.name)),
  160. const SizedBox(width: 8),
  161. Text(active.userList.length.toString())
  162. ],
  163. ),
  164. ));
  165. }
  166. Widget eUserList(BuildContext context, PersonalRankController c) {
  167. final active = c.selectActive.value;
  168. if (active == null) {
  169. return const SizedBox();
  170. }
  171. final userList = c.selectActive.value?.userList ?? <RankUserInfo>[];
  172. return Column(
  173. children: [
  174. Row(
  175. children: [
  176. Padding(padding: const EdgeInsets.all(8), child: titlePoint()),
  177. Text('个人排名',
  178. style: context.textTheme.titleLarge
  179. ?.copyWith(color: Colors.white, fontSize: 14.22)),
  180. Text(' (${active.name})',
  181. style: context.textTheme.titleLarge?.copyWith(
  182. color: const Color(0xffffcb00), fontSize: 14.22)),
  183. ],
  184. ),
  185. Expanded(
  186. child: Padding(
  187. padding: const EdgeInsets.all(18),
  188. child: Column(
  189. children: [
  190. eUserListTitle(context),
  191. Expanded(
  192. child: ListView(
  193. children: userList.indexed.map<Widget>((t) {
  194. return eUserCard(context, c, t.$1 + 1, t.$2);
  195. }).toList(),
  196. ))
  197. ],
  198. )))
  199. ],
  200. );
  201. }
  202. Widget eUserListTitle(BuildContext context) {
  203. return DefaultTextStyle(
  204. style: const TextStyle(
  205. color: Color(0xff98d8ff),
  206. fontSize: 14.22,
  207. fontWeight: FontWeight.w700),
  208. child: Row(
  209. children: [
  210. const SizedBox(
  211. width: _userIndexWidth,
  212. child: Text('排名', textAlign: TextAlign.center)),
  213. const SizedBox(width: 4),
  214. const SizedBox(
  215. width: _userNameWidth,
  216. child: Text('用户名', textAlign: TextAlign.center)),
  217. verticalDivider(show: false),
  218. const SizedBox(
  219. width: _userPhoneWidth,
  220. child: Text(
  221. '手机号',
  222. textAlign: TextAlign.center,
  223. ),
  224. ),
  225. verticalDivider(show: false),
  226. const Expanded(
  227. flex: 5, child: Text('路线ID', textAlign: TextAlign.center)),
  228. verticalDivider(show: false),
  229. const SizedBox(
  230. width: _userTimeWidth,
  231. child: Text('总时间', textAlign: TextAlign.center)),
  232. verticalDivider(show: false),
  233. const Expanded(
  234. flex: 3, child: Text('总里程', textAlign: TextAlign.center)),
  235. verticalDivider(show: false),
  236. const Expanded(
  237. flex: 4, child: Text('配速', textAlign: TextAlign.center)),
  238. verticalDivider(show: false),
  239. const SizedBox(
  240. width: _userResultWidth,
  241. child: Text('状态', textAlign: TextAlign.center)),
  242. // verticalDivider(show: false),
  243. // const SizedBox(
  244. // width: _userFlagWidth,
  245. // child: Text('分组', textAlign: TextAlign.center)),
  246. // verticalDivider(show: false),
  247. ],
  248. ));
  249. }
  250. Widget eUserCard(BuildContext context, PersonalRankController c, int index,
  251. RankUserInfo data) {
  252. return DefaultTextStyle(
  253. style: context.textTheme.bodyMedium!.copyWith(color: Colors.white),
  254. child: Container(
  255. height: _cardHeight,
  256. width: double.infinity,
  257. margin: const EdgeInsets.only(top: 3),
  258. child: Row(
  259. children: [
  260. Container(
  261. width: _userIndexWidth,
  262. height: double.infinity,
  263. decoration: const BoxDecoration(
  264. color: Color(0xffff870d),
  265. borderRadius: BorderRadius.only(
  266. topLeft: Radius.circular(6),
  267. bottomLeft: Radius.circular(6))),
  268. alignment: Alignment.center,
  269. child: Text(
  270. index.toString(),
  271. textAlign: TextAlign.center,
  272. style: const TextStyle(
  273. fontSize: 34,
  274. fontWeight: FontWeight.w700,
  275. fontStyle: FontStyle.italic),
  276. )),
  277. const SizedBox(width: 4),
  278. Expanded(
  279. child: Container(
  280. height: double.infinity,
  281. decoration: const BoxDecoration(
  282. color: Color(0xff003656),
  283. borderRadius: BorderRadius.only(
  284. topRight: Radius.circular(6),
  285. bottomRight: Radius.circular(6))),
  286. child: Row(
  287. children: [
  288. SizedBox(
  289. width: _userNameWidth,
  290. child: Text(
  291. data.name,
  292. maxLines: 1,
  293. textAlign: TextAlign.center,
  294. )),
  295. verticalDivider(),
  296. SizedBox(
  297. width: _userPhoneWidth,
  298. child: Text(
  299. data.phone,
  300. textAlign: TextAlign.center,
  301. )),
  302. verticalDivider(),
  303. Expanded(
  304. flex: 5,
  305. child: Text(
  306. data.routeName,
  307. textAlign: TextAlign.center,
  308. )),
  309. verticalDivider(),
  310. SizedBox(
  311. width: _userTimeWidth,
  312. child: Text(
  313. data.duration.toMinSecondString(),
  314. textAlign: TextAlign.center,
  315. )),
  316. verticalDivider(),
  317. Expanded(
  318. flex: 3,
  319. child: Text(
  320. data.distance.toString(),
  321. textAlign: TextAlign.center,
  322. )),
  323. verticalDivider(),
  324. Expanded(
  325. flex: 4,
  326. child: Container(
  327. margin: const EdgeInsets.only(left: 8, right: 8),
  328. alignment: Alignment.center,
  329. height: 18,
  330. decoration: BoxDecoration(
  331. color: data.pace.color,
  332. borderRadius: BorderRadius.circular(9)),
  333. child: Text(
  334. data.pace.toString(),
  335. textAlign: TextAlign.center,
  336. ))),
  337. // verticalDivider(),
  338. verticalDivider(),
  339. SizedBox(
  340. width: _userResultWidth,
  341. child: Text(
  342. switch (data.state) {
  343. GameState.processing => '进行中',
  344. GameState.finish => '完赛',
  345. GameState.unFinish => '退赛'
  346. },
  347. textAlign: TextAlign.center,
  348. )),
  349. // Container(
  350. // alignment: Alignment.center,
  351. // width: _userFlagWidth,
  352. // child: Icon(Icons.flag, color: data.flag.color)),
  353. ],
  354. ),
  355. ))
  356. ],
  357. )));
  358. }
  359. Widget verticalDivider({bool show = true}) {
  360. return VerticalDivider(
  361. width: 3,
  362. indent: 14,
  363. endIndent: 14,
  364. color: show ? Colors.white : Colors.transparent,
  365. );
  366. }
  367. static const _userIndexWidth = 56.0;
  368. static const _userNameWidth = 70.0;
  369. static const _userPhoneWidth = 94.0;
  370. static const _userResultWidth = 62.0;
  371. static const _userTimeWidth = 62.0;
  372. static const _cardHeight = 45.0;
  373. // static const _userFlagWidth = 52.0;
  374. }
  375. enum GameState {
  376. processing,
  377. finish,
  378. unFinish,
  379. }
  380. class RankUserInfo {
  381. var id = 0;
  382. var name = '';
  383. var routeName = '';
  384. var state = GameState.processing;
  385. var duration = 0.seconds;
  386. var distance = 0.meter;
  387. var startAt = DateTime(2000);
  388. var phone = '';
  389. Pace get pace => Pace(distance, duration);
  390. var flag = Flag.red;
  391. }
  392. class RankActiveInfo {
  393. var id = 0;
  394. var name = '';
  395. var userList = <RankUserInfo>[];
  396. }