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,中文零残留。
84 lines
4.5 KiB
HTML
84 lines
4.5 KiB
HTML
{{$pinTitle := t .LangCode "ask.pinned"}}
|
|
<div id="question-list">
|
|
{{range $index, $elem := .PageQuestions}}
|
|
<div data-qid="{{$elem.ID}}">
|
|
<hr>
|
|
<a class="uk-button uk-button-default uk-button-small uk-float-right"
|
|
href="/_/{{$.PageUser.Domain}}/{{$elem.ID}}">{{t $.LangCode "qlist.viewAnswer"}}</a>
|
|
<div class="uk-text-left uk-text-small uk-text-muted">
|
|
{{if $elem.IsPinned}}<span class="tb-pin-flag" title="{{t $.LangCode "ask.pinned"}}">⭐</span>{{end}}
|
|
{{Date $elem.CreatedAt "Y-m-d H:i:s"}}
|
|
</div>
|
|
<p class="uk-text-small uk-text-break tb-preline tb-clamp-5">{{MarkdownPlainText $elem.Content}}</p>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
<!-- 翻页游标与服务端 EncodePinCursor 保持一致:
|
|
isPinned(0/1) + createdAt 的 UnixNano + id 组成的复合键集游标。 -->
|
|
<!-- 初始 more 由服务端给出:首屏已展示全部提问时按钮直接不渲染,
|
|
之后每次加载以接口返回的 has_more 为准,最后一页加载完立即隐藏按钮。 -->
|
|
<div x-data="{ more: {{ if .PageQuestionHasMore }}true{{ else }}false{{ end }}, loading: false, cursor: '{{.PageQuestionCursor}}' }">
|
|
<button x-show="more" x-bind:disabled="loading" x-on:click.debounce="() => {
|
|
if (loading) return
|
|
loading = true
|
|
fetch(`/api/v1/user/{{.PageUser.Domain}}/questions?cursor=${cursor}`)
|
|
.then(response => response.json())
|
|
.then(resp => {
|
|
loading = false
|
|
// 外层为 ctx.JSON 的统一包装 {code, data, message},内层才是分页负载;
|
|
// code !== 0(接口报错)时保持 more 不变,仅复位 loading,下次可重试。
|
|
if (!resp || resp.code !== 0) return
|
|
const payload = resp.data || {}
|
|
let items = Array.isArray(payload.data) ? payload.data : []
|
|
if (items.length === 0) {
|
|
more = false
|
|
return
|
|
}
|
|
|
|
// 防重兜底:先扫描 #question-list 下全部已渲染条目(含服务端首屏与既往
|
|
// 追加)的 data-qid 构造“已见”集合,仅追加未出现过的提问。若服务端因游标
|
|
// 未前进等原因返回了与已见内容相同的批次(重复加载历史 bug 的表现形态),
|
|
// 直接按“到底”处理并隐藏按钮,杜绝无限重复追加。
|
|
const seenQIDs = new Set()
|
|
document.querySelectorAll('#question-list [data-qid]').forEach(el => seenQIDs.add(Number(el.dataset.qid)))
|
|
items = items.filter(q => !seenQIDs.has(q.id))
|
|
if (items.length === 0) {
|
|
more = false
|
|
return
|
|
}
|
|
|
|
// 服务端下发复合键集游标(见上方注释),前端不自行拼接;
|
|
// has_more 由服务端多取一条探测得出,最后一页加载完立即隐藏按钮。
|
|
more = payload.has_more !== false
|
|
cursor = payload.next_cursor || cursor
|
|
items.forEach(question => {
|
|
const div = document.createElement('div')
|
|
div.dataset.qid = question.id
|
|
div.innerHTML = `
|
|
<div>
|
|
<hr>
|
|
<a class='uk-button uk-button-default uk-button-small uk-float-right'
|
|
href='/_/{{.PageUser.Domain}}/${question.id}'>{{t .LangCode "qlist.viewAnswer"}}</a>
|
|
<div class='uk-text-left uk-text-small uk-text-muted'>
|
|
${question.is_pinned ? '<span class=\'tb-pin-flag\' title=\'{{$pinTitle}}\'>⭐</span>' : ''}
|
|
${dayjs(question.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
|
</div>
|
|
<p class='uk-text-small uk-text-break tb-preline tb-clamp-5'></p>
|
|
</div>`
|
|
div.getElementsByTagName('p')[0].innerText = question.content
|
|
document.getElementById('question-list').appendChild(div)
|
|
})
|
|
})
|
|
// 网络失败/响应非法 JSON 时复位 loading,避免按钮永远停留在加载中状态
|
|
.catch(() => { loading = false })
|
|
}" class="uk-button uk-button-default uk-width-1-1 uk-margin-small-bottom">
|
|
<span x-show="!loading">{{t .LangCode "qlist.loadMore"}}</span>
|
|
<span x-show="loading"><div uk-spinner="ratio: 0.8"></div> {{t .LangCode "qlist.loading"}}</span>
|
|
</button>
|
|
<div x-show="!more" class="uk-text-meta uk-text-center">
|
|
<hr>
|
|
{{t .LangCode "qlist.noMore"}}
|
|
<br><br>
|
|
</div>
|
|
</div>
|