全局美化 v1

This commit is contained in:
tamakyi
2026-10-19 11:08:30 +08:00
parent b9a8afb2ec
commit d47329d505
27 changed files with 10220 additions and 219 deletions
+48
View File
@@ -125,3 +125,51 @@ admin6 的 L2 于 2026-10-19 21:29 自动到期。
`社区规则 | 找站长` → `盘 | 公告板 | 聊天室 | 监控板 | 导航页` → `Powered by NekoBox | ICP` → `提问箱已成功存活:…`。
实现只调整了 `<br>` 的位置,仍共 4 个 `<br>`,所以块高与顶部空行跟原来一致(未新增占位)。
注意:`box_runtime` 的脚本按 id 取元素,移动位置不影响;但 `<br>` 数量别随意增删,否则页脚高度会变。
## 前端样式体检(只出结论,未改代码)
量化事实(脚本统计 templates/**/*.html):
- 全站 **0 个 .css 文件**,样式全部内联:`base/header.html` 内含 **17.5KB CSS + 10.6KB JS**(模板总 42.5KB),
footer 3.0KB JS;全模板共 **24 个 `<style>` 块**,`user/profile.html` 自身还有 11.2KB CSS + 22.8KB JS(总 62KB)。
- `--tb-*` 设计变量 **只在 `html[data-theme="dark"]` 里定义**,light 主题没有 `:root` 一套;
调用处全靠 fallback(`#e9e9e9` / `#e5e5e5` / `#fafafa` / `#333` / `#666` 混用),explore 等页面还写死色值。
- **深色模式 FOUC**:`applyTheme(getTheme())` 在 body 内、`<nav>` 之后执行(header.html:812),
深色用户刷新时顶栏会先白闪一下。修法:把 theme 初始化内联脚本提到 `<head>`。
- 结构:`uk-container-xsmall`(750px)由 header 统一输出,仅 explore/vote/chat 页面自套 small;
`question/list.html` 仍用 `<center>`、内联 `style="min-height:260px"`,用户名/简介颜色由用户自选压在背景图上,无 scrim → 可读性无保障。
- 首页 `home.html` 是纯 UIkit 卡片 + `<hr>` + `style="color: red"` + `<pre>` 统计 + 首页挂 Waline(250 行 locale)。
建议分档(未实施):P0 = light/dark 双套 token + 圆角/阴影/中文字体栈 + 首页重排 + 背景图 scrim;
P1 = header CSS 抽成 `static/css/*.css`(注意 `embed.go`/`static/fs.go` 都是显式清单)+ FOUC 修复;
P2 = 图标统一(UIkit icons 取代 emoji/位图 png)、标签减量、viewpoint/meta 去重。
## P0 前端美化(已实施)
用户拍板:卡片**结构不变,只加圆角和柔阴影**;先做 P0,不抽 CSS 文件。
改动文件:
- `templates/base/header.html`
- head 顶部新增**主题初始化内联脚本**(深色用户不再白闪;原 `applyTheme` 在 body 里、nav 之后执行)。
顺手删掉 body 脚本里的 `applyTheme(getTheme())`,只保留切换逻辑。
- 新增**设计令牌块**:`:root` 亮色 + `html[data-theme=dark]` 暗色,同名 14 个(bg/surface/surface-2/surface-hover/
border/text/text-secondary/text-muted/primary/primary-weak/radius/radius-sm/radius-xs/shadow/shadow-hover/font)。
原暗色块里的 7 个变量定义已删除(现在统一在令牌块)。
- 新增**基础质感层**:html+body 底色与字体栈(system-ui + 苹方/冬青黑/雅黑)、卡片 12px 圆角 + 双层柔投影、
按钮/输入 8px、标签 6px、alert 10px、modal 14px、导航栏白底、`:focus-visible` 描边、`tabular-nums`、
`prefers-reduced-motion`。
- `.tb-cover`:封面遮罩;默认浅色(配 `.uk-light` 的黑字兜底),`.tb-cover-dark` 为深色。
- `.tb-img-uploader` 上收到 header(原来 item / new-question 各写一份**内联 style**,写死 #f8f8f8 → 深色下露白)。
- `templates/home.html`:整页重排为 hero(标题+一句话+探索/规则按钮)+ 3 张功能卡 + 域名迁移公告
+ 提醒列表 + `<details>` 折叠更新日志 + 3 个数字统计卡(`.SiteStats` 数值字段,30 分钟 JS 刷新改为按字段更新)。
全部原有信息保留;页内 `style=` 归零。
- `templates/question/list.html`:封面加 `.tb-cover`(仅当有背景图),新增 12 行 JS 按用户名颜色亮度
决定遮罩方向;`min-height:260px` 内联改为 `.tb-profile-cover`。
- 6 个页面样式块里的硬编码色值收敛到令牌(explore/question-list/profile/list/item/vote-page,共 ~84 行),
CRLF 文件用字节级替换(未改动行尾)。
验证:`go test ./templates/... ./internal/i18n/...` 全绿;`go build`(overlay)通过。
新增 `templates/zz_preview_test.go`:`TEST_PREVIEW=1 go test ./templates -run TestGenPreview` 产出
`outputs/preview-{home,box,explore}-{light,dark}.html`,用无头 Chrome 截图核对过亮暗两套。
⚠️ 本机 **Chrome 新版 headless 会挂住**,必须 `--headless=old`,且页面外部资源要本地化
(`outputs/preview-assets/`,从 cdnjs.snrat.com 取 UIkit 3.3.3;cloudflare/jsdelivr 在本机不通)。
+9
View File
@@ -79,6 +79,15 @@
`ErrDeactivatedEmail/Domain`;封禁 = 物理删用户行,邮箱释放。级联删除一律 `Unscoped()`。
- ⚠️ GORM 链式对象 Count/Find 后带状态,统计/预览/更新必须各自重建 query。
## 前端样式
- 全站**无 .css 文件**,样式内联在模板;设计令牌在 `base/header.html` 顶部 `<style>`:`:root`(亮)+ `html[data-theme="dark"]`(暗)。
新样式一律用 `var(--tb-*)`,别再写死色值/圆角/阴影,也别为深色另写一套分支。
- 主题属性在 head 内联脚本里打(cookie `theme`),**不能挪回 body**,否则深色用户白闪。
- 页面级样式尽量用类,不用 `style=`(深色下内联写死色值会露白,`.tb-img-uploader` 踩过)。
- 视觉自检:`TEST_PREVIEW=1 go test ./templates -run TestGenPreview` → `outputs/preview-*.html`;
截图必须 `chrome --headless=old`(新版 headless 在本机挂住),外部资源先落到 `outputs/preview-assets/`。
## 其它约定
- 校验靠 `valid:"required;maxlen:N"` 标签(`internal/form/*.go`),别在 handler 手写。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+181 -11
View File
@@ -7,6 +7,52 @@
<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>
@@ -185,15 +231,7 @@
<!-- 深色模式:html[data-theme="dark"] 时覆盖 UIkit 3.3.3 编译主题的关键颜色 -->
<style>
html[data-theme="dark"] {
--tb-bg: #1a1d23;
--tb-surface: #242830;
--tb-surface-2: #2d323c;
--tb-border: #3a404c;
--tb-text: #d8dce4;
--tb-text-muted: #8b93a1;
--tb-primary: #5c8dff;
}
/* 变量(--tb-*)已上移到 head 顶部的令牌块,亮暗两套同名常量在那里定义。 */
/* 根元素与 body 一起覆盖,确保整个视口(含内容不足一屏时的空白区)都是深色背景 */
html[data-theme="dark"],
html[data-theme="dark"] body {
@@ -331,6 +369,139 @@
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;
}
.tb-cover::after {
content: "";
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 {
@@ -830,8 +1001,7 @@ document.addEventListener("DOMContentLoaded", function() {
var expires = new Date(Date.now() + 365 * 24 * 3600 * 1000).toUTCString();
document.cookie = THEME_COOKIE + '=' + next + '; path=/; max-age=' + (365 * 24 * 3600) + '; SameSite=Lax';
}
// 首屏前应用,避免闪烁
applyTheme(getTheme());
// 首屏主题已由 <head> 顶部的内联脚本打过属性(避免白闪),这里只负责点击切换。
document.addEventListener('DOMContentLoaded', function() {
// 桌面顶栏按钮与移动端底部按钮都绑定
var buttons = document.querySelectorAll('#theme-toggle, #theme-toggle-mobile');
+320 -109
View File
@@ -1,123 +1,334 @@
{{template "base/header" .}}
<div class="uk-card uk-card-default uk-card-body">
<h1 class="uk-heading-small">{{t .LangCode "site.title"}}</h1>
<p style="color: red;">重大消息!本站已切换到<a href="https://box.shiroko.one/">box.shiroko.one</a>,原域名将在2026年12月24日到期,期间将通过301转发指向新域名,到期后旧域名将彻底不在使用。<br>
现已切换内容审核服务到阿里云,审核速度提升且误伤变少了。另外允许用户调整审核严格度,在不碰底线的前提下允许用户设置不接受辱骂、广告等信息(即不听坏话模式)。 </p><hr>
<p><br>⭐一个无需注册即可提问的匿名提问箱。你不需要下载App,不需要绑定社交账号,甚至连邮箱都不需要是真的。<br>⭐本站在原版的基础上新增了问题页面的二维码、私聊、投票、追加提问等功能以及更多自定义内容。各位用户可自行修改二维码背景图、颜色等个性化配置。<br>⭐可以到站长唯一提问提问箱链接<a href="//box.shiroko.one/_/tamakyi">小狼的提问箱</a>尝试相关功能或给我提意见。<br>⭐本站提倡友好、友善的发炎,请遵守:<a href="https://box.shiroko.one/rules">社区规则</a>,无论匿名与否,我都会看着你,你要对你的发言负责。<br></p>
<hr><p style="color: red; font-weight: bold;">近期更新:<br>⭐2026.9 新增审核严格度选项,增加了提问、投票、私聊的总开关,可以设置定时收集、定量收集或关闭功能,顺便调整了前后端显示,看起来更易用了一些。<br>⭐2026.8 新增了<a href="https://box.shiroko.one/vote/sANyLkiBNUGN">投票功能</a>、追加提问、多语言、<a href="https://box.shiroko.one/_/tamakyi/2894">Markdown显示</a>、置顶显示等多个功能,详情见<a href="https://blog.shiroko.one/record/tamabox_changelog_20260813">这里</a>。<br>⭐2026.8 放弃使用recaptcha等对国内用户非常不友好的验证方式,更新了密码加密方式,修复了图片上传等一系列问题。<br>⭐2026.5 增加了私聊功能,可进行连续性对话,详见<a href="https://blog.shiroko.one/record/tamabox-update-20260513">介绍</a>;另有增加提问箱的卡片分享,更直观和美观。<br>⭐2026.2 新增了用户展示和标签功能,如果你希望陌生人给你提问,可以在设置页面给自己贴上5个标签并选择是否展示在探索页面:<a href="https://box.shiroko.one/explore">点我访问</a>,详情见<a href="https://blog.shiroko.one/record/tamabox-tags">狼的公告板</a><br></p>
<hr><p>一点提醒:<br>⭐本站唯一指定认证站长:<a href="//box.shiroko.one/_/tamakyi">小狼</a>,注意不要被骗。
<br>⭐本站不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问的出现,如有涉证或不良言论将直接删除。</p>
<style>
/* 首页:hero + 功能卡 + 统计条。仅首页使用的排版,颜色全部走令牌,亮暗自动适配。
正文文案沿用本站既有的中文(首页一直是中文直写,不随语言切换)。 */
.tb-hero {
padding: 36px 24px 32px;
text-align: center;
background-image: linear-gradient(180deg, var(--tb-primary-weak), rgba(255, 255, 255, 0));
}
.tb-hero-title {
margin: 0;
font-size: 1.75rem;
line-height: 1.35;
font-weight: 600;
color: var(--tb-text);
}
.tb-hero-lead {
margin: 12px auto 0;
max-width: 30rem;
font-size: 0.95rem;
line-height: 1.8;
color: var(--tb-text-muted);
}
.tb-hero-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 22px;
}
.tb-hero-actions .uk-button {
min-width: 112px;
}
.tb-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 12px;
margin-top: 12px;
}
.tb-grid .uk-card {
padding: 18px 18px 20px;
text-align: left;
}
.tb-card-title {
margin: 0 0 6px;
font-size: 0.98rem;
font-weight: 600;
color: var(--tb-text);
}
.tb-card-text {
margin: 0;
font-size: 0.86rem;
line-height: 1.7;
color: var(--tb-text-muted);
}
.tb-card-text + .tb-card-text {
margin-top: 8px;
}
.tb-card-text.tb-lead {
margin-top: 12px;
}
.tb-changelog summary + ul {
margin-top: 10px;
}
.tb-section-title {
margin: 0 0 10px;
font-size: 0.95rem;
font-weight: 600;
color: var(--tb-text);
}
.tb-notice-tag {
display: inline-block;
margin: 0 0 8px;
padding: 2px 8px;
border-radius: var(--tb-radius-xs);
background: var(--tb-primary-weak);
color: var(--tb-primary);
font-size: 0.75rem;
}
.tb-list {
margin: 0;
padding-left: 1.15em;
font-size: 0.9rem;
line-height: 1.9;
color: var(--tb-text);
}
.tb-changelog summary {
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
color: var(--tb-text);
list-style: none;
}
.tb-changelog summary::-webkit-details-marker { display: none; }
.tb-changelog summary::before {
content: "▸";
display: inline-block;
margin-right: 8px;
color: var(--tb-text-muted);
transition: transform 0.2s ease;
}
.tb-changelog[open] summary::before { transform: rotate(90deg); }
.tb-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
text-align: center;
}
.tb-stat-num {
display: block;
font-size: 1.5rem;
font-weight: 600;
line-height: 1.3;
color: var(--tb-text);
}
.tb-stat-label {
display: block;
margin-top: 2px;
font-size: 0.78rem;
color: var(--tb-text-muted);
}
.tb-stats-meta {
margin: 14px 0 0;
font-size: 0.78rem;
line-height: 1.8;
color: var(--tb-text-muted);
text-align: center;
}
@media (max-width: 480px) {
.tb-hero { padding: 28px 16px 24px; }
.tb-hero-title { font-size: 1.4rem; }
.tb-stat-num { font-size: 1.25rem; }
}
</style>
<div class="uk-card uk-card-default tb-hero">
<h1 class="tb-hero-title">{{t .LangCode "site.title"}}</h1>
<p class="tb-hero-lead">
一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。
</p>
<div class="tb-hero-actions">
<a href="/explore" class="uk-button uk-button-primary">{{t .LangCode "nav.explore"}}</a>
<a href="/rules" class="uk-button uk-button-default">{{t .LangCode "site.footer.rules"}}</a>
</div>
</div>
<div class="tb-grid">
<div class="uk-card uk-card-default">
<p class="tb-card-title">匿名也能问</p>
<p class="tb-card-text">不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。</p>
</div>
<div class="uk-card uk-card-default">
<p class="tb-card-title">不止一问一答</p>
<p class="tb-card-text">问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。</p>
</div>
<div class="uk-card uk-card-default">
<p class="tb-card-title">箱子归你</p>
<p class="tb-card-text">头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。</p>
</div>
</div>
<div class="uk-card uk-card-default uk-card-body">
<p class="tb-notice-tag">域名迁移</p>
<p class="tb-card-text">
本站已切换到 <a href="https://box.shiroko.one/">box.shiroko.one</a>,原域名将在 2026 年 12 月 24 日到期,
期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。
</p>
<p class="tb-card-text">
审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下
可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。
</p>
</div>
<div class="uk-card uk-card-default uk-card-body">
<h2 class="tb-section-title">一点提醒</h2>
<ul class="tb-list">
<li>本站唯一指定认证站长:<a href="https://box.shiroko.one/_/tamakyi">小狼</a>,注意不要被骗。</li>
<li>本站提倡友好、友善的发言,请遵守<a href="/rules">社区规则</a>。无论匿名与否,都要为自己的发言负责。</li>
<li>不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。</li>
</ul>
<p class="tb-card-text tb-lead">
想先试试功能,可以到站长的提问箱
<a href="//box.shiroko.one/_/tamakyi">小狼的提问箱</a> 提问或提意见。
</p>
</div>
<details class="uk-card uk-card-default uk-card-body tb-changelog">
<summary>近期更新</summary>
<ul class="tb-list">
<li><strong>2026.9</strong> 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。</li>
<li><strong>2026.8</strong> 新增<a href="https://box.shiroko.one/vote/sANyLkiBNUGN">投票功能</a>、追加提问、多语言、<a href="https://box.shiroko.one/_/tamakyi/2894">Markdown 显示</a>、置顶显示等,详情见<a href="https://blog.shiroko.one/record/tamabox_changelog_20260813">这里</a>。</li>
<li><strong>2026.8</strong> 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。</li>
<li><strong>2026.5</strong> 增加私聊功能,可连续对话,详见<a href="https://blog.shiroko.one/record/tamabox-update-20260513">介绍</a>;另有提问箱卡片分享。</li>
<li><strong>2026.2</strong> 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在<a href="https://box.shiroko.one/explore">探索页</a>展示,详情见<a href="https://blog.shiroko.one/record/tamabox-tags">狼的公告板</a>。</li>
</ul>
</details>
<div class="uk-card uk-card-default uk-card-body">
<h2 class="tb-section-title">{{t .LangCode "site.stats"}}</h2>
<div class="tb-stats">
<div>
<span class="tb-stat-num tb-num" data-stat="registered_users">{{if .SiteStats}}{{.SiteStats.RegisteredUsers}}{{else}}-{{end}}</span>
<span class="tb-stat-label">注册用户</span>
</div>
<div>
<span class="tb-stat-num tb-num" data-stat="total_questions">{{if .SiteStats}}{{.SiteStats.TotalQuestions}}{{else}}-{{end}}</span>
<span class="tb-stat-label">累计提问</span>
</div>
<div>
<span class="tb-stat-num tb-num" data-stat="answered_questions">{{if .SiteStats}}{{.SiteStats.AnsweredQuestions}}{{else}}-{{end}}</span>
<span class="tb-stat-label">已获回答</span>
</div>
</div>
<p class="tb-stats-meta">
最后提问:<span data-stat="last_question_at">{{if .SiteStats}}{{.SiteStats.LastQuestionAt}}{{else}}-{{end}}</span><br>
最后回答:<span data-stat="last_answer_at">{{if .SiteStats}}{{.SiteStats.LastAnswerAt}}{{else}}-{{end}}</span><br>
数据更新于:<span data-stat="updated_at">{{if .SiteStats}}{{.SiteStats.UpdatedAt}}{{else}}-{{end}}</span>
</p>
</div>
<p>{{t .LangCode "site.stats"}}:<pre style="overflow: hidden;" id="site-stats-text">{{if .SiteStats}}{{.SiteStats.Text}}{{end}}</pre></p>
<script type="text/javascript">
// 首页统计已改为程序内直接计算(服务端 30 分钟缓存),这里每 30 分钟定时拉取最新值刷新,
// 与原来 crontab 生成 question.count.txt 的节奏一致,且不再依赖外部静态文件。
// 统计数字保持服务端 30 分钟缓存,页面每 30 分钟拉一次最新值就地更新(不整页刷新)。
// 与原来写 textContent 整块文本的做法等价,只是改为按字段更新三张数字卡。
document.addEventListener('DOMContentLoaded', function () {
setInterval(function () {
fetch("/api/v1/site-stats")
.then(function (r) { return r.json(); })
.then(function (resp) {
var d = resp.data;
if (!d || !d.text) return;
var el = document.getElementById("site-stats-text");
if (el) el.textContent = d.text;
if (!d) return;
["registered_users", "total_questions", "answered_questions",
"last_question_at", "last_answer_at", "updated_at"].forEach(function (k) {
var v = d[k];
if (v === undefined || v === null || v === "") return;
var el = document.querySelector('[data-stat="' + k + '"]');
if (el) el.textContent = v;
});
})
.catch(function () {});
}, 30 * 60 * 1000);
});
</script>
<!--<script src="https://r2.337845818.best/oss-to-r2/nekobox/cdn/waline/client/v2/waline.js"></script>
<link href='https://r2.337845818.best/oss-to-r2/nekobox/cdn/waline/client/v2/waline-no-commits.css' rel='stylesheet' />-->
<div id="waline"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const waline = Waline.init({
el: '#waline',
//后端服务器地址
serverURL: 'https://waline.337845818.best/',
path: window.location.href,
// 中文默认
locale: {
nick: '昵称',
nickError: '昵称不能少于3个字符',
mail: '邮箱',
mailError: '请填写正确的邮件地址',
link: '网址',
optional: '可选',
placeholder: '欢迎评论(可以无需登录,但需要手工审核,不会立即展现)',
sofa: '来发评论吧~',
submit: '提交',
like: '喜欢',
cancelLike: '取消喜欢',
reply: '回复',
cancelReply: '取消回复',
comment: '评论',
refresh: '刷新',
more: '加载更多...',
preview: '预览',
emoji: '表情',
uploadImage: '上传图片',
seconds: '秒前',
minutes: '分钟前',
hours: '小时前',
days: '天前',
now: '刚刚',
uploading: '正在上传',
login: '登录',
logout: '退出',
admin: '博主',
sticky: '置顶',
word: '字',
wordHint: '评论字数应在 $0 到 $1 字之间!\n当前字数:$2',
anonymous: '无名小狼',
level0: '潜水',
level1: '冒泡',
level2: '吐槽',
level3: '活跃',
level4: '话痨',
level5: '传说',
gif: '表情包',
gifSearchPlaceholder: '搜索表情包',
profile: '个人资料',
approved: '通过',
waiting: '待审核',
spam: '垃圾',
unsticky: '取消置顶',
oldest: '按倒序',
latest: '按正序',
hottest: '按热度',
reactionTitle: '',
reaction0: '哇哦',
reaction1: 'prpr',
reaction2: '流汗',
reaction3: '生气',
reaction4: '流鼻血',
reaction5: 'あべ',
},
reaction: [
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGQ3OTRhYzRjMTBkZmZhZDVmZTA1EIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChQwOGEyOTlhYzRjMTBmOWFkZjAwYRCIzNcv.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGRhOTVhYzRjMTBkNmViYTVlNTA2EIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGU1OTJhYzRjMTBmMzk3ZmVmNTAyEIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGNkODdhYzRjMTBjYmEwOTFjNjAxEIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChQwODliOGVhYzRjMTBmOGM0ZWY1ZRCIzNcv.gif',
],
meta: ['nick','mail'],
requiredMeta: ['mail'],
pageview: true,
search: false,
login: 'enable',
wordLimit: 50,
pageSize: 5,
avatar: 'identicon',
emoji: [
'https://gcore.jsdelivr.net/gh/tamakyi/emoji-collection@main/acfun-color/60x60',
],
//禁止图片上传按钮
imageUploader: false,
//不显示版权标志
copyright: false,
});
});
</script>
{{template "base/footer" .}}
<div id="waline"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const waline = Waline.init({
el: '#waline',
//后端服务器地址
serverURL: 'https://waline.337845818.best/',
path: window.location.href,
// 中文默认
locale: {
nick: '昵称',
nickError: '昵称不能少于3个字符',
mail: '邮箱',
mailError: '请填写正确的邮件地址',
link: '网址',
optional: '可选',
placeholder: '欢迎评论(可以无需登录,但需要手工审核,不会立即展现)',
sofa: '来发评论吧~',
submit: '提交',
like: '喜欢',
cancelLike: '取消喜欢',
reply: '回复',
cancelReply: '取消回复',
comment: '评论',
refresh: '刷新',
more: '加载更多...',
preview: '预览',
emoji: '表情',
uploadImage: '上传图片',
seconds: '秒前',
minutes: '分钟前',
hours: '小时前',
days: '天前',
now: '刚刚',
uploading: '正在上传',
login: '登录',
logout: '退出',
admin: '博主',
sticky: '置顶',
word: '字',
wordHint: '评论字数应在 $0 到 $1 字之间!\n当前字数:$2',
anonymous: '无名小狼',
level0: '潜水',
level1: '冒泡',
level2: '吐槽',
level3: '活跃',
level4: '话痨',
level5: '传说',
gif: '表情包',
gifSearchPlaceholder: '搜索表情包',
profile: '个人资料',
approved: '通过',
waiting: '待审核',
spam: '垃圾',
unsticky: '取消置顶',
oldest: '按倒序',
latest: '按正序',
hottest: '按热度',
reactionTitle: '',
reaction0: '哇哦',
reaction1: 'prpr',
reaction2: '流汗',
reaction3: '生气',
reaction4: '流鼻血',
reaction5: 'あべ',
},
reaction: [
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGQ3OTRhYzRjMTBkZmZhZDVmZTA1EIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChQwOGEyOTlhYzRjMTBmOWFkZjAwYRCIzNcv.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGRhOTVhYzRjMTBkNmViYTVlNTA2EIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGU1OTJhYzRjMTBmMzk3ZmVmNTAyEIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChYwOGNkODdhYzRjMTBjYmEwOTFjNjAxEIjM1y8.gif',
'https://ali-imgs.acfun.cn/kimg/bs2/zt-image-host/ChQwODliOGVhYzRjMTBmOGM0ZWY1ZRCIzNcv.gif',
],
meta: ['nick','mail'],
requiredMeta: ['mail'],
pageview: true,
search: false,
login: 'enable',
wordLimit: 50,
pageSize: 5,
avatar: 'identicon',
emoji: [
'https://gcore.jsdelivr.net/gh/tamakyi/emoji-collection@main/acfun-color/60x60',
],
//禁止图片上传按钮
imageUploader: false,
//不显示版权标志
copyright: false,
});
});
</script>
{{template "base/footer" .}}
+2 -14
View File
@@ -35,20 +35,8 @@
background-color: #353941;
color: #ffffff;
}
.tb-img-uploader {
width: 100%;
background-color: #f8f8f8;
display: flex;
padding-top: 5px;
padding-bottom: 5px;
align-items: center;
justify-content: flex-start;
gap: 5px;
}
html[data-theme="dark"] .tb-img-uploader {
background-color: #1b1e23;
}
</style>
<div>
<div class="uk-card uk-card-default">
<div class="uk-card-header">
@@ -262,7 +250,7 @@
<div class="uk-margin uk-text-center">
<textarea name="answer" class="uk-textarea" rows="5" maxlength="{{ .Limits.AnswerContentLen }}"
placeholder="{{t .LangCode "qitem.answerPlaceholder"}}">{{ .answer }}</textarea>
<div x-data="{ files: null }" style="width: 100%; background-color: #f8f8f8; display: flex; padding-top: 5px; padding-bottom: 5px; align-items: center; justify-content: start;gap: 5px">
<div x-data="{ files: null }" class="tb-img-uploader">
<div uk-form-custom>
<a href="#" class="uk-icon-link" uk-icon="image" style="margin-left: 10px"></a>
<span style="font-size: 12px" x-text="files ? files[0].name : '{{t .LangCode "ask.addImage"}}'"></span>
+28 -14
View File
@@ -5,6 +5,8 @@
<link rel="apple-touch-icon" href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous"/>
<link rel="icon" sizes="192x192" href="{{.PageUser.Avatar}}?v=2" crossorigin="anonymous">
<style>
/* 封面高度(原内联 style,改到类里便于和遮罩一起维护) */
.tb-profile-cover { min-height: 260px; }
/* 投票列表:与探索页"全部投票"一致的列表行样式 */
.profile-votes-list { display: grid; gap: 0; }
.profile-vote-item {
@@ -14,16 +16,16 @@
gap: 16px;
min-width: 0;
padding: 14px 16px;
border-bottom: 1px solid var(--tb-border, #e9e9e9);
border-bottom: 1px solid var(--tb-border);
text-decoration: none;
}
.profile-vote-item:first-child { border-top: 1px solid var(--tb-border, #e9e9e9); }
.profile-vote-item:hover { background: var(--tb-surface-hover, #f8fbff); text-decoration: none; }
.profile-vote-item:first-child { border-top: 1px solid var(--tb-border); }
.profile-vote-item:hover { background: var(--tb-surface-hover); text-decoration: none; }
.profile-vote-content { min-width: 0; }
.profile-vote-title {
display: block;
overflow: hidden;
color: var(--tb-text, #333);
color: var(--tb-text);
font-size: 0.98rem;
font-weight: 600;
line-height: 1.45;
@@ -40,7 +42,7 @@
.profile-vote-meta .uk-label { font-size: 0.72rem; line-height: 1.6; }
.profile-vote-action {
flex: 0 0 auto;
color: var(--tb-primary, #1e87f0);
color: var(--tb-primary);
font-size: 0.82rem;
white-space: nowrap;
}
@@ -55,9 +57,9 @@
padding: 4px;
margin-top: 20px;
margin-bottom: 18px;
border: 1px solid var(--tb-border, #e5e5e5);
border: 1px solid var(--tb-border);
border-radius: 9px;
background: var(--tb-surface-2, #fafafa);
background: var(--tb-surface-2);
}
.profile-tab {
flex: 1;
@@ -65,26 +67,26 @@
border-radius: 6px;
padding: 9px 15px;
background: transparent;
color: var(--tb-text-muted, #666);
color: var(--tb-text-muted);
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s ease, color 0.2s ease;
}
.profile-tab:hover { color: var(--tb-primary, #1e87f0); }
.profile-tab:hover { color: var(--tb-primary); }
.profile-tab.is-active {
background: var(--tb-surface, #fff);
background: var(--tb-surface);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
color: var(--tb-primary, #1e87f0);
color: var(--tb-primary);
font-weight: 600;
}
/* 功能关闭时置灰禁用的 Tab(如投票功能关闭) */
.profile-tab.is-disabled,
.profile-tab:disabled {
cursor: not-allowed;
color: var(--tb-text-muted, #999);
color: var(--tb-text-muted);
opacity: 0.7;
}
.profile-tab.is-disabled:hover { color: var(--tb-text-muted, #999); }
.profile-tab.is-disabled:hover { color: var(--tb-text-muted); }
.profile-panel[hidden] { display: none !important; }
html[data-theme="dark"] .profile-tabs {
border-color: var(--tb-border);
@@ -95,7 +97,7 @@
html[data-theme="dark"] .profile-tab.is-disabled { color: var(--tb-text-muted); opacity: 0.55; }
</style>
<div class="uk-card uk-card-default uk-text-center">
<div class="uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light" style="min-height: 260px;" data-src="{{ .PageUser.Background }}" uk-img>
<div class="tb-profile-cover uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light{{ if .PageUser.Background }} tb-cover{{ end }}" data-src="{{ .PageUser.Background }}" uk-img>
<div class="uk-card-body">
<img class="uk-border-circle uk-box-shadow-large" src="{{ if .PageUser.Avatar }}{{ .PageUser.Avatar }}?v=2{{ else }}{{ .DefaultAvatar }}{{ end }}" crossorigin="anonymous" style="width: 100px; height: 100px; max-width: 100%; max-height: 100%;">
<h3 id="usernamecolor">{{ .PageUser.Name }}</h3>
@@ -133,6 +135,18 @@
<script>
document.getElementById("usernamecolor").style.color = "{{.PageUser.Usernamecolor}}";
document.getElementById("introcolor").style.color = "{{.PageUser.Introcolor}}";
// 封面上的文字颜色由箱主自选,深浅不定:按用户名颜色的亮度决定遮罩方向
// (深字上面压浅色遮罩、浅字上面压深色遮罩),保证任何配色都能读。
// 无背景图时页面没有 .tb-cover,这里什么都不做,保持原样。
(function () {
var cover = document.querySelector('.tb-cover');
var el = document.getElementById('usernamecolor');
if (!cover || !el) return;
var m = window.getComputedStyle(el).color.match(/\d+/g);
if (!m || m.length < 3) return;
var lum = (0.299 * m[0] + 0.587 * m[1] + 0.114 * m[2]) / 255;
if (lum > 0.62) cover.classList.add('tb-cover-dark');
})();
</script>
<div>
<div class="uk-card uk-card-default uk-card-small uk-card-body">
@@ -97,7 +97,7 @@
<div class="uk-margin uk-text-center">
<textarea name="content" class="uk-textarea" rows="5" placeholder="{{t .LangCode "ask.placeholder"}}"
maxlength="{{ .Limits.QuestionContentLen }}">{{.content}}</textarea>
<div x-data="{ files: null }" style="width: 100%; background-color: #f8f8f8; display: flex; padding-top: 5px; padding-bottom: 5px; align-items: center; justify-content: start;gap: 5px">
<div x-data="{ files: null }" class="tb-img-uploader">
<div uk-form-custom>
<a href="#" class="uk-icon-link" uk-icon="image" style="margin-left: 10px"></a>
<span style="font-size: 12px" x-text="files ? files[0].name : '{{t .LangCode "ask.addImage"}}'"></span>
+24 -24
View File
@@ -30,13 +30,13 @@
}
.explore-name a {
text-decoration: none;
color: #333;
color: var(--tb-text);
}
.explore-name a:hover {
color: #1e87f0;
color: var(--tb-primary);
}
.explore-intro {
color: #666;
color: var(--tb-text-secondary);
margin-bottom: 10px;
font-size: 0.9rem;
display: -webkit-box;
@@ -60,7 +60,7 @@
display: flex;
align-items: center;
padding: 10px;
background: #f8f8f8;
background: var(--tb-surface-2);
border-radius: 8px;
transition: background 0.2s;
}
@@ -83,7 +83,7 @@
}
.recommend-name a {
text-decoration: none;
color: #333;
color: var(--tb-text);
}
.recommend-tags {
display: flex;
@@ -108,14 +108,14 @@
gap: 16px;
min-width: 0;
padding: 16px 18px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 8px;
background: #fafafa;
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: #1e87f0;
border-color: var(--tb-primary);
background: #f5faff;
text-decoration: none;
transform: translateY(-1px);
@@ -126,7 +126,7 @@
.hot-vote-title {
display: block;
overflow: hidden;
color: #333;
color: var(--tb-text);
font-size: 0.98rem;
font-weight: 600;
line-height: 1.45;
@@ -146,7 +146,7 @@
}
.hot-vote-action {
flex: 0 0 auto;
color: #1e87f0;
color: var(--tb-primary);
font-size: 0.82rem;
white-space: nowrap;
}
@@ -176,25 +176,25 @@
padding: 4px;
margin: 2px auto 20px;
max-width: 320px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 9px;
background: #fafafa;
background: var(--tb-surface-2);
}
.explore-tab {
border: 0;
border-radius: 6px;
padding: 8px 15px;
background: transparent;
color: #666;
color: var(--tb-text-secondary);
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s ease, color 0.2s ease;
}
.explore-tab:hover { color: #1e87f0; }
.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: #1e87f0;
color: var(--tb-primary);
font-weight: 600;
}
.explore-panel[hidden] { display: none !important; }
@@ -211,12 +211,12 @@
border: 1px solid #dce9ff;
border-radius: 7px;
padding: 7px 11px;
color: #1e87f0;
color: var(--tb-primary);
background: #f5faff;
cursor: pointer;
font-size: 0.84rem;
}
.vote-random-btn:hover { border-color: #1e87f0; background: #edf6ff; }
.vote-random-btn:hover { border-color: var(--tb-primary); background: #edf6ff; }
.all-votes-list { display: grid; gap: 10px; }
.all-vote-item {
display: flex;
@@ -224,10 +224,10 @@
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border-bottom: 1px solid #e9e9e9;
border-bottom: 1px solid var(--tb-border);
text-decoration: none;
}
.all-vote-item:first-child { border-top: 1px solid #e9e9e9; }
.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 {
@@ -241,16 +241,16 @@
html[data-theme="dark"] .all-vote-item:hover { background: rgba(78, 168, 245, 0.10); }
.explore-notice {
background: #f0f7ff;
border-left: 4px solid #1e87f0;
border-left: 4px solid var(--tb-primary);
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 4px;
color: #333;
color: var(--tb-text);
font-size: 0.9rem;
line-height: 1.6;
}
.explore-notice strong {
color: #1e87f0;
color: var(--tb-primary);
}
/* 极简「换一批」按钮:描边胶囊 + 悬停浅蓝底色 + 点击旋转反馈 */
.tb-shuffle-btn {
@@ -260,7 +260,7 @@
padding: 5px 14px;
font-size: 0.85rem;
line-height: 1.4;
color: #1e87f0;
color: var(--tb-primary);
background: transparent;
border: 1px solid #e3e3e3;
border-radius: 999px;
@@ -269,7 +269,7 @@
}
.tb-shuffle-btn:hover {
background: #f0f7ff;
border-color: #1e87f0;
border-color: var(--tb-primary);
}
.tb-shuffle-btn:active {
transform: scale(0.96);
+29 -29
View File
@@ -35,7 +35,7 @@
.preview-card {
background: #fff;
padding: 20px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 8px;
text-align: center;
}
@@ -76,9 +76,9 @@
gap: 4px;
padding: 4px;
margin-bottom: 18px;
border: 1px solid var(--tb-border, #e5e5e5);
border: 1px solid var(--tb-border);
border-radius: 9px;
background: var(--tb-surface-2, #fafafa);
background: var(--tb-surface-2);
}
.profile-tab {
flex: 1;
@@ -86,17 +86,17 @@
border-radius: 6px;
padding: 9px 8px;
background: transparent;
color: var(--tb-text-muted, #666);
color: var(--tb-text-muted);
cursor: pointer;
font-size: 0.9rem;
white-space: nowrap;
transition: background 0.2s ease, color 0.2s ease;
}
.profile-tab:hover { color: var(--tb-primary, #1e87f0); }
.profile-tab:hover { color: var(--tb-primary); }
.profile-tab.is-active {
background: var(--tb-surface, #fff);
background: var(--tb-surface);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
color: var(--tb-primary, #1e87f0);
color: var(--tb-primary);
font-weight: 600;
}
.profile-panel[hidden] { display: none !important; }
@@ -113,7 +113,7 @@
flex-direction: column;
gap: 12px;
padding: 16px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 8px;
background: #fff;
}
@@ -137,7 +137,7 @@
display: block;
font-size: 0.78rem;
line-height: 1.5;
color: #999;
color: var(--tb-text-muted);
}
.feat-field { display: block; }
.feat-field > label > span {
@@ -172,18 +172,18 @@
align-items: center;
gap: 3px;
padding: 3px 8px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 999px;
font-size: 0.78rem;
color: #666;
color: var(--tb-text-secondary);
cursor: pointer;
user-select: none;
}
.feat-day input { margin: 0; }
.feat-day.is-on {
border-color: #1e87f0;
border-color: var(--tb-primary);
background: rgba(30, 135, 240, 0.08);
color: #1e87f0;
color: var(--tb-primary);
}
.feat-schedule-times {
display: flex;
@@ -195,12 +195,12 @@
align-items: center;
gap: 6px;
font-size: 0.82rem;
color: #666;
color: var(--tb-text-secondary);
}
.feat-schedule-times input[type="time"] {
width: 130px;
padding: 4px 8px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 4px;
}
html[data-theme="dark"] .feat-day {
@@ -208,9 +208,9 @@
color: var(--tb-text-muted);
}
html[data-theme="dark"] .feat-day.is-on {
border-color: #1e87f0;
border-color: var(--tb-primary);
background: rgba(30, 135, 240, 0.14);
color: #1e87f0;
color: var(--tb-primary);
}
html[data-theme="dark"] .feat-schedule-times input[type="time"] {
border-color: var(--tb-border);
@@ -219,7 +219,7 @@
}
.feat-quota-line {
font-size: 0.82rem;
color: #666;
color: var(--tb-text-secondary);
text-align: center;
}
.feat-actions {
@@ -259,19 +259,19 @@
align-items: center;
gap: 6px;
padding: 6px 8px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 4px;
background: #fff;
cursor: text;
}
.tb-tag-box:focus-within { border-color: #1e87f0; }
.tb-tag-box:focus-within { border-color: var(--tb-primary); }
.tb-tag-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 8px;
border-radius: 3px;
background: #1e87f0;
background: var(--tb-primary);
color: #fff;
font-size: 0.875rem;
line-height: 1.8;
@@ -302,7 +302,7 @@
gap: 8px;
margin-top: 4px;
font-size: 0.75rem;
color: #999;
color: var(--tb-text-muted);
}
.tb-tag-error { color: #f0506e; }
html[data-theme="dark"] .tb-tag-box {
@@ -316,7 +316,7 @@
/* ===== profile account 面板卡片化(复用 UIkit 变量体系,暗色自动跟随) ===== */
.tb-pcard {
background: #fff;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 10px;
padding: 13px 14px 8px;
margin-bottom: 12px;
@@ -332,7 +332,7 @@
float: right;
font-size: 0.76rem;
font-weight: 400;
color: #999;
color: var(--tb-text-muted);
line-height: 1.6;
}
/* 头像与背景预览:两项同行、整体居中;头像框与背景框“显示高度”一致(150px) */
@@ -355,7 +355,7 @@
.tb-media-frame {
display: block;
flex: none;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
background: #f7f7f7;
overflow: hidden;
}
@@ -382,9 +382,9 @@
margin: 8px 0 0;
font-size: 0.72rem;
line-height: 1;
color: #666;
color: var(--tb-text-secondary);
background: #f4f4f4;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 999px;
padding: 4px 9px;
}
@@ -392,7 +392,7 @@
display: block;
font-size: 0.78rem;
line-height: 1.5;
color: #999;
color: var(--tb-text-muted);
margin-top: 6px;
}
.tb-media-actions { margin-top: 10px; }
@@ -421,7 +421,7 @@
text-align: center;
font-size: 0.78rem;
line-height: 1.6;
color: #999;
color: var(--tb-text-muted);
}
.tb-account-note:last-child { margin-bottom: 0; }
/* 裁剪弹窗:图片区域限制在视口内,移动端可用 */
+11 -11
View File
@@ -8,9 +8,9 @@
padding: 4px;
margin: 6px auto 20px;
max-width: 320px;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 9px;
background: #fafafa;
background: var(--tb-surface-2);
}
.qlist-tab {
flex: 1 1 0;
@@ -18,16 +18,16 @@
border-radius: 6px;
padding: 8px 15px;
background: transparent;
color: #666;
color: var(--tb-text-secondary);
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s ease, color 0.2s ease;
}
.qlist-tab:hover { color: #1e87f0; }
.qlist-tab:hover { color: var(--tb-primary); }
.qlist-tab.is-active {
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
color: #1e87f0;
color: var(--tb-primary);
font-weight: 600;
}
.qlist-panel[hidden] { display: none !important; }
@@ -38,23 +38,23 @@
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border-bottom: 1px solid #e9e9e9;
border-bottom: 1px solid var(--tb-border);
text-decoration: none;
}
.all-vote-item:first-child { border-top: 1px solid #e9e9e9; }
.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%; }
.hot-vote-title {
display: block;
overflow: hidden;
color: #333;
color: var(--tb-text);
font-size: 0.98rem;
font-weight: 600;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
}
.hot-vote-title:hover { color: #1e87f0; }
.hot-vote-title:hover { color: var(--tb-primary); }
.hot-vote-meta {
display: flex;
flex-wrap: wrap;
@@ -63,7 +63,7 @@
margin-top: 8px;
}
.hot-vote-meta .uk-label { font-size: 0.72rem; line-height: 1.6; }
.hot-vote-action { flex: 0 0 auto; color: #1e87f0; font-size: 0.82rem; white-space: nowrap; }
.hot-vote-action { flex: 0 0 auto; color: var(--tb-primary); font-size: 0.82rem; white-space: nowrap; }
/* 提问列表右上角:状态标签与操作按钮统一尺寸(等宽等高、垂直居中、完全对齐)。
每个元素显式带 .qlist-pill 类,选择器挂在 .qlist-pills 容器下提升优先级,
避免浏览器对 <button> 的默认样式或 UIkit .uk-label 造成差异。
@@ -132,7 +132,7 @@
}
.qlist-stat {
background: #fff;
border: 1px solid #e5e5e5;
border: 1px solid var(--tb-border);
border-radius: 9px;
padding: 8px 6px;
text-align: center;
+6 -6
View File
@@ -23,9 +23,9 @@
gap: 10px;
padding: 12px 14px;
margin-bottom: 8px;
border: 1px solid var(--tb-border, #e5e5e5);
border: 1px solid var(--tb-border);
border-radius: 8px;
background: var(--tb-surface, #fff);
background: var(--tb-surface);
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
}
@@ -41,12 +41,12 @@
overflow-wrap: anywhere;
}
.tb-choice:hover {
border-color: var(--tb-primary, #1e87f0);
background: var(--tb-surface-hover, #f5faff);
border-color: var(--tb-primary);
background: var(--tb-surface-hover);
}
.tb-choice:has(input:checked) {
border-color: var(--tb-primary, #1e87f0);
background: var(--tb-surface-active, #edf6ff);
border-color: var(--tb-primary);
background: var(--tb-surface-active);
}
html[data-theme="dark"] .tb-choice {
border-color: var(--tb-border);
+114
View File
@@ -0,0 +1,114 @@
package templates
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/stats"
)
// TestGenPreview 仅用于本地肉眼核对样式改动,产出 outputs/preview-*.html。
// 不在 CI 里跑:需要 TEST_PREVIEW=1 才执行。
func TestGenPreview(t *testing.T) {
if os.Getenv("TEST_PREVIEW") != "1" {
t.Skip("set TEST_PREVIEW=1 to generate preview files")
}
outDir := filepath.Join("..", "outputs")
if err := os.MkdirAll(outDir, 0o755); err != nil {
t.Fatal(err)
}
base := map[string]interface{}{
"LangCode": "zh-CN",
"SupportedLangs": []map[string]string{{"Code": "zh-CN", "Name": "简体中文"}, {"Code": "ja", "Name": "日本語"}, {"Code": "en", "Name": "English"}},
"IsLogged": false,
"IsUserPage": false,
"IsPixel": false,
"VConsole": false,
"CurrentURI": "/",
"ExternalURL": "https://box.shiroko.one",
"CaptchaType": "math",
"Limits": conf.Limits,
"Title": "TamaBox",
"SiteStats": &stats.View{
RegisteredUsers: 1284, TotalQuestions: 56321, AnsweredQuestions: 31208,
LastQuestionAt: "2026-10-19 10:12:44",
LastAnswerAt: "2026-10-19 09:58:02",
UpdatedAt: "2026-10-19 10:00:00",
},
}
for _, theme := range []string{"light", "dark"} {
home, err := renderToString(t, "home", base)
if err != nil {
t.Fatalf("render home: %v", err)
}
writePreview(t, outDir, "preview-home-"+theme+".html", home, theme, "../static")
}
// 提问箱主页:带背景图(渐变占位)+ 浅色文字,验证遮罩方向
boxData := publicBaseData()
boxData["VConsole"] = false
boxData["Background"] = "data:image/svg+xml;utf8," +
strings.NewReplacer("#", "%23").Replace(
`<svg xmlns='http://www.w3.org/2000/svg' width='900' height='500'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='#3b6ea5'/><stop offset='1' stop-color='#d98b6a'/></linearGradient></defs><rect width='900' height='500' fill='url(#g)'/></svg>`)
boxData["PageUser"].(map[string]interface{})["Usernamecolor"] = "#ffffff"
boxData["PageUser"].(map[string]interface{})["Introcolor"] = "#ffffff"
// 无头 Chrome 里 uk-img 的懒加载不一定触发,这里直接把背景图写进 CSS 便于核对遮罩
coverCSS := `<style>.tb-profile-cover{background-image:url("` + boxData["Background"].(string) +
`");background-size:cover;background-position:center;}</style>`
for _, theme := range []string{"light", "dark"} {
box, err := renderToString(t, "question/list", boxData)
if err != nil {
t.Fatalf("render list: %v", err)
}
box = strings.Replace(box, "</head>", coverCSS+"</head>", 1)
writePreview(t, outDir, "preview-box-"+theme+".html", box, theme, "../static")
}
// 探索页:核对令牌替换后的卡片/列表配色
explore := voteBaseData()
explore["LangCode"] = "zh-CN"
explore["IsUserPage"] = false
explore["Keyword"] = ""
explore["Sort"] = "latest"
explore["Page"] = 1
explore["TotalPages"] = 1
explore["PrevPage"] = 0
explore["NextPage"] = 2
explore["HotTags"] = []string{"杂谈", "学习", "音乐"}
explore["HotVotes"] = nil
explore["AllVotes"] = nil
explore["RecommendUsers"] = []interface{}{}
explore["Users"] = []map[string]interface{}{
{"Name": "小狼", "Domain": "tamakyi", "Avatar": "", "Intro": "随便问,友善一点,看到都会回。", "Tags": "杂谈,日常"},
{"Name": "Kitsune", "Domain": "kitsune", "Avatar": "", "Intro": "摄影 / 音乐 / 一点点代码", "Tags": "摄影"},
{"Name": "残响", "Domain": "echo", "Avatar": "", "Intro": "长期潜水,偶尔冒泡。", "Tags": "杂谈"},
{"Name": "Mika", "Domain": "mika", "Avatar": "", "Intro": "想认识更多有趣的人。", "Tags": "学习,音乐"},
}
for _, theme := range []string{"light", "dark"} {
page, err := renderToString(t, "user/explore", explore)
if err != nil {
t.Fatalf("render explore: %v", err)
}
writePreview(t, outDir, "preview-explore-"+theme+".html", page, theme, "../static")
}
}
// writePreview 把 CDN 换成可直连的镜像、钉死主题,仅用于本地预览。
func writePreview(t *testing.T, dir, name, html, theme, staticPrefix string) {
t.Helper()
html = strings.ReplaceAll(html, "https://cdnjs.snrat.com/", "https://cdnjs.cloudflare.com/")
if theme == "dark" {
html = strings.Replace(html, `<html lang="zh-CN">`, `<html lang="zh-CN" data-theme="dark">`, 1)
}
html = strings.ReplaceAll(html, `href="/favicon.ico"`, `href="`+staticPrefix+`/favicon.ico"`)
html = strings.Replace(html, `<title>`, `<title>预览 `+time.Now().Format("15:04")+` · `, 1)
if err := os.WriteFile(filepath.Join(dir, name), []byte(html), 0o644); err != nil {
t.Fatal(err)
}
}