Enhance user authentication and authentication

Fix the modification of personal information
Fix the formatted page template
This commit is contained in:
2025-10-26 03:05:27 +08:00
parent 3e170ad526
commit c93ee377fe
22 changed files with 2728 additions and 2420 deletions

View File

@@ -37,6 +37,12 @@
"security": { "security": {
"jwt_secret": "your-jwt-secret-key", "jwt_secret": "your-jwt-secret-key",
"encryption_key": "your-encryption-key", "encryption_key": "your-encryption-key",
"jwt_refresh_threshold_hours": 6 "jwt_refresh_threshold_hours": 6,
"cookie": {
"secure": false,
"same_site": "Lax",
"domain": "",
"max_age": 86400
}
} }
} }

View File

@@ -26,8 +26,33 @@ func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// 获取或生成CSRF令牌
var token string
if existingToken := utils.GetCSRFTokenFromCookie(r); existingToken != "" {
// 重用现有的Cookie令牌
token = existingToken
} else {
// 生成新的CSRF令牌并设置到Cookie
newToken, err := utils.GenerateCSRFToken()
if err != nil {
http.Error(w, "生成CSRF令牌失败", http.StatusInternalServerError)
return
}
token = newToken
utils.SetCSRFToken(w, token)
}
// 准备模板数据
extraData := map[string]interface{}{
"Title": "管理员登录",
}
data := utils.GetDefaultTemplateData() data := utils.GetDefaultTemplateData()
data["Title"] = "管理员登录" data["CSRFToken"] = token
// 合并额外数据
for key, value := range extraData {
data[key] = value
}
utils.RenderTemplate(w, "login.html", data) utils.RenderTemplate(w, "login.html", data)
} }
@@ -97,15 +122,8 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// 设置JWT CookieHttpOnly安全 // 设置JWT Cookie使用安全配置
cookie := &http.Cookie{ cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge())
Name: "admin_session",
Value: token,
Path: "/",
HttpOnly: true,
Secure: false, // 生产环境应设置为trueHTTPS
MaxAge: 24 * 60 * 60, // 24小时
}
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
utils.JsonResponse(w, http.StatusOK, true, "登录成功", map[string]interface{}{ utils.JsonResponse(w, http.StatusOK, true, "登录成功", map[string]interface{}{
@@ -132,15 +150,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
// - 统一的Cookie清理函数确保一致性 // - 统一的Cookie清理函数确保一致性
// - 在JWT校验失败时自动调用提升安全性和用户体验 // - 在JWT校验失败时自动调用提升安全性和用户体验
func clearInvalidJWTCookie(w http.ResponseWriter) { func clearInvalidJWTCookie(w http.ResponseWriter) {
cookie := &http.Cookie{ cookie := utils.CreateExpiredCookie("admin_session")
Name: "admin_session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: false, // 生产环境应设置为true
MaxAge: -1, // 立即失效
Expires: time.Unix(0, 0), // 确保过期
}
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
} }
@@ -434,15 +444,8 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW
} }
newToken, err := generateJWTToken(user) newToken, err := generateJWTToken(user)
if err == nil { if err == nil {
// 更新Cookie // 更新Cookie(使用安全配置)
newCookie := &http.Cookie{ newCookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge())
Name: "admin_session",
Value: newToken,
Path: "/",
HttpOnly: true,
Secure: false, // 生产环境应设置为trueHTTPS
MaxAge: 24 * 60 * 60, // 24小时
}
http.SetCookie(w, newCookie) http.SetCookie(w, newCookie)
refreshed = true refreshed = true

View File

@@ -7,7 +7,8 @@ import (
"math/big" "math/big"
"net/http" "net/http"
"strings" "strings"
"time"
"networkDev/utils"
"github.com/mojocn/base64Captcha" "github.com/mojocn/base64Captcha"
) )
@@ -62,15 +63,7 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
// 将验证码ID存储到session中这里简化处理实际项目中应该使用更安全的方式 // 将验证码ID存储到session中这里简化处理实际项目中应该使用更安全的方式
// 设置cookie来存储验证码ID // 设置cookie来存储验证码ID
cookie := &http.Cookie{ cookie := utils.CreateSecureCookie("captcha_id", id, 300) // 5分钟过期
Name: "captcha_id",
Value: id,
Path: "/",
HttpOnly: true,
Secure: false, // 生产环境应设置为true
MaxAge: 300, // 5分钟过期
Expires: time.Now().Add(5 * time.Minute),
}
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
// 解码base64图片数据并返回 // 解码base64图片数据并返回

View File

@@ -26,21 +26,47 @@ func AdminIndexHandler(w http.ResponseWriter, r *http.Request) {
// AdminLayoutHandler 后台布局页渲染 // AdminLayoutHandler 后台布局页渲染
// - 渲染 layout.html包含顶部导航、侧边栏与动态内容容器 // - 渲染 layout.html包含顶部导航、侧边栏与动态内容容器
func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) { func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) {
data := utils.GetDefaultTemplateData() // 获取或生成CSRF令牌
var token string
if existingToken := utils.GetCSRFTokenFromCookie(r); existingToken != "" {
// 重用现有的Cookie令牌
token = existingToken
} else {
// 生成新的CSRF令牌并设置到Cookie
newToken, err := utils.GenerateCSRFToken()
if err != nil {
http.Error(w, "生成CSRF令牌失败", http.StatusInternalServerError)
return
}
token = newToken
utils.SetCSRFToken(w, token)
}
// 准备额外的模板数据
extraData := make(map[string]interface{})
// 从数据库读取站点标题 // 从数据库读取站点标题
db, err := database.GetDB() db, dbErr := database.GetDB()
if err != nil { if dbErr != nil {
data["Title"] = "凌动技术" extraData["Title"] = "凌动技术"
} else { } else {
siteTitle, err := services.FindSettingByName("site_title", db) siteTitle, settingErr := services.FindSettingByName("site_title", db)
if err != nil || siteTitle == nil { if settingErr != nil || siteTitle == nil {
data["Title"] = "凌动技术" extraData["Title"] = "凌动技术"
} else { } else {
data["Title"] = siteTitle.Value extraData["Title"] = siteTitle.Value
} }
} }
// 准备模板数据
data := utils.GetDefaultTemplateData()
data["CSRFToken"] = token
// 合并额外数据
for key, value := range extraData {
data[key] = value
}
utils.RenderTemplate(w, "layout.html", data) utils.RenderTemplate(w, "layout.html", data)
} }

View File

@@ -160,15 +160,8 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// 更新Cookie // 更新Cookie(使用安全配置)
cookie := &http.Cookie{ cookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge())
Name: "admin_session",
Value: newToken,
Path: "/",
HttpOnly: true,
Secure: false, // 生产环境应设置为trueHTTPS
MaxAge: 24 * 60 * 60, // 24小时
}
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
// 密码修改成功已重新生成JWT令牌 // 密码修改成功已重新生成JWT令牌
@@ -260,20 +253,14 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
} }
// 重新签发JWT并写入Cookie // 重新签发JWT并写入Cookie
newUser := models.User{UUID: claims.UserUUID, Username: username, Role: claims.Role} // 使用完整的用户信息包含密码来生成JWT令牌
token, err := generateJWTToken(newUser) user.Username = username // 更新用户名
token, err := generateJWTToken(user)
if err != nil { if err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil) utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
return return
} }
cookie := &http.Cookie{ cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge())
Name: "admin_session",
Value: token,
Path: "/",
HttpOnly: true,
Secure: false,
MaxAge: 24 * 60 * 60,
}
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{ utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{

5
cookies.txt Normal file
View File

@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / TRUE 1761422606 csrf_token QLYaH1VddKCyAFgijZ80OYxzDht7zVLPbXH-rprEXvM=

View File

@@ -8,21 +8,22 @@ import (
) )
// SeedDefaultAdmin 初始化默认管理员账号 // SeedDefaultAdmin 初始化默认管理员账号
// - 如果用户名为 admin 的用户已存在,则跳过 // - 如果已存在任何管理员用户role=0,则跳过
// - 如不存在,则创建用户名为 admin、密码为 admin123以 bcrypt 哈希存储)、角色 Role=0 的管理员 // - 如不存在,则创建用户名为 admin、密码为 admin123以 bcrypt 哈希存储)、角色 Role=0 的管理员
// - ID和UUID将自动生成 // - 根据需求:默认 admin 用户的 ID 固定为 10000
func SeedDefaultAdmin() error { func SeedDefaultAdmin() error {
db, err := GetDB() db, err := GetDB()
if err != nil { if err != nil {
return err return err
} }
// 检查是否存在用户名为 admin 的用户 // 检查是否存在任何管理员用户role=0
var count int64 var count int64
if dbErr := db.Model(&models.User{}).Where("username = ?", "admin").Count(&count).Error; dbErr != nil { if dbErr := db.Model(&models.User{}).Where("role = ?", 0).Count(&count).Error; dbErr != nil {
return dbErr return dbErr
} }
if count > 0 { if count > 0 {
logrus.Info("已存在管理员用户,跳过默认管理员创建")
return nil return nil
} }

View File

@@ -3,6 +3,7 @@ package server
import ( import (
"net/http" "net/http"
adminctl "networkDev/controllers/admin" adminctl "networkDev/controllers/admin"
"networkDev/utils"
) )
// RegisterAdminRoutes 注册管理员后台相关路由 // RegisterAdminRoutes 注册管理员后台相关路由
@@ -23,7 +24,8 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
return return
} }
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
adminctl.LoginHandler(w, r) // 应用CSRF保护
utils.RequireCSRFToken(adminctl.LoginHandler)(w, r)
return return
} }
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
@@ -35,6 +37,9 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
// 验证码生成路由(无需认证) // 验证码生成路由(无需认证)
mux.HandleFunc("/admin/captcha", adminctl.CaptchaHandler) mux.HandleFunc("/admin/captcha", adminctl.CaptchaHandler)
// CSRF令牌获取API无需认证但需要在登录页面等地方获取
mux.HandleFunc("/admin/api/csrf-token", utils.CSRFTokenHandler)
// 后台布局页(需要管理员认证) // 后台布局页(需要管理员认证)
mux.HandleFunc("/admin/layout", adminctl.AdminAuthRequired(adminctl.AdminLayoutHandler)) mux.HandleFunc("/admin/layout", adminctl.AdminAuthRequired(adminctl.AdminLayoutHandler))
@@ -51,44 +56,44 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
// 个人资料API // 个人资料API
mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler)) mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler))
mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(adminctl.UserProfileUpdateHandler)) mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler)))
mux.HandleFunc("/admin/api/user/password", adminctl.AdminAuthRequired(adminctl.UserPasswordUpdateHandler)) mux.HandleFunc("/admin/api/user/password", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserPasswordUpdateHandler)))
// 系统设置API // 系统设置API
mux.HandleFunc("/admin/api/settings", adminctl.AdminAuthRequired(adminctl.SettingsQueryHandler)) mux.HandleFunc("/admin/api/settings", adminctl.AdminAuthRequired(adminctl.SettingsQueryHandler))
mux.HandleFunc("/admin/api/settings/update", adminctl.AdminAuthRequired(adminctl.SettingsUpdateHandler)) mux.HandleFunc("/admin/api/settings/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.SettingsUpdateHandler)))
// 应用管理API // 应用管理API
mux.HandleFunc("/admin/api/apps/list", adminctl.AdminAuthRequired(adminctl.AppsListHandler)) mux.HandleFunc("/admin/api/apps/list", adminctl.AdminAuthRequired(adminctl.AppsListHandler))
mux.HandleFunc("/admin/api/apps/create", adminctl.AdminAuthRequired(adminctl.AppCreateHandler)) mux.HandleFunc("/admin/api/apps/create", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppCreateHandler)))
mux.HandleFunc("/admin/api/apps/update", adminctl.AdminAuthRequired(adminctl.AppUpdateHandler)) mux.HandleFunc("/admin/api/apps/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateHandler)))
mux.HandleFunc("/admin/api/apps/delete", adminctl.AdminAuthRequired(adminctl.AppDeleteHandler)) mux.HandleFunc("/admin/api/apps/delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppDeleteHandler)))
mux.HandleFunc("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(adminctl.AppsBatchDeleteHandler)) mux.HandleFunc("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppsBatchDeleteHandler)))
mux.HandleFunc("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(adminctl.AppsBatchUpdateStatusHandler)) mux.HandleFunc("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppsBatchUpdateStatusHandler)))
mux.HandleFunc("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(adminctl.AppResetSecretHandler)) mux.HandleFunc("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppResetSecretHandler)))
mux.HandleFunc("/admin/api/apps/get_app_data", adminctl.AdminAuthRequired(adminctl.AppGetAppDataHandler)) mux.HandleFunc("/admin/api/apps/get_app_data", adminctl.AdminAuthRequired(adminctl.AppGetAppDataHandler))
mux.HandleFunc("/admin/api/apps/update_app_data", adminctl.AdminAuthRequired(adminctl.AppUpdateAppDataHandler)) mux.HandleFunc("/admin/api/apps/update_app_data", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateAppDataHandler)))
mux.HandleFunc("/admin/api/apps/get_announcement", adminctl.AdminAuthRequired(adminctl.AppGetAnnouncementHandler)) mux.HandleFunc("/admin/api/apps/get_announcement", adminctl.AdminAuthRequired(adminctl.AppGetAnnouncementHandler))
mux.HandleFunc("/admin/api/apps/update_announcement", adminctl.AdminAuthRequired(adminctl.AppUpdateAnnouncementHandler)) mux.HandleFunc("/admin/api/apps/update_announcement", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateAnnouncementHandler)))
mux.HandleFunc("/admin/api/apps/get_multi_config", adminctl.AdminAuthRequired(adminctl.AppGetMultiConfigHandler)) mux.HandleFunc("/admin/api/apps/get_multi_config", adminctl.AdminAuthRequired(adminctl.AppGetMultiConfigHandler))
mux.HandleFunc("/admin/api/apps/update_multi_config", adminctl.AdminAuthRequired(adminctl.AppUpdateMultiConfigHandler)) mux.HandleFunc("/admin/api/apps/update_multi_config", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateMultiConfigHandler)))
mux.HandleFunc("/admin/api/apps/get_bind_config", adminctl.AdminAuthRequired(adminctl.AppGetBindConfigHandler)) mux.HandleFunc("/admin/api/apps/get_bind_config", adminctl.AdminAuthRequired(adminctl.AppGetBindConfigHandler))
mux.HandleFunc("/admin/api/apps/update_bind_config", adminctl.AdminAuthRequired(adminctl.AppUpdateBindConfigHandler)) mux.HandleFunc("/admin/api/apps/update_bind_config", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateBindConfigHandler)))
mux.HandleFunc("/admin/api/apps/get_register_config", adminctl.AdminAuthRequired(adminctl.AppGetRegisterConfigHandler)) mux.HandleFunc("/admin/api/apps/get_register_config", adminctl.AdminAuthRequired(adminctl.AppGetRegisterConfigHandler))
mux.HandleFunc("/admin/api/apps/update_register_config", adminctl.AdminAuthRequired(adminctl.AppUpdateRegisterConfigHandler)) mux.HandleFunc("/admin/api/apps/update_register_config", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateRegisterConfigHandler)))
// API接口管理API // API接口管理API
mux.HandleFunc("/admin/api/apis/list", adminctl.AdminAuthRequired(adminctl.APIListHandler)) mux.HandleFunc("/admin/api/apis/list", adminctl.AdminAuthRequired(adminctl.APIListHandler))
mux.HandleFunc("/admin/api/apis/update", adminctl.AdminAuthRequired(adminctl.APIUpdateHandler)) mux.HandleFunc("/admin/api/apis/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.APIUpdateHandler)))
mux.HandleFunc("/admin/api/apis/apps", adminctl.AdminAuthRequired(adminctl.APIGetAppsHandler)) mux.HandleFunc("/admin/api/apis/apps", adminctl.AdminAuthRequired(adminctl.APIGetAppsHandler))
mux.HandleFunc("/admin/api/apis/types", adminctl.AdminAuthRequired(adminctl.APIGetTypesHandler)) mux.HandleFunc("/admin/api/apis/types", adminctl.AdminAuthRequired(adminctl.APIGetTypesHandler))
mux.HandleFunc("/admin/api/apis/generate_keys", adminctl.AdminAuthRequired(adminctl.APIGenerateKeysHandler)) mux.HandleFunc("/admin/api/apis/generate_keys", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.APIGenerateKeysHandler)))
// 变量管理API // 变量管理API
mux.HandleFunc("/admin/variable/list", adminctl.AdminAuthRequired(adminctl.VariableListHandler)) mux.HandleFunc("/admin/variable/list", adminctl.AdminAuthRequired(adminctl.VariableListHandler))
mux.HandleFunc("/admin/variable/apps", adminctl.AdminAuthRequired(adminctl.VariableGetAppsHandler)) mux.HandleFunc("/admin/variable/apps", adminctl.AdminAuthRequired(adminctl.VariableGetAppsHandler))
mux.HandleFunc("/admin/variable/create", adminctl.AdminAuthRequired(adminctl.VariableCreateHandler)) mux.HandleFunc("/admin/variable/create", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableCreateHandler)))
mux.HandleFunc("/admin/variable/update", adminctl.AdminAuthRequired(adminctl.VariableUpdateHandler)) mux.HandleFunc("/admin/variable/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableUpdateHandler)))
mux.HandleFunc("/admin/variable/delete", adminctl.AdminAuthRequired(adminctl.VariableDeleteHandler)) mux.HandleFunc("/admin/variable/delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableDeleteHandler)))
mux.HandleFunc("/admin/variable/batch_delete", adminctl.AdminAuthRequired(adminctl.VariablesBatchDeleteHandler)) mux.HandleFunc("/admin/variable/batch_delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariablesBatchDeleteHandler)))
} }

View File

@@ -55,6 +55,25 @@ func GetDefaultTemplateData() map[string]interface{} {
} }
} }
// GetTemplateDataWithCSRF 获取包含CSRF令牌的模板数据
// 合并默认数据和CSRF令牌用于需要CSRF保护的页面
func GetTemplateDataWithCSRF(r *http.Request, additionalData map[string]interface{}) map[string]interface{} {
// 获取默认模板数据
data := GetDefaultTemplateData()
// 添加CSRF令牌
data["CSRFToken"] = GetCSRFTokenForTemplate(r)
// 合并额外数据
if additionalData != nil {
for key, value := range additionalData {
data[key] = value
}
}
return data
}
// GetClientIP 获取客户端IP地址 // GetClientIP 获取客户端IP地址
// 优先从 X-Forwarded-For 和 X-Real-IP 头部获取,否则使用 RemoteAddr // 优先从 X-Forwarded-For 和 X-Real-IP 头部获取,否则使用 RemoteAddr
func GetClientIP(r *http.Request) string { func GetClientIP(r *http.Request) string {

77
utils/cookie.go Normal file
View File

@@ -0,0 +1,77 @@
package utils
import (
"net/http"
"time"
"github.com/spf13/viper"
)
// CreateSecureCookie 创建安全的Cookie
// name: Cookie名称
// value: Cookie值
// maxAge: 过期时间0表示会话Cookie-1表示立即过期
func CreateSecureCookie(name, value string, maxAge int) *http.Cookie {
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
HttpOnly: true,
MaxAge: maxAge,
}
// 从配置读取安全设置
if viper.GetBool("security.cookie.secure") {
cookie.Secure = true
}
// 设置SameSite属性
sameSite := viper.GetString("security.cookie.same_site")
switch sameSite {
case "Strict":
cookie.SameSite = http.SameSiteStrictMode
case "Lax":
cookie.SameSite = http.SameSiteLaxMode
case "None":
cookie.SameSite = http.SameSiteNoneMode
// SameSite=None 必须配合 Secure=true 使用
cookie.Secure = true
default:
cookie.SameSite = http.SameSiteStrictMode
}
// 设置Domain如果配置了
domain := viper.GetString("security.cookie.domain")
if domain != "" {
cookie.Domain = domain
}
// 如果maxAge > 0设置Expires时间
if maxAge > 0 {
cookie.Expires = time.Now().Add(time.Duration(maxAge) * time.Second)
} else if maxAge == -1 {
// 立即过期
cookie.Expires = time.Unix(0, 0)
}
return cookie
}
// CreateSessionCookie 创建会话Cookie浏览器关闭时过期
func CreateSessionCookie(name, value string) *http.Cookie {
return CreateSecureCookie(name, value, 0)
}
// CreateExpiredCookie 创建立即过期的Cookie用于清理
func CreateExpiredCookie(name string) *http.Cookie {
return CreateSecureCookie(name, "", -1)
}
// GetDefaultCookieMaxAge 获取默认Cookie过期时间
func GetDefaultCookieMaxAge() int {
maxAge := viper.GetInt("security.cookie.max_age")
if maxAge <= 0 {
return 86400 // 默认24小时
}
return maxAge
}

View File

@@ -291,7 +291,8 @@ func DecryptStringWithSalt(enc, salt string) (string, error) {
} }
// HashPasswordWithSalt 使用盐值对密码进行哈希处理 // HashPasswordWithSalt 使用盐值对密码进行哈希处理
// 将密码和盐值组合后使用bcrypt进行哈希 // 将密码和盐值组合后先用SHA256处理使用bcrypt进行哈希
// 这样可以避免bcrypt的72字节限制问题
// password: 原始密码 // password: 原始密码
// salt: 密码盐值 // salt: 密码盐值
// 返回: bcrypt哈希值和错误信息 // 返回: bcrypt哈希值和错误信息
@@ -299,8 +300,12 @@ func HashPasswordWithSalt(password, salt string) (string, error) {
// 将密码和盐值组合 // 将密码和盐值组合
combined := password + salt combined := password + salt
// 先使用SHA256处理组合后的字符串确保长度固定且不超过bcrypt限制
hash := sha256.Sum256([]byte(combined))
sha256Hash := fmt.Sprintf("%x", hash) // 64字节的十六进制字符串
// 使用bcrypt进行哈希成本因子10平衡安全性和性能 // 使用bcrypt进行哈希成本因子10平衡安全性和性能
hashed, err := bcrypt.GenerateFromPassword([]byte(combined), 10) hashed, err := bcrypt.GenerateFromPassword([]byte(sha256Hash), 10)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -317,8 +322,12 @@ func VerifyPasswordWithSalt(password, salt, hashedPassword string) bool {
// 将密码和盐值组合 // 将密码和盐值组合
combined := password + salt combined := password + salt
// 先使用SHA256处理组合后的字符串与哈希生成逻辑保持一致
hash := sha256.Sum256([]byte(combined))
sha256Hash := fmt.Sprintf("%x", hash) // 64字节的十六进制字符串
// 使用bcrypt验证 // 使用bcrypt验证
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(combined)) err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(sha256Hash))
return err == nil return err == nil
} }

