From 03cb5ddcf0c8ec05acba0c2fe9296e3c543f1356 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Thu, 29 Oct 2026 16:28:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E6=8F=90=E4=BA=A4=E9=98=B2=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E8=A6=86=E7=9B=96=E9=AA=8C=E8=AF=81=E7=A0=81=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E3=80=81waline=20=E6=8C=89=E9=A1=B5=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E3=80=81=E6=87=92=E5=8A=A0=E8=BD=BD=E3=80=81safe-area=E3=80=81?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E6=96=87=E6=A1=88=E6=8E=A5=E5=AD=97?= =?UTF-8?q?=E5=85=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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,中文零残留。 --- internal/i18n/dict_en.go | 4 ++ internal/i18n/dict_ja.go | 4 ++ internal/i18n/dict_zh_cn.go | 4 ++ internal/i18n/dict_zh_tw.go | 4 ++ static/css/base.css | 6 +- templates/base/footer.html | 68 ++++++++++++++-------- templates/base/header.html | 21 +++++-- templates/home.html | 3 + templates/question/history-template.html | 10 ++-- templates/question/item.html | 7 ++- templates/question/question-not-found.html | 2 +- templates/user/explore.html | 4 +- templates/vote/page.html | 2 +- 13 files changed, 98 insertions(+), 41 deletions(-) diff --git a/internal/i18n/dict_en.go b/internal/i18n/dict_en.go index 7fa7d3d..8732e3e 100644 --- a/internal/i18n/dict_en.go +++ b/internal/i18n/dict_en.go @@ -299,6 +299,9 @@ var enDict = map[string]string{ "qlist.unpinShort": "Unpin", "qlist.myVotes": "My votes", "qlist.loadMore": "Load more", + "qlist.noMore": "No more posts", + "qlist.loading": "Loading...", + "qlist.viewAnswer": "View answer", "qlist.newVote": "New vote", "qlist.edit": "Edit", "qlist.untitledVote": "Untitled vote", @@ -630,6 +633,7 @@ var enDict = map[string]string{ "vote.page.rankHint": "Rank each option 1-N (smaller = higher, no duplicates)", "vote.page.rankPlaceholder": "Rank", "vote.page.multipleHint": "Multiple choice", + "vote.page.multipleHintWithMax": "Multiple choice (up to %d)", "vote.page.customOption": "Custom option (optional)", "vote.page.customPlaceholder": "Type your own option", "vote.page.customOther": "Other", diff --git a/internal/i18n/dict_ja.go b/internal/i18n/dict_ja.go index 1feb0a0..86a08e0 100644 --- a/internal/i18n/dict_ja.go +++ b/internal/i18n/dict_ja.go @@ -299,6 +299,9 @@ var jaDict = map[string]string{ "qlist.unpinShort": "解除", "qlist.myVotes": "作成した投票", "qlist.loadMore": "もっと読み込む", + "qlist.noMore": "これ以上の投稿はありません", + "qlist.loading": "読み込み中...", + "qlist.viewAnswer": "回答を見る", "qlist.newVote": "投票を作成", "qlist.edit": "編集", "qlist.untitledVote": "無題の投票", @@ -629,6 +632,7 @@ var jaDict = map[string]string{ "vote.page.rankHint": "各選択肢に 1-N の順位を入力してください(小さいほど上位、重複不可)", "vote.page.rankPlaceholder": "順位", "vote.page.multipleHint": "複数選択可", + "vote.page.multipleHintWithMax": "複数選択可(最大 %d 件)", "vote.page.customOption": "カスタム選択肢(任意)", "vote.page.customPlaceholder": "あなたの選択肢を入力", "vote.page.customOther": "その他", diff --git a/internal/i18n/dict_zh_cn.go b/internal/i18n/dict_zh_cn.go index e104215..4539a23 100644 --- a/internal/i18n/dict_zh_cn.go +++ b/internal/i18n/dict_zh_cn.go @@ -299,6 +299,9 @@ var zhCNDict = map[string]string{ "qlist.unpinShort": "取消置顶", "qlist.myVotes": "我创建的投票", "qlist.loadMore": "加载更多", + "qlist.noMore": "无更多留言", + "qlist.loading": "加载中...", + "qlist.viewAnswer": "查看回答", "qlist.newVote": "新建投票", "qlist.edit": "编辑", "qlist.untitledVote": "未命名投票", @@ -630,6 +633,7 @@ var zhCNDict = map[string]string{ "vote.page.rankHint": "请为每个选项填写 1-N 的排名(数字越小越靠前,不可重复)", "vote.page.rankPlaceholder": "排名", "vote.page.multipleHint": "可多选", + "vote.page.multipleHintWithMax": "可多选(最多 %d 项)", "vote.page.customOption": "补充选项(可选)", "vote.page.customPlaceholder": "填写你心中的其他选项", "vote.page.customOther": "其他", diff --git a/internal/i18n/dict_zh_tw.go b/internal/i18n/dict_zh_tw.go index 0249d1d..7dd4449 100644 --- a/internal/i18n/dict_zh_tw.go +++ b/internal/i18n/dict_zh_tw.go @@ -299,6 +299,9 @@ var zhTWDict = map[string]string{ "qlist.unpinShort": "取消置頂", "qlist.myVotes": "我建立的投票", "qlist.loadMore": "載入更多", + "qlist.noMore": "無更多留言", + "qlist.loading": "載入中...", + "qlist.viewAnswer": "查看回答", "qlist.newVote": "新建投票", "qlist.edit": "編輯", "qlist.untitledVote": "未命名投票", @@ -629,6 +632,7 @@ var zhTWDict = map[string]string{ "vote.page.rankHint": "請為每個選項填寫 1-N 的排名(數字越小越靠前,不可重複)", "vote.page.rankPlaceholder": "排名", "vote.page.multipleHint": "可多選", + "vote.page.multipleHintWithMax": "可多選(最多 %d 項)", "vote.page.customOption": "補充選項(可選)", "vote.page.customPlaceholder": "填寫你心中的其他選項", "vote.page.customOther": "其他", diff --git a/static/css/base.css b/static/css/base.css index 22e1e6e..f65bd10 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -854,6 +854,10 @@ a.tb-chip:hover { } /* 给页面底部预留空间,避免页脚内容被固定底栏遮挡 */ body { - padding-bottom: 56px; + /* 必须加上 safe-area-inset-bottom:底栏自身的 padding 已经包含了它, + 而 body 只预留了固定的 56px。带 Home Indicator 的 iPhone 上 inset 约 34px, + 底栏实际高度约 88px —— 最后约 32px 内容(页脚备案、存活计时)会被压在栏下, + 滚到底也看不到。 */ + padding-bottom: calc(56px + env(safe-area-inset-bottom)); } } diff --git a/templates/base/footer.html b/templates/base/footer.html index efa894d..65e4309 100644 --- a/templates/base/footer.html +++ b/templates/base/footer.html @@ -62,6 +62,49 @@ }); } + // 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 = ' ' + 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; @@ -73,30 +116,7 @@ return; } - // 下载 / 新窗口类表单(target="_blank",如导出):当前页不发生导航,也没有 bfcache - // 复位机会,进入“提交中”状态后按钮会永远卡在转圈 + 禁用。这类表单直接跳过提交反馈。 - if (form.getAttribute('target') === '_blank') return; - - // 触发提交的按钮(programmatic submit 时 e.submitter 为 null,回退到首个 submit 按钮) - var trigger = e.submitter || 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 (trigger && trigger.tagName === 'BUTTON') { - trigger.setAttribute('data-tb-restore', trigger.innerHTML); - trigger.innerHTML = ' ' + label; - } - - // 禁用表单内所有按钮,防止重复提交 - form.querySelectorAll('button').forEach(function(b) { - b.disabled = true; - b.setAttribute('data-tb-submitting', '1'); - }); + tbBeginSubmit(form, e.submitter); }); // bfcache 还原时复位按钮 diff --git a/templates/base/header.html b/templates/base/header.html index 2db6fe8..06ca306 100644 --- a/templates/base/header.html +++ b/templates/base/header.html @@ -24,11 +24,9 @@ {{ if .ThemeSlug }} {{ end }} - - - + {{ if .VConsole }} @@ -36,7 +34,6 @@ {{ end }} - {{ if eq .CaptchaType "math" }}