增加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/
/.vscode
bash.exe.stackdump

View File

@@ -133,7 +133,8 @@ func GetCurrentTheme() string {
return "default"
}
func SetCurrentTheme(theme string) {
func SetCurrentTheme(theme string) error {
return nil
}
// GetJWTSecret 获取 JWT 密钥
@@ -144,8 +145,8 @@ func SetCurrentTheme(theme string) {
// 新增包级函数获取 JWT 密钥
func GetJWTSecret() string {
if globalConfig == nil {
panic("配置未加载,请先调用 LoadConfig")
}
return globalConfig.JwtSecretKey.SecretKey
if globalConfig == nil {
//panic("配置未加载,请先调用 LoadConfig")
}
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
import (
"net/http"
"go_blog/config"
"go_blog/themes"
"os"
"github.com/gin-gonic/gin"
)
// ListThemes 获取可用主题列表
// ListThemes 显示主题管理页面(返回主题列表和当前主题)
func ListThemes(c *gin.Context) {
// 从主题管理器获取当前主题
// 从上下文中获取主题管理器
tm, exists := c.Get("ThemeManager")
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
}
themeManager := tm.(*themes.ThemeManager)
// 读取 web/themes 目录下的所有主题文件夹
themesDir := "web/themes"
entries, err := os.ReadDir(themesDir)
// 获取可用主题列表(读取 web/themes 目录下的所有子目录)
entries, err := themeManager.GetAvailableThemes() // 假设 ThemeManager 新增获取主题列表方法
if err != nil {
c.JSON(500, gin.H{"error": "读取主题目录失败: " + err.Error()})
c.String(http.StatusInternalServerError, "读取主题目录失败: "+err.Error())
return
}
var themeList []string
for _, entry := range entries {
if entry.IsDir() {
themeList = append(themeList, entry.Name())
}
// 渲染管理页面模板
tpl := themeManager.GetTemplate("admin/themes") // 对应 templates/admin/themes.tmpl
if tpl == nil {
c.String(http.StatusInternalServerError, "模板 'admin/themes' 未找到")
return
}
c.JSON(200, gin.H{
"current": themeManager.CurrentTheme(),
"themes": themeList,
c.Header("Content-Type", "text/html; charset=utf-8")
err = tpl.Execute(c.Writer, gin.H{
"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/util"
"go_blog/serializers"
"go_blog/themes"
"html/template"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
@@ -43,6 +47,24 @@ func UsersLoginHandler(ctx *gin.Context) {
response.ServerError("生成令牌失败: " + err.Error())
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["token"] = token
response.Response(data, nil)
@@ -104,12 +126,12 @@ func UsersSetPwdHandler(ctx *gin.Context) {
response := Response{Ctx: ctx}
// 从上下文中获取用户(替换原 jwt.AssertUser 调用)
user, exists := ctx.Get("user")
ctxuser, exists := ctx.Get("user")
if !exists {
response.Unauthenticated("未验证登录")
return
}
currentUser, ok := user.(*models.Account)
currentUser, ok := ctxuser.(*models.Account)
if !ok {
response.ServerError("用户类型错误")
return
@@ -155,7 +177,57 @@ func UsersListHandler(ctx *gin.Context) {
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()
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"
"time"
"go_blog/config"
"go_blog/models"
"github.com/gin-gonic/gin"
@@ -15,7 +14,8 @@ import (
)
// 定义 JWT 密钥(从配置文件读取)
var jwtSecret = []byte(config.GetJWTSecret())
// var jwtSecret = []byte(config.GetJWTSecret())
var jwtSecret = []byte("your-hardcoded-secret-key")
// CustomClaims 自定义 JWT 载荷(包含用户 ID
type CustomClaims struct {

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"go_blog/controllers"
"go_blog/models"
"go_blog/pkg/jwt"
"go_blog/serializers"
"go_blog/themes" // <-- 确保导入 themes 包
"log"
@@ -15,6 +16,31 @@ import (
"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) {
// 注册WebSocket路由
@@ -106,34 +132,34 @@ func RegisterRoutes(r *gin.Engine) {
r.GET("/ws", controllers.WebSocketHandler)
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)
r.GET("/", controllers.Home)
r.GET("/post/:id", controllers.ShowPost)
// Admin panel取消注释并完善
admin := r.Group("/admin", jwt.AuthRequired()) // 新增中间件
admin := r.Group("/admin")
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
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) {
w := c.Writer
@@ -156,9 +182,8 @@ func esSSE(c *gin.Context) {
time.Sleep(1 * time.Second)
}
_, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf")
_, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime")
if err != nil || er1r != nil {
_, err := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime")
if err != nil {
print("error", err)
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)
}
// 获取可用主题列表
// ListThemes 获取所有可用主题(目录名)
func (m *ThemeManager) ListThemes() ([]string, error) {
dirs, err := os.ReadDir(m.themesDir)
entries, err := os.ReadDir(m.themesDir)
if err != nil {
return nil, err
return nil, fmt.Errorf("读取主题目录失败: %w", err)
}
var themes []string
for _, d := range dirs {
if d.IsDir() && isValidTheme(filepath.Join(m.themesDir, d.Name())) {
themes = append(themes, d.Name())
for _, entry := range entries {
if entry.IsDir() {
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

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