168
utils/csrf.go Normal file
View File

@@ -0,0 +1,168 @@
package utils
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"net/http"
)
const (
CSRFTokenLength = 32
CSRFCookieName = "csrf_token"
CSRFHeaderName = "X-CSRF-Token"
CSRFFormField = "csrf_token"
)
// generateRandomBytes 生成指定长度的随机字节
func generateRandomBytes(length int) ([]byte, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return nil, err
}
return bytes, nil
}
// GenerateCSRFToken 生成CSRF令牌
func GenerateCSRFToken() (string, error) {
bytes, err := generateRandomBytes(CSRFTokenLength)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
// SetCSRFToken 设置CSRF令牌到Cookie和响应头
func SetCSRFToken(w http.ResponseWriter, token string) {
// 设置CSRF令牌到Cookie
cookie := CreateSecureCookie(CSRFCookieName, token, 3600) // 1小时过期
http.SetCookie(w, cookie)
// 设置CSRF令牌到响应头方便JavaScript获取
w.Header().Set("X-CSRF-Token", token)
}
// GetCSRFTokenFromRequest 从请求中获取CSRF令牌
// 优先级Header > Form > Cookie
func GetCSRFTokenFromRequest(r *http.Request) string {
// 1. 从Header获取
if token := r.Header.Get(CSRFHeaderName); token != "" {
return token
}
// 2. 从Form获取
if token := r.FormValue(CSRFFormField); token != "" {
return token
}
// 3. 从Cookie获取作为备选
if cookie, err := r.Cookie(CSRFCookieName); err == nil {
return cookie.Value
}
return ""
}
// GetCSRFTokenFromCookie 从Cookie中获取CSRF令牌
func GetCSRFTokenFromCookie(r *http.Request) string {
cookie, err := r.Cookie(CSRFCookieName)
if err != nil {
return ""
}
return cookie.Value
}
// ValidateCSRFToken 验证CSRF令牌
func ValidateCSRFToken(r *http.Request) bool {
// 获取Cookie中的令牌服务器端存储的
cookieToken := GetCSRFTokenFromCookie(r)
if cookieToken == "" {
return false
}
// 获取请求中的令牌(客户端提交的)
requestToken := GetCSRFTokenFromRequest(r)
if requestToken == "" {
return false
}
// 使用常量时间比较防止时序攻击
return subtle.ConstantTimeCompare([]byte(cookieToken), []byte(requestToken)) == 1
}
// CSRFProtection CSRF保护中间件
func CSRFProtection(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 对于GET、HEAD、OPTIONS请求只生成令牌不验证
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
// 生成新的CSRF令牌
token, err := GenerateCSRFToken()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
SetCSRFToken(w, token)
next(w, r)
return
}
// 对于POST、PUT、DELETE等修改性请求验证CSRF令牌
if !ValidateCSRFToken(r) {
JsonResponse(w, http.StatusForbidden, false, "CSRF令牌验证失败", nil)
return
}
// 验证通过,继续处理请求
next(w, r)
}
}
// RequireCSRFToken 要求CSRF令牌的中间件用于特定路由
func RequireCSRFToken(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !ValidateCSRFToken(r) {
JsonResponse(w, http.StatusForbidden, false, "CSRF令牌验证失败", nil)
return
}
next(w, r)
}
}
// GetCSRFTokenForTemplate 获取用于模板的CSRF令牌
func GetCSRFTokenForTemplate(r *http.Request) string {
// 尝试从Cookie获取现有令牌
if token := GetCSRFTokenFromCookie(r); token != "" {
return token
}
// 如果没有现有令牌,生成新的(但不设置到响应中)
token, err := GenerateCSRFToken()
if err != nil {
return ""
}
return token
}
// CSRFTokenHandler 专门用于获取CSRF令牌的API端点
func CSRFTokenHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
JsonResponse(w, http.StatusMethodNotAllowed, false, "只支持GET请求", nil)
return
}
// 生成新的CSRF令牌
token, err := GenerateCSRFToken()
if err != nil {
JsonResponse(w, http.StatusInternalServerError, false, "生成CSRF令牌失败", nil)
return
}
// 设置令牌到Cookie和响应头
SetCSRFToken(w, token)
// 返回令牌给前端
JsonResponse(w, http.StatusOK, true, "CSRF令牌获取成功", map[string]interface{}{
"csrf_token": token,
})
}

