1) 防重复点击原来漏了最主要的一条路径
footer 的全局 submit 监听会禁用按钮,但**程序化 form.submit() 不触发 submit 事件** ——
走 reCAPTCHA / Turnstile 的流程是在回调里调 form.submit() 的,而默认配置
captcha_type = recaptcha,也就是主提问流一直没有防重复。登录、注册、找回密码、
投票、注销账号同样走这个回调。
→ 把「切提交中状态」抽成 tbBeginSubmit(form, trigger) 挂到 window,
验证码回调先调它再 submit。7 个 data-callback="onSubmit" 的模板全部受益。
顺带加了 data-tb-restore 的重复覆盖保护,避免 bfcache 复位后按钮卡在转圈。
2) waline.js / waline.css 从全站改按页加载
只有 home.html 与 item.html 用评论区,但此前挂在 header 里 —— 登录、后台、投票、
私聊每页都下载解析 waline.js(2.15 未压缩约 200KB)。实测 /login 已无引用。
3) 图片懒加载:explore 列表头像与题目配图加 loading="lazy"(首屏推荐位不加,避免影响 LCP)。
4) 移动端底栏:body 只预留固定 56px,而底栏 padding 含 safe-area-inset-bottom。
带 Home Indicator 的 iPhone 上 inset 约 34px、底栏实际约 88px,最后约 32px
(页脚备案、存活计时)被压在栏下,滚到底也看不到。改成 calc(56px + env(...))。
5) 硬编码中文接字典(en/ja 用户此前直接看到中文):
history-template 的「查看回答 / 加载更多 / 加载中... / 无更多留言」、
单题页「@xxx 以前回答过的问题」(字典里本来就有 ask.history)、
投票页「(最多 N 项)」改为带参数的整句(原先前半走字典、后半手写中文)。
新增 4 个 key × 4 语言。
实测:箱主语言设为 en 后箱主页显示 View answer ×2 / Load more / No more posts,中文零残留。
127 lines
5.9 KiB
HTML
127 lines
5.9 KiB
HTML
<p class="{{ if .IsPixel }}uk-container{{end}} uk-text-right uk-text-muted uk-text-small">
|
||
<br>
|
||
<a href="/rules">{{t .LangCode "site.footer.rules"}}</a> |
|
||
<a href="/stats">{{t .LangCode "site.footer.stats"}}</a> |
|
||
<!--<a href="/pixel">画板</a> | -->
|
||
<a href="https://box.shiroko.one/_/tamakyi" target="_blank">{{t .LangCode "site.footer.admin"}}</a>
|
||
<br>
|
||
<a href="https://share.shiroko.one" target="_blank">{{t .LangCode "site.footer.drive"}}</a>
|
||
| <a href="https://blog.shiroko.one" target="_blank">{{t .LangCode "site.footer.notice"}}</a>
|
||
| <a href="https://chat.shiroko.one" target="_blank">{{t .LangCode "site.footer.chat"}}</a>
|
||
| <a href="https://status.shiroko.one" target="_blank">{{t .LangCode "site.footer.status"}}</a>
|
||
| <a href="https://shiroko.one" target="_blank">{{t .LangCode "site.footer.nav"}}</a>
|
||
<br>Powered by NekoBox | {{ ICP }}
|
||
<br>{{t .LangCode "site.footer.runtime"}}:<span id="box_runtime"></span>
|
||
</p>
|
||
|
||
<script>
|
||
// 页脚「提问箱已成功存活」倒计时:原 R2 cdn/time.box.js(1.6KB)内联于此,
|
||
// 消除全站每页一个 R2 慢 defer(实测 0.7-1.7s 波动)。相对原脚本两处修复:
|
||
// 1) 先判空再写入(原版先赋值后判空,box_runtime 缺失时会抛 ReferenceError 中断脚本);
|
||
// 2) 首帧立即填充(原版 DOMContentLoaded + 首个 1s 间隔后才显示,首秒为空)。
|
||
(function () {
|
||
var create = Math.round(Date.UTC(2023, 6, 1, 18, 15, 43) / 1000); // 提问箱创建时刻(UTC)
|
||
function tick() {
|
||
var el = document.getElementById("box_runtime");
|
||
if (!el) return;
|
||
var s = Math.round((Date.now() + 8 * 3600 * 1000) / 1000) - create; // UTC+8 秒级存活
|
||
var t = [0, 0, 0, 0, 0];
|
||
if (s >= 31536000) { t[0] = s / 31536000 | 0; s %= 31536000; }
|
||
if (s >= 86400) { t[1] = s / 86400 | 0; s %= 86400; }
|
||
if (s >= 3600) { t[2] = s / 3600 | 0; s %= 3600; }
|
||
if (s >= 60) { t[3] = s / 60 | 0; s %= 60; }
|
||
if (s > 0) { t[4] = s; }
|
||
el.innerHTML = t[0] + '年 ' + t[1] + '天 ' + t[2] + '时 ' + t[3] + '分 ' + t[4] + '秒 ';
|
||
}
|
||
tick();
|
||
setInterval(tick, 1000);
|
||
})();
|
||
</script>
|
||
<br>
|
||
</div>
|
||
|
||
<script>
|
||
// 提交反馈(P0):点击任意表单的提交按钮后,立即把触发按钮变成「转圈 + 本地化文字」,
|
||
// 并禁用表单内全部按钮,物理阻止在上传/网络慢时的重复点击与重复提交。
|
||
// 浏览器前进/后退(bfcache)回到本页时自动复位,避免停留在禁用或转圈状态。
|
||
// 纯内联反馈,不使用全屏遮罩。
|
||
var SUBMIT_I18N = {
|
||
submitting: '{{t .LangCode "common.submitting"}}',
|
||
uploading: '{{t .LangCode "common.uploading"}}'
|
||
};
|
||
|
||
function tbResetSubmitButtons() {
|
||
document.querySelectorAll('button[data-tb-submitting]').forEach(function(b) {
|
||
b.disabled = false;
|
||
b.removeAttribute('data-tb-submitting');
|
||
var orig = b.getAttribute('data-tb-restore');
|
||
if (orig !== null) {
|
||
b.innerHTML = orig;
|
||
b.removeAttribute('data-tb-restore');
|
||
}
|
||
});
|
||
}
|
||
|
||
// tbBeginSubmit 把表单切到「提交中」状态:触发按钮换成 spinner + 本地化文案,
|
||
// 并禁用表单内所有按钮。
|
||
//
|
||
// 抽成具名函数(而不是内联在下面的事件监听里)是因为 **程序化 submit() 不触发
|
||
// submit 事件**:走 reCAPTCHA / Turnstile 的流程是在回调里调 form.submit() 的,
|
||
// 原本完全绕过下面那个监听器 —— 而默认部署 captcha_type = recaptcha,
|
||
// 也就是主提问流一直没有防重复点击保护。那些回调现在先调本函数再 submit()。
|
||
function tbBeginSubmit(form, trigger) {
|
||
if (!form) return;
|
||
|
||
// 下载 / 新窗口类表单(target="_blank",如导出):当前页不发生导航,也没有 bfcache
|
||
// 复位机会,进入“提交中”状态后按钮会永远卡在转圈 + 禁用。这类表单直接跳过提交反馈。
|
||
if (form.getAttribute('target') === '_blank') return;
|
||
|
||
// 触发提交的按钮(programmatic submit 时调用方传 null,回退到首个 submit 按钮)
|
||
var btn = trigger || form.querySelector('button[type="submit"]');
|
||
|
||
// 表单含已选中的文件 → 提示“上传中”,否则“提交中”
|
||
var hasFile = false;
|
||
form.querySelectorAll('input[type="file"]').forEach(function(f) {
|
||
if (f.files && f.files.length > 0) hasFile = true;
|
||
});
|
||
var label = hasFile ? SUBMIT_I18N.uploading : SUBMIT_I18N.submitting;
|
||
|
||
if (btn && btn.tagName === 'BUTTON') {
|
||
// 已经切过一次就不再覆盖 data-tb-restore,否则会把 spinner 的 HTML 当成原始内容,
|
||
// 导致 bfcache 复位后按钮永远停在转圈上。
|
||
if (!btn.hasAttribute('data-tb-restore')) {
|
||
btn.setAttribute('data-tb-restore', btn.innerHTML);
|
||
}
|
||
btn.innerHTML = '<span uk-spinner="ratio: 0.6"></span> ' + label;
|
||
}
|
||
|
||
// 禁用表单内所有按钮,防止重复提交
|
||
form.querySelectorAll('button').forEach(function(b) {
|
||
b.disabled = true;
|
||
b.setAttribute('data-tb-submitting', '1');
|
||
});
|
||
}
|
||
// 挂到 window 上供 header 里的验证码回调调用(脚本顺序:header → footer,
|
||
// 但回调发生在用户点击之后,此时本函数已定义)
|
||
window.tbBeginSubmit = tbBeginSubmit;
|
||
|
||
document.addEventListener('submit', function(e) {
|
||
var form = e.target;
|
||
if (!form || form.tagName !== 'FORM') return;
|
||
|
||
// 页面级提交反馈必须尊重前置校验:投票编辑页等 onsubmit 返回 false 时,
|
||
// submit 事件仍可能继续到达此监听器;此时绝不能切换到“提交中”状态。
|
||
if (e.defaultPrevented || form.getAttribute('data-tb-skip-submit-feedback') === '1') {
|
||
form.removeAttribute('data-tb-skip-submit-feedback');
|
||
return;
|
||
}
|
||
|
||
tbBeginSubmit(form, e.submitter);
|
||
});
|
||
|
||
// bfcache 还原时复位按钮
|
||
window.addEventListener('pageshow', tbResetSubmitButtons);
|
||
</script>
|
||
</body>
|
||
</html>
|