personal_rank.dart 15 KB

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