signup.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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=Roboto:wght@400;700&display=swap');
  11. body { font-family: 'Roboto', sans-serif; }
  12. .page-top {
  13. background-image: url('./static/backgroud/top_bg_sddx.png');
  14. background-repeat: no-repeat;
  15. background-position: center;
  16. background-size: cover;
  17. min-height: 220px;
  18. }
  19. .logo-bg {
  20. background-image: url('./static/logo/jbs.png');
  21. background-repeat: no-repeat;
  22. background-position: center;
  23. background-size: contain;
  24. width: 80px; height: 80px; margin-top: 10px;
  25. }
  26. .e-select-wrapper { position: relative; }
  27. .e-select-input { width: 100%; padding: 10px; border: 1px solid #dcdfe6; border-radius: 4px; background-color: white; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
  28. .e-select-dropdown { position: absolute; width: 100%; background: white; border: 1px solid #dcdfe6; border-radius: 4px; max-height: 200px; overflow-y: auto; z-index: 10; }
  29. .e-select-item { padding: 10px; cursor: pointer; }
  30. .e-select-item:hover { background-color: #f0f0f0; }
  31. </style>
  32. </head>
  33. <body class="bg-gray-100 min-h-screen flex flex-col items-center">
  34. <!-- Top Section -->
  35. <div class="page-top w-full flex flex-col justify-between items-center pb-4">
  36. <div class="w-full flex justify-between items-center p-4">
  37. <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">
  38. <i class="fas fa-chevron-left"></i>
  39. </button>
  40. <h1 id="mc-name" class="text-white text-lg font-bold">赛事名称</h1>
  41. <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">
  42. <i class="fas fa-question-circle"></i> 说明
  43. </button>
  44. </div>
  45. <div class="flex flex-col items-center gap-2">
  46. <div class="logo-bg"></div>
  47. <p id="sub-title" class="text-yellow-400 text-lg font-bold">活动时间</p>
  48. </div>
  49. </div>
  50. <!-- Time Bar -->
  51. <div class="timebar flex items-center justify-center -mt-4 bg-white px-4 py-2 rounded-full shadow-md z-10 border border-gray-200">
  52. <img src="https://img.icons8.com/ios-filled/50/888888/time.png" class="w-4 h-4 mr-2" alt="clock">
  53. <span id="act-time" class="text-gray-800 text-sm font-semibold whitespace-nowrap"></span>
  54. </div>
  55. <!-- Main Form Section -->
  56. <div class="flex flex-col items-center w-11/12 max-w-sm px-4 py-6 bg-white rounded-lg shadow-lg mt-4">
  57. <input type="text" id="nickNameInput" maxlength="12" placeholder="请输入昵称"
  58. class="w-full h-10 px-3 my-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm" />
  59. <div class="e-select-wrapper w-full my-2">
  60. <div id="coiSelectInput" class="e-select-input text-gray-700 text-sm" tabindex="0">
  61. <span id="selectedCoiName">请选择组织</span>
  62. <i class="fas fa-chevron-down text-gray-400"></i>
  63. </div>
  64. <div id="coiDropdown" class="e-select-dropdown hidden">
  65. <input type="text" id="coiSearchInput" placeholder="搜索组织" class="w-full px-3 py-2 border-b border-gray-200 focus:outline-none" />
  66. <div id="coiOptionsContainer"></div>
  67. </div>
  68. </div>
  69. <div id="introduce-section" class="w-full mt-4 text-gray-700 text-sm leading-relaxed hidden">
  70. <h3 id="introduce-title" class="font-bold text-base mb-1"></h3>
  71. <div id="introduce-content"></div>
  72. </div>
  73. <div id="rules-section" class="w-full mt-4 p-4 bg-gray-100 rounded-lg hidden">
  74. <h3 id="rules-title" class="font-bold text-sm mb-1"></h3>
  75. <div id="rules-content" class="text-xs text-gray-600"></div>
  76. </div>
  77. <button id="signup-btn" onclick="handleSignup()" class="w-full h-12 mt-6 text-white text-lg font-bold bg-green-500 rounded-full shadow-lg active:scale-95 transition-transform">
  78. 我要报名
  79. </button>
  80. <button id="signup-btn-disabled" class="w-full h-12 mt-6 text-white text-lg font-bold bg-gray-400 rounded-full shadow-lg cursor-not-allowed hidden">
  81. 活动已结束
  82. </button>
  83. </div>
  84. <!-- Info Modal -->
  85. <div id="infoModal" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
  86. <div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeInfoModal()"></div>
  87. <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">
  88. <button onclick="closeInfoModal()" class="absolute -top-10 right-0 text-white/80 hover:text-white w-8 h-8 flex items-center justify-center rounded-full border border-white/30">
  89. <i class="fas fa-times"></i>
  90. </button>
  91. <h3 class="text-center font-bold text-lg mb-4 text-blue-600">活动说明</h3>
  92. <div id="info-modal-content" class="text-sm text-gray-700 max-h-80 overflow-y-auto"></div>
  93. </div>
  94. </div>
  95. <!-- Alert Dialog -->
  96. <div id="alertDialog" class="fixed inset-0 z-50 hidden transition-opacity duration-300">
  97. <div class="absolute inset-0 bg-slate-900/70 backdrop-blur-sm" onclick="closeAlertDialog()"></div>
  98. <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[90%] max-w-xs bg-white rounded-lg p-6 shadow-2xl text-center">
  99. <h3 class="font-bold text-lg mb-4">请确认报名信息</h3>
  100. <div class="text-sm text-gray-700 space-y-2">
  101. <p id="alert-mc-name" class="font-bold"></p>
  102. <p id="alert-nickname-label"></p>
  103. <p id="alert-coi-label"></p>
  104. </div>
  105. <div class="flex justify-around mt-6 space-x-4">
  106. <button onclick="closeAlertDialog()" class="flex-1 py-2 rounded-md border border-gray-300 text-gray-600">取消</button>
  107. <button onclick="confirmSignup()" class="flex-1 py-2 rounded-md bg-blue-500 text-white">确认</button>
  108. </div>
  109. </div>
  110. </div>
  111. <script src="./js/utils.js"></script>
  112. <script src="./js/bridge.js"></script>
  113. <script src="./js/api.js"></script>
  114. <script>
  115. const STATE = {
  116. ecId: 0,
  117. token: '',
  118. mcId: 0,
  119. mcType: 0,
  120. mcName: '',
  121. beginSecond: 0,
  122. endSecond: 0,
  123. mcState: 0,
  124. nickName: '',
  125. coiId: 0,
  126. coiName: '',
  127. coiOptions: [],
  128. fromPage: '',
  129. configParam: { labelName: '昵称', labelOrg: '组织', subTitle: '' },
  130. introduce: { title: '', content: '' },
  131. activityRules: { title: '', content: '' },
  132. popupRuleList: []
  133. };
  134. const mcNameEl = document.getElementById('mc-name');
  135. const subTitleEl = document.getElementById('sub-title');
  136. const actTimeEl = document.getElementById('act-time');
  137. const nickNameInput = document.getElementById('nickNameInput');
  138. const signupBtn = document.getElementById('signup-btn');
  139. const signupBtnDisabled = document.getElementById('signup-btn-disabled');
  140. const coiSelectInput = document.getElementById('coiSelectInput');
  141. const selectedCoiNameEl = document.getElementById('selectedCoiName');
  142. const coiDropdown = document.getElementById('coiDropdown');
  143. const coiSearchInput = document.getElementById('coiSearchInput');
  144. const coiOptionsContainer = document.getElementById('coiOptionsContainer');
  145. const introduceSection = document.getElementById('introduce-section');
  146. const introduceTitleEl = document.getElementById('introduce-title');
  147. const introduceContentEl = document.getElementById('introduce-content');
  148. const rulesSection = document.getElementById('rules-section');
  149. const rulesTitleEl = document.getElementById('rules-title');
  150. const rulesContentEl = document.getElementById('rules-content');
  151. const infoModal = document.getElementById('infoModal');
  152. const infoModalContentEl = document.getElementById('info-modal-content');
  153. const alertDialog = document.getElementById('alertDialog');
  154. const alertMcNameEl = document.getElementById('alert-mc-name');
  155. const alertNicknameLabelEl = document.getElementById('alert-nickname-label');
  156. const alertCoiLabelEl = document.getElementById('alert-coi-label');
  157. function injectCss(css) {
  158. if (!css) return;
  159. const style = document.createElement('style');
  160. style.innerHTML = css;
  161. document.head.appendChild(style);
  162. }
  163. window.onload = function() {
  164. STATE.token = Tools.getQueryParam('token') || '';
  165. STATE.ecId = Tools.getQueryParam('id') || 0;
  166. STATE.fromPage = Tools.getQueryParam('from') || '';
  167. if (window.API) {
  168. API.setToken(STATE.token);
  169. if (Tools.getQueryParam('env') === 'mock') {
  170. API.init({ useMock: true });
  171. if (!STATE.ecId) STATE.ecId = 'mock_id';
  172. }
  173. }
  174. loadCardConfig();
  175. getCardDetail();
  176. matchRsDetail();
  177. coiSelectInput.addEventListener('click', toggleCoiDropdown);
  178. coiSearchInput.addEventListener('input', filterCoiOptions);
  179. document.addEventListener('click', (event) => {
  180. if (!coiDropdown.contains(event.target) && !coiSelectInput.contains(event.target)) {
  181. coiDropdown.classList.add('hidden');
  182. }
  183. });
  184. };
  185. function loadCardConfig() {
  186. if (!window.API) return;
  187. API.getCardConfig(STATE.ecId, 'signup').then(configRes => {
  188. let cfg = configRes;
  189. if (configRes && configRes.configJson) {
  190. try { cfg = JSON.parse(configRes.configJson); } catch (e) { console.warn('config parse fail', e); }
  191. }
  192. if (!cfg) return;
  193. if (cfg.common && cfg.common.css) injectCss(cfg.common.css);
  194. const pageCfg = cfg.signup || cfg;
  195. if (pageCfg && pageCfg.css) injectCss(pageCfg.css);
  196. if (pageCfg && pageCfg.introduce) {
  197. STATE.introduce = pageCfg.introduce;
  198. introduceTitleEl.innerText = STATE.introduce.title || '';
  199. introduceContentEl.innerHTML = STATE.introduce.content || '';
  200. introduceSection.classList.remove('hidden');
  201. }
  202. if (pageCfg && pageCfg.activityRules) {
  203. STATE.activityRules = pageCfg.activityRules;
  204. rulesTitleEl.innerText = STATE.activityRules.title || '';
  205. rulesContentEl.innerHTML = STATE.activityRules.content || '';
  206. rulesSection.classList.remove('hidden');
  207. }
  208. if (pageCfg && pageCfg.param) STATE.configParam = Object.assign(STATE.configParam, pageCfg.param);
  209. if (cfg.popupRuleList) STATE.popupRuleList = cfg.popupRuleList;
  210. nickNameInput.placeholder = `请输入${STATE.configParam.labelName}`;
  211. selectedCoiNameEl.innerText = `请选择${STATE.configParam.labelOrg}`;
  212. });
  213. }
  214. function getCardDetail() {
  215. if (!window.API) return;
  216. API.getCardDetail(STATE.ecId).then(res => {
  217. if (!res) return;
  218. STATE.mcType = res.mcType;
  219. STATE.mcId = res.mcId;
  220. STATE.mcName = res.mcName;
  221. STATE.beginSecond = res.beginSecond;
  222. STATE.endSecond = res.endSecond;
  223. STATE.coiId = res.coiId;
  224. STATE.coiName = res.coiName;
  225. STATE.nickName = res.nickName || '';
  226. STATE.mcState = Tools.checkMcState(STATE.beginSecond, STATE.endSecond);
  227. updateUI();
  228. getOnlineMcSignUpDetail();
  229. });
  230. }
  231. function matchRsDetail() {
  232. if (!window.API) return;
  233. API.getMatchRsDetail(STATE.ecId,0).then(() => {});
  234. }
  235. function getOnlineMcSignUpDetail() {
  236. if (!window.API || !STATE.mcId) return;
  237. API.getOnlineMcSignUpDetail(STATE.ecId, STATE.mcId).then(res => {
  238. if (!res) return;
  239. if (res.coiRs) {
  240. STATE.coiOptions = res.coiRs.map(item => ({ text: item.coiName, value: item.coiId }));
  241. populateCoiOptions(STATE.coiOptions);
  242. }
  243. if (!STATE.nickName && res.name) STATE.nickName = res.name;
  244. nickNameInput.value = STATE.nickName;
  245. if (STATE.coiId > 0) {
  246. const selected = STATE.coiOptions.find(item => item.value == STATE.coiId);
  247. if (selected) {
  248. selectedCoiNameEl.innerText = selected.text;
  249. STATE.coiName = selected.text;
  250. }
  251. }
  252. });
  253. }
  254. function updateUI() {
  255. mcNameEl.innerText = STATE.mcName;
  256. subTitleEl.innerText = STATE.configParam.subTitle || Tools.fmtMcTime2(STATE.beginSecond, STATE.endSecond);
  257. actTimeEl.innerText = Tools.getActtime(STATE.beginSecond, STATE.endSecond);
  258. nickNameInput.value = STATE.nickName;
  259. if (STATE.mcState === 2) {
  260. signupBtn.classList.add('hidden');
  261. signupBtnDisabled.classList.remove('hidden');
  262. } else {
  263. signupBtn.classList.remove('hidden');
  264. signupBtnDisabled.classList.add('hidden');
  265. }
  266. }
  267. function toggleCoiDropdown() {
  268. coiDropdown.classList.toggle('hidden');
  269. coiSearchInput.value = '';
  270. populateCoiOptions(STATE.coiOptions);
  271. }
  272. function filterCoiOptions() {
  273. const searchTerm = coiSearchInput.value.toLowerCase();
  274. const filtered = STATE.coiOptions.filter(item => item.text.toLowerCase().includes(searchTerm));
  275. populateCoiOptions(filtered);
  276. }
  277. function populateCoiOptions(options) {
  278. coiOptionsContainer.innerHTML = '';
  279. options.forEach(option => {
  280. const div = document.createElement('div');
  281. div.classList.add('e-select-item');
  282. div.innerText = option.text;
  283. div.dataset.value = option.value;
  284. div.addEventListener('click', () => selectCoiOption(option));
  285. coiOptionsContainer.appendChild(div);
  286. });
  287. }
  288. function selectCoiOption(option) {
  289. STATE.coiId = option.value;
  290. STATE.coiName = option.text;
  291. selectedCoiNameEl.innerText = option.text;
  292. coiDropdown.classList.add('hidden');
  293. }
  294. function handleInfo() {
  295. let contentHtml = '';
  296. if (STATE.popupRuleList && STATE.popupRuleList.length > 0) {
  297. STATE.popupRuleList.forEach(rule => {
  298. if (rule.data && rule.data.title) contentHtml += `<h4 class="font-bold mt-2">${rule.data.title}</h4>`;
  299. if (rule.data && rule.data.content) contentHtml += `<p>${rule.data.content}</p>`;
  300. });
  301. } else if (STATE.activityRules && STATE.activityRules.content) {
  302. contentHtml = `<h4 class="font-bold">${STATE.activityRules.title}</h4><p>${STATE.activityRules.content}</p>`;
  303. } else {
  304. contentHtml = '<p>暂无说明信息。</p>';
  305. }
  306. infoModalContentEl.innerHTML = contentHtml;
  307. infoModal.classList.remove('hidden');
  308. }
  309. function closeInfoModal() { infoModal.classList.add('hidden'); }
  310. function handleBack() {
  311. const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
  312. if (STATE.fromPage) {
  313. Bridge.appAction(`./${STATE.fromPage}.html${qs}`);
  314. } else {
  315. Bridge.appAction('action://to_home/');
  316. }
  317. }
  318. function handleSignup() {
  319. STATE.nickName = nickNameInput.value.trim();
  320. if (!STATE.nickName) {
  321. Tools.showToast(`请输入${STATE.configParam.labelName}`);
  322. return;
  323. }
  324. if (!STATE.coiId) {
  325. Tools.showToast(`请选择${STATE.configParam.labelOrg}`);
  326. return;
  327. }
  328. alertMcNameEl.innerText = STATE.mcName;
  329. alertNicknameLabelEl.innerText = `${STATE.configParam.labelName}: ${STATE.nickName}`;
  330. alertCoiLabelEl.innerText = `${STATE.configParam.labelOrg}: ${STATE.coiName}`;
  331. alertDialog.classList.remove('hidden');
  332. }
  333. function confirmSignup() {
  334. if (!window.API) return;
  335. API.signUpOnline(STATE.mcId, STATE.coiId, 0, STATE.nickName).then(() => {
  336. Tools.showToast('报名成功');
  337. const qs = window.location.search || `?id=${STATE.ecId}&token=${STATE.token}`;
  338. Bridge.appAction(`./ranklist.html${qs}`);
  339. }).catch(() => {
  340. Tools.showToast('报名失败,请稍后重试');
  341. });
  342. closeAlertDialog();
  343. }
  344. function closeAlertDialog() { alertDialog.classList.add('hidden'); }
  345. </script>
  346. </body>
  347. </html>