11 个模板里的硬编码字号/圆角换成令牌,为之后的深色与主题一致性打底。 字号(base.css 的 --tb-fs-xs/md/lg/xl/hero = 12/14/16/24/28): - 0.70~0.88rem、0.72rem 等五六档零碎值 → --tb-fs-xs(标签/meta) / --tb-fs-md(正文、列表、按钮) - 0.95~1rem → --tb-fs-lg;1.5rem 大数字 → --tb-fs-xl;1.75rem 首页主标题 → --tb-fs-hero - 以下几处**故意改变了观感**(不是等价替换,先记下来免得以后当 regression 查): explore 的 .explore-name 1.2rem → 1rem;.hot-vote-title 0.98 → 0.875; 首页 .tb-stat-num 手机端 1.25 → 1rem;.hot-vote-action 0.82 → 0.875(变大)。 圆角: - 8/9/10px → --tb-radius-sm(8) / --tb-radius(12,对话框与气泡);3/4/5/6px → --tb-radius-xs(6) - 头像 50%、胶囊 999px 保留原样(语义值,不是视觉随意值) 其它:header 验证码输入框边框 #ccc → var(--tb-border);chat 气泡与图片的圆角走内联 var()。 仍未令牌化(留给下一轮):templates/user/profile.html 还有 16 处 font-size、base.css 自身 11 处 (0.78/0.82/0.88/0.95/1.1),邮件模板 mail/* 保持 px 不动(邮件客户端不保证 var())。
621 lines
21 KiB
HTML
621 lines
21 KiB
HTML
{{template "base/header" .}}
|
|
<style>
|
|
.truncate {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
word-break: break-word;
|
|
}
|
|
.explore-avatar {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
margin-right: 15px;
|
|
}
|
|
.explore-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.explore-name {
|
|
font-size: var(--tb-fs-lg);
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
}
|
|
.explore-name a {
|
|
text-decoration: none;
|
|
color: var(--tb-text);
|
|
}
|
|
.explore-name a:hover {
|
|
color: var(--tb-primary);
|
|
}
|
|
.explore-intro {
|
|
color: var(--tb-text-secondary);
|
|
margin-bottom: 10px;
|
|
font-size: var(--tb-fs-md);
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
min-height: 2.6em;
|
|
}
|
|
.explore-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
}
|
|
.explore-tags .uk-label {
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.recommend-card {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 10px;
|
|
background: var(--tb-surface-2);
|
|
border-radius: var(--tb-radius-sm);
|
|
transition: background 0.2s;
|
|
}
|
|
.recommend-card:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
.recommend-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
margin-right: 10px;
|
|
}
|
|
.recommend-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.recommend-name {
|
|
font-weight: bold;
|
|
font-size: var(--tb-fs-lg);
|
|
}
|
|
.recommend-name a {
|
|
text-decoration: none;
|
|
color: var(--tb-text);
|
|
}
|
|
.recommend-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 3px;
|
|
margin-top: 3px;
|
|
}
|
|
.recommend-tags .uk-label {
|
|
font-size: var(--tb-fs-xs);
|
|
padding: 0 5px;
|
|
}
|
|
.hot-votes-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 12px;
|
|
margin-top: 16px;
|
|
}
|
|
.hot-vote-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
min-width: 0;
|
|
padding: 16px 18px;
|
|
border: 1px solid var(--tb-border);
|
|
border-radius: var(--tb-radius-sm);
|
|
background: var(--tb-surface-2);
|
|
text-decoration: none;
|
|
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
|
|
}
|
|
.hot-vote-item:hover {
|
|
border-color: var(--tb-primary);
|
|
background: #f5faff;
|
|
text-decoration: none;
|
|
transform: translateY(-1px);
|
|
}
|
|
.hot-vote-content {
|
|
min-width: 0;
|
|
}
|
|
.hot-vote-title {
|
|
display: block;
|
|
overflow: hidden;
|
|
color: var(--tb-text);
|
|
font-size: var(--tb-fs-md);
|
|
font-weight: 600;
|
|
line-height: 1.45;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.hot-vote-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 7px;
|
|
margin-top: 8px;
|
|
}
|
|
.hot-vote-meta .uk-label {
|
|
font-size: var(--tb-fs-xs);
|
|
line-height: 1.6;
|
|
}
|
|
.hot-vote-action {
|
|
flex: 0 0 auto;
|
|
color: var(--tb-primary);
|
|
font-size: var(--tb-fs-md);
|
|
white-space: nowrap;
|
|
}
|
|
html[data-theme="dark"] .hot-vote-item {
|
|
border-color: var(--tb-border);
|
|
background: var(--tb-surface-2);
|
|
}
|
|
html[data-theme="dark"] .hot-vote-item:hover {
|
|
border-color: var(--tb-primary);
|
|
background: rgba(78, 168, 245, 0.10);
|
|
}
|
|
html[data-theme="dark"] .hot-vote-title {
|
|
color: var(--tb-text);
|
|
}
|
|
@media (max-width: 640px) {
|
|
.hot-votes-list {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.hot-vote-item {
|
|
padding: 14px;
|
|
}
|
|
}
|
|
.explore-tabs {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
padding: 4px;
|
|
margin: 2px auto 20px;
|
|
max-width: 320px;
|
|
border: 1px solid var(--tb-border);
|
|
border-radius: var(--tb-radius-sm);
|
|
background: var(--tb-surface-2);
|
|
}
|
|
.explore-tab {
|
|
border: 0;
|
|
border-radius: var(--tb-radius-xs);
|
|
padding: 8px 15px;
|
|
background: transparent;
|
|
color: var(--tb-text-secondary);
|
|
cursor: pointer;
|
|
font-size: var(--tb-fs-md);
|
|
transition: background 0.2s ease, color 0.2s ease;
|
|
}
|
|
.explore-tab:hover { color: var(--tb-primary); }
|
|
.explore-tab.is-active {
|
|
background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
|
|
color: var(--tb-primary);
|
|
font-weight: 600;
|
|
}
|
|
.explore-panel[hidden] { display: none !important; }
|
|
.explore-vote-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.explore-vote-toolbar p { margin: 4px 0 0; }
|
|
.vote-random-btn {
|
|
flex: 0 0 auto;
|
|
border: 1px solid #dce9ff;
|
|
border-radius: var(--tb-radius-sm);
|
|
padding: 7px 11px;
|
|
color: var(--tb-primary);
|
|
background: #f5faff;
|
|
cursor: pointer;
|
|
font-size: var(--tb-fs-xs);
|
|
}
|
|
.vote-random-btn:hover { border-color: var(--tb-primary); background: #edf6ff; }
|
|
.all-votes-list { display: grid; gap: 10px; }
|
|
.all-vote-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--tb-border);
|
|
text-decoration: none;
|
|
}
|
|
.all-vote-item:first-child { border-top: 1px solid var(--tb-border); }
|
|
.all-vote-item:hover { background: #f8fbff; text-decoration: none; }
|
|
.all-vote-item .hot-vote-title { max-width: 100%; }
|
|
html[data-theme="dark"] .explore-tabs {
|
|
border-color: var(--tb-border);
|
|
background: var(--tb-surface-2);
|
|
}
|
|
html[data-theme="dark"] .explore-tab { color: var(--tb-text-muted); }
|
|
html[data-theme="dark"] .explore-tab.is-active { background: var(--tb-surface); color: var(--tb-primary); }
|
|
html[data-theme="dark"] .vote-random-btn { border-color: var(--tb-border); background: var(--tb-surface-2); color: var(--tb-primary); }
|
|
html[data-theme="dark"] .all-vote-item { border-color: var(--tb-border); }
|
|
html[data-theme="dark"] .all-vote-item:hover { background: rgba(78, 168, 245, 0.10); }
|
|
.explore-notice {
|
|
background: #f0f7ff;
|
|
border-left: 4px solid var(--tb-primary);
|
|
padding: 15px 20px;
|
|
margin-bottom: 20px;
|
|
border-radius: var(--tb-radius-xs);
|
|
color: var(--tb-text);
|
|
font-size: var(--tb-fs-md);
|
|
line-height: 1.6;
|
|
}
|
|
.explore-notice strong {
|
|
color: var(--tb-primary);
|
|
}
|
|
/* 极简「换一批」按钮:描边胶囊 + 悬停浅蓝底色 + 点击旋转反馈 */
|
|
.tb-shuffle-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 14px;
|
|
font-size: var(--tb-fs-md);
|
|
line-height: 1.4;
|
|
color: var(--tb-primary);
|
|
background: transparent;
|
|
border: 1px solid #e3e3e3;
|
|
border-radius: 999px;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
|
}
|
|
.tb-shuffle-btn:hover {
|
|
background: #f0f7ff;
|
|
border-color: var(--tb-primary);
|
|
}
|
|
.tb-shuffle-btn:active {
|
|
transform: scale(0.96);
|
|
}
|
|
.tb-shuffle-btn .uk-icon {
|
|
transition: transform 0.5s ease;
|
|
}
|
|
.tb-shuffle-btn.is-spinning .uk-icon {
|
|
transform: rotate(360deg);
|
|
}
|
|
html[data-theme="dark"] .tb-shuffle-btn {
|
|
color: #4ea8f5;
|
|
border-color: #3a3f45;
|
|
}
|
|
html[data-theme="dark"] .tb-shuffle-btn:hover {
|
|
background: rgba(78, 168, 245, 0.12);
|
|
border-color: #4ea8f5;
|
|
}
|
|
</style>
|
|
|
|
<div class="uk-container uk-container-small">
|
|
<h2>{{t .LangCode "explore.title"}}</h2>
|
|
|
|
<div class="explore-notice">
|
|
<p>{{t .LangCode "explore.notice" | safe}}</p>
|
|
</div>
|
|
|
|
<div class="explore-tabs" role="tablist" aria-label="{{t .LangCode "explore.title"}}">
|
|
<button type="button" class="explore-tab is-active" data-explore-tab="questions" role="tab" aria-selected="true">{{t .LangCode "explore.questionsTab"}}</button>
|
|
<button type="button" class="explore-tab" data-explore-tab="votes" role="tab" aria-selected="false">{{t .LangCode "explore.votesTab"}}</button>
|
|
</div>
|
|
|
|
<div class="explore-panel" data-explore-panel="questions">
|
|
<div class="uk-card uk-card-default uk-card-body uk-margin-bottom">
|
|
<div class="uk-flex uk-flex-between uk-flex-middle">
|
|
<h3 class="uk-card-title uk-margin-remove">{{t .LangCode "explore.recommend"}}</h3>
|
|
<button class="tb-shuffle-btn" id="refresh-recommend" type="button">
|
|
<span uk-icon="icon: refresh"></span> {{t .LangCode "explore.shuffle"}}
|
|
</button>
|
|
</div>
|
|
<div id="recommend-list" class="uk-grid-small uk-child-width-1-2@s uk-child-width-1-3@m uk-grid-match" uk-grid>
|
|
{{if .RecommendUsers}}
|
|
{{range $user := .RecommendUsers}}
|
|
<div>
|
|
<a href="/_/{{$user.Domain}}" class="recommend-card">
|
|
<img class="recommend-avatar" src="{{$user.Avatar}}" alt="avatar">
|
|
<div class="recommend-info">
|
|
<div class="recommend-name truncate">{{$user.Name}}</div>
|
|
<div class="recommend-tags">
|
|
{{if $user.TagList}}
|
|
{{range $index, $tag := $user.TagList}}
|
|
<span class="uk-label {{index $user.TagColors $index}}">{{$tag}}</span>
|
|
{{end}}
|
|
{{else}}
|
|
<span class="uk-text-muted">{{t $.LangCode "explore.noTags"}}</span>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
{{else}}
|
|
<div class="uk-text-center uk-padding">{{t .LangCode "explore.noRecommend"}}</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 热门标签榜 -->
|
|
{{if .HotTags}}
|
|
<div class="uk-card uk-card-default uk-card-body uk-margin-bottom">
|
|
<h4 class="uk-card-title uk-margin-remove-bottom">{{t .LangCode "explore.hotTags"}}</h4>
|
|
<div class="uk-margin-small-top uk-flex uk-flex-wrap uk-flex-middle" style="gap: 6px;">
|
|
{{range $i, $tag := .HotTags}}
|
|
<a class="tb-chip" href="/explore?q={{urlquery $tag}}">#{{$tag}}</a>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<form method="get" action="/explore" class="uk-margin uk-margin-bottom">
|
|
<div class="uk-grid-small" uk-grid>
|
|
<div class="uk-width-expand">
|
|
<div class="uk-inline uk-width-1-1">
|
|
<button class="uk-form-icon uk-form-icon-flip" uk-icon="icon: search" type="submit"></button>
|
|
<input class="uk-input" type="text" name="q" placeholder="{{t .LangCode "explore.search"}}" value="{{.Keyword}}">
|
|
</div>
|
|
</div>
|
|
<div class="uk-width-auto">
|
|
<select name="sort" class="uk-select" onchange="this.form.submit()">
|
|
<option value="latest" {{if eq .Sort "latest"}}selected{{end}}>{{t .LangCode "explore.sortLatest"}}</option>
|
|
<option value="oldest" {{if eq .Sort "oldest"}}selected{{end}}>{{t .LangCode "explore.sortOldest"}}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{{if .Users}}
|
|
<div class="uk-child-width-1-1 uk-child-width-1-2@s uk-grid-match" uk-grid>
|
|
{{range .Users}}
|
|
{{$user := .}}
|
|
<div>
|
|
<div class="uk-card uk-card-default uk-card-hover uk-card-body">
|
|
<div class="uk-flex uk-flex-top">
|
|
<div>
|
|
<img class="explore-avatar" src="{{$user.Avatar}}" alt="avatar">
|
|
</div>
|
|
<div class="explore-info">
|
|
<div class="explore-name truncate">
|
|
<a href="/_/{{$user.Domain}}" title="{{$user.Name}}">{{$user.Name}}</a>
|
|
</div>
|
|
<div class="explore-intro" title="{{$user.Intro}}">
|
|
{{if $user.Intro}}{{$user.Intro}}{{else}}{{t $.LangCode "explore.lazy"}}{{end}}
|
|
</div>
|
|
<div class="explore-tags">
|
|
{{if $user.TagList}}
|
|
{{range $index, $tag := $user.TagList}}
|
|
<span class="uk-label {{index $user.TagColors $index}}" title="{{$tag}}">{{$tag}}</span>
|
|
{{end}}
|
|
{{else}}
|
|
<span class="uk-text-muted">{{t $.LangCode "explore.noTags"}}</span>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<ul class="uk-pagination uk-flex-center uk-margin-top" uk-margin>
|
|
{{if gt .Page 1}}
|
|
<li><a href="?{{if .Keyword}}q={{.Keyword}}&{{end}}sort={{.Sort}}&page={{.PrevPage}}"><span uk-pagination-previous></span> {{t .LangCode "explore.prevPage"}}</a></li>
|
|
{{else}}
|
|
<li class="uk-disabled"><span><span uk-pagination-previous></span> {{t .LangCode "explore.prevPage"}}</span></li>
|
|
{{end}}
|
|
<li class="uk-active"><span>{{.Page}} / {{.TotalPages}}</span></li>
|
|
{{if lt .Page .TotalPages}}
|
|
<li><a href="?{{if .Keyword}}q={{.Keyword}}&{{end}}sort={{.Sort}}&page={{.NextPage}}">{{t .LangCode "explore.nextPage"}} <span uk-pagination-next></span></a></li>
|
|
{{else}}
|
|
<li class="uk-disabled"><span>{{t .LangCode "explore.nextPage"}} <span uk-pagination-next></span></span></li>
|
|
{{end}}
|
|
</ul>
|
|
{{else}}
|
|
<div class="uk-alert uk-alert-warning">{{t .LangCode "explore.noResult"}}</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="explore-panel" data-explore-panel="votes" hidden>
|
|
<div class="uk-card uk-card-default uk-card-body uk-margin-bottom">
|
|
<div class="explore-vote-toolbar">
|
|
<div>
|
|
<h3 class="uk-card-title uk-margin-remove">{{t .LangCode "explore.votesTitle"}}</h3>
|
|
<p class="uk-text-meta uk-text-small">{{t .LangCode "explore.votesDesc"}}</p>
|
|
</div>
|
|
<button type="button" class="vote-random-btn" id="random-vote-btn"><span uk-icon="icon: refresh; ratio: .8"></span> {{t .LangCode "explore.randomVote"}}</button>
|
|
</div>
|
|
|
|
{{if .HotVotes}}
|
|
<h4 class="uk-heading-bullet uk-margin-small-bottom">{{t .LangCode "explore.hotVotes"}}</h4>
|
|
<div class="hot-votes-list uk-margin-small-bottom">
|
|
{{range $vote := .HotVotes}}
|
|
<a href="/vote/{{$vote.LinkToken}}" class="hot-vote-item">
|
|
<div class="hot-vote-content">
|
|
<span class="hot-vote-title" title="{{$vote.Title}}">{{$vote.Title}}</span>
|
|
<span class="hot-vote-meta">
|
|
{{if eq $vote.Type "single"}}<span class="tb-chip">{{t $.LangCode "vote.typeSingle"}}</span>{{end}}
|
|
{{if eq $vote.Type "multiple"}}<span class="tb-chip">{{t $.LangCode "vote.typeMultiple"}}</span>{{end}}
|
|
{{if eq $vote.Type "rating"}}<span class="tb-chip">{{t $.LangCode "vote.typeRating"}}</span>{{end}}
|
|
{{if eq $vote.Type "rank"}}<span class="tb-chip">{{t $.LangCode "vote.typeRank"}}</span>{{end}}
|
|
{{if eq $vote.Type "form"}}<span class="tb-chip">{{t $.LangCode "vote.typeForm"}}</span>{{end}}
|
|
</span>
|
|
</div>
|
|
<span class="hot-vote-action">{{t $.LangCode "vote.page.goVote"}} <span uk-icon="icon: arrow-right; ratio: .8"></span></span>
|
|
</a>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<h4 class="uk-heading-bullet uk-margin-medium-top uk-margin-small-bottom">{{t .LangCode "explore.allVotes"}}</h4>
|
|
{{if .AllVotes}}
|
|
<div class="all-votes-list" id="all-votes-list">
|
|
{{range $vote := .AllVotes}}
|
|
<a href="/vote/{{$vote.LinkToken}}" class="all-vote-item">
|
|
<div class="hot-vote-content">
|
|
<span class="hot-vote-title" title="{{$vote.Title}}">{{$vote.Title}}</span>
|
|
<span class="hot-vote-meta">
|
|
{{if eq $vote.Type "single"}}<span class="tb-chip">{{t $.LangCode "vote.typeSingle"}}</span>{{end}}
|
|
{{if eq $vote.Type "multiple"}}<span class="tb-chip">{{t $.LangCode "vote.typeMultiple"}}</span>{{end}}
|
|
{{if eq $vote.Type "rating"}}<span class="tb-chip">{{t $.LangCode "vote.typeRating"}}</span>{{end}}
|
|
{{if eq $vote.Type "rank"}}<span class="tb-chip">{{t $.LangCode "vote.typeRank"}}</span>{{end}}
|
|
{{if eq $vote.Type "form"}}<span class="tb-chip">{{t $.LangCode "vote.typeForm"}}</span>{{end}}
|
|
<span class="uk-text-meta">{{$vote.CreatedAt.Format "2006-01-02"}}</span>
|
|
</span>
|
|
</div>
|
|
<span class="hot-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">{{t .LangCode "explore.noVotes"}}</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const recommendContainer = document.getElementById('recommend-list');
|
|
const refreshBtn = document.getElementById('refresh-recommend');
|
|
const tabs = document.querySelectorAll('[data-explore-tab]');
|
|
const panels = document.querySelectorAll('[data-explore-panel]');
|
|
|
|
function showExplorePanel(name) {
|
|
tabs.forEach(function(tab) {
|
|
const active = tab.dataset.exploreTab === name;
|
|
tab.classList.toggle('is-active', active);
|
|
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
});
|
|
panels.forEach(function(panel) {
|
|
panel.hidden = panel.dataset.explorePanel !== name;
|
|
});
|
|
if (window.history && window.history.replaceState) {
|
|
window.history.replaceState({}, document.title, window.location.pathname + (name === 'votes' ? '#votes' : ''));
|
|
}
|
|
}
|
|
|
|
tabs.forEach(function(tab) {
|
|
tab.addEventListener('click', function() { showExplorePanel(tab.dataset.exploreTab); });
|
|
});
|
|
if (window.location.hash === '#votes') showExplorePanel('votes');
|
|
|
|
const randomVoteBtn = document.getElementById('random-vote-btn');
|
|
if (randomVoteBtn) {
|
|
randomVoteBtn.addEventListener('click', function() {
|
|
const votes = Array.from(document.querySelectorAll('#all-votes-list .all-vote-item'));
|
|
if (!votes.length) return;
|
|
const target = votes[Math.floor(Math.random() * votes.length)];
|
|
randomVoteBtn.classList.add('is-spinning');
|
|
target.style.outline = '2px solid #1e87f0';
|
|
target.style.outlineOffset = '2px';
|
|
window.setTimeout(function() {
|
|
window.location.href = target.href;
|
|
}, 350);
|
|
});
|
|
}
|
|
|
|
// 递归提取用户数组,无视包裹层数
|
|
function extractUsers(obj) {
|
|
if (Array.isArray(obj)) return obj;
|
|
if (!obj || typeof obj !== 'object') return [];
|
|
// 单个用户对象
|
|
if (obj.domain || obj.id) {
|
|
return [obj];
|
|
}
|
|
// 尝试从 data 中递归
|
|
if (obj.data) {
|
|
return extractUsers(obj.data);
|
|
}
|
|
// 遍历所有值
|
|
for (const key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
const val = obj[key];
|
|
if (Array.isArray(val)) {
|
|
return val;
|
|
} else if (val && typeof val === 'object') {
|
|
const result = extractUsers(val);
|
|
if (result.length > 0) return result;
|
|
}
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function escapeHTML(str) {
|
|
if (str == null) return '';
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// 仅允许 http(s) 头像地址,阻断 javascript: 等伪协议。
|
|
function safeAvatarURL(url) {
|
|
if (typeof url !== 'string') return '';
|
|
if (/^https?:\/\//i.test(url)) return url;
|
|
return '';
|
|
}
|
|
|
|
function loadRandomRecommend() {
|
|
recommendContainer.innerHTML = '<div class="uk-text-center uk-padding"><div uk-spinner></div> {{t .LangCode "explore.loading"}}</div>';
|
|
fetch('/api/v1/explore/random')
|
|
.then(response => {
|
|
if (!response.ok) throw new Error(`HTTP {{t .LangCode "explore.loadFailed"}} ${response.status}`);
|
|
return response.json();
|
|
})
|
|
.then(resp => {
|
|
const users = extractUsers(resp);
|
|
if (users.length === 0) {
|
|
recommendContainer.innerHTML = '<div class="uk-text-center uk-padding">{{t .LangCode "explore.noRecommend"}}</div>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
users.forEach(user => {
|
|
let tagsHtml = '';
|
|
if (user.tag_list && user.tag_list.length) {
|
|
user.tag_list.forEach((tag, idx) => {
|
|
const colorClass = (user.tag_color && user.tag_color[idx]) || 'uk-label-default';
|
|
tagsHtml += `<span class="uk-label ${escapeHTML(colorClass)}" title="${escapeHTML(tag)}">${escapeHTML(tag)}</span>`;
|
|
});
|
|
} else {
|
|
tagsHtml = '<span class="uk-text-muted">{{t .LangCode "explore.noTags"}}</span>';
|
|
}
|
|
html += `
|
|
<div>
|
|
<a href="/_/${escapeHTML(user.domain)}" class="recommend-card">
|
|
<img class="recommend-avatar" src="${safeAvatarURL(user.avatar)}" alt="avatar">
|
|
<div class="recommend-info">
|
|
<div class="recommend-name truncate">${escapeHTML(user.name)}</div>
|
|
<div class="recommend-tags">${tagsHtml}</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
`;
|
|
});
|
|
recommendContainer.innerHTML = html;
|
|
if (window.UIkit && UIkit.grid) {
|
|
UIkit.grid(recommendContainer);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Failed to load random recommends:', error);
|
|
recommendContainer.innerHTML = '<div class="uk-alert uk-alert-danger">加载失败:' + escapeHTML(error.message) + '</div>';
|
|
});
|
|
}
|
|
|
|
// 初始推荐已由服务端渲染,只有"换一批"按钮触发 AJAX
|
|
refreshBtn.addEventListener('click', function() {
|
|
// 重启旋转动画(先移再触发 reflow 再加,确保每次点击都播放)
|
|
refreshBtn.classList.remove('is-spinning');
|
|
void refreshBtn.offsetWidth;
|
|
refreshBtn.classList.add('is-spinning');
|
|
loadRandomRecommend();
|
|
});
|
|
});
|
|
</script>
|
|
{{template "base/footer" .}}
|