View File

@@ -6,6 +6,63 @@ const rootPath = (function (src) {
return src.substring(0, src.lastIndexOf('/') + 1); return src.substring(0, src.lastIndexOf('/') + 1);
})(); })();
// CSRF令牌管理
const CSRFManager = {
// 缓存的CSRF令牌
token: null,
// 获取CSRF令牌
async getToken() {
if (this.token) {
return this.token;
}
try {
const response = await fetch('/admin/api/csrf-token', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (data.code === 0 && data.data && data.data.csrf_token) {
this.token = data.data.csrf_token;
return this.token;
}
}
} catch (error) {
console.error('获取CSRF令牌失败:', error);
}
return null;
},
// 清除缓存的令牌
clearToken() {
this.token = null;
},
// 为fetch请求添加CSRF令牌
async addCSRFHeader(headers = {}) {
const token = await this.getToken();
if (token) {
headers['X-CSRF-Token'] = token;
}
return headers;
}
};
// 增强的fetch函数自动添加CSRF令牌
window.fetchWithCSRF = async function(url, options = {}) {
const headers = await CSRFManager.addCSRFHeader(options.headers || {});
return fetch(url, {
...options,
headers
});
};
const app = document.querySelector('#app') const app = document.querySelector('#app')
addLink({ href: layuicss }).then(() => { addLink({ href: layuicss }).then(() => {
@@ -150,7 +207,7 @@ loadScript(layuijs, function () {
}); });
// 调用登出接口 // 调用登出接口
fetch('/admin/logout', { fetchWithCSRF('/admin/logout', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -38,59 +38,54 @@
</div> </div>
</div> </div>
</section> </section>
<script> <script>
// 仪表盘统计脚本(采用箭头函数与中文注释) // 仪表盘统计脚本(采用箭头函数与中文注释)
layui.use(['layer', 'util'], function(){ layui.use(['layer', 'util'], function () {
const layer = layui.layer; const layer = layui.layer;
const util = layui.util; const util = layui.util;
const $ = layui.$; const $ = layui.$;
// 全局引用ECharts CDN 地址 // 全局引用ECharts CDN 地址
const echartsCdn = 'https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js'; const echartsCdn = 'https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js';
// 工具函数:加载 ECharts 库(若已加载则直接回调) // 工具函数:加载 ECharts 库(若已加载则直接回调)
// 功能:通过全局的 loadScript 方法按需加载图表库,避免重复加载 // 功能:通过全局的 loadScript 方法按需加载图表库,避免重复加载
const ensureECharts = (cb) => { const ensureECharts = (cb) => {
if (window.echarts) { cb && cb(); return; } if (window.echarts) { cb && cb(); return; }
if (typeof loadScript === 'function') { if (typeof loadScript === 'function') {
loadScript(echartsCdn, () => cb && cb()); loadScript(echartsCdn, () => cb && cb());
} else { } else {
// 兜底:直接插入 <script> // 兜底:直接插入 <script>
const s = document.createElement('script'); const s = document.createElement('script');
s.src = echartsCdn; s.src = echartsCdn;
s.onload = () => cb && cb(); s.onload = () => cb && cb();
document.head.appendChild(s); document.head.appendChild(s);
}
};
// 函数:刷新基本信息和运行状态
// 说明:请求后台获取最新的系统信息并更新页面显示
const refreshSystemInfo = () => {
$.get('/admin/api/system/info', (res) => {
if (res && res.code === 0 && res.data) {
const data = res.data;
// 更新运行时长
if (data.uptime) {
$('.system-info-item').each(function() {
const label = $(this).find('.system-info-label').text();
if (label === '运行时长') {
$(this).find('.system-info-value').text(data.uptime);
}
});
}
} }
}).fail(() => { };
console.log('获取系统信息失败');
});
};
// 立即刷新一次系统信息 // 函数:刷新基本信息和运行状态
refreshSystemInfo(); // 说明:请求后台获取最新的系统信息并更新页面显示
}); const refreshSystemInfo = () => {
$.get('/admin/api/system/info', (res) => {
if (res && res.code === 0 && res.data) {
const data = res.data;
// 更新运行时长
if (data.uptime) {
$('.system-info-item').each(function () {
const label = $(this).find('.system-info-label').text();
if (label === '运行时长') {
$(this).find('.system-info-value').text(data.uptime);
}
});
}
}
}).fail(() => {
console.log('获取系统信息失败');
});
};
// 立即刷新一次系统信息
refreshSystemInfo();
});
</script> </script>
{{ end }} {{ end }}

View File

@@ -1,73 +1,75 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{{ .Title }} - {{ .SystemName }}</title>
<!-- 站点图标 -->
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/static/css/admin.css" />
<script type="module" src="./static/lib/include.js"></script>
</head>
<body> <head>
<div class="layui-layout layui-layout-admin" id="app"> <meta charset="utf-8" />
<div class="layui-header"> <meta name="viewport" content="width=device-width" />
<!-- 头部区域可配合layui 已有的水平导航) --> <title>{{ .Title }} - {{ .SystemName }}</title>
<ul class="layui-nav layui-layout-left"> <!-- 站点图标 -->
<!-- 移动端显示 --> <link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
<li class="layui-nav-item layui-show-xs-inline-block" lay-header-event="menuLeft"> <link rel="shortcut icon" href="/favicon.ico" />
<i class="layui-icon layui-icon-spread-left"></i> <link rel="stylesheet" href="/static/css/admin.css" />
</li> <script type="module" src="./static/lib/include.js"></script>
</ul> </head>
<ul class="layui-nav layui-layout-right">
<!-- 刷新页面按钮 --> <body>
<li class="layui-nav-item" lay-unselect> <div class="layui-layout layui-layout-admin" id="app">
<a href="javascript:;" id="refresh-btn" style="background-color: unset" title="刷新页面"> <div class="layui-header">
<i class="layui-icon layui-icon-refresh-3" style="font-size: 20px"></i> <!-- 头部区域可配合layui 已有的水平导航) -->
</a> <ul class="layui-nav layui-layout-left">
<!-- 移动端显示 -->
<li class="layui-nav-item layui-show-xs-inline-block" lay-header-event="menuLeft">
<i class="layui-icon layui-icon-spread-left"></i>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<!-- 刷新页面按钮 -->
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" id="refresh-btn" style="background-color: unset" title="刷新页面">
<i class="layui-icon layui-icon-refresh-3" style="font-size: 20px"></i>
</a>
</li>
<li class="layui-nav-item">
<i id="change-theme" class="layui-icon layui-icon-theme" style="font-size: 20px"></i>
</li>
<li class="layui-nav-item" lay-unselect>
<a href="javascript:;" id="logout-btn" style="background-color: unset" title="退出登录">
<i class="layui-icon layui-icon-logout" style="font-size: 20px"></i>
</a>
</li>
</ul>
</div>
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域 -->
<div class="layui-logo layui-bg-black logo-enhanced">{{ .SystemName }}</div>
<ul class="layui-nav layui-nav-tree" lay-shrink="all" lay-unselect lay-filter="nav-side" id="ws-nav-side">
<li class="layui-nav-item">
<a class="" href="javascript:;">系统管理</a>
<dl class="layui-nav-child">
<dd><a data-path="dashboard" href="javascript:;">仪表盘</a></dd>
<dd><a data-path="user" href="javascript:;">个人资料</a></dd>
<dd><a data-path="settings" href="javascript:;">系统设置</a></dd>
</dl>
</li> </li>
<li class="layui-nav-item"> <li class="layui-nav-item">
<i id="change-theme" class="layui-icon layui-icon-theme" style="font-size: 20px"></i> <a href="javascript:;">应用管理</a>
</li> <dl class="layui-nav-child">
<li class="layui-nav-item" lay-unselect> <dd><a data-path="apps" href="javascript:;">应用列表</a></dd>
<a href="javascript:;" id="logout-btn" style="background-color: unset" title="退出登录"> <dd><a data-path="apis" href="javascript:;">接口列表</a></dd>
<i class="layui-icon layui-icon-logout" style="font-size: 20px"></i> <dd><a data-path="variables" href="javascript:;">变量列表</a></dd>
</a> </dl>
</li> </li>
</ul> </ul>
</div> </div>
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域 -->
<div class="layui-logo layui-bg-black logo-enhanced">{{ .SystemName }}</div>
<ul class="layui-nav layui-nav-tree" lay-shrink="all" lay-unselect lay-filter="nav-side" id="ws-nav-side">
<li class="layui-nav-item">
<a class="" href="javascript:;">系统管理</a>
<dl class="layui-nav-child">
<dd><a data-path="dashboard" href="javascript:;">仪表盘</a></dd>
<dd><a data-path="user" href="javascript:;">个人资料</a></dd>
<dd><a data-path="settings" href="javascript:;">系统设置</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">应用管理</a>
<dl class="layui-nav-child">
<dd><a data-path="apps" href="javascript:;">应用列表</a></dd>
<dd><a data-path="apis" href="javascript:;">接口列表</a></dd>
<dd><a data-path="variables" href="javascript:;">变量列表</a></dd>
</dl>
</li>
</ul>
</div>
</div>
<div class="layui-body">
<!-- 内容主体区域 -->
<wc-include id="router-view" allow-scripts></wc-include>
</div>
<div class="layui-footer">{{ .FooterText }}</div>
</div> </div>
<script type="module" src="./static/js/admin.js"></script> <div class="layui-body">
</body> <!-- 内容主体区域 -->
<wc-include id="router-view" allow-scripts></wc-include>
</div>
<div class="layui-footer">{{ .FooterText }}</div>
</div>
<script type="module" src="./static/js/admin.js"></script>
</body>
</html> </html>

View File

@@ -1,6 +1,7 @@
{{/* 管理员登录页面模板使用layui构建的登录界面 */}} {{/* 管理员登录页面模板使用layui构建的登录界面 */}}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
@@ -21,6 +22,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.demo-login-container { .demo-login-container {
width: 400px; width: 400px;
margin: 21px auto 0; margin: 21px auto 0;
@@ -29,22 +31,26 @@
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
} }
.login-header { .login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px; padding: 30px 20px;
text-align: center; text-align: center;
color: #fff; color: #fff;
} }
.login-header h1 { .login-header h1 {
margin: 0; margin: 0;
font-size: 24px; font-size: 24px;
font-weight: 300; font-weight: 300;
} }
.login-header p { .login-header p {
margin: 8px 0 0; margin: 8px 0 0;
opacity: 0.8; opacity: 0.8;
font-size: 14px; font-size: 14px;
} }
.login-form { .login-form {
padding: 30px 20px; padding: 30px 20px;
} }
@@ -58,13 +64,15 @@
.login-form .layui-form-item:last-child { .login-form .layui-form-item:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.demo-login-other .layui-icon { .demo-login-other .layui-icon {
position: relative; position: relative;
display: inline-block; display: inline-block;
margin: 0 2px; margin: 0 2px;
top: 2px; top: 2px;
font-size: 26px; font-size: 26px;
} }
.login-footer { .login-footer {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
@@ -80,12 +88,15 @@
margin: 10px auto; margin: 10px auto;
border-radius: 4px; border-radius: 4px;
} }
.login-header { .login-header {
padding: 25px 15px; padding: 25px 15px;
} }
.login-header h1 { .login-header h1 {
font-size: 20px; font-size: 20px;
} }
.login-form { .login-form {
padding: 25px 15px; padding: 25px 15px;
} }
@@ -105,12 +116,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.login-header { .login-header {
padding: 20px 15px; padding: 20px 15px;
} }
.login-header h1 { .login-header h1 {
font-size: 18px; font-size: 18px;
} }
.login-form { .login-form {
padding: 20px 15px; padding: 20px 15px;
} }
@@ -126,6 +140,7 @@
} }
</style> </style>
</head> </head>
<body> <body>
<form class="layui-form"> <form class="layui-form">
<div class="demo-login-container"> <div class="demo-login-container">
@@ -135,12 +150,16 @@
</div> </div>
<div class="login-form"> <div class="login-form">
<!-- CSRF令牌隐藏字段 -->
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}" id="csrf-token">
<div class="layui-form-item"> <div class="layui-form-item">
<div class="layui-input-wrap"> <div class="layui-input-wrap">
<div class="layui-input-prefix"> <div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i> <i class="layui-icon layui-icon-username"></i>
</div> </div>
<input type="text" name="username" value="" lay-verify="required" placeholder="用户名" lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear"> <input type="text" name="username" value="" lay-verify="required" placeholder="用户名"
lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
</div> </div>
</div> </div>
@@ -149,7 +168,8 @@
<div class="layui-input-prefix"> <div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i> <i class="layui-icon layui-icon-password"></i>
</div> </div>
<input type="password" name="password" value="" lay-verify="required" placeholder="密 码" lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye"> <input type="password" name="password" value="" lay-verify="required" placeholder="密 码"
lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
</div> </div>
</div> </div>
@@ -160,12 +180,16 @@
<div class="layui-input-prefix"> <div class="layui-input-prefix">
<i class="layui-icon layui-icon-vercode"></i> <i class="layui-icon layui-icon-vercode"></i>
</div> </div>
<input type="text" name="captcha" value="" lay-verify="required" placeholder="验证码" lay-reqtext="请填写验证码" autocomplete="off" class="layui-input" lay-affix="clear"> <input type="text" name="captcha" value="" lay-verify="required" placeholder="验证码"
lay-reqtext="请填写验证码" autocomplete="off" class="layui-input" lay-affix="clear">
</div> </div>
</div> </div>
<div class="layui-col-xs5"> <div class="layui-col-xs5">
<div style="margin-left: 5px; text-align: right;"> <div style="margin-left: 5px; text-align: right;">
<img id="captcha-img" src="/admin/captcha" onclick="this.src='/admin/captcha?t='+ new Date().getTime();" style="cursor: pointer; height: 38px; border-radius: 4px; width: 100%;" title="点击刷新验证码"> <img id="captcha-img" src="/admin/captcha"
onclick="this.src='/admin/captcha?t='+ new Date().getTime();"
style="cursor: pointer; height: 38px; border-radius: 4px; width: 100%;"
title="点击刷新验证码">
</div> </div>
</div> </div>
</div> </div>
@@ -185,58 +209,63 @@
<!-- 请勿在项目正式环境中引用该 layui.js 地址 --> <!-- 请勿在项目正式环境中引用该 layui.js 地址 -->
<script src="//unpkg.com/layui@2.12.1/dist/layui.js"></script> <script src="//unpkg.com/layui@2.12.1/dist/layui.js"></script>
<script> <script>
layui.use(function(){ layui.use(function () {
var form = layui.form; var form = layui.form;
var layer = layui.layer; var layer = layui.layer;
// 登录提交回调:向 /admin/login 发送请求,并依据 code===0 判断成功与否 // 登录提交回调:向 /admin/login 发送请求,并依据 code===0 判断成功与否
form.on('submit(demo-login)', function(data){ form.on('submit(demo-login)', function (data) {
var loadIndex = layer.load(1, { var loadIndex = layer.load(1, {
shade: [0.1, '#fff'] shade: [0.1, '#fff']
}); });
// 获取CSRF令牌
var csrfToken = document.getElementById('csrf-token').value;
// 发送登录请求 // 发送登录请求
fetch('/admin/login', { fetch('/admin/login', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
}, },
body: JSON.stringify(data.field) body: JSON.stringify(data.field)
}) })
.then(response => response.json()) .then(response => response.json())
.then(result => { .then(result => {
layer.close(loadIndex); layer.close(loadIndex);
// 根据统一接口code === 0 表示成功 // 根据统一接口code === 0 表示成功
const isOk = result && result.code === 0; const isOk = result && result.code === 0;
if (isOk) { if (isOk) {
layer.msg('登录成功', { layer.msg('登录成功', {
icon: 1, icon: 1,
time: 1500 time: 1500
}, function(){ }, function () {
const redirect = (result.data && result.data.redirect) || '/admin'; const redirect = (result.data && result.data.redirect) || '/admin';
window.location.href = redirect; window.location.href = redirect;
}); });
} else { } else {
const msg = (result && (result.msg || result.message)) || '登录失败,请检查用户名和密码'; const msg = (result && (result.msg || result.message)) || '登录失败,请检查用户名和密码';
layer.msg(msg, {icon: 2}); layer.msg(msg, { icon: 2 });
// 登录失败时刷新验证码 // 登录失败时刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
}
})
.catch(error => {
layer.close(loadIndex);
console.error('登录错误:', error);
layer.msg('网络错误,请稍后重试', { icon: 2 });
// 网络错误时也刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime(); document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
} });
})
.catch(error => {
layer.close(loadIndex);
console.error('登录错误:', error);
layer.msg('网络错误,请稍后重试', {icon: 2});
// 网络错误时也刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
});
return false; // 阻止表单跳转 return false; // 阻止表单跳转
}); });
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,6 @@
{{ define "settings.html" }} {{ define "settings.html" }}
<section> <section>
<h2>系统设置</h2> <h2>系统设置</h2>
<!-- 基本信息设置 --> <!-- 基本信息设置 -->
<div class="layui-card" style="margin-top: 16px;"> <div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">基本信息设置</div> <div class="layui-card-header">基本信息设置</div>
@@ -61,7 +60,8 @@
<label class="layui-form-label" style="cursor: pointer;" data-tips="session-timeout">会话超时</label> <label class="layui-form-label" style="cursor: pointer;" data-tips="session-timeout">会话超时</label>
<div class="layui-input-block"> <div class="layui-input-block">
<div style="display: flex; align-items: center; gap: 10px;"> <div style="display: flex; align-items: center; gap: 10px;">
<input type="number" name="session_timeout" placeholder="3600" min="300" max="86400" class="layui-input" style="width: 120px;" /> <input type="number" name="session_timeout" placeholder="3600" min="300" max="86400" class="layui-input"
style="width: 120px;" />
<span class="layui-form-mid">300-86400秒</span> <span class="layui-form-mid">300-86400秒</span>
</div> </div>
</div> </div>
@@ -103,7 +103,8 @@
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="psb-record-link">备案链接</label> <label class="layui-form-label" style="cursor: pointer;" data-tips="psb-record-link">备案链接</label>
<div class="layui-input-block"> <div class="layui-input-block">
<input type="url" name="psb_record_link" placeholder="http://www.beian.gov.cn/portal/registerSystemInfo" class="layui-input" /> <input type="url" name="psb_record_link" placeholder="http://www.beian.gov.cn/portal/registerSystemInfo"
class="layui-input" />
</div> </div>
</div> </div>
</form> </form>
@@ -129,159 +130,159 @@
} }
} }
waitForLayui(function() { waitForLayui(function () {
layui.use(['jquery', 'form', 'layer', 'util'], function() { layui.use(['jquery', 'form', 'layer', 'util'], function () {
const { $, form, layer, util } = layui; const { $, form, layer, util } = layui;
// 缓存上次加载的设置值,用于“重置”恢复 // 缓存上次加载的设置值,用于“重置”恢复
let originalSettings = {}; let originalSettings = {};
/** /**
* 加载后台所有设置并回填到三个表单 * 加载后台所有设置并回填到三个表单
* - 从 /admin/api/settings 获取 name:value 映射 * - 从 /admin/api/settings 获取 name:value 映射
* - 处理开关型字段maintenance_mode * - 处理开关型字段maintenance_mode
* - 渲染 layui 组件 * - 渲染 layui 组件
*/ */
const loadSettings = async () => { const loadSettings = async () => {
try { try {
const res = await fetch('/admin/api/settings', { const res = await fetch('/admin/api/settings', {
method: 'GET', method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
if (data.code !== 0) {
layer.msg(data.msg || '加载设置失败', { icon: 2 });
return;
}
originalSettings = data.data || {};
fillForms(originalSettings);
} catch (err) {
console.error('获取设置失败:', err);
layer.msg('网络错误,无法加载设置', { icon: 2 });
}
};
/**
* 将 settings 数据回填到各表单控件
* - 文本/文本域/下拉:直接赋值
* - 开关:根据 "1"/"0" 置为选中/未选中
*/
const fillForms = (settings = {}) => {
// 基本信息
$('[name="site_title"]').val(settings.site_title || '');
$('[name="site_keywords"]').val(settings.site_keywords || '');
$('[name="site_description"]').val(settings.site_description || '');
$('[name="site_logo"]').val(settings.site_logo || '');
// 系统配置
const maintenanceChecked = (settings.maintenance_mode || '0') === '1';
$('[name="maintenance_mode"]').prop('checked', maintenanceChecked);
$('[name="default_user_role"]').val(settings.default_user_role || '1');
$('[name="session_timeout"]').val(settings.session_timeout || '3600');
// 页脚与备案
$('[name="footer_text"]').val(settings.footer_text || '');
$('[name="icp_record"]').val(settings.icp_record || '');
$('[name="icp_record_link"]').val(settings.icp_record_link || '');
$('[name="psb_record"]').val(settings.psb_record || '');
$('[name="psb_record_link"]').val(settings.psb_record_link || '');
// 渲染 layui 组件
form.render();
};
/**
* 收集某个表单下所有可用控件的值
* - 统一将 checkbox 转为 "1"/"0"
* - 其他控件转为字符串,避免后端类型不一致
*/
const collectForm = (selector) => {
const obj = {};
const $form = $(selector);
$form.find('input, textarea, select').each(function() {
const $el = $(this);
const name = $el.attr('name');
if (!name) return; // 无 name 不纳入
const type = ($el.attr('type') || '').toLowerCase();
let value = '';
if (type === 'checkbox') {
value = $el.prop('checked') ? '1' : '0';
} else {
value = ($el.val() ?? '').toString();
}
obj[name] = value;
});
return obj;
};
/**
* 汇总三个表单的字段为一个扁平对象
*/
const collectAllSettings = () => {
return {
...collectForm('#basicForm'),
...collectForm('#systemForm'),
...collectForm('#footerForm'),
};
};
/**
* 处理“保存所有设置”点击
* - 二次确认后提交
* - 显示加载中,防重复提交
* - 成功后提示并刷新缓存的 originalSettings
*/
const handleSaveAll = () => {
const payload = collectAllSettings();
layer.confirm('确认保存所有设置?', { icon: 3, title: '提示' }, (idx) => {
layer.close(idx);
const btn = $('#saveAllBtn');
btn.prop('disabled', true).addClass('layui-btn-disabled');
const loadIdx = layer.load(2, { content: '正在保存...' });
fetch('/admin/api/settings/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
})
.then(resp => resp.json())
.then(res => {
if (res.code === 0) {
layer.msg(res.msg || '保存成功', { icon: 1, time: 1000 });
originalSettings = { ...payload };
} else {
layer.msg(res.msg || '保存失败', { icon: 2 });
}
})
.catch(err => {
console.error('保存设置失败:', err);
layer.msg('网络错误,保存失败', { icon: 2 });
})
.finally(() => {
layer.close(loadIdx);
btn.prop('disabled', false).removeClass('layui-btn-disabled');
}); });
}); const data = await res.json();
}; if (data.code !== 0) {
layer.msg(data.msg || '加载设置失败', { icon: 2 });
return;
}
originalSettings = data.data || {};
fillForms(originalSettings);
} catch (err) {
console.error('获取设置失败:', err);
layer.msg('网络错误,无法加载设置', { icon: 2 });
}
};
/** /**
* 处理“重置”点击 * 将 settings 数据回填到各表单控件
* - 恢复为上次加载的 originalSettings * - 文本/文本域/下拉:直接赋值
*/ * - 开关:根据 "1"/"0" 置为选中/未选中
const handleReset = () => { */
fillForms(originalSettings); const fillForms = (settings = {}) => {
layer.msg('已恢复到上次加载的值', { icon: 1, time: 800 }); // 基本信息
}; $('[name="site_title"]').val(settings.site_title || '');
$('[name="site_keywords"]').val(settings.site_keywords || '');
$('[name="site_description"]').val(settings.site_description || '');
$('[name="site_logo"]').val(settings.site_logo || '');
// 事件绑定 // 系统配置
$('#saveAllBtn').off('click').on('click', handleSaveAll); const maintenanceChecked = (settings.maintenance_mode || '0') === '1';
$('#resetBtn').off('click').on('click', handleReset); $('[name="maintenance_mode"]').prop('checked', maintenanceChecked);
$('[name="default_user_role"]').val(settings.default_user_role || '1');
$('[name="session_timeout"]').val(settings.session_timeout || '3600');
// 初始化:加载设置 // 页脚与备案
loadSettings(); $('[name="footer_text"]').val(settings.footer_text || '');
$('[name="icp_record"]').val(settings.icp_record || '');
$('[name="icp_record_link"]').val(settings.icp_record_link || '');
$('[name="psb_record"]').val(settings.psb_record || '');
$('[name="psb_record_link"]').val(settings.psb_record_link || '');
// 渲染 layui 组件
form.render();
};
/**
* 收集某个表单下所有可用控件的值
* - 统一将 checkbox 转为 "1"/"0"
* - 其他控件转为字符串,避免后端类型不一致
*/
const collectForm = (selector) => {
const obj = {};
const $form = $(selector);
$form.find('input, textarea, select').each(function () {
const $el = $(this);
const name = $el.attr('name');
if (!name) return; // 无 name 不纳入
const type = ($el.attr('type') || '').toLowerCase();
let value = '';
if (type === 'checkbox') {
value = $el.prop('checked') ? '1' : '0';
} else {
value = ($el.val() ?? '').toString();
}
obj[name] = value;
});
return obj;
};
/**
* 汇总三个表单的字段为一个扁平对象
*/
const collectAllSettings = () => {
return {
...collectForm('#basicForm'),
...collectForm('#systemForm'),
...collectForm('#footerForm'),
};
};
/**
* 处理“保存所有设置”点击
* - 二次确认后提交
* - 显示加载中,防重复提交
* - 成功后提示并刷新缓存的 originalSettings
*/
const handleSaveAll = () => {
const payload = collectAllSettings();
layer.confirm('确认保存所有设置?', { icon: 3, title: '提示' }, (idx) => {
layer.close(idx);
const btn = $('#saveAllBtn');
btn.prop('disabled', true).addClass('layui-btn-disabled');
const loadIdx = layer.load(2, { content: '正在保存...' });
fetch('/admin/api/settings/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
})
.then(resp => resp.json())
.then(res => {
if (res.code === 0) {
layer.msg(res.msg || '保存成功', { icon: 1, time: 1000 });
originalSettings = { ...payload };
} else {
layer.msg(res.msg || '保存失败', { icon: 2 });
}
})
.catch(err => {
console.error('保存设置失败:', err);
layer.msg('网络错误,保存失败', { icon: 2 });
})
.finally(() => {
layer.close(loadIdx);
btn.prop('disabled', false).removeClass('layui-btn-disabled');
});
});
};
/**
* 处理“重置”点击
* - 恢复为上次加载的 originalSettings
*/
const handleReset = () => {
fillForms(originalSettings);
layer.msg('已恢复到上次加载的值', { icon: 1, time: 800 });
};
// 事件绑定
$('#saveAllBtn').off('click').on('click', handleSaveAll);
$('#resetBtn').off('click').on('click', handleReset);
// 初始化:加载设置
loadSettings();
}); });
}); });
</script> </script>

