diff --git a/plugins/itstudio-recruitment/assets/otp.css b/plugins/itstudio-recruitment/assets/otp.css new file mode 100644 index 0000000..f2fd4f3 --- /dev/null +++ b/plugins/itstudio-recruitment/assets/otp.css @@ -0,0 +1,145 @@ +/* ========================================= + ITStudio 邮箱 OTP 验证码组件样式 (Modern Geek Style) + ========================================= */ + +.itstudio-otp-wrap { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + margin-top: 4px; +} + +.itstudio-otp-group { + display: flex; + align-items: stretch; + gap: 10px; + width: 100%; +} + +.itstudio-otp-group .itstudio-otp-input { + flex: 1 1 auto !important; + min-width: 0 !important; + letter-spacing: 0.1em !important; + font-family: var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace) !important; + font-weight: 600 !important; +} + +.itstudio-otp-send { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 0 20px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.16); + background: linear-gradient(135deg, #1f6feb 0%, #1158c7 100%); + color: #ffffff; + font-family: inherit; + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 0.02em; + white-space: nowrap; + cursor: pointer; + box-shadow: 0 2px 8px rgba(31, 111, 235, 0.35); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + user-select: none; + -webkit-user-select: none; +} + +.itstudio-otp-send:hover:not(:disabled) { + transform: translateY(-1px); + filter: brightness(1.08); + box-shadow: 0 4px 14px rgba(31, 111, 235, 0.45); +} + +.itstudio-otp-send:active:not(:disabled) { + transform: translateY(0); + filter: brightness(0.96); + box-shadow: 0 1px 4px rgba(31, 111, 235, 0.3); +} + +.itstudio-otp-send:disabled { + cursor: not-allowed; + opacity: 0.7; + transform: none !important; + box-shadow: none !important; + filter: none !important; +} + +.itstudio-otp-send.is-countdown { + background: rgba(56, 139, 253, 0.12) !important; + border: 1px solid rgba(56, 139, 253, 0.3) !important; + color: #58a6ff !important; + font-variant-numeric: tabular-nums; + font-weight: 600; +} + +.itstudio-otp-send.is-loading { + opacity: 0.85; + cursor: wait; +} + +.itstudio-otp-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.84rem; + line-height: 1.45; + padding: 2px 4px; + color: var(--text-secondary); + transition: all 0.2s ease; +} + +.itstudio-otp-status.is-success { + color: #3fb950; + font-weight: 500; +} + +.itstudio-otp-status.is-error { + color: #f85149; + font-weight: 500; +} + +.itstudio-otp-status.is-info { + color: #58a6ff; +} + +/* 浅色模式适配 */ +[data-theme="light"] .itstudio-otp-send { + background: linear-gradient(135deg, #0969da 0%, #0550ae 100%); + border: 1px solid rgba(255, 255, 255, 0.25); + box-shadow: 0 2px 8px rgba(9, 105, 218, 0.28); +} + +[data-theme="light"] .itstudio-otp-send:hover:not(:disabled) { + box-shadow: 0 4px 14px rgba(9, 105, 218, 0.38); +} + +[data-theme="light"] .itstudio-otp-send.is-countdown { + background: rgba(9, 105, 218, 0.08) !important; + border: 1px solid rgba(9, 105, 218, 0.25) !important; + color: #0969da !important; +} + +[data-theme="light"] .itstudio-otp-status.is-success { + color: #1a7f37; +} + +[data-theme="light"] .itstudio-otp-status.is-error { + color: #cf222e; +} + +[data-theme="light"] .itstudio-otp-status.is-info { + color: #0969da; +} + +@media (max-width: 480px) { + .itstudio-otp-group { + flex-direction: column; + } + .itstudio-otp-send { + width: 100%; + } +} \ No newline at end of file diff --git a/plugins/itstudio-recruitment/assets/otp.js b/plugins/itstudio-recruitment/assets/otp.js new file mode 100644 index 0000000..350753b --- /dev/null +++ b/plugins/itstudio-recruitment/assets/otp.js @@ -0,0 +1,177 @@ +(function () { + 'use strict'; + + const config = window.itstudioJoinOtp; + if (!config) { + return; + } + + function getLanguage() { + return document.documentElement.lang === 'en' ? 'en' : 'cn'; + } + + function getMessage(payload, fallback) { + const key = getLanguage() === 'en' ? 'message_en' : 'message_cn'; + return payload && payload[key] ? payload[key] : fallback; + } + + function findField(form, fieldId) { + return form.querySelector('[name="item_meta[' + fieldId + ']"]'); + } + + function findTargetForm() { + const candidates = document.querySelectorAll('.join-form-content .frm_forms form'); + for (const form of candidates) { + const formIdInput = form.querySelector('input[name="form_id"]'); + if (!formIdInput || Number(formIdInput.value) === Number(config.formId)) { + if (findField(form, config.emailFieldId) && findField(form, config.codeFieldId)) { + return form; + } + } + } + return null; + } + + function setStatus(status, message, tone) { + if (!message) { + status.textContent = ''; + status.className = 'itstudio-otp-status'; + status.style.display = 'none'; + return; + } + status.style.display = 'flex'; + status.textContent = message; + status.className = 'itstudio-otp-status' + (tone ? ' is-' + tone : ''); + } + + function startCooldown(button, seconds) { + let remaining = Math.max(1, Number(seconds) || Number(config.cooldown) || 60); + button.disabled = true; + button.classList.add('is-countdown'); + + const update = function () { + button.textContent = String(config.labels.resend || '重新发送 (%s秒)').replace('%s', remaining); + remaining -= 1; + if (remaining < 0) { + window.clearInterval(button.itstudioOtpTimer); + button.itstudioOtpTimer = null; + button.classList.remove('is-countdown'); + button.textContent = config.labels.send || '发送验证码'; + button.disabled = false; + } + }; + + update(); + button.itstudioOtpTimer = window.setInterval(update, 1000); + } + + async function sendCode(form, button, status) { + const emailInput = findField(form, config.emailFieldId); + const email = emailInput ? emailInput.value.trim() : ''; + if (!email) { + setStatus(status, config.labels.emailRequired || '请先填写邮箱地址。', 'error'); + emailInput?.focus(); + return; + } + if (emailInput && !emailInput.checkValidity()) { + setStatus(status, config.labels.invalidEmail || '请输入有效的邮箱地址。', 'error'); + emailInput.focus(); + return; + } + + button.disabled = true; + button.classList.add('is-loading'); + button.textContent = config.labels.sending || '发送中…'; + setStatus(status, config.labels.sending || '正在发送验证码…', 'info'); + + try { + const response = await window.fetch(config.ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + body: new URLSearchParams({ + action: 'itstudio_join_otp_send', + email: email, + nonce: config.nonce + }).toString() + }); + const result = await response.json(); + const payload = result && result.data ? result.data : {}; + button.classList.remove('is-loading'); + + if (!result.success) { + setStatus(status, getMessage(payload, config.labels.networkError || '发送失败,请稍后重试。'), 'error'); + if (payload.wait) { + startCooldown(button, payload.wait); + } else { + button.textContent = config.labels.send || '发送验证码'; + button.disabled = false; + } + return; + } + + setStatus(status, getMessage(payload, '验证码已发送,请检查邮箱及垃圾箱。'), 'success'); + startCooldown(button, payload.cooldown || config.cooldown); + } catch (error) { + button.classList.remove('is-loading'); + setStatus(status, config.labels.networkError || '网络错误,请稍后重试。', 'error'); + button.textContent = config.labels.send || '发送验证码'; + button.disabled = false; + } + } + + function mount() { + const form = findTargetForm(); + if (!form || form.querySelector('.itstudio-otp-wrap')) { + return; + } + + const codeInput = findField(form, config.codeFieldId); + if (!codeInput) { + return; + } + + codeInput.setAttribute('inputmode', 'numeric'); + codeInput.setAttribute('pattern', '[0-9]{6}'); + codeInput.setAttribute('maxlength', '6'); + codeInput.setAttribute('autocomplete', 'one-time-code'); + if (!codeInput.getAttribute('placeholder')) { + codeInput.setAttribute('placeholder', '请输入 6 位验证码'); + } + codeInput.classList.add('itstudio-otp-input'); + + const wrapper = document.createElement('div'); + wrapper.className = 'itstudio-otp-wrap'; + + const group = document.createElement('div'); + group.className = 'itstudio-otp-group'; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'itstudio-otp-send'; + button.textContent = config.labels.send || '发送验证码'; + + const status = document.createElement('div'); + status.className = 'itstudio-otp-status'; + status.setAttribute('role', 'status'); + status.setAttribute('aria-live', 'polite'); + status.style.display = 'none'; + + if (codeInput.parentNode) { + codeInput.parentNode.insertBefore(wrapper, codeInput); + group.appendChild(codeInput); + group.appendChild(button); + wrapper.appendChild(group); + wrapper.appendChild(status); + } + + button.addEventListener('click', function (e) { + e.preventDefault(); + sendCode(form, button, status); + }); + } + + mount(); + const observer = new MutationObserver(mount); + observer.observe(document.body, {childList: true, subtree: true}); +})(); \ No newline at end of file