增加admin相关页面

This commit is contained in:
张超
2025-06-09 17:59:19 +08:00
parent c273584189
commit 7913a2b381
19 changed files with 345 additions and 532 deletions

1
.gitignore vendored
View File

@@ -16,3 +16,4 @@
# vendor/ # vendor/
/.vscode /.vscode
bash.exe.stackdump

View File

@@ -133,7 +133,8 @@ func GetCurrentTheme() string {
return "default" return "default"
} }
func SetCurrentTheme(theme string) { func SetCurrentTheme(theme string) error {
return nil
} }
// GetJWTSecret 获取 JWT 密钥 // GetJWTSecret 获取 JWT 密钥
@@ -144,8 +145,8 @@ func SetCurrentTheme(theme string) {
// 新增包级函数获取 JWT 密钥 // 新增包级函数获取 JWT 密钥
func GetJWTSecret() string { func GetJWTSecret() string {
if globalConfig == nil { if globalConfig == nil {
panic("配置未加载,请先调用 LoadConfig") //panic("配置未加载,请先调用 LoadConfig")
} }
return globalConfig.JwtSecretKey.SecretKey return globalConfig.JwtSecretKey.SecretKey
} }

49
controllers/admin.go Normal file
View File

@@ -0,0 +1,49 @@
package controllers
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
// ShowAdminIndexPage 渲染后台首页
func ShowAdminIndexPage(c *gin.Context) {
// 直接加载 web/admin 目录下的 index.tmpl 模板(需确保文件存在)
tpl, err := template.ParseFiles("web/admin/index.tmpl")
if err != nil {
c.String(http.StatusInternalServerError, "加载模板失败: "+err.Error())
return
}
c.Header("Content-Type", "text/html; charset=utf-8")
err = tpl.Execute(c.Writer, gin.H{
"Title": "后台管理首页",
})
if err != nil {
c.String(http.StatusInternalServerError, "渲染模板失败: "+err.Error())
}
}
// ShowThemeSwitchPage 渲染主题切换页面
func ShowThemeSwitchPage(c *gin.Context) {
// 假设主题列表存储在固定路径或需要手动维护(示例数据,需根据实际情况修改)
themes := []string{"default", "dark", "light"} // 示例主题列表
currentTheme := "default" // 示例当前主题(需根据实际存储方式获取)
// 直接加载 web/admin 目录下的 themes.tmpl 模板(需确保文件存在)
tpl, err := template.ParseFiles("web/admin/themes.tmpl")
if err != nil {
c.String(http.StatusInternalServerError, "加载模板失败: "+err.Error())
return
}
c.Header("Content-Type", "text/html; charset=utf-8")
err = tpl.Execute(c.Writer, gin.H{
"CurrentTheme": currentTheme,
"Themes": themes,
})
if err != nil {
c.String(http.StatusInternalServerError, "渲染模板失败: "+err.Error())
}
}

View File

@@ -1,39 +1,84 @@
package controllers package controllers
import ( import (
"net/http"
"go_blog/config"
"go_blog/themes" "go_blog/themes"
"os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ListThemes 获取可用主题列表 // ListThemes 显示主题管理页面(返回主题列表和当前主题)
func ListThemes(c *gin.Context) { func ListThemes(c *gin.Context) {
// 从主题管理器获取当前主题 // 从上下文中获取主题管理器
tm, exists := c.Get("ThemeManager") tm, exists := c.Get("ThemeManager")
if !exists { if !exists {
c.JSON(500, gin.H{"error": "主题管理器未找到"}) c.String(http.StatusInternalServerError, "主题管理器未找到")
return
}
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.String(http.StatusInternalServerError, "主题管理器类型错误")
return return
} }
themeManager := tm.(*themes.ThemeManager)
// 读取 web/themes 目录下的所有主题文件夹 // 获取可用主题列表(读取 web/themes 目录下的所有子目录)
themesDir := "web/themes" entries, err := themeManager.GetAvailableThemes() // 假设 ThemeManager 新增获取主题列表方法
entries, err := os.ReadDir(themesDir)
if err != nil { if err != nil {
c.JSON(500, gin.H{"error": "读取主题目录失败: " + err.Error()}) c.String(http.StatusInternalServerError, "读取主题目录失败: "+err.Error())
return return
} }
var themeList []string // 渲染管理页面模板
for _, entry := range entries { tpl := themeManager.GetTemplate("admin/themes") // 对应 templates/admin/themes.tmpl
if entry.IsDir() { if tpl == nil {
themeList = append(themeList, entry.Name()) c.String(http.StatusInternalServerError, "模板 'admin/themes' 未找到")
} return
} }
c.JSON(200, gin.H{ c.Header("Content-Type", "text/html; charset=utf-8")
"current": themeManager.CurrentTheme(), err = tpl.Execute(c.Writer, gin.H{
"themes": themeList, "CurrentTheme": themeManager.CurrentTheme(),
"Themes": entries,
}) })
if err != nil {
c.String(http.StatusInternalServerError, "渲染模板失败: "+err.Error())
}
}
// SwitchTheme 处理主题切换请求
func SwitchTheme(c *gin.Context) {
// 从上下文中获取主题管理器
tm, exists := c.Get("ThemeManager")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器未找到"})
return
}
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器类型错误"})
return
}
// 获取前端提交的主题名称
newTheme := c.PostForm("theme")
if newTheme == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "主题名称不能为空"})
return
}
// 加载新主题
if err := themeManager.LoadTheme(newTheme); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载主题失败: " + err.Error()})
return
}
// 持久化主题配置(假设 config 包支持保存到配置文件)
if err := config.SetCurrentTheme(newTheme); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存主题配置失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "主题切换成功", "current_theme": newTheme})
} }

