signup.html 15 KB

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