diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.yaml b/.gitea/ISSUE_TEMPLATE/feature_request.yaml
index 0bd5c34..7bb012d 100644
--- a/.gitea/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.gitea/ISSUE_TEMPLATE/feature_request.yaml
@@ -13,7 +13,7 @@ body:
attributes:
label: 要解决什么问题?
description: 从用户场景出发描述痛点
- placeholder: 管理员在 React 前台无法置顶帖子,必须切到旧版后台…
+ placeholder: 管理员在帖子详情页缺少某某操作入口…
validations:
required: true
- type: textarea
diff --git a/README.md b/README.md
index 7b70888..9213846 100644
--- a/README.md
+++ b/README.md
@@ -279,7 +279,7 @@ make dev # Linux / macOS
**何时需要完整构建:**
-- 修改 Go 代码、HTML 模板、embed 静态资源 → `go build` / `make build`
+- 修改 Go 代码、前端或 embed 静态资源 → `go build` / `make build`(或 `build.bat`)
- 发布单二进制前 → `npm run build` + `make build`
- 更新 README 界面截图 → 启动服务后执行 `node scripts/capture-screenshots.mjs`
diff --git a/app.ini.example b/app.ini.example
index 967b9ec..b1b483b 100644
--- a/app.ini.example
+++ b/app.ini.example
@@ -17,7 +17,7 @@ DATA = data
JWT_SECRET =
[oauth]
-; 可选:启动时若管理后台尚未配置,会用此处种子写入数据库一次
+; 可选:启动时若管理后台尚未配置,会用此处种子写入 oauth_clients 一次
; 日常请优先在管理后台「系统设置 → OIDC / SSO」修改(保存即生效)
; Gitea 认证源名称须与回调路径一致,例如名称 jiang13 对应:
; https://git.iioio.com/user/oauth2/jiang13/callback
@@ -34,3 +34,17 @@ BASE_URL =
TOKEN =
; 首次种子时若 BASE_URL+TOKEN 齐全且此项为 true,则写入并启用同步
SYNC_ENABLED = false
+
+[storage]
+; 可选种子:仅当管理后台尚未配置时写入一次;日常请在「系统设置 → 对象存储」修改(保存即生效)
+; TYPE = local | s3
+TYPE = local
+; 以下仅 TYPE = s3 时作为种子(MinIO / 七牛 / 又拍 / 阿里云 OSS 等)
+ENDPOINT =
+REGION = us-east-1
+BUCKET =
+ACCESS_KEY =
+SECRET_KEY =
+PUBLIC_BASE_URL =
+PREFIX =
+FORCE_PATH_STYLE = true
diff --git a/config/config.go b/config/config.go
index 5368015..0c05557 100644
--- a/config/config.go
+++ b/config/config.go
@@ -8,6 +8,24 @@ import (
"strings"
)
+// StorageTypeLocal / StorageTypeS3 上传存储后端
+const (
+ StorageTypeLocal = "local"
+ StorageTypeS3 = "s3"
+)
+
+// S3Config S3 兼容对象存储(MinIO / 七牛 / 又拍 / 阿里云 OSS 等)
+type S3Config struct {
+ Endpoint string // 例:https://s3.example.com 或 s3.example.com:9000
+ Region string
+ Bucket string
+ AccessKey string
+ SecretKey string
+ PublicBaseURL string // 公开访问根 URL(无尾斜杠),上传后返回此前缀下的绝对地址
+ Prefix string // 对象 key 前缀(可选,如 forum/)
+ ForcePathStyle bool // path-style(MinIO 等通常为 true;AWS 官方多为 false)
+}
+
// Config 应用全局配置:默认读工作目录下 app.ini,命令行可覆盖
type Config struct {
// 工作目录(默认可执行文件所在目录)
@@ -30,6 +48,9 @@ type Config struct {
GiteaBaseURL string
GiteaToken string
GiteaSyncEnabled bool
+ // 上传存储:local(默认)或 s3
+ StorageType string
+ S3 S3Config
// 日志文件路径
LogFile string
// 系统服务控制动作:install|uninstall|start|stop|restart|status,空表示正常运行
@@ -92,6 +113,14 @@ func Parse() (*Config, error) {
jwtSecret = fileCfg.JWTSecret
}
+ storageType := strings.ToLower(strings.TrimSpace(fileCfg.StorageType))
+ if storageType == "" {
+ storageType = StorageTypeLocal
+ }
+ if storageType != StorageTypeLocal && storageType != StorageTypeS3 {
+ return nil, fmt.Errorf("storage.TYPE 无效: %q(可选 local / s3)", fileCfg.StorageType)
+ }
+
cfg := &Config{
WorkPath: workPath,
ConfigFile: configFile,
@@ -105,18 +134,34 @@ func Parse() (*Config, error) {
GiteaBaseURL: normalizeRootURL(fileCfg.GiteaBaseURL),
GiteaToken: fileCfg.GiteaToken,
GiteaSyncEnabled: fileCfg.GiteaSyncEnabled,
- LogFile: filepath.Join(absData, "jiang13.log"),
- ServiceAction: action,
+ StorageType: storageType,
+ S3: S3Config{
+ Endpoint: strings.TrimSpace(fileCfg.S3Endpoint),
+ Region: strings.TrimSpace(fileCfg.S3Region),
+ Bucket: strings.TrimSpace(fileCfg.S3Bucket),
+ AccessKey: strings.TrimSpace(fileCfg.S3AccessKey),
+ SecretKey: strings.TrimSpace(fileCfg.S3SecretKey),
+ PublicBaseURL: normalizeRootURL(fileCfg.S3PublicBaseURL),
+ Prefix: normalizeStoragePrefix(fileCfg.S3Prefix),
+ ForcePathStyle: fileCfg.S3ForcePathStyle,
+ },
+ LogFile: filepath.Join(absData, "jiang13.log"),
+ ServiceAction: action,
}
+ // [storage] 仅作首次种子;运行时以管理后台为准,此处不强制校验 S3 完整性
+
needDirs := action == "" || action == "install"
if needDirs {
// 首次启动自动生成 app.ini,便于像 Gitea 一样改文件而不记一长串参数
if !configExists {
dataRel := resolveDataRelForINI(workPath, absData)
if err := writeAppINI(configFile, fileSettings{
- Port: port,
- DataRel: dataRel,
+ Port: port,
+ DataRel: dataRel,
+ StorageType: StorageTypeLocal,
+ S3ForcePathStyle: true,
+ S3Region: "us-east-1",
}); err != nil {
return nil, fmt.Errorf("生成默认配置文件失败: %w", err)
}
@@ -136,6 +181,18 @@ func Parse() (*Config, error) {
OAuthClientID: fileCfg.OAuthClientID,
OAuthClientSecret: fileCfg.OAuthClientSecret,
OAuthRedirectURIs: fileCfg.OAuthRedirectURIs,
+ GiteaBaseURL: fileCfg.GiteaBaseURL,
+ GiteaToken: fileCfg.GiteaToken,
+ GiteaSyncEnabled: fileCfg.GiteaSyncEnabled,
+ StorageType: fileCfg.StorageType,
+ S3Endpoint: fileCfg.S3Endpoint,
+ S3Region: fileCfg.S3Region,
+ S3Bucket: fileCfg.S3Bucket,
+ S3AccessKey: fileCfg.S3AccessKey,
+ S3SecretKey: fileCfg.S3SecretKey,
+ S3PublicBaseURL: fileCfg.S3PublicBaseURL,
+ S3Prefix: fileCfg.S3Prefix,
+ S3ForcePathStyle: fileCfg.S3ForcePathStyle,
}); err != nil {
return nil, fmt.Errorf("更新配置文件失败: %w", err)
}
@@ -232,27 +289,43 @@ func (c *Config) SiteUploadDir() string {
return filepath.Join(c.DataDir, "uploads", "site")
}
-// UploadDir 返回头像上传目录(兼容旧调用)
-func (c *Config) UploadDir() string {
- return c.AvatarUploadDir()
-}
-
// FilterWordsPath 返回敏感词配置文件路径
func (c *Config) FilterWordsPath() string {
return filepath.Join(c.DataDir, "filter_words.txt")
}
-// OIDCEnabled 是否已配置可作为 OIDC Provider
-func (c *Config) OIDCEnabled() bool {
- return c.RootURL != "" && c.OAuthClientID != "" && c.OAuthClientSecret != "" && len(c.OAuthRedirectURIs) > 0
-}
-
func normalizeRootURL(raw string) string {
u := strings.TrimSpace(raw)
u = strings.TrimRight(u, "/")
return u
}
+// normalizeStoragePrefix 规范化对象 key 前缀:去首尾空白与首斜杠,非空时保证尾斜杠
+func normalizeStoragePrefix(raw string) string {
+ p := strings.TrimSpace(raw)
+ p = strings.TrimPrefix(p, "/")
+ if p == "" {
+ return ""
+ }
+ return strings.TrimSuffix(p, "/") + "/"
+}
+
+func (s S3Config) validate() error {
+ if s.Endpoint == "" {
+ return fmt.Errorf("storage.TYPE=s3 时必须配置 ENDPOINT")
+ }
+ if s.Bucket == "" {
+ return fmt.Errorf("storage.TYPE=s3 时必须配置 BUCKET")
+ }
+ if s.AccessKey == "" || s.SecretKey == "" {
+ return fmt.Errorf("storage.TYPE=s3 时必须配置 ACCESS_KEY 与 SECRET_KEY")
+ }
+ if s.PublicBaseURL == "" {
+ return fmt.Errorf("storage.TYPE=s3 时必须配置 PUBLIC_BASE_URL(公开访问根地址)")
+ }
+ return nil
+}
+
func splitCSV(raw string) []string {
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
diff --git a/config/ini.go b/config/ini.go
index 7d2b4dc..c6971ff 100644
--- a/config/ini.go
+++ b/config/ini.go
@@ -28,12 +28,24 @@ type fileSettings struct {
GiteaBaseURL string
GiteaToken string
GiteaSyncEnabled bool
+ StorageType string
+ S3Endpoint string
+ S3Region string
+ S3Bucket string
+ S3AccessKey string
+ S3SecretKey string
+ S3PublicBaseURL string
+ S3Prefix string
+ S3ForcePathStyle bool
}
func defaultFileSettings() fileSettings {
return fileSettings{
- Port: defaultPort,
- DataRel: defaultDataRel,
+ Port: defaultPort,
+ DataRel: defaultDataRel,
+ StorageType: StorageTypeLocal,
+ S3Region: "us-east-1",
+ S3ForcePathStyle: true,
}
}
@@ -82,6 +94,24 @@ func loadAppINI(path string) (fileSettings, error) {
out.GiteaSyncEnabled = sec.Key("SYNC_ENABLED").MustBool(false)
}
+ if sec, err := cfg.GetSection("storage"); err == nil {
+ if v := strings.TrimSpace(sec.Key("TYPE").String()); v != "" {
+ out.StorageType = v
+ }
+ out.S3Endpoint = strings.TrimSpace(sec.Key("ENDPOINT").String())
+ if v := strings.TrimSpace(sec.Key("REGION").String()); v != "" {
+ out.S3Region = v
+ }
+ out.S3Bucket = strings.TrimSpace(sec.Key("BUCKET").String())
+ out.S3AccessKey = strings.TrimSpace(sec.Key("ACCESS_KEY").String())
+ out.S3SecretKey = strings.TrimSpace(sec.Key("SECRET_KEY").String())
+ out.S3PublicBaseURL = strings.TrimSpace(sec.Key("PUBLIC_BASE_URL").String())
+ out.S3Prefix = strings.TrimSpace(sec.Key("PREFIX").String())
+ if sec.HasKey("FORCE_PATH_STYLE") {
+ out.S3ForcePathStyle = sec.Key("FORCE_PATH_STYLE").MustBool(true)
+ }
+ }
+
return out, nil
}
@@ -91,6 +121,15 @@ func writeAppINI(path string, s fileSettings) error {
return err
}
+ storageType := strings.TrimSpace(s.StorageType)
+ if storageType == "" {
+ storageType = StorageTypeLocal
+ }
+ region := strings.TrimSpace(s.S3Region)
+ if region == "" {
+ region = "us-east-1"
+ }
+
var b strings.Builder
b.WriteString("; 姜十三论坛 Jiang13 Forum — 配置文件(风格类似 Gitea app.ini)\n")
b.WriteString("; 修改后重启进程/服务生效。命令行参数优先级高于本文件。\n")
@@ -130,6 +169,52 @@ func writeAppINI(path string, s fileSettings) error {
b.WriteString("; 多个回调用逗号分隔\n")
b.WriteString("REDIRECT_URIS = ")
b.WriteString(s.OAuthRedirectURIs)
+ b.WriteString("\n\n")
+ b.WriteString("[gitea]\n")
+ b.WriteString("; 可选:同步会员公开仓库到侧栏 /projects\n")
+ b.WriteString("BASE_URL = ")
+ b.WriteString(s.GiteaBaseURL)
+ b.WriteString("\n")
+ b.WriteString("TOKEN = ")
+ b.WriteString(s.GiteaToken)
+ b.WriteString("\n")
+ b.WriteString("SYNC_ENABLED = ")
+ b.WriteString(strconv.FormatBool(s.GiteaSyncEnabled))
+ b.WriteString("\n\n")
+ b.WriteString("[storage]\n")
+ b.WriteString("; 可选种子:日常请在管理后台「系统设置 → 对象存储」配置(保存即生效)\n")
+ b.WriteString("TYPE = ")
+ b.WriteString(storageType)
+ b.WriteString("\n")
+ b.WriteString("; 以下仅作首次种子(MinIO / 七牛 / 又拍 / 阿里云 OSS 等)\n")
+ b.WriteString("; ENDPOINT = https://s3.example.com\n")
+ b.WriteString("ENDPOINT = ")
+ b.WriteString(s.S3Endpoint)
+ b.WriteString("\n")
+ b.WriteString("REGION = ")
+ b.WriteString(region)
+ b.WriteString("\n")
+ b.WriteString("BUCKET = ")
+ b.WriteString(s.S3Bucket)
+ b.WriteString("\n")
+ b.WriteString("ACCESS_KEY = ")
+ b.WriteString(s.S3AccessKey)
+ b.WriteString("\n")
+ b.WriteString("SECRET_KEY = ")
+ b.WriteString(s.S3SecretKey)
+ b.WriteString("\n")
+ b.WriteString("; 公开访问根 URL(无尾斜杠);上传后返回 PUBLIC_BASE_URL/avatars/xxx.jpg\n")
+ b.WriteString("; PUBLIC_BASE_URL = https://cdn.example.com/forum\n")
+ b.WriteString("PUBLIC_BASE_URL = ")
+ b.WriteString(s.S3PublicBaseURL)
+ b.WriteString("\n")
+ b.WriteString("; 对象 key 前缀(可选),如 forum/\n")
+ b.WriteString("PREFIX = ")
+ b.WriteString(s.S3Prefix)
+ b.WriteString("\n")
+ b.WriteString("; MinIO 等多为 true;AWS S3 官方多为 false\n")
+ b.WriteString("FORCE_PATH_STYLE = ")
+ b.WriteString(strconv.FormatBool(s.S3ForcePathStyle))
b.WriteString("\n")
return os.WriteFile(path, []byte(b.String()), 0644)
diff --git a/docs/issue-templates.md b/docs/issue-templates.md
index 3e1324a..3d2efc8 100644
--- a/docs/issue-templates.md
+++ b/docs/issue-templates.md
@@ -55,9 +55,9 @@
---
-## Issue #2 · React 前台支持帖子置顶
+## Issue #2 · 示例:管理能力扩展(模板文案)
-**标题:** `[Feature] React 前台增加帖子置顶操作`
+**标题:** `[Feature] 管理后台增加某某能力`
**标签:** `enhancement` `ui/ux` `good first issue`
@@ -65,35 +65,28 @@
### 要解决的问题
-管理员无法在 React SPA 前台对帖子执行置顶/取消置顶,必须跳转到旧版 HTML 管理后台(`/admin/posts`),体验割裂。
+描述管理员在 React SPA 管理后台 / 前台中缺少的操作入口或能力。
### 现状
-
-| 能力 | 状态 |
-| ----------------------------------- | ---- |
-| 数据模型 `pinned` 字段 | ✅ 已有 |
-| 列表按置顶排序 | ✅ 已有 |
-| API `POST /admin/api/posts/:id/pin` | ✅ 已有 |
-| 旧版后台置顶按钮 | ✅ 已有 |
-| React 列表/详情显示置顶徽章 | ✅ 已有 |
-| **React 前台置顶操作入口** | ❌ 缺失 |
-
+| 能力 | 状态 |
+| --- | --- |
+| 数据模型与业务逻辑 | ✅ / ❌ |
+| JSON API(如 `POST /api/admin/...`) | ✅ / ❌ |
+| React 管理后台入口 | ✅ / ❌ |
+| React 前台操作入口(如适用) | ✅ / ❌ |
### 期望方案
-在 React SPA 中为管理员提供置顶操作,例如:
-
-1. **帖子详情页**:标题旁增加「置顶 / 取消置顶」按钮(仅 `role === 'admin'` 可见)
-2. **帖列表项**:管理员 hover 时显示置顶快捷操作(可选)
-3. 调用已有 API,成功后刷新列表/详情,无需跳转旧后台
+1. 在对应页面为管理员增加操作入口(仅 `role === 'admin'` 可见)
+2. 调用已有或新增的 `/api/admin/*` JSON API
+3. 成功后刷新列表/详情,无需离开当前页面
### 相关代码
-- 后端:`service/post.go` → `SetPinned`,`handler/admin.go` → `AdminAPIPinPost`
-- 前端:`frontend/src/pages/PostDetailPage.tsx`、`frontend/src/components/PostListItem.tsx`
-- 参考旧版:`embed_static/templates/admin/posts.html` 中的 `togglePin`
+- 后端:`service/`、`handler/api.go`、`router/router.go`
+- 前端:`frontend/src/pages/admin/`、`frontend/src/api/client.ts`
### 备注
-适合作为 `good first issue`,改动范围小、API 已就绪。
\ No newline at end of file
+适合作为 `good first issue` 时,优先选择 API 已就绪、只需补 UI 的小改动。
diff --git a/embed_static/embed.go b/embed_static/embed.go
index fa77bd1..5a0501c 100644
--- a/embed_static/embed.go
+++ b/embed_static/embed.go
@@ -2,8 +2,6 @@ package embed_static
import (
"embed"
- "html"
- "html/template"
"io/fs"
"net/http"
"regexp"
@@ -15,12 +13,10 @@ import (
//go:embed static/*
var staticFS embed.FS
-//go:embed templates/*
-var templatesFS embed.FS
-
var (
spaTitleRe = regexp.MustCompile(`(?s)
.*?`)
spaBrandTitleFn func() string
+ spaBrandJSONFn func() []byte // 站点品牌 JSON,注入 window.__J13_BRANDING__
)
// SetSPADocumentTitle 注册站点标题提供者,ServeSPA 会注入到入口 HTML,避免刷新闪烁
@@ -28,50 +24,45 @@ func SetSPADocumentTitle(fn func() string) {
spaBrandTitleFn = fn
}
-// SetupEmbed 配置内嵌资源:SPA 前端 + 后台 HTML 模板
-func SetupEmbed(r *gin.Engine) error {
- tmpl, err := LoadTemplates()
- if err != nil {
- return err
- }
- r.SetHTMLTemplate(tmpl)
+// SetSPABrandingJSON 注册品牌 JSON 提供者(须为合法 JSON 对象),供前端首屏同步读入
+func SetSPABrandingJSON(fn func() []byte) {
+ spaBrandJSONFn = fn
+}
- // React SPA 构建产物(Vite)
+// SetupEmbed 配置内嵌资源:React SPA 静态资源
+func SetupEmbed(r *gin.Engine) error {
if sub, err := fs.Sub(staticFS, "static/spa/assets"); err == nil {
r.GET("/assets/*filepath", gin.WrapH(http.StripPrefix("/assets", http.FileServer(http.FS(sub)))))
}
-
- // 后台管理遗留静态资源
- if sub, err := fs.Sub(staticFS, "static/legacy"); err == nil {
- r.GET("/legacy/*filepath", gin.WrapH(http.StripPrefix("/legacy", http.FileServer(http.FS(sub)))))
- }
-
return nil
}
-// ServeSPA 返回 React SPA 入口
+// ServeSPA 返回 React SPA 入口(仅注入站点默认标题)
func ServeSPA(c *gin.Context) {
- data, err := staticFS.ReadFile("static/spa/index.html")
- if err != nil {
- c.String(http.StatusNotFound, "前端未构建,请运行: cd frontend && npm run build")
- return
- }
+ ServeSPAWithMeta(c, nil)
+}
+
+// ServeSPANoIndex 返回带 noindex 的 SPA(登录/后台等私密页)
+func ServeSPANoIndex(c *gin.Context) {
+ title := ""
if spaBrandTitleFn != nil {
- if title := strings.TrimSpace(spaBrandTitleFn()); title != "" {
- escaped := html.EscapeString(title)
- data = spaTitleRe.ReplaceAll(data, []byte(""+escaped+""))
- }
+ title = strings.TrimSpace(spaBrandTitleFn())
}
- c.Data(http.StatusOK, "text/html; charset=utf-8", data)
+ ServeSPAWithMeta(c, &SPAPageMeta{
+ Title: title,
+ Robots: "noindex,nofollow",
+ })
}
// IsSPARoute 判断是否应由 SPA 处理
func IsSPARoute(path string) bool {
+ if path == "/robots.txt" || path == "/sitemap.xml" {
+ return false
+ }
if strings.HasPrefix(path, "/api") ||
strings.HasPrefix(path, "/admin") ||
strings.HasPrefix(path, "/uploads") ||
strings.HasPrefix(path, "/media") ||
- strings.HasPrefix(path, "/legacy") ||
strings.HasPrefix(path, "/assets") ||
strings.HasPrefix(path, "/oauth") ||
strings.HasPrefix(path, "/.well-known") {
@@ -79,16 +70,3 @@ func IsSPARoute(path string) bool {
}
return true
}
-
-func LoadTemplates() (*template.Template, error) {
- sub, err := fs.Sub(templatesFS, "templates")
- if err != nil {
- return nil, err
- }
- tmpl := template.New("").Funcs(template.FuncMap{
- "safeHTML": func(s string) template.HTML { return template.HTML(s) },
- "add": func(a, b int) int { return a + b },
- "sub": func(a, b int) int { return a - b },
- })
- return tmpl.ParseFS(sub, "*.html", "admin/*.html")
-}
diff --git a/embed_static/spa_meta.go b/embed_static/spa_meta.go
new file mode 100644
index 0000000..c534ac9
--- /dev/null
+++ b/embed_static/spa_meta.go
@@ -0,0 +1,154 @@
+package embed_static
+
+import (
+ "bytes"
+ "encoding/json"
+ "html"
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SPAPageMeta 注入到 SPA 入口 HTML 的 SEO / 社交预览元数据(仅 ,不写 #root,避免刷新闪屏)
+type SPAPageMeta struct {
+ Title string // 完整
+ Description string
+ Keywords string // meta keywords
+ Canonical string
+ OGType string // 默认 website
+ OGImage string
+ SiteName string // og:site_name
+ Locale string // og:locale,默认 zh_CN
+ Robots string // 如 noindex,nofollow
+ JSONLD string // 已序列化的 JSON-LD 对象(不含 script 标签)
+ Status int // HTTP 状态码,0 视为 200
+}
+
+// ServeSPAWithMeta 返回带页面级 meta / JSON-LD 的干净 SPA 入口
+func ServeSPAWithMeta(c *gin.Context, meta *SPAPageMeta) {
+ status := http.StatusOK
+ if meta != nil && meta.Status != 0 {
+ status = meta.Status
+ }
+ data, err := staticFS.ReadFile("static/spa/index.html")
+ if err != nil {
+ c.String(http.StatusNotFound, "前端未构建,请运行: cd frontend && npm run build")
+ return
+ }
+ data = applySPAPageMeta(data, meta)
+ c.Data(status, "text/html; charset=utf-8", data)
+}
+
+func applySPAPageMeta(data []byte, meta *SPAPageMeta) []byte {
+ if meta == nil {
+ meta = &SPAPageMeta{}
+ }
+
+ title := strings.TrimSpace(meta.Title)
+ if title == "" && spaBrandTitleFn != nil {
+ title = strings.TrimSpace(spaBrandTitleFn())
+ }
+ if title != "" {
+ escaped := html.EscapeString(title)
+ data = spaTitleRe.ReplaceAll(data, []byte(""+escaped+""))
+ }
+
+ var head strings.Builder
+ writeMeta(&head, "description", meta.Description)
+ writeMeta(&head, "keywords", meta.Keywords)
+ if canonical := strings.TrimSpace(meta.Canonical); canonical != "" {
+ head.WriteString(``)
+ }
+ robots := strings.TrimSpace(meta.Robots)
+ if robots != "" {
+ writeMeta(&head, "robots", robots)
+ }
+
+ ogType := strings.TrimSpace(meta.OGType)
+ if ogType == "" {
+ ogType = "website"
+ }
+ locale := strings.TrimSpace(meta.Locale)
+ if locale == "" {
+ locale = "zh_CN"
+ }
+ writeProp(&head, "og:type", ogType)
+ writeProp(&head, "og:site_name", meta.SiteName)
+ writeProp(&head, "og:locale", locale)
+ writeProp(&head, "og:title", firstNonEmpty(meta.Title, title))
+ writeProp(&head, "og:description", meta.Description)
+ writeProp(&head, "og:url", meta.Canonical)
+ writeProp(&head, "og:image", meta.OGImage)
+ writeMetaName(&head, "twitter:card", twitterCard(meta.OGImage))
+ writeMetaName(&head, "twitter:title", firstNonEmpty(meta.Title, title))
+ writeMetaName(&head, "twitter:description", meta.Description)
+ writeMetaName(&head, "twitter:image", meta.OGImage)
+
+ if jsonld := strings.TrimSpace(meta.JSONLD); jsonld != "" {
+ head.WriteString(``)
+ }
+
+ // 同步注入品牌配置,避免 React 首屏用默认名闪一下
+ if boot := spaBrandingBootScript(); boot != "" {
+ head.WriteString(boot)
+ }
+
+ if head.Len() > 0 {
+ data = bytes.Replace(data, []byte(""), []byte(head.String()+""), 1)
+ }
+
+ return data
+}
+
+// spaBrandingBootScript 生成 window.__J13_BRANDING__=...; 内联脚本
+func spaBrandingBootScript() string {
+ if spaBrandJSONFn == nil {
+ return ""
+ }
+ raw := bytes.TrimSpace(spaBrandJSONFn())
+ if len(raw) == 0 || !json.Valid(raw) {
+ return ""
+ }
+ // 防止 JSON 字符串中的 提前闭合标签
+ safe := bytes.ReplaceAll(raw, []byte("<"), []byte(`\u003c`))
+ return ""
+}
+
+func writeMeta(b *strings.Builder, name, content string) {
+ content = strings.TrimSpace(content)
+ if content == "" {
+ return
+ }
+ b.WriteString(``)
+}
+
+func writeMetaName(b *strings.Builder, name, content string) {
+ writeMeta(b, name, content)
+}
+
+func writeProp(b *strings.Builder, prop, content string) {
+ content = strings.TrimSpace(content)
+ if content == "" {
+ return
+ }
+ b.WriteString(``)
+}
+
+func twitterCard(ogImage string) string {
+ if strings.TrimSpace(ogImage) != "" {
+ return "summary_large_image"
+ }
+ return "summary"
+}
+
+func firstNonEmpty(vals ...string) string {
+ for _, v := range vals {
+ if s := strings.TrimSpace(v); s != "" {
+ return s
+ }
+ }
+ return ""
+}
diff --git a/embed_static/static/css/style.css b/embed_static/static/css/style.css
deleted file mode 100644
index 7d0c069..0000000
--- a/embed_static/static/css/style.css
+++ /dev/null
@@ -1,80 +0,0 @@
-/* 姜十三论坛全局样式 */
-:root {
- --primary: #2c5530;
- --primary-light: #3d7a44;
- --accent: #c9a227;
-}
-
-body {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- background-color: #f8f9fa;
-}
-
-.navbar-brand {
- font-weight: 700;
- letter-spacing: 0.05em;
-}
-
-.site-footer {
- margin-top: auto;
- background: #212529;
- color: #adb5bd;
- padding: 1.5rem 0;
- font-size: 0.875rem;
-}
-
-.post-content {
- line-height: 1.8;
- word-break: break-word;
-}
-
-.post-content img {
- max-width: 100%;
- height: auto;
-}
-
-.comment-floor {
- border-left: 3px solid var(--primary);
- padding-left: 1rem;
-}
-
-.avatar-sm {
- width: 36px;
- height: 36px;
- object-fit: cover;
- border-radius: 50%;
-}
-
-.avatar-md {
- width: 64px;
- height: 64px;
- object-fit: cover;
- border-radius: 50%;
-}
-
-.badge-pin {
- background: var(--accent);
-}
-
-.admin-sidebar .nav-link.active {
- background: var(--primary);
- color: #fff;
-}
-
-.admin-sidebar .nav-link {
- color: #333;
- border-radius: 0.375rem;
- margin-bottom: 0.25rem;
-}
-
-.login-slogan {
- font-style: italic;
- color: #6c757d;
- letter-spacing: 0.1em;
-}
-
-@media (max-width: 576px) {
- .navbar-brand span.en { display: none; }
-}
diff --git a/embed_static/static/js/app.js b/embed_static/static/js/app.js
deleted file mode 100644
index f811c12..0000000
--- a/embed_static/static/js/app.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// 姜十三论坛前端通用脚本
-function apiPost(url, data, isForm) {
- const opts = { method: 'POST', credentials: 'same-origin' };
- if (isForm) {
- opts.body = new FormData(data instanceof HTMLFormElement ? data : undefined);
- if (!(data instanceof HTMLFormElement)) {
- const fd = new FormData();
- for (const k in data) fd.append(k, data[k]);
- opts.body = fd;
- }
- } else {
- opts.headers = { 'Content-Type': 'application/json' };
- opts.body = JSON.stringify(data);
- }
- return fetch(url, opts).then(r => r.json());
-}
-
-function showToast(msg, type) {
- const el = document.getElementById('toast');
- if (!el) { alert(msg); return; }
- el.className = 'toast align-items-center text-bg-' + (type || 'success') + ' border-0 show';
- el.querySelector('.toast-body').textContent = msg;
- new bootstrap.Toast(el).show();
-}
-
-function confirmAction(msg, callback) {
- if (confirm(msg)) callback();
-}
diff --git a/embed_static/static/legacy/css/style.css b/embed_static/static/legacy/css/style.css
deleted file mode 100644
index 95826b3..0000000
--- a/embed_static/static/legacy/css/style.css
+++ /dev/null
@@ -1,499 +0,0 @@
-/* 姜十三论坛 - 前台 + 管理后台样式 */
-:root {
- --primary: #1a7f4b;
- --primary-dark: #156b3f;
- --primary-light: #e8f5ee;
- --accent: #c9a227;
- --admin-sidebar-w: 220px;
-}
-
-body {
- min-height: 100vh;
- background-color: #f4f6f8;
- color: #1d2129;
-}
-
-/* ===== 管理后台 ===== */
-.admin-body {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
- background: #f2f3f5;
-}
-
-.admin-topbar {
- height: 56px;
- background: var(--primary);
- color: #fff;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 24px;
- position: sticky;
- top: 0;
- z-index: 100;
- box-shadow: 0 2px 8px rgba(0,0,0,.1);
-}
-
-.admin-topbar-brand {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.admin-topbar-mark {
- width: 34px;
- height: 34px;
- border-radius: 8px;
- background: rgba(255,255,255,.2);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- font-size: 15px;
- flex-shrink: 0;
-}
-
-.admin-topbar-title {
- display: block;
- font-weight: 600;
- font-size: 15px;
- line-height: 1.3;
-}
-
-.admin-topbar-sub {
- display: block;
- font-size: 12px;
- opacity: .75;
- font-weight: 400;
-}
-
-.admin-topbar-actions {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.admin-topbar-user {
- font-size: 13px;
- opacity: .9;
- padding-right: 4px;
- border-right: 1px solid rgba(255,255,255,.25);
- margin-right: 4px;
- padding-left: 0;
-}
-
-.admin-shell {
- display: flex;
- flex: 1;
- min-height: 0;
-}
-
-.admin-sidebar {
- width: var(--admin-sidebar-w);
- flex-shrink: 0;
- background: #fff;
- border-right: 1px solid #e5e6eb;
- padding: 20px 14px;
- overflow-y: auto;
-}
-
-.admin-sidebar-section {
- font-size: 11px;
- font-weight: 600;
- color: #86909c;
- padding: 12px 12px 6px;
- letter-spacing: .06em;
-}
-
-.admin-sidebar-section:first-child {
- padding-top: 4px;
-}
-
-.admin-sidebar .nav-link {
- display: block;
- color: #4e5969;
- border-radius: 8px;
- padding: 10px 12px;
- margin-bottom: 2px;
- font-size: 14px;
- text-decoration: none;
- transition: background .15s, color .15s;
- border-left: 3px solid transparent;
-}
-
-.admin-sidebar .nav-link:hover {
- background: #f7f8fa;
- color: #1d2129;
-}
-
-.admin-sidebar .nav-link.active {
- background: var(--primary-light);
- color: var(--primary);
- font-weight: 600;
- border-left-color: var(--primary);
-}
-
-.admin-main {
- flex: 1;
- min-width: 0;
- padding: 28px 32px;
- overflow-y: auto;
- max-width: 1280px;
-}
-
-.admin-page-head {
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- gap: 16px;
- margin-bottom: 24px;
- flex-wrap: wrap;
-}
-
-.admin-page-head h1 {
- font-size: 24px;
- font-weight: 600;
- margin: 0 0 6px;
- color: #1d2129;
-}
-
-.admin-page-head p {
- margin: 0;
- color: #86909c;
- font-size: 14px;
-}
-
-.admin-search-bar {
- display: flex;
- gap: 8px;
- align-items: center;
-}
-
-.admin-search-bar .form-control {
- width: 240px;
- border-color: #e5e6eb;
- font-size: 13px;
-}
-
-.admin-stat-grid {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 16px;
- margin-bottom: 24px;
-}
-
-.admin-stat-card {
- background: #fff;
- border: 1px solid #e5e6eb;
- border-radius: 12px;
- padding: 18px 20px;
- display: flex;
- align-items: center;
- gap: 14px;
- transition: box-shadow .2s;
-}
-
-.admin-stat-card:hover {
- box-shadow: 0 4px 12px rgba(0,0,0,.06);
-}
-
-.admin-stat-icon {
- width: 44px;
- height: 44px;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 15px;
- font-weight: 700;
- flex-shrink: 0;
-}
-
-.admin-stat-users .admin-stat-icon { background: #e8f3ff; color: #3491fa; }
-.admin-stat-posts .admin-stat-icon { background: #e8f5ee; color: var(--primary); }
-.admin-stat-boards .admin-stat-icon { background: #fff7e8; color: #ff7d00; }
-.admin-stat-comments .admin-stat-icon { background: #f5e8ff; color: #722ed1; }
-.admin-stat-online .admin-stat-icon { background: #e8ffea; color: #00b42a; }
-
-.admin-stat-users .value { color: #3491fa; }
-.admin-stat-posts .value { color: var(--primary); }
-.admin-stat-boards .value { color: #ff7d00; }
-.admin-stat-comments .value { color: #722ed1; }
-.admin-stat-online .value { color: #00b42a; }
-
-.admin-stat-card .value {
- font-size: 26px;
- font-weight: 700;
- line-height: 1.2;
-}
-
-.admin-stat-card .label {
- font-size: 13px;
- color: #86909c;
- margin-top: 2px;
-}
-
-.admin-card {
- background: #fff;
- border: 1px solid #e5e6eb;
- border-radius: 12px;
- margin-bottom: 20px;
- overflow: hidden;
- box-shadow: 0 1px 2px rgba(0,0,0,.04);
-}
-
-.admin-card-head {
- padding: 16px 20px;
- border-bottom: 1px solid #f2f3f5;
- font-weight: 600;
- font-size: 15px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- color: #1d2129;
-}
-
-.admin-card-link {
- font-size: 13px;
- font-weight: 500;
- color: var(--primary);
- text-decoration: none;
- transition: opacity .15s;
-}
-
-.admin-card-link:hover {
- opacity: .75;
- color: var(--primary-dark);
-}
-
-.admin-card-body {
- padding: 20px;
-}
-
-.admin-table {
- margin: 0;
- font-size: 13px;
- --bs-table-hover-bg: #f7f8fa;
-}
-
-.admin-table th {
- background: #fafbfc;
- font-weight: 600;
- white-space: nowrap;
- color: #4e5969;
- font-size: 12px;
- padding: 12px 16px;
- border-bottom: 1px solid #e5e6eb;
-}
-
-.admin-table td {
- vertical-align: middle;
- padding: 12px 16px;
- border-bottom: 1px solid #f2f3f5;
- color: #1d2129;
-}
-
-.admin-table tbody tr:last-child td {
- border-bottom: none;
-}
-
-.admin-table tbody tr:hover td {
- background: #fafbfc;
-}
-
-.admin-table .btn-link {
- color: var(--primary);
- text-decoration: none;
- font-weight: 500;
-}
-
-.admin-table .btn-link:hover {
- text-decoration: underline;
-}
-
-.text-truncate-cell {
- max-width: 360px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.badge-pin {
- background: #ff7d00;
- color: #fff;
- font-weight: 500;
- font-size: 11px;
-}
-
-.badge-admin {
- background: var(--primary-light);
- color: var(--primary);
- font-weight: 500;
-}
-
-.badge-banned {
- background: #ffece8;
- color: #f53f3f;
- font-weight: 500;
-}
-
-.admin-empty {
- text-align: center;
- padding: 48px 20px;
- color: #86909c;
- font-size: 14px;
-}
-
-.admin-pagination {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 10px;
- padding: 16px 20px;
- border-top: 1px solid #f2f3f5;
-}
-
-.admin-pagination-info {
- font-size: 13px;
- color: #86909c;
- padding: 0 8px;
-}
-
-.admin-body .btn-success {
- background: var(--primary);
- border-color: var(--primary);
-}
-
-.admin-body .btn-success:hover {
- background: var(--primary-dark);
- border-color: var(--primary-dark);
-}
-
-.admin-body .btn-outline-success {
- color: var(--primary);
- border-color: var(--primary);
-}
-
-.admin-body .btn-outline-success:hover {
- background: var(--primary);
- border-color: var(--primary);
-}
-
-.admin-body .form-control:focus {
- border-color: var(--primary);
- box-shadow: 0 0 0 3px rgba(26,127,75,.12);
-}
-
-.admin-body .form-label {
- color: #4e5969;
- margin-bottom: 4px;
-}
-
-.admin-login-page {
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(160deg, #f2f3f5 0%, #e8f5ee 60%, #d4edda 100%);
- padding: 24px;
-}
-
-.admin-login-card {
- width: 100%;
- max-width: 400px;
- background: #fff;
- border: 1px solid #e5e6eb;
- border-radius: 16px;
- padding: 36px 32px;
- box-shadow: 0 12px 40px rgba(0,0,0,.08);
-}
-
-.admin-login-mark {
- width: 52px;
- height: 52px;
- border-radius: 14px;
- background: var(--primary);
- color: #fff;
- font-size: 24px;
- font-weight: 700;
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 0 auto 18px;
- box-shadow: 0 4px 12px rgba(26,127,75,.25);
-}
-
-.login-slogan {
- color: #86909c;
- font-size: 13px;
-}
-
-.info-row {
- display: flex;
- gap: 12px;
- padding: 12px 0;
- border-bottom: 1px solid #f2f3f5;
- font-size: 13px;
-}
-
-.info-row:last-child { border-bottom: none; }
-.info-row .info-label { width: 100px; color: #86909c; flex-shrink: 0; }
-.info-row .info-value { flex: 1; word-break: break-all; color: #1d2129; }
-
-#adminToast.toast {
- background: #1d2129;
- color: #fff;
- border: none;
- border-radius: 10px;
- box-shadow: 0 8px 24px rgba(0,0,0,.15);
-}
-
-@media (max-width: 1100px) {
- .admin-stat-grid {
- grid-template-columns: repeat(3, 1fr);
- }
-}
-
-@media (max-width: 768px) {
- .admin-shell { flex-direction: column; }
- .admin-sidebar {
- width: 100%;
- border-right: none;
- border-bottom: 1px solid #e5e6eb;
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- padding: 10px 12px;
- }
- .admin-sidebar-section { display: none; }
- .admin-sidebar .nav-link {
- padding: 7px 12px;
- font-size: 13px;
- border-left: none;
- }
- .admin-sidebar .nav-link.active {
- border-left: none;
- }
- .admin-main { padding: 16px; max-width: none; }
- .admin-stat-grid {
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- }
- .admin-stat-card { padding: 14px 16px; }
- .admin-stat-card .value { font-size: 22px; }
- .admin-topbar-user { display: none; }
- .admin-search-bar .form-control { width: 160px; }
-}
-
-@media (max-width: 480px) {
- .admin-stat-grid {
- grid-template-columns: 1fr;
- }
-}
-
-/* ===== 旧版前台兼容 ===== */
-.navbar-brand { font-weight: 700; letter-spacing: 0.05em; }
-.post-content { line-height: 1.8; word-break: break-word; }
-.post-content img { max-width: 100%; height: auto; }
-.avatar-sm { width: 36px; height: 36px; object-fit: cover; border-radius: 50%; }
diff --git a/embed_static/static/legacy/js/app.js b/embed_static/static/legacy/js/app.js
deleted file mode 100644
index 843c60c..0000000
--- a/embed_static/static/legacy/js/app.js
+++ /dev/null
@@ -1,79 +0,0 @@
-// 姜十三论坛 - 管理后台通用脚本
-
-async function adminFetch(url, opts) {
- opts = opts || {};
- var res = await fetch(url, Object.assign({ credentials: 'same-origin' }, opts));
- var data;
- try {
- data = await res.json();
- } catch (e) {
- throw new Error('服务器响应异常,请重新登录后再试');
- }
- if (!res.ok) {
- throw new Error(data.error || '请求失败');
- }
- return data;
-}
-
-function showToast(msg, type) {
- var el = document.getElementById('adminToast');
- if (!el) {
- alert(msg);
- return;
- }
- el.className = 'toast align-items-center text-bg-' + (type || 'success') + ' border-0 show';
- el.querySelector('.toast-body').textContent = msg;
- bootstrap.Toast.getOrCreateInstance(el, { delay: 2800 }).show();
-}
-
-function adminConfirm(msg, fn) {
- if (confirm(msg)) fn();
-}
-
-function setBtnLoading(btn, loading) {
- if (!btn) return;
- btn.disabled = loading;
- if (loading) {
- btn.dataset.originalHtml = btn.innerHTML;
- btn.innerHTML = '';
- } else if (btn.dataset.originalHtml) {
- btn.innerHTML = btn.dataset.originalHtml;
- }
-}
-
-function adminLogout() {
- adminFetch('/admin/api/logout', { method: 'POST' })
- .then(function () { location.href = '/admin/login'; })
- .catch(function (e) { showToast(e.message, 'danger'); });
-}
-
-function postForm(url, method, fields) {
- var fd = new FormData();
- Object.keys(fields).forEach(function (k) { fd.append(k, fields[k]); });
- return adminFetch(url, { method: method, body: fd });
-}
-
-function reloadSoon() {
- setTimeout(function () { location.reload(); }, 600);
-}
-
-// 兼容旧模板
-function apiPost(url, data, isForm) {
- var opts = { method: 'POST', credentials: 'same-origin' };
- if (isForm) {
- opts.body = new FormData(data instanceof HTMLFormElement ? data : undefined);
- if (!(data instanceof HTMLFormElement)) {
- var fd = new FormData();
- for (var k in data) fd.append(k, data[k]);
- opts.body = fd;
- }
- } else {
- opts.headers = { 'Content-Type': 'application/json' };
- opts.body = JSON.stringify(data);
- }
- return fetch(url, opts).then(function (r) { return r.json(); });
-}
-
-function confirmAction(msg, callback) {
- adminConfirm(msg, callback);
-}
diff --git a/embed_static/templates/admin/boards.html b/embed_static/templates/admin/boards.html
deleted file mode 100644
index ab704a4..0000000
--- a/embed_static/templates/admin/boards.html
+++ /dev/null
@@ -1,95 +0,0 @@
-{{define "admin/boards.html"}}{{template "admin/layout" .}}{{end}}
-{{define "admin_content_boards"}}
-
-
-
板块管理
-
创建和维护论坛板块,有帖子的板块无法删除
-
-
-
-
-
-
-
板块列表 共 {{len .Boards}} 个
-
-
-{{end}}
-
-{{define "admin_scripts_boards"}}
-
-{{end}}
diff --git a/embed_static/templates/admin/comments.html b/embed_static/templates/admin/comments.html
deleted file mode 100644
index f46f699..0000000
--- a/embed_static/templates/admin/comments.html
+++ /dev/null
@@ -1,58 +0,0 @@
-{{define "admin/comments.html"}}{{template "admin/layout" .}}{{end}}
-{{define "admin_content_comments"}}
-
-
-
评论管理
-
查看和删除评论,共 {{.Total}} 条
-
-
-
-
-
-
-
- | ID | 楼层 | 帖子 | 作者 | 回复 | 内容 | 时间 | 操作 |
-
-
- {{range .Comments}}
-
- | {{.ID}} |
- #{{.Floor}} |
-
- {{if .Post}}
- {{.Post.Title}}
- {{else}}帖子 #{{.PostID}}{{end}}
- |
- {{if .UserID}}{{if .User}}{{.User.Nickname}}{{else}}-{{end}}{{else}}{{.GuestNick}}{{end}} |
-
- {{if .ReplyTarget}}
- @{{if .ReplyTarget.UserID}}{{if .ReplyTarget.User}}{{.ReplyTarget.User.Nickname}}{{else}}-{{end}}{{else}}{{.ReplyTarget.GuestNick}}{{end}}
- {{else}}-{{end}}
- |
- {{.Content}} |
- {{.CreatedAt.Format "01-02 15:04"}} |
-
-
- |
-
- {{else}}
- | 暂无评论 |
- {{end}}
-
-
-
- {{template "admin_pagination" .}}
-
-{{end}}
-
-{{define "admin_scripts_comments"}}
-
-{{end}}
diff --git a/embed_static/templates/admin/dashboard.html b/embed_static/templates/admin/dashboard.html
deleted file mode 100644
index 5ffc993..0000000
--- a/embed_static/templates/admin/dashboard.html
+++ /dev/null
@@ -1,69 +0,0 @@
-{{define "admin/dashboard.html"}}{{template "admin/layout" .}}{{end}}
-{{define "admin_content_dashboard"}}
-
-
-
仪表盘
-
欢迎回来{{if .CurrentUser}},{{.CurrentUser.Nickname}}{{end}} · 论坛运行概况
-
-
-
-
-
-
用
-
-
{{.UserCount}}
-
注册用户
-
-
-
-
帖
-
-
{{.PostCount}}
-
帖子总数
-
-
-
-
板
-
-
{{.BoardCount}}
-
板块数量
-
-
-
-
-
-
-
-
-
- | 标题 | 板块 | 作者 | 时间 | |
-
- {{range .RecentPosts}}
-
- |
- {{if .Pinned}}置顶{{end}}{{.Title}}
- |
- {{if .Board}}{{.Board.Name}}{{else}}-{{end}} |
- {{if .User}}{{.User.Nickname}}{{else}}-{{end}} |
- {{.CreatedAt.Format "01-02 15:04"}} |
- 查看 |
-
- {{else}}
- | 暂无帖子 |
- {{end}}
-
-
-
-
-{{end}}
-
-{{define "admin_scripts_dashboard"}}{{end}}
diff --git a/embed_static/templates/admin/layout.html b/embed_static/templates/admin/layout.html
deleted file mode 100644
index 8502194..0000000
--- a/embed_static/templates/admin/layout.html
+++ /dev/null
@@ -1,88 +0,0 @@
-{{define "admin/layout"}}
-
-
-
-
-
- {{.Title}} - 姜十三论坛管理后台
-
-
-
-
-
-
-
-
-
- {{if .CurrentUser}}
-
{{.CurrentUser.Nickname}}
- {{end}}
-
返回前台
-
-
-
-
-
-
-
-
- {{if eq .ActiveNav "dashboard"}}{{template "admin_content_dashboard" .}}
- {{else if eq .ActiveNav "boards"}}{{template "admin_content_boards" .}}
- {{else if eq .ActiveNav "posts"}}{{template "admin_content_posts" .}}
- {{else if eq .ActiveNav "comments"}}{{template "admin_content_comments" .}}
- {{else if eq .ActiveNav "users"}}{{template "admin_content_users" .}}
- {{else if eq .ActiveNav "settings"}}{{template "admin_content_settings" .}}
- {{end}}
-
-
-
-
-
-{{if eq .ActiveNav "boards"}}{{template "admin_scripts_boards" .}}
-{{else if eq .ActiveNav "posts"}}{{template "admin_scripts_posts" .}}
-{{else if eq .ActiveNav "comments"}}{{template "admin_scripts_comments" .}}
-{{else if eq .ActiveNav "users"}}{{template "admin_scripts_users" .}}
-{{else if eq .ActiveNav "settings"}}{{template "admin_scripts_settings" .}}
-{{end}}
-
-
-{{end}}
-
-{{define "admin_pagination"}}
-{{if gt .TotalPages 1}}
-
-{{end}}
-{{end}}
diff --git a/embed_static/templates/admin/login.html b/embed_static/templates/admin/login.html
deleted file mode 100644
index d1e2ba2..0000000
--- a/embed_static/templates/admin/login.html
+++ /dev/null
@@ -1,49 +0,0 @@
-{{define "admin/login.html"}}
-
-
-
-
-
- 后台登录 - 姜十三论坛
-
-
-
-
-
-
姜
-
管理后台登录
-
姜十三论坛 · 仅管理员可访问
- {{if eq .QueryBanned "1"}}
-
账号已被禁言,无法登录后台
- {{end}}
-
-
- ← 返回论坛前台
-
-
-
-
-
-
-
-{{end}}
diff --git a/embed_static/templates/admin/posts.html b/embed_static/templates/admin/posts.html
deleted file mode 100644
index 0a40fd8..0000000
--- a/embed_static/templates/admin/posts.html
+++ /dev/null
@@ -1,67 +0,0 @@
-{{define "admin/posts.html"}}{{template "admin/layout" .}}{{end}}
-{{define "admin_content_posts"}}
-
-
-
帖子管理
-
置顶、删除帖子,共 {{.Total}} 篇
-
-
-
-
-
-
-
-
-
- | ID | 标题 | 板块 | 作者 |
- 置顶 | 点赞 | 浏览 | 时间 | 操作 |
-
-
-
- {{range .Posts}}
-
- | {{.ID}} |
- {{.Title}} |
- {{if .Board}}{{.Board.Name}}{{else}}-{{end}} |
- {{if .User}}{{.User.Nickname}}{{else}}-{{end}} |
- {{if .Pinned}}是{{else}}否{{end}} |
- {{.LikeCount}} |
- {{.ViewCount}} |
- {{.CreatedAt.Format "01-02 15:04"}} |
-
- 查看
-
-
- |
-
- {{else}}
- | 没有找到帖子 |
- {{end}}
-
-
-
- {{template "admin_pagination" .}}
-
-{{end}}
-
-{{define "admin_scripts_posts"}}
-
-{{end}}
diff --git a/embed_static/templates/admin/settings.html b/embed_static/templates/admin/settings.html
deleted file mode 100644
index d237da9..0000000
--- a/embed_static/templates/admin/settings.html
+++ /dev/null
@@ -1,56 +0,0 @@
-{{define "admin/settings.html"}}{{template "admin/layout" .}}{{end}}
-{{define "admin_content_settings"}}
-
-
-
系统设置
-
数据目录、敏感词配置与数据库备份
-
-
-
-
-
-
-
运行信息
-
-
-
-
-
-
敏感词文件每行一个词,# 开头为注释,修改后需重启服务生效。
-
-
-
-
-
-
数据库备份
-
-
将 SQLite 数据库复制到数据目录,生成带时间戳的备份文件。
-
-
-
-
-
-
-{{end}}
-
-{{define "admin_scripts_settings"}}
-
-{{end}}
diff --git a/embed_static/templates/admin/users.html b/embed_static/templates/admin/users.html
deleted file mode 100644
index a614d6c..0000000
--- a/embed_static/templates/admin/users.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{{define "admin/users.html"}}{{template "admin/layout" .}}{{end}}
-{{define "admin_content_users"}}
-
-
-
用户管理
-
禁言违规用户,共 {{.Total}} 位注册用户
-
-
-
-
-
-
-
- | ID | 用户名 | 昵称 | 邮箱 | 角色 | 状态 | 上次登录 | 登录 IP | 注册时间 | 操作 |
-
-
- {{range .Users}}
-
- | {{.ID}} |
- {{.Username}} |
- {{.Nickname}} |
- {{if .Email}}{{.Email}}{{else}}—{{end}} |
-
- {{if eq .Role "admin"}}
- 管理员
- {{else}}普通用户{{end}}
- |
-
- {{if .Banned}}已禁言{{else}}正常{{end}}
- |
- {{if .LastLoginAt}}{{.LastLoginAt.Format "2006-01-02 15:04"}}{{else}}—{{end}} |
- {{if .LastLoginIP}}{{.LastLoginIP}}{{else}}—{{end}} |
- {{.CreatedAt.Format "2006-01-02"}} |
-
- {{if eq .Role "admin"}}
- —
- {{else if .Banned}}
-
- {{else}}
-
- {{end}}
- |
-
- {{else}}
- | 暂无用户 |
- {{end}}
-
-
-
- {{template "admin_pagination" .}}
-
-{{end}}
-
-{{define "admin_scripts_users"}}
-
-{{end}}
diff --git a/embed_static/templates/board.html b/embed_static/templates/board.html
deleted file mode 100644
index 56eb280..0000000
--- a/embed_static/templates/board.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{{define "board.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-{{.Board.Name}}
-{{.Board.Description}}
-{{if .CurrentUser}}在此板块发帖{{end}}
-{{range .Posts}}
-
-
- {{if .Pinned}}
置顶{{end}}
-
{{.Title}}
-
{{.User.Nickname}} · {{.CreatedAt.Format "2006-01-02 15:04"}} · 👍 {{.LikeCount}}
-
-
-{{else}}该板块暂无帖子
{{end}}
-{{end}}
diff --git a/embed_static/templates/error.html b/embed_static/templates/error.html
deleted file mode 100644
index ec060c8..0000000
--- a/embed_static/templates/error.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{{define "error.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-{{end}}
diff --git a/embed_static/templates/favorites.html b/embed_static/templates/favorites.html
deleted file mode 100644
index be6b6a0..0000000
--- a/embed_static/templates/favorites.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{{define "favorites.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-我的收藏
-{{range .Favorites}}
-
-
{{.Post.Title}}
-
{{.Post.Board.Name}} · 收藏于 {{.CreatedAt.Format "2006-01-02"}}
-
-{{else}}暂无收藏
{{end}}
-{{end}}
diff --git a/embed_static/templates/index.html b/embed_static/templates/index.html
deleted file mode 100644
index 3422e64..0000000
--- a/embed_static/templates/index.html
+++ /dev/null
@@ -1,34 +0,0 @@
-{{define "index.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-
-
最新帖子
- {{range .Posts}}
-
-
- {{if .Pinned}}
置顶{{end}}
-
{{.Title}}
-
- {{.Board.Name}} · {{.User.Nickname}} · {{.CreatedAt.Format "2006-01-02 15:04"}} · 👍 {{.LikeCount}} · 👁 {{.ViewCount}}
-
-
-
- {{else}}
-
暂无帖子,快来发帖吧!
- {{end}}
-
-
-
-{{end}}
diff --git a/embed_static/templates/layout.html b/embed_static/templates/layout.html
deleted file mode 100644
index d71ad05..0000000
--- a/embed_static/templates/layout.html
+++ /dev/null
@@ -1,56 +0,0 @@
-{{define "layout"}}
-
-
-
-
-
- {{.Title}} - 姜十三论坛
-
-
-
-
-
-
- {{template "content" .}}
-
-
-
-
-
-
-{{block "scripts" .}}{{end}}
-
-
-{{end}}
diff --git a/embed_static/templates/login.html b/embed_static/templates/login.html
deleted file mode 100644
index 351f35b..0000000
--- a/embed_static/templates/login.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{{define "login.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-
-
-
-
登录
-
拾三一隅,自在交流
-
-
还没有账号?立即注册
-
-
-
-
-{{end}}
-{{define "scripts"}}
-
-{{end}}
diff --git a/embed_static/templates/post.html b/embed_static/templates/post.html
deleted file mode 100644
index b69e08e..0000000
--- a/embed_static/templates/post.html
+++ /dev/null
@@ -1,72 +0,0 @@
-{{define "post.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-
-
-
{{if .Post.Pinned}}置顶{{end}}{{.Post.Title}}
-
-
{{.Post.User.Nickname}} · {{.Post.CreatedAt.Format "2006-01-02 15:04"}} · 👁 {{.Post.ViewCount}}
- {{if .Post.Tags}}· 标签:{{.Post.Tags}}{{end}}
-
-
{{safeHTML .Post.Content}}
-
- {{if .CurrentUser}}
-
-
- {{if or (eq .CurrentUserID .Post.UserID) .IsAdmin}}
-
编辑
-
- {{end}}
- {{if .IsAdmin}}
-
- {{end}}
- {{else}}
登录后互动{{end}}
-
-
-
-评论 ({{len .Comments}})
-{{range .Comments}}
-
-{{end}}
-{{if .CurrentUser}}
-
-{{end}}
-{{end}}
-{{define "scripts"}}
-
-{{end}}
diff --git a/embed_static/templates/post_edit.html b/embed_static/templates/post_edit.html
deleted file mode 100644
index b35ddd1..0000000
--- a/embed_static/templates/post_edit.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{{define "post_edit.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-编辑帖子
-
-{{end}}
-{{define "scripts"}}
-
-{{end}}
diff --git a/embed_static/templates/post_new.html b/embed_static/templates/post_new.html
deleted file mode 100644
index 0fa88e4..0000000
--- a/embed_static/templates/post_new.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{{define "post_new.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-发布新帖
-
-{{end}}
-{{define "scripts"}}
-
-{{end}}
diff --git a/embed_static/templates/profile.html b/embed_static/templates/profile.html
deleted file mode 100644
index bc68f6c..0000000
--- a/embed_static/templates/profile.html
+++ /dev/null
@@ -1,35 +0,0 @@
-{{define "profile.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-
-

-
{{.ProfileUser.Nickname}}
-
@{{.ProfileUser.Username}}
-
{{if .ProfileUser.Email}}{{.ProfileUser.Email}}{{else}}未设置邮箱{{end}}
-
-
-
-
-{{end}}
-{{define "scripts"}}
-
-{{end}}
diff --git a/embed_static/templates/register.html b/embed_static/templates/register.html
deleted file mode 100644
index 5dd0b42..0000000
--- a/embed_static/templates/register.html
+++ /dev/null
@@ -1,71 +0,0 @@
-{{define "register.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-{{end}}
-{{define "scripts"}}
-
-{{end}}
diff --git a/embed_static/templates/user_profile.html b/embed_static/templates/user_profile.html
deleted file mode 100644
index 823e217..0000000
--- a/embed_static/templates/user_profile.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{{define "user_profile.html"}}{{template "layout" .}}{{end}}
-{{define "content"}}
-
-

-
{{.ProfileUser.Nickname}}
-
@{{.ProfileUser.Username}} · 注册于 {{.ProfileUser.CreatedAt.Format "2006-01-02"}}
-
-{{end}}
diff --git a/frontend/index.html b/frontend/index.html
index b2d5884..52841a5 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,6 +3,7 @@
+
姜十三论坛 - 拾三一隅,自在交流
`)
+ b.WriteString("")
+ b.WriteString(bodyInner)
+ b.WriteString(`← 返回首页
`)
+ b.WriteString("