logic-ranklist.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. var state = {
  2. token: "",
  3. ecId: 0,
  4. mcId: 0,
  5. mcName: "",
  6. ocaId: 0, // Current Map/Group ID
  7. // Personal Info
  8. userInfo: {
  9. nickName: '',
  10. coiName: '',
  11. totalScore: 0, // Points
  12. avatar: 12 // Random or from API if available
  13. },
  14. // Global Stats
  15. stats: {
  16. totalDistance: 0,
  17. totalAnswerNum: 0,
  18. totalCp: 0,
  19. endSecond: 0
  20. },
  21. // Leaderboard Data
  22. rankList: {}, // Stores raw data from API
  23. // UI State
  24. currentTab: 'team', // 'team' or 'individual'
  25. currentMetric: 'score', // 'score', 'mileage', 'accuracy', 'count', 'lap' (pace)
  26. mapList: [], // For drawer
  27. mcState: 0
  28. };
  29. function initRankListPage() {
  30. const params = new URLSearchParams(window.location.search);
  31. state.token = params.get('token') || '';
  32. state.ecId = params.get('id') || 0;
  33. const savedOcaId = uni.getStorageSync(`rank-tpl-style3-map-${state.ecId}`);
  34. if(savedOcaId) state.ocaId = savedOcaId;
  35. window.cardfunc.init(state.token, state.ecId);
  36. window.cardfunc.getCardConfig(onConfigLoaded);
  37. document.getElementById('drawer-backdrop').addEventListener('click', closeDrawer);
  38. }
  39. function onConfigLoaded(config) {
  40. matchRsDetailQuery();
  41. compStatisticQuery();
  42. mapListQuery();
  43. window.cardfunc.unReadMessageQuery();
  44. }
  45. function matchRsDetailQuery() {
  46. uni.request({
  47. url: window.apiMatchRsDetailQuery,
  48. header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
  49. method: "POST",
  50. data: { ecId: state.ecId, ocaId: state.ocaId },
  51. success: (res) => {
  52. if(window.checkResCode(res)) {
  53. const data = res.data.data;
  54. state.mcId = data.mcId;
  55. state.mcName = data.mcName;
  56. state.userInfo.nickName = data.nickName;
  57. state.userInfo.coiName = data.coiName;
  58. state.userInfo.totalScore = data.regionTotalSysPoint || 0;
  59. state.mcState = window.tools.checkMcState(data.beginSecond, data.endSecond);
  60. state.stats.endSecond = data.endSecond;
  61. updateHeaderUI();
  62. fetchCompStats();
  63. fetchMapList();
  64. }
  65. }
  66. });
  67. }
  68. function fetchCompStats() {
  69. uni.request({
  70. url: window.apiCompStatisticQuery,
  71. header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
  72. method: "POST",
  73. data: { mcId: state.mcId },
  74. success: (res) => {
  75. if(res.data.code == 0) {
  76. const data = res.data.data;
  77. state.stats.totalDistance = data.totalDistance;
  78. state.stats.totalAnswerNum = data.totalAnswerNum;
  79. state.stats.totalCp = data.totalCp;
  80. updateMarquee();
  81. }
  82. }
  83. });
  84. }
  85. function fetchMapList() {
  86. uni.request({
  87. url: window.apiMapListQuery,
  88. header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
  89. method: "POST",
  90. data: { mcId: state.mcId },
  91. success: (res) => {
  92. if(res.data.code == 0) {
  93. state.mapList = res.data.data;
  94. if(state.ocaId == 0 && state.mapList.length > 0) {
  95. state.ocaId = state.mapList[0].ocaId;
  96. uni.setStorageSync(`rank-tpl-style3-map-${state.ecId}`, state.ocaId);
  97. }
  98. fetchRankDetail();
  99. renderDrawer();
  100. }
  101. }
  102. });
  103. }
  104. function fetchRankDetail() {
  105. const dispArrStr = "teamCp,teamTodayCp,teamDistance,teamRightAnswerPer,teamTodayPace,regionCp,regionTodayCp,regionDistance,regionRightAnswerPer,regionTodayPace";
  106. uni.request({
  107. url: window.apiCardRankDetailQuery,
  108. header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
  109. method: "POST",
  110. data: {
  111. mcIdListStr: state.mcId,
  112. mcType: 1,
  113. ocaId: state.ocaId,
  114. dispArrStr: dispArrStr
  115. },
  116. success: (res) => {
  117. if(res.data.code == 0) {
  118. state.rankList = res.data.data;
  119. renderLeaderboard();
  120. }
  121. }
  122. });
  123. }
  124. function updateHeaderUI() {
  125. const nameEl = document.getElementById('profileName');
  126. if(nameEl) nameEl.innerText = state.userInfo.nickName || '未命名';
  127. const teamEl = document.getElementById('profileTeam');
  128. if(teamEl) teamEl.innerText = state.userInfo.coiName || '未加入战队';
  129. // Score is in the personal card right side.
  130. // HTML structure: <div class="text-2xl font-black text-primary font-mono leading-none">120</div>
  131. // I'll look for that div or expect user to add ID.
  132. // I'll try to select it by context if ID not present.
  133. // "当前积分" is in the next div.
  134. const scoreLabel = Array.from(document.querySelectorAll('div')).find(el => el.innerText === '当前积分');
  135. if(scoreLabel) {
  136. const valEl = scoreLabel.previousElementSibling;
  137. if(valEl) valEl.innerText = state.userInfo.totalScore;
  138. }
  139. }
  140. function updateMarquee() {
  141. const marquee = document.querySelector('.animate-marquee');
  142. if(marquee) {
  143. const now = Date.now() / 1000;
  144. const dif = state.stats.endSecond - now;
  145. let timeStr = "已结束";
  146. if(dif > 0) timeStr = window.tools.convertSecondsToDHM(dif);
  147. marquee.innerText = `当前总题目: ${state.stats.totalAnswerNum}道 | 总里程: ${window.tools.fmtDistanct(state.stats.totalDistance)}km | 总打点数: ${state.stats.totalCp}个 | 距离比赛结束还有 ${timeStr} | 加油!冲鸭!`;
  148. }
  149. }
  150. const metricMap = {
  151. team: {
  152. score: 'teamCpRs',
  153. mileage: 'teamDistanceRs',
  154. accuracy: 'teamRightAnswerPerRs',
  155. count: 'teamCpRs',
  156. lap: 'teamTodayPaceRs'
  157. },
  158. individual: {
  159. score: 'regionCpRs',
  160. mileage: 'regionDistanceRs',
  161. accuracy: 'regionRightAnswerPerRs',
  162. count: 'regionCpRs',
  163. lap: 'regionTodayPaceRs'
  164. }
  165. };
  166. function renderLeaderboard() {
  167. const container = document.getElementById('leaderboard-container');
  168. const key = metricMap[state.currentTab][state.currentMetric];
  169. const list = state.rankList[key] || [];
  170. let html = '';
  171. list.forEach((item, index) => {
  172. const rank = index + 1;
  173. const isTop3 = index < 3;
  174. const isMe = (item.nickName === state.userInfo.nickName);
  175. let rankIconHtml = '';
  176. if (isTop3) {
  177. const colors = ['text-yellow-400', 'text-gray-400', 'text-orange-600'];
  178. const icon = state.currentTab === 'team' ? 'fa-trophy' : 'fa-medal';
  179. rankIconHtml = `<div class="w-8 flex justify-center shrink-0 mr-1"><i class="fas ${icon} ${colors[index]} text-lg drop-shadow-sm"></i></div>`;
  180. } else {
  181. rankIconHtml = `<div class="w-8 text-center font-bold text-gray-400 text-sm mr-1">${rank}</div>`;
  182. }
  183. let avatarHtml = '';
  184. if (state.currentTab === 'team') {
  185. avatarHtml = `<div class="w-9 h-9 bg-blue-50 rounded-full mr-3 flex items-center justify-center shrink-0 text-primary"><i class="fas fa-user-friends text-base"></i></div>`;
  186. } else {
  187. const img = item.avatar || (index % 10 + 1);
  188. avatarHtml = `<img src="https://i.pravatar.cc/100?img=${img}" class="w-9 h-9 rounded-full mr-3 border-2 ${isTop3 ? 'border-yellow-400' : 'border-transparent'} shrink-0">`;
  189. }
  190. let containerClass = "bg-white rounded-xl py-2 px-3 flex items-center shadow-sm border border-gray-100 relative fade-in-up";
  191. let nameClass = "font-bold text-gray-800 text-sm";
  192. let valClass = "font-bold text-gray-600 font-mono text-base";
  193. if (isMe) {
  194. containerClass = "bg-blue-50 rounded-xl py-2 px-3 flex items-center shadow-md border-2 border-primary/30 relative overflow-hidden transform scale-[1.02] fade-in-up z-10 my-2";
  195. nameClass = "font-bold text-primary text-sm";
  196. valClass = "font-bold text-primary font-mono text-lg";
  197. }
  198. let val = item.val || item.score || item.value || 0;
  199. if(item.teamCp !== undefined) val = item.teamCp; // Specifics based on API key
  200. // Actually API usually returns numeric values in fields like `value` or `score` or same as key name (e.g. teamDistanceRs array contains objects with teamDistance)
  201. // Let's try to find the value based on metric.
  202. if(state.currentMetric === 'mileage') {
  203. val = item.teamDistance || item.regionDistance || item.value || 0;
  204. val = window.tools.fmtDistanct(val);
  205. } else if (state.currentMetric === 'score') {
  206. val = item.teamCp || item.regionCp || item.value || 0;
  207. } else if (state.currentMetric === 'accuracy') {
  208. val = item.teamRightAnswerPer || item.regionRightAnswerPer || 0;
  209. } else if (state.currentMetric === 'lap') {
  210. val = item.teamTodayPace || item.regionTodayPace || 0;
  211. val = window.tools.fmtPace(val);
  212. } else {
  213. val = item.teamCp || item.regionCp || 0;
  214. }
  215. let unit = '';
  216. if(state.currentMetric === 'mileage') unit = 'km';
  217. else if (state.currentMetric === 'score') unit = '分';
  218. else if (state.currentMetric === 'accuracy') unit = '%';
  219. else if (state.currentMetric === 'count') unit = '个';
  220. let itemName = item.coiName || item.nickName || item.name || '未知';
  221. html += `
  222. <div class="${containerClass}">
  223. ${isMe ? '<div class="absolute right-0 top-0 bg-primary text-white text-[8px] px-1.5 py-0.5 rounded-bl-lg">我</div>' : ''}
  224. ${rankIconHtml}
  225. ${avatarHtml}
  226. <div class="flex-1 min-w-0">
  227. <h4 class="${nameClass} truncate">${itemName}</h4>
  228. <p class="text-[10px] text-gray-400">${item.coiName || ''}</p>
  229. </div>
  230. <div class="text-right">
  231. <div class="${valClass}">${val}</div>
  232. <div class="text-[10px] text-gray-400">${unit}</div>
  233. </div>
  234. </div>
  235. `;
  236. });
  237. container.innerHTML = html || '<div class="text-center text-gray-400 py-10">暂无数据</div>';
  238. }
  239. // Exposed UI Functions
  240. window.switchMainTab = function(type) {
  241. state.currentTab = type;
  242. updateTabUI();
  243. renderLeaderboard();
  244. };
  245. window.switchMetric = function(metric) {
  246. state.currentMetric = metric;
  247. updateMetricUI();
  248. renderLeaderboard();
  249. };
  250. function updateTabUI() {
  251. const tTeam = document.getElementById('tab-team');
  252. const tInd = document.getElementById('tab-ind');
  253. const active = "flex-1 py-2 rounded-full text-sm font-bold text-center transition-all duration-300 bg-white text-primary shadow-sm";
  254. const inactive = "flex-1 py-2 rounded-full text-sm font-bold text-center transition-all duration-300 text-gray-500 hover:text-gray-700";
  255. if(state.currentTab === 'team') {
  256. tTeam.className = active;
  257. tInd.className = inactive;
  258. } else {
  259. tTeam.className = inactive;
  260. tInd.className = active;
  261. }
  262. }
  263. function updateMetricUI() {
  264. document.querySelectorAll('.metric-btn').forEach(btn => {
  265. btn.className = 'metric-btn bg-white text-gray-500 border border-gray-200 px-3 py-1 rounded-full text-xs font-bold shrink-0 active:bg-gray-50 transition-colors';
  266. });
  267. const activeBtn = document.getElementById(`metric-${state.currentMetric}`);
  268. if(activeBtn) activeBtn.className = 'metric-btn bg-primary text-white px-3 py-1 rounded-full text-xs shadow-md shadow-blue-100 font-bold shrink-0 relative overflow-visible transition-colors';
  269. }
  270. // Drawer
  271. window.openDrawer = function() {
  272. document.getElementById('drawer-backdrop').classList.remove('hidden');
  273. document.getElementById('drawer').classList.remove('translate-y-full');
  274. };
  275. window.closeDrawer = function() {
  276. document.getElementById('drawer-backdrop').classList.add('hidden');
  277. document.getElementById('drawer').classList.add('translate-y-full');
  278. };
  279. function renderDrawer() {
  280. const drawerContent = document.querySelector('#drawer .space-y-6');
  281. // We can populate map options here
  282. }
  283. window.triggerJump = function(target) {
  284. const map = state.mapList.find(m => m.mapName === target);
  285. if(map) {
  286. if(state.ocaId !== map.ocaId) {
  287. state.ocaId = map.ocaId;
  288. uni.setStorageSync(`rank-tpl-style3-map-${state.ecId}`, state.ocaId);
  289. matchRsDetailQuery();
  290. fetchRankDetail();
  291. }
  292. } else {
  293. // If it's '高德地图' etc.
  294. if(target.includes('地图')) {
  295. alert("正在打开导航:" + target);
  296. } else {
  297. // Assume map name
  298. const map2 = state.mapList.find(m => m.mapName === target);
  299. if(map2) {
  300. state.ocaId = map2.ocaId;
  301. uni.setStorageSync(`rank-tpl-style3-map-${state.ecId}`, state.ocaId);
  302. matchRsDetailQuery();
  303. fetchRankDetail();
  304. }
  305. }
  306. }
  307. closeDrawer();
  308. };
  309. // Info Modal
  310. window.openInfoModal = function() { document.getElementById('infoModal').classList.remove('hidden'); }
  311. window.closeInfoModal = function() { document.getElementById('infoModal').classList.add('hidden'); }
  312. // Edit Modal
  313. window.openEditModal = function() {
  314. const editNameInput = document.getElementById('editNameInput');
  315. const selectedTeamText = document.getElementById('selectedTeamText');
  316. editNameInput.value = state.userInfo.nickName;
  317. selectedTeamText.innerHTML = `<i class="fas fa-user-friends text-primary"></i> ${state.userInfo.coiName || '选择战队'}`;
  318. document.getElementById('editProfileModal').classList.remove('hidden');
  319. // Populate teams if empty (reuse mapList or fetch teams if different API?
  320. // Actually teams come from apiOnlineMcSignUpDetail which we might need to call if we want to allow changing teams.
  321. // For simplicity, assuming mapList contains teams or we need to fetch cois.
  322. // The signup page fetches apiOnlineMcSignUpDetail. Let's do that here too if needed.
  323. getOnlineMcSignUpDetail();
  324. };
  325. window.closeEditModal = function() {
  326. document.getElementById('editProfileModal').classList.add('hidden');
  327. window.closeDropdown();
  328. };
  329. // Fetch teams for dropdown
  330. function getOnlineMcSignUpDetail() {
  331. uni.request({
  332. url: window.apiOnlineMcSignUpDetail,
  333. header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
  334. method: "POST",
  335. data: { mcId: state.mcId },
  336. success: (res) => {
  337. const data = res.data.data;
  338. if(data) {
  339. const teams = data.coiRs;
  340. renderEditDropdown(teams);
  341. }
  342. }
  343. });
  344. }
  345. function renderEditDropdown(teams) {
  346. const ul = document.querySelector('#dropdownMenu ul');
  347. if(!ul) return;
  348. let html = '';
  349. teams.forEach(team => {
  350. html += `<li onclick="selectEditOption('${team.coiId}', '${team.coiName}')" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between border-b border-gray-50"><span class="font-bold flex items-center gap-2"><i class="fas fa-user-friends text-blue-200"></i> ${team.coiName}</span></li>`;
  351. });
  352. ul.innerHTML = html;
  353. }
  354. window.toggleDropdown = function(event) {
  355. event.stopPropagation();
  356. const dropdownMenu = document.getElementById('dropdownMenu');
  357. const dropdownArrow = document.getElementById('dropdownArrow');
  358. if (dropdownMenu.classList.contains('hidden')) {
  359. dropdownMenu.classList.remove('hidden');
  360. setTimeout(() => dropdownMenu.classList.add('dropdown-enter-active'), 10);
  361. dropdownArrow.style.transform = 'rotate(180deg)';
  362. } else { window.closeDropdown(); }
  363. };
  364. window.closeDropdown = function() {
  365. const dropdownMenu = document.getElementById('dropdownMenu');
  366. const dropdownArrow = document.getElementById('dropdownArrow');
  367. dropdownMenu.classList.remove('dropdown-enter-active');
  368. dropdownArrow.style.transform = 'rotate(0deg)';
  369. setTimeout(() => dropdownMenu.classList.add('hidden'), 200);
  370. };
  371. window.closeDropdownOnClickOutside = function(event) {
  372. if (!document.getElementById('editProfileModal').classList.contains('hidden')) {
  373. const btn = document.getElementById('dropdownBtn');
  374. const menu = document.getElementById('dropdownMenu');
  375. if (menu && !menu.contains(event.target) && btn && !btn.contains(event.target)) {
  376. window.closeDropdown();
  377. }
  378. }
  379. };
  380. let editingCoiId = 0;
  381. window.selectEditOption = function(id, name) {
  382. editingCoiId = id;
  383. document.getElementById('selectedTeamText').innerHTML = `<i class="fas fa-user-friends text-primary"></i> ${name}`;
  384. window.closeDropdown();
  385. };
  386. window.saveProfile = function() {
  387. const newName = document.getElementById('editNameInput').value;
  388. if(!newName) { uni.showToast({title:'请输入昵称',icon:'none'}); return; }
  389. // Call API
  390. uni.request({
  391. url: window.apiOnlineMcSignUp,
  392. header: { "Content-Type": "application/x-www-form-urlencoded", "token": state.token },
  393. method: "POST",
  394. data: {
  395. mcId: state.mcId,
  396. coiId: editingCoiId || 0, // If 0, maybe keep old? API requires valid ID usually.
  397. selectTeam: 0,
  398. nickName: newName
  399. },
  400. success: (res) => {
  401. if(window.checkResCode(res)) {
  402. uni.showToast({title:'修改成功',icon:'none'});
  403. window.closeEditModal();
  404. matchRsDetailQuery(); // Refresh
  405. }
  406. }
  407. });
  408. };
  409. document.addEventListener('DOMContentLoaded', () => {
  410. initRankListPage();
  411. });