personal_rank.dart 14 KB

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