527 lines
25 KiB
HTML
527 lines
25 KiB
HTML
{{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);
|
||
border-radius: 8px;
|
||
background: var(--tb-surface);
|
||
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);
|
||
background: var(--tb-surface-hover);
|
||
}
|
||
.tb-choice:has(input:checked) {
|
||
border-color: var(--tb-primary);
|
||
background: var(--tb-surface-active);
|
||
}
|
||
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" .}}
|