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" }}