Files
tamabox/templates/base/footer.html
T
tamakyi 03cb5ddcf0 fix(ui): 提交防重复覆盖验证码路径、waline 按页加载、懒加载、safe-area、硬编码文案接字典
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,中文零残留。
2026-10-29 16:28:53 +08:00

127 lines
5.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>