Files
tamakyi 5fdc3df22d 修复: 评论组件泄露私密 token、cwebp 无超时、选项长度与去重
Waline(安全)
- item.html 的 path 从 window.location.href 改为剔除 ?t= 后的 URL。
  私密题 /_/<domain>/<id>?t=<token> 的钥匙原本会整条发往 Waline 服务端
  并落进其访问日志。只删 t、保留其他参数,公开题的评论线程键不变。

cwebp(可靠性)
- ConvertGifToAnimatedWebP 改用 exec.CommandContext + 30s 上限。
  原来裸 exec.Command 无超时,畸形/超大 GIF 会把子进程和上传 goroutine 一起挂住。
  超时杀进程并记 Warn,调用方按既有逻辑回退为原 GIF,不影响上传成功。

投票选项长度(正确性)
- vote_options.content 由 varchar(200) 放宽到 300,与配置默认值一致。
  此前 201–300 字的选项通过校验后在写库阶段失败。
- 新增 db.MaxVoteOptionLen;voteOptionMaxLen() 取配置与存储上限的较小值,
  校验与前端 maxlength 同源,避免配置调大后再次越界。

多选 checkbox 去重(正确性)
- 抽出 appendUniqueValue,预置值与「其他」文本共用同一 seen 集合。
  此前重复提交同名字段会让同一选项在统计里被计多次。

测试
- vote_dedupe_test.go:去重与顺序。
- vote_option_len_test.go:配置小于/等于/大于存储上限三种情形。
- TestVoteOptionMaxLengthRoundTrip:300 个多字节字符存回不截断。
- TestVoteOptionColumnWidens:把列改回 200 后 AutoMigrate 能扩回 300(老库升级路径)。

验证:WSL2 Debian + PostgreSQL 全量 go test ./... 27 个包全绿;
隔离实例渲染单题页确认新的 path 逻辑。
2026-11-05 17:04:47 +08:00

