rankOverview.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>S3 赛事总览</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
  9. <style>
  10. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;900&display=swap');
  11. body { font-family: 'Noto Sans SC', sans-serif; }
  12. .page-top {
  13. background-image: url('./static/backgroud/top_bg_egg2.png');
  14. background-repeat: no-repeat;
  15. background-position: center;
  16. background-size: cover;
  17. min-height: 270px;
  18. }
  19. .logo-bg {
  20. background-image: url('./static/logo/sddx.png');
  21. background-repeat: no-repeat;
  22. background-position: center;
  23. background-size: contain;
  24. width: 80px; height: 80px; margin-top: 10px;
  25. }
  26. .mid-card {
  27. width: 90%;
  28. background: #ffffff;
  29. border-radius: 9px;
  30. box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.13);
  31. position: relative;
  32. z-index: 20;
  33. margin-left: auto;
  34. margin-right: auto;
  35. }
  36. .mid-line { width: 1px; height: 40px; background-color: #e6e6e6; }
  37. .path-item { display: flex; align-items: center; justify-content: space-between; background: white; padding: 15px; margin-bottom: 10px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); cursor: pointer; border: 2px solid transparent; }
  38. .path-item.selected { border-color: #81cd00; background-color: #f9fff0; }
  39. </style>
  40. </head>
  41. <body class="bg-gray-50 min-h-screen flex flex-col relative pb-20">
  42. <!-- Top Section -->
  43. <div class="page-top w-full flex flex-col items-center pt-8 pb-12">
  44. <!-- Navbar -->
  45. <div class="w-full flex justify-between items-center px-4">
  46. <button onclick="handleBack()" class="bg-black/20 backdrop-blur-sm p-2 rounded-full w-9 h-9 flex items-center justify-center text-white active:scale-90 transition">
  47. <i class="fas fa-chevron-left"></i>
  48. </button>
  49. <h1 id="mc-name" class="text-gray-800 text-lg font-bold">赛事名称</h1>
  50. <button onclick="handleInfo()" class="bg-black/20 backdrop-blur-sm px-3 py-1.5 rounded-full text-xs font-semibold flex items-center gap-1 text-white active:scale-90 transition">
  51. <i class="fas fa-question-circle"></i> 说明
  52. </button>
  53. </div>
  54. <div class="flex flex-col items-center gap-2 mt-4">
  55. <div class="logo-bg"></div>
  56. <p id="sub-title" class="text-yellow-400 text-xl font-bold drop-shadow-md">活动时间</p>
  57. </div>
  58. </div>
  59. <!-- Mid Section -->
  60. <div id="mid-type-0" class="mid-card -mt-10 flex flex-col p-4">
  61. <div class="flex justify-center items-center mb-3 relative">
  62. <select id="map-select-0" onchange="handleMapChange(this.value)" class="bg-transparent text-gray-500 font-medium text-sm outline-none cursor-pointer">
  63. </select>
  64. <button onclick="handleHelp()" class="absolute right-0 text-red-800 text-xs font-medium">帮助</button>
  65. </div>
  66. <div class="flex justify-around items-center mb-4 text-xs font-medium text-red-900">
  67. <div class="flex items-center gap-1"><span id="nick-name-0">昵称</span></div>
  68. <span id="coi-name-0">组织</span>
  69. <span id="regroup-btn-0" class="text-gray-400 cursor-pointer hidden" onclick="handleRegroup()">修改</span>
  70. </div>
  71. <div class="flex justify-around items-center text-center">
  72. <div><div class="text-xl font-black" id="stat-num-0">--</div><div class="text-gray-400 text-xs">场次</div></div>
  73. <div class="mid-line"></div>
  74. <div><div class="text-xl font-black" id="stat-cp-0">--</div><div class="text-gray-400 text-xs">打点数</div></div>
  75. <div class="mid-line"></div>
  76. <div><div class="text-xl font-black" id="stat-rank-0">--</div><div class="text-gray-400 text-xs">个人排名</div></div>
  77. </div>
  78. </div>
  79. <div id="mid-type-1" class="mid-card -mt-16 flex flex-col p-4 hidden">
  80. <div class="flex justify-center items-center mb-3 relative">
  81. <select id="map-select-1" onchange="handleMapChange(this.value)" class="bg-transparent text-gray-500 font-medium text-sm outline-none cursor-pointer"></select>
  82. <div class="absolute right-0 flex gap-3 text-xs font-medium text-red-800">
  83. <button id="regroup-btn-1" class="hidden" onclick="handleRegroup()">修改</button>
  84. <button onclick="handleHelp()">帮助</button>
  85. </div>
  86. </div>
  87. <div class="flex justify-between items-center mb-4 text-sm font-medium text-gray-500 px-2">
  88. <div class="flex items-center gap-1"><span id="nick-name-1" class="text-red-900">昵称</span></div>
  89. <span id="coi-name-1" class="text-red-900 truncate max-w-[100px]">组织</span>
  90. <span>场次:<span id="stat-num-1">--</span></span>
  91. </div>
  92. <div class="flex justify-around items-center text-center">
  93. <div><div class="text-xl font-black text-pink-600" id="stat-point-1">--</div><div class="text-gray-400 text-xs">百味豆</div></div>
  94. <div class="mid-line"></div>
  95. <div><div class="text-xl font-black" id="stat-dist-1">--</div><div class="text-gray-400 text-xs">里程 km</div></div>
  96. <div class="mid-line"></div>
  97. <div><div class="text-xl font-black" id="stat-cp-1">--</div><div class="text-gray-400 text-xs">打点数</div></div>
  98. <div class="mid-line"></div>
  99. <div><div class="text-xl font-black" id="stat-pace-1">--</div><div class="text-gray-400 text-xs">最快配速</div></div>
  100. </div>
  101. </div>
  102. <!-- Main Content: Path List -->
  103. <div class="w-full px-4 mt-6">
  104. <h3 class="font-bold text-gray-800 mb-3">选择比赛路线</h3>
  105. <div id="path-list-container" class="flex flex-col gap-3"></div>
  106. </div>
  107. <!-- Bottom Action Bar -->
  108. <div class="fixed bottom-0 w-full bg-white border-t border-gray-100 p-4 z-40">
  109. <button onclick="handleStartGame()" class="w-full bg-[#81cd00] text-white font-bold py-3 rounded-full shadow-lg active:scale-95 transition-transform">
  110. 开始比赛
  111. </button>
  112. </div>
  113. <!-- Info Modal -->
  114. <div id="infoModal" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
  115. <div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeModal('infoModal')"></div>
  116. <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[90%] bg-white rounded-2xl p-6 shadow-2xl max-h-[80vh] overflow-y-auto">
  117. <button onclick="closeModal('infoModal')" class="absolute top-2 right-2 text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
  118. <div id="info-modal-content" class="text-sm"></div>
  119. </div>
  120. </div>
  121. <script src="./js/utils.js"></script>
  122. <script src="./js/bridge.js"></script>
  123. <script src="./js/api.js"></script>
  124. <script>
  125. const STATE = {
  126. ecId: 0,
  127. token: '',
  128. mcId: 0,
  129. mcType: 0,
  130. mcName: '',
  131. coiName: '',
  132. beginSecond: 0,
  133. endSecond: 0,
  134. ocaId: 0,
  135. nickName: '',
  136. mapList: [],
  137. pathList: [],
  138. configParam: { subTitle: '', midType: 0 },
  139. mapKey: 'rank-tpl-style3-map',
  140. mcState: 0,
  141. allowMcSignUp: false,
  142. stats: {
  143. regionTotalNum: 0,
  144. regionTotalCp: 0,
  145. regionTotalCpRankNum: 0,
  146. regionTotalSysPoint: 0,
  147. regionTotalDistance: 0,
  148. regionFastPace: 0
  149. }
  150. };
  151. function injectCss(css) {
  152. if (!css) return;
  153. const style = document.createElement('style');
  154. style.innerHTML = css;
  155. document.head.appendChild(style);
  156. }
  157. window.onload = function() {
  158. STATE.token = Tools.getQueryParam('token') || '';
  159. STATE.ecId = Tools.getQueryParam('id') || 0;
  160. STATE.mapKey = `${STATE.mapKey}-${STATE.ecId}`;
  161. const cachedMap = localStorage.getItem(STATE.mapKey);
  162. if (cachedMap) STATE.ocaId = cachedMap;
  163. if (window.API) {
  164. API.setToken(STATE.token);
  165. if (Tools.getQueryParam('env') === 'mock') {
  166. API.init({ useMock: true });
  167. if (!STATE.ecId) STATE.ecId = 'mock_id';
  168. }
  169. }
  170. loadCardConfig();
  171. matchRsDetailQuery();
  172. };
  173. function loadCardConfig() {
  174. if (!window.API) return;
  175. API.getCardConfig(STATE.ecId, 'rankOverview').then(res => {
  176. let cfg = res;
  177. if (res && res.configJson) {
  178. try { cfg = JSON.parse(res.configJson); } catch (e) { console.warn('config parse fail', e); }
  179. }
  180. if (!cfg) return;
  181. if (cfg.common && cfg.common.css) injectCss(cfg.common.css);
  182. const pageCfg = cfg.rankOverview || cfg.rank_overview || cfg;
  183. if (pageCfg && pageCfg.css) injectCss(pageCfg.css);
  184. if (pageCfg && pageCfg.pathList) STATE.pathList = pageCfg.pathList;
  185. if (pageCfg && pageCfg.pathListStyle && pageCfg.pathListStyle.showLine === false) {
  186. // 保留占位,样式已在 pathList 中体现
  187. }
  188. if (pageCfg && pageCfg.param) STATE.configParam = Object.assign(STATE.configParam, pageCfg.param);
  189. document.getElementById('sub-title').innerText = STATE.configParam.subTitle || '';
  190. });
  191. }
  192. function matchRsDetailQuery() {
  193. if (!window.API) return;
  194. const payload = { ecId: STATE.ecId };
  195. if (STATE.ocaId) payload.ocaId = STATE.ocaId; // 仅在有值时传入
  196. API.getMatchRsDetail(payload.ecId, payload.ocaId).then(res => {
  197. if (!res) return;
  198. STATE.mcType = res.mcType;
  199. STATE.mcId = res.mcId;
  200. STATE.mcName = res.mcName;
  201. STATE.coiName = res.coiName;
  202. STATE.beginSecond = res.beginSecond;
  203. STATE.endSecond = res.endSecond;
  204. STATE.nickName = res.nickName;
  205. STATE.stats.regionTotalNum = res.regionTotalNum;
  206. STATE.stats.regionTotalCp = res.regionTotalCp;
  207. STATE.stats.regionTotalCpRankNum = res.regionTotalCpRankNum;
  208. STATE.stats.regionTotalSysPoint = res.regionTotalSysPoint;
  209. STATE.stats.regionTotalDistance = res.regionTotalDictance;
  210. STATE.stats.regionFastPace = res.regionFastPace;
  211. STATE.mcState = Tools.checkMcState(STATE.beginSecond, STATE.endSecond);
  212. document.getElementById('mc-name').innerText = STATE.mcName || '赛事';
  213. document.getElementById('sub-title').innerText = STATE.configParam.subTitle || Tools.fmtMcTime2(STATE.beginSecond, STATE.endSecond);
  214. updateMidStats();
  215. isAllowMcSignUp();
  216. mapListQuery();
  217. });
  218. }
  219. function isAllowMcSignUp() {
  220. if (!window.API) return;
  221. API.isAllowMcSignUp(STATE.ecId).then(res => {
  222. if (res) STATE.allowMcSignUp = res.allowSignUp;
  223. updateMidStats();
  224. });
  225. }
  226. function mapListQuery() {
  227. if (!window.API || !STATE.mcId) return;
  228. API.getMapList(STATE.mcId).then(res => {
  229. if (res && res.length > 0) {
  230. STATE.mapList = res;
  231. if (!STATE.ocaId) STATE.ocaId = res[0].ocaId || res[0].mapId || 0;
  232. renderMapSelect();
  233. renderPathList();
  234. localStorage.setItem(STATE.mapKey, STATE.ocaId);
  235. }
  236. });
  237. }
  238. function renderMapSelect() {
  239. const midType = STATE.configParam.midType || 0;
  240. const select = document.getElementById(`map-select-${midType}`);
  241. select.innerHTML = '';
  242. STATE.mapList.forEach(map => {
  243. const opt = document.createElement('option');
  244. opt.value = map.ocaId;
  245. opt.innerText = map.mapName || '地图';
  246. if (map.ocaId == STATE.ocaId) opt.selected = true;
  247. select.appendChild(opt);
  248. });
  249. }
  250. function renderPathList() {
  251. const container = document.getElementById('path-list-container');
  252. container.innerHTML = '';
  253. const list = STATE.pathList && Object.keys(STATE.pathList).length > 0
  254. ? Object.values(STATE.pathList).flat()
  255. : STATE.mapList.map(m => ({ path: { ocaId: m.ocaId, mcType: STATE.mcType }, pathImg: m.mapPic, navImg: '', type: 3, text: m.mapName }));
  256. list.forEach(item => {
  257. const ocaId = item.path?.ocaId || item.ocaId || 0;
  258. const name = item.text || item.pathName || item.mapName || '路线';
  259. const div = document.createElement('div');
  260. div.className = `path-item ${ocaId == STATE.ocaId ? 'selected' : ''}`;
  261. div.innerHTML = `
  262. <div class="flex items-center gap-3">
  263. <div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-500 font-bold">
  264. <i class="fas fa-map-marker-alt"></i>
  265. </div>
  266. <div>
  267. <div class="font-bold text-gray-800">${name}</div>
  268. <div class="text-xs text-gray-400">点击选择此路线</div>
  269. </div>
  270. </div>
  271. ${ocaId == STATE.ocaId ? '<i class="fas fa-check-circle text-green-500 text-xl"></i>' : ''}
  272. `;
  273. div.onclick = () => handleMapChange(ocaId);
  274. container.appendChild(div);
  275. });
  276. }
  277. function updateMidStats() {
  278. const midType = STATE.configParam.midType || 0;
  279. document.getElementById('mid-type-0').classList.toggle('hidden', midType !== 0);
  280. document.getElementById('mid-type-1').classList.toggle('hidden', midType !== 1);
  281. document.getElementById(`nick-name-${midType}`).innerText = STATE.nickName || '昵称';
  282. document.getElementById(`coi-name-${midType}`).innerText = STATE.coiName || '组织';
  283. if (midType === 0) {
  284. document.getElementById('stat-num-0').innerText = STATE.stats.regionTotalNum ?? '--';
  285. document.getElementById('stat-cp-0').innerText = STATE.stats.regionTotalCp ?? '--';
  286. document.getElementById('stat-rank-0').innerText = STATE.stats.regionTotalCpRankNum ?? '--';
  287. const btn = document.getElementById('regroup-btn-0');
  288. if (STATE.mcState === 1 && STATE.allowMcSignUp) btn.classList.remove('hidden'); else btn.classList.add('hidden');
  289. } else {
  290. document.getElementById('stat-num-1').innerText = STATE.stats.regionTotalNum ?? '--';
  291. document.getElementById('stat-point-1').innerText = STATE.stats.regionTotalSysPoint ?? '--';
  292. document.getElementById('stat-dist-1').innerText = Tools.fmtDistance(STATE.stats.regionTotalDistance) ?? '--';
  293. document.getElementById('stat-cp-1').innerText = STATE.stats.regionTotalCp ?? '--';
  294. document.getElementById('stat-pace-1').innerText = Tools.convertSecondsToHMS(STATE.stats.regionFastPace, 2);
  295. const btn = document.getElementById('regroup-btn-1');
  296. if (STATE.mcState === 1 && STATE.allowMcSignUp) btn.classList.remove('hidden'); else btn.classList.add('hidden');
  297. }
  298. }
  299. function handleMapChange(newOcaId) {
  300. if (newOcaId != STATE.ocaId) {
  301. STATE.ocaId = newOcaId;
  302. localStorage.setItem(STATE.mapKey, STATE.ocaId);
  303. matchRsDetailQuery();
  304. renderPathList();
  305. }
  306. }
  307. function handleBack() {
  308. const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
  309. Bridge.appAction(`./ranklist.html${qs}`);
  310. }
  311. function handleInfo() {
  312. document.getElementById('info-modal-content').innerHTML = `<p>${STATE.configParam.subTitle || '暂无说明'}</p>`;
  313. document.getElementById('infoModal').classList.remove('hidden');
  314. }
  315. function handleHelp() {
  316. handleInfo();
  317. }
  318. function handleRegroup() {
  319. const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
  320. Bridge.appAction(`./signup.html${qs}&from=rankOverview`);
  321. }
  322. function handleStartGame() {
  323. if (STATE.mcState === 1) {
  324. Bridge.appAction(`action://to_detail/?id=${STATE.ocaId}&matchType=${STATE.mcType}`);
  325. } else if (STATE.mcState === 0) {
  326. Tools.showToast('比赛尚未开始');
  327. } else {
  328. Tools.showToast('比赛已结束');
  329. }
  330. }
  331. function closeModal(id) {
  332. document.getElementById(id).classList.add('hidden');
  333. }
  334. </script>
  335. </body>
  336. </html>