View File

@@ -1,472 +1,357 @@
{{ define "user.html" }} {{ define "user.html" }}
<style> <section>
/* 基础模块样式 */ <h2>个人资料</h2>
.user-module { <div class="layui-tab layui-tab-brief" lay-filter="userTabs" style="margin-top: 16px;">
margin-bottom: 20px; <ul class="layui-tab-title">
} <li class="layui-this">个人资料</li>
<li>修改密码</li>
.user-module .layui-card-header { <li>修改用户名</li>d
font-weight: 600; </ul>
transition: background-color 0.3s ease, color 0.3s ease; <div class="layui-tab-content">
} <!-- 个人资料模块 -->
<div class="layui-tab-item layui-show">
.module-tabs { <div class="layui-card" style="margin-top: 16px;">
margin-bottom: 20px; <div class="layui-card-header">个人资料</div>
} <div class="layui-card-body">
<form class="layui-form" id="profileForm" lay-filter="profileForm">
.module-tabs .layui-tab-title li { <div class="layui-form-item">
font-weight: 500; <label class="layui-form-label">UUID</label>
} <div class="layui-input-block">
<input type="text" name="uuid" disabled readonly class="layui-input readonly-field"
.readonly-field { style="font-family: monospace; font-size: 12px;" />
cursor: not-allowed !important; </div>
transition: background-color 0.3s ease, color 0.3s ease;
}
/* 浅色模式样式 */
:root {
--user-card-header-bg: #f8f9fa;
--user-card-header-color: #333;
--user-readonly-bg: #f5f5f5;
--user-readonly-color: #666;
--user-card-bg: #ffffff;
--user-card-border: #e6e6e6;
--user-input-bg: #ffffff;
--user-input-border: #d9d9d9;
--user-input-color: #333;
}
/* 深色模式样式 */
@media (prefers-color-scheme: dark) {
:root {
--user-card-header-bg: #2f2f2f;
--user-card-header-color: #e6e6e6;
--user-readonly-bg: #3a3a3a;
--user-readonly-color: #999;
--user-card-bg: #1f1f1f;
--user-card-border: #404040;
--user-input-bg: #2a2a2a;
--user-input-border: #404040;
--user-input-color: #e6e6e6;
}
}
/* 手动深色模式类 */
.dark {
--user-card-header-bg: #2f2f2f;
--user-card-header-color: #e6e6e6;
--user-readonly-bg: #3a3a3a;
--user-readonly-color: #999;
--user-card-bg: #1f1f1f;
--user-card-border: #404040;
--user-input-bg: #2a2a2a;
--user-input-border: #404040;
--user-input-color: #e6e6e6;
}
/* 应用CSS变量到元素 */
.user-module .layui-card-header {
background-color: var(--user-card-header-bg) !important;
color: var(--user-card-header-color) !important;
}
.readonly-field {
background-color: var(--user-readonly-bg) !important;
color: var(--user-readonly-color) !important;
}
.user-module .layui-card {
background-color: var(--user-card-bg);
border-color: var(--user-card-border);
}
.user-module .layui-input {
background-color: var(--user-input-bg);
border-color: var(--user-input-border);
color: var(--user-input-color);
}
/* 确保表单元素在深色模式下的可读性 */
.user-module .layui-form-label {
color: var(--user-card-header-color);
}
/* 按钮在深色模式下的样式调整 */
.user-module .layui-btn-primary {
background-color: var(--user-input-bg);
border-color: var(--user-input-border);
color: var(--user-input-color);
}
.user-module .layui-btn-primary:hover {
background-color: var(--user-readonly-bg);
}
/* 标签页在深色模式下的样式 */
.module-tabs .layui-tab-title {
border-bottom-color: var(--user-card-border);
}
.module-tabs .layui-tab-title li {
color: var(--user-input-color);
}
.module-tabs .layui-tab-title .layui-this {
color: var(--lay-color-primary, #1e9fff);
}
/* 图标颜色适配 */
.user-module .layui-icon {
color: var(--user-card-header-color);
}
</style>
<div class="layui-tab layui-tab-brief module-tabs" lay-filter="userTabs">
<ul class="layui-tab-title">
<li class="layui-this">个人资料</li>
<li>修改密码</li>
<li>修改用户名</li>
</ul>
<div class="layui-tab-content">
<!-- 个人资料模块 -->
<div class="layui-tab-item layui-show">
<div class="layui-card user-module">
<div class="layui-card-header">
<i class="layui-icon layui-icon-user"></i> 个人资料
</div>
<div class="layui-card-body">
<form class="layui-form" id="profileForm" lay-filter="profileForm">
<div class="layui-form-item">
<label class="layui-form-label">UUID</label>
<div class="layui-input-block">
<input type="text" name="uuid" disabled readonly class="layui-input readonly-field" style="font-family: monospace; font-size: 12px;" />
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <label class="layui-form-label">用户组</label>
<label class="layui-form-label">用户组</label> <div class="layui-input-block">
<div class="layui-input-block"> <input type="text" name="role" disabled readonly class="layui-input readonly-field" />
<input type="text" name="role" disabled readonly class="layui-input readonly-field" /> </div>
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <label class="layui-form-label">用户名</label>
<label class="layui-form-label">用户名</label> <div class="layui-input-block">
<div class="layui-input-block"> <input type="text" name="username" disabled readonly class="layui-input readonly-field" />
<input type="text" name="username" disabled readonly class="layui-input readonly-field" /> </div>
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <label class="layui-form-label">创建时间</label>
<label class="layui-form-label">创建时间</label> <div class="layui-input-block">
<div class="layui-input-block"> <input type="text" name="created_at" disabled readonly class="layui-input readonly-field" />
<input type="text" name="created_at" disabled readonly class="layui-input readonly-field" /> </div>
</div> </div>
</div> </form>
</form> </div>
</div> </div>
</div> </div>
</div>
<!-- 修改密码模块 --> <!-- 修改密码模块 -->
<div class="layui-tab-item"> <div class="layui-tab-item">
<div class="layui-card user-module"> <div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header"> <div class="layui-card-header">修改密码</div>
<i class="layui-icon layui-icon-password"></i> 修改密码 <div class="layui-card-body">
</div> <form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
<div class="layui-card-body"> <div class="layui-form-item">
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false"> <label class="layui-form-label">当前密码</label>
<div class="layui-form-item"> <div class="layui-input-block">
<label class="layui-form-label">当前密码</label> <input type="password" name="old_password" placeholder="请输入当前密码" autocomplete="off"
<div class="layui-input-block"> class="layui-input" lay-verify="required" />
<input type="password" name="old_password" placeholder="请输入当前密码" autocomplete="off" class="layui-input" lay-verify="required" /> </div>
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <label class="layui-form-label">新密码</label>
<label class="layui-form-label">新密码</label> <div class="layui-input-block">
<div class="layui-input-block"> <input type="password" name="new_password" placeholder="请输入新密码至少6位" autocomplete="off"
<input type="password" name="new_password" placeholder="请输入新密码至少6位" autocomplete="off" class="layui-input" lay-verify="required" /> class="layui-input" lay-verify="required" />
</div>
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <label class="layui-form-label">确认密码</label>
<label class="layui-form-label">确认密码</label> <div class="layui-input-block">
<div class="layui-input-block"> <input type="password" name="confirm_password" placeholder="请再次输入新密码" autocomplete="off"
<input type="password" name="confirm_password" placeholder="请再次输入新密码" autocomplete="off" class="layui-input" lay-verify="required" /> class="layui-input" lay-verify="required" />
</div>
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <div class="layui-input-block">
<div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="submitPassword">
<button class="layui-btn" lay-submit lay-filter="submitPassword"> <i class="layui-icon layui-icon-ok"></i> 修改密码
<i class="layui-icon layui-icon-ok"></i> 修改密码 </button>
</button> <button type="button" id="resetPasswordBtn" class="layui-btn layui-btn-primary">
<button type="button" id="resetPasswordBtn" class="layui-btn layui-btn-primary"> <i class="layui-icon layui-icon-refresh"></i> 重置
<i class="layui-icon layui-icon-refresh"></i> 重置 </button>
</button> </div>
</div> </div>
</div> </form>
</form> </div>
</div> </div>
</div> </div>
</div>
<!-- 修改用户名模块 --> <!-- 修改用户名模块 -->
<div class="layui-tab-item"> <div class="layui-tab-item">
<div class="layui-card user-module"> <div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header"> <div class="layui-card-header">修改用户名</div>
<i class="layui-icon layui-icon-edit"></i> 修改用户名 <div class="layui-card-body">
</div> <form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false">
<div class="layui-card-body"> <div class="layui-form-item">
<form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false"> <label class="layui-form-label">当前用户名</label>
<div class="layui-form-item"> <div class="layui-input-block">
<label class="layui-form-label">当前用户名</label> <input type="text" name="current_username" disabled readonly class="layui-input readonly-field" />
<div class="layui-input-block"> </div>
<input type="text" name="current_username" disabled readonly class="layui-input readonly-field" />
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <label class="layui-form-label">新用户名</label>
<label class="layui-form-label">新用户名</label> <div class="layui-input-block">
<div class="layui-input-block"> <input type="text" name="new_username" placeholder="请输入新用户名" autocomplete="off" class="layui-input"
<input type="text" name="new_username" placeholder="请输入新用户名" autocomplete="off" class="layui-input" lay-verify="required" /> lay-verify="required" />
</div>
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <label class="layui-form-label">当前密码</label>
<label class="layui-form-label">当前密码</label> <div class="layui-input-block">
<div class="layui-input-block"> <input type="password" name="password" placeholder="请输入当前密码以确认身份" autocomplete="off"
<input type="password" name="password" placeholder="请输入当前密码以确认身份" autocomplete="off" class="layui-input" lay-verify="required" /> class="layui-input" lay-verify="required" />
</div>
</div> </div>
</div> <div class="layui-form-item">
<div class="layui-form-item"> <div class="layui-input-block">
<div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="submitUsername">
<button class="layui-btn" lay-submit lay-filter="submitUsername"> <i class="layui-icon layui-icon-ok"></i> 修改用户名
<i class="layui-icon layui-icon-ok"></i> 修改用户名 </button>
</button> <button type="button" id="resetUsernameBtn" class="layui-btn layui-btn-primary">
<button type="button" id="resetUsernameBtn" class="layui-btn layui-btn-primary"> <i class="layui-icon layui-icon-refresh"></i> 重置
<i class="layui-icon layui-icon-refresh"></i> 重置 </button>
</button> </div>
</div> </div>
</div> </form>
</form> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<script> <script>
// 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突 // 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突
(() => { (() => {
// 工具方法:将数值角色转为中文标签 // 工具方法:将数值角色转为中文标签
const roleToText = (role) => { const roleToText = (role) => {
const r = typeof role === 'string' ? parseInt(role, 10) : role const r = typeof role === 'string' ? parseInt(role, 10) : role
return r === 0 ? '管理员' : '普通成员' return r === 0 ? '管理员' : '普通成员'
}
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return ''
const date = new Date(timeStr)
return date.toLocaleString('zh-CN')
}
// 如果未加载 layui则按需加载
const ensureLayui = () => new Promise((resolve) => {
if (window.layui) return resolve(window.layui)
const css = document.createElement('link')
css.rel = 'stylesheet'
css.href = 'https://unpkg.com/layui@2.10.1/dist/css/layui.css'
document.head.appendChild(css)
const script = document.createElement('script')
script.src = 'https://unpkg.com/layui@2.10.1/dist/layui.js'
script.onload = () => resolve(window.layui)
document.head.appendChild(script)
})
// 在确保 Layui 可用后再执行页面逻辑
ensureLayui().then(() => {
layui.use(['form', 'layer', 'element'], () => {
const form = layui.form
const layer = layui.layer
const element = layui.element
// 全局变量
let userProfile = null
// 加载个人资料
const loadProfile = async () => {
try {
const res = await fetch('/admin/api/user/profile')
const data = await res.json()
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '加载失败')
userProfile = data.data || {}
// 填充个人资料表单
const profileData = {
...userProfile,
role: roleToText(userProfile.role),
created_at: formatTime(userProfile.created_at)
}
form.val('profileForm', profileData)
// 填充用户名修改表单的当前用户名
form.val('usernameForm', { current_username: userProfile.username })
} catch (e) {
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
}
} }
// 修改密码模块 // 格式化时间
const PasswordModule = { const formatTime = (timeStr) => {
validate: (fields) => { if (!timeStr) return ''
const { old_password, new_password, confirm_password } = fields const date = new Date(timeStr)
return date.toLocaleString('zh-CN')
}
if (!old_password || !new_password || !confirm_password) { // 如果未加载 layui则按需加载
return { ok: false, msg: '请填写完整的密码信息' } const ensureLayui = () => new Promise((resolve) => {
} if (window.layui) return resolve(window.layui)
const css = document.createElement('link')
css.rel = 'stylesheet'
css.href = 'https://unpkg.com/layui@2.10.1/dist/css/layui.css'
document.head.appendChild(css)
const script = document.createElement('script')
script.src = 'https://unpkg.com/layui@2.10.1/dist/layui.js'
script.onload = () => resolve(window.layui)
document.head.appendChild(script)
})
if (new_password.length < 6) { // 在确保 Layui 可用后再执行页面逻辑
return { ok: false, msg: '新密码长度不能少于6位' } ensureLayui().then(() => {
} layui.use(['form', 'layer', 'element'], () => {
const form = layui.form
const layer = layui.layer
const element = layui.element
if (new_password !== confirm_password) { // 全局变量
return { ok: false, msg: '两次输入的新密码不一致' } let userProfile = null
}
if (old_password === new_password) { // 加载个人资料
return { ok: false, msg: '新密码不能与当前密码相同' } const loadProfile = async () => {
} try {
const res = await fetch('/admin/api/user/profile')
const data = await res.json()
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '加载失败')
return { ok: true } userProfile = data.data || {}
},
submit: async (fields) => { // 填充个人资料表单
const validation = PasswordModule.validate(fields) const profileData = {
if (!validation.ok) { ...userProfile,
layer.msg(validation.msg, { icon: 2 }) role: roleToText(userProfile.role),
return false created_at: formatTime(userProfile.created_at)
} }
form.val('profileForm', profileData)
try { // 填充用户名修改表单的当前用户名
const res = await fetch('/admin/api/user/password', { form.val('usernameForm', { current_username: userProfile.username })
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
old_password: fields.old_password,
new_password: fields.new_password,
confirm_password: fields.confirm_password
})
})
const data = await res.json() } catch (e) {
const ok = (data.success === true) || (data.code === 0) layer.msg(e.message || '加载个人资料失败', { icon: 2 })
if (!ok) throw new Error(data.message || data.msg || '修改密码失败')
// 检查是否需要跳转
if (data.data?.redirect) {
layer.msg('密码修改成功,即将跳转到登录页', { icon: 1, time: 1500 }, () => {
window.location.href = data.data.redirect
})
} else {
// 密码修改成功,不跳转,重置表单
layer.msg('密码修改成功', { icon: 1 })
document.getElementById('passwordForm').reset()
} }
} catch (e) {
layer.msg(e.message || '修改密码失败', { icon: 2 })
} }
return false // 修改密码模块
}, const PasswordModule = {
validate: (fields) => {
const { old_password, new_password, confirm_password } = fields
reset: () => { if (!old_password || !new_password || !confirm_password) {
document.getElementById('passwordForm').reset() return { ok: false, msg: '请填写完整的密码信息' }
layer.msg('表单已重置', { icon: 1 }) }
}
}
// 修改用户名模块 if (new_password.length < 6) {
const UsernameModule = { return { ok: false, msg: '新密码长度不能少于6位' }
validate: (fields) => { }
const { new_username, password } = fields
if (!new_username || !password) { if (new_password !== confirm_password) {
return { ok: false, msg: '请填写新用户名和当前密码' } return { ok: false, msg: '两次输入的新密码不一致' }
}
if (old_password === new_password) {
return { ok: false, msg: '新密码不能与当前密码相同' }
}
return { ok: true }
},
submit: async (fields) => {
const validation = PasswordModule.validate(fields)
if (!validation.ok) {
layer.msg(validation.msg, { icon: 2 })
return false
}
try {
const res = await fetch('/admin/api/user/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
old_password: fields.old_password,
new_password: fields.new_password,
confirm_password: fields.confirm_password
})
})
const data = await res.json()
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '修改密码失败')
// 检查是否需要跳转
if (data.data?.redirect) {
layer.msg('密码修改成功,即将跳转到登录页', { icon: 1, time: 1500 }, () => {
window.location.href = data.data.redirect
})
} else {
// 密码修改成功,不跳转,重置表单
layer.msg('密码修改成功', { icon: 1 })
document.getElementById('passwordForm').reset()
}
} catch (e) {
layer.msg(e.message || '修改密码失败', { icon: 2 })
}
return false
},
reset: () => {
document.getElementById('passwordForm').reset()
layer.msg('表单已重置', { icon: 1 })
}
} }
if (new_username === userProfile?.username) { // 修改用户名模块
return { ok: false, msg: '新用户名不能与当前用户名相同' } const UsernameModule = {
} validate: (fields) => {
const { new_username, password } = fields
if (new_username.length < 3) { if (!new_username || !password) {
return { ok: false, msg: '用户名长度不能少于3位' } return { ok: false, msg: '请填写新用户名和当前密码' }
} }
return { ok: true } if (new_username === userProfile?.username) {
}, return { ok: false, msg: '新用户名不能与当前用户名相同' }
}
submit: async (fields) => { if (new_username.length < 3) {
const validation = UsernameModule.validate(fields) return { ok: false, msg: '用户名长度不能少于3位' }
if (!validation.ok) { }
layer.msg(validation.msg, { icon: 2 })
return false
}
try { return { ok: true }
const res = await fetch('/admin/api/user/profile/update', { },
method: 'POST',
headers: { 'Content-Type': 'application/json' }, submit: async (fields) => {
body: JSON.stringify({ const validation = UsernameModule.validate(fields)
username: fields.new_username, if (!validation.ok) {
old_password: fields.password layer.msg(validation.msg, { icon: 2 })
return false
}
try {
const res = await fetch('/admin/api/user/profile/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: fields.new_username,
old_password: fields.password
})
})
const data = await res.json()
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '修改用户名失败')
layer.msg('用户名修改成功', { icon: 1 })
// 重新加载个人资料
await loadProfile()
// 清空表单(不显示重置提示)
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
})
} catch (e) {
layer.msg(e.message || '修改用户名失败', { icon: 2 })
}
return false
},
reset: () => {
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
}) })
}) layer.msg('表单已重置', { icon: 1 })
}
const data = await res.json()
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '修改用户名失败')
layer.msg('用户名修改成功', { icon: 1 })
// 重新加载个人资料
await loadProfile()
// 清空表单
UsernameModule.reset()
} catch (e) {
layer.msg(e.message || '修改用户名失败', { icon: 2 })
} }
return false // 绑定表单提交事件
}, form.on('submit(submitPassword)', (obj) => {
return PasswordModule.submit(obj.field)
reset: () => {
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
}) })
layer.msg('表单已重置', { icon: 1 })
}
}
// 绑定表单提交事件 form.on('submit(submitUsername)', (obj) => {
form.on('submit(submitPassword)', (obj) => { return UsernameModule.submit(obj.field)
return PasswordModule.submit(obj.field) })
// 绑定重置按钮
document.getElementById('resetPasswordBtn')?.addEventListener('click', PasswordModule.reset)
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
// 初始化加载
loadProfile()
})
}) })
})()
form.on('submit(submitUsername)', (obj) => { </script>
return UsernameModule.submit(obj.field) </section>
})
// 绑定重置按钮
document.getElementById('resetPasswordBtn')?.addEventListener('click', PasswordModule.reset)
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
// 初始化加载
loadProfile()
})
})
})()
</script>
{{ end }} {{ end }}

