1207 lines
49 KiB
HTML
1207 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="{{.LangCode}}">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width">
|
||
<title>{{ .Title }}</title>
|
||
<meta name="msvalidate.01" content="2F185E8E63C93635F50B3574F3B31E42" />
|
||
<meta name="description" content="{{ if .MetaDescription }}{{ .MetaDescription }}{{ else }}{{ t .LangCode "site.description" }}{{ end }}">
|
||
<meta name="keywords" content="{{ t .LangCode "site.keywords" }}">
|
||
<!-- 主题初始化:必须在任何样式生效前执行,否则深色模式用户会先渲染出一帧亮色(顶栏白闪)。
|
||
只读 cookie 并打属性,切换逻辑仍在页面底部(#theme-toggle / #theme-toggle-mobile)。 -->
|
||
<script>
|
||
(function () {
|
||
var m = document.cookie.match(/(?:^|;\s*)theme=([^;]*)/);
|
||
if (m && m[1] === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
|
||
})();
|
||
</script>
|
||
<!-- 设计令牌:亮色为默认(:root),深色在 html[data-theme="dark"] 覆盖。
|
||
页面与组件样式一律引用 var(--tb-*),不再写死色值/圆角/阴影;
|
||
var() 的第二个参数仅作兜底(旧缓存页、邮件模板等不引入本文件的场景)。
|
||
改配色只动这一块,亮暗两套自动保持一致。 -->
|
||
<style>
|
||
:root {
|
||
--tb-bg: #f7f8fa;
|
||
--tb-surface: #ffffff;
|
||
--tb-surface-2: #f4f6f8;
|
||
--tb-surface-hover: #f3f7ff;
|
||
--tb-border: #e8eaee;
|
||
--tb-text: #1f2329;
|
||
--tb-text-secondary: #5c6370;
|
||
--tb-text-muted: #8a919c;
|
||
--tb-primary: #1e87f0;
|
||
--tb-primary-weak: rgba(30, 135, 240, 0.10);
|
||
--tb-radius: 12px;
|
||
--tb-radius-sm: 8px;
|
||
--tb-radius-xs: 6px;
|
||
--tb-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -18px rgba(16, 24, 40, 0.20);
|
||
--tb-shadow-hover: 0 1px 2px rgba(16, 24, 40, 0.05), 0 18px 40px -20px rgba(16, 24, 40, 0.28);
|
||
--tb-font: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif;
|
||
}
|
||
html[data-theme="dark"] {
|
||
--tb-bg: #1a1d23;
|
||
--tb-surface: #242830;
|
||
--tb-surface-2: #2d323c;
|
||
--tb-surface-hover: rgba(92, 141, 255, 0.12);
|
||
--tb-border: #3a404c;
|
||
--tb-text: #d8dce4;
|
||
--tb-text-secondary: #a8b0bd;
|
||
--tb-text-muted: #8b93a1;
|
||
--tb-primary: #5c8dff;
|
||
--tb-primary-weak: rgba(92, 141, 255, 0.14);
|
||
--tb-shadow: 0 1px 2px rgba(0, 0, 0, 0.30), 0 12px 28px -18px rgba(0, 0, 0, 0.55);
|
||
--tb-shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 40px -20px rgba(0, 0, 0, 0.65);
|
||
}
|
||
</style>
|
||
<!-- 预连接:UIkit 走公共 CDN(cdnjs.snrat.com),其余自托管库仍在本站 R2,均需预连接 -->
|
||
<link rel="preconnect" href="https://r2.337845818.best" crossorigin>
|
||
<link rel="preconnect" href="https://cdnjs.snrat.com" crossorigin>
|
||
<!-- UIkit 3.3.3 切到公共 CDN(cdnjs.snrat.com,国内实测全量下载 ~0.25s,原 R2 自建 ~1.4-2.0s) -->
|
||
<link rel="stylesheet" href="https://cdnjs.snrat.com/ajax/libs/uikit/3.3.3/css/uikit.min.css"/>
|
||
<!-- Waline 评论区样式:非阻塞加载,不阻塞首屏渲染 -->
|
||
<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/uikit/3.3.3/js/uikit.min.js"></script>
|
||
<script defer src="https://cdnjs.snrat.com/ajax/libs/uikit/3.3.3/js/uikit-icons.min.js"></script>
|
||
<!-- UIkit 补丁:复刻原自建版 "modify-black-font"(官方版与自建版仅 3 处颜色差异——把有色容器内默认白字/白标题改为黑字)。
|
||
模板当前只用到 .uk-light 容器(用户背景封面,question/list.html、question-not-found.html;
|
||
其中 h3/p 已由 #usernamecolor/#introcolor 内联 style 覆盖),故只需补该容器的 3 处投影即可保持渲染一致。
|
||
若日后新增 .uk-card-primary/.uk-card-secondary/.uk-section-primary/.uk-tile-*/.uk-overlay-primary 等容器且需黑字,须同步扩展。 -->
|
||
<style>
|
||
.uk-light { color: rgba(0,0,0,.7); }
|
||
.uk-light .uk-h1, .uk-light .uk-h2, .uk-light .uk-h3, .uk-light .uk-h4, .uk-light .uk-h5, .uk-light .uk-h6,
|
||
.uk-light .uk-heading-2xlarge, .uk-light .uk-heading-large, .uk-light .uk-heading-medium, .uk-light .uk-heading-small, .uk-light .uk-heading-xlarge,
|
||
.uk-light h1, .uk-light h2, .uk-light h3, .uk-light h4, .uk-light h5, .uk-light h6 { color: #000; }
|
||
.uk-light .uk-table th { color: rgba(0,0,0,.7); }
|
||
</style>
|
||
<!-- dayjs/alpinejs/vconsole/waline.js 已迁 cdnjs.snrat.com(sha256 与官方一致,国内实测快 2-7x) -->
|
||
<script defer src="https://cdnjs.snrat.com/ajax/libs/dayjs/1.10.8/dayjs.min.js"></script>
|
||
<script defer src="https://cdnjs.snrat.com/ajax/libs/alpinejs/3.9.0/cdn.min.js"></script>
|
||
{{ if .VConsole }}
|
||
<script src="https://cdnjs.snrat.com/ajax/libs/vConsole/3.12.1/vconsole.min.js" type="application/javascript"></script>
|
||
<script>new window.VConsole()</script>
|
||
{{ end }}
|
||
<!-- jQuery 已改为按需加载:仅 easy.qrcode 二维码页(list/question-not-found/profile)自行引入 -->
|
||
<script defer src="https://cdnjs.snrat.com/ajax/libs/waline/2.15.8/waline.js"></script>
|
||
{{ if eq .CaptchaType "math" }}
|
||
<!-- 算数验证码:弹窗模式,不影响原布局 -->
|
||
<style>
|
||
.captcha-equation-box {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
background: #f0f2f5;
|
||
border: 1px solid #dcdcdc;
|
||
border-radius: 8px;
|
||
padding: 8px 8px 8px 16px;
|
||
gap: 6px;
|
||
transition: border-color 0.2s;
|
||
}
|
||
.captcha-equation-box:focus-within {
|
||
border-color: #1e87f0;
|
||
box-shadow: 0 0 0 3px rgba(30,135,240,0.12);
|
||
}
|
||
.math-captcha-modal-question {
|
||
font-size: 1.4rem;
|
||
font-family: "Courier New", monospace;
|
||
font-weight: 700;
|
||
color: #333;
|
||
letter-spacing: 1px;
|
||
white-space: nowrap;
|
||
}
|
||
#captcha-modal-answer {
|
||
width: 80px;
|
||
padding: 6px 10px;
|
||
text-align: center;
|
||
font-size: 1.2rem;
|
||
font-family: "Courier New", monospace;
|
||
font-weight: 600;
|
||
border: 1px solid #ccc;
|
||
border-radius: 5px;
|
||
outline: none;
|
||
transition: border-color 0.15s, box-shadow 0.15s;
|
||
background: #fff;
|
||
}
|
||
#captcha-modal-answer:focus {
|
||
border-color: #1e87f0;
|
||
box-shadow: 0 0 0 2px rgba(30,135,240,0.2);
|
||
}
|
||
/* 隐藏数字输入框的上下箭头 */
|
||
#captcha-modal-answer::-webkit-inner-spin-button,
|
||
#captcha-modal-answer::-webkit-outer-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
#captcha-modal-answer {
|
||
-moz-appearance: textfield;
|
||
}
|
||
</style>
|
||
{{ else if .RecaptchaTurnstileStyle }}
|
||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?compat=recaptcha" async defer></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.querySelectorAll('.g-recaptcha').forEach(item => {
|
||
item.disabled = true;
|
||
});
|
||
});
|
||
|
||
function onSubmit() {
|
||
document.querySelectorAll('.g-recaptcha').forEach(item => {
|
||
item.disabled = false;
|
||
})
|
||
}</script>
|
||
{{ else }}
|
||
<script src="{{.RecaptchaDomain}}/recaptcha/api.js" async defer></script>
|
||
<script>function onSubmit() {document.getElementById('form').submit();}</script>
|
||
{{ end }}
|
||
{{ if ne SentryDSN "" }}
|
||
<script
|
||
src="https://browser.sentry-cdn.com/7.37.2/bundle.tracing.replay.min.js"
|
||
integrity="sha384-o0mU2P+zsvXua0Y6f/J55PKWZt3sZCv3qDqJe6J5lZklyIQkpgjBVqNnPWYWqGhf"
|
||
crossorigin="anonymous"
|
||
></script>
|
||
<script>
|
||
Sentry.init({
|
||
dsn: "{{ SentryDSN }}",
|
||
integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()],
|
||
tracesSampleRate: 1.0,
|
||
replaysSessionSampleRate: 0.1,
|
||
replaysOnErrorSampleRate: 1.0,
|
||
});
|
||
</script>
|
||
{{ end }}
|
||
<style>
|
||
.grecaptcha-badge {visibility: hidden;}
|
||
.icon-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
width: 100%;
|
||
}
|
||
.icon {
|
||
margin-left: 10px;
|
||
width: 20px; /* 设置图标的宽度 */
|
||
height: auto; /* 保持图标的高度自动调整 */
|
||
}
|
||
.emoji-placeholder {
|
||
display: inline-block;
|
||
animation: jump 0.5s ease-in-out;
|
||
}
|
||
|
||
@keyframes jump {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-5px); }
|
||
}
|
||
/* 防止 UIkit Dropbar 初始闪烁 */
|
||
.uk-dropbar {
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||
}
|
||
|
||
.uk-dropbar.uk-open {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
a.uk-button.uk-button-link {
|
||
text-transform: none;
|
||
}
|
||
|
||
/* 移动端优化:缩小字体和间距,避免右侧空白 */
|
||
@media (max-width: 640px) {
|
||
.uk-logo {
|
||
font-size: 1rem;
|
||
}
|
||
.uk-navbar-nav a {
|
||
font-size: 0.78rem;
|
||
padding: 0 4px;
|
||
}
|
||
.emoji-placeholder {
|
||
font-size: 1rem;
|
||
}
|
||
/* 移动端用户名限宽省略,避免长英文用户名撑爆顶栏 */
|
||
.tb-nav-username {
|
||
max-width: 64px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- 深色模式:html[data-theme="dark"] 时覆盖 UIkit 3.3.3 编译主题的关键颜色 -->
|
||
<style>
|
||
/* 变量(--tb-*)已上移到 head 顶部的令牌块,亮暗两套同名常量在那里定义。 */
|
||
/* 根元素与 body 一起覆盖,确保整个视口(含内容不足一屏时的空白区)都是深色背景 */
|
||
html[data-theme="dark"],
|
||
html[data-theme="dark"] body {
|
||
background-color: var(--tb-bg) !important;
|
||
color: var(--tb-text) !important;
|
||
}
|
||
/* 通用表面容器 */
|
||
html[data-theme="dark"] .uk-card,
|
||
html[data-theme="dark"] .uk-card-default,
|
||
html[data-theme="dark"] .uk-card-secondary,
|
||
html[data-theme="dark"] .uk-dropbar,
|
||
html[data-theme="dark"] .uk-navbar-dropdown,
|
||
html[data-theme="dark"] .uk-modal-dialog,
|
||
html[data-theme="dark"] .uk-dropdown,
|
||
html[data-theme="dark"] .uk-offcanvas-bar,
|
||
html[data-theme="dark"] .uk-section,
|
||
html[data-theme="dark"] .uk-tile,
|
||
html[data-theme="dark"] .uk-panel,
|
||
html[data-theme="dark"] .uk-background-default,
|
||
html[data-theme="dark"] .uk-search-default .uk-search-input {
|
||
background-color: var(--tb-surface) !important;
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-navbar-container {
|
||
background-color: var(--tb-surface) !important;
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-navbar-nav > li > a,
|
||
html[data-theme="dark"] .uk-card a,
|
||
html[data-theme="dark"] .uk-link,
|
||
html[data-theme="dark"] a.uk-button-link,
|
||
html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a,
|
||
html[data-theme="dark"] .uk-dropdown-nav > li > a,
|
||
html[data-theme="dark"] .uk-list a {
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-navbar-nav > li > a:hover,
|
||
html[data-theme="dark"] .uk-card a:hover,
|
||
html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a:hover,
|
||
html[data-theme="dark"] .uk-dropdown-nav > li > a:hover {
|
||
color: var(--tb-primary) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-input,
|
||
html[data-theme="dark"] .uk-textarea,
|
||
html[data-theme="dark"] .uk-select {
|
||
background-color: var(--tb-surface-2) !important;
|
||
color: var(--tb-text) !important;
|
||
border-color: var(--tb-border) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-input:focus,
|
||
html[data-theme="dark"] .uk-textarea:focus,
|
||
html[data-theme="dark"] .uk-select:focus {
|
||
border-color: var(--tb-primary) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-text-muted,
|
||
html[data-theme="dark"] .uk-text-meta,
|
||
html[data-theme="dark"] .uk-text-secondary,
|
||
html[data-theme="dark"] .uk-text-muted a,
|
||
html[data-theme="dark"] .uk-text-small.uk-text-muted {
|
||
color: var(--tb-text-muted) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-divider-icon::after,
|
||
html[data-theme="dark"] hr,
|
||
html[data-theme="dark"] .uk-table th,
|
||
html[data-theme="dark"] .uk-table td,
|
||
html[data-theme="dark"] .uk-list-divider > li:nth-child(n+2) {
|
||
border-color: var(--tb-border) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-button-default {
|
||
background-color: var(--tb-surface-2) !important;
|
||
color: var(--tb-text) !important;
|
||
border-color: var(--tb-border) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-modal-title,
|
||
html[data-theme="dark"] h1, html[data-theme="dark"] h2, html[data-theme="dark"] h3,
|
||
html[data-theme="dark"] h4, html[data-theme="dark"] h5, html[data-theme="dark"] h6,
|
||
html[data-theme="dark"] .uk-card-title {
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-alert-warning,
|
||
html[data-theme="dark"] .uk-alert-danger,
|
||
html[data-theme="dark"] .uk-alert-success,
|
||
html[data-theme="dark"] .uk-alert-primary {
|
||
background-color: var(--tb-surface-2) !important;
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] #math-captcha-modal .uk-modal-dialog {
|
||
background-color: var(--tb-surface) !important;
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] #math-captcha-modal .captcha-equation-box {
|
||
background-color: var(--tb-surface-2) !important;
|
||
border-color: var(--tb-border) !important;
|
||
}
|
||
html[data-theme="dark"] #math-captcha-modal .math-captcha-modal-question {
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer {
|
||
background-color: var(--tb-surface) !important;
|
||
color: var(--tb-text) !important;
|
||
border-color: var(--tb-border) !important;
|
||
}
|
||
html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer::placeholder {
|
||
color: var(--tb-text-muted) !important;
|
||
}
|
||
html[data-theme="dark"] #math-captcha-modal .uk-text-meta,
|
||
html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh {
|
||
color: var(--tb-text-muted) !important;
|
||
}
|
||
html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh:hover {
|
||
color: var(--tb-primary) !important;
|
||
}
|
||
html[data-theme="dark"] .uk-label {
|
||
color: #fff !important;
|
||
}
|
||
/* 自定义组件(探索页推荐卡片等)深色覆盖 */
|
||
html[data-theme="dark"] .recommend-card,
|
||
html[data-theme="dark"] .recommend-card:hover {
|
||
background-color: var(--tb-surface-2) !important;
|
||
}
|
||
html[data-theme="dark"] .explore-name a {
|
||
color: var(--tb-text) !important;
|
||
}
|
||
html[data-theme="dark"] .explore-name a:hover {
|
||
color: var(--tb-primary) !important;
|
||
}
|
||
html[data-theme="dark"] .explore-intro {
|
||
color: var(--tb-text-muted) !important;
|
||
}
|
||
/* 切换按钮图标切换 */
|
||
#theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { display: none; }
|
||
html[data-theme="dark"] #theme-toggle .tb-icon-light,
|
||
html[data-theme="dark"] #theme-toggle-mobile .tb-icon-light { display: none; }
|
||
html[data-theme="dark"] #theme-toggle .tb-icon-dark,
|
||
html[data-theme="dark"] #theme-toggle-mobile .tb-icon-dark { display: inline; }
|
||
</style>
|
||
|
||
<!-- 基础质感层:只调整外观(圆角/投影/字体/数字对齐),不改任何结构与布局。
|
||
颜色一律取自头部令牌,亮暗两套自动生效,因此这里不需要再写 html[data-theme="dark"] 分支。 -->
|
||
<style>
|
||
/* 根元素与 body 同时上色:内容不足一屏时,body 之外的视口区域也要是同一底色 */
|
||
html,
|
||
body {
|
||
font-family: var(--tb-font);
|
||
background-color: var(--tb-bg);
|
||
color: var(--tb-text);
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
/* 卡片:12px 圆角 + 一层极淡接触影 + 一层大扩散影(UIkit 默认的 0 5px 15px 偏硬) */
|
||
.uk-card {
|
||
border-radius: var(--tb-radius);
|
||
}
|
||
.uk-card-default {
|
||
background-color: var(--tb-surface);
|
||
box-shadow: var(--tb-shadow);
|
||
}
|
||
.uk-card-default.uk-card-hover:hover {
|
||
box-shadow: var(--tb-shadow-hover);
|
||
}
|
||
.uk-card-header {
|
||
border-radius: var(--tb-radius) var(--tb-radius) 0 0;
|
||
}
|
||
.uk-card-footer {
|
||
border-radius: 0 0 var(--tb-radius) var(--tb-radius);
|
||
}
|
||
.uk-card,
|
||
.uk-button,
|
||
.uk-alert,
|
||
.uk-label {
|
||
transition: box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
|
||
}
|
||
/* 交互元素:统一 8px 圆角,标签 6px */
|
||
.uk-button {
|
||
border-radius: var(--tb-radius-sm);
|
||
}
|
||
.uk-input,
|
||
.uk-select,
|
||
.uk-textarea,
|
||
.uk-form-small,
|
||
.uk-search-input {
|
||
border-radius: var(--tb-radius-sm);
|
||
}
|
||
.uk-label {
|
||
border-radius: var(--tb-radius-xs);
|
||
}
|
||
.uk-alert {
|
||
border-radius: 10px;
|
||
}
|
||
.uk-modal-dialog {
|
||
border-radius: 14px;
|
||
}
|
||
.uk-dropdown,
|
||
.uk-navbar-dropdown {
|
||
border-radius: 10px;
|
||
box-shadow: var(--tb-shadow);
|
||
}
|
||
.uk-navbar-container:not(.uk-navbar-transparent) {
|
||
background-color: var(--tb-surface);
|
||
}
|
||
/* 键盘可达性:仅在键盘聚焦时出现描边,鼠标点击不出现 */
|
||
.uk-button:focus-visible,
|
||
.uk-navbar-nav > li > a:focus-visible {
|
||
outline: 2px solid var(--tb-primary);
|
||
outline-offset: 2px;
|
||
}
|
||
/* 次要文字统一走令牌,避免 #999 / #8a919c 混用 */
|
||
.uk-text-muted,
|
||
.uk-text-meta {
|
||
color: var(--tb-text-muted);
|
||
}
|
||
/* 统计/日期等数字列等宽对齐,避免刷新时左右跳动 */
|
||
.uk-table,
|
||
.tb-num {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
/* 图片上传条:原为 item/new-question 两处各写一份内联 style(写死 #f8f8f8,深色下露白),
|
||
收敛到一处并走令牌,亮暗自动适配 */
|
||
.tb-img-uploader {
|
||
width: 100%;
|
||
background-color: var(--tb-surface-2);
|
||
display: flex;
|
||
padding-top: 5px;
|
||
padding-bottom: 5px;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 5px;
|
||
}
|
||
/* 背景图封面(提问箱主页):渐变遮罩,保证用户自选文字色叠加后仍可读。
|
||
默认浅色遮罩(页面 .uk-light 补丁把文字兜底成深色);若箱主选了浅色文字,
|
||
question/list.html 会按亮度加上 .tb-cover-dark 换成深色遮罩。 */
|
||
.tb-cover {
|
||
position: relative;
|
||
border-radius: var(--tb-radius) var(--tb-radius) 0 0;
|
||
overflow: hidden;
|
||
}
|
||
/* ⚠️ 必须带 .uk-flex 复合选择器:UIkit 3.3.3 里有
|
||
.uk-flex::after,.uk-flex::before{display:none}(优先级 0,2,0),
|
||
会把这个遮罩伪元素整个干掉——不报错、只是什么都不显示(踩过一次)。
|
||
单写 .tb-cover::after 优先级不够,会被它压回 display:none。 */
|
||
.tb-cover::after,
|
||
.tb-cover.uk-flex::after {
|
||
content: "";
|
||
display: block;
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 0;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.58) 55%, rgba(255, 255, 255, 0.84) 100%);
|
||
pointer-events: none;
|
||
}
|
||
.tb-cover.tb-cover-dark::after {
|
||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.16) 45%, rgba(0, 0, 0, 0.46) 100%);
|
||
}
|
||
.tb-cover > * {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
.tb-cover h3,
|
||
.tb-cover p {
|
||
text-shadow: 0 1px 10px rgba(255, 255, 255, 0.65);
|
||
}
|
||
.tb-cover.tb-cover-dark h3,
|
||
.tb-cover.tb-cover-dark p {
|
||
text-shadow: 0 1px 12px rgba(0, 0, 0, 0.45);
|
||
}
|
||
/* 尊重系统的减少动效设置 */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.uk-card,
|
||
.uk-button,
|
||
.emoji-placeholder {
|
||
transition: none;
|
||
animation: none;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- 顶栏自适应:导航全部直接显示,窄屏时分级缩小字号/间距,保证英文/日文等长文字不溢出 -->
|
||
<style>
|
||
.uk-navbar-nav a {
|
||
font-size: 0.95rem;
|
||
padding: 0 10px;
|
||
}
|
||
/* 未登录移动端占位:桌面端隐藏,仅 ≤640px 显示(见下方 media 查询)。
|
||
桌面端布局与改动前完全一致,不受影响。 */
|
||
.tb-mobile-balancer { display: none; }
|
||
/* 桌面端保持 UIkit 原生居中三列布局(无需额外 CSS) */
|
||
.tb-navbar .uk-logo {
|
||
white-space: nowrap;
|
||
}
|
||
@media (max-width: 1200px) {
|
||
.uk-navbar-nav a {
|
||
font-size: 0.88rem;
|
||
padding: 0 8px;
|
||
}
|
||
}
|
||
@media (max-width: 1000px) {
|
||
.uk-navbar-nav a {
|
||
font-size: 0.82rem;
|
||
padding: 0 6px;
|
||
}
|
||
.uk-logo {
|
||
font-size: 1.1rem;
|
||
}
|
||
/* 登录用户名限宽,过长省略号截断 */
|
||
.tb-nav-username {
|
||
max-width: 110px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
}
|
||
@media (max-width: 800px) {
|
||
.uk-navbar-nav a {
|
||
font-size: 0.78rem;
|
||
padding: 0 4px;
|
||
}
|
||
.uk-logo {
|
||
font-size: 1rem;
|
||
}
|
||
.tb-nav-username {
|
||
max-width: 80px;
|
||
}
|
||
}
|
||
/* 手机端(≤640px):沿用 UIkit 桌面端 navbar 的原生三列定位。
|
||
不再重写 .uk-navbar-center / center-left / center-right 的定位,
|
||
避免 logo 被挤到下一行;底部语言与深色模式按钮仍由下方独立栏承载。 */
|
||
@media (max-width: 640px) {
|
||
.tb-navbar {
|
||
position: relative;
|
||
min-height: 56px;
|
||
overflow: visible;
|
||
}
|
||
.tb-navbar > .uk-navbar-center {
|
||
position: relative !important;
|
||
top: auto !important;
|
||
left: auto !important;
|
||
right: auto !important;
|
||
bottom: auto !important;
|
||
transform: none !important;
|
||
display: flex !important;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
min-height: 56px;
|
||
white-space: nowrap;
|
||
flex-wrap: nowrap;
|
||
}
|
||
.tb-navbar .uk-navbar-center-left,
|
||
.tb-navbar .uk-navbar-center-right {
|
||
position: static !important;
|
||
top: auto !important;
|
||
left: auto !important;
|
||
right: auto !important;
|
||
bottom: auto !important;
|
||
transform: none !important;
|
||
display: flex !important;
|
||
align-items: center;
|
||
flex: 0 0 auto;
|
||
white-space: nowrap;
|
||
}
|
||
.tb-navbar .uk-navbar-center-left > div,
|
||
.tb-navbar .uk-navbar-center-right > div {
|
||
display: flex;
|
||
align-items: center;
|
||
white-space: nowrap;
|
||
}
|
||
.tb-navbar .uk-navbar-nav {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
flex: 0 0 auto;
|
||
}
|
||
.tb-navbar .uk-navbar-nav > li > a,
|
||
.tb-navbar .uk-logo {
|
||
white-space: nowrap;
|
||
}
|
||
/* 未登录:移动端显示与「我的」等宽的隐藏占位,使左栏「探索+占位」与
|
||
右栏「注册/登录」两项等宽,logo 真正居中,左右视觉平衡 */
|
||
.tb-mobile-balancer { display: block; }
|
||
}
|
||
</style>
|
||
|
||
<!-- 标签样式的按钮(管理页置顶等):
|
||
尺寸完全继承 .uk-label(font-size/padding/line-height/text-transform 等),
|
||
仅覆盖 button 元素特有的默认样式(边框/字体族/光标等),保证与相邻 span 标签完全一致。 -->
|
||
<style>
|
||
.tb-label-btn {
|
||
border: none !important;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
margin: 0;
|
||
vertical-align: middle;
|
||
outline: none;
|
||
background-image: none;
|
||
box-sizing: border-box;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
}
|
||
.tb-label-btn:hover {
|
||
opacity: 0.85;
|
||
}
|
||
/* 公开页置顶小图标:仅一个图钉,不占行高、不影响原布局 */
|
||
.tb-pin-flag {
|
||
font-size: 0.9em;
|
||
margin-right: 4px;
|
||
vertical-align: -1px;
|
||
cursor: default;
|
||
}
|
||
</style>
|
||
|
||
<!-- Markdown 渲染排版(问题/回答/追问正文,左对齐以保留排版格式) -->
|
||
<style>
|
||
/* 纯文本列表:保留换行与连续空格(pre-wrap),不渲染 Markdown 大字号/彩色 */
|
||
.tb-preline {
|
||
white-space: pre-wrap;
|
||
}
|
||
/* 纯文本列表:超过 5 行截断并显示省略号(不足 5 行则完整显示) */
|
||
.tb-clamp-5 {
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 5;
|
||
overflow: hidden;
|
||
}
|
||
.md-body {
|
||
line-height: 1.7;
|
||
text-align: left;
|
||
word-break: break-word;
|
||
}
|
||
.md-body p { margin: 0.5em 0; }
|
||
.md-body h1, .md-body h2, .md-body h3,
|
||
.md-body h4, .md-body h5, .md-body h6 {
|
||
margin: 0.8em 0 0.4em;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
.md-body ul, .md-body ol { padding-left: 1.6em; margin: 0.5em 0; }
|
||
.md-body li { margin: 0.2em 0; }
|
||
.md-body blockquote {
|
||
border-left: 3px solid #d3d1c7;
|
||
margin: 0.6em 0;
|
||
padding: 0.2em 1em;
|
||
color: #5f5e5a;
|
||
}
|
||
.md-body pre {
|
||
background: #f5f5f5;
|
||
padding: 0.8em 1em;
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
margin: 0.6em 0;
|
||
}
|
||
.md-body code {
|
||
background: #f0f0f0;
|
||
padding: 0.1em 0.35em;
|
||
border-radius: 4px;
|
||
font-size: 0.9em;
|
||
}
|
||
.md-body pre code { background: none; padding: 0; }
|
||
.md-body img { max-width: 100%; height: auto; }
|
||
.md-body table { border-collapse: collapse; margin: 0.6em 0; }
|
||
.md-body th, .md-body td {
|
||
border: 1px solid #ddd;
|
||
padding: 0.4em 0.8em;
|
||
}
|
||
.md-body hr { margin: 1em 0; }
|
||
html[data-theme="dark"] .md-body blockquote {
|
||
border-color: #3a404c;
|
||
color: #8b93a1;
|
||
}
|
||
html[data-theme="dark"] .md-body pre,
|
||
html[data-theme="dark"] .md-body code { background: #2d323c; }
|
||
html[data-theme="dark"] .md-body th,
|
||
html[data-theme="dark"] .md-body td { border-color: #3a404c; }
|
||
</style>
|
||
|
||
<!-- 投票结果占比条:UIkit 3 已无 .uk-progressbar 类(那是 UIkit 2 的),
|
||
模板用 <div class="uk-progress"><div class="uk-progressbar"> 结构,需补一套填充样式,
|
||
否则内层填充无高度/背景,所有占比条都只剩外层灰色轨道,看起来一模一样。 -->
|
||
<style>
|
||
.uk-progress {
|
||
-webkit-appearance: none;
|
||
-moz-appearance: none;
|
||
appearance: none;
|
||
height: 10px;
|
||
margin: 0;
|
||
border: none;
|
||
border-radius: 6px;
|
||
background-color: #e5e5e5;
|
||
overflow: hidden;
|
||
box-shadow: none;
|
||
}
|
||
.uk-progressbar {
|
||
display: block;
|
||
height: 100%;
|
||
background-color: #1e87f0;
|
||
border-radius: 6px 0 0 6px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
html[data-theme="dark"] .uk-progress {
|
||
background-color: var(--tb-border);
|
||
}
|
||
html[data-theme="dark"] .uk-progressbar {
|
||
background-color: var(--tb-primary);
|
||
}
|
||
</style>
|
||
|
||
<!-- 移动端底部操作栏:固定底栏承载语言切换与深色模式,桌面端隐藏 -->
|
||
<style>
|
||
#tb-mobile-utils {
|
||
display: none;
|
||
}
|
||
@media (max-width: 640px) {
|
||
/* 顶栏不再容纳语言/深色按钮,恢复原有导航空间 */
|
||
.tb-nav-utils {
|
||
display: none !important;
|
||
}
|
||
/* 手机端隐藏登录用户名(左边仅保留"探索"),保证 logo 居中、
|
||
两侧各不超过 2 个短按钮,避免用户名宽度挤压布局 */
|
||
.tb-nav-username {
|
||
display: none !important;
|
||
}
|
||
/* 固定底部操作栏 */
|
||
#tb-mobile-utils {
|
||
display: flex;
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
|
||
background: rgba(255, 255, 255, 0.92);
|
||
-webkit-backdrop-filter: blur(8px);
|
||
backdrop-filter: blur(8px);
|
||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
#tb-mobile-utils .uk-select {
|
||
width: 110px;
|
||
}
|
||
html[data-theme="dark"] #tb-mobile-utils {
|
||
background: rgba(36, 40, 48, 0.92);
|
||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.4);
|
||
}
|
||
/* 给页面底部预留空间,避免页脚内容被固定底栏遮挡 */
|
||
body {
|
||
padding-bottom: 56px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- Commit 6d0a3ad -->
|
||
<meta property="og:site_name" content="{{ t .LangCode "site.title" }}" />
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:title" content="{{ if .IsUserPage }}{{ .PageUser.Name }} - {{ t .LangCode "site.title" }}{{ else }}{{ t .LangCode "site.title" }}{{ end }}" />
|
||
<meta property="og:url" content="{{ .ExternalURL }}{{ .CurrentURI }}" />
|
||
<meta property="og:description" content="{{ if .IsUserPage }}{{ .PageUser.Name }} - {{ t .LangCode "site.description" }}{{ else }}{{ t .LangCode "site.description" }}{{ end }}" />
|
||
{{ if .NoIndex }}<meta name="robots" content="noindex">{{ end }}
|
||
{{ if .IsUserPage }}
|
||
<meta property="og:image" content="{{ .PageUser.Avatar }}?og-image=1" />
|
||
<meta property="og:image:alt" content="{{ .PageUser.Name }}" />
|
||
<meta itemprop="image" content="{{ .PageUser.Avatar }}?og-image=1"/>
|
||
<meta name="msapplication-TileImage" content="{{ .PageUser.Avatar }}"/>
|
||
<link rel="shortcut icon" href="{{ .PageUser.Avatar }}?og-image=1"/>
|
||
<link rel="apple-touch-icon" href="{{ .PageUser.Avatar }}?og-image=1"/>
|
||
<link rel="icon" sizes="192x192" href="{{ .PageUser.Avatar }}?og-image=1">
|
||
<meta name="twitter:image:src" content="{{ .PageUser.Avatar }}?og-image=1" />
|
||
{{ else }}
|
||
<meta itemprop="image" content="https://r2.337845818.best/tamabox/tamabox_low.png"/>
|
||
<meta name="msapplication-TileImage" content="https://r2.337845818.best/tamabox/tamabox_low.png"/>
|
||
<link rel="shortcut icon" href="https://r2.337845818.best/tamabox/tamabox_low.png" />
|
||
<link rel="apple-touch-icon" href="https://r2.337845818.best/tamabox/tamabox_low.png"/>
|
||
<link rel="icon" sizes="192x192" href="https://r2.337845818.best/tamabox/tamabox_low.png">
|
||
<meta name="twitter:image:src" content="https://r2.337845818.best/tamabox/tamabox_low.png" />
|
||
{{ end }}
|
||
|
||
<!-- Google and Bing 站点验证:token 与域名绑定,
|
||
域名迁移(box.tama.guru → box.shiroko.one)后须用新域重新验证并替换下面的 content。
|
||
注:msvalidate.01 在本文件第 7 行重复了一份,可保留(无害)或一并清理。 -->
|
||
<meta name="google-site-verification" content="fEPwRRJfiGsgwc5SINBQwZhPK8F0cwRaOvRx0YRurI4" />
|
||
<meta name="msvalidate.01" content="2F185E8E63C93635F50B3574F3B31E42" />
|
||
<!-- 站点统计(umami):非关键资源,window load 后再动态注入,避免 mami.shiroko.one
|
||
(Vercel 托管、无国内节点)在高峰/冷访问时阻塞 DOMContentLoaded 与 load -->
|
||
<script>
|
||
window.addEventListener("load", function () {
|
||
var s = document.createElement("script");
|
||
s.src = "https://mami.shiroko.one/tamami.js";
|
||
s.setAttribute("data-website-id", "c42ebe8b-9281-4da0-83a4-8f18b3e710fe");
|
||
document.body.appendChild(s);
|
||
});
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<script>
|
||
// 二维码降级渲染(P0-②):easy.qrcode 的 logo(用户头像)/ 背景图加载失败或极慢时,
|
||
// 库内部仅 console.error 且无降级 → 二维码区域会永久空白。
|
||
// 策略:未知状态的远程图先不画(基础码立即可扫)→ 后台探针(Image 预载)→
|
||
// 判定完成后再重绘带 logo/背景的升级版;失败则保持基础码,绝不空白。
|
||
// 同会话记忆:探针结果按 URL 记忆(good/bad),编辑页拖动滑块高频重绘时零等待、不闪降级。
|
||
// 使用:window.tbRenderQR(el, function(){ return options; }) // options 含完整 logo/backgroundImage
|
||
window.tbRenderQR = function (el, getOptions) {
|
||
if (!el || typeof QRCode === 'undefined') return;
|
||
var opts = getOptions();
|
||
function remoteOf(o, key) {
|
||
var v = o[key];
|
||
return (typeof v === 'string' && v.indexOf('http') === 0) ? v : '';
|
||
}
|
||
var good = window._tbQrGood || (window._tbQrGood = {}); // URL -> true(可 CORS 加载)
|
||
var bad = window._tbQrBad || (window._tbQrBad = {}); // URL -> true(加载失败)
|
||
// 每层状态:''=无需, 1=已知可用, 0=已知失败, null=未知需探针
|
||
function layerState(url) { return !url ? '' : (good[url] ? 1 : (bad[url] ? 0 : null)); }
|
||
var layers = [
|
||
{ url: remoteOf(opts, 'logo'), st: null, present: function (o, on) {
|
||
if (on) { /* 保留 */ } else { o.logo = undefined; o.logoWidth = undefined; o.logoHeight = undefined; o.logoBackgroundTransparent = undefined; }
|
||
} },
|
||
{ url: remoteOf(opts, 'backgroundImage'), st: null, present: function (o, on) {
|
||
if (on) { /* 保留 */ } else { o.backgroundImage = undefined; o.backgroundImageAlpha = undefined; }
|
||
} }
|
||
];
|
||
var needProbe = false;
|
||
for (var i = 0; i < layers.length; i++) {
|
||
layers[i].st = layerState(layers[i].url);
|
||
if (layers[i].st === null) needProbe = true;
|
||
}
|
||
function paint(states) {
|
||
var o = {};
|
||
for (var k in opts) o[k] = opts[k];
|
||
for (var j = 0; j < layers.length; j++) layers[j].present(o, states[j] === 1);
|
||
el.innerHTML = '';
|
||
new QRCode(el, o);
|
||
}
|
||
// 已知可用层立即带上;未知层先省略 → 立即绘制(logo 记忆命中时不会闪降级)
|
||
var initStates = [];
|
||
for (var n = 0; n < layers.length; n++) initStates.push(layers[n].st === 1 ? 1 : 0);
|
||
paint(initStates);
|
||
if (!needProbe) return;
|
||
// 后台探针未知层;全部判定后若有变化再重绘一次升级版
|
||
var pending = 0, cross = opts.crossOrigin || 'anonymous';
|
||
layers.forEach(function (L) {
|
||
if (L.st !== null) return;
|
||
pending++;
|
||
var im = new Image();
|
||
im.onload = function () { good[L.url] = true; L.st = 1; onDone(); };
|
||
im.onerror = function () { bad[L.url] = true; L.st = 0; onDone(); };
|
||
if (cross) im.crossOrigin = cross;
|
||
im.src = L.url;
|
||
});
|
||
function onDone() {
|
||
if (--pending > 0) return;
|
||
var finalStates = [];
|
||
for (var m = 0; m < layers.length; m++) finalStates.push(layers[m].st === 1 ? 1 : 0);
|
||
var changed = false;
|
||
for (var q = 0; q < finalStates.length; q++) if (finalStates[q] !== initStates[q]) changed = true;
|
||
if (changed) paint(finalStates);
|
||
}
|
||
// 超时保险:个别挂起连接永不回调时,3s 后按当前已知状态收尾(基础码早已可扫)
|
||
setTimeout(function () { if (pending > 0) { pending = 0; onDone(); } }, 3000);
|
||
};
|
||
</script>
|
||
<nav class="uk-navbar-container uk-margin tb-navbar" uk-navbar>
|
||
<div class="uk-navbar-center">
|
||
<div class="uk-navbar-center-left">
|
||
<div>
|
||
<ul class="uk-navbar-nav">
|
||
<!-- 探索链接,始终显示 -->
|
||
<li><a href="/explore">{{t .LangCode "nav.explore"}}</a></li>
|
||
{{ if .IsLogged }}
|
||
<!-- 我的:进入自己的提问箱主页(与右侧「提问/设置」形成左右均衡) -->
|
||
<li><a href="/_/{{ .LoggedUser.Domain }}">{{t .LangCode "nav.mine"}}</a></li>
|
||
{{ else }}
|
||
<!-- 移动端占位:未登录时左栏仅「探索」一项,与右栏「注册/登录」两项不等宽,
|
||
导致 logo 整体被拉向左(偏左)。移动端(≤640px)显示一个与「我的」等宽的隐藏占位,
|
||
使左右视觉平衡;桌面端由 .tb-mobile-balancer 隐藏,保持原布局完全不变。 -->
|
||
<li class="tb-mobile-balancer" aria-hidden="true"><a tabindex="-1" style="visibility:hidden">{{t .LangCode "nav.mine"}}</a></li>
|
||
{{ end }}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<a class="uk-navbar-item uk-logo" href="/"><span class="emoji-placeholder" aria-label="random animal">🐺</span>{{t .LangCode "site.logoSuffix"}}</a>
|
||
|
||
<div class="uk-navbar-center-right">
|
||
<div>
|
||
{{ if .IsLogged }}
|
||
<ul class="uk-navbar-nav">
|
||
<li><a href="/user/questions">{{t .LangCode "nav.ask"}}</a></li>
|
||
</ul>
|
||
<ul class="uk-navbar-nav">
|
||
<li><a href="/user/profile">{{t .LangCode "nav.settings"}}</a></li>
|
||
</ul>
|
||
{{ else}}
|
||
<ul class="uk-navbar-nav">
|
||
<li><a href="/register">{{t .LangCode "nav.register"}}</a></li>
|
||
</ul>
|
||
<ul class="uk-navbar-nav">
|
||
<li><a href="/login">{{t .LangCode "nav.login"}}</a></li>
|
||
</ul>
|
||
{{ end }}
|
||
{{ if not .IsUserPage }}
|
||
<!-- 语言切换(紧凑图标,点击展开;移动端隐藏,见底部操作栏) -->
|
||
<ul class="uk-navbar-nav tb-nav-utils">
|
||
<li>
|
||
<a href="#" aria-label="{{t .LangCode "common.lang"}}" title="{{t .LangCode "common.lang"}}">🌐</a>
|
||
<div class="uk-navbar-dropdown" uk-drop="mode: click; pos: bottom-right">
|
||
<ul class="uk-nav uk-navbar-dropdown-nav">
|
||
{{ range .SupportedLangs }}
|
||
<li class="{{ if eq .Code $.LangCode }}uk-active{{ end }}">
|
||
<a href="/i18n/{{ .Code }}?to={{ urlquery $.CurrentURI }}">{{ .Name }}</a>
|
||
</li>
|
||
{{ end }}
|
||
</ul>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
{{ end }}
|
||
<!-- 深色模式切换(紧凑图标;移动端隐藏,见底部操作栏) -->
|
||
<ul class="uk-navbar-nav tb-nav-utils">
|
||
<li>
|
||
<a href="#" id="theme-toggle" title="{{t .LangCode "common.theme"}}" aria-label="{{t .LangCode "common.theme"}}">
|
||
<span class="tb-icon-light">🌙</span><span class="tb-icon-dark">☀️</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function() {
|
||
const emojis = ["🐺","🐗","🐻","🐨","🐼","🐹","🐭","🐰","🦊","🐮","🐷","🐽","🐸","🐒","🦍","🐔","🐧","🐦","🐤","🐦⬛","🦅","🦉","🦇","🐴","🦄","🐝","🦎","🦖","🦕","🐙","🦑","🦐","🦞","🦀","🐡","🐠","🐟","🐬","🐳","🐋","🦈","🐊","🐅","🐆","🦓","🦍","🦧","🐘","🦛","🦏","🐪","🐫","🦒","🦘","🐃","🐂","🐄","🐎","🐖","🐏","🐑","🦙","🐐","🦌","🐕","🐩","🦮","🐕🦺","🐈","🐋","🐳","🐬","🐉","🐲"];
|
||
const placeholder = document.querySelector('.emoji-placeholder');
|
||
if (placeholder) {
|
||
placeholder.textContent = emojis[Math.floor(Math.random() * emojis.length)];
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<!-- 移动端固定底部操作栏:语言切换 + 深色模式(桌面端隐藏) -->
|
||
<div id="tb-mobile-utils">
|
||
{{ if not .IsUserPage }}
|
||
<select id="mobile-lang-select" class="uk-select uk-form-small" aria-label="{{t .LangCode "common.lang"}}">
|
||
{{ range .SupportedLangs }}
|
||
<option value="{{ .Code }}" {{ if eq .Code $.LangCode }}selected{{ end }}>{{ .Name }}</option>
|
||
{{ end }}
|
||
</select>
|
||
{{ end }}
|
||
<button type="button" id="theme-toggle-mobile" class="uk-button uk-button-default uk-button-small"
|
||
title="{{t .LangCode "common.theme"}}" aria-label="{{t .LangCode "common.theme"}}">
|
||
<span class="tb-icon-light">🌙</span><span class="tb-icon-dark">☀️</span>
|
||
</button>
|
||
</div>
|
||
|
||
<script>
|
||
(function() {
|
||
// 深色模式:读取 cookie 应用主题,按钮切换并持久化
|
||
var THEME_COOKIE = 'theme';
|
||
function getTheme() {
|
||
var m = document.cookie.match(/(?:^|;\s*)theme=([^;]*)/);
|
||
return m ? m[1] : '';
|
||
}
|
||
function applyTheme(theme) {
|
||
if (theme === 'dark') {
|
||
document.documentElement.setAttribute('data-theme', 'dark');
|
||
} else {
|
||
document.documentElement.removeAttribute('data-theme');
|
||
}
|
||
}
|
||
function toggleTheme() {
|
||
var next = getTheme() === 'dark' ? 'light' : 'dark';
|
||
applyTheme(next);
|
||
var expires = new Date(Date.now() + 365 * 24 * 3600 * 1000).toUTCString();
|
||
document.cookie = THEME_COOKIE + '=' + next + '; path=/; max-age=' + (365 * 24 * 3600) + '; SameSite=Lax';
|
||
}
|
||
// 首屏主题已由 <head> 顶部的内联脚本打过属性(避免白闪),这里只负责点击切换。
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 桌面顶栏按钮与移动端底部按钮都绑定
|
||
var buttons = document.querySelectorAll('#theme-toggle, #theme-toggle-mobile');
|
||
buttons.forEach(function(btn) {
|
||
btn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
toggleTheme();
|
||
});
|
||
});
|
||
|
||
// 移动端底部语言切换:onchange 跳转到 /i18n/{code},保留当前页面位置
|
||
var langSel = document.getElementById('mobile-lang-select');
|
||
if (langSel) {
|
||
langSel.addEventListener('change', function() {
|
||
if (!this.value) return;
|
||
var to = window.location.pathname + window.location.search;
|
||
window.location.href = '/i18n/' + encodeURIComponent(this.value) + '?to=' + encodeURIComponent(to);
|
||
});
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
{{ if .IsPixel }}
|
||
<div>
|
||
{{ else }}
|
||
<div class="uk-container uk-container-xsmall">
|
||
{{ end }}
|
||
|
||
{{ if eq .CaptchaType "math" }}
|
||
<!-- 算数验证码弹窗 --><div id="math-captcha-modal" uk-modal>
|
||
<div class="uk-modal-dialog uk-modal-body uk-text-center">
|
||
<h3 class="uk-modal-title">{{t .LangCode "captcha.title"}}</h3>
|
||
<p class="uk-text-meta">{{t .LangCode "captcha.hint"}}</p>
|
||
<div class="captcha-equation-box">
|
||
<span id="captcha-modal-question" class="math-captcha-modal-question">{{t .LangCode "captcha.loading"}}</span>
|
||
<input type="number" id="captcha-modal-answer" placeholder="?" autocomplete="off" inputmode="numeric">
|
||
</div>
|
||
<div style="margin-top:6px;">
|
||
<button type="button" class="uk-button uk-button-text uk-text-small" id="captcha-modal-refresh">{{t .LangCode "captcha.refresh"}}</button>
|
||
</div>
|
||
<div id="captcha-modal-error" class="uk-text-danger uk-text-small" style="display:none;min-height:20px;"></div>
|
||
<div class="uk-margin-small-top">
|
||
<button type="button" class="uk-button uk-button-primary" id="captcha-modal-submit">{{t .LangCode "captcha.submit"}}</button>
|
||
<button type="button" class="uk-button uk-button-default uk-modal-close">{{t .LangCode "common.cancel"}}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
var CAPTCHA_I18N = {
|
||
loading: "{{t .LangCode "captcha.loading"}}",
|
||
loadFailed: "{{t .LangCode "captcha.loadFailed"}}",
|
||
loadRetry: "{{t .LangCode "captcha.loadRetry"}}",
|
||
inputAnswer: "{{t .LangCode "captcha.inputAnswer"}}",
|
||
wrongAnswer: "{{t .LangCode "captcha.wrongAnswer"}}",
|
||
verifyFailed: "{{t .LangCode "captcha.verifyFailed"}}",
|
||
needPassword: "{{t .LangCode "ask.needPassword"}}",
|
||
needEmail: "{{t .LangCode "ask.pleaseEmail"}}"
|
||
};
|
||
</script>
|
||
|
||
<script>
|
||
(function() {
|
||
var captchaId = '';
|
||
var pendingForm = null;
|
||
var pendingNeedPassword = false;
|
||
var verifying = false;
|
||
|
||
function fetchCaptcha() {
|
||
var qEl = document.getElementById('captcha-modal-question');
|
||
var errEl = document.getElementById('captcha-modal-error');
|
||
var ansEl = document.getElementById('captcha-modal-answer');
|
||
qEl.textContent = CAPTCHA_I18N.loading;
|
||
fetch('/api/captcha/generate')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.code === 0 && data.data) {
|
||
captchaId = data.data.id;
|
||
qEl.textContent = data.data.question;
|
||
if (errEl) errEl.style.display = 'none';
|
||
if (ansEl) { ansEl.value = ''; ansEl.focus(); }
|
||
} else {
|
||
qEl.textContent = CAPTCHA_I18N.loadFailed;
|
||
}
|
||
})
|
||
.catch(function() {
|
||
qEl.textContent = CAPTCHA_I18N.loadRetry;
|
||
});
|
||
}
|
||
|
||
function getCSRFToken() {
|
||
var input = document.querySelector('input[name="_csrf"]');
|
||
return input ? input.value : '';
|
||
}
|
||
|
||
function doSubmit(form) {
|
||
// 将 captcha 答案填入隐藏字段
|
||
var ans = document.getElementById('captcha-modal-answer').value.trim();
|
||
var hidden = form.querySelector('[name="g-recaptcha-response"]');
|
||
if (hidden) hidden.value = captchaId + ':' + (ans || '0');
|
||
|
||
UIkit.modal('#math-captcha-modal').hide();
|
||
|
||
// 处理 AskPassword(提问箱密码)
|
||
if (pendingNeedPassword) {
|
||
UIkit.modal.prompt(CAPTCHA_I18N.needPassword, '', 'password')
|
||
.then(function(password) {
|
||
if (password) {
|
||
var pwInput = document.getElementById('ask_password_input');
|
||
if (pwInput) pwInput.value = password;
|
||
form.submit();
|
||
}
|
||
});
|
||
} else {
|
||
form.submit();
|
||
}
|
||
}
|
||
|
||
// 验证按钮
|
||
document.getElementById('captcha-modal-submit').addEventListener('click', function() {
|
||
if (verifying) return;
|
||
var ans = document.getElementById('captcha-modal-answer').value.trim();
|
||
if (!ans) {
|
||
var errEl = document.getElementById('captcha-modal-error');
|
||
errEl.textContent = CAPTCHA_I18N.inputAnswer;
|
||
errEl.style.display = 'block';
|
||
return;
|
||
}
|
||
verifying = true;
|
||
fetch('/api/captcha/verify', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-Token': getCSRFToken()
|
||
},
|
||
body: JSON.stringify({ id: captchaId, answer: parseInt(ans, 10) })
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
verifying = false;
|
||
if (data.correct) {
|
||
if (pendingForm) doSubmit(pendingForm);
|
||
} else {
|
||
var errEl = document.getElementById('captcha-modal-error');
|
||
errEl.textContent = CAPTCHA_I18N.wrongAnswer;
|
||
errEl.style.display = 'block';
|
||
fetchCaptcha(); // 自动刷新题目
|
||
}
|
||
})
|
||
.catch(function() {
|
||
verifying = false;
|
||
var errEl = document.getElementById('captcha-modal-error');
|
||
errEl.textContent = CAPTCHA_I18N.verifyFailed;
|
||
errEl.style.display = 'block';
|
||
fetchCaptcha();
|
||
});
|
||
});
|
||
|
||
// 回车提交
|
||
document.getElementById('captcha-modal-answer').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
document.getElementById('captcha-modal-submit').click();
|
||
}
|
||
});
|
||
|
||
// 刷新按钮
|
||
document.getElementById('captcha-modal-refresh').addEventListener('click', fetchCaptcha);
|
||
|
||
// 拦截所有带 class="math-captcha-trigger" 的按钮
|
||
document.addEventListener('click', function(e) {
|
||
var btn = e.target.closest('.math-captcha-trigger');
|
||
if (!btn) return;
|
||
e.preventDefault();
|
||
|
||
var form = btn.closest('form');
|
||
if (!form) return;
|
||
pendingForm = form;
|
||
|
||
// 检查是否需要提问密码
|
||
pendingNeedPassword = !!(form.querySelector('#ask_password_input'));
|
||
|
||
// 邮箱校验(提问页面)
|
||
var notifyCb = document.getElementById('notify-checkbox');
|
||
var emailInput = document.getElementById('email-input');
|
||
if (notifyCb && notifyCb.checked && emailInput && emailInput.value.trim() === '') {
|
||
UIkit.modal.alert(CAPTCHA_I18N.needEmail);
|
||
return;
|
||
}
|
||
|
||
fetchCaptcha();
|
||
UIkit.modal('#math-captcha-modal').show();
|
||
});
|
||
})();
|
||
</script>
|
||
{{ end }} |