diff --git a/assets/app.js b/assets/app.js index 0c0d3f9..22ec0fb 100644 --- a/assets/app.js +++ b/assets/app.js @@ -611,9 +611,93 @@ } } + /* ---------- 表单就地校验(不刷新页面,不打扰排版) ---------- + * 目标:必填项没填时,在字段下方立刻给出提示并把焦点移过去, + * 而不是先 POST 上去、整页重刷、再在顶部弹一条错误。 + * 规则来源:input 上原生 required / minlength / pattern / type=email。 + * 用法:给 form 加 data-validate 即可,其余零配置。 + * + * ⚠️ 必须在 bindNoDoubleSubmit 之前绑:后者会在 submit 时禁用按钮 5 秒, + * 若校验放在它之后,被拦下的表单按钮会被锁住,用户以为点坏了。 + */ + function bindInlineValidate() { + var forms = document.querySelectorAll('form[data-validate]'); + for (var i = 0; i < forms.length; i++) { + (function (form) { + form.setAttribute('novalidate', 'novalidate'); // 关掉浏览器默认的整页提示 + form.addEventListener('submit', function (ev) { + var bad = null, msg = ''; + var fields = form.querySelectorAll('input, textarea, select'); + for (var j = 0; j < fields.length; j++) { + var f = fields[j]; + if (f.disabled || f.type === 'hidden' || f.type === 'submit') { continue; } + var v = (f.value || '').trim(); + msg = ''; + if (f.hasAttribute('required') && v === '') { + msg = '这一项必须填写。'; + } else if (v !== '' && f.hasAttribute('minlength') && + v.length < Number(f.getAttribute('minlength'))) { + msg = '至少 ' + f.getAttribute('minlength') + ' 个字符,现在 ' + v.length + ' 个。'; + } else if (v !== '' && f.type === 'email' && + !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v)) { + msg = '邮箱格式不正确。'; + } else if (v !== '' && f.hasAttribute('pattern') && + !new RegExp('^(?:' + f.getAttribute('pattern') + ')$').test(v)) { + msg = f.getAttribute('data-pattern-msg') || '格式不正确。'; + } + if (msg) { bad = f; break; } + } + if (bad) { + ev.preventDefault(); + ev.stopImmediatePropagation(); // 拦住后续的"禁用按钮"逻辑 + fieldError(bad, msg); + return false; + } + clearAllErrors(form); + }, true); // ★ 捕获阶段:先于 bindNoDoubleSubmit 的冒泡处理 + })(forms[i]); + } + } + + /** 在字段下显示错误;重复调用只更新文字,不叠加 */ + function fieldError(field, msg) { + var wrap = field.closest('.field') || field.parentNode; + if (!wrap) { return; } + wrap.classList.add('has-error'); + var tip = wrap.querySelector('.field-error'); + if (!tip) { + tip = document.createElement('i'); + tip.className = 'field-error'; + wrap.appendChild(tip); + } + tip.textContent = msg; + // 卷到可见 + 聚焦,方便直接改 + field.focus({ preventScroll: false }); + field.scrollIntoView({ block: 'center', behavior: 'smooth' }); + // 用户一改就撤掉提示,不要一直红着 + var clear = function () { + wrap.classList.remove('has-error'); + if (tip && tip.parentNode) { tip.parentNode.removeChild(tip); } + field.removeEventListener('input', clear); + field.removeEventListener('change', clear); + }; + field.addEventListener('input', clear); + field.addEventListener('change', clear); + } + + function clearAllErrors(form) { + var errs = form.querySelectorAll('.field-error'); + for (var i = 0; i < errs.length; i++) { + if (errs[i].parentNode) { errs[i].parentNode.removeChild(errs[i]); } + } + var wraps = form.querySelectorAll('.has-error'); + for (var k = 0; k < wraps.length; k++) { wraps[k].classList.remove('has-error'); } + } + function boot() { bindCaptchaRefresh(); bindAdminToggle(); + bindInlineValidate(); // ★ 必须在 bindNoDoubleSubmit 之前 bindNoDoubleSubmit(); bindUploaders(); bindLightbox(); diff --git a/assets/style.css b/assets/style.css index 6e1d602..e28cdfd 100644 --- a/assets/style.css +++ b/assets/style.css @@ -81,6 +81,20 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh} .page-head h1{margin-bottom:4px} .page-head .muted{font-size:14px; margin:0} +/* ---------- 表单就地校验的错误提示 ---------- */ +.field.has-error > input, +.field.has-error > textarea, +.field.has-error > select, +.field.has-error .chips{ + border-color:#d9534f !important; + box-shadow:0 0 0 3px rgba(217,83,79,.12) !important; +} +.field-error{ + display:block; margin-top:6px; font-style:normal; + font-size:12.5px; color:#c9302c; line-height:1.5; +} +.field-error::before{content:"⚠ "; font-style:normal} + /* ---------- 后台:软件 / 分类 / 状态 ---------- */ .card-list{display:flex; flex-direction:column; gap:8px; margin-bottom:16px} .tax-row{ diff --git a/install.php b/install.php index bd2ee2e..4a95b14 100644 --- a/install.php +++ b/install.php @@ -424,7 +424,7 @@ function fb_install_self_delete() {