View File

@@ -3,7 +3,8 @@
<h2>变量管理</h2> <h2>变量管理</h2>
<div class="layui-btn-container" style="margin:12px 0"> <div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddVariable"><i class="layui-icon layui-icon-add-1"></i> 新增变量</button> <button class="layui-btn" id="btnAddVariable"><i class="layui-icon layui-icon-add-1"></i> 新增变量</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteVariables"><i class="layui-icon layui-icon-delete"></i> 批量删除</button> <button class="layui-btn layui-btn-danger" id="btnBatchDeleteVariables"><i class="layui-icon layui-icon-delete"></i>
批量删除</button>
</div> </div>
<div class="layui-card" style="margin-top:12px"> <div class="layui-card" style="margin-top:12px">
@@ -62,7 +63,8 @@
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">变量别名</label> <label class="layui-form-label">变量别名</label>
<div class="layui-input-block"> <div class="layui-input-block">
<input type="text" name="alias" lay-verify="required|alias" placeholder="请输入变量别名(英文开头,只能包含数字和英文字母)" autocomplete="off" class="layui-input" /> <input type="text" name="alias" lay-verify="required|alias" placeholder="请输入变量别名(英文开头,只能包含数字和英文字母)"
autocomplete="off" class="layui-input" />
</div> </div>
</div> </div>
<div class="layui-form-item"> <div class="layui-form-item">
@@ -90,47 +92,47 @@
} }
} }
waitForLayui(function() { waitForLayui(function () {
layui.use(['table', 'form', 'layer', 'element'], function() { layui.use(['table', 'form', 'layer', 'element'], function () {
const table = layui.table; const table = layui.table;
const form = layui.form; const form = layui.form;
const layer = layui.layer; const layer = layui.layer;
const $ = layui.$; const $ = layui.$;
// 自定义验证规则 // 自定义验证规则
form.verify({ form.verify({
alias: function(value) { alias: function (value) {
if (!value) return '别名不能为空'; if (!value) return '别名不能为空';
// 检查是否以英文字母开头,且只包含数字和英文字母 // 检查是否以英文字母开头,且只包含数字和英文字母
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) { if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) {
return '别名必须以英文字母开头,只能包含数字和英文字母'; return '别名必须以英文字母开头,只能包含数字和英文字母';
}
} }
} });
});
// 格式化时间函数 // 格式化时间函数
function formatDateTime(dateStr) { function formatDateTime(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';
return new Date(dateStr).toLocaleString(); return new Date(dateStr).toLocaleString();
} }
// 加载应用列表到下拉框 // 加载应用列表到下拉框
function loadApps() { function loadApps() {
$.ajax({ $.ajax({
url: '/admin/variable/apps', url: '/admin/variable/apps',
type: 'GET', type: 'GET',
success: function(res) { success: function (res) {
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
let options = '<option value="">请选择应用</option>'; let options = '<option value="">请选择应用</option>';
res.data.forEach(function(app) { res.data.forEach(function (app) {
options += '<option value="' + app.uuid + '">' + app.name + '</option>'; options += '<option value="' + app.uuid + '">' + app.name + '</option>';
}); });
$('select[name="app_uuid"]').html(options); $('select[name="app_uuid"]').html(options);
form.render('select'); form.render('select');
} }
}, },
error: function() { error: function () {
layer.msg('加载应用列表失败', {icon: 2}); layer.msg('加载应用列表失败', { icon: 2 });
} }
}); });
} }
@@ -143,7 +145,7 @@
elem: '#variablesTable', elem: '#variablesTable',
id: 'variablesTable', id: 'variablesTable',
url: '/admin/variable/list', url: '/admin/variable/list',
parseData: function(res) { parseData: function (res) {
return { return {
code: res.code, code: res.code,
msg: res.msg || '', msg: res.msg || '',
@@ -160,20 +162,20 @@
limit: 20, limit: 20,
limits: [10, 20, 50, 100], limits: [10, 20, 50, 100],
loading: true, loading: true,
done: function(res, curr, count) { done: function (res, curr, count) {
// 表格渲染完成后的回调 // 表格渲染完成后的回调
}, },
cols: [[ cols: [[
{type: 'checkbox', width: 50}, { type: 'checkbox', width: 50 },
{field: 'id', title: 'ID', width: 80, sort: true}, { field: 'id', title: 'ID', width: 80, sort: true },
{field: 'app_name', title: '应用名称', minWidth: 120}, { field: 'app_name', title: '应用名称', minWidth: 120 },
{field: 'number', title: '变量编号', width: 180}, { field: 'number', title: '变量编号', width: 180 },
{field: 'alias', title: '变量别名', minWidth: 150}, { field: 'alias', title: '变量别名', minWidth: 150 },
{ {
field: 'data', field: 'data',
title: '变量数据', title: '变量数据',
minWidth: 200, minWidth: 200,
templet: function(d) { templet: function (d) {
// 限制显示长度,避免内容过长影响布局 // 限制显示长度,避免内容过长影响布局
if (d.data && d.data.length > 50) { if (d.data && d.data.length > 50) {
return '<span title="' + d.data + '">' + d.data.substring(0, 50) + '...</span>'; return '<span title="' + d.data + '">' + d.data.substring(0, 50) + '...</span>';
@@ -185,7 +187,7 @@
field: 'remark', field: 'remark',
title: '备注', title: '备注',
minWidth: 150, minWidth: 150,
templet: function(d) { templet: function (d) {
// 限制显示长度,避免内容过长影响布局 // 限制显示长度,避免内容过长影响布局
if (d.remark && d.remark.length > 30) { if (d.remark && d.remark.length > 30) {
return '<span title="' + d.remark + '">' + d.remark.substring(0, 30) + '...</span>'; return '<span title="' + d.remark + '">' + d.remark.substring(0, 30) + '...</span>';
@@ -197,29 +199,29 @@
field: 'created_at', field: 'created_at',
title: '创建时间', title: '创建时间',
width: 180, width: 180,
templet: function(d) { templet: function (d) {
return formatDateTime(d.created_at); return formatDateTime(d.created_at);
} }
}, },
{title: '操作', width: 180, align: 'center', toolbar: '#tpl-variables-ops', fixed: 'right'} { title: '操作', width: 180, align: 'center', toolbar: '#tpl-variables-ops', fixed: 'right' }
]] ]]
}); });
// 监听应用选择变化 // 监听应用选择变化
form.on('select(appSelect)', function(data) { form.on('select(appSelect)', function (data) {
variablesTable.reload({ variablesTable.reload({
where: { where: {
app_uuid: data.value, app_uuid: data.value,
search: $('input[name="search"]').val() search: $('input[name="search"]').val()
}, },
page: { page: {
curr: 1 curr: 1
} }
}); });
}); });
// 搜索功能 // 搜索功能
$('#btnSearchVariables').on('click', function() { $('#btnSearchVariables').on('click', function () {
variablesTable.reload({ variablesTable.reload({
where: { where: {
app_uuid: $('select[name="app_uuid"]').val(), app_uuid: $('select[name="app_uuid"]').val(),
@@ -232,7 +234,7 @@
}); });
// 重置搜索 // 重置搜索
$('#btnResetVariables').on('click', function() { $('#btnResetVariables').on('click', function () {
$('#variableFilterForm')[0].reset(); $('#variableFilterForm')[0].reset();
form.render(); form.render();
variablesTable.reload({ variablesTable.reload({
@@ -244,7 +246,7 @@
}); });
// 新增变量 // 新增变量
$('#btnAddVariable').on('click', function() { $('#btnAddVariable').on('click', function () {
console.log('新增变量按钮被点击'); console.log('新增变量按钮被点击');
$('#variableForm')[0].reset(); $('#variableForm')[0].reset();
$('input[name="id"]').val(''); $('input[name="id"]').val('');
@@ -258,10 +260,10 @@
content: $('#variableFormLayer'), content: $('#variableFormLayer'),
area: ['500px', '460px'], area: ['500px', '460px'],
btn: ['创建', '取消'], btn: ['创建', '取消'],
yes: function(index, layero) { yes: function (index, layero) {
// 手动收集表单数据 // 手动收集表单数据
var formData = {}; var formData = {};
$('#variableForm').find('input, select, textarea').each(function() { $('#variableForm').find('input, select, textarea').each(function () {
var $this = $(this); var $this = $(this);
var name = $this.attr('name'); var name = $this.attr('name');
if (name && name !== 'id') { if (name && name !== 'id') {
@@ -273,15 +275,15 @@
// 验证必填字段 // 验证必填字段
if (!formData.app_uuid || formData.app_uuid.trim() === '') { if (!formData.app_uuid || formData.app_uuid.trim() === '') {
layer.msg('应用UUID不能为空', {icon: 2}); layer.msg('应用UUID不能为空', { icon: 2 });
return; return;
} }
if (!formData.alias || formData.alias.trim() === '') { if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入变量别名', {icon: 2}); layer.msg('请输入变量别名', { icon: 2 });
return; return;
} }
if (!formData.data || formData.data.trim() === '') { if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', {icon: 2}); layer.msg('请输入变量数据', { icon: 2 });
return; return;
} }
@@ -292,24 +294,24 @@
type: 'POST', type: 'POST',
data: JSON.stringify(formData), data: JSON.stringify(formData),
contentType: 'application/json', contentType: 'application/json',
success: function(res) { success: function (res) {
if (res.code === 0) { if (res.code === 0) {
layer.msg(res.msg, {icon: 1}); layer.msg(res.msg, { icon: 1 });
layer.close(index); layer.close(index);
variablesTable.reload(); variablesTable.reload();
} else { } else {
layer.msg(res.msg || '操作失败', {icon: 2}); layer.msg(res.msg || '操作失败', { icon: 2 });
} }
}, },
error: function(xhr) { error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', {icon: 2}); layer.msg(xhr.responseText || '操作失败', { icon: 2 });
} }
}); });
}, },
btn2: function(index) { btn2: function (index) {
layer.close(index); layer.close(index);
}, },
success: function() { success: function () {
form.render(); form.render();
}, },
shadeClose: false shadeClose: false
@@ -317,32 +319,32 @@
}); });
// 批量删除 // 批量删除
$('#btnBatchDeleteVariables').on('click', function() { $('#btnBatchDeleteVariables').on('click', function () {
const checkStatus = table.checkStatus('variablesTable'); const checkStatus = table.checkStatus('variablesTable');
const data = checkStatus.data; const data = checkStatus.data;
if (data.length === 0) { if (data.length === 0) {
layer.msg('请选择要删除的变量', {icon: 2}); layer.msg('请选择要删除的变量', { icon: 2 });
return; return;
} }
layer.confirm('确定删除选中的 ' + data.length + ' 个变量吗?', {icon: 3, title: '提示'}, function(index) { layer.confirm('确定删除选中的 ' + data.length + ' 个变量吗?', { icon: 3, title: '提示' }, function (index) {
const ids = data.map(item => item.id); const ids = data.map(item => item.id);
$.ajax({ $.ajax({
url: '/admin/variable/batch_delete', url: '/admin/variable/batch_delete',
type: 'POST', type: 'POST',
data: JSON.stringify({ids: ids}), data: JSON.stringify({ ids: ids }),
contentType: 'application/json', contentType: 'application/json',
success: function(res) { success: function (res) {
if (res.code === 0) { if (res.code === 0) {
layer.msg(res.msg, {icon: 1}); layer.msg(res.msg, { icon: 1 });
variablesTable.reload(); variablesTable.reload();
} else { } else {
layer.msg(res.msg || '批量删除失败', {icon: 2}); layer.msg(res.msg || '批量删除失败', { icon: 2 });
} }
}, },
error: function(xhr) { error: function (xhr) {
layer.msg(xhr.responseText || '批量删除失败', {icon: 2}); layer.msg(xhr.responseText || '批量删除失败', { icon: 2 });
} }
}); });
layer.close(index); layer.close(index);
@@ -350,7 +352,7 @@
}); });
// 表格工具栏事件 // 表格工具栏事件
table.on('tool(variablesTableFilter)', function(obj) { table.on('tool(variablesTableFilter)', function (obj) {
const data = obj.data; const data = obj.data;
if (obj.event === 'edit') { if (obj.event === 'edit') {
@@ -361,7 +363,7 @@
// 重新加载应用列表,然后设置选中值 // 重新加载应用列表,然后设置选中值
loadApps(); loadApps();
setTimeout(function() { setTimeout(function () {
$('select[name="app_uuid"]').val(data.app_uuid); $('select[name="app_uuid"]').val(data.app_uuid);
form.render('select'); form.render('select');
}, 100); }, 100);
@@ -376,10 +378,10 @@
content: $('#variableFormLayer'), content: $('#variableFormLayer'),
area: ['500px', '460px'], area: ['500px', '460px'],
btn: ['保存', '取消'], btn: ['保存', '取消'],
yes: function(index, layero) { yes: function (index, layero) {
// 手动收集表单数据 // 手动收集表单数据
var formData = {}; var formData = {};
$('#variableForm').find('input, select, textarea').each(function() { $('#variableForm').find('input, select, textarea').each(function () {
var $this = $(this); var $this = $(this);
var name = $this.attr('name'); var name = $this.attr('name');
if (name && name !== 'id') { if (name && name !== 'id') {
@@ -391,15 +393,15 @@
// 验证必填字段 // 验证必填字段
if (!formData.app_uuid || formData.app_uuid.trim() === '') { if (!formData.app_uuid || formData.app_uuid.trim() === '') {
layer.msg('应用UUID不能为空', {icon: 2}); layer.msg('应用UUID不能为空', { icon: 2 });
return; return;
} }
if (!formData.alias || formData.alias.trim() === '') { if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入变量别名', {icon: 2}); layer.msg('请输入变量别名', { icon: 2 });
return; return;
} }
if (!formData.data || formData.data.trim() === '') { if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', {icon: 2}); layer.msg('请输入变量数据', { icon: 2 });
return; return;
} }
@@ -410,24 +412,24 @@
type: 'POST', type: 'POST',
data: JSON.stringify(formData), data: JSON.stringify(formData),
contentType: 'application/json', contentType: 'application/json',
success: function(res) { success: function (res) {
if (res.code === 0) { if (res.code === 0) {
layer.msg(res.msg, {icon: 1}); layer.msg(res.msg, { icon: 1 });
layer.close(index); layer.close(index);
variablesTable.reload(); variablesTable.reload();
} else { } else {
layer.msg(res.msg || '操作失败', {icon: 2}); layer.msg(res.msg || '操作失败', { icon: 2 });
} }
}, },
error: function(xhr) { error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', {icon: 2}); layer.msg(xhr.responseText || '操作失败', { icon: 2 });
} }
}); });
}, },
btn2: function(index) { btn2: function (index) {
layer.close(index); layer.close(index);
}, },
success: function() { success: function () {
form.render(); form.render();
}, },
shadeClose: false shadeClose: false
@@ -435,22 +437,22 @@
} else if (obj.event === 'del') { } else if (obj.event === 'del') {
// 删除 // 删除
layer.confirm('确定删除该变量吗?', {icon: 3, title: '提示'}, function(index) { layer.confirm('确定删除该变量吗?', { icon: 3, title: '提示' }, function (index) {
$.ajax({ $.ajax({
url: '/admin/variable/delete', url: '/admin/variable/delete',
type: 'POST', type: 'POST',
data: JSON.stringify({id: data.id}), data: JSON.stringify({ id: data.id }),
contentType: 'application/json', contentType: 'application/json',
success: function(res) { success: function (res) {
if (res.code === 0) { if (res.code === 0) {
layer.msg(res.msg, {icon: 1}); layer.msg(res.msg, { icon: 1 });
variablesTable.reload(); variablesTable.reload();
} else { } else {
layer.msg(res.msg || '删除失败', {icon: 2}); layer.msg(res.msg || '删除失败', { icon: 2 });
} }
}, },
error: function(xhr) { error: function (xhr) {
layer.msg(xhr.responseText || '删除失败', {icon: 2}); layer.msg(xhr.responseText || '删除失败', { icon: 2 });
} }
}); });
layer.close(index); layer.close(index);

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-cn"> <html lang="zh-cn">
<head> <head>
<title>{{.SystemName}} - 生活就像愤怒的小鸟,失败后总有几只猪在笑。</title> <title>{{.SystemName}} - 生活就像愤怒的小鸟,失败后总有几只猪在笑。</title>
<!-- 站 点 协 议 --> <!-- 站 点 协 议 -->
@@ -17,9 +18,10 @@
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="bookmark" href="/favicon.ico" /> <link rel="bookmark" href="/favicon.ico" />
<!-- 样 式 文 件 --> <!-- 样 式 文 件 -->
<link rel="stylesheet" href="//lib.baomitu.com/layui/2.8.17/css/layui.css"/> <link rel="stylesheet" href="//lib.baomitu.com/layui/2.8.17/css/layui.css" />
<style> <style>
html, body { html,
body {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
@@ -69,6 +71,7 @@
from { from {
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5); text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
} }
to { to {
text-shadow: 0 0 30px rgba(0, 212, 255, 0.8), 0 0 40px rgba(0, 212, 255, 0.6); text-shadow: 0 0 30px rgba(0, 212, 255, 0.8), 0 0 40px rgba(0, 212, 255, 0.6);
} }
@@ -100,8 +103,13 @@
} }
@keyframes shimmer { @keyframes shimmer {
0% { left: -100%; } 0% {
100% { left: 100%; } left: -100%;
}
100% {
left: 100%;
}
} }
.box-form .layui-form-item { .box-form .layui-form-item {
@@ -119,8 +127,15 @@
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; } 0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
} }
.info-text { .info-text {
@@ -174,193 +189,197 @@
} }
</style> </style>
</head> </head>
<body> <body>
<!-- 代 码 结 构 --> <!-- 代 码 结 构 -->
<div class="layui-container"> <div class="layui-container">
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<div class="body-background body_box"> <div class="body-background body_box">
<div class="layui-form box-form body_box"> <div class="layui-form box-form body_box">
<div class="layui-form-item logo-title"> <div class="layui-form-item logo-title">
<h1><strong>系统提醒</strong></h1> <h1><strong>系统提醒</strong></h1>
</div> </div>
<hr> <hr>
<div class="layui-form-item"> <div class="layui-form-item">
<div class="warning-text">🚫 未授权,拒绝访问</div> <div class="warning-text">🚫 未授权,拒绝访问</div>
</div> </div>
<div class="layui-form-item"> <div class="layui-form-item">
<div class="info-text">💬 如有问题,请联系网站管理员</div> <div class="info-text">💬 如有问题,请联系网站管理员</div>
</div>
</div> </div>
<div class="body_footer">{{.FooterText}}</div>
{{if or .ICPRecord .PSBRecord}}<div class="body_beian">{{if .ICPRecord}}<a href="{{.ICPRecordLink}}"
target="_blank">{{.ICPRecord}}</a>{{end}}{{if and .ICPRecord .PSBRecord}} {{end}}{{if .PSBRecord}}<a
href="{{.PSBRecordLink}}" target="_blank">{{.PSBRecord}}</a>{{end}}</div>{{end}}
</div> </div>
<div class="body_footer">{{.FooterText}}</div>
{{if or .ICPRecord .PSBRecord}}<div class="body_beian">{{if .ICPRecord}}<a href="{{.ICPRecordLink}}" target="_blank">{{.ICPRecord}}</a>{{end}}{{if and .ICPRecord .PSBRecord}} {{end}}{{if .PSBRecord}}<a href="{{.PSBRecordLink}}" target="_blank">{{.PSBRecord}}</a>{{end}}</div>{{end}}
</div> </div>
</div> <!-- 资 源 引 入 -->
<!-- 资 源 引 入 --> <script src="//lib.baomitu.com/jquery/3.6.4/jquery.min.js" type="text/javascript"></script>
<script src="//lib.baomitu.com/jquery/3.6.4/jquery.min.js" type="text/javascript"></script>
<script> <script>
// 获取canvas元素和绘图上下文 // 获取canvas元素和绘图上下文
const canvas = document.getElementById('canvas'); const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
// 设置canvas尺寸为全屏 // 设置canvas尺寸为全屏
const resizeCanvas = () => { const resizeCanvas = () => {
canvas.width = window.innerWidth; canvas.width = window.innerWidth;
canvas.height = window.innerHeight; canvas.height = window.innerHeight;
}; };
resizeCanvas(); resizeCanvas();
window.addEventListener('resize', resizeCanvas); window.addEventListener('resize', resizeCanvas);
// 粒子类 // 粒子类
class Particle { class Particle {
constructor() { constructor() {
this.reset();
}
// 重置粒子位置和属性
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 2;
this.vy = (Math.random() - 0.5) * 2;
this.size = Math.random() * 3 + 1;
this.opacity = Math.random() * 0.8 + 0.2;
this.color = this.getRandomColor();
}
// 获取随机颜色
getRandomColor() {
const colors = [
'#00FF00', '#0080FF', '#FF0080', '#FFFF00',
'#FF8000', '#8000FF', '#00FFFF', '#FF4000'
];
return colors[Math.floor(Math.random() * colors.length)];
}
// 更新粒子位置
update() {
this.x += this.vx;
this.y += this.vy;
// 边界检测,粒子超出边界时重置
if (this.x < 0 || this.x > canvas.width ||
this.y < 0 || this.y > canvas.height) {
this.reset(); this.reset();
} }
// 随机改变透明度 // 重置粒子位置和属性
this.opacity += (Math.random() - 0.5) * 0.02; reset() {
this.opacity = Math.max(0.1, Math.min(1, this.opacity)); this.x = Math.random() * canvas.width;
} this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 2;
this.vy = (Math.random() - 0.5) * 2;
this.size = Math.random() * 3 + 1;
this.opacity = Math.random() * 0.8 + 0.2;
this.color = this.getRandomColor();
}
// 绘制粒子 // 获取随机颜色
draw() { getRandomColor() {
ctx.save(); const colors = [
ctx.globalAlpha = this.opacity; '#00FF00', '#0080FF', '#FF0080', '#FFFF00',
ctx.fillStyle = this.color; '#FF8000', '#8000FF', '#00FFFF', '#FF4000'
ctx.beginPath(); ];
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); return colors[Math.floor(Math.random() * colors.length)];
ctx.fill(); }
ctx.restore();
}
}
// 创建粒子数组 // 更新粒子位置
const particles = []; update() {
const particleCount = 150; this.x += this.vx;
this.y += this.vy;
// 初始化粒子 // 边界检测,粒子超出边界时重置
const initParticles = () => { if (this.x < 0 || this.x > canvas.width ||
for (let i = 0; i < particleCount; i++) { this.y < 0 || this.y > canvas.height) {
particles.push(new Particle()); this.reset();
}
};
// 绘制连线
const drawConnections = () => {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 如果距离小于100像素绘制连线
if (distance < 100) {
ctx.save();
ctx.globalAlpha = (100 - distance) / 100 * 0.3;
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
ctx.restore();
} }
// 随机改变透明度
this.opacity += (Math.random() - 0.5) * 0.02;
this.opacity = Math.max(0.1, Math.min(1, this.opacity));
}
// 绘制粒子
draw() {
ctx.save();
ctx.globalAlpha = this.opacity;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
} }
} }
};
// 动画循环 // 创建粒子数组
const animate = () => { const particles = [];
// 清除画布,使用半透明黑色创建拖尾效果 const particleCount = 150;
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 更新和绘制所有粒子 // 初始化粒子
particles.forEach(particle => { const initParticles = () => {
particle.update(); for (let i = 0; i < particleCount; i++) {
particle.draw(); particles.push(new Particle());
}); }
};
// 绘制粒子间的连线 // 绘制连线
drawConnections(); const drawConnections = () => {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
requestAnimationFrame(animate); // 如果距离小于100像素绘制连线
}; if (distance < 100) {
ctx.save();
ctx.globalAlpha = (100 - distance) / 100 * 0.3;
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
ctx.restore();
}
}
}
};
// 鼠标交互效果 // 动画循环
const addMouseInteraction = () => { const animate = () => {
let mouseX = 0; // 清除画布,使用半透明黑色创建拖尾效果
let mouseY = 0; ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
canvas.addEventListener('mousemove', (e) => { // 更新和绘制所有粒子
mouseX = e.clientX;
mouseY = e.clientY;
// 鼠标附近的粒子会被吸引
particles.forEach(particle => { particles.forEach(particle => {
const dx = mouseX - particle.x; particle.update();
const dy = mouseY - particle.y; particle.draw();
const distance = Math.sqrt(dx * dx + dy * dy); });
if (distance < 150) { // 绘制粒子间的连线
particle.vx += dx * 0.0001; drawConnections();
particle.vy += dy * 0.0001;
requestAnimationFrame(animate);
};
// 鼠标交互效果
const addMouseInteraction = () => {
let mouseX = 0;
let mouseY = 0;
canvas.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
// 鼠标附近的粒子会被吸引
particles.forEach(particle => {
const dx = mouseX - particle.x;
const dy = mouseY - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 150) {
particle.vx += dx * 0.0001;
particle.vy += dy * 0.0001;
}
});
});
// 点击时添加新粒子
canvas.addEventListener('click', (e) => {
for (let i = 0; i < 5; i++) {
const newParticle = new Particle();
newParticle.x = e.clientX + (Math.random() - 0.5) * 20;
newParticle.y = e.clientY + (Math.random() - 0.5) * 20;
particles.push(newParticle);
}
// 限制粒子数量
if (particles.length > particleCount + 50) {
particles.splice(0, 5);
} }
}); });
}); };
// 点击时添加新粒子 // 启动粒子系统
canvas.addEventListener('click', (e) => { initParticles();
for (let i = 0; i < 5; i++) { addMouseInteraction();
const newParticle = new Particle(); animate();
newParticle.x = e.clientX + (Math.random() - 0.5) * 20; </script>
newParticle.y = e.clientY + (Math.random() - 0.5) * 20;
particles.push(newParticle);
}
// 限制粒子数量
if (particles.length > particleCount + 50) {
particles.splice(0, 5);
}
});
};
// 启动粒子系统
initParticles();
addMouseInteraction();
animate();
</script>
</body> </body>
</html> </html>