This commit is contained in:
tamakyi
2026-10-19 15:23:11 +08:00
parent 86dc8384bc
commit e9fba8b9ed
84 changed files with 34468 additions and 5386 deletions
+5 -3
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{.LangCode}}">
<html lang="{{.LangCode}}"{{ if .ThemeSlug }} data-theme-style="{{ .ThemeSlug }}"{{ end }}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -20,8 +20,10 @@
<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"/>
<!-- 全站基础样式(由原内联块抽出,可被浏览器缓存):改 static/css/base.css 后把 v 版本号 +1 -->
<link rel="stylesheet" href="/static/css/base.css?v=20261019d"/>
<!-- 全站基础样式(由原内联块抽出,可被浏览器缓存):改样式后把 conf.StaticVersion +1 -->
<link rel="stylesheet" href="/static/css/base.css?v={{ .StaticVersion }}"/>
{{ if .ThemeSlug }}<!-- 箱主主题:只覆盖配色/图案/页面背景,不涉及布局;仅提问箱相关页面加载 -->
<link rel="stylesheet" href="/static/css/themes/{{ .ThemeSlug }}.css?v={{ .StaticVersion }}"/>{{ end }}
<!-- 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>
+28
View File
@@ -788,6 +788,34 @@
<span class="tb-account-note">{{t .LangCode "admin.deactivateHint" | safe}}</span>
</div>
</section>
<!-- 主题外观:只改配色 / 图案 / 整页背景,不涉及布局;仅提问箱相关页面生效 -->
{{ if .Themes }}
<section class="tb-pcard">
<h3 class="tb-pcard-title">{{t .LangCode "profile.theme.title"}}</h3>
<form method="post" action="/user/profile/theme">
{{ .CSRFTokenHTML }}
<p class="tb-theme-hint">{{t .LangCode "profile.theme.hint"}}</p>
<div class="tb-theme-grid">
<label class="tb-theme-opt">
<input class="uk-radio" type="radio" name="theme" value=""{{ if eq .CurrentTheme "" }} checked{{ end }}>
<span class="tb-theme-swatch" style="background: linear-gradient(135deg, var(--tb-surface-2) 0 55%, var(--tb-border) 55% 100%)"></span>
<span class="tb-theme-name">{{t .LangCode "profile.theme.followDefault"}}</span>
</label>
{{ range .Themes }}
<label class="tb-theme-opt">
<input class="uk-radio" type="radio" name="theme" value="{{ .Slug }}"{{ if eq $.CurrentTheme .Slug }} checked{{ end }}>
<span class="tb-theme-swatch"{{ if .Swatch }} style="background: {{ .Swatch }}"{{ end }}></span>
<span class="tb-theme-name">{{ t $.LangCode (printf "theme.name.%s" .Slug) }}</span>
</label>
{{ end }}
</div>
<div class="tb-card-save">
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "profile.theme.save"}}</button>
</div>
</form>
</section>
{{ end }}
</div><!-- /用户设置面板 -->
+56 -1
View File
@@ -3,6 +3,7 @@ package templates
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
@@ -72,6 +73,29 @@ func TestGenPreview(t *testing.T) {
writePreview(t, outDir, "preview-box-"+theme+".html", box, theme, "../static")
}
// 主题预览:与箱主页同一份数据,只是打开 sakura 主题(验证令牌覆盖与深色块)
themeBox := publicBaseData()
themeBox["VConsole"] = false
themeBox["ThemeSlug"] = "sakura"
themeBox["PageUser"].(map[string]interface{})["Background"] = bgURI
themeBox["PageUser"].(map[string]interface{})["Usernamecolor"] = "#3a2b30"
themeBox["PageUser"].(map[string]interface{})["Introcolor"] = "#6b545c"
themeCSS, err := os.ReadFile(filepath.Join("..", "static", "css", "themes", "sakura.css"))
if err != nil {
t.Fatalf("read theme css: %v", err)
}
for _, theme := range []string{"light", "dark"} {
page, err := renderToString(t, "question/list", themeBox)
if err != nil {
t.Fatalf("render themed list: %v", err)
}
// 预览页走 file://,把主题 css 内联进去(真实页面是一次带缓存的静态请求)。
// 用正则匹配:预览数据里没有 StaticVersion,v 参数为空串,不能按固定串替换。
link := regexp.MustCompile(`<link rel="stylesheet" href="/static/css/themes/sakura\.css\?v=[^"]*"/>`)
page = link.ReplaceAllString(page, "<style>"+string(themeCSS)+"</style>")
writePreview(t, outDir, "preview-theme-sakura-"+theme+".html", page, theme, "../static")
}
// 探索页:核对令牌替换后的卡片/列表配色
explore := voteBaseData()
explore["LangCode"] = "zh-CN"
@@ -102,6 +126,12 @@ func TestGenPreview(t *testing.T) {
// 设置页「内容防护」面板:核对是否与其它面板同样是白底卡片
profile := profileBaseData()
// 主题选择器(account 面板):用固定假数据,避免测试依赖 conf 的 [theme] 段
profile["Themes"] = []map[string]interface{}{
{"Slug": "default", "Swatch": ""},
{"Slug": "sakura", "Swatch": "linear-gradient(135deg, #fff6f8 0 55%, #e5678f 55% 100%)"},
}
profile["CurrentTheme"] = "sakura"
profile["ActiveProfileTab"] = "security"
profile["FeatureQuestion"] = nil
profile["FeatureChat"] = nil
@@ -113,6 +143,22 @@ func TestGenPreview(t *testing.T) {
}
writePreview(t, outDir, "preview-profile-"+theme+".html", page, theme, "../static")
}
// 设置页「账号」面板:主题选择器的实际排版(卡片在面板末尾)
profileAccount := profileBaseData()
profileAccount["Themes"] = profile["Themes"]
profileAccount["CurrentTheme"] = "sakura"
profileAccount["ActiveProfileTab"] = "account"
profileAccount["FeatureQuestion"] = nil
profileAccount["FeatureChat"] = nil
profileAccount["FeatureVote"] = nil
for _, theme := range []string{"light"} {
page, err := renderToString(t, "user/profile", profileAccount)
if err != nil {
t.Fatalf("render account profile: %v", err)
}
writePreview(t, outDir, "preview-profile-account-"+theme+".html", page, theme, "../static")
}
}
// writePreview 把 CDN 换成可直连的镜像、钉死主题,仅用于本地预览。
@@ -120,7 +166,16 @@ 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> 上可能还有 data-theme-style,只在这个开标签内判断/插入;
// 不能全文搜索 —— 内联的主题 css 里有 html[data-theme="dark"] 选择器
if i := strings.Index(html, "<html"); i >= 0 {
if j := strings.Index(html[i:], ">"); j > 0 {
tag := html[i : i+j]
if !strings.Contains(tag, "data-theme=") {
html = html[:i+j] + ` data-theme="dark"` + html[i+j:]
}
}
}
}
html = strings.ReplaceAll(html, `href="/favicon.ico"`, `href="`+staticPrefix+`/favicon.ico"`)
html = strings.Replace(html, `<title>`, `<title>预览 `+time.Now().Format("15:04")+` · `, 1)