rankOverview.html 15 KB

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