diff --git a/assets/css/routes/sign.css b/assets/css/routes/sign.css index d350e486..71eb0f8e 100644 --- a/assets/css/routes/sign.css +++ b/assets/css/routes/sign.css @@ -51,9 +51,7 @@ .sign-submit.jb-blue { background: linear-gradient(135deg, #59c3ff, #2b70ff); } .sign-submit.jb-green { background: linear-gradient(135deg, #50e3a4, #16a085); } .sign-mode-switch { justify-self: center; font-size: 12px; } -.sign-message, .sign-notice { margin-bottom: 16px; padding: 10px 12px; border-radius: 5px; background: rgba(255, 86, 86, .08); color: #e55353; font-size: 12px; } -.sign-message.is-success { background: rgba(45, 194, 117, .1); color: #1e9d5d; } .sign-hint { margin-top: -10px; color: var(--muted-2-color, #999); font-size: 11px; } .sign-agreement { display: flex; align-items: flex-start; gap: 6px; margin: 0; color: var(--muted-2-color, #999); font-size: 11px; font-weight: 400; } .sign-agreement a { color: var(--focus-color, #f04494); } diff --git a/assets/js/auth.js b/assets/js/auth.js index cf28877a..b366ec51 100644 --- a/assets/js/auth.js +++ b/assets/js/auth.js @@ -30,13 +30,15 @@ return data; } - function showMessage(root, text, success) { - var active = root.querySelector('[data-auth-panel]:not([hidden])'); - var box = active && active.querySelector('[data-auth-message]'); - if (!box) return; - box.textContent = text || ''; - box.hidden = !text; - box.classList.toggle('is-success', !!success); + function escapeHtml(value) { + return String(value || '').replace(/[&<>"']/g, function (char) { + return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]; + }); + } + + function showToast(text, type) { + if (!text) return; + window.notyf(escapeHtml(text), type || 'info'); } function activate(root, tab, updateUrl) { @@ -44,7 +46,6 @@ if (!panel) return; root.querySelectorAll('[data-auth-panel]').forEach(function (item) { item.hidden = item !== panel; }); root.dataset.activeTab = tab; - showMessage(root, '', false); if (updateUrl && root.dataset.surface === 'page' && window.history && window.URL) { var url = new URL(window.location.href); url.searchParams.set('tab', tab); @@ -63,42 +64,45 @@ box.dataset.loading = '0'; box.dataset.loaded = '1'; box.innerHTML = result.qr_html || ''; - if (result.mode === 'scan' && result.token) pollQr(root, box, result.token, 0); + if (result.mode === 'scan' && result.token) pollQr(box, result.token, 0); }).catch(function (error) { box.dataset.loading = '0'; - box.innerHTML = '' + String(error.message).replace(/[&<>"']/g, '') + ''; + box.dataset.loaded = '0'; + box.innerHTML = ''; + showToast(error.message, 'danger'); }); } - function pollQr(root, box, token, count) { + function pollQr(box, token, count) { if (!document.documentElement.contains(box) || count >= 100) return; window.setTimeout(function () { request('check-qrcode-login', { token: token }).then(function (result) { if (result.logged_in) { - showMessage(root, '登录成功', true); + showToast('登录成功', 'success'); window.location.assign(result.redirect || window.location.href); return; } if (result.status === 'expired') { box.dataset.loaded = '0'; box.innerHTML = ''; + showToast('二维码已过期,请刷新后重试', 'warning'); return; } - pollQr(root, box, token, count + 1); - }).catch(function () { pollQr(root, box, token, count + 1); }); + pollQr(box, token, count + 1); + }).catch(function () { pollQr(box, token, count + 1); }); }, 3000); } - function submitQrCode(root, form) { + function submitQrCode(form) { var data = formData(form); var button = form.querySelector('button[type="submit"]'); if (button) button.disabled = true; request('weixingzh-code', data).then(function (result) { - showMessage(root, '登录成功', true); + showToast('登录成功', 'success'); window.location.assign(result.redirect || window.location.href); }).catch(function (error) { if (button) button.disabled = false; - showMessage(root, error.message, false); + showToast(error.message, 'danger'); }); } @@ -148,11 +152,11 @@ if (captcha && typeof captcha.reset === 'function') captcha.reset(); } - function sendCode(root, button) { + function sendCode(button) { var form = button.closest('form'); var target = form && form.querySelector('[name="target"]'); if (!target || !target.value.trim()) { - showMessage(root, '请先填写邮箱或手机号', false); + showToast('请先填写邮箱或手机号', 'warning'); return; } button.disabled = true; @@ -163,6 +167,7 @@ verifyCaptcha(payload.scene, function (verified) { if (!verified) { button.disabled = false; + showToast('请先完成人机验证', 'warning'); return; } payload = formData(form); @@ -170,30 +175,29 @@ payload.target = target.value.trim(); payload.channel = (form.querySelector('[name="channel"]') || {}).value || ''; request('code', payload).then(function (result) { - showMessage(root, result.message || '验证码已发送', true); + showToast(result.message || '验证码已发送', 'success'); startCountdown(button, result.countdown || 60); }).catch(function (error) { button.disabled = false; - showMessage(root, error.message, false); + showToast(error.message, 'danger'); }).finally(function () { resetCaptcha(payload.scene); }); }); } - function submit(root, form) { + function submit(form) { var action = form.dataset.authForm; var payload = formData(form); if (action === 'register' && payload.channel !== 'phone') { payload.user_email = payload.target; } setBusy(form, true); - showMessage(root, '', false); request(action, payload).then(function (result) { - showMessage(root, result.message || '操作成功', true); + showToast(result.message || '操作成功', 'success'); if (result.redirect) window.setTimeout(function () { window.location.assign(result.redirect); }, 350); }).catch(function (error) { - showMessage(root, error.message, false); + showToast(error.message, 'danger'); setBusy(form, false); }); } @@ -212,6 +216,7 @@ root.querySelectorAll('input').forEach(function (input) { if (!input.hasAttribute('placeholder')) input.setAttribute('placeholder', ' '); }); + root.querySelectorAll('form').forEach(function (form) { form.noValidate = true; }); var initial = root.dataset.initialTab || 'signin'; if (initial === 'oauth' && window.matchMedia && window.matchMedia('(max-width: 767px)').matches) initial = 'signin'; activate(root, initial, false); @@ -226,7 +231,7 @@ return; } var send = event.target.closest('[data-send-code]'); - if (send) { event.preventDefault(); sendCode(root, send); return; } + if (send) { event.preventDefault(); sendCode(send); return; } var mode = event.target.closest('[data-login-mode]'); if (mode) { event.preventDefault(); toggleLoginMode(root, mode); } var refreshQr = event.target.closest('[data-auth-refresh-qr]'); @@ -236,17 +241,25 @@ var qrForm = event.target.closest('[data-aksrt-weixingzh-code]'); if (qrForm) { event.preventDefault(); - if (!qrForm.reportValidity || qrForm.reportValidity()) submitQrCode(root, qrForm); + if (validateForm(qrForm)) submitQrCode(qrForm); return; } var form = event.target.closest('[data-auth-form]'); if (!form) return; event.preventDefault(); - if (form.reportValidity && !form.reportValidity()) return; - submit(root, form); + if (!validateForm(form)) return; + submit(form); }); } + function validateForm(form) { + if (!form.checkValidity || form.checkValidity()) return true; + var field = form.querySelector(':invalid'); + showToast((field && field.validationMessage) || '请完整填写表单', 'warning'); + if (field && typeof field.focus === 'function') field.focus(); + return false; + } + var modal; function authPageUrl(tab, redirect) { var routes = config.routes || {}; diff --git a/src/Core/ScriptsPart1c.php b/src/Core/ScriptsPart1c.php index 83013e76..2517688b 100644 --- a/src/Core/ScriptsPart1c.php +++ b/src/Core/ScriptsPart1c.php @@ -404,7 +404,7 @@ trait ScriptsPart1c { wp_enqueue_script( 'aksrt-auth', $base_uri . '/assets/js/auth.js', - ['aksrt-runtime'], + ['aksrt-notyf', 'aksrt-runtime'], (string) filemtime($auth_js), true ); diff --git a/templates/parts/auth-content.php b/templates/parts/auth-content.php index c9ee41d1..1a5d17d3 100644 --- a/templates/parts/auth-content.php +++ b/templates/parts/auth-content.php @@ -56,7 +56,6 @@ $oauth_provider_name = $is_oauth_bind ? \AKSRT\Auth\Service::get_oauth_provider_
>

-
@@ -81,7 +80,6 @@ $oauth_provider_name = $is_oauth_bind ? \AKSRT\Auth\Service::get_oauth_provider_
>

-
@@ -101,7 +99,6 @@ $oauth_provider_name = $is_oauth_bind ? \AKSRT\Auth\Service::get_oauth_provider_
>

-
@@ -115,13 +112,11 @@ $oauth_provider_name = $is_oauth_bind ? \AKSRT\Auth\Service::get_oauth_provider_
>

-
>

-
diff --git a/tests/widget-system/auth-source-contract.test.cjs b/tests/widget-system/auth-source-contract.test.cjs index b8a09e7c..ceeb244d 100644 --- a/tests/widget-system/auth-source-contract.test.cjs +++ b/tests/widget-system/auth-source-contract.test.cjs @@ -22,6 +22,7 @@ test('modal and sign page use one renderer and one state machine', () => { assert.match(shared, /data-auth-panel="bind"/); assert.match(shared, /data-auth-panel="oauth"/); assert.match(scripts, /assets\/js\/auth\.js/); + assert.match(scripts, /'aksrt-notyf', 'aksrt-runtime'/); assert.doesNotMatch(scripts, /modal-auth\.js|sign-register\.js/); assert.doesNotMatch(runtime, /signsubmit-loader|tbquire\(\['sign-register'\]\)|\.on\('click', '\.(?:signin|signup)-loader'/); assert.doesNotMatch(loader, /sign-register/); @@ -172,7 +173,12 @@ test('shared renderer contracts every configured authentication state', () => { assert.match(client, /aksrtAuthAutoUntil/); assert.match(client, /data-password-show/); assert.match(client, /pollQr/); + assert.match(client, /window\.notyf\(escapeHtml\(text\), type \|\| 'info'\)/); + assert.match(client, /showToast\('请先完成人机验证', 'warning'\)/); + assert.match(client, /form\.noValidate = true/); + assert.match(client, /form\.checkValidity\(\)[\s\S]*?field\.validationMessage/); assert.match(client, /window\.location\.assign\(authPageUrl/); + assert.doesNotMatch(client + shared, /showMessage|data-auth-message|sign-message/); assert.match(settings, /invitation_mode/); assert.match(settings, /\['close', 'open', 'must'\]/); });