From 0b22d4f126342af39071a67b3b5f07e359e50d98 Mon Sep 17 00:00:00 2001 From: Zhang Chao Date: Tue, 28 Jul 2026 18:02:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/migrate_typecho/main.go | 262 ++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 4 + goblog.db | Bin 143360 -> 208896 bytes goblog.db.bak-migrate | Bin 0 -> 143360 bytes handlers/user.go | 255 +++++++++++++++++++++++++++++++++++ routers/router.go | 7 + static/admin/app.js | 203 ++++++++++++++++++++++++++++ static/admin/index.html | 81 ++++++++++- static/admin/style.css | 16 ++- 10 files changed, 822 insertions(+), 8 deletions(-) create mode 100644 cmd/migrate_typecho/main.go create mode 100644 goblog.db.bak-migrate create mode 100644 handlers/user.go diff --git a/cmd/migrate_typecho/main.go b/cmd/migrate_typecho/main.go new file mode 100644 index 0000000..988c667 --- /dev/null +++ b/cmd/migrate_typecho/main.go @@ -0,0 +1,262 @@ +// 一次性迁移工具:将远程 Typecho 数据库 typecho_contents 表中的文章(post)和页面(page) +// 迁移到本地 goblog.db 的 posts / pages 表。slug 相同的记录执行覆盖更新。 +// 用法: go run ./cmd/migrate_typecho -host 47.93.160.42 -port 3630 -user blog -pass 'xxx' +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + "strings" + "time" + + "goblog/models" + + "github.com/glebarez/sqlite" + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// typechoContent 对应 typecho_contents 表的一行 +type typechoContent struct { + CID int64 + Title sql.NullString + Slug sql.NullString + Created int64 + Modified int64 + Text sql.NullString + Order int64 + Template sql.NullString + Type string + Status string + Password sql.NullString + AllowComment string + AllowPing string +} + +func main() { + host := flag.String("host", "47.93.160.42", "MySQL主机") + port := flag.Int("port", 3630, "MySQL端口") + user := flag.String("user", "blog", "MySQL用户名") + pass := flag.String("pass", "qI7=bL4@iJ", "MySQL密码") + dbName := flag.String("db", "typecho", "typecho数据库名(留空自动探测)") + sqlitePath := flag.String("sqlite", "goblog.db", "本地SQLite文件路径") + dryRun := flag.Bool("dry-run", false, "只对比统计,不写入") + flag.Parse() + + // ---------- 连接远程 MySQL ---------- + cfg := mysql.NewConfig() + cfg.User = *user + cfg.Passwd = *pass + cfg.Net = "tcp" + cfg.Addr = fmt.Sprintf("%s:%d", *host, *port) + cfg.DBName = *dbName + cfg.Timeout = 10 * time.Second + cfg.ReadTimeout = 30 * time.Second + cfg.Params = map[string]string{"charset": "utf8mb4"} + + mysqlDB, err := sql.Open("mysql", cfg.FormatDSN()) + if err != nil { + log.Fatalf("MySQL DSN错误: %v", err) + } + defer mysqlDB.Close() + if err = mysqlDB.Ping(); err != nil { + log.Fatalf("MySQL连接失败: %v", err) + } + log.Printf("MySQL连接成功: %s", cfg.Addr) + + // 自动探测包含 typecho_contents 的数据库 + schema := *dbName + if schema == "" { + err = mysqlDB.QueryRow( + "SELECT table_schema FROM information_schema.tables WHERE table_name = 'typecho_contents' LIMIT 1", + ).Scan(&schema) + if err != nil { + log.Fatalf("未找到 typecho_contents 表: %v", err) + } + log.Printf("探测到typecho数据库: %s", schema) + } + + // ---------- 连接本地 SQLite ---------- + localDB, err := gorm.Open(sqlite.Open(*sqlitePath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Warn), + }) + if err != nil { + log.Fatalf("SQLite连接失败: %v", err) + } + + // 迁移文章归属到本地第一个管理员账号 + var author models.User + if err = localDB.Where("role = ?", "admin").First(&author).Error; err != nil { + if err = localDB.First(&author).Error; err != nil { + log.Fatalf("本地无任何用户,无法确定文章作者: %v", err) + } + } + log.Printf("迁移内容将归属本地用户: %s (ID=%d)", author.Username, author.ID) + + // ---------- 读取远程内容 ---------- + rows, err := mysqlDB.Query(fmt.Sprintf( + "SELECT cid, title, slug, created, modified, text, `order`, template, type, status, password, allowComment, allowPing "+ + "FROM `%s`.typecho_contents WHERE type IN ('post', 'page') ORDER BY cid", schema)) + if err != nil { + log.Fatalf("查询 typecho_contents 失败: %v", err) + } + defer rows.Close() + + var contents []typechoContent + for rows.Next() { + var c typechoContent + if err = rows.Scan(&c.CID, &c.Title, &c.Slug, &c.Created, &c.Modified, &c.Text, + &c.Order, &c.Template, &c.Type, &c.Status, &c.Password, &c.AllowComment, &c.AllowPing); err != nil { + log.Fatalf("读取行失败: %v", err) + } + contents = append(contents, c) + } + if err = rows.Err(); err != nil { + log.Fatalf("遍历结果失败: %v", err) + } + log.Printf("远程共读取 %d 条内容(post+page)", len(contents)) + + // ---------- 写入本地 ---------- + var postNew, postUpd, pageNew, pageUpd, skipped int + for _, c := range contents { + title := strings.TrimSpace(c.Title.String) + if title == "" { + log.Printf("跳过 cid=%d: 标题为空", c.CID) + skipped++ + continue + } + slug := strings.TrimSpace(c.Slug.String) + if slug == "" { + slug = fmt.Sprintf("typecho-%d", c.CID) + } + content := cleanContent(c.Text.String) + createdAt := time.Unix(c.Created, 0) + updatedAt := time.Unix(c.Modified, 0) + if c.Modified == 0 { + updatedAt = createdAt + } + status := "draft" + if c.Status == "publish" { + status = "published" + } + + if *dryRun { + log.Printf("[dry-run] %s cid=%d slug=%s title=%s status=%s", c.Type, c.CID, slug, title, status) + continue + } + + switch c.Type { + case "post": + post := models.Post{ + Title: title, + Slug: slug, + Content: content, + Summary: extractSummary(content), + AuthorID: author.ID, + Status: status, + Password: c.Password.String, + AllowComment: c.AllowComment == "1", + AllowPing: c.AllowPing == "1", + Template: c.Template.String, + } + if status == "published" { + post.PublishedAt = &createdAt + } + + var existing models.Post + err = localDB.Unscoped().Where("slug = ?", slug).First(&existing).Error + if err == nil { + // 覆盖更新已存在的文章 + post.ID = existing.ID + post.CreatedAt = createdAt + post.UpdatedAt = updatedAt + post.Views = existing.Views + post.CategoryID = existing.CategoryID + post.IsTop = existing.IsTop + if err = localDB.Unscoped().Model(&models.Post{}).Where("id = ?", existing.ID). + Select("Title", "Content", "Summary", "AuthorID", "Status", "Password", + "AllowComment", "AllowPing", "Template", "PublishedAt", "CreatedAt", "UpdatedAt", "DeletedAt"). + Updates(map[string]interface{}{ + "title": post.Title, "content": post.Content, "summary": post.Summary, + "author_id": post.AuthorID, "status": post.Status, "password": post.Password, + "allow_comment": post.AllowComment, "allow_ping": post.AllowPing, + "template": post.Template, "published_at": post.PublishedAt, + "created_at": createdAt, "updated_at": updatedAt, "deleted_at": nil, + }).Error; err != nil { + log.Fatalf("更新文章失败 slug=%s: %v", slug, err) + } + postUpd++ + } else if err == gorm.ErrRecordNotFound { + post.CreatedAt = createdAt + post.UpdatedAt = updatedAt + if err = localDB.Create(&post).Error; err != nil { + log.Fatalf("插入文章失败 slug=%s: %v", slug, err) + } + postNew++ + } else { + log.Fatalf("查询本地文章失败 slug=%s: %v", slug, err) + } + + case "page": + var existing models.Page + err = localDB.Unscoped().Where("slug = ?", slug).First(&existing).Error + if err == nil { + if err = localDB.Unscoped().Model(&models.Page{}).Where("id = ?", existing.ID). + Updates(map[string]interface{}{ + "title": title, "content": content, "author_id": author.ID, + "status": status, "order": c.Order, "password": c.Password.String, + "allow_comment": c.AllowComment == "1", + "created_at": createdAt, "updated_at": updatedAt, "deleted_at": nil, + }).Error; err != nil { + log.Fatalf("更新页面失败 slug=%s: %v", slug, err) + } + pageUpd++ + } else if err == gorm.ErrRecordNotFound { + page := models.Page{ + Title: title, + Slug: slug, + Content: content, + AuthorID: author.ID, + Status: status, + Order: int(c.Order), + Password: c.Password.String, + AllowComment: c.AllowComment == "1", + } + page.CreatedAt = createdAt + page.UpdatedAt = updatedAt + if err = localDB.Create(&page).Error; err != nil { + log.Fatalf("插入页面失败 slug=%s: %v", slug, err) + } + pageNew++ + } else { + log.Fatalf("查询本地页面失败 slug=%s: %v", slug, err) + } + } + } + + log.Printf("迁移完成: 文章新增 %d 篇 / 覆盖 %d 篇, 页面新增 %d 个 / 覆盖 %d 个, 跳过 %d 条", + postNew, postUpd, pageNew, pageUpd, skipped) +} + +// cleanContent 清理typecho内容: 去掉 标记前缀 +func cleanContent(text string) string { + return strings.TrimPrefix(text, "") +} + +// extractSummary 提取摘要: 优先取 之前的部分, 否则取前200个字符 +func extractSummary(content string) string { + if idx := strings.Index(content, ""); idx >= 0 { + content = content[:idx] + } + // 去掉常见markdown标记后截断 + plain := strings.NewReplacer("#", "", "*", "", "`", "", ">", "", "\r", "").Replace(content) + plain = strings.TrimSpace(plain) + runes := []rune(plain) + if len(runes) > 200 { + return string(runes[:200]) + } + return plain +} diff --git a/go.mod b/go.mod index 2b57522..431a8fb 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 github.com/gin-gonic/gin v1.11.0 github.com/glebarez/sqlite v1.11.0 + github.com/go-sql-driver/mysql v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/spf13/viper v1.21.0 github.com/yuin/goldmark v1.8.5 @@ -14,6 +15,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.2.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect diff --git a/go.sum b/go.sum index 3034aa4..8993b60 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= @@ -31,6 +33,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= diff --git a/goblog.db b/goblog.db index e68b331c57bbf4a34585f13586f95a283a354772..d6277efcd02b28ecb18ef899d1f68553cc45bf16 100644 GIT binary patch literal 208896 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 delta 678 zcmZWmO=uHA6rMM;Nq3s;zKx<0MUo;ENhIA?X+x-ziZ_u6f*w5BrCmx3$*$R`porDltVW}6f2mdgLAccg2fl|E)f)+v2f)ksA(1AJ3_r3SM_kB}sMXN3O z?tYbQ5kd}iFVqeXQ?m7Z{2_(`Qv_3fUowfiX3v2}&J;*|Hk24p;+z-U(L^dXk{le& z4G!h>Y-U71mL1aFYYBT`2YNKEXLGtPEBKl8-~z$l_zmCVD!#-;T<~N19A1p{umfP( zm1(Qwq$6Q=kWW~}qFJgqJjCK)8&jr3WybbE#h7v=v9HW+%i&Uow490)Rw(P^6Gp|H zvdYt@BLj=`D~?%q7RTF&#$<82)GOKmHwkX~vHVx8osIgN1v%LZ*cJ@ghix~e4g06; z%jmW)g}fKHU`!FjUg&g&u}1t%wWYdA?Xg>%bF?rn3oZPFAMgz>WDBe%ur9|Ho>K5#CXzYMtP9wJr)@sX<+Z5s)c12>!x`P^+WYNI?35l-C|f zI^@+5Z4$f?DMb5~JGAM?w6`>9mf^GvQqX?`j#J14lQ&_WN?}|hL1rG#qgeNLur>#M z+79rk0ELUC&X;@_9=-@KOJeQXhi&( zVcTWvsyVrs|AV_ZP{ug#lu}&pSXvj-jcw`RCsI|D1^4d+V+I^i{bywpyrKo8pm{46 Gr2Yb!ySZNg diff --git a/goblog.db.bak-migrate b/goblog.db.bak-migrate new file mode 100644 index 0000000000000000000000000000000000000000..c9bb116397ce6e0948e4b90ee70c0072a8f70dc8 GIT binary patch literal 143360 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 literal 0 HcmV?d00001 diff --git a/handlers/user.go b/handlers/user.go new file mode 100644 index 0000000..2ed6b56 --- /dev/null +++ b/handlers/user.go @@ -0,0 +1,255 @@ +package handlers + +import ( + "net/http" + "strconv" + + "goblog/database" + "goblog/models" + + "github.com/gin-gonic/gin" +) + +// 创建用户请求 +type CreateUserRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Password string `json:"password" binding:"required,min=6"` + Nickname string `json:"nickname"` + Email string `json:"email" binding:"required,email"` + Role string `json:"role"` + Status *int `json:"status"` +} + +// 更新用户请求 +type AdminUpdateUserRequest struct { + Nickname string `json:"nickname"` + Email string `json:"email"` + Role string `json:"role"` + Status *int `json:"status"` +} + +// 重置密码请求 +type ResetPasswordRequest struct { + NewPassword string `json:"new_password" binding:"required,min=6"` +} + +// 获取用户列表(管理员) +func GetUsers(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + keyword := c.Query("keyword") + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + query := database.DB.Model(&models.User{}) + if keyword != "" { + like := "%" + keyword + "%" + query = query.Where("username LIKE ? OR nickname LIKE ? OR email LIKE ?", like, like, like) + } + + var total int64 + query.Count(&total) + + var users []models.User + if err := query.Order("id ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&users).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户列表失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "data": users, + "total": total, + "page": page, + "page_size": pageSize, + }) +} + +// 创建用户(管理员) +func CreateUser(c *gin.Context) { + var req CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 检查用户名是否已存在 + var count int64 + database.DB.Model(&models.User{}).Where("username = ?", req.Username).Count(&count) + if count > 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"}) + return + } + + // 检查邮箱是否已存在 + database.DB.Model(&models.User{}).Where("email = ?", req.Email).Count(&count) + if count > 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已被注册"}) + return + } + + role := req.Role + if role != "admin" { + role = "user" + } + + status := 1 + if req.Status != nil { + status = *req.Status + } + + user := models.User{ + Username: req.Username, + Nickname: req.Nickname, + Email: req.Email, + Role: role, + Status: status, + IsActive: true, + } + + if err := user.SetPassword(req.Password); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) + return + } + + if err := database.DB.Create(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"data": user}) +} + +// 更新用户信息(管理员) +func AdminUpdateUser(c *gin.Context) { + id := c.Param("id") + operatorID, _ := c.Get("userID") + + var user models.User + if err := database.DB.First(&user, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + + var req AdminUpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + isSelf := operatorID == user.ID + + updates := map[string]interface{}{ + "nickname": req.Nickname, + } + + if req.Email != "" && req.Email != user.Email { + // 检查邮箱是否已被其他用户使用 + var count int64 + database.DB.Model(&models.User{}).Where("email = ? AND id != ?", req.Email, user.ID).Count(&count) + if count > 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已被其他用户使用"}) + return + } + updates["email"] = req.Email + } + + if req.Role != "" && req.Role != user.Role { + if isSelf { + c.JSON(http.StatusBadRequest, gin.H{"error": "不能修改自己的角色"}) + return + } + if req.Role != "admin" && req.Role != "user" { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的角色"}) + return + } + updates["role"] = req.Role + } + + if req.Status != nil && *req.Status != user.Status { + if isSelf { + c.JSON(http.StatusBadRequest, gin.H{"error": "不能禁用自己的账号"}) + return + } + updates["status"] = *req.Status + } + + if err := database.DB.Model(&user).Updates(updates).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"data": user}) +} + +// 重置用户密码(管理员) +func AdminResetPassword(c *gin.Context) { + id := c.Param("id") + + var user models.User + if err := database.DB.First(&user, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + + var req ResetPasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := user.SetPassword(req.NewPassword); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) + return + } + + if err := database.DB.Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "重置密码失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "密码重置成功"}) +} + +// 删除用户(管理员) +func DeleteUser(c *gin.Context) { + id := c.Param("id") + operatorID, _ := c.Get("userID") + + var user models.User + if err := database.DB.First(&user, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + + if operatorID == user.ID { + c.JSON(http.StatusBadRequest, gin.H{"error": "不能删除自己的账号"}) + return + } + + // 检查该用户是否有文章 + var count int64 + database.DB.Model(&models.Post{}).Where("author_id = ?", user.ID).Count(&count) + if count > 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "该用户名下还有文章,无法删除"}) + return + } + + // 检查该用户是否有页面 + database.DB.Model(&models.Page{}).Where("author_id = ?", user.ID).Count(&count) + if count > 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "该用户名下还有页面,无法删除"}) + return + } + + if err := database.DB.Delete(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} diff --git a/routers/router.go b/routers/router.go index cc2fdb4..361a409 100644 --- a/routers/router.go +++ b/routers/router.go @@ -100,6 +100,13 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) { admin.PUT("/comments/:id/spam", handlers.MarkSpamComment) admin.DELETE("/comments/:id", handlers.DeleteComment) + // 用户管理 + admin.GET("/users", handlers.GetUsers) + admin.POST("/users", handlers.CreateUser) + admin.PUT("/users/:id", handlers.AdminUpdateUser) + admin.PUT("/users/:id/password", handlers.AdminResetPassword) + admin.DELETE("/users/:id", handlers.DeleteUser) + // 文件管理(图片上传) admin.POST("/upload", handlers.UploadImage) admin.GET("/uploads", handlers.ListUploads) diff --git a/static/admin/app.js b/static/admin/app.js index 335c82f..42fd03a 100644 --- a/static/admin/app.js +++ b/static/admin/app.js @@ -43,6 +43,9 @@ document.addEventListener('DOMContentLoaded', () => { // 模态框表单 document.getElementById('modal-form').addEventListener('submit', handleModalSubmit); + // 用户表单 + document.getElementById('user-form').addEventListener('submit', handleUserSubmit); + // 封面图上传 const coverFileInput = document.getElementById('post-cover-file'); if (coverFileInput) { @@ -104,6 +107,9 @@ function showPage(pageName) { case 'comments': loadComments(); break; + case 'users': + loadUsers(); + break; case 'files': loadFiles(); break; @@ -709,6 +715,203 @@ function getStatusText(status) { return map[status] || status; } +// ==================== 用户管理 ==================== + +// 加载用户列表 +async function loadUsers() { + try { + const res = await fetch(`${API_BASE}/admin/users?page_size=100`, { headers: getHeaders() }); + const data = await res.json(); + + const tbody = document.getElementById('users-list'); + tbody.innerHTML = data.data?.map(u => { + const isSelf = currentUser && u.id === currentUser.id; + return ` + + ${u.username}${isSelf ? ' 当前' : ''} + ${u.nickname || '-'} + ${u.email || '-'} + ${u.role === 'admin' ? '管理员' : '普通用户'} + ${u.status === 1 ? '启用' : '禁用'} + ${u.last_login_at ? new Date(u.last_login_at).toLocaleString() : '-'} + + + + ${isSelf ? '' : ` + ${u.status === 1 ? `` : ``} + `} + + + `; + }).join('') || '暂无用户'; + + // 缓存用户数据供编辑使用 + usersCache = data.data || []; + } catch (err) { + console.error('加载用户失败:', err); + } +} + +let usersCache = []; + +// 显示新建用户表单 +function showUserForm() { + document.getElementById('user-modal-title').textContent = '新建用户'; + document.getElementById('user-form').reset(); + document.getElementById('user-id').value = ''; + document.getElementById('user-username').disabled = false; + document.getElementById('user-password-group').classList.remove('hidden'); + document.getElementById('user-password').required = true; + document.getElementById('user-role').disabled = false; + document.getElementById('user-status').disabled = false; + document.getElementById('user-modal').classList.remove('hidden'); +} + +// 编辑用户 +function editUser(id) { + const user = usersCache.find(u => u.id === id); + if (!user) return; + + const isSelf = currentUser && user.id === currentUser.id; + + document.getElementById('user-modal-title').textContent = '编辑用户'; + document.getElementById('user-id').value = user.id; + document.getElementById('user-username').value = user.username; + document.getElementById('user-username').disabled = true; + // 编辑时不修改密码,隐藏密码输入 + document.getElementById('user-password-group').classList.add('hidden'); + document.getElementById('user-password').required = false; + document.getElementById('user-password').value = ''; + document.getElementById('user-nickname').value = user.nickname || ''; + document.getElementById('user-email').value = user.email || ''; + document.getElementById('user-role').value = user.role; + document.getElementById('user-status').value = String(user.status); + // 不能修改自己的角色和状态 + document.getElementById('user-role').disabled = isSelf; + document.getElementById('user-status').disabled = isSelf; + document.getElementById('user-modal').classList.remove('hidden'); +} + +// 隐藏用户弹窗 +function hideUserModal() { + document.getElementById('user-modal').classList.add('hidden'); +} + +// 提交用户表单 +async function handleUserSubmit(e) { + e.preventDefault(); + + const id = document.getElementById('user-id').value; + const data = { + nickname: document.getElementById('user-nickname').value, + email: document.getElementById('user-email').value, + role: document.getElementById('user-role').value, + status: parseInt(document.getElementById('user-status').value) + }; + + if (!id) { + data.username = document.getElementById('user-username').value; + data.password = document.getElementById('user-password').value; + } + + try { + const url = id ? `${API_BASE}/admin/users/${id}` : `${API_BASE}/admin/users`; + const method = id ? 'PUT' : 'POST'; + + const res = await fetch(url, { + method, + headers: getHeaders(), + body: JSON.stringify(data) + }); + + if (res.ok) { + hideUserModal(); + loadUsers(); + } else { + const err = await res.json().catch(() => ({})); + alert(err.error || '操作失败'); + } + } catch (err) { + alert('网络错误'); + } +} + +// 启用/禁用用户 +async function toggleUserStatus(id, status) { + const actionText = status === 1 ? '启用' : '禁用'; + if (!confirm(`确定要${actionText}这个用户吗?`)) return; + + try { + const user = usersCache.find(u => u.id === id); + const res = await fetch(`${API_BASE}/admin/users/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify({ + nickname: user?.nickname || '', + email: user?.email || '', + status: status + }) + }); + + if (res.ok) { + loadUsers(); + } else { + const err = await res.json().catch(() => ({})); + alert(err.error || '操作失败'); + } + } catch (err) { + alert('网络错误'); + } +} + +// 重置用户密码 +async function resetUserPassword(id, username) { + const newPassword = prompt(`请输入用户「${username}」的新密码(至少 6 位):`); + if (newPassword === null) return; + if (newPassword.length < 6) { + alert('密码至少 6 位'); + return; + } + + try { + const res = await fetch(`${API_BASE}/admin/users/${id}/password`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify({ new_password: newPassword }) + }); + + if (res.ok) { + alert('密码重置成功'); + } else { + const err = await res.json().catch(() => ({})); + alert(err.error || '重置失败'); + } + } catch (err) { + alert('网络错误'); + } +} + +// 删除用户 +async function deleteUser(id, username) { + if (!confirm(`确定要删除用户「${username}」吗?`)) return; + + try { + const res = await fetch(`${API_BASE}/admin/users/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + + if (res.ok) { + loadUsers(); + } else { + const err = await res.json().catch(() => ({})); + alert(err.error || '删除失败'); + } + } catch (err) { + alert('网络错误'); + } +} + // ==================== 主题管理 ==================== // 加载主题列表 diff --git a/static/admin/index.html b/static/admin/index.html index 563b0ff..5dd54ae 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -5,7 +5,7 @@ 后台管理 - GoBlog - + - - +
+ + +
@@ -172,7 +175,7 @@ 置顶文章
- + @@ -254,6 +257,28 @@ + + + + + + - + diff --git a/static/admin/style.css b/static/admin/style.css index dbd55e7..c18c9d2 100644 --- a/static/admin/style.css +++ b/static/admin/style.css @@ -841,11 +841,22 @@ body { color: #fff; } +/* 编辑/预览共享同一容器,切换时区域尺寸不变 */ +.md-body { + position: relative; + height: 460px; + min-height: 240px; + resize: vertical; + overflow: hidden; +} + .md-editor textarea { display: block; + width: 100%; + height: 100%; border: none !important; border-radius: 0 !important; - resize: vertical; + resize: none; font-family: Consolas, Monaco, 'Courier New', monospace; line-height: 1.6; } @@ -855,8 +866,7 @@ body { } .md-preview { - min-height: 320px; - max-height: 600px; + height: 100%; overflow-y: auto; padding: 16px; background: #fff;