personal_rank.dart 14 KB

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