186 lines
7.8 KiB
Go
186 lines
7.8 KiB
Go
package templates
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"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
|
|
bgURI := "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>`)
|
|
// 注意:模板读的是 .PageUser.Background,必须写进 PageUser 里,否则 {{ if .PageUser.Background }} 为假、遮罩类不会渲染
|
|
boxData["PageUser"].(map[string]interface{})["Background"] = bgURI
|
|
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("` + bgURI +
|
|
`");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")
|
|
}
|
|
|
|
// 主题预览:与箱主页同一份数据,只是打开 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"
|
|
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")
|
|
}
|
|
|
|
// 设置页「内容防护」面板:核对是否与其它面板同样是白底卡片
|
|
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
|
|
profile["FeatureVote"] = nil
|
|
for _, theme := range []string{"light", "dark"} {
|
|
page, err := renderToString(t, "user/profile", profile)
|
|
if err != nil {
|
|
t.Fatalf("render profile: %v", err)
|
|
}
|
|
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 换成可直连的镜像、钉死主题,仅用于本地预览。
|
|
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> 上可能还有 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)
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(html), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|