chore: baseline snapshot before moderation-strictness feature

Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
{{$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}}">查看回答</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}'>查看回答</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">加载更多</span>
<span x-show="loading"><div uk-spinner="ratio: 0.8"></div> 加载中...</span>
</button>
<div x-show="!more" class="uk-text-meta uk-text-center">
<hr>
无更多留言
<br><br>
</div>
</div>
+425
View File
@@ -0,0 +1,425 @@
{{template "base/header" .}}
<link rel="shortcut icon"
href="{{.PageUser.Avatar}}?x-oss-process=image/auto-orient,1/quality,q_70/sharpen,200/resize,limit_0,m_fill,w_200,h_200"/>
<link rel="apple-touch-icon"
href="{{.PageUser.Avatar}}?x-oss-process=image/auto-orient,1/quality,q_70/sharpen,200/resize,limit_0,m_fill,w_200,h_200"/>
<link rel="icon" sizes="192x192"
href="{{.PageUser.Avatar}}?x-oss-process=image/auto-orient,1/quality,q_70/sharpen,200/resize,limit_0,m_fill,w_200,h_200">
<style>
a.uk-button.uk-button-link {
text-transform: none;
}
/* 回答行:左侧「回答」与右侧「标记为已读/标记为未读」按钮等高 */
.tb-ans-row .uk-button {
height: 40px;
line-height: 38px;
}
.tb-followup-card {
border: 1px solid #e7e9ec;
border-left: 3px solid #b0b6bd;
border-radius: 8px;
padding: 12px 14px;
background: #fbfbfc;
}
html[data-theme="dark"] .tb-followup-card {
background: #1b1e23;
border-color: #2c2f36;
border-left-color: #3a4250;
}
html[data-theme="dark"] .tb-followup-card .uk-button-default {
background-color: #2c2f36;
color: #e5e9ec;
border-color: #3a4250;
}
html[data-theme="dark"] .tb-followup-card .uk-button-default:hover {
background-color: #353941;
color: #ffffff;
}
.tb-img-uploader {
width: 100%;
background-color: #f8f8f8;
display: flex;
padding-top: 5px;
padding-bottom: 5px;
align-items: center;
justify-content: flex-start;
gap: 5px;
}
html[data-theme="dark"] .tb-img-uploader {
background-color: #1b1e23;
}
</style>
<div>
<div class="uk-card uk-card-default">
<div class="uk-card-header">
<div class="uk-text-left uk-text-small uk-text-muted">{{Date .Question.CreatedAt "Y-m-d H:i:s"}}</div>
<div class="md-body uk-text-break uk-margin-top uk-margin-bottom">{{MarkdownFormat .Question.Content}}</div>
{{ if .AskUploadImages }}
<ul class="uk-thumbnav" uk-margin>
{{ range $index, $elem := .AskUploadImages }}
<div uk-lightbox>
<a href="https://{{ ImageBucketCDNHost }}/{{ $elem.Key }}">
<img class="uk-border-rounded uk-object-fill" src="https://{{ ImageBucketCDNHost }}/{{ $elem.Key }}" width="100" height="100" alt="">
</a>
</div>
{{ end }}
</ul>
{{ end }}
</div>
{{if ne .Question.Answer ""}}
<div class="uk-card-body">
<!--<div class="uk-text-left uk-text-small uk-text-muted">{{Date .Question.UpdatedAt "Y-m-d H:i:s"}}</div>-->
<div id="answer-display">
<div class="md-body uk-text-break">{{MarkdownFormat .Question.Answer}}</div>
{{ if .AnswerUploadImages }}
<ul class="uk-thumbnav" uk-margin>
{{ range $index, $elem := .AnswerUploadImages }}
<div uk-lightbox>
<a href="https://{{ ImageBucketCDNHost }}/{{ $elem.Key }}">
<img class="uk-border-rounded uk-object-fill" src="https://{{ ImageBucketCDNHost }}/{{ $elem.Key }}" width="100" height="100" alt="">
</a>
</div>
{{ end }}
</ul>
{{ end }}
</div>
<p class="uk-text-small uk-text-right uk-text-muted">{{t .LangCode "qitem.answeredBy" .PageUser.Name (Date .Question.UpdatedAt "Y-m-d H:i:s")}}</p>
<!-- 分享此提问(左)与 更新回答(右)一左一右分开;箱主未登录(无更新回答按钮)时分享按钮居中;编辑区在下方由 tbToggleAnswerEditor 控制 -->
<div class="uk-flex uk-flex-middle uk-flex-wrap uk-margin-small-top {{ if not .CanDelete }}uk-flex-center{{ end }}" style="gap: 8px;">
{{ if and (not .Question.IsPrivate) (ne .Question.Answer "") }}
<a class="uk-button uk-button-default uk-button-small" href="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/share-card" target="_blank">📤 {{t .LangCode "qitem.shareCard"}}</a>
{{ end }}
{{ if .CanDelete }}
<div id="answer-edit-trigger" style="margin-left: auto;">
<button type="button" class="uk-button uk-button-default uk-button-small" onclick="tbToggleAnswerEditor()">{{t .LangCode "qitem.updateAnswer"}}</button>
</div>
{{ end }}
</div>
{{ if not .CanDelete }}
<div id="waline"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const waline = Waline.init({
el: '#waline',
//后端服务器地址
serverURL: 'https://waline.337845818.best/',
path: window.location.href,
// 中文默认
locale: {
nick: '昵称',
nickError: '昵称不能少于3个字符',
mail: '邮箱',
mailError: '请填写正确的邮件地址',
link: '网址',
optional: '可选',
placeholder: '欢迎评论(需要手工审核,不会立即展现)',
sofa: '来发评论吧~',
submit: '提交',
like: '喜欢',
cancelLike: '取消喜欢',
reply: '回复',
cancelReply: '取消回复',
comment: '评论',
refresh: '刷新',
more: '加载更多...',
preview: '预览',
emoji: '表情',
uploadImage: '上传图片',
seconds: '秒前',
minutes: '分钟前',
hours: '小时前',
days: '天前',
now: '刚刚',
uploading: '正在上传',
login: '登录',
logout: '退出',
admin: '博主',
sticky: '置顶',
word: '字',
wordHint: '评论字数应在 $0 到 $1 字之间!\n当前字数:$2',
anonymous: '无名小狼',
level0: '潜水',
level1: '冒泡',
level2: '吐槽',
level3: '活跃',
level4: '话痨',
level5: '传说',
gif: '表情包',
gifSearchPlaceholder: '搜索表情包',
profile: '个人资料',
approved: '通过',
waiting: '待审核',
spam: '垃圾',
unsticky: '取消置顶',
oldest: '按倒序',
latest: '按正序',
hottest: '按热度',
reactionTitle: '',
reaction0: '哇哦',
reaction1: 'prpr',
reaction2: '流汗',
reaction3: '生气',
reaction4: '流鼻血',
reaction5: 'あべ',
},
reaction: [
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGQ3OTRhYzRjMTBkZmZhZDVmZTA1EIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChQwOGEyOTlhYzRjMTBmOWFkZjAwYRCIzNcv.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGRhOTVhYzRjMTBkNmViYTVlNTA2EIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGU1OTJhYzRjMTBmMzk3ZmVmNTAyEIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGNkODdhYzRjMTBjYmEwOTFjNjAxEIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChQwODliOGVhYzRjMTBmOGM0ZWY1ZRCIzNcv.gif',
],
meta: ['nick','mail'],
requiredMeta: ['mail'],
pageview: true,
search: false,
comment: false,
login: 'disable',
wordLimit: 50,
pageSize: 5,
avatar: 'monsterid',
emoji: [
'https://gcore.jsdelivr.net/gh/tamakyi/emoji-collection@main/acfun-color/60x60',
],
//禁止图片上传按钮
imageUploader: false,
//不显示版权标志
copyright: false,
});
});
</script>
{{ end }}
{{ if .CanDelete }}
<div id="answer-editor" style="display:none;">
<hr>
<form method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/answer" enctype="multipart/form-data">
{{ .CSRFTokenHTML }}
<div class="uk-margin uk-text-center">
<textarea name="answer" class="uk-textarea" rows="5" maxlength="{{ .Limits.AnswerContentLen }}"
placeholder="{{t .LangCode "qitem.answerPlaceholder"}}">{{ .Question.Answer }}</textarea>
<div x-data="{ files: null }" class="tb-img-uploader">
<div uk-form-custom>
<a href="#" class="uk-icon-link" uk-icon="image" style="margin-left: 10px"></a>
<span style="font-size: 12px" x-text="files ? files[0].name : '{{t .LangCode "ask.addImage"}}'"></span>
<input id="uploader" name="images" type="file" accept="image/*" x-on:change="files = Object.values($event.target.files)">
</div>
<button x-show="files" type="button" class="uk-icon-link" uk-icon="close" style="margin-left: 10px;"
@click="files = null"></button>
</div>
</div>
{{ if ne .Question.ReceiveReplyEmail "" }}
<div class="uk-alert-warning uk-text-small" uk-alert>
<p>{{t .LangCode "qitem.emailNotifyHint"}}</p>
</div>
{{ end }}
<div class="uk-margin uk-text-center">
<button type="submit" class="uk-button uk-button-primary uk-button-small">{{t .LangCode "qitem.updateAnswer"}}</button>
<button type="button" class="uk-button uk-button-default uk-button-small uk-margin-small-left" onclick="tbToggleAnswerEditor()">{{t .LangCode "common.cancel"}}</button>
</div>
</form>
</div>
{{ end }}
</div>
{{ else if .CanDelete }}
<div class="uk-card-body uk-padding-remove-top">
<hr>
<h5 class="uk-text-center">{{t .LangCode "qitem.answerTitle"}}</h5>
<form id="tb-answer-form" method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/answer" enctype="multipart/form-data">
{{ .CSRFTokenHTML }}
<div class="uk-margin uk-text-center">
<textarea name="answer" class="uk-textarea" rows="5" maxlength="{{ .Limits.AnswerContentLen }}"
placeholder="{{t .LangCode "qitem.answerPlaceholder"}}">{{ .answer }}</textarea>
<div x-data="{ files: null }" style="width: 100%; background-color: #f8f8f8; display: flex; padding-top: 5px; padding-bottom: 5px; align-items: center; justify-content: start;gap: 5px">
<div uk-form-custom>
<a href="#" class="uk-icon-link" uk-icon="image" style="margin-left: 10px"></a>
<span style="font-size: 12px" x-text="files ? files[0].name : '{{t .LangCode "ask.addImage"}}'"></span>
<input id="uploader" name="images" type="file" accept="image/*" x-on:change="files = Object.values($event.target.files)">
</div>
<button x-show="files" type="button" class="uk-icon-link" uk-icon="close" style="margin-left: 10px;"
@click="files = null"></button>
</div>
</div>
{{ if ne .Question.ReceiveReplyEmail "" }}
<div class="uk-alert-warning uk-text-small" uk-alert>
<p>{{t .LangCode "qitem.emailNotifyHint"}}</p>
</div>
{{ end }}
</form>
<!-- 回答(左)+ 阅读状态(右)同一行,压缩空间;tb-ans-row 保证左右按钮等高 -->
<div class="uk-flex uk-flex-middle uk-flex-wrap uk-margin-small-top tb-ans-row" style="gap: 12px; justify-content: space-between;">
<button type="submit" form="tb-answer-form" class="uk-button uk-button-primary">{{t .LangCode "qitem.submitAnswer"}}</button>
<div class="uk-flex uk-flex-middle uk-flex-wrap" style="gap: 8px;">
{{ if .Question.IsRead }}
<span class="uk-text-small uk-text-muted">✓ {{t .LangCode "qlist.read"}}</span>
<form class="uk-display-inline" method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/unread">
{{ .CSRFTokenHTML }}
<button type="submit" class="uk-button uk-button-default">{{t .LangCode "qitem.markUnread"}}</button>
</form>
{{ else }}
<form method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/read">
{{ .CSRFTokenHTML }}
<button type="submit" class="uk-button uk-button-default">{{t .LangCode "qitem.markRead"}}</button>
</form>
{{ end }}
</div>
</div>
</div>
{{ end }}
<!-- 已回答的阅读状态(箱主):该行仅此一个元素,居中显示 -->
{{ if and .CanDelete (ne .Question.Answer "") }}
<div class="uk-card-body uk-padding-remove-top">
<div class="uk-text-center">
<span class="uk-label uk-label-success">✓ {{t .LangCode "qlist.read"}}</span>
<span class="uk-text-small uk-text-muted">{{t .LangCode "qitem.readLocked"}}</span>
</div>
</div>
{{ end }}
{{ if .FollowUps }}
<div class="uk-card-body uk-padding-remove-top">
{{ range $index, $elem := .FollowUps }}
<div class="tb-followup-card uk-margin-small-bottom">
<p class="uk-text-small uk-text-break uk-margin-remove-bottom"><strong>{{t $.LangCode "followup.content"}}</strong>{{ $elem.Content }}</p>
{{ if ne $elem.Answer "" }}
{{ if $.CanDelete }}
<div id="fu-display-{{ $index }}" class="uk-text-small uk-text-muted uk-text-break uk-margin-small-top">@{{ $.PageUser.Name }}:{{ MarkdownFormat $elem.Answer }}</div>
<div class="uk-margin-small-top uk-text-right">
<button type="button" class="uk-button uk-button-default uk-button-small" onclick="tbToggleFollowUpEditor({{ $index }})">{{t $.LangCode "qitem.updateAnswer"}}</button>
</div>
<div id="fu-editor-{{ $index }}" style="display:none;">
<hr>
<form method="post" action="/_/{{ $.PageUser.Domain }}/{{ $.Question.ID }}/follow-up/{{ $elem.ID }}/answer" class="uk-margin-small-top">
{{ $.CSRFTokenHTML }}
<textarea name="answer" class="uk-textarea" rows="3" maxlength="{{ $.Limits.AnswerContentLen }}" placeholder="{{t $.LangCode "followup.answerPlaceholder"}}">{{ $elem.Answer }}</textarea>
<div class="uk-margin-small-top uk-text-right">
<button type="submit" class="uk-button uk-button-primary uk-button-small">{{t $.LangCode "qitem.updateAnswer"}}</button>
<button type="button" class="uk-button uk-button-default uk-button-small uk-margin-small-left" onclick="tbToggleFollowUpEditor({{ $index }})">{{t $.LangCode "common.cancel"}}</button>
</div>
</form>
</div>
{{ else }}
<div class="uk-text-small uk-text-muted uk-text-break uk-margin-small-top">@{{ $.PageUser.Name }}:{{ MarkdownFormat $elem.Answer }}</div>
{{ end }}
{{ else if $.CanDelete }}
<form method="post" action="/_/{{ $.PageUser.Domain }}/{{ $.Question.ID }}/follow-up/{{ $elem.ID }}/answer" class="uk-margin-small-top">
{{ $.CSRFTokenHTML }}
<textarea name="answer" class="uk-textarea" rows="3" maxlength="{{ $.Limits.AnswerContentLen }}" placeholder="{{t $.LangCode "followup.answerPlaceholder"}}"></textarea>
<div class="uk-margin-small-top uk-text-right">
<button type="submit" class="uk-button uk-button-primary uk-button-small">{{t $.LangCode "followup.answerBtn"}}</button>
</div>
</form>
{{ else }}
<p class="uk-text-small uk-text-muted uk-margin-small-top">{{t $.LangCode "followup.waiting"}}</p>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
{{ if and .CanFollowUp (not .CanDelete) }}
<div class="uk-card-body uk-padding-remove-top">
<div class="tb-followup-card">
<h5 class="uk-margin-remove-bottom">{{t .LangCode "followup.askMore"}}</h5>
<p class="uk-text-small uk-text-muted">{{t .LangCode "followup.remaining" .FollowUpRemaining .FollowUpLimit}}</p>
<form method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/follow-up">
{{ .CSRFTokenHTML }}
<input type="hidden" name="token" value="{{ .QuestionToken }}">
<textarea name="content" class="uk-textarea" rows="3" maxlength="{{ .Limits.FollowUpContentLen }}" placeholder="{{t .LangCode "followup.contentPlaceholder"}}"></textarea>
<div class="uk-margin uk-text-center">
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "followup.submit"}}</button>
</div>
</form>
</div>
</div>
{{ end }}
{{ if and .WaitingFollowUpReply (not .CanDelete) }}
<div class="uk-card-body uk-padding-remove-top">
<div class="tb-followup-card uk-text-small uk-text-muted uk-text-center">{{t .LangCode "followup.waitingReply"}}</div>
</div>
{{ end }}
<div class="uk-card-footer">
{{template "base/alert" .}}
{{ if .CanDelete }}
{{ if .Question.IsPrivate }}
<form class="uk-float-right" method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/set-public">
{{ .CSRFTokenHTML }}
<button type="submit" class="uk-button uk-button-default uk-button-small">{{t .LangCode "qitem.public"}}</button>
</form>
{{ else }}
<form class="uk-float-right" method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/set-private">
{{ .CSRFTokenHTML }}
<button type="submit" class="uk-button uk-button-default uk-button-small">{{t .LangCode "qitem.private"}}</button>
</form>
{{ end }}
<a class="uk-button uk-button-danger uk-button-small" href="#">{{t .LangCode "qitem.delete"}}</a>
<div class="uk-dropbar uk-dropbar-top" uk-drop="stretch: x; mode: click">
<div class="uk-card uk-card-default uk-card-body">
<h3 class="uk-card-title">{{t .LangCode "qitem.danger"}}</h3>
<p>{{t .LangCode "qitem.deleteConfirm"}}</p>
<form class="uk-float-right"
method="post"
action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/delete">
{{ .CSRFTokenHTML }}
<button class="uk-button uk-button-danger">{{t .LangCode "qitem.deleteConfirmBtn"}}</button>
</form>
</div>
</div>
{{ end }}
<div class="uk-margin uk-text-center">
{{ $backTo := printf "/_/%s" .PageUser.Domain }}
{{ if eq .BackFrom "admin" }}{{ $backTo = .BackTo }}{{ end }}
<a class="uk-button uk-button-link" href="{{ $backTo }}">{{t .LangCode "qitem.backToBox" .PageUser.Name}}</a>
</div>
<!-- Box owner can't create new question here. -->
{{if not .IsOwnPage }}
<h5 class="uk-text-center">{{t .LangCode "qitem.askMore"}}</h5>
{{template "question/new-question-template" .}}
{{ end }}
<hr class="uk-divider-icon">
<p class="uk-text-left uk-text-muted uk-text-small">{{t .LangCode "ask.history" .PageUser.Name}} ({{ .AnsweredCount }})</p>
{{ template "question/history-template" . }}
</div>
</div>
</div>
<script>
function tbToggleAnswerEditor() {
var disp = document.getElementById('answer-display');
var trig = document.getElementById('answer-edit-trigger');
var ed = document.getElementById('answer-editor');
if (!ed) return;
if (ed.style.display === 'none') {
ed.style.display = 'block';
if (disp) disp.style.display = 'none';
if (trig) trig.style.display = 'none';
} else {
ed.style.display = 'none';
if (disp) disp.style.display = 'block';
if (trig) trig.style.display = 'block';
}
}
function tbToggleFollowUpEditor(idx) {
var disp = document.getElementById('fu-display-' + idx);
var ed = document.getElementById('fu-editor-' + idx);
if (!ed) return;
if (ed.style.display === 'none') {
ed.style.display = 'block';
if (disp) disp.style.display = 'none';
} else {
ed.style.display = 'none';
if (disp) disp.style.display = 'block';
}
}
</script>
{{template "base/footer" .}}
+310
View File
@@ -0,0 +1,310 @@
{{template "base/header" .}}
<!-- 头像直连 R2:crossorigin 让 favicon 也以 CORS 模式请求,确保浏览器缓存的响应都带 ACAO;
?v=2 击穿历史上被「无 ACAO 响应」污染的缓存条目(R2 对无 Origin 请求的响应无 Vary,可缓存 31 天) -->
<link rel="shortcut icon" href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous"/>
<link rel="apple-touch-icon" href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous"/>
<link rel="icon" sizes="192x192" href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous">
<style>
/* 投票列表:与探索页"全部投票"一致的列表行样式 */
.profile-votes-list { display: grid; gap: 0; }
.profile-vote-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-width: 0;
padding: 14px 16px;
border-bottom: 1px solid var(--tb-border, #e9e9e9);
text-decoration: none;
}
.profile-vote-item:first-child { border-top: 1px solid var(--tb-border, #e9e9e9); }
.profile-vote-item:hover { background: var(--tb-surface-hover, #f8fbff); text-decoration: none; }
.profile-vote-content { min-width: 0; }
.profile-vote-title {
display: block;
overflow: hidden;
color: var(--tb-text, #333);
font-size: 0.98rem;
font-weight: 600;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-vote-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 7px;
margin-top: 8px;
}
.profile-vote-meta .uk-label { font-size: 0.72rem; line-height: 1.6; }
.profile-vote-action {
flex: 0 0 auto;
color: var(--tb-primary, #1e87f0);
font-size: 0.82rem;
white-space: nowrap;
}
html[data-theme="dark"] .profile-vote-item { border-color: var(--tb-border); }
html[data-theme="dark"] .profile-vote-title { color: var(--tb-text); }
html[data-theme="dark"] .profile-vote-item:hover { background: rgba(78, 168, 245, 0.10); }
/* 个人主页 Tab:与探索页一致的胶囊切换样式 */
/* 个人主页 Tab:全宽等分,置于主体顶部,作为一级导航(移动端整行可点) */
.profile-tabs {
display: flex;
gap: 4px;
padding: 4px;
margin-top: 20px;
margin-bottom: 18px;
border: 1px solid var(--tb-border, #e5e5e5);
border-radius: 9px;
background: var(--tb-surface-2, #fafafa);
}
.profile-tab {
flex: 1;
border: 0;
border-radius: 6px;
padding: 9px 15px;
background: transparent;
color: var(--tb-text-muted, #666);
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s ease, color 0.2s ease;
}
.profile-tab:hover { color: var(--tb-primary, #1e87f0); }
.profile-tab.is-active {
background: var(--tb-surface, #fff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
color: var(--tb-primary, #1e87f0);
font-weight: 600;
}
/* 功能关闭时置灰禁用的 Tab(如投票功能关闭) */
.profile-tab.is-disabled,
.profile-tab:disabled {
cursor: not-allowed;
color: var(--tb-text-muted, #999);
opacity: 0.7;
}
.profile-tab.is-disabled:hover { color: var(--tb-text-muted, #999); }
.profile-panel[hidden] { display: none !important; }
html[data-theme="dark"] .profile-tabs {
border-color: var(--tb-border);
background: var(--tb-surface-2);
}
html[data-theme="dark"] .profile-tab { color: var(--tb-text-muted); }
html[data-theme="dark"] .profile-tab.is-active { background: var(--tb-surface); color: var(--tb-primary); }
html[data-theme="dark"] .profile-tab.is-disabled { color: var(--tb-text-muted); opacity: 0.55; }
</style>
<div class="uk-card uk-card-default uk-text-center">
<div class="uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light" style="min-height: 260px;" data-src="{{ .PageUser.Background }}" uk-img>
<div class="uk-card-body">
<img class="uk-border-circle uk-box-shadow-large" src="{{ if .PageUser.Avatar }}{{ .PageUser.Avatar }}?v=2{{ else }}{{ .DefaultAvatar }}{{ end }}" crossorigin="anonymous" style="width: 100px; height: 100px; max-width: 100%; max-height: 100%;">
<h3 id="usernamecolor">{{ .PageUser.Name }}</h3>
<p id="introcolor">{{ .PageUser.Intro }}</p>
<div class="icon-container">
{{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }}
<a href="https://{{.PageUser.Acfunlink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/ailicili.png" alt="acfunlink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }}
<a href="https://{{.PageUser.Bililink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/bili.png" alt="bililink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }}
<a href="https://{{.PageUser.Weibolink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/weibo.png" alt="weibolink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }}
<a href="https://{{.PageUser.Xiaohongshulink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/xiaohongshu.png" alt="xiaohongshulink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }}
<a href="https://{{.PageUser.Bloglink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/blog.png" alt="bloglink" class="icon">
</a>
{{ end }}
</div>
</div>
</div>
</div>
<script>
document.getElementById("usernamecolor").style.color = "{{.PageUser.Usernamecolor}}";
document.getElementById("introcolor").style.color = "{{.PageUser.Introcolor}}";
</script>
<div>
<div class="uk-card uk-card-default uk-card-small uk-card-body">
{{ if .CanAsk }}
<!-- jQuery 已改为按需加载:defer 按文档顺序先于 easy.qrcode 执行,保证就绪 -->
<script defer src="https://cdnjs.snrat.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script defer src='https://r2.337845818.best/tamabox/cdn/js/remake/easy.qrcode.min.js' type="text/javascript" charset="utf-8"></script>
<center><div id="boxqrimg"></div></center>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function () {
var options = {
text: window.location.href,
width: 200,
height: 200,
// 与 Profile 编辑器保持一致:仅当启用自定义前景色时,才把 Qrcodebackcolor 作为前景色;
// 否则强制黑色 #000000,避免未勾选却显示自定义前景色、与 Profile 预览不一致。
colorDark: ({{.PageUser.UseCustomQRColor}} ? "{{.PageUser.Qrcodebackcolor}}" : "#000000") || "#000000",
colorLight: "{{.PageUser.Qrcodecolor}}" || "#FFFFFF",
dotScale: parseFloat("{{.PageUser.Dotscale}}") || 0.4,
dotScaleTiming: parseFloat("{{.PageUser.Dotscale}}") || 0.4,
dotScaleA: parseFloat("{{.PageUser.Dotscale}}") || 0.4,
correctLevel: QRCode.CorrectLevel.H,
logo: (function () {
// ?v=2:击穿曾被无 ACAO 响应污染的浏览器缓存条目;仅对 http(s) 绝对地址追加
var a = "{{.PageUser.Avatar}}" || "{{.DefaultAvatar}}";
return a ? (a.indexOf("http") === 0 ? a + "?v=2" : a) : undefined;
})(),
logoWidth: 60,
logoHeight: 60,
logoBackgroundTransparent: true,
// 直连 R2 等跨域图片须以 CORS 模式加载,canvas 才不被污染、可正常导出
crossOrigin: 'anonymous',
backgroundImage: ("{{.PageUser.BackgroundImage}}" ? "{{.PageUser.BackgroundImage}}" : undefined),
backgroundImageAlpha: parseFloat("{{.PageUser.Backgroundimagealpha}}") || 0.8,
autoColor: false,
PO: "{{.PageUser.Qrcodepdpcolor}}" || "#000000",
PI: "{{.PageUser.Qrcodepdpcolor}}" || "#000000",
AO: '',
AI: '',
timing: "{{.PageUser.Qrcodepdpcolor}}" || "#000000"
};
// 降级渲染:头像(logo)/ 背景图加载慢/失败时先出基础码,探针成功后再升级带 logo 版
window.tbRenderQR(document.getElementById("boxqrimg"), function () { return options; });
});
</script>
<p class="uk-text-center uk-text-small">{{t .LangCode "ask.scan" .PageUser.Name}}<br>{{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}</p>
<div class="uk-text-center uk-margin-small-top">
<a id="share-btn" href="/_/{{ .PageUser.Domain }}/share-card" download class="uk-button uk-button-default uk-button-small">
📤 {{t .LangCode "ask.share"}}
</a>
</div>
<div id="share-loading-overlay" style="display:none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(255,255,255,0.85); z-index:9999;">
<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%;">
<div uk-spinner="ratio: 2" class="uk-margin-bottom"></div>
<p class="uk-text-large uk-text-muted">{{t .LangCode "ask.generating"}}</p>
</div>
</div>
<script>
(function() {
var btn = document.getElementById('share-btn');
var overlay = document.getElementById('share-loading-overlay');
if (!btn || !overlay) return;
btn.addEventListener('click', function(e) {
e.preventDefault();
overlay.style.display = 'block';
btn.style.pointerEvents = 'none';
fetch(btn.href)
.then(function(response) {
if (!response.ok) throw new Error('{{t .LangCode "ask.networkError"}}');
return response.blob();
})
.then(function(blob) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = '{{t .LangCode "ask.shareFilename"}}';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
})
.catch(function(err) {
alert('{{t .LangCode "ask.genFailed"}}');
console.error(err);
})
.finally(function() {
overlay.style.display = 'none';
btn.style.pointerEvents = 'auto';
});
});
})();
</script>
{{ end }}
{{template "base/alert" .}}
<!-- 提问 / 投票 切换:默认显示提问,支持 ?tab=votes 直达(服务端初始态)与 #votes hash 覆盖 -->
<div class="profile-tabs" role="tablist">
<button type="button" class="profile-tab{{ if eq .ActiveProfileTab "questions" }} is-active{{ end }}" data-profile-tab="questions" role="tab" aria-selected="{{ if eq .ActiveProfileTab "questions" }}true{{ else }}false{{ end }}">{{t .LangCode "explore.questionsTab"}}</button>
{{ if .VoteEnabled }}
<button type="button" class="profile-tab{{ if eq .ActiveProfileTab "votes" }} is-active{{ end }}" data-profile-tab="votes" role="tab" aria-selected="{{ if eq .ActiveProfileTab "votes" }}true{{ else }}false{{ end }}">{{t .LangCode "explore.votesTab"}}</button>
{{ else }}
<!-- 投票功能关闭:保留 Tab 展示但置灰禁用,hover 提示原因 -->
<button type="button" class="profile-tab is-disabled" disabled aria-disabled="true" uk-tooltip="{{ .VoteClosedMessage }}">{{t .LangCode "explore.votesTab"}} <span uk-icon="icon: lock; ratio: 0.8"></span></button>
{{ end }}
</div>
<div class="profile-panel" data-profile-panel="questions"{{ if eq .ActiveProfileTab "votes" }} hidden{{ end }}>
{{template "question/new-question-template" .}}
{{ if ne (len .PageQuestions) 0 }}
<p class="uk-text-left uk-text-muted uk-text-small">{{t .LangCode "ask.history" .PageUser.Name}} ({{ .AnsweredCount }})</p>
{{ template "question/history-template" . }}
{{ end }}
</div>
<div class="profile-panel" data-profile-panel="votes"{{ if ne .ActiveProfileTab "votes" }} hidden{{ end }}>
{{ if .PageVotes }}
<!-- 匿名投票板块:箱主"进行中"的公开投票(列表行样式,与探索页"全部投票"一致) -->
<div class="profile-votes-list">
{{ range .PageVotes }}
<a href="/vote/{{ .LinkToken }}" class="profile-vote-item">
<div class="profile-vote-content">
<span class="profile-vote-title" title="{{ .Title }}">{{ .Title }}</span>
<span class="profile-vote-meta">
{{ if eq .Type "single" }}<span class="uk-label uk-label-primary">{{t $.LangCode "vote.typeSingle"}}</span>{{ end }}
{{ if eq .Type "multiple" }}<span class="uk-label uk-label-primary">{{t $.LangCode "vote.typeMultiple"}}</span>{{ end }}
{{ if eq .Type "rating" }}<span class="uk-label uk-label-primary">{{t $.LangCode "vote.typeRating"}}</span>{{ end }}
{{ if eq .Type "rank" }}<span class="uk-label uk-label-primary">{{t $.LangCode "vote.typeRank"}}</span>{{ end }}
{{ if eq .Type "form" }}<span class="uk-label uk-label-primary">{{t $.LangCode "vote.typeForm"}}</span>{{ end }}
<span class="uk-text-meta">{{ .CreatedAt.Format "2006-01-02" }}</span>
</span>
</div>
<span class="profile-vote-action">{{t $.LangCode "vote.page.goVote"}} <span uk-icon="icon: arrow-right; ratio: .8"></span></span>
</a>
{{ end }}
</div>
{{ else }}
<div class="uk-alert uk-alert-primary uk-margin-remove-top">{{t .LangCode "explore.noVotes"}}</div>
{{ end }}
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var tabs = document.querySelectorAll('[data-profile-tab]');
var panels = document.querySelectorAll('[data-profile-panel]');
function showPanel(name) {
tabs.forEach(function(tab) {
var active = tab.dataset.profileTab === name;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
panels.forEach(function(panel) {
panel.hidden = panel.dataset.profilePanel !== name;
});
if (window.history && window.history.replaceState) {
window.history.replaceState({}, document.title, window.location.pathname + window.location.search + (name === 'votes' ? '#votes' : ''));
}
}
tabs.forEach(function(tab) {
tab.addEventListener('click', function() { showPanel(tab.dataset.profileTab); });
});
// 仅当投票 Tab 可点击(功能开启)时才响应 #votes hash,避免投票关闭时切到空面板
if (window.location.hash === '#votes' && document.querySelector('[data-profile-tab="votes"]')) {
showPanel('votes');
}
});
</script>
{{template "base/footer" .}}
@@ -0,0 +1,232 @@
{{ if .AskClosedMessage }}
<div>
<div uk-alert class="uk-text-center">
<p>{{ .AskClosedMessage }}</p>
{{ if .AskNextOpenAtStr }}
<p class="uk-text-small uk-text-muted">{{t .LangCode "ask.nextOpenAt" .AskNextOpenAtStr}}</p>
{{ end }}
{{ if .AskLastCloseAtStr }}
<p class="uk-text-small uk-text-muted">{{t .LangCode "ask.lastCloseAt" .AskLastCloseAtStr}}</p>
{{ end }}
</div>
<!-- 提问功能关闭但私聊开启:保留独立私聊入口(提问与私聊是独立功能) -->
{{ if .AllowChat }}
<div class="uk-margin-top">
{{ if .LoggedUser }}
<form method="post" action="/_/{{.PageUser.Domain}}" id="chat-only-form" enctype="multipart/form-data">
{{ .CSRFTokenHTML }}
{{ if .PageUser.AskPassword }}
<input type="hidden" name="ask_password" id="ask_password_input" value="">
{{ end }}
<input type="hidden" name="start_chat_val" value="true">
<input type="hidden" name="g-recaptcha-response" id="chat-g-recaptcha-response" value="">
<div class="uk-margin uk-text-center">
<textarea name="content" class="uk-textarea" rows="4" placeholder="{{t .LangCode "ask.chatPlaceholder"}}" maxlength="{{ .Limits.ChatMessageLen }}"></textarea>
</div>
<div class="uk-margin uk-text-center">
{{ if eq .CaptchaType "math" }}
<button type="button" class="uk-button uk-button-secondary math-captcha-trigger" id="chat-only-submit-btn">{{t .LangCode "ask.chat"}}</button>
{{ else }}
<button type="submit" class="uk-button uk-button-secondary g-recaptcha"
data-sitekey="{{.RecaptchaSiteKey}}" data-callback="onChatOnlySubmit"
id="chat-only-submit-btn">{{t .LangCode "ask.chat"}}</button>
{{ end }}
<div class="uk-text-meta">{{t .LangCode "ask.chatLimit" .PageUser.ChatMaxMessages}}</div>
</div>
</form>
{{ else }}
<!-- 未登录:私聊需登录 -->
<div class="uk-text-center uk-margin">
<p class="uk-text-small uk-text-muted">{{t .LangCode "ask.loginRequiredChat"}}</p>
<a class="uk-button uk-button-secondary uk-button-small" href="/login?to={{.CurrentURI}}">{{t .LangCode "ask.goLogin"}}</a>
</div>
{{ end }}
</div>
{{ end }}
{{ if and .AllowChat .LoggedUser }}
{{ if ne .CaptchaType "math" }}
<!-- 独立私聊表单的验证码回调(提问表单的 onSubmit 只提交 #form) -->
<script>
function onChatOnlySubmit(token) {
var form = document.getElementById('chat-only-form');
if (!form) return;
var btn = document.getElementById('chat-only-submit-btn');
if (btn) { btn.disabled = true; btn.innerText = '{{t .LangCode "common.verifying"}}'; }
var needPassword = {{ if .PageUser.AskPassword }}true{{ else }}false{{ end }};
if (needPassword) {
UIkit.modal.prompt('{{t .LangCode "ask.needPassword"}}', '', 'password')
.then(function(password) {
if (password) {
var pw = form.querySelector('#ask_password_input');
if (pw) pw.value = password;
form.submit();
} else {
if (btn) { btn.disabled = false; btn.innerText = '{{t .LangCode "ask.chat"}}'; }
}
});
} else {
form.submit();
}
}
</script>
{{ end }}
{{ end }}
</div>
{{ else if not .CanAsk }}
<div>
<div uk-alert class="uk-text-center">
<p>{{t .LangCode "ask.needLogin"}}</p>
</div>
<div class="uk-margin uk-text-center">
<a class="uk-button uk-button-primary" href="/login?to={{.CurrentURI}}">{{t .LangCode "ask.goLogin"}}</a>
</div>
</div>
{{ else }}
<form method="post" action="/_/{{.PageUser.Domain}}" enctype="multipart/form-data" id="form">
{{ .CSRFTokenHTML }}
{{if .PageUser.AskPassword}}
<input type="hidden" name="ask_password" id="ask_password_input" value="">
{{end}}
<input type="hidden" name="start_chat_val" id="start_chat_val" value="false">
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" value="">
<div class="uk-margin uk-text-center">
<textarea name="content" class="uk-textarea" rows="5" placeholder="{{t .LangCode "ask.placeholder"}}"
maxlength="{{ .Limits.QuestionContentLen }}">{{.content}}</textarea>
<div x-data="{ files: null }" style="width: 100%; background-color: #f8f8f8; display: flex; padding-top: 5px; padding-bottom: 5px; align-items: center; justify-content: start;gap: 5px">
<div uk-form-custom>
<a href="#" class="uk-icon-link" uk-icon="image" style="margin-left: 10px"></a>
<span style="font-size: 12px" x-text="files ? files[0].name : '{{t .LangCode "ask.addImage"}}'"></span>
<input id="uploader" name="images" type="file" accept="image/*" x-on:change="files = Object.values($event.target.files)">
</div>
<button x-show="files" type="button" class="uk-icon-link" uk-icon="close" style="margin-left: 10px;"
@click="files = null"></button>
</div>
</div>
<div class="uk-margin uk-grid-small">
<label class="uk-text-small">
<input name="private" class="uk-checkbox" type="checkbox"> {{t .LangCode "ask.private"}}
</label>
<label class="uk-text-small">
<input name="receive_reply_via_email" id="notify-checkbox" class="uk-checkbox" type="checkbox"> {{t .LangCode "ask.notify"}}
</label>
<div id="email-container" style="width: 100%; display: none;" class="uk-margin-small-top">
<input class="uk-input uk-form-small" type="email" name="receive_reply_email" id="email-input" placeholder="{{t .LangCode "ask.emailPlaceholder"}}">
</div>
</div>
<div class="uk-margin uk-text-center">
<div class="uk-grid-small uk-flex-center" uk-grid>
<div>
{{ if eq .CaptchaType "math" }}
<button type="button" class="uk-button uk-button-primary math-captcha-trigger" id="normal-submit-btn">{{t .LangCode "ask.submit"}}</button>
{{ else }}
<button type="submit" class="uk-button uk-button-primary g-recaptcha"
data-sitekey="{{.RecaptchaSiteKey}}" data-callback="onSubmit"
id="normal-submit-btn">{{t .LangCode "ask.submit"}}</button>
{{ end }}
</div>
{{ if not .AllowChat }}
<div>
<button type="button" class="uk-button uk-button-secondary" disabled
uk-tooltip="{{t .LangCode "ask.chatDisabled"}}">{{t .LangCode "ask.chat"}}</button>
</div>
{{ else if .LoggedUser }}
<div>
<button type="button" class="uk-button uk-button-secondary" id="chat-submit-btn">{{t .LangCode "ask.chat"}}</button>
<div class="uk-text-meta">{{t .LangCode "ask.chatLimit" .PageUser.ChatMaxMessages}}</div>
</div>
{{ else }}
<div>
<button type="button" class="uk-button uk-button-secondary" id="chat-submit-btn">{{t .LangCode "ask.chat"}}</button>
</div>
{{ end }}
</div>
</div>
</form>
<script>
// 邮箱显隐与验证逻辑
var notifyCheckbox = document.getElementById('notify-checkbox');
var emailContainer = document.getElementById('email-container');
var emailInput = document.getElementById('email-input');
if (notifyCheckbox && emailContainer && emailInput) {
notifyCheckbox.addEventListener('change', function() {
if (this.checked) {
emailContainer.style.display = 'block';
emailInput.setAttribute('required', 'required');
} else {
emailContainer.style.display = 'none';
emailInput.removeAttribute('required');
emailInput.value = '';
}
});
}
// 私聊按钮:设置标识后触发验证弹窗
var chatBtn = document.getElementById('chat-submit-btn');
if (chatBtn) {
chatBtn.addEventListener('click', function() {
var isLogged = {{ if .LoggedUser }}true{{ else }}false{{ end }};
if (!isLogged) {
var currentURI = '{{.CurrentURI}}';
UIkit.modal.dialog(
'<div class="uk-text-center uk-padding" style="min-height: 150px; display: flex; flex-direction: column; justify-content: center;">' +
'<h3 class="uk-modal-title uk-margin-small-bottom">{{t .LangCode "ask.loginRequired"}}</h3>' +
'<p class="uk-text-muted uk-margin-small-bottom">{{t .LangCode "ask.loginRequiredChat"}}</p>' +
'<a href="/login?to=' + encodeURIComponent(currentURI) + '" class="uk-button uk-button-primary">{{t .LangCode "ask.goLogin"}}</a>' +
'</div>',
{width: '400px'}
);
return;
}
document.getElementById('start_chat_val').value = 'true';
{{ if eq .CaptchaType "math" }}
var submitBtn = document.getElementById('normal-submit-btn');
if (submitBtn) submitBtn.click();
{{ else }}
document.getElementById('normal-submit-btn').click();
{{ end }}
});
}
{{ if ne .CaptchaType "math" }}
function onSubmit(token) {
var btn = document.querySelector('.g-recaptcha');
var originalText = btn.innerText;
if (notifyCheckbox && notifyCheckbox.checked && emailInput && emailInput.value.trim() === '') {
alert(CAPTCHA_I18N.needEmail);
grecaptcha.reset();
return;
}
btn.disabled = true;
btn.innerText = '{{t .LangCode "common.verifying"}}';
var needPassword = {{if .PageUser.AskPassword}}true{{else}}false{{end}};
if (needPassword) {
UIkit.modal.prompt(CAPTCHA_I18N.needPassword, '', 'password')
.then(function(password) {
if (password) {
document.getElementById('ask_password_input').value = password;
document.getElementById('form').submit();
} else {
btn.disabled = false;
btn.innerText = originalText;
grecaptcha.reset();
}
});
} else {
document.getElementById('form').submit();
}
}
{{ end }}
</script>
{{ end }}
@@ -0,0 +1,145 @@
{{ if not .CanAsk }}
<div>
<div uk-alert class="uk-text-center">
<p>提问箱的主人设置了仅注册用户才能提问,你需要先登录才能向他提问。</p>
</div>
<div class="uk-margin uk-text-center">
<a class="uk-button uk-button-primary" href="/login?to={{.CurrentURI}}">前往登录</a>
</div>
</div>
{{ else }}
<form method="post" action="/_/{{.PageUser.Domain}}" enctype="multipart/form-data" id="form">
{{ .CSRFTokenHTML }}
{{if .PageUser.AskPassword}}
<input type="hidden" name="ask_password" id="ask_password_input" value="">
{{end}}
<input type="hidden" name="start_chat_val" id="start_chat_val" value="false">
<div class="uk-margin uk-text-center">
<textarea name="content" class="uk-textarea" rows="5" placeholder="在此处撰写你的问题..."
maxlength="1000">{{.content}}</textarea>
<div x-data="{ files: null }" style="width: 100%; background-color: #f8f8f8; display: flex; padding-top: 5px; padding-bottom: 5px; align-items: center; justify-content: start;gap: 5px">
<div uk-form-custom>
<a href="#" class="uk-icon-link" uk-icon="image" style="margin-left: 10px"></a>
<span style="font-size: 12px" x-text="files ? files[0].name : '添加图片'"></span>
<input id="uploader" name="images" type="file" accept="image/*" x-on:change="files = Object.values($event.target.files)">
</div>
<button x-show="files" type="button" class="uk-icon-link" uk-icon="close" style="margin-left: 10px;"
@click="files = null"></button>
</div>
</div>
<div class="uk-margin uk-grid-small">
<label class="uk-text-small">
<input name="private" class="uk-checkbox" type="checkbox"> 回复后不公开
</label>
<label class="uk-text-small">
<input name="receive_reply_via_email" id="notify-checkbox" class="uk-checkbox" type="checkbox"> 接收通知
</label>
<div id="email-container" style="width: 100%; display: none;" class="uk-margin-small-top">
<input class="uk-input uk-form-small" type="email" name="receive_reply_email" id="email-input" placeholder="请输入你的邮箱地址">
</div>
</div>
<div class="uk-margin uk-text-center">
<div class="uk-grid-small uk-flex-center" uk-grid>
<div>
<button type="submit" class="uk-button uk-button-primary g-recaptcha"
data-sitekey="{{.RecaptchaSiteKey}}" data-callback="onSubmit"
id="normal-submit-btn">发送提问</button>
</div>
{{ if and .LoggedUser .PageUser.AllowChat }}
<div>
<button type="button" class="uk-button uk-button-secondary" id="chat-submit-btn">发起私聊</button>
<div class="uk-text-meta">消息上限:{{.PageUser.ChatMaxMessages}}条</div>
</div>
{{ else if not .LoggedUser }}
<div>
<button type="button" class="uk-button uk-button-secondary" id="chat-submit-btn">发起私聊</button>
</div>
{{ else }}
<div>
<button type="button" class="uk-button uk-button-secondary" disabled uk-tooltip="该提问箱未开启私聊">发起私聊</button>
</div>
{{ end }}
</div>
</div>
</form>
<script>
// 邮箱显隐与验证逻辑
var notifyCheckbox = document.getElementById('notify-checkbox');
var emailContainer = document.getElementById('email-container');
var emailInput = document.getElementById('email-input');
if (notifyCheckbox && emailContainer && emailInput) {
notifyCheckbox.addEventListener('change', function() {
if (this.checked) {
emailContainer.style.display = 'block';
emailInput.setAttribute('required', 'required');
} else {
emailContainer.style.display = 'none';
emailInput.removeAttribute('required');
emailInput.value = ''; // 清空已填内容
}
});
}
// 私聊按钮点击处理
var chatBtn = document.getElementById('chat-submit-btn');
if (chatBtn) {
chatBtn.addEventListener('click', function() {
var isLogged = {{ if .LoggedUser }}true{{ else }}false{{ end }};
if (!isLogged) {
var currentURI = '{{.CurrentURI}}';
UIkit.modal.dialog(
'<div class="uk-text-center uk-padding" style="min-height: 150px; display: flex; flex-direction: column; justify-content: center;">' +
'<h3 class="uk-modal-title uk-margin-small-bottom">需要登录</h3>' +
'<p class="uk-text-muted uk-margin-small-bottom">发起私聊需要登录账号</p>' +
'<a href="/login?to=' + encodeURIComponent(currentURI) + '" class="uk-button uk-button-primary">去登录</a>' +
'</div>',
{width: '400px'}
);
return;
}
// 已登录,设置私聊标识并触发提交
document.getElementById('start_chat_val').value = 'true';
document.getElementById('normal-submit-btn').click();
});
}
function onSubmit(token) {
var btn = document.querySelector('.g-recaptcha');
var originalText = btn.innerText;
// 手动验证邮箱(字段名已修正为 receive_reply_email)
if (notifyCheckbox && notifyCheckbox.checked && emailInput && emailInput.value.trim() === '') {
alert('请填写接收通知的邮箱地址');
grecaptcha.reset(); // 重置验证码
return;
}
btn.disabled = true;
btn.innerText = '验证中...';
var needPassword = {{if .PageUser.AskPassword}}true{{else}}false{{end}};
if (needPassword) {
UIkit.modal.prompt('此提问箱需要密码', '', 'password')
.then(function(password) {
if (password) {
document.getElementById('ask_password_input').value = password;
document.getElementById('form').submit();
} else {
btn.disabled = false;
btn.innerText = originalText;
grecaptcha.reset();
}
});
} else {
document.getElementById('form').submit();
}
}
</script>
{{ end }}
+62
View File
@@ -0,0 +1,62 @@
{{template "base/header" .}}
<!-- 头像直连 R2:crossorigin 让 favicon 也以 CORS 模式请求,确保浏览器缓存的响应都带 ACAO;
?v=2 击穿历史上被「无 ACAO 响应」污染的缓存条目 -->
<link rel="shortcut icon"
href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous"/>
<link rel="apple-touch-icon"
href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous"/>
<link rel="icon" sizes="192x192"
href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous">
<style>
.password-card {
max-width: 450px;
margin: 0 auto;
margin-top: 50px;
}
.password-icon {
text-align: center;
font-size: 3em;
color: #faa05a;
margin-bottom: 15px;
}
.password-desc {
text-align: center;
color: #666;
margin-bottom: 25px;
}
</style>
<div class="uk-container uk-container-xsmall">
<div class="uk-card uk-card-default uk-card-body password-card uk-box-shadow-medium">
<div class="password-icon">
<span uk-icon="icon: lock; ratio: 3"></span>
</div>
<h3 class="uk-text-center uk-margin-small-top">{{ .PageUser.Name }}{{t .LangCode "site.logoSuffix"}}</h3>
<p class="password-desc">{{t .LangCode "password.desc"}}</p>
{{template "base/alert" .}}
<form method="post" action="/_/{{.PageUser.Domain}}/password" class="uk-form-stacked">
{{ .CSRFTokenHTML }}
<input type="hidden" name="next" value="{{.Next}}">
<div class="uk-margin">
<div class="uk-inline uk-width-1-1">
<span class="uk-form-icon uk-form-icon" uk-icon="icon: lock"></span>
<input name="password" class="uk-input uk-form-large" type="password" placeholder="{{t .LangCode "password.input"}}" autofocus>
</div>
</div>
<div class="uk-margin">
<button type="submit" class="uk-button uk-button-primary uk-width-1-1 uk-button-large">
<span uk-icon="icon: sign-in"></span> {{t .LangCode "password.enter"}}
</button>
</div>
</form>
<div class="uk-text-center uk-margin-small-top">
<a href="/" class="uk-text-muted uk-text-small">← {{t .LangCode "common.backHome"}}</a>
</div>
</div>
</div>
{{template "base/footer" .}}
+106
View File
@@ -0,0 +1,106 @@
{{template "base/header" .}}
<!--<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/uikit/3.3.3/css/uikit.min.css">-->
<!-- 头像直连 R2:crossorigin 让 favicon 也以 CORS 模式请求,确保浏览器缓存的响应都带 ACAO;
?v=2 击穿历史上被「无 ACAO 响应」污染的缓存条目 -->
<link rel="shortcut icon"
href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous"/>
<link rel="apple-touch-icon"
href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous"/>
<link rel="icon" sizes="192x192"
href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous">
<div class="uk-card uk-card-default uk-text-center">
<div class="uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light" style="min-height: 260px;" data-src="{{ .PageUser.Background }}" uk-img>
<div class="uk-card-body">
<img class="uk-border-circle uk-box-shadow-large" src="{{ if .PageUser.Avatar }}{{ .PageUser.Avatar }}?v=2{{ else }}{{ .DefaultAvatar }}{{ end }}" crossorigin="anonymous" style="width: 100px; height: 100px; max-width: 100%; max-height: 100%;">
<h3 id="usernamecolor">{{ .PageUser.Name }}</h3>
<p id="introcolor">{{ .PageUser.Intro }}</p>
<div class="icon-container">
{{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }}
<a href="https://{{.PageUser.Acfunlink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/ailicili.png" alt="acfunlink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }}
<a href="https://{{.PageUser.Bililink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/bili.png" alt="bililink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }}
<a href="https://{{.PageUser.Weibolink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/weibo.png" alt="weibolink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }}
<a href="https://{{.PageUser.Xiaohongshulink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/xiaohongshu.png" alt="xiaohongshulink" class="icon">
</a>
{{ end }}
{{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }}
<a href="https://{{.PageUser.Bloglink}}" target="_blank">
<img src="https://r2.337845818.best/tamabox/icon/social/blog.png" alt="bloglink" class="icon">
</a>
{{ end }}
</div>
</div>
</div>
</div>
<script>
document.getElementById("usernamecolor").style.color = {{.PageUser.Usernamecolor}};
document.getElementById("introcolor").style.color = {{.PageUser.Introcolor}};
</script>
<div>
<div class="uk-card uk-card-default uk-card-small uk-card-body">
{{ if .CanAsk }}
<!-- jQuery 按需加载(同步,保证下方内联二维码初始化就绪) -->
<script src="https://cdnjs.snrat.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src='https://r2.337845818.best/tamabox/cdn/js/remake/easy.qrcode.min.js' type="text/javascript" charset="utf-8"></script>
<center><div id="boxqrimg"></div></center>
<script type="text/javascript">
var options = {
text: window.location.href,
width: 200,
height: 200,
// 与 Profile 编辑器保持一致:仅当启用自定义前景色时,才把 Qrcodebackcolor 作为前景色;
// 否则强制黑色 #000000,避免未勾选却显示自定义前景色、与 Profile 预览不一致。
colorDark: ({{.PageUser.UseCustomQRColor}} ? "{{.PageUser.Qrcodebackcolor}}" : "#000000") || "#000000",
colorLight: "{{.PageUser.Qrcodecolor}}" || "#FFFFFF",
dotScale: parseFloat("{{.PageUser.Dotscale}}") || 0.4,
dotScaleTiming: parseFloat("{{.PageUser.Dotscale}}") || 0.4,
dotScaleA: parseFloat("{{.PageUser.Dotscale}}") || 0.4,
correctLevel: QRCode.CorrectLevel.H,
logo: (function () {
// ?v=2:击穿曾被无 ACAO 响应污染的浏览器缓存条目;仅对 http(s) 绝对地址追加
var a = "{{.PageUser.Avatar}}" || "{{.DefaultAvatar}}";
return a ? (a.indexOf("http") === 0 ? a + "?v=2" : a) : undefined;
})(),
logoWidth: 60,
logoHeight: 60,
logoBackgroundTransparent: true,
// 直连 R2 等跨域图片须以 CORS 模式加载,canvas 才不被污染、可正常导出
crossOrigin: 'anonymous',
backgroundImage: ("{{.PageUser.BackgroundImage}}" ? "{{.PageUser.BackgroundImage}}" : undefined),
backgroundImageAlpha: parseFloat("{{.PageUser.Backgroundimagealpha}}") || 0.8,
autoColor: false,
PO: "{{.PageUser.Qrcodepdpcolor}}" || "#000000",
PI: "{{.PageUser.Qrcodepdpcolor}}" || "#000000",
AO: '',
AI: '',
timing: "{{.PageUser.Qrcodepdpcolor}}" || "#000000"
};
// 降级渲染:头像(logo)/ 背景图加载慢/失败时先出基础码,探针成功后再升级带 logo 版
window.tbRenderQR(document.getElementById("boxqrimg"), function () { return options; });
</script>
<p class="uk-text-center uk-text-small">扫码向@{{ .PageUser.Name }}提问<br>{{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}</p>
{{ end }}
{{template "base/alert" .}}
{{template "question/new-question-template" .}}
<hr class="uk-divider-icon">
{{ if ne (len .PageQuestions) 0 }}
<p class="uk-text-left uk-text-muted uk-text-small">@{{ .PageUser.Name }} 以前回答过的问题 ({{ .AnsweredCount }})</p>
{{ template "question/history-template" . }}
{{ end }}
</div>
</div>
{{template "base/footer" .}}
+54
View File
@@ -0,0 +1,54 @@
{{template "base/header" .}}
<div class="uk-card uk-card-default uk-card-body">
<div class="uk-text-center uk-margin-bottom">
<span uk-icon="icon: check; ratio: 2" class="uk-text-success"></span>
<h3 class="uk-margin-remove">{{t .LangCode "success.title"}}</h3>
</div>
<p class="uk-text-small uk-text-muted">{{t .LangCode "success.yourQuestion"}}</p>
<div class="md-body uk-text-break">{{MarkdownFormat .Question.Content}}</div>
{{ if .HasReceiveEmail }}
<div class="uk-alert-success" uk-alert>
<p>{{t .LangCode "success.emailSent"}}</p>
</div>
{{ else }}
<div class="uk-alert-danger" uk-alert>
<p>{{t .LangCode "success.saveLink" | safe}}</p>
</div>
{{ end }}
<div class="uk-margin">
<label class="uk-form-label uk-text-small">{{t .LangCode "success.privateLink"}}</label>
<div class="uk-flex uk-flex-middle">
<input id="private-url-input" class="uk-input uk-form-small uk-width-expand" type="text" value="{{.PrivateAbsURL}}" readonly>
<button id="copy-btn" class="uk-button uk-button-primary uk-button-small uk-margin-small-left uk-flex-none" type="button">{{t .LangCode "success.copy"}}</button>
</div>
<p class="uk-text-small uk-text-muted">{{t .LangCode "success.token" .Question.Token}}</p>
</div>
<div class="uk-text-center uk-margin-top">
<a class="uk-button uk-button-default uk-button-small" href="{{.PrivateURL}}">{{t .LangCode "success.viewQuestion"}}</a>
<a class="uk-button uk-button-default uk-button-small uk-margin-small-left" href="/_/{{.PageUser.Domain}}">{{t .LangCode "success.backToBox" .PageUser.Name}}</a>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var input = document.getElementById('private-url-input');
var btn = document.getElementById('copy-btn');
if (!input || !btn) return;
btn.addEventListener('click', function () {
input.select();
input.setSelectionRange(0, 99999);
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) {}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(input.value).then(function () { ok = true; }, function () {});
}
btn.textContent = ok ? '{{t .LangCode "success.copied"}}' : '{{t .LangCode "success.copyFailed"}}';
setTimeout(function () { btn.textContent = '{{t .LangCode "success.copy"}}'; }, 2000);
});
});
</script>
{{template "base/footer" .}}