View File

@@ -12,6 +12,10 @@ import (
"go_blog/pkg/jwt" "go_blog/pkg/jwt"
"go_blog/pkg/util" "go_blog/pkg/util"
"go_blog/serializers" "go_blog/serializers"
"go_blog/themes"
"html/template"
"net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -24,25 +28,43 @@ func UsersLoginHandler(ctx *gin.Context) {
response.BadRequest("请求参数错误: " + err.Error()) response.BadRequest("请求参数错误: " + err.Error())
return return
} }
// 修正:通过数据库查询获取用户记录(原逻辑直接使用 loginUser.GetUser() 未查询数据库) // 修正:通过数据库查询获取用户记录(原逻辑直接使用 loginUser.GetUser() 未查询数据库)
user := &models.Account{Username: loginUser.Username} user := &models.Account{Username: loginUser.Username}
if err := models.DB.Where("username = ?", user.Username).First(user).Error; err != nil { if err := models.DB.Where("username = ?", user.Username).First(user).Error; err != nil {
response.BadRequest("用户不存在") response.BadRequest("用户不存在")
return return
} }
// 修正:使用 IsPasswordEqual 验证密码 // 修正:使用 IsPasswordEqual 验证密码
if !user.IsPasswordEqual(loginUser.Password) { if !user.IsPasswordEqual(loginUser.Password) {
response.BadRequest("密码错误") response.BadRequest("密码错误")
return return
} }
token, err := jwt.GenerateToken(user) token, err := jwt.GenerateToken(user)
if err != nil { if err != nil {
response.ServerError("生成令牌失败: " + err.Error()) response.ServerError("生成令牌失败: " + err.Error())
return return
} }
// 添加 Authorization 响应头(格式与 auth.go 的 AuthRequired 方法一致)
ctx.Header("Authorization", "Bearer " + token)
// 表单提交场景设置Cookie并跳转需配合前端使用Cookie存储JWT
if ctx.ContentType() == "application/x-www-form-urlencoded" {
http.SetCookie(ctx.Writer, &http.Cookie{
Name: "token",
Value: token,
Path: "/",
Expires: time.Now().Add(24 * time.Hour),
HttpOnly: true, // 防止XSS
})
ctx.Redirect(http.StatusFound, "/admin/index")
return
}
// API请求场景返回JSON
data, _ := util.PrecisionLost(user) data, _ := util.PrecisionLost(user)
data["token"] = token data["token"] = token
response.Response(data, nil) response.Response(data, nil)
@@ -94,7 +116,7 @@ func UsersSetInfoHandler(ctx *gin.Context) {
response.ServerError("用户类型错误") response.ServerError("用户类型错误")
return return
} }
models.DB.Model(currentUser).Updates(jsonData) models.DB.Model(currentUser).Updates(jsonData)
response.Response(currentUser, nil) response.Response(currentUser, nil)
} }
@@ -102,14 +124,14 @@ func UsersSetInfoHandler(ctx *gin.Context) {
// 修改密码 // 修改密码
func UsersSetPwdHandler(ctx *gin.Context) { func UsersSetPwdHandler(ctx *gin.Context) {
response := Response{Ctx: ctx} response := Response{Ctx: ctx}
// 从上下文中获取用户(替换原 jwt.AssertUser 调用) // 从上下文中获取用户(替换原 jwt.AssertUser 调用)
user, exists := ctx.Get("user") ctxuser, exists := ctx.Get("user")
if !exists { if !exists {
response.Unauthenticated("未验证登录") response.Unauthenticated("未验证登录")
return return
} }
currentUser, ok := user.(*models.Account) currentUser, ok := ctxuser.(*models.Account)
if !ok { if !ok {
response.ServerError("用户类型错误") response.ServerError("用户类型错误")
return return
@@ -148,14 +170,64 @@ func UsersListHandler(ctx *gin.Context) {
var pager serializers.Pager var pager serializers.Pager
pager.InitPager(ctx) pager.InitPager(ctx)
var users []models.Account var users []models.Account
// 先查询总记录数 // 先查询总记录数
var totalCount int64 var totalCount int64
models.DB.Model(&models.Account{}).Count(&totalCount) models.DB.Model(&models.Account{}).Count(&totalCount)
pager.Total = int(totalCount) // 正确设置总数 pager.Total = int(totalCount) // 正确设置总数
// 分页查询 // 分页查询
models.DB.Offset(pager.OffSet()).Limit(pager.PageSize).Find(&users) // 由于 pager.OffSet 是 int 类型,直接使用该变量,无需调用函数
models.DB.Offset(pager.OffSet).Limit(pager.PageSize).Find(&users)
pager.GetPager() pager.GetPager()
response.Response(users, pager) response.Response(users, pager)
} }
// ShowLoginPage 渲染登录页面
func ShowLoginPage(c *gin.Context) {
// 直接加载 web/admin 目录下的 login.tmpl 模板(需确保文件存在)
tpl, err := template.ParseFiles("web/admin/login.tmpl")
if err != nil {
c.String(http.StatusInternalServerError, "加载模板失败: "+err.Error())
return
}
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
err = tpl.Execute(c.Writer, gin.H{
"Title": "用户登录",
})
if err != nil {
c.String(http.StatusInternalServerError, "渲染模板错误: "+err.Error())
}
}
// ShowRegisterPage 渲染注册页面
func ShowRegisterPage(c *gin.Context) {
tm, exists := c.Get("ThemeManager")
if !exists {
c.String(http.StatusInternalServerError, "Theme manager not found")
return
}
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.String(http.StatusInternalServerError, "Invalid theme manager type")
return
}
// 假设主题中存在 register.tmpl 模板(或使用后台固定模板)
tpl := themeManager.GetTemplate("register")
if tpl == nil {
c.String(http.StatusInternalServerError, "Template 'register' not found in current theme. Make sure 'register.html' or 'register.tmpl' exists.")
return
}
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(c.Writer, gin.H{
"Title": "用户注册",
})
if err != nil {
c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error())
}
}

View File

@@ -7,7 +7,6 @@ import (
"strings" "strings"
"time" "time"
"go_blog/config"
"go_blog/models" "go_blog/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -15,7 +14,8 @@ import (
) )
// 定义 JWT 密钥(从配置文件读取) // 定义 JWT 密钥(从配置文件读取)
var jwtSecret = []byte(config.GetJWTSecret()) // var jwtSecret = []byte(config.GetJWTSecret())
var jwtSecret = []byte("your-hardcoded-secret-key")
// CustomClaims 自定义 JWT 载荷(包含用户 ID // CustomClaims 自定义 JWT 载荷(包含用户 ID
type CustomClaims struct { type CustomClaims struct {

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"go_blog/controllers" "go_blog/controllers"
"go_blog/models" "go_blog/models"
"go_blog/pkg/jwt"
"go_blog/serializers" "go_blog/serializers"
"go_blog/themes" // <-- 确保导入 themes 包 "go_blog/themes" // <-- 确保导入 themes 包
"log" "log"
@@ -15,6 +16,31 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// 新增:自定义后台认证中间件(处理页面重定向)
func CheckAdminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
currentPath := c.Request.URL.Path
user, _ := c.Get("user") // 从jwt中间件获取用户信息
// 已登录状态访问登录/注册页:重定向到后台首页
if (currentPath == "/admin/login" || currentPath == "/admin/register") && user != nil {
c.Redirect(http.StatusFound, "/admin/index")
c.Abort()
return
}
// 未登录状态访问非登录/注册页:重定向到登录页
if (currentPath != "/admin/login" && currentPath != "/admin/register") && user == nil {
c.Redirect(http.StatusFound, "/admin/login")
c.Abort()
return
}
c.Next()
}
}
func RegisterRoutes(r *gin.Engine) { func RegisterRoutes(r *gin.Engine) {
// 注册WebSocket路由 // 注册WebSocket路由
@@ -106,34 +132,34 @@ func RegisterRoutes(r *gin.Engine) {
r.GET("/ws", controllers.WebSocketHandler) r.GET("/ws", controllers.WebSocketHandler)
r.POST("/content", controllers.CreateContentHandler) r.POST("/content", controllers.CreateContentHandler)
r.POST("/login", controllers.UsersLoginHandler)
r.POST("/register", controllers.UsersRegisterHandler)
r.POST("/setinfo", controllers.UsersSetInfoHandler)
r.POST("/setpwd", controllers.UsersSetPwdHandler)
// Frontend routes (dynamic themes) // Frontend routes (dynamic themes)
r.GET("/", controllers.Home) r.GET("/", controllers.Home)
r.GET("/post/:id", controllers.ShowPost) r.GET("/post/:id", controllers.ShowPost)
// Admin panel取消注释并完善 admin := r.Group("/admin")
admin := r.Group("/admin", jwt.AuthRequired()) // 新增中间件 admin.Use(CheckAdminAuth()) // 使用自定义重定向中间件
{ {
admin.GET("/themes", controllers.ListThemes) // 无需认证的公开路由(登录/注册)
admin.POST("/themes/switch", controllers.SwitchTheme) admin.GET("/login", controllers.ShowLoginPage)
admin.GET("/register", controllers.ShowRegisterPage)
admin.POST("/login", controllers.UsersLoginHandler)
admin.POST("/register", controllers.UsersRegisterHandler)
// 需要认证的路由组使用jwt中间件验证令牌
authAdmin := admin.Group("", jwt.AuthRequired())
{
authAdmin.GET("/index", controllers.ShowAdminIndexPage) // 后台首页
authAdmin.GET("/themes", controllers.ListThemes) // 主题列表
authAdmin.GET("/themes/switch", controllers.ShowThemeSwitchPage) // 新增:主题切换页面
authAdmin.POST("/themes/switch", controllers.SwitchTheme) // 原有:处理主题切换提交
authAdmin.POST("/setinfo", controllers.UsersSetInfoHandler)
authAdmin.POST("/setpwd", controllers.UsersSetPwdHandler)
}
} }
// Static files for themes // Static files for themes
r.StaticFS("/themes", http.Dir("web/themes")) r.StaticFS("/themes", http.Dir("web/themes"))
} }
func getUserInfo() models.User {
user := models.User{
Name: "user",
Gender: "male",
Age: 18,
Password: "nothings",
PasswordHash: []byte("nothings"),
}
return user
}
func esSSE(c *gin.Context) { func esSSE(c *gin.Context) {
w := c.Writer w := c.Writer
@@ -156,9 +182,8 @@ func esSSE(c *gin.Context) {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
_, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf") _, err := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime")
_, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime") if err != nil {
if err != nil || er1r != nil {
print("error", err) print("error", err)
return return
} }

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>主题管理</title>
<style>
.container { max-width: 800px; margin: 20px auto; padding: 20px; }
.theme-list { margin-top: 20px; }
.theme-item { padding: 10px; border: 1px solid #eee; margin: 5px 0; }
</style>
</head>
<body>
<div class="container">
<h1>主题管理</h1>
<p>当前主题:<strong>{{.CurrentTheme}}</strong></p>
<div class="theme-list">
<h3>可选主题</h3>
<form action="/admin/themes/switch" method="post">
<select name="theme" style="padding: 8px; width: 200px;">
{{range .Themes}}
<option value="{{.}}" {{if eq . $.CurrentTheme}}selected{{end}}>{{.}}</option>
{{end}}
</select>
<button type="submit" style="padding: 8px 15px; margin-left: 10px;">切换主题</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content Creation Form</title>
</head>
<body>
<h1>Create New Content</h1>
<form id="contentForm">
<label for="title">Title:</label>
<input type="text" id="title" name="title" required><br>
<label for="slug">Slug:</label>
<input type="text" id="slug" name="slug" required><br>
<label for="text">Text:</label>
<textarea id="text" name="text" required></textarea><br>
<button type="submit">Create Content</button>
</form>
<script>
document.getElementById('contentForm').addEventListener('submit', function(event) {
event.preventDefault();
const title = document.getElementById('title').value;
const slug = document.getElementById('slug').value;
const text = document.getElementById('text').value;
fetch('/content', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, slug, text })
})
.then(response => response.json())
.then(data => {
console.log('Content created:', data);
alert('Content created successfully!');
document.getElementById('title').value = '';
document.getElementById('slug').value = '';
document.getElementById('text').value = '';
})
.catch(error => {
console.error('Error creating content:', error);
alert('An error occurred while creating the content.');
});
});
</script>
</body>
</html>

View File

@@ -1,193 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ .Title}}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body{
background: #f8f8f8;
}
.layui-main {
width: 1140px;
margin: 0 auto;
}
.container {
width: 1170px;
margin: 0 auto;
}
.header .layui-nav {
position: absolute;
right: 0;
top: 0;
padding: 0;
background: none;
}
.post-list {
width: 75%;
float: left;
display: block;
}
.list-card {
background: #fff;
overflow: hidden;
padding: 20px 20px 10px 20px;
position: relative;
border-radius: 10px;
margin-bottom: 10px;
height: 200px;
}
.sidebar {
float: left;
width: 25%;
}
.header {}
</style>
<script type="text/javascript">
window.addEventListener('load', (event) => {
var evsrc = new EventSource("http://localhost:8090/events");
evsrc.onmessage = function (ev) {
console.log("readyStateOnmessage = " + ev.currentTarget.readyState);
console.log("getdata = " + ev.data + "lastEventId = " + ev.lastEventId);
//document.getElementById("log")
//.insertAdjacentHTML("beforeend", "<li>" + ev.data + "</li>");
}
evsrc.onopen = function (event) {
console.log("Connection open ...");
};
evsrc.onerror = function (ev) {
console.log("readyStateOnError = " + ev.currentTarget.readyState);
//evsrc.close();
}
evsrc.addEventListener('connecttime', function (event) {
console.log("Start time: " + event.data );
}, false);
});
</script>
</head>
<body>
<h1>SSE test</h1>
<div>
<ul id="log">
</ul>
</div>
<div class="header">
<div class="layui-main">
<h1><a class="logo" href="https://www.hanxiaonuan.cn/">韩小暖的博客</a></h1>
</div>
<ul class="layui-nav">
<li class="layui-nav-item layui-hide-xs layui-this">
<a href="https://www.hanxiaonuan.cn/">首页</a>
</li>
<li class="layui-nav-item layui-hide-xs ">
<a href="https://www.hanxiaonuan.cn/start-page.html" title="关于">关于</a>
</li>
<span class="layui-nav-bar"></span></ul>
</div>
<div class="container">
<div class="post-list">
{{ range .Items }}
<div class="list-card">
<div><a href="/post/{{ .Cid }}">
<h1>Title: {{ .Title }}</h1>
</a>
<p> Slug: {{ .Slug }}</p>
<p>Text: {{ .Text }}</p>
<div>Created: {{ .Created }},</div>
</div>
</div>
{{ end }}
<div class="page-navigator">
共 {{ .Total }} 条,每页 {{ .PageSize }} 条,当前第 {{ .Page }} 页</div>
<div class="pagination">
{{ if .PrevPage }}
<a href="/page/{{ .PrevPage }}">上一页</a>
{{ end }}
{{ if .NextPage }}
<a href="/page/{{ .NextPage }}">下一页</a>
{{ end }}
<a href="/">首页</a>
<a href="/page/{{ .Total }}">尾页</a>
</div>
</div>
<div class="sidebar">
<div class="column">
<h3 class="title-sidebar"><i class="layui-icon"></i> 博客信息</h3>
<div class="personal-information">
<div class="user">
<img src="https://www.hanxiaonuan.cn/usr/uploads/2021/07/3991382612.jpg" alt="韩小暖的博客的头像"
class="rounded-circle avatar">
<div class="p-2">
<a class="user-name" target="_blank" href="https://www.hanxiaonuan.cn/">
韩小暖的博客</a>
<p class="introduction mt-1">这里是小暖的日常记录,欢迎!</p>
</div>
</div>
</div>
</div>
<div class="component">
<form class="layui-form" id="search" method="post" action="https://www.hanxiaonuan.cn/"
role="search">
<div class="layui-inline input">
<input type="text" id="s" name="s" class="layui-input" required="" lay-verify="required"
placeholder="输入关键字搜索">
</div>
<div class="layui-inline">
<button class="layui-btn layui-btn-sm layui-btn-primary"><i
class="layui-icon"></i></button>
</div>
</form>
</div>
<div class="column">
<h3 class="title-sidebar"><i class="layui-icon"></i> 栏目分类</h3>
<ul class="layui-row layui-col-space5">
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/default/"><i class="layui-icon"></i>
默认分类<span class="layui-badge layui-bg-gray">12</span></a></li>
<li class="layui-col-md12 layui-col-xs6"><a href="https://www.hanxiaonuan.cn/category/zc/"><i
class="layui-icon"></i> 日常随想<span class="layui-badge layui-bg-gray">2</span></a>
</li>
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/%E5%85%B3%E4%BA%8E%E6%88%BF%E5%AD%90/"><i
class="layui-icon"></i> 关于房子<span class="layui-badge layui-bg-gray">1</span></a>
</li>
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/%E5%B7%A5%E4%BD%9C%E6%97%A5%E5%BF%97/"><i
class="layui-icon"></i> 工作日志<span class="layui-badge layui-bg-gray">0</span></a>
</li>
</ul>
</div>
<div class="tags">
<h3 class="title-sidebar"><i class="layui-icon"></i>标签云</h3>
<div>
<a class="layui-btn layui-btn-xs layui-btn-primary" style="color: rgb(231, 229, 26)"
href="https://www.hanxiaonuan.cn/tag/%E5%B0%8F%E6%9A%96/" title="小暖">小暖</a>
<a class="layui-btn layui-btn-xs layui-btn-primary" style="color: rgb(125, 196, 207)"
href="https://www.hanxiaonuan.cn/tag/%E6%84%9F%E6%82%9F/" title="感悟">感悟</a>
</div>
</div>
<div class="tags">
<h3 class="title-sidebar"><i class="layui-icon"></i>系统</h3>
<div>
<li class="last"><a href="https://www.hanxiaonuan.cn/admin/">进入后台 (admin)</a></li>
<li><a href="https://www.hanxiaonuan.cn/action/logout?_=5b09b0e1048bbd45bdddc56fc43667b2">退出</a>
</li>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div>
{{ range $key, $val := . -}}
{{ $key }}:{{ $val }}
{{ end -}}
<h2>
<a
href="<?php $this->permalink() ?>"><?php $this->title() ?></a>
</h2>
<ul class="post-meta">
<li itemprop="author" itemscope itemtype="http://schema.org/Person"><?php _e('作者: '); ?><a
itemprop="name" href="<?php $this->author->permalink(); ?>"
rel="author"><?php $this->author(); ?></a></li>
<li><?php _e('时间: '); ?>
<time datetime="<?php $this->date('c'); ?>" itemprop="datePublished"><?php $this->date(); ?></time>
</li>
<li><?php _e('分类: '); ?><?php $this->category(','); ?></li>
<li itemprop="interactionCount">
<a itemprop="discussionUrl"
href="<?php $this->permalink() ?>#comments"><?php $this->commentsNum('评论', '1 条评论', '%d 条评论'); ?></a>
</li>
</ul>
<div class="post-content" itemprop="articleBody">
<?php $this->content('- 阅读剩余部分 -'); ?>
</div>
</article>
<?php endwhile; ?>
<?php $this->pageNav('&laquo; 前一页', '后一页 &raquo;'); ?>
</div><!-- end #main-->
<?php $this->need('sidebar.php'); ?>
<?php $this->need('footer.php'); ?>

