Files
tamabox/templates/vote/page.html
T
tamakyi e3e59e7c69 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.
2026-09-10 10:56:27 +08:00

527 lines
25 KiB
HTML
Raw 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" .}}
<style>
/* 表单字段头:序号 + 类型标签一行,题目文字独占一行(长标题自动换行,不再挤压) */
.tb-field-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
margin-bottom: 10px;
}
.tb-field-head .tb-field-label {
flex-basis: 100%;
font-weight: 600;
line-height: 1.5;
word-break: break-word;
overflow-wrap: anywhere;
}
/* 卡片式选项:radio/checkbox 统一为可点选卡片,视觉更清晰、间距更舒适 */
.tb-choice {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
margin-bottom: 8px;
border: 1px solid var(--tb-border, #e5e5e5);
border-radius: 8px;
background: var(--tb-surface, #fff);
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
}
.tb-choice:last-child { margin-bottom: 0; }
.tb-choice input[type="radio"], .tb-choice input[type="checkbox"] {
margin-top: 3px;
}
.tb-choice span {
flex: 1;
min-width: 0;
line-height: 1.5;
word-break: break-word;
overflow-wrap: anywhere;
}
.tb-choice:hover {
border-color: var(--tb-primary, #1e87f0);
background: var(--tb-surface-hover, #f5faff);
}
.tb-choice:has(input:checked) {
border-color: var(--tb-primary, #1e87f0);
background: var(--tb-surface-active, #edf6ff);
}
html[data-theme="dark"] .tb-choice {
border-color: var(--tb-border);
background: var(--tb-surface);
}
html[data-theme="dark"] .tb-choice:hover {
border-color: var(--tb-primary);
background: rgba(78, 168, 245, 0.08);
}
html[data-theme="dark"] .tb-choice:has(input:checked) {
border-color: var(--tb-primary);
background: rgba(78, 168, 245, 0.14);
}
/* 自定义补充选项的文本输入:与卡片间距对齐 */
.tb-custom-input {
margin-top: 2px;
}
/* 字数计数:紧跟在输入下方,超限由 JS 加 uk-text-danger 标红 */
.tb-text-counter {
margin-top: 2px;
text-align: right;
}
@media (max-width: 480px) {
.tb-choice { padding: 10px 12px; }
}
</style>
{{template "base/alert" .}}
<div class="uk-card uk-card-default uk-card-body uk-margin-bottom">
<div class="uk-flex uk-flex-middle uk-margin-small-bottom" style="gap: 10px;">
{{ if .Owner }}
<img class="uk-border-circle" src="{{ .Owner.Avatar }}" style="width: 40px; height: 40px;">
<div>
<a class="uk-text-bold" href="/_{{ .Owner.Domain }}">{{ .Owner.Name }}</a>
<div class="uk-text-meta uk-text-small">{{t .LangCode "vote.page.by"}}</div>
</div>
{{ end }}
<div class="uk-margin-auto-left">
{{ if .NotStarted }}<span class="uk-label">{{t .LangCode "vote.page.notStarted"}}</span>{{ end }}
{{ if .Ended }}<span class="uk-label uk-label-warning">{{t .LangCode "vote.page.ended"}}</span>{{ end }}
{{ if and .CanVote (not .Ended) }}<span class="uk-label uk-label-success">{{t .LangCode "vote.page.active"}}</span>{{ end }}
{{ if and .HasVoted (not .Ended) }}<span class="uk-label uk-label-primary">{{t .LangCode "vote.page.participated"}}</span>{{ end }}
</div>
</div>
<h3 class="uk-margin-remove-top uk-margin-small-bottom">{{ .Vote.Title }}</h3>
{{ if .Vote.Description }}
<p class="uk-margin-remove-top">{{ .Vote.Description }}</p>
{{ end }}
</div>
{{ if .PreviewMode }}
<div class="uk-alert uk-alert-warning">{{t .LangCode "vote.page.previewHint"}}</div>
{{ end }}
{{ if .FeatureClosedMessage }}
<div class="uk-alert uk-alert-warning">{{ .FeatureClosedMessage }}</div>
{{ end }}
{{ if and .NotStarted (not .Error) }}
<div class="uk-alert uk-alert-primary">{{t .LangCode "vote.page.notStartedHint"}}</div>
{{ else if and .Ended (not .ShowResults) (not .Error) }}
<div class="uk-alert uk-alert-warning">{{t .LangCode "vote.page.endedHint"}}</div>
{{ end }}
{{ if .CanVote }}
{{ if .VoteScopeRegisteredOnly }}
{{ if not .IsLogged }}
<div class="uk-alert uk-alert-warning">
{{t .LangCode "vote.page.loginRequired"}} <a href="/login?to={{ .CurrentURI }}">{{t .LangCode "common.login"}}</a>
</div>
{{ end }}
{{ else }}
<div class="uk-alert uk-alert-primary uk-text-small">{{t .LangCode "vote.page.anonymousHint"}}</div>
{{ end }}
<form method="post" action="/vote/{{ .Vote.LinkToken }}/submit" id="vote-form"{{ if not .PreviewMode }} onsubmit="return validateVoteForm()"{{ end }}>
{{ .CSRFTokenHTML }}
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" value="">
<div class="uk-card uk-card-default uk-card-body uk-margin-bottom">
{{ if eq .Vote.Type "single" }}
{{ range .Options }}
<label class="tb-choice">
<input class="uk-radio" type="radio" name="option_id" value="{{ .ID }}" data-vote-required data-vote-label="{{ $.Vote.Title }}"><span>{{ .Content }}</span>
</label>
{{ end }}
{{ else if eq .Vote.Type "multiple" }}
<div class="uk-text-meta uk-text-small uk-margin-small-bottom">{{t .LangCode "vote.page.multipleHint"}}(最多 {{ .Vote.MaxChoices }} 项)</div>
<div id="multiple-options">
{{ range .Options }}
<label class="tb-choice">
<input class="uk-checkbox" type="checkbox" name="option_ids" value="{{ .ID }}" data-vote-required data-vote-label="{{ $.Vote.Title }}"><span>{{ .Content }}</span>
</label>
{{ end }}
</div>
{{ else if eq .Vote.Type "rating" }}
<label class="uk-form-label">{{t .LangCode "vote.page.ratingLabel"}}</label>
<select class="uk-select" name="rating_value" style="max-width: 200px;" data-vote-required data-vote-label="{{ .Vote.Title }}">
<option value=""></option>
{{ range $i := loop 10 }}
<option value="{{ $i }}">{{ $i }} {{t $.LangCode "vote.page.ratingUnit"}}</option>
{{ end }}
</select>
{{ else if eq .Vote.Type "rank" }}
<div class="uk-text-meta uk-text-small uk-margin-small-bottom">{{t .LangCode "vote.page.rankHint"}}</div>
{{ range .Options }}
<div class="uk-margin-small-bottom uk-flex uk-flex-middle" style="gap: 10px;">
<span style="flex: 1;">{{ .Content }}</span>
<input class="uk-input" type="number" name="rank_{{ .ID }}" min="1" max="{{ len $.Options }}" style="width: 90px;" placeholder="{{t $.LangCode "vote.page.rankPlaceholder"}}" data-vote-required data-vote-label="{{ $.Vote.Title }}">
</div>
{{ end }}
{{ else if eq .Vote.Type "form" }}
{{ range $fi, $f := .FormFields }}
<div class="uk-margin">
<div class="tb-field-head">
<span class="uk-label">{{ add $fi 1 }}</span>
<span class="uk-label {{ fieldTypeLabelClass $f.FieldType }}">{{ fieldTypeLabel $.LangCode $f.FieldType }}</span>
<div class="tb-field-label">{{ $f.Label }}{{ if $f.Required }} <span class="uk-text-danger">*</span>{{ end }}</div>
</div>
{{ if eq $f.FieldType "text" }}
<input class="uk-input" type="text" name="field_{{ $f.ID }}" {{ if $f.MaxLength }}maxlength="{{ $f.MaxLength }}"{{ end }} {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
{{ else if eq $f.FieldType "textarea" }}
<textarea class="uk-textarea" rows="3" name="field_{{ $f.ID }}" {{ if $f.MaxLength }}maxlength="{{ $f.MaxLength }}"{{ end }} {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}></textarea>
{{ else if eq $f.FieldType "select" }}
<select class="uk-select" name="field_{{ $f.ID }}" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
<option value="">{{t $.LangCode "vote.page.selectPlaceholder"}}</option>
{{ range parseOptions $f.Options }}
<option value="{{ . }}">{{ . }}</option>
{{ end }}
{{ if $f.AllowCustom }}
<option value="{{ $.CustomFieldMarker }}">{{t $.LangCode "vote.page.customOther"}}</option>
{{ end }}
</select>
{{ if $f.AllowCustom }}
<input class="uk-input tb-custom-input" type="text" name="field_{{ $f.ID }}_custom" placeholder="{{t $.LangCode "vote.page.customFieldPlaceholder"}}" data-vote-custom-input="{{ $f.ID }}" data-tb-text-counter disabled {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
{{ end }}
{{ else if eq $f.FieldType "radio" }}
{{ range parseOptions $f.Options }}
<label class="tb-choice">
<input class="uk-radio" type="radio" name="field_{{ $f.ID }}" value="{{ . }}" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}><span>{{ . }}</span>
</label>
{{ end }}
{{ if $f.AllowCustom }}
<label class="tb-choice">
<input class="uk-radio" type="radio" name="field_{{ $f.ID }}" value="{{ $.CustomFieldMarker }}" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}><span>{{t $.LangCode "vote.page.customOther"}}</span>
</label>
<input class="uk-input tb-custom-input" type="text" name="field_{{ $f.ID }}_custom" placeholder="{{t $.LangCode "vote.page.customFieldPlaceholder"}}" data-vote-custom-input="{{ $f.ID }}" data-tb-text-counter disabled {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
{{ end }}
{{ else if eq $f.FieldType "checkbox" }}
{{ range parseOptions $f.Options }}
<label class="tb-choice">
<input class="uk-checkbox" type="checkbox" name="field_{{ $f.ID }}" value="{{ . }}" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}><span>{{ . }}</span>
</label>
{{ end }}
{{ if $f.AllowCustom }}
<label class="tb-choice">
<input class="uk-checkbox" type="checkbox" name="field_{{ $f.ID }}" value="{{ $.CustomFieldMarker }}" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}><span>{{t $.LangCode "vote.page.customOther"}}</span>
</label>
<input class="uk-input tb-custom-input" type="text" name="field_{{ $f.ID }}_custom" placeholder="{{t $.LangCode "vote.page.customFieldPlaceholder"}}" data-vote-custom-input="{{ $f.ID }}" data-tb-text-counter disabled {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
{{ end }}
{{ else if eq $f.FieldType "number" }}
<input class="uk-input" type="number" name="field_{{ $f.ID }}" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
{{ else if eq $f.FieldType "rating" }}
<select class="uk-select" name="field_{{ $f.ID }}" style="max-width: 200px;" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
<option value=""></option>
{{ range $i := loop 10 }}
<option value="{{ $i }}">{{ $i }} {{t $.LangCode "vote.page.ratingUnit"}}</option>
{{ end }}
</select>
{{ else if eq $f.FieldType "rank" }}
<div class="uk-text-meta uk-text-small uk-margin-small-bottom">
{{ if $f.Required }}{{t $.LangCode "vote.page.rankHintRequired"}}{{ else }}{{t $.LangCode "vote.page.rankHintOptional"}}{{ end }}
</div>
{{ $opts := parseOptions $f.Options }}
{{ range $oi, $opt := $opts }}
<div class="uk-flex uk-flex-middle" style="gap: 8px;">
<span class="uk-flex-1">{{ $opt }}</span>
<input class="uk-input" type="number" name="field_{{ $f.ID }}_{{ $oi }}" min="1" max="{{ len $opts }}" style="width: 90px;" placeholder="{{t $.LangCode "vote.page.rankPlaceholder"}}" {{ if $f.Required }}data-vote-required data-vote-label="{{ $f.Label }}"{{ end }}>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ end }}
{{ if and .Vote.AllowCustom (or (eq .Vote.Type "single") (eq .Vote.Type "multiple") (eq .Vote.Type "rank")) }}
<div class="uk-margin">
<label class="uk-form-label">{{t .LangCode "vote.page.customOption"}}</label>
<input class="uk-input" type="text" name="custom" placeholder="{{t .LangCode "vote.page.customPlaceholder"}}" data-tb-text-counter data-tb-text-max="{{ $.MaxCustomTextLen }}">
</div>
{{ end }}
</div>
<div class="uk-margin">
{{ if .PreviewMode }}
<button type="button" class="uk-button uk-button-primary" disabled>{{t .LangCode "vote.page.submit"}}</button>
<span class="uk-text-meta uk-text-small uk-margin-small-left">{{t .LangCode "vote.page.previewSubmitHint"}}</span>
{{ else if eq .CaptchaType "math" }}
<button type="button" class="uk-button uk-button-primary math-captcha-trigger" onclick="return voteValidate(event)">{{t .LangCode "vote.page.submit"}}</button>
{{ else }}
<button type="submit" class="uk-button uk-button-primary g-recaptcha"
data-sitekey="{{.RecaptchaSiteKey}}" data-callback="onSubmit">{{t .LangCode "vote.page.submit"}}</button>
{{ end }}
</div>
</form>
{{ else if .HasVoted }}
{{ if and (not .Success) (not .Error) }}
<div class="uk-alert uk-alert-success">{{t .LangCode "vote.page.alreadyVoted"}}</div>
{{ end }}
{{ end }}
{{ if .ShowResults }}
<div class="uk-card uk-card-default uk-card-body">
<h4 class="uk-margin-small-bottom">{{t .LangCode "vote.page.resultsTitle"}}</h4>
{{ if .Results }}
{{ if or (eq .Results.Type "single") (eq .Results.Type "multiple") }}
<div class="uk-text-meta uk-margin-small-bottom">{{t .LangCode "vote.page.totalVotes"}}: {{ .Results.Total }}</div>
{{ range .Results.Options }}
<div class="uk-margin">
<div class="uk-flex uk-flex-between">
<span>{{ .Option.Content }}</span>
<span class="uk-text-meta">{{ .Count }} {{t $.LangCode "vote.resultsVotes"}}({{ .Percent }}%)</span>
</div>
<div class="uk-progress uk-margin-small-top">
<div class="uk-progressbar" style="width: {{ .Percent }}%;"></div>
</div>
</div>
{{ end }}
{{ else if eq .Results.Type "rating" }}
<div class="uk-text-meta uk-margin-small-bottom">{{t .LangCode "vote.page.totalVotes"}}: {{ .Results.Total }} · {{t .LangCode "vote.page.avgRating"}}: {{ printf "%.1f" .Results.AvgRating }}</div>
{{ range .Results.Ratings }}
<div class="uk-margin">
<div class="uk-flex uk-flex-between">
<span>{{ .Score }} {{t $.LangCode "vote.page.ratingUnit"}}</span>
<span class="uk-text-meta">{{ .Count }} {{t $.LangCode "vote.resultsVotes"}}</span>
</div>
<div class="uk-progress uk-margin-small-top">
<div class="uk-progressbar" style="width: {{ if eq $.Results.Total 0 }}0{{ else }}{{ percentOf .Count $.Results.Total }}{{ end }}%;"></div>
</div>
</div>
{{ end }}
{{ else if eq .Results.Type "rank" }}
<div class="uk-text-meta uk-margin-small-bottom">{{t .LangCode "vote.page.totalVotes"}}: {{ .Results.Total }}</div>
<table class="uk-table uk-table-small uk-table-divider">
<thead><tr><th>{{t .LangCode "vote.page.optionColumn"}}</th><th>{{t .LangCode "vote.page.avgRank"}}</th></tr></thead>
<tbody>
{{ range .Results.Rankings }}
<tr><td>{{ .Option.Content }}</td><td>{{ if gt .AvgRank 0.0 }}{{ printf "%.2f" .AvgRank }}{{ else }}-{{ end }}</td></tr>
{{ end }}
</tbody>
</table>
{{ else if eq .Results.Type "form" }}
<div class="uk-alert uk-alert-primary uk-margin-remove">{{t .LangCode "vote.page.formCount"}}: {{ .Results.FormCount }}</div>
{{ range $si, $stat := .Results.FormFieldStats }}
<div class="uk-margin-top">
<div class="uk-flex uk-flex-middle uk-margin-small-bottom" style="gap: 8px;">
<span class="uk-label">{{ add $si 1 }}</span>
<span class="uk-label {{ fieldTypeLabelClass $stat.Field.FieldType }}">{{ fieldTypeLabel $.LangCode $stat.Field.FieldType }}</span>
<span class="uk-text-bold">{{ $stat.Field.Label }}</span>
<span class="uk-text-meta uk-text-small">{{t $.LangCode "vote.resultsAnswered"}}: {{ $stat.Total }}</span>
</div>
{{ if or (eq $stat.Field.FieldType "radio") (eq $stat.Field.FieldType "select") (eq $stat.Field.FieldType "checkbox") }}
{{ $denom := $stat.Total }}
{{ if eq $stat.Field.FieldType "checkbox" }}{{ $denom = $stat.TotalSelections }}{{ end }}
{{ range $o := $stat.OptionStats }}
<div class="uk-margin-small-top">
<div class="uk-flex uk-flex-between">
<span>{{ $o.Option.Content }}</span>
<span class="uk-text-meta">{{ $o.Count }} {{t $.LangCode "vote.resultsVotes"}}({{ if eq $denom 0 }}0{{ else }}{{ percentOf $o.Count $denom }}{{ end }}%)</span>
</div>
<div class="uk-progress uk-margin-small-top">
<div class="uk-progressbar" style="width: {{ if eq $denom 0 }}0{{ else }}{{ percentOf $o.Count $denom }}{{ end }}%;"></div>
</div>
</div>
{{ end }}
{{ if $stat.CustomStats }}
<div class="uk-margin-small-top">
<div class="uk-text-meta">{{t $.LangCode "vote.page.customStats"}}:</div>
{{ range $c := $stat.CustomStats }}
<div class="uk-flex uk-flex-between uk-margin-small-top">
<span>{{ $c.Text }}</span>
<span class="uk-text-meta">{{ $c.Count }} {{t $.LangCode "vote.resultsVotes"}}</span>
</div>
{{ end }}
</div>
{{ end }}
{{ else if eq $stat.Field.FieldType "rating" }}
<div class="uk-text-meta uk-margin-small-top">{{t $.LangCode "vote.page.avgRating"}}: {{ printf "%.1f" $stat.AvgRating }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})</div>
{{ range $r := $stat.RatingCounts }}
<div class="uk-flex uk-flex-between uk-margin-small-top">
<span>{{ $r.Score }} {{t $.LangCode "vote.page.ratingUnit"}}</span>
<span class="uk-text-meta">{{ $r.Count }} {{t $.LangCode "vote.resultsVotes"}}({{ if eq $stat.Total 0 }}0{{ else }}{{ percentOf $r.Count $stat.Total }}{{ end }}%)</span>
</div>
<div class="uk-progress uk-margin-small-top">
<div class="uk-progressbar" style="width: {{ if eq $stat.Total 0 }}0{{ else }}{{ percentOf $r.Count $stat.Total }}{{ end }}%;"></div>
</div>
{{ end }}
{{ else if eq $stat.Field.FieldType "rank" }}
<div class="uk-text-meta uk-margin-small-top">{{t $.LangCode "vote.page.avgRank"}}: {{ printf "%.2f" $stat.AvgRank }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})</div>
{{ range $o := $stat.OptionStats }}
<div class="uk-flex uk-flex-between uk-margin-small-top">
<span>{{ $o.Option.Content }}</span>
<span class="uk-text-meta">{{t $.LangCode "vote.page.avgRank"}}: {{ if gt $o.Count 0 }}{{ printf "%.2f" $o.AvgRank }}{{ else }}-{{ end }} · {{t $.LangCode "vote.page.rankedTimes"}}: {{ $o.Count }}</span>
</div>
{{ end }}
{{ else if eq $stat.Field.FieldType "number" }}
<div class="uk-text-meta uk-margin-small-top">{{t $.LangCode "vote.page.avgNumber"}}: {{ printf "%.1f" $stat.AvgNumber }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})</div>
{{ else }}
<div class="uk-text-meta uk-margin-small-top">{{t $.LangCode "vote.page.fieldFilled"}}: {{ $stat.TextCount }}</div>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ if .Results.CustomStats }}
<div class="uk-margin-top">
<div class="uk-text-bold">{{t .LangCode "vote.page.customStats"}}</div>
{{ range .Results.CustomStats }}
<div class="uk-flex uk-flex-between uk-margin-small-top">
<span>{{ .Text }}</span>
<span class="uk-text-meta">{{ .Count }} {{t $.LangCode "vote.resultsVotes"}}</span>
</div>
{{ end }}
</div>
{{ end }}
{{ else }}
<div class="uk-alert uk-alert-primary uk-margin-remove">{{t .LangCode "vote.resultsEmpty"}}</div>
{{ end }}
</div>
{{ end }}
{{ if and .IsOwner (not .ShowResults) }}
<div class="uk-margin-top uk-text-small">
<a href="/user/votes/{{ .Vote.ID }}/results">{{t .LangCode "vote.page.ownerManage"}}</a>
</div>
{{ end }}
<script>
// 自定义补充选项的字数上限,与后端校验同源(conf.Limits.CustomAnswerLen,[limits] 配置段)。
// 去掉输入框的 maxlength 后由这里做提示:超限不静默截断,而是标红并阻止提交。
var MAX_CUSTOM_LEN = {{ .MaxCustomTextLen }};
// 按 Unicode 码点计数,与后端 len([]rune(s)) 口径一致:
// emoji、部分生僻字在 UTF-16 中占两个 code unit,用 .length 会多算一倍。
function countChars(s) { return Array.from(String(s || '')).length; }
// 给带 data-tb-text-counter 的输入框挂实时字数计数;上限取 data-tb-text-max,缺省 MAX_CUSTOM_LEN。
function setupTextCounters() {
var form = document.getElementById('vote-form');
if (!form) { return; }
form.querySelectorAll('[data-tb-text-counter]').forEach(function (input) {
var max = parseInt(input.getAttribute('data-tb-text-max'), 10);
if (!max || max < 1) { max = MAX_CUSTOM_LEN; }
var counter = document.createElement('div');
counter.className = 'uk-text-meta uk-text-small tb-text-counter';
if (input.parentNode) { input.parentNode.insertBefore(counter, input.nextSibling); }
var update = function () {
var n = countChars(input.value);
counter.textContent = n + ' / ' + max;
if (n > max) {
counter.classList.add('uk-text-danger');
input.setAttribute('data-tb-too-long', '1');
} else {
counter.classList.remove('uk-text-danger');
input.removeAttribute('data-tb-too-long');
}
};
input.addEventListener('input', update);
update();
});
}
// 前端必填校验:text/textarea/number/select/rating 值非空;radio/checkbox 按 name 至少选一个;
// 排序题(vote 题型与表单 rank 字段)每个排名输入必填。
function validateVoteForm() {
var form = document.getElementById('vote-form');
if (!form) { return true; }
// 自定义补充选项超长:计数口径与后端一致,超限直接拦下并聚焦到该输入
var tooLong = form.querySelector('[data-tb-text-counter][data-tb-too-long]');
if (tooLong) {
alert('{{t .LangCode "vote.err.customTooLong" $.MaxCustomTextLen}}');
tooLong.focus();
return false;
}
// 单选/多选组:按 name 分组检查
var groups = {};
form.querySelectorAll('input[data-vote-required][type="radio"], input[data-vote-required][type="checkbox"]').forEach(function (el) {
(groups[el.name] = groups[el.name] || []).push(el);
});
for (var name in groups) {
var any = groups[name].some(function (el) { return el.checked; });
if (!any) {
alert('{{t .LangCode "vote.page.mustComplete"}}「' + groups[name][0].getAttribute('data-vote-label') + '」');
return false;
}
}
// 其余必填元素:值非空(disabled 输入跳过——"其他"文本输入在未选中时处于禁用态)
var els = form.querySelectorAll('[data-vote-required]:not(input[type="radio"]):not(input[type="checkbox"])');
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (el.disabled) { continue; }
if ((el.value || '').trim() === '') {
alert('{{t .LangCode "vote.page.mustComplete"}}「' + el.getAttribute('data-vote-label') + '」');
return false;
}
}
return true;
}
// math 验证码按钮:先校验,失败时阻止冒泡(不打开验证码弹窗)
function voteValidate(e) {
if (!validateVoteForm()) {
if (e) { e.stopPropagation(); e.preventDefault(); }
return false;
}
return true;
}
// recaptcha/turnstile 成功回调(覆盖 header 的通用 onSubmit):校验通过才提交
function onSubmit() {
if (validateVoteForm()) {
document.getElementById('vote-form').submit();
}
}
(function () {
var form = document.getElementById('vote-form');
if (!form) { return; }
setupTextCounters();
var maxChoices = {{ if eq .Vote.Type "multiple" }}{{ .Vote.MaxChoices }}{{ else }}0{{ end }};
if (maxChoices > 0) {
var boxes = form.querySelectorAll('input[type="checkbox"][name="option_ids"]');
boxes.forEach(function (box) {
box.addEventListener('change', function () {
var checked = form.querySelectorAll('input[type="checkbox"][name="option_ids"]:checked').length;
if (checked > maxChoices) { this.checked = false; }
});
});
}
// 表单字段"其他/自定义选项"输入框联动:选中保留标记时启用文本输入,否则禁用并清空
var CUSTOM_MARKER = "{{ .CustomFieldMarker }}";
form.querySelectorAll('[data-vote-custom-input]').forEach(function (input) {
var fid = input.getAttribute('data-vote-custom-input');
var group = form.querySelectorAll('input[name="field_' + fid + '"]');
var select = form.querySelector('select[name="field_' + fid + '"]');
var sync = function () {
var on = false;
if (select) {
on = (select.value === CUSTOM_MARKER);
} else {
group.forEach(function (r) { if (r.value === CUSTOM_MARKER && r.checked) { on = true; } });
}
input.disabled = !on;
if (!on) { input.value = ''; }
};
if (select) { select.addEventListener('change', sync); } else { group.forEach(function (r) { r.addEventListener('change', sync); }); }
sync();
});
})();
</script>
{{template "base/footer" .}}