detail3.html 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  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, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
  6. <title>亲子月挑战赛</title>
  7. <script src="./bridge.js"></script>
  8. <script src="./api.js"></script>
  9. <script src="./js/multiavatar.min.js"></script>
  10. <link href="./css/all.min.css" rel="stylesheet">
  11. <style>
  12. :root {
  13. --kid-blue: #48dbfb;
  14. --kid-blue-dark: #0abde3;
  15. --kid-yellow: #feca57;
  16. --kid-orange: #ff9f43;
  17. --kid-red: #ff6b6b;
  18. --kid-green: #1dd1a1;
  19. --kid-bg: #f7f1e3;
  20. --kid-text: #576574;
  21. --footer-bg: #ff6b6b;
  22. }
  23. * {
  24. box-sizing: border-box;
  25. margin: 0;
  26. padding: 0;
  27. font-family: 'Varela Round', 'Segoe UI', 'Microsoft YaHei', sans-serif;
  28. -webkit-tap-highlight-color: transparent;
  29. user-select: none;
  30. }
  31. body {
  32. background: var(--kid-bg);
  33. background-image: radial-gradient(#dfe6e9 1px, transparent 1px);
  34. background-size: 20px 20px;
  35. width: 100%;
  36. height: 100vh;
  37. overflow: hidden;
  38. display: flex;
  39. flex-direction: column;
  40. }
  41. /* 顶部 Header:高度减小,更紧凑 */
  42. .header-area {
  43. height: 250px; /* 原 290px -> 250px */
  44. background: url('./bgkt.jpg') center/cover no-repeat;
  45. padding: 15px 20px;
  46. padding-top: max(20px, env(safe-area-inset-top));
  47. color: white;
  48. border-bottom-left-radius: 35px; border-bottom-right-radius: 35px;
  49. position: relative; flex-shrink: 0;
  50. z-index: 1;
  51. box-shadow: 0 8px 0 rgba(0,0,0,0.05);
  52. overflow: hidden;
  53. }
  54. .header-area::after {
  55. content: '';
  56. position: absolute;
  57. top: 0; left: 0; width: 100%; height: 100%;
  58. background: linear-gradient(to bottom, rgba(72, 219, 251, 0.4) 0%, rgba(255,255,255,0.1) 100%);
  59. border-bottom-left-radius: 35px; border-bottom-right-radius: 35px;
  60. z-index: -1;
  61. }
  62. .nav-bar {
  63. display: flex; justify-content: space-between; align-items: center;
  64. margin-top: 0px;
  65. position: relative;
  66. z-index: 20;
  67. }
  68. .icon-btn {
  69. width: 32px; height: 32px; /* 略微缩小按钮 */
  70. background: rgba(255,255,255,0.9);
  71. border-radius: 50%; display: flex; align-items: center; justify-content: center;
  72. cursor: pointer;
  73. border: 2px solid white;
  74. box-shadow: 0 3px 0 rgba(0,0,0,0.1);
  75. color: var(--kid-blue-dark);
  76. font-size: 14px;
  77. }
  78. .icon-btn:active { transform: translateY(3px); box-shadow: none; }
  79. .month-select {
  80. font-size: 16px; font-weight: 800;
  81. display: flex; align-items: center; gap: 6px; cursor: pointer;
  82. text-shadow: 1px 1px 0 rgba(0,0,0,0.1);
  83. background: rgba(255,255,255,0.25); padding: 4px 12px; border-radius: 18px;
  84. backdrop-filter: blur(5px);
  85. border: 1px solid rgba(255,255,255,0.5);
  86. }
  87. /* 云朵动画保持不变 */
  88. .cloud {
  89. position: absolute;
  90. background: #fff;
  91. border-radius: 50%;
  92. z-index: 0;
  93. opacity: 0.9;
  94. box-shadow: 0 5px 10px rgba(0,0,0,0.05);
  95. pointer-events: none;
  96. }
  97. .c1 {
  98. width: 70px; height: 70px; top: 15%; left: -10px;
  99. box-shadow: 25px 8px 0 #fff, -20px 12px 0 #fff;
  100. animation: floatCloudHeader1 8s infinite ease-in-out alternate;
  101. }
  102. .c2 {
  103. width: 50px; height: 50px; top: 25%; right: 5%;
  104. box-shadow: 15px 6px 0 #fff, -10px 8px 0 #fff;
  105. animation: floatCloudHeader2 6s infinite ease-in-out alternate-reverse;
  106. }
  107. .c3 {
  108. width: 40px; height: 40px; top: 40%; left: 40%;
  109. opacity: 0.6;
  110. box-shadow: 12px 5px 0 #fff, -8px 6px 0 #fff;
  111. animation: floatCloudHeader3 10s infinite linear;
  112. }
  113. @keyframes floatCloudHeader1 { from { transform: translate(0, 0); } to { transform: translate(30px, 10px); } }
  114. @keyframes floatCloudHeader2 { from { transform: translate(0, 0); } to { transform: translate(-25px, 15px); } }
  115. @keyframes floatCloudHeader3 { 0% { transform: translateX(-20px); } 50% { transform: translateX(20px); } 100% { transform: translateX(-20px); } }
  116. /* 领奖台容器:高度压缩 */
  117. .podium-wrap {
  118. height: 130px; /* 原 160px -> 130px */
  119. display: flex; justify-content: center; align-items: flex-end;
  120. margin-top: -145px; /* 上移调整 */
  121. position: relative;
  122. z-index: 10;
  123. padding-bottom: 0px;
  124. }
  125. .p-col { display: flex; flex-direction: column; align-items: center; width: 30%; position: relative;}
  126. .p-2 { z-index: 2; margin-right: -10px; }
  127. .p-1 { z-index: 3; margin-bottom: 10px; }
  128. .p-3 { z-index: 1; margin-left: -10px; }
  129. /* 头像压缩 */
  130. .p-img { width: 44px; height: 44px; border-radius: 50%; border: 3px solid white; background: #eee; margin-bottom: -12px; position: relative; z-index: 2; overflow: hidden; box-shadow: 0 3px 5px rgba(0,0,0,0.1);}
  131. .p-1 .p-img { width: 60px; height: 60px; margin-bottom: -15px;}
  132. .p-img img { width: 100%; height: 100%; object-fit: cover; }
  133. .crown {
  134. position: absolute; top: -24px; color: var(--kid-yellow); font-size: 28px;
  135. filter: drop-shadow(1px 1px 0 rgba(0,0,0,0.2));
  136. animation: crownFloat 2s ease-in-out infinite;
  137. z-index: 20;
  138. }
  139. /* 领奖台底座:高度压缩 */
  140. .p-box {
  141. width: 100%; text-align: center;
  142. padding-top: 15px; padding-bottom: 5px;
  143. border-radius: 12px 12px 0 0;
  144. color: white;
  145. box-shadow: 0 4px 10px rgba(0,0,0,0.1);
  146. border: 2px solid rgba(255,255,255,0.6);
  147. border-bottom: none;
  148. }
  149. /* 第一名高度 105 -> 85 */
  150. .p-1 .p-box { height: 85px; background: var(--kid-yellow); padding-top: 20px; }
  151. /* 第二名高度 85 -> 65 */
  152. .p-2 .p-box { height: 65px; background: var(--kid-blue); }
  153. /* 第三名高度 75 -> 55 */
  154. .p-3 .p-box { height: 55px; background: var(--kid-orange); }
  155. .p-name { font-size: 11px; margin-bottom: 2px; font-weight: bold; text-shadow: 1px 1px 0 rgba(0,0,0,0.1); white-space: nowrap; overflow: hidden; max-width: 60px; margin: 0 auto 2px auto; text-overflow: ellipsis;}
  156. .p-score { font-size: 13px; font-weight: 900; background: rgba(0,0,0,0.1); display: inline-block; padding: 1px 6px; border-radius: 6px;}
  157. /* 列表容器 */
  158. .list-container {
  159. flex: 1;
  160. background: transparent;
  161. margin-top: 5px;
  162. display: flex; flex-direction: column;
  163. overflow: hidden;
  164. position: relative; z-index: 8;
  165. }
  166. /* 仪表盘卡片:极致压缩,变为条状 */
  167. .dashboard-card {
  168. margin: 0 15px 10px 15px;
  169. background: #fff;
  170. border-radius: 15px; /* 圆角减小 */
  171. padding: 6px 12px; /* 内边距大幅减小 */
  172. border: 2px solid var(--kid-blue); /* 边框减细 */
  173. box-shadow: 0 3px 0 rgba(72, 219, 251, 0.3);
  174. position: relative;
  175. flex-shrink: 0;
  176. display: flex;
  177. align-items: center;
  178. justify-content: space-between;
  179. }
  180. /* 左侧标题区域 */
  181. .dash-header {
  182. display: flex; flex-direction: column; justify-content: center; align-items: flex-start;
  183. margin-bottom: 0; margin-right: 10px; width: 80px; flex-shrink: 0;
  184. }
  185. .dash-title { font-size: 11px; font-weight: 800; display: flex; align-items: center; gap: 4px; color: var(--kid-text); margin-bottom: 2px;}
  186. .dash-icon { color: var(--kid-orange); font-size: 12px; }
  187. .dash-badge {
  188. background: var(--kid-blue); color: white;
  189. font-size: 9px; font-weight: 800; padding: 2px 6px;
  190. border-radius: 8px; width: fit-content;
  191. }
  192. .dash-badge.completed { background: var(--kid-green); }
  193. /* 右侧进度条区域 */
  194. .trophy-track-container {
  195. position: relative; display: flex; justify-content: space-between; align-items: center;
  196. flex: 1; padding: 0 5px;
  197. }
  198. .track-line-bg { position: absolute; top: 50%; left: 5px; right: 5px; height: 4px; background: #eee; border-radius: 4px; transform: translateY(-50%); z-index: 0; }
  199. .track-line-active {
  200. position: absolute; top: 50%; left: 5px; width: 25%; height: 4px;
  201. background: linear-gradient(90deg, var(--kid-yellow), var(--kid-orange));
  202. border-radius: 4px; transform: translateY(-50%); z-index: 0;
  203. transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
  204. }
  205. /* 图标压缩 */
  206. .trophy-item {
  207. position: relative; z-index: 1;
  208. width: 20px; height: 20px; /* 缩小图标 */
  209. background: #fff; border: 2px solid #eee; border-radius: 50%;
  210. display: flex; justify-content: center; align-items: center;
  211. color: #b2bec3; font-size: 9px; transition: all 0.4s;
  212. }
  213. .trophy-item.active { background: #fff; border-color: var(--kid-green); color: var(--kid-green); transform: scale(1.1); }
  214. .trophy-item.final { width: 26px; height: 26px; font-size: 11px; border-color: var(--kid-yellow); }
  215. .trophy-item.final.active { background: var(--kid-yellow); color: white; border-color: #fff; transform: scale(1.2); box-shadow: 0 0 0 2px var(--kid-yellow); animation: pulseTrophy 2s infinite; }
  216. /* 滚动区域 */
  217. .rank-scroll-area {
  218. flex: 1;
  219. background: white;
  220. border-radius: 20px 20px 0 0;
  221. padding: 0 15px 120px 15px;
  222. overflow-y: auto;
  223. -webkit-overflow-scrolling: touch;
  224. box-shadow: 0 -4px 8px rgba(0,0,0,0.03);
  225. }
  226. .tabs {
  227. display: flex; justify-content: center; gap: 10px;
  228. position: sticky; top: 0; background: white; z-index: 9;
  229. padding-top: 12px; padding-bottom: 8px;
  230. }
  231. .tab { padding: 5px 16px; border-radius: 15px; font-size: 12px; color: #b2bec3; background: #f1f2f6; cursor: pointer; transition: 0.2s; font-weight: bold; border: 2px solid transparent;}
  232. .tab.active { background: var(--kid-blue); color: white; border-color: var(--kid-blue-dark); box-shadow: 0 2px 0 var(--kid-blue-dark); transform: translateY(-1px);}
  233. .list-item { display: flex; align-items: center; padding: 10px 0; border-bottom: 2px dashed #f1f2f6; }
  234. .rank { width: 25px; text-align: center; font-weight: 900; color: var(--kid-blue-dark); font-style: italic; font-size: 14px;}
  235. .avatar { width: 36px; height: 36px; border-radius: 50%; margin: 0 10px; background: #eee; overflow: hidden; border: 2px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.1);}
  236. .avatar img { width: 100%; height: 100%; object-fit: cover;}
  237. .info { flex: 1; }
  238. .name { font-size: 13px; color: var(--kid-text); font-weight: 800; }
  239. .team { font-size: 10px; color: #a4b0be; display: flex; align-items: center; gap: 4px; background: #f1f2f6; display: inline-block; padding: 1px 5px; border-radius: 5px; margin-top: 2px;}
  240. .score { font-size: 15px; font-weight: 900; color: var(--kid-orange); }
  241. /* 底部我的排名 */
  242. .my-rank-bar {
  243. position: fixed; bottom: 0; left: 0; width: 100%;
  244. background: var(--footer-bg); color: white;
  245. display: flex; align-items: center;
  246. padding: 0 15px;
  247. padding-top: 8px;
  248. padding-bottom: calc(8px + env(safe-area-inset-bottom));
  249. box-sizing: border-box;
  250. border-radius: 20px 20px 0 0;
  251. box-shadow: 0 -4px 15px rgba(255, 107, 107, 0.3); z-index: 99;
  252. border-top: 3px solid rgba(255,255,255,0.2);
  253. }
  254. @supports (-webkit-touch-callout: none) {
  255. .my-rank-bar { padding-top: 6px !important; padding-bottom: env(safe-area-inset-bottom) !important; }
  256. }
  257. .my-rank-bar .rank { font-size: 18px; color: white; text-shadow: 1px 1px 0 rgba(0,0,0,0.2); }
  258. .my-rank-bar .avatar { width: 34px; height: 34px; border: 2px solid white; box-shadow: none; }
  259. .my-rank-bar .name { font-size: 14px; color: white; }
  260. .my-score { font-size: 18px; font-weight: 900; color: #fff !important; margin-left: auto; text-shadow: 1px 1px 0 rgba(0,0,0,0.2); }
  261. .dropdown { position: absolute; top: 60px; left: 50%; transform: translateX(-50%); width: 200px; background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: none; flex-direction: column; overflow: hidden; z-index: 200; border: 3px solid var(--kid-blue); }
  262. .dropdown.show { display: flex; animation: dropIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
  263. .dd-item { padding: 12px; color: var(--kid-text); font-size: 14px; text-align: center; border-bottom: 2px dashed #f1f2f6; cursor: pointer; font-weight: bold;}
  264. .dd-active { color: var(--kid-blue-dark); background: #e0f7fa; }
  265. .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 999; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; backdrop-filter: blur(4px); }
  266. .modal-mask.show { opacity: 1; pointer-events: auto; }
  267. .modal-body { width: 85%; max-width: 300px; background: white; border-radius: 25px; padding: 20px; text-align: center; transform: scale(0.8); transition: transform 0.3s; border: 4px solid var(--kid-blue); box-shadow: 0 15px 30px rgba(0,0,0,0.2); }
  268. .modal-mask.show .modal-body { transform: scale(1); }
  269. .m-close { background: var(--kid-yellow); color: #fff; padding: 8px 20px; border-radius: 20px; border: none; font-weight: 900; cursor: pointer; margin-top: 15px; box-shadow: 0 3px 0 #f39c12; }
  270. .m-close:active { transform: translateY(3px); box-shadow: none; }
  271. .rule-box { text-align: left; background: #fff8e1; border: 2px dashed #ffeaa7; border-radius: 12px; padding: 12px; color: #d35400; font-size: 13px; line-height: 1.5; }
  272. .demo-section { margin-top: 15px; border-top: 2px dashed #eee; padding-top: 10px; }
  273. .demo-btn { background: #f1f2f6; border: none; padding: 5px 10px; border-radius: 10px; font-weight: bold; color: #576574; font-size: 12px;}
  274. .loading-mask { position: fixed; inset: 0; background: rgba(255,255,255,0.6); display: none; align-items: center; justify-content: center; z-index: 300; color: var(--kid-blue-dark); font-weight: bold; backdrop-filter: blur(5px); }
  275. .loading-spinner { border: 4px solid #eee; border-top-color: var(--kid-blue); border-radius: 50%; width: 32px; height: 32px; animation: spin 1s linear infinite; margin-right: 10px; }
  276. @keyframes wiggle { 0%, 100% { transform: rotate(-10deg); } 50% { transform: rotate(10deg); } }
  277. @keyframes dropIn { from{opacity:0; transform:translateX(-50%) translateY(-20px) scale(0.9);} to{opacity:1; transform:translateX(-50%) translateY(0) scale(1);} }
  278. @keyframes pulseTrophy { 0% { box-shadow: 0 0 0 0 rgba(254, 202, 87, 0.7); } 100% { box-shadow: 0 0 0 10px rgba(254, 202, 87, 0); } }
  279. @keyframes crownFloat { 0%, 100% { transform: translateY(0) rotate(-5deg); } 50% { transform: translateY(-8px) rotate(5deg); } }
  280. @keyframes spin { to { transform: rotate(360deg); } }
  281. </style>
  282. </head>
  283. <body>
  284. <!-- 头部 -->
  285. <div class="header-area">
  286. <div class="cloud c1"></div>
  287. <div class="cloud c2"></div>
  288. <div class="cloud c3"></div>
  289. <div class="nav-bar">
  290. <div class="icon-btn" onclick="handleBack()"><i class="fa-solid fa-chevron-left" style="color:var(--kid-blue-dark)"></i></div>
  291. <div class="month-select" onclick="toggleDropdown()">
  292. <span id="currentMonthText">11月挑战赛</span> <i class="fa-solid fa-caret-down" style="margin-left:5px"></i>
  293. </div>
  294. <div class="icon-btn" onclick="openModal()"><i class="fa-solid fa-question" style="color:var(--kid-blue-dark)"></i></div>
  295. </div>
  296. </div>
  297. <!-- 领奖台 (高度压缩) -->
  298. <div class="podium-wrap" id="podiumWrap"></div>
  299. <!-- 列表区 -->
  300. <div class="list-container">
  301. <!-- 仪表盘卡片 (高度减半,紧凑布局) -->
  302. <div class="dashboard-card">
  303. <!-- 左侧:文字和徽章 -->
  304. <div class="dash-header">
  305. <div class="dash-title">
  306. <i class="fa-solid fa-map-marked-alt dash-icon"></i> 任务
  307. </div>
  308. <div class="dash-badge" id="dashBadge">1 / 4</div>
  309. </div>
  310. <!-- 右侧:细长的进度条 -->
  311. <div class="trophy-track-container">
  312. <div class="track-line-bg"></div>
  313. <div class="track-line-active" id="progressLine"></div>
  314. <div class="trophy-item active" id="t1"><i class="fa-solid fa-check"></i></div>
  315. <div class="trophy-item" id="t2"><i class="fa-solid fa-lock"></i></div>
  316. <div class="trophy-item" id="t3"><i class="fa-solid fa-lock"></i></div>
  317. <div class="trophy-item final" id="t4"><i class="fa-solid fa-star"></i></div>
  318. </div>
  319. </div>
  320. <!-- 滚动区域 -->
  321. <div class="rank-scroll-area">
  322. <div class="tabs">
  323. <div class="tab active" onclick="switchTab('score', this)">✨ 积分榜</div>
  324. <div class="tab" onclick="switchTab('venue', this)">🚩 场地榜</div>
  325. </div>
  326. <div id="rankList"></div>
  327. </div>
  328. </div>
  329. <div class="my-rank-bar">
  330. <div class="rank" id="myRankNum">--</div>
  331. <div class="avatar" id="myAvatar" style="border:2px solid #fff; overflow:hidden;"></div>
  332. <div class="info"><div class="name" id="myName">我</div><div class="team" id="myTeam">正在加载...</div></div>
  333. <div class="my-score" id="myScoreValue">--</div>
  334. </div>
  335. <div class="dropdown" id="dropdown"></div>
  336. <div class="modal-mask" id="infoModal">
  337. <div class="modal-body">
  338. <h3 style="color:var(--kid-blue-dark); margin-bottom:10px; font-size:18px;">📜 探险指南</h3>
  339. <div id="ruleContent" class="rule-box">
  340. <div class="rule-item"><i class="fa-solid fa-bullseye"></i><div><strong>怎么玩:</strong> 在公园找到打卡点,时间越短越厉害!</div></div>
  341. <div class="rule-item"><i class="fa-solid fa-map-location-dot"></i><div><strong>比什么:</strong> 看看谁探索的公园最多。</div></div>
  342. <div class="rule-item"><i class="fa-solid fa-star"></i><div><strong>拿奖杯:</strong> 完成4次挑战,点亮所有星星!</div></div>
  343. </div>
  344. <div class="demo-section">
  345. <div class="demo-label" style="font-size:11px;color:#aaa;margin-bottom:5px">测试小工具</div>
  346. <div class="demo-controls" style="display:flex;justify-content:center;gap:8px">
  347. <button class="demo-btn" onclick="demoProgress(1);closeModal()">1/4</button>
  348. <button class="demo-btn" onclick="demoProgress(3);closeModal()">3/4</button>
  349. <button class="demo-btn" onclick="demoProgress(4);closeModal()">通关</button>
  350. </div>
  351. </div>
  352. <button class="m-close" onclick="closeModal()">明白啦!</button>
  353. </div>
  354. </div>
  355. <div class="loading-mask" id="loadingMask" style="display: none !important;">
  356. <div class="loading-spinner"></div>
  357. <div>准备出发...</div>
  358. </div>
  359. <script>
  360. // ------------------ JS 逻辑 (保持不变) ------------------
  361. const dropdown = document.getElementById('dropdown');
  362. const rankListEl = document.getElementById('rankList');
  363. const trackLine = document.getElementById('progressLine');
  364. const podiumWrap = document.getElementById('podiumWrap');
  365. const ruleContent = document.getElementById('ruleContent');
  366. const loadingMask = document.getElementById('loadingMask');
  367. const state = {
  368. activeTab: 'score',
  369. scoreList: [],
  370. scoreListRendered: null,
  371. venueList: [],
  372. venueListRendered: null,
  373. months: [],
  374. currentYM: null,
  375. currentMonthData: null,
  376. allMonthsData: null
  377. };
  378. const Logger = {
  379. _isDev: false,
  380. init: function(isDev) { this._isDev = isDev; },
  381. log: function() { if (this._isDev) console.log.apply(console, arguments); },
  382. warn: function() { if (this._isDev) console.warn.apply(console, arguments); },
  383. error: function() { console.error.apply(console, arguments); }
  384. };
  385. function getQuery(name) {
  386. const params = new URLSearchParams(window.location.search);
  387. return params.get(name);
  388. }
  389. function getYearMonth() {
  390. const now = new Date();
  391. const year = parseInt(getQuery('year'), 10) || now.getFullYear();
  392. const month = parseInt(getQuery('month'), 10) || (now.getMonth() + 1);
  393. return { year: year, month: month };
  394. }
  395. function getRecentMonths(count) {
  396. const ym = getYearMonth();
  397. const list = [];
  398. let y = ym.year;
  399. let m = ym.month;
  400. for (let i = 0; i < (count || 3); i++) {
  401. list.push({ year: y, month: m });
  402. m -= 1;
  403. if (m === 0) { m = 12; y -= 1; }
  404. }
  405. return list;
  406. }
  407. function handleBack() {
  408. if (window.Bridge && Bridge.toHome) Bridge.toHome();
  409. else window.history.back();
  410. }
  411. function openModal() { document.getElementById('infoModal').classList.add('show'); }
  412. function closeModal() { document.getElementById('infoModal').classList.remove('show'); }
  413. function toggleDropdown() { dropdown.style.display = (dropdown.style.display === 'flex') ? 'none' : 'flex'; }
  414. function setLoading(isLoading) {
  415. rankListEl.style.opacity = isLoading ? '0.4' : '1';
  416. loadingMask.classList.toggle('show', isLoading);
  417. }
  418. function buildAvatar(name, salt) {
  419. const seedBase = name || 'user';
  420. const seed = seedBase + (salt || '');
  421. return multiavatar(seed);
  422. }
  423. function setProgress(real, target) {
  424. const badge = document.getElementById('dashBadge');
  425. const t1 = document.getElementById('t1');
  426. const t2 = document.getElementById('t2');
  427. const t3 = document.getElementById('t3');
  428. const t4 = document.getElementById('t4');
  429. const safeReal = Math.max(real || 0, 0);
  430. const safeTarget = target && target > 0 ? target : 4;
  431. const ratio = Math.min(safeReal / safeTarget, 1);
  432. const percent = Math.max(0, Math.min(1, ratio)) * 100;
  433. trackLine.style.width = percent + '%';
  434. const textReal = safeReal >= safeTarget ? safeTarget : safeReal;
  435. badge.innerText = safeReal >= safeTarget ? '成功' : `${textReal} / ${safeTarget}`;
  436. badge.classList.toggle('completed', safeReal >= safeTarget);
  437. function setIconActive(el, active) {
  438. if (active) {
  439. el.classList.add('active');
  440. if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-check"></i>';
  441. } else {
  442. el.classList.remove('active');
  443. if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-lock"></i>';
  444. }
  445. }
  446. setIconActive(t1, safeReal >= 1);
  447. setIconActive(t2, safeReal >= 2);
  448. setIconActive(t3, safeReal >= 3);
  449. setIconActive(t4, safeReal >= safeTarget);
  450. }
  451. function renderBadge(real, target) {
  452. setProgress(real, target);
  453. }
  454. function findMonthProgress(year, month, currentMonthData, allMonthsData) {
  455. const result = { realNum: 0, targetNum: 4 };
  456. const pick = (arr) => {
  457. if (!arr || !arr.length) return null;
  458. for (let i = 0; i < arr.length; i++) {
  459. if (Number(arr[i].month) === Number(month)) return arr[i];
  460. }
  461. return null;
  462. };
  463. const fromCurrent = currentMonthData && pick(currentMonthData.monthRs || []);
  464. if (fromCurrent) return { realNum: fromCurrent.realNum || 0, targetNum: fromCurrent.targetNum || 4 };
  465. if (allMonthsData && allMonthsData.length) {
  466. for (let i = 0; i < allMonthsData.length; i++) {
  467. const item = allMonthsData[i];
  468. if (item.year && Number(item.year) !== Number(year)) continue;
  469. const found = pick(item.monthRs || []);
  470. if (found) return { realNum: found.realNum || 0, targetNum: found.targetNum || 4 };
  471. }
  472. }
  473. return result;
  474. }
  475. function renderMonths(list) {
  476. dropdown.innerHTML = '';
  477. if (!list || list.length === 0) {
  478. dropdown.innerHTML = '<div class="dd-item">暂无数据</div>';
  479. return;
  480. }
  481. list.forEach((item, idx) => {
  482. const title = `${item.month}月挑战赛`;
  483. const div = document.createElement('div');
  484. const isCurrent = state.currentYM && state.currentYM.year === item.year && state.currentYM.month === item.month;
  485. div.className = 'dd-item' + (isCurrent ? ' dd-active' : '');
  486. div.innerText = (idx === 0 && !isCurrent) ? `${title} (本月)` : title;
  487. div.onclick = () => { selectMonth(item.year, item.month, title); };
  488. dropdown.appendChild(div);
  489. });
  490. }
  491. function renderPodium(list, tabType) {
  492. const type = tabType || state.activeTab || 'score';
  493. podiumWrap.innerHTML = '';
  494. const podiumData = Array(3).fill(null);
  495. if (list && list.length > 0) {
  496. list.slice(0, 3).forEach((item, index) => {
  497. podiumData[index] = item;
  498. });
  499. }
  500. const p1 = podiumData[0];
  501. const p2 = podiumData[1];
  502. const p3 = podiumData[2];
  503. const podiumItems = [
  504. { person: p2, className: 'p-2' },
  505. { person: p1, className: 'p-1' },
  506. { person: p3, className: 'p-3' }
  507. ];
  508. podiumItems.forEach(itemConfig => {
  509. const person = itemConfig.person;
  510. const col = document.createElement('div');
  511. col.className = 'p-col ' + itemConfig.className;
  512. const name = person ? (person.nickName || person.name || person.userName) : '虚位以待';
  513. const score = person ? (person.score != null ? person.score : (person.inRankNum != null ? person.inRankNum : '')) : '';
  514. const avatarContent = person ? buildAvatar(name, person.rankNum) : '<div style="width:100%;height:100%;background:#eee;color:#aaa;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:20px;">?</div>';
  515. const isActualFirst = (person === p1 && p1 !== null);
  516. col.innerHTML = `
  517. ${isActualFirst ? '<div class="crown"><i class="fa-solid fa-crown"></i></div>' : ''}
  518. <div class="p-img">${avatarContent}</div>
  519. <div class="p-box">
  520. <div class="p-name">${name}</div>
  521. <div class="p-score">${score}</div>
  522. </div>
  523. `;
  524. podiumWrap.appendChild(col);
  525. });
  526. const remaining = list && list.length > 3 ? list.slice(3) : [];
  527. if (type === 'venue') {
  528. state.venueListRendered = remaining;
  529. } else {
  530. state.scoreListRendered = remaining;
  531. }
  532. if (state.activeTab === type) renderRankList(remaining);
  533. }
  534. function renderRankList(list) {
  535. rankListEl.innerHTML = '';
  536. if (!list || list.length === 0) {
  537. rankListEl.innerHTML = '<div style="padding:40px; color:#b2bec3; text-align:center; font-weight:bold;">暂时没人上榜哦<br>快去挑战吧!</div>';
  538. return;
  539. }
  540. list.forEach((item, idx) => {
  541. const rankNum = item.rankNum || idx + 1;
  542. const name = item.nickName || item.name || item.userName || item.teamName || '探险家';
  543. const scoreVal = (item.score != null ? item.score : item.inRankNum);
  544. const score = scoreVal != null ? scoreVal : '--';
  545. const team = item.teamName || item.coiName || '';
  546. const teamIcon = team ? '<i class="fa-solid fa-user-group"></i> ' : '<i class="fa-solid fa-user"></i> ';
  547. const row = document.createElement('div');
  548. row.className = 'list-item';
  549. row.innerHTML = `
  550. <div class="rank">${rankNum}</div>
  551. <div class="avatar">${buildAvatar(name, rankNum)}</div>
  552. <div class="info">
  553. <div class="name">${name}</div>
  554. <div class="team">${teamIcon}${team || '小小探险家'}</div>
  555. </div>
  556. <div class="score">${score}</div>
  557. `;
  558. rankListEl.appendChild(row);
  559. });
  560. }
  561. function renderMyInfo(myRank, myScore, userInfo) {
  562. const rankNumEl = document.getElementById('myRankNum');
  563. const myScoreEl = document.getElementById('myScoreValue');
  564. const myNameEl = document.getElementById('myName');
  565. const myTeamEl = document.getElementById('myTeam');
  566. const myAvatarEl = document.getElementById('myAvatar');
  567. const rankVal = myRank && Number(myRank.rankNum);
  568. const hasRank = rankVal > 0;
  569. rankNumEl.innerText = hasRank ? rankVal : '-';
  570. myScoreEl.innerText = hasRank && myScore && myScore.score != null ? myScore.score : '--';
  571. const name = (userInfo && (userInfo.nickName || userInfo.userName)) || '我';
  572. myNameEl.innerText = name;
  573. myTeamEl.innerText = hasRank ? '继续冲刺!' : '加油!';
  574. myAvatarEl.innerHTML = buildAvatar(name, 'me');
  575. document.querySelector('.my-rank-bar').onclick = null;
  576. }
  577. function renderGuestState() {
  578. const rankNumEl = document.getElementById('myRankNum');
  579. const myScoreEl = document.getElementById('myScoreValue');
  580. const myNameEl = document.getElementById('myName');
  581. const myTeamEl = document.getElementById('myTeam');
  582. const myAvatarEl = document.getElementById('myAvatar');
  583. const myRankBar = document.querySelector('.my-rank-bar');
  584. rankNumEl.innerText = '--';
  585. myScoreEl.innerText = '';
  586. myNameEl.innerText = '未登录';
  587. myTeamEl.innerText = '点击登录探险';
  588. myTeamEl.style.color = 'white';
  589. const randomSeed = 'guest_' + Math.floor(Math.random() * 10000);
  590. myAvatarEl.innerHTML = buildAvatar('Guest', randomSeed);
  591. myRankBar.onclick = function() {
  592. if (window.Bridge && Bridge.toLogin) {
  593. Bridge.toLogin();
  594. }
  595. };
  596. }
  597. function renderRules(config) {
  598. if (!config || !config.configJson) return;
  599. try {
  600. const parsed = JSON.parse(config.configJson);
  601. const rules = parsed.popupRuleList || [];
  602. if (rules.length > 0 && rules[0].data && rules[0].data.content) {
  603. ruleContent.innerHTML = rules[0].data.content;
  604. }
  605. } catch (err) {
  606. Logger.warn('[Rule] parse error', err);
  607. }
  608. }
  609. function switchTab(type, tabElement) {
  610. state.activeTab = type;
  611. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  612. if (tabElement) tabElement.classList.add('active');
  613. const baseList = type === 'venue' ? state.venueList : state.scoreList;
  614. renderPodium(baseList, type);
  615. const rendered = type === 'venue' ? (state.venueListRendered || baseList.slice(3)) : (state.scoreListRendered || baseList.slice(3));
  616. renderRankList(rendered);
  617. }
  618. function demoProgress(step) {
  619. setProgress(step, 4);
  620. }
  621. async function loadMonthData(year, month) {
  622. state.currentYM = { year: year, month: month };
  623. document.getElementById('currentMonthText').innerText = `${month}月挑战赛`;
  624. renderMonths(state.months);
  625. setLoading(true);
  626. try {
  627. const monthRank = await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
  628. const monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
  629. const monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
  630. state.scoreList = monthGrad;
  631. state.venueList = monthMap;
  632. const prog = findMonthProgress(year, month, state.currentMonthData, state.allMonthsData);
  633. renderBadge(prog.realNum, prog.targetNum);
  634. renderPodium(state.activeTab === 'venue' ? state.venueList : state.scoreList, state.activeTab);
  635. switchTab(state.activeTab, document.querySelector('.tab.active'));
  636. } catch (err) {
  637. Logger.error('[Month] 加载失败', err);
  638. rankListEl.innerHTML = '<div style="padding:20px; color:#ff6b6b;">数据加载失败,请重试</div>';
  639. } finally {
  640. setLoading(false);
  641. }
  642. }
  643. function selectMonth(year, month, title) {
  644. dropdown.style.display = 'none';
  645. loadMonthData(year, month);
  646. }
  647. async function initPage() {
  648. if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
  649. document.body.classList.add('platform-ios');
  650. }
  651. const ecId = getQuery('ecId') || '4';
  652. const token = getQuery('token');
  653. let baseUrl = getQuery('baseUrl') || undefined;
  654. const env = (getQuery('env') || '').toLowerCase();
  655. const useMock = env === 'mock';
  656. Logger.init(useMock);
  657. const ym = getYearMonth();
  658. state.months = getRecentMonths(6);
  659. if (!baseUrl && !useMock) baseUrl = 'https://colormaprun.com/api/card/';
  660. if (window.Bridge && window.Bridge.onToken) Bridge.onToken(API.setToken);
  661. API.init({ token: token || '', useMock: useMock, baseUrl: baseUrl });
  662. const allowLogin = !useMock && token;
  663. renderMonths(state.months);
  664. setLoading(true);
  665. async function safeCall(promiseFactory) {
  666. try { return await promiseFactory(); }
  667. catch (err) { Logger.warn('[Optional API] ignore error', err); return null; }
  668. }
  669. async function fetchMonthData(year, month) {
  670. return await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
  671. }
  672. try {
  673. let targetYM = { year: ym.year, month: ym.month };
  674. let monthRank = await fetchMonthData(targetYM.year, targetYM.month);
  675. let monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
  676. let monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
  677. let hasData = monthGrad.length > 0 || monthMap.length > 0;
  678. if (!hasData) {
  679. for (let i = 1; i < state.months.length; i++) {
  680. const checkYM = state.months[i];
  681. const checkRank = await fetchMonthData(checkYM.year, checkYM.month);
  682. const checkGrad = checkRank && checkRank.gradRs ? checkRank.gradRs : [];
  683. const checkMap = checkRank && checkRank.mapNumRs ? checkRank.mapNumRs : [];
  684. if (checkGrad.length > 0 || checkMap.length > 0) {
  685. targetYM = checkYM;
  686. monthRank = checkRank;
  687. monthGrad = checkGrad;
  688. monthMap = checkMap;
  689. hasData = true;
  690. break;
  691. }
  692. }
  693. }
  694. state.currentYM = targetYM;
  695. state.scoreList = monthGrad;
  696. state.venueList = monthMap;
  697. let base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig;
  698. if (useMock || allowLogin) {
  699. [base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig] = await Promise.all([
  700. safeCall(() => API.getCardBase(ecId, 'rank')),
  701. safeCall(() => API.request('CurrentMonthlyChallengeQuery', { ecId: ecId })),
  702. safeCall(() => API.getMonthlyChallenge()),
  703. safeCall(() => API.getUserCurrentRank(ecId)),
  704. safeCall(() => API.getScore(ecId)),
  705. safeCall(() => API.getUserInfo()),
  706. safeCall(() => API.getCardConfig(ecId, 'rank'))
  707. ]);
  708. } else {
  709. base = { ecName: `${targetYM.month}月挑战赛` };
  710. currentMonth = { monthRs: [{ month: targetYM.month, realNum: 0, targetNum: 4 }] };
  711. allMonths = [];
  712. myRank = null;
  713. myScore = null;
  714. userInfo = null;
  715. cardConfig = null;
  716. }
  717. const selfRow = monthGrad.find && monthGrad.find(item => item.isSelf === 1);
  718. if (!base && monthGrad.length) base = { ecName: `${targetYM.month}月挑战赛` };
  719. state.currentMonthData = currentMonth || null;
  720. state.allMonthsData = allMonths || null;
  721. const prog = findMonthProgress(targetYM.year, targetYM.month, state.currentMonthData, state.allMonthsData);
  722. document.getElementById('currentMonthText').innerText = `${targetYM.month}月挑战赛`;
  723. renderMonths(state.months);
  724. renderBadge(prog.realNum, prog.targetNum);
  725. renderPodium(state.scoreList, 'score');
  726. switchTab(state.activeTab, document.querySelector('.tab.active'));
  727. if (!token && !useMock) {
  728. renderGuestState();
  729. } else {
  730. let hasRenderedMyInfo = false;
  731. if (allowLogin && myRank) {
  732. const rankVal = Number(myRank.rankNum);
  733. if (rankVal > 0) {
  734. renderMyInfo({ rankNum: rankVal }, myScore, userInfo);
  735. } else {
  736. renderMyInfo({ rankNum: null }, null, userInfo);
  737. }
  738. hasRenderedMyInfo = true;
  739. } else if (selfRow) {
  740. renderMyInfo({ rankNum: selfRow.rankNum }, { score: selfRow.inRankNum || selfRow.score }, { nickName: selfRow.userName });
  741. hasRenderedMyInfo = true;
  742. }
  743. if (!hasRenderedMyInfo) {
  744. renderMyInfo(myRank, myScore, userInfo);
  745. }
  746. }
  747. renderRules(cardConfig);
  748. } finally {
  749. setLoading(false);
  750. loadingMask.classList.remove('show');
  751. }
  752. }
  753. document.addEventListener('click', (e) => {
  754. if(!e.target.closest('.month-select') && !e.target.closest('.dropdown')) { dropdown.style.display = 'none'; }
  755. });
  756. document.addEventListener('DOMContentLoaded', initPage);
  757. </script>
  758. </body>
  759. </html>