View File

@@ -1,33 +0,0 @@
<div class="typecho-login-wrap">
<div class="typecho-login">
<h1><a href="login.html" class="i-logo">登录</a></h1>
<form action="login" method="post" name="login" role="form">
<p>
<label for="name" class="sr-only">{{.user.Name}}</label>
<input type="text" id="name" name="username" value="{{.user.Name}}" placeholder="{{.User.Name}}" class="text-l w-100" autofocus />
</p>
<p>
<label for="password" class="sr-only">{{.user.Password}}</label>
<input type="password" id="password" name="password" class="text-l w-100" placeholder="{{.user.Password}}" />
</p>
<p class="submit">
<button type="submit" class="btn btn-l w-100 primary">'登录'</button>
<input type="hidden" name="referer" value="'referer'" />
</p>
<p>
<label for="remember">
<input type="checkbox" name="remember" class="checkbox" value="1" id="remember" /> '下次自动登录'
</label>
</p>
</form>
</div>
</div>
<script>
$(document).ready(function () {
$('#name').focus();
});
</script>

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Page 1</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
.container {
width: 1170px;
margin: 0 auto;
}
.post-list {
width: 75%;
float: left;
display: block;
}
.sidebar {
float: left;
width: 25%;
}
.header {}
</style>
</head>
<body>
<div class="container">
<div class="layui-col-md9 layui-col-lg9">
<div class="title-article">
<h1>{{.Title}}</h1>
<div class="title-msg">
<span><i class="layui-icon">&#xe612;</i> {{.AuthorID}} </span>
<span><i class="layui-icon">&#xe60e;</i> {{.Created}}</span>
<span><i class="layui-icon">&#xe63a;</i> {{.CommentsNum}}</span>
</div>
</div>
<div class="text" itemprop="articleBody">
{{.Text}}
</div>
<div class="tags-text">
<i class="layui-icon">&#xe66e;</i>标签:{{.Type}}
</div>
<div class="copy-text">
<div>
<p>非特殊说明,本博所有文章均为博主原创。</p>
<p class="hidden-xs">如若转载,请注明出处: </p>
</div>
</div>
<div class="page-text">
<div>
<span class="layui-badge layui-bg-gray">上一篇</span>
</div>
<div>
<span class="layui-badge layui-bg-gray">下一篇</span>
</div>
</div>
<div class="comment-text layui-form">
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,37 +0,0 @@
<div class="typecho-login-wrap">
<div class="typecho-login">
<h1><a href="login.html" class="i-logo">登录</a></h1>
<form action="login" method="post" name="login" role="form">
<p>
<label for="name" class="sr-only">{{.user.Name}}</label>
<input type="text" id="name" name="username" value="{{.user.Name}}" placeholder="{{.User.Name}}" class="text-l w-100" autofocus />
</p>
<p>
<label for="password" class="sr-only">{{.user.Password}}</label>
<input type="password" id="password" name="password" class="text-l w-100" placeholder="{{.user.Password}}" />
</p>
<p>
<label for="conformPassword" class="sr-only">{{.user.ConformPassword}}</label>
<input type="conformPassword" id="conformPassword" name="conformPassword" class="text-l w-100" placeholder="{{.user.ConformPassword}}" />
</p>
<p class="submit">
<button type="submit" class="btn btn-l w-100 primary">'登录'</button>
<input type="hidden" name="referer" value="'referer'" />
</p>
<p>
<label for="remember">
<input type="checkbox" name="remember" class="checkbox" value="1" id="remember" /> '下次自动登录'
</label>
</p>
</form>
</div>
</div>
<script>
$(document).ready(function () {
$('#name').focus();
});
</script>