502 lines
25 KiB
HTML
Raw Permalink 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.
{{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-foot-sep {
margin: 0 4px;
color: #b0b6bd;
}
html[data-theme="dark"] .tb-foot-sep {
color: #5c6674;
}
/* 手机端三个按钮在 300px 内放不下一行(实测需 416px):
保留原来的「回答 / 标记为已读」一行不动,让「回答并下一条未读」独占下一行整行宽度。
display:contents 把两个提交按钮提升为该行的直接子项,才能用 order 把它排到状态按钮之后。 */
@media (max-width: 640px) {
.tb-ans-actions {
display: contents;
}
.tb-ans-actions .uk-button {
order: 1;
}
.tb-ans-state {
order: 2;
margin-left: auto;
}
.tb-ans-next {
order: 3;
flex: 1 1 100%;
}
/* 上面 .tb-ans-actions .uk-button 的 order:1 特异性更高,这里必须同级别盖回去 */
.tb-ans-actions .uk-button.tb-ans-next {
order: 3;
}
}
.tb-followup-card {
border: 1px solid #e7e9ec;
border-left: 3px solid #b0b6bd;
border-radius: var(--tb-radius-sm);
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;
}
</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="" loading="lazy">
</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="" loading="lazy">
</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>
{{ if not .CanDelete }}
<!-- 举报入口:仅对非箱主显示;默认折叠,避免在每条回答下方都堆一个显眼按钮。
理由为固定枚举(不接受自由文本),重复举报服务端幂等处理。 -->
<div class="uk-margin-small-top">
<details>
<summary class="uk-text-small uk-text-muted" style="cursor: pointer;">⚑ {{t .LangCode "report.entry"}}</summary>
<form method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/report" class="uk-margin-small-top uk-flex uk-flex-middle uk-flex-wrap" style="gap: 8px;">
{{ .CSRFTokenHTML }}
<select name="reason" class="uk-select uk-form-small" style="max-width: 200px;">
<option value="harassment">{{t .LangCode "report.reason.harassment"}}</option>
<option value="doxxing">{{t .LangCode "report.reason.doxxing"}}</option>
<option value="divisive">{{t .LangCode "report.reason.divisive"}}</option>
<option value="other">{{t .LangCode "report.reason.other"}}</option>
</select>
{{ if eq .CaptchaType "math" }}
<!-- 算数验证码:点击后由 header 的全局弹窗接管,答案回填到本表单的隐藏域 -->
<input type="hidden" name="g-recaptcha-response" value="">
<button type="button" class="uk-button uk-button-default uk-button-small math-captcha-trigger">{{t .LangCode "report.submit"}}</button>
{{ else }}
<button type="submit" class="uk-button uk-button-default uk-button-small g-recaptcha" data-sitekey="{{.RecaptchaSiteKey}}" data-callback="tbReportCaptchaOk">{{t .LangCode "report.submit"}}</button>
<script>
function tbReportCaptchaOk() {
var f = document.querySelector('form[action$="/report"]');
if (f) { f.submit(); }
}
</script>
{{ end }}
</form>
</details>
</div>
{{ end }}
<!-- 分享此提问(左)与 更新回答(右)一左一右分开;箱主未登录(无更新回答按钮)时分享按钮居中;编辑区在下方由 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 }}
<!-- 评论区资源只在本页加载(此前挂在 header 里,等于每页都下载) -->
<link rel="stylesheet" href="https://r2.337845818.best/tamabox/cdn/waline/client/v2/waline-no-commits.css" media="print" onload="this.media='all'"/>
<script defer src="https://cdnjs.snrat.com/ajax/libs/waline/2.15.8/waline.js"></script>
<div id="waline"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const waline = Waline.init({
el: '#waline',
//后端服务器地址
serverURL: 'https://waline.337845818.best/',
// 评论线程键:必须剔除 ?t= 私密题 token。
// 原来直接传 location.href,私密题 /_/<domain>/<id>?t=<token> 的钥匙
// 会整条 URL 发到 Waline 服务端并落进它的访问日志;token 本身就能读该题,
// 等于凭空多出一份可被日志检索的凭证副本。
// 只删 t、保留其余参数:公开题的 page/sort 等分页参数不变,
// 已有评论线程的键不受影响。
path: (function () {
var u = new URL(window.location.href);
u.searchParams.delete('t');
return u.toString();
})(),
// 中文默认
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: var(--tb-fs-xs)" 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="{{ if .AnswerAction }}{{ .AnswerAction }}{{ else }}/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/answer{{ end }}" 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 }" 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: var(--tb-fs-xs)" 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;">
<div class="uk-flex uk-flex-middle uk-flex-wrap tb-ans-actions" style="gap: 8px;">
<button type="submit" form="tb-answer-form" class="uk-button uk-button-primary">{{t .LangCode "qitem.submitAnswer"}}</button>
{{ if .NextUnreadID }}
<button type="submit" form="tb-answer-form" name="next" value="1" class="uk-button uk-button-default tb-ans-next">{{t .LangCode "qitem.submitAndNext"}}</button>
{{ end }}
</div>
<div class="uk-flex uk-flex-middle uk-flex-wrap tb-ans-state" 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>
{{ if and .CanDelete .NextUnreadID }}
<!-- 下一条未读:标记本条已读并跳转,会改状态,故用 POST 表单而非链接 -->
<span class="tb-foot-sep" aria-hidden="true">·</span>
<form class="uk-display-inline" method="post" action="{{ if .NextUnreadAction }}{{ .NextUnreadAction }}{{ else }}/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/next-unread{{ end }}">
{{ .CSRFTokenHTML }}
<button type="submit" class="uk-button uk-button-link" title="{{t .LangCode "qitem.nextUnreadTitle"}}">{{t .LangCode "qitem.nextUnread"}} ›</button>
</form>
{{ end }}
</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" .}}