View File

@@ -200,17 +200,31 @@ func readFile(path string) ([]byte, error) {
return os.ReadFile(path) return os.ReadFile(path)
} }
// 获取可用主题列表 // ListThemes 获取所有可用主题(目录名)
func (m *ThemeManager) ListThemes() ([]string, error) { func (m *ThemeManager) ListThemes() ([]string, error) {
dirs, err := os.ReadDir(m.themesDir) entries, err := os.ReadDir(m.themesDir)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("读取主题目录失败: %w", err)
} }
var themes []string var themes []string
for _, d := range dirs { for _, entry := range entries {
if d.IsDir() && isValidTheme(filepath.Join(m.themesDir, d.Name())) { if entry.IsDir() {
themes = append(themes, d.Name()) themes = append(themes, entry.Name())
}
}
return themes, nil
}
// GetAvailableThemes 获取所有可用主题(读取 themesDir 目录下的子目录)
func (m *ThemeManager) GetAvailableThemes() ([]string, error) {
entries, err := os.ReadDir(m.themesDir)
if err != nil {
return nil, fmt.Errorf("读取主题目录失败: %w", err)
}
var themes []string
for _, entry := range entries {
if entry.IsDir() {
themes = append(themes, entry.Name())
} }
} }
return themes, nil return themes, nil

View File

@@ -23,23 +23,23 @@ func RenderTemplate(c *gin.Context, tmpl string, data gin.H) {
// SwitchTheme handles POST requests to switch the current theme // SwitchTheme handles POST requests to switch the current theme
// SwitchTheme 处理主题切换请求(修正后) // SwitchTheme 处理主题切换请求(修正后)
func SwitchTheme(c *gin.Context) { func SwitchTheme(c *gin.Context) {
// 从上下文中获取主题管理器(通过 main.go 的 themeMiddleware 注入) // 从上下文中获取主题管理器(通过 main.go 的 themeMiddleware 注入)
tm, exists := c.Get("ThemeManager") tm, exists := c.Get("ThemeManager")
if !exists { if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器未找到"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器未找到"})
return return
} }
themeManager, ok := tm.(*themes.ThemeManager) themeManager, ok := tm.(*ThemeManager)
if !ok { if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器类型错误"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器类型错误"})
return return
} }
newTheme := c.PostForm("theme") newTheme := c.PostForm("theme")
if err := themeManager.LoadTheme(newTheme); err != nil { if err := themeManager.LoadTheme(newTheme); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载主题失败: " + err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": "加载主题失败: " + err.Error()})
return return
} }
config.SetCurrentTheme(newTheme) // 持久化主题配置(需确保 config 包支持) config.SetCurrentTheme(newTheme) // 持久化主题配置(需确保 config 包支持)
c.JSON(http.StatusOK, gin.H{"status": "主题切换成功"}) c.JSON(http.StatusOK, gin.H{"status": "主题切换成功"})
} }

14
web/admin/index.tmpl Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Title}}</h1>
<nav>
<a href="/admin/themes">主题管理</a>
<a href="/admin/setinfo">个人信息</a>
</nav>
<p>欢迎登录后台管理系统!</p>
</body>
</html>

20
web/admin/login.tmpl Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Title}}web</h1>
<form action="/admin/login" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required>
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required>
</div>
<button type="submit">登录</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Title}}web下</h1>
<form action="/register" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required>
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required>
</div>
<div>
<label>确认密码:</label>
<input type="password" name="confirm_password" required>
</div>
<button type="submit">注册</button>
</form>
</body>
</html>