diff --git a/config/config.json b/config/config.json index a0b4059..b87c00f 100644 --- a/config/config.json +++ b/config/config.json @@ -37,6 +37,12 @@ "security": { "jwt_secret": "your-jwt-secret-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 + } } } \ No newline at end of file diff --git a/controllers/admin/auth.go b/controllers/admin/auth.go index 088e1b2..fa3a055 100644 --- a/controllers/admin/auth.go +++ b/controllers/admin/auth.go @@ -26,8 +26,33 @@ func LoginPageHandler(w http.ResponseWriter, r *http.Request) { 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["Title"] = "管理员登录" + data["CSRFToken"] = token + + // 合并额外数据 + for key, value := range extraData { + data[key] = value + } utils.RenderTemplate(w, "login.html", data) } @@ -97,15 +122,8 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { return } - // 设置JWT Cookie(HttpOnly,安全) - cookie := &http.Cookie{ - Name: "admin_session", - Value: token, - Path: "/", - HttpOnly: true, - Secure: false, // 生产环境应设置为true(HTTPS) - MaxAge: 24 * 60 * 60, // 24小时 - } + // 设置JWT Cookie(使用安全配置) + cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge()) http.SetCookie(w, cookie) utils.JsonResponse(w, http.StatusOK, true, "登录成功", map[string]interface{}{ @@ -132,15 +150,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { // - 统一的Cookie清理函数,确保一致性 // - 在JWT校验失败时自动调用,提升安全性和用户体验 func clearInvalidJWTCookie(w http.ResponseWriter) { - cookie := &http.Cookie{ - Name: "admin_session", - Value: "", - Path: "/", - HttpOnly: true, - Secure: false, // 生产环境应设置为true - MaxAge: -1, // 立即失效 - Expires: time.Unix(0, 0), // 确保过期 - } + cookie := utils.CreateExpiredCookie("admin_session") http.SetCookie(w, cookie) } @@ -434,15 +444,8 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW } newToken, err := generateJWTToken(user) if err == nil { - // 更新Cookie - newCookie := &http.Cookie{ - Name: "admin_session", - Value: newToken, - Path: "/", - HttpOnly: true, - Secure: false, // 生产环境应设置为true(HTTPS) - MaxAge: 24 * 60 * 60, // 24小时 - } + // 更新Cookie(使用安全配置) + newCookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge()) http.SetCookie(w, newCookie) refreshed = true diff --git a/controllers/admin/captcha.go b/controllers/admin/captcha.go index dc738b0..6c7d471 100644 --- a/controllers/admin/captcha.go +++ b/controllers/admin/captcha.go @@ -7,7 +7,8 @@ import ( "math/big" "net/http" "strings" - "time" + + "networkDev/utils" "github.com/mojocn/base64Captcha" ) @@ -62,15 +63,7 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) { // 将验证码ID存储到session中(这里简化处理,实际项目中应该使用更安全的方式) // 设置cookie来存储验证码ID - cookie := &http.Cookie{ - Name: "captcha_id", - Value: id, - Path: "/", - HttpOnly: true, - Secure: false, // 生产环境应设置为true - MaxAge: 300, // 5分钟过期 - Expires: time.Now().Add(5 * time.Minute), - } + cookie := utils.CreateSecureCookie("captcha_id", id, 300) // 5分钟过期 http.SetCookie(w, cookie) // 解码base64图片数据并返回 diff --git a/controllers/admin/handlers.go b/controllers/admin/handlers.go index 314b270..a5dc7e3 100644 --- a/controllers/admin/handlers.go +++ b/controllers/admin/handlers.go @@ -26,21 +26,47 @@ func AdminIndexHandler(w http.ResponseWriter, r *http.Request) { // AdminLayoutHandler 后台布局页渲染 // - 渲染 layout.html,包含顶部导航、侧边栏与动态内容容器 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() - if err != nil { - data["Title"] = "凌动技术" + db, dbErr := database.GetDB() + if dbErr != nil { + extraData["Title"] = "凌动技术" } else { - siteTitle, err := services.FindSettingByName("site_title", db) - if err != nil || siteTitle == nil { - data["Title"] = "凌动技术" + siteTitle, settingErr := services.FindSettingByName("site_title", db) + if settingErr != nil || siteTitle == nil { + extraData["Title"] = "凌动技术" } 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) } diff --git a/controllers/admin/user.go b/controllers/admin/user.go index 57f30f7..56ba7ef 100644 --- a/controllers/admin/user.go +++ b/controllers/admin/user.go @@ -160,15 +160,8 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - // 更新Cookie - cookie := &http.Cookie{ - Name: "admin_session", - Value: newToken, - Path: "/", - HttpOnly: true, - Secure: false, // 生产环境应设置为true(HTTPS) - MaxAge: 24 * 60 * 60, // 24小时 - } + // 更新Cookie(使用安全配置) + cookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge()) http.SetCookie(w, cookie) // 密码修改成功,已重新生成JWT令牌 @@ -260,20 +253,14 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) { } // 重新签发JWT并写入Cookie - newUser := models.User{UUID: claims.UserUUID, Username: username, Role: claims.Role} - token, err := generateJWTToken(newUser) + // 使用完整的用户信息(包含密码)来生成JWT令牌 + user.Username = username // 更新用户名 + token, err := generateJWTToken(user) if err != nil { utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil) return } - cookie := &http.Cookie{ - Name: "admin_session", - Value: token, - Path: "/", - HttpOnly: true, - Secure: false, - MaxAge: 24 * 60 * 60, - } + cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge()) http.SetCookie(w, cookie) utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{ diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..73a80a5 --- /dev/null +++ b/cookies.txt @@ -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= diff --git a/database/seed_user.go b/database/seed_user.go index d387e74..d991e35 100644 --- a/database/seed_user.go +++ b/database/seed_user.go @@ -8,21 +8,22 @@ import ( ) // SeedDefaultAdmin 初始化默认管理员账号 -// - 如果用户名为 admin 的用户已存在,则跳过 +// - 如果已存在任何管理员用户(role=0),则跳过 // - 如不存在,则创建用户名为 admin、密码为 admin123(以 bcrypt 哈希存储)、角色 Role=0 的管理员 -// - ID和UUID将自动生成 +// - 根据需求:默认 admin 用户的 ID 固定为 10000 func SeedDefaultAdmin() error { db, err := GetDB() if err != nil { return err } - // 检查是否存在用户名为 admin 的用户 + // 检查是否存在任何管理员用户(role=0) 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 } if count > 0 { + logrus.Info("已存在管理员用户,跳过默认管理员创建") return nil } diff --git a/server/admin.go b/server/admin.go index 3e3e9ea..dc8c871 100644 --- a/server/admin.go +++ b/server/admin.go @@ -3,6 +3,7 @@ package server import ( "net/http" adminctl "networkDev/controllers/admin" + "networkDev/utils" ) // RegisterAdminRoutes 注册管理员后台相关路由 @@ -23,7 +24,8 @@ func RegisterAdminRoutes(mux *http.ServeMux) { return } if r.Method == http.MethodPost { - adminctl.LoginHandler(w, r) + // 应用CSRF保护 + utils.RequireCSRFToken(adminctl.LoginHandler)(w, r) return } w.WriteHeader(http.StatusMethodNotAllowed) @@ -35,6 +37,9 @@ func RegisterAdminRoutes(mux *http.ServeMux) { // 验证码生成路由(无需认证) mux.HandleFunc("/admin/captcha", adminctl.CaptchaHandler) + // CSRF令牌获取API(无需认证,但需要在登录页面等地方获取) + mux.HandleFunc("/admin/api/csrf-token", utils.CSRFTokenHandler) + // 后台布局页(需要管理员认证) mux.HandleFunc("/admin/layout", adminctl.AdminAuthRequired(adminctl.AdminLayoutHandler)) @@ -51,44 +56,44 @@ func RegisterAdminRoutes(mux *http.ServeMux) { // 个人资料API 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/password", adminctl.AdminAuthRequired(adminctl.UserPasswordUpdateHandler)) + mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler))) + mux.HandleFunc("/admin/api/user/password", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserPasswordUpdateHandler))) // 系统设置API 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 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/update", adminctl.AdminAuthRequired(adminctl.AppUpdateHandler)) - mux.HandleFunc("/admin/api/apps/delete", adminctl.AdminAuthRequired(adminctl.AppDeleteHandler)) - mux.HandleFunc("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(adminctl.AppsBatchDeleteHandler)) - mux.HandleFunc("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(adminctl.AppsBatchUpdateStatusHandler)) - mux.HandleFunc("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(adminctl.AppResetSecretHandler)) + mux.HandleFunc("/admin/api/apps/create", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppCreateHandler))) + mux.HandleFunc("/admin/api/apps/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateHandler))) + mux.HandleFunc("/admin/api/apps/delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppDeleteHandler))) + mux.HandleFunc("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppsBatchDeleteHandler))) + mux.HandleFunc("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppsBatchUpdateStatusHandler))) + 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/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/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/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/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/update_register_config", adminctl.AdminAuthRequired(adminctl.AppUpdateRegisterConfigHandler)) + mux.HandleFunc("/admin/api/apps/update_register_config", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateRegisterConfigHandler))) // API接口管理API 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/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 mux.HandleFunc("/admin/variable/list", adminctl.AdminAuthRequired(adminctl.VariableListHandler)) mux.HandleFunc("/admin/variable/apps", adminctl.AdminAuthRequired(adminctl.VariableGetAppsHandler)) - mux.HandleFunc("/admin/variable/create", adminctl.AdminAuthRequired(adminctl.VariableCreateHandler)) - mux.HandleFunc("/admin/variable/update", adminctl.AdminAuthRequired(adminctl.VariableUpdateHandler)) - mux.HandleFunc("/admin/variable/delete", adminctl.AdminAuthRequired(adminctl.VariableDeleteHandler)) - mux.HandleFunc("/admin/variable/batch_delete", adminctl.AdminAuthRequired(adminctl.VariablesBatchDeleteHandler)) + mux.HandleFunc("/admin/variable/create", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableCreateHandler))) + mux.HandleFunc("/admin/variable/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableUpdateHandler))) + mux.HandleFunc("/admin/variable/delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableDeleteHandler))) + mux.HandleFunc("/admin/variable/batch_delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariablesBatchDeleteHandler))) } diff --git a/utils/common.go b/utils/common.go index 02be805..5f93771 100644 --- a/utils/common.go +++ b/utils/common.go @@ -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地址 // 优先从 X-Forwarded-For 和 X-Real-IP 头部获取,否则使用 RemoteAddr func GetClientIP(r *http.Request) string { diff --git a/utils/cookie.go b/utils/cookie.go new file mode 100644 index 0000000..92090f7 --- /dev/null +++ b/utils/cookie.go @@ -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 +} \ No newline at end of file diff --git a/utils/crypto.go b/utils/crypto.go index 71fec79..01ba823 100644 --- a/utils/crypto.go +++ b/utils/crypto.go @@ -291,7 +291,8 @@ func DecryptStringWithSalt(enc, salt string) (string, error) { } // HashPasswordWithSalt 使用盐值对密码进行哈希处理 -// 将密码和盐值组合后使用bcrypt进行哈希 +// 将密码和盐值组合后先用SHA256处理,再使用bcrypt进行哈希 +// 这样可以避免bcrypt的72字节限制问题 // password: 原始密码 // salt: 密码盐值 // 返回: bcrypt哈希值和错误信息 @@ -299,8 +300,12 @@ func HashPasswordWithSalt(password, salt string) (string, error) { // 将密码和盐值组合 combined := password + salt + // 先使用SHA256处理组合后的字符串,确保长度固定且不超过bcrypt限制 + hash := sha256.Sum256([]byte(combined)) + sha256Hash := fmt.Sprintf("%x", hash) // 64字节的十六进制字符串 + // 使用bcrypt进行哈希(成本因子10,平衡安全性和性能) - hashed, err := bcrypt.GenerateFromPassword([]byte(combined), 10) + hashed, err := bcrypt.GenerateFromPassword([]byte(sha256Hash), 10) if err != nil { return "", err } @@ -317,8 +322,12 @@ func VerifyPasswordWithSalt(password, salt, hashedPassword string) bool { // 将密码和盐值组合 combined := password + salt + // 先使用SHA256处理组合后的字符串,与哈希生成逻辑保持一致 + hash := sha256.Sum256([]byte(combined)) + sha256Hash := fmt.Sprintf("%x", hash) // 64字节的十六进制字符串 + // 使用bcrypt验证 - err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(combined)) + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(sha256Hash)) return err == nil } diff --git a/utils/csrf.go b/utils/csrf.go new file mode 100644 index 0000000..55c7aba --- /dev/null +++ b/utils/csrf.go @@ -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, + }) +} \ No newline at end of file diff --git a/web/static/js/admin.js b/web/static/js/admin.js index cc94518..4809871 100755 --- a/web/static/js/admin.js +++ b/web/static/js/admin.js @@ -6,6 +6,63 @@ const rootPath = (function (src) { 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') addLink({ href: layuicss }).then(() => { @@ -150,7 +207,7 @@ loadScript(layuijs, function () { }); // 调用登出接口 - fetch('/admin/logout', { + fetchWithCSRF('/admin/logout', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/web/template/admin/apis.html b/web/template/admin/apis.html index fbdc290..5bcb8a3 100644 --- a/web/template/admin/apis.html +++ b/web/template/admin/apis.html @@ -1,10 +1,8 @@ {{ define "apis.html" }}
-

接口管理

- - +

接口管理

d
-
筛选
+
筛选
d
@@ -12,7 +10,7 @@
@@ -73,14 +71,18 @@
@@ -105,14 +107,18 @@
@@ -133,494 +139,494 @@
{{ end }} \ No newline at end of file diff --git a/web/template/admin/apps.html b/web/template/admin/apps.html index fef3db4..4f58b86 100644 --- a/web/template/admin/apps.html +++ b/web/template/admin/apps.html @@ -3,9 +3,12 @@

应用管理

- - - + + +
@@ -58,7 +61,8 @@
- +
@@ -119,7 +123,8 @@
- +
小时
@@ -128,7 +133,8 @@
- +
分钟
@@ -136,7 +142,8 @@
- +
@@ -173,22 +180,25 @@
- +
- +
- +
- +
IP地址验证设置 @@ -231,7 +241,8 @@
- +
@@ -268,10 +279,11 @@
- +
- +
领取试用设置 @@ -293,7 +305,8 @@
- +
@@ -309,209 +322,117 @@ } } - waitForLayui(function() { - layui.use(['table', 'form', 'layer', 'element', 'dropdown', 'util'], function() { - const table = layui.table; - const form = layui.form; - const layer = layui.layer; - const dropdown = layui.dropdown; - const util = layui.util; - const $ = layui.$; + waitForLayui(function () { + layui.use(['table', 'form', 'layer', 'element', 'dropdown', 'util'], function () { + const table = layui.table; + const form = layui.form; + const layer = layui.layer; + const dropdown = layui.dropdown; + const util = layui.util; + const $ = layui.$; - // 格式化时间函数 - function formatDateTime(dateStr) { - if (!dateStr) return '-'; - return new Date(dateStr).toLocaleString(); - } + // 格式化时间函数 + function formatDateTime(dateStr) { + if (!dateStr) return '-'; + return new Date(dateStr).toLocaleString(); + } - // 渲染表格 - const appsTable = table.render({ - elem: '#appsTable', - id: 'appsTable', - url: '/admin/api/apps/list', - parseData: function(res) { - // 后端返回的数据结构处理 - return { - code: res.code, - msg: res.msg || '', - count: res.count || 0, - data: res.data || [] - }; - }, - request: { - pageName: 'page', // 页码的参数名称,默认:page - limitName: 'page_size' // 每页数据量的参数名称,默认:limit - }, - method: 'GET', - page: true, - limit: 20, - limits: [10, 20, 50, 100], - loading: true, - done: function(res, curr, count) { - // 表格渲染完成后的回调 - }, - cols: [[ - { type: 'checkbox', width: 50 }, - { field: 'id', title: 'ID', width: 80, sort: true }, - { field: 'name', title: '应用名称', minWidth: 180 }, - { field: 'uuid', title: 'UUID', minWidth: 335 }, - { field: 'version', title: '应用版本', width: 100 }, - { - field: 'status', - title: '应用状态', - width: 100, - templet: (d) => { - if (d.status === 1) return '启用'; - return '禁用'; - } + // 渲染表格 + const appsTable = table.render({ + elem: '#appsTable', + id: 'appsTable', + url: '/admin/api/apps/list', + parseData: function (res) { + // 后端返回的数据结构处理 + return { + code: res.code, + msg: res.msg || '', + count: res.count || 0, + data: res.data || [] + }; }, - { - field: 'secret', - title: '密钥', - minWidth: 320, - templet: (d) => '' + d.secret + '' + request: { + pageName: 'page', // 页码的参数名称,默认:page + limitName: 'page_size' // 每页数据量的参数名称,默认:limit }, - { - field: 'created_at', - title: '创建时间', - width: 180, - templet: (d) => formatDateTime(d.created_at) + method: 'GET', + page: true, + limit: 20, + limits: [10, 20, 50, 100], + loading: true, + done: function (res, curr, count) { + // 表格渲染完成后的回调 }, - { fixed: 'right', title: '操作', toolbar: '#tpl-apps-ops', width: 180 } - ]] - }); - - // 搜索功能 - $('#btnSearchApps').on('click', function() { - const search = $('input[name="search"]').val(); - appsTable.reload({ - where: { - search: search - }, - page: { - curr: 1 - } - }); - }); - - // 重置搜索 - $('#btnResetApps').on('click', function() { - $('#appFilterForm')[0].reset(); - appsTable.reload({ - where: {}, - page: { - curr: 1 - } - }); - }); - - // 新增应用 - $('#btnAddApp').on('click', function() { - $('#appForm')[0].reset(); - $('input[name="id"]').val(''); - - layer.open({ - type: 1, - title: '新增应用', - content: $('#appFormModal'), - area: ['500px', '460px'], - btn: ['创建', '取消'], - yes: function(index, layero) { - // 手动触发表单提交验证 - var formData = {}; - $('#appForm').find('input, select, textarea').each(function() { - var $this = $(this); - var name = $this.attr('name'); - if (name) { - if ($this.attr('type') === 'checkbox') { - if ($this.attr('lay-skin') === 'switch') { - formData[name] = $this.prop('checked') ? 1 : 0; - } else { - formData[name] = $this.prop('checked') ? $this.val() : ''; - } - } else if ($this.attr('type') === 'radio') { - if ($this.prop('checked')) { - formData[name] = $this.val(); - } - } else { - formData[name] = $this.val(); - } + cols: [[ + { type: 'checkbox', width: 50 }, + { field: 'id', title: 'ID', width: 80, sort: true }, + { field: 'name', title: '应用名称', minWidth: 180 }, + { field: 'uuid', title: 'UUID', minWidth: 335 }, + { field: 'version', title: '应用版本', width: 100 }, + { + field: 'status', + title: '应用状态', + width: 100, + templet: (d) => { + if (d.status === 1) return '启用'; + return '禁用'; } - }); - - // 验证必填字段 - if (!formData.name || formData.name.trim() === '') { - layer.msg('请输入应用名称', {icon: 2}); - return; - } - - // 处理数据类型转换 - formData.download_type = parseInt(formData.download_type) || 0; - - $.ajax({ - url: '/admin/api/apps/create', - type: 'POST', - data: JSON.stringify(formData), - contentType: 'application/json', - success: function(res) { - if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); - layer.close(index); - appsTable.reload(); - } else { - layer.msg(res.msg || '操作失败', {icon: 2}); - } - }, - error: function(xhr) { - layer.msg(xhr.responseText || '操作失败', {icon: 2}); - } - }); - }, - btn2: function(index) { - layer.close(index); - }, - success: function() { - form.render(); - }, - shadeClose: false + }, + { + field: 'secret', + title: '密钥', + minWidth: 320, + templet: (d) => '' + d.secret + '' + }, + { + field: 'created_at', + title: '创建时间', + width: 180, + templet: (d) => formatDateTime(d.created_at) + }, + { fixed: 'right', title: '操作', toolbar: '#tpl-apps-ops', width: 180 } + ]] }); - }); - // 监听更新方式切换(保留事件监听器以备将来扩展) - form.on('radio(downloadTypeChange)', function(data) { - // 下载地址字段现在始终显示,无需切换显示状态 - }); + // 搜索功能 + $('#btnSearchApps').on('click', function () { + const search = $('input[name="search"]').val(); + appsTable.reload({ + where: { + search: search + }, + page: { + curr: 1 + } + }); + }); + // 重置搜索 + $('#btnResetApps').on('click', function () { + $('#appFilterForm')[0].reset(); + appsTable.reload({ + where: {}, + page: { + curr: 1 + } + }); + }); - - // 表格工具栏事件 - table.on('tool(appsTableFilter)', function(obj) { - const data = obj.data; - - if (obj.event === 'edit') { - // 编辑 + // 新增应用 + $('#btnAddApp').on('click', function () { $('#appForm')[0].reset(); - $('input[name="id"]').val(data.id); - $('input[name="name"]').val(data.name); - $('input[name="version"]').val(data.version); - // 设置应用状态开关 - $('input[name="status"]').prop('checked', data.status === 1); - // 设置更新方式单选按钮 - $('input[name="download_type"][value="' + (data.download_type || 0) + '"]').prop('checked', true); - $('input[name="download_url"]').val(data.download_url || ''); - // 设置强制更新开关 - $('input[name="force_update"]').prop('checked', data.force_update === 1); - + $('input[name="id"]').val(''); + layer.open({ type: 1, - title: '编辑应用', + title: '新增应用', content: $('#appFormModal'), area: ['500px', '460px'], - btn: ['保存', '取消'], - yes: function(index, layero) { + btn: ['创建', '取消'], + yes: function (index, layero) { // 手动触发表单提交验证 var formData = {}; - $('#appForm').find('input, select, textarea').each(function() { + $('#appForm').find('input, select, textarea').each(function () { var $this = $(this); var name = $this.attr('name'); if (name) { @@ -530,644 +451,736 @@ } } }); - + // 验证必填字段 if (!formData.name || formData.name.trim() === '') { - layer.msg('请输入应用名称', {icon: 2}); + layer.msg('请输入应用名称', { icon: 2 }); return; } - + // 处理数据类型转换 formData.download_type = parseInt(formData.download_type) || 0; - formData.id = parseInt(formData.id); - + $.ajax({ - url: '/admin/api/apps/update', + url: '/admin/api/apps/create', type: 'POST', data: JSON.stringify(formData), contentType: 'application/json', - success: function(res) { + success: function (res) { if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); + layer.msg(res.msg, { icon: 1 }); layer.close(index); appsTable.reload(); } else { - layer.msg(res.msg || '操作失败', {icon: 2}); + layer.msg(res.msg || '操作失败', { icon: 2 }); } }, - error: function(xhr) { - layer.msg(xhr.responseText || '操作失败', {icon: 2}); + error: function (xhr) { + layer.msg(xhr.responseText || '操作失败', { icon: 2 }); } }); }, - btn2: function(index) { + btn2: function (index) { layer.close(index); }, - success: function() { + success: function () { form.render(); }, shadeClose: false }); - - } else if (obj.event === 'del') { - // 删除 - layer.confirm('确定删除该应用吗?', {icon: 3, title: '提示'}, function(index) { - $.ajax({ - url: '/admin/api/apps/delete', - type: 'POST', - data: JSON.stringify({id: data.id}), - contentType: 'application/json', - success: function(res) { - if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); - appsTable.reload(); - } else { - layer.msg(res.msg || '删除失败', {icon: 2}); + }); + + // 监听更新方式切换(保留事件监听器以备将来扩展) + form.on('radio(downloadTypeChange)', function (data) { + // 下载地址字段现在始终显示,无需切换显示状态 + }); + + + + // 表格工具栏事件 + table.on('tool(appsTableFilter)', function (obj) { + const data = obj.data; + + if (obj.event === 'edit') { + // 编辑 + $('#appForm')[0].reset(); + $('input[name="id"]').val(data.id); + $('input[name="name"]').val(data.name); + $('input[name="version"]').val(data.version); + // 设置应用状态开关 + $('input[name="status"]').prop('checked', data.status === 1); + // 设置更新方式单选按钮 + $('input[name="download_type"][value="' + (data.download_type || 0) + '"]').prop('checked', true); + $('input[name="download_url"]').val(data.download_url || ''); + // 设置强制更新开关 + $('input[name="force_update"]').prop('checked', data.force_update === 1); + + layer.open({ + type: 1, + title: '编辑应用', + content: $('#appFormModal'), + area: ['500px', '460px'], + btn: ['保存', '取消'], + yes: function (index, layero) { + // 手动触发表单提交验证 + var formData = {}; + $('#appForm').find('input, select, textarea').each(function () { + var $this = $(this); + var name = $this.attr('name'); + if (name) { + if ($this.attr('type') === 'checkbox') { + if ($this.attr('lay-skin') === 'switch') { + formData[name] = $this.prop('checked') ? 1 : 0; + } else { + formData[name] = $this.prop('checked') ? $this.val() : ''; + } + } else if ($this.attr('type') === 'radio') { + if ($this.prop('checked')) { + formData[name] = $this.val(); + } + } else { + formData[name] = $this.val(); + } + } + }); + + // 验证必填字段 + if (!formData.name || formData.name.trim() === '') { + layer.msg('请输入应用名称', { icon: 2 }); + return; + } + + // 处理数据类型转换 + formData.download_type = parseInt(formData.download_type) || 0; + formData.id = parseInt(formData.id); + + $.ajax({ + url: '/admin/api/apps/update', + type: 'POST', + data: JSON.stringify(formData), + contentType: 'application/json', + success: function (res) { + if (res.code === 0) { + layer.msg(res.msg, { icon: 1 }); + layer.close(index); + appsTable.reload(); + } else { + layer.msg(res.msg || '操作失败', { icon: 2 }); + } + }, + error: function (xhr) { + layer.msg(xhr.responseText || '操作失败', { icon: 2 }); + } + }); + }, + btn2: function (index) { + layer.close(index); + }, + success: function () { + form.render(); + }, + shadeClose: false + }); + + } else if (obj.event === 'del') { + // 删除 + layer.confirm('确定删除该应用吗?', { icon: 3, title: '提示' }, function (index) { + $.ajax({ + url: '/admin/api/apps/delete', + type: 'POST', + data: JSON.stringify({ id: data.id }), + contentType: 'application/json', + success: function (res) { + if (res.code === 0) { + layer.msg(res.msg, { icon: 1 }); + appsTable.reload(); + } else { + layer.msg(res.msg || '删除失败', { icon: 2 }); + } + }, + error: function (xhr) { + layer.msg(xhr.responseText || '删除失败', { icon: 2 }); + } + }); + layer.close(index); + }); + } else if (obj.event === 'more') { + // 更多操作下拉菜单 + dropdown.render({ + elem: this, // 使用 this 而不是查找元素 + show: true, // 外部事件触发即显示 + data: [ + { + title: '应用数据', + id: 'app_data' + }, + { + title: '程序公告', + id: 'announcement' + }, + { + title: '多开配置', + id: 'multi_instance' + }, + { + title: '绑定设置', + id: 'bind_settings' + }, + { + title: '注册设置', + id: 'register_settings' + }, + { + title: '重置密钥', + id: 'reset_secret' + } + ], + click: function (menudata, othis) { + if (menudata.id === 'app_data') { + // 应用数据 + // 先获取当前应用数据内容 + $.ajax({ + url: '/admin/api/apps/get_app_data?uuid=' + obj.data.uuid, + type: 'GET', + success: function (res) { + var currentAppData = ''; + if (res.code === 0 && res.data && res.data.app_data) { + currentAppData = res.data.app_data; + } + + // 显示编辑弹窗 + layer.open({ + type: 1, + title: '编辑应用数据 - ' + obj.data.name, + area: ['600px', '400px'], + content: '
' + + '' + + '
', + btn: ['保存', '取消'], + yes: function (index, layero) { + var appDataContent = $('#appDataEditor').val(); + + // 发送更新请求 + $.ajax({ + url: '/admin/api/apps/update_app_data', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + uuid: obj.data.uuid, + app_data: appDataContent + }), + success: function (res) { + if (res.code === 0) { + layer.msg('应用数据更新成功!', { + icon: 1, + time: 2000 + }); + layer.close(index); + } else { + layer.msg(res.msg || '更新应用数据失败', { icon: 2 }); + } + }, + error: function () { + layer.msg('网络错误,请稍后重试', { icon: 2 }); + } + }); + }, + btn2: function (index) { + layer.close(index); + } + }); + }, + error: function () { + layer.msg('获取应用数据失败,请稍后重试', { icon: 2 }); + } + }); + } else if (menudata.id === 'announcement') { + // 程序公告 + // 先获取当前公告内容 + $.ajax({ + url: '/admin/api/apps/get_announcement?uuid=' + obj.data.uuid, + type: 'GET', + success: function (res) { + var currentAnnouncement = ''; + if (res.code === 0 && res.data && res.data.announcement) { + currentAnnouncement = res.data.announcement; + } + + // 显示编辑弹窗 + layer.open({ + type: 1, + title: '编辑程序公告 - ' + obj.data.name, + area: ['600px', '400px'], + content: '
' + + '' + + '
', + btn: ['保存', '取消'], + yes: function (index, layero) { + var announcementContent = $('#announcementEditor').val(); + + // 发送更新请求 + $.ajax({ + url: '/admin/api/apps/update_announcement', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + uuid: obj.data.uuid, + announcement: announcementContent + }), + success: function (res) { + if (res.code === 0) { + layer.msg('程序公告更新成功!', { + icon: 1, + time: 2000 + }); + layer.close(index); + } else { + layer.msg(res.msg || '更新程序公告失败', { icon: 2 }); + } + }, + error: function () { + layer.msg('网络错误,请稍后重试', { icon: 2 }); + } + }); + }, + btn2: function (index) { + layer.close(index); + } + }); + }, + error: function () { + layer.msg('获取程序公告失败,请稍后重试', { icon: 2 }); + } + }); + } else if (menudata.id === 'multi_instance') { + // 多开配置 + $.ajax({ + url: '/admin/api/apps/get_multi_config?uuid=' + obj.data.uuid, + type: 'GET', + success: function (config) { + // 填充表单数据 + $('input[name="login_type"][value="' + config.login_type + '"]').prop('checked', true); + $('input[name="multi_open_scope"][value="' + config.multi_open_scope + '"]').prop('checked', true); + $('input[name="clean_interval"]').val(config.clean_interval); + $('input[name="check_interval"]').val(config.check_interval); + $('input[name="multi_open_count"]').val(config.multi_open_count); + + // 打开静态弹窗 + var multiConfigIndex = layer.open({ + type: 1, + title: '多开配置 - ' + obj.data.name, + area: ['550px', '450px'], + content: $('#multiConfigModal'), + btn: ['保存', '取消'], + yes: function (index, layero) { + var formData = { + uuid: obj.data.uuid, + login_type: parseInt($('input[name="login_type"]:checked').val()), + multi_open_scope: parseInt($('input[name="multi_open_scope"]:checked').val()), + clean_interval: parseInt($('input[name="clean_interval"]').val()), + check_interval: parseInt($('input[name="check_interval"]').val()), + multi_open_count: parseInt($('input[name="multi_open_count"]').val()) + }; + + // 验证数据 + if (isNaN(formData.login_type) || formData.login_type < 0 || formData.login_type > 1) { + layer.msg('请选择登录方式', { icon: 2 }); + return; + } + if (isNaN(formData.multi_open_scope) || formData.multi_open_scope < 0 || formData.multi_open_scope > 2) { + layer.msg('请选择多开范围', { icon: 2 }); + return; + } + if (isNaN(formData.clean_interval) || formData.clean_interval < 1) { + layer.msg('清理间隔必须大于0', { icon: 2 }); + return; + } + if (isNaN(formData.check_interval) || formData.check_interval < 1) { + layer.msg('校验间隔必须大于0', { icon: 2 }); + return; + } + if (isNaN(formData.multi_open_count) || formData.multi_open_count < 1) { + layer.msg('多开数量必须大于0', { icon: 2 }); + return; + } + + // 发送更新请求 + $.ajax({ + url: '/admin/api/apps/update_multi_config', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(formData), + success: function (res) { + if (res.message) { + layer.msg('多开配置更新成功', { icon: 1 }); + layer.close(index); + table.reload('appsTable'); + } else { + layer.msg(res.msg || '更新多开配置失败', { icon: 2 }); + } + }, + error: function () { + layer.msg('网络错误,请稍后重试', { icon: 2 }); + } + }); + }, + btn2: function (index) { + layer.close(index); + }, + success: function () { + // 重新渲染表单 + form.render(); + } + }); + }, + error: function () { + layer.msg('获取多开配置失败,请稍后重试', { icon: 2 }); + } + }); + } else if (menudata.id === 'reset_secret') { + // 重置密钥 + layer.confirm('确定重置该应用的密钥吗?重置后原密钥将失效!', { icon: 3, title: '提示' }, function (index) { + // 发送重置密钥请求 + $.ajax({ + url: '/admin/api/apps/reset_secret', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + uuid: obj.data.uuid + }), + success: function (res) { + if (res.code === 0) { + layer.msg('密钥重置成功!', { + icon: 1, + time: 2000 // 显示2秒 + }); + // 刷新表格数据 + table.reload('appsTable'); + } else { + layer.msg(res.msg || '重置密钥失败', { icon: 2 }); + } + }, + error: function (xhr) { + let errorMsg = '重置密钥失败'; + if (xhr.responseText) { + try { + const errorRes = JSON.parse(xhr.responseText); + errorMsg = errorRes.msg || errorMsg; + } catch (e) { + errorMsg = xhr.responseText; + } + } + layer.msg(errorMsg, { icon: 2 }); + } + }); + layer.close(index); + }); + } else if (menudata.id === 'bind_settings') { + // 绑定设置 + $.ajax({ + url: '/admin/api/apps/get_bind_config?uuid=' + obj.data.uuid, + type: 'GET', + success: function (config) { + // 填充表单数据 + $('#bindConfigModal input[name="machine_verify"][value="' + config.machine_verify + '"]').prop('checked', true); + $('#bindConfigModal input[name="machine_rebind_enabled"][value="' + config.machine_rebind_enabled + '"]').prop('checked', true); + $('#bindConfigModal input[name="machine_rebind_limit"][value="' + config.machine_rebind_limit + '"]').prop('checked', true); + $('#bindConfigModal input[name="machine_free_count"]').val(config.machine_free_count); + $('#bindConfigModal input[name="machine_rebind_count"]').val(config.machine_rebind_count); + $('#bindConfigModal input[name="machine_rebind_deduct"]').val(config.machine_rebind_deduct); + $('#bindConfigModal input[name="ip_verify"][value="' + config.ip_verify + '"]').prop('checked', true); + $('#bindConfigModal input[name="ip_rebind_enabled"][value="' + config.ip_rebind_enabled + '"]').prop('checked', true); + $('#bindConfigModal input[name="ip_rebind_limit"][value="' + config.ip_rebind_limit + '"]').prop('checked', true); + $('#bindConfigModal input[name="ip_free_count"]').val(config.ip_free_count); + $('#bindConfigModal input[name="ip_rebind_count"]').val(config.ip_rebind_count); + $('#bindConfigModal input[name="ip_rebind_deduct"]').val(config.ip_rebind_deduct); + + // 打开静态弹窗 + var bindConfigIndex = layer.open({ + type: 1, + title: '绑定设置 - ' + obj.data.name, + area: ['650px', '600px'], + content: $('#bindConfigModal'), + btn: ['保存', '取消'], + yes: function (index, layero) { + var formData = { + uuid: obj.data.uuid, + machine_verify: parseInt($('#bindConfigModal input[name="machine_verify"]:checked').val()), + machine_rebind_enabled: parseInt($('#bindConfigModal input[name="machine_rebind_enabled"]:checked').val()), + machine_rebind_limit: parseInt($('#bindConfigModal input[name="machine_rebind_limit"]:checked').val()), + machine_free_count: parseInt($('#bindConfigModal input[name="machine_free_count"]').val()) || 0, + machine_rebind_count: parseInt($('#bindConfigModal input[name="machine_rebind_count"]').val()) || 0, + machine_rebind_deduct: parseInt($('#bindConfigModal input[name="machine_rebind_deduct"]').val()) || 0, + ip_verify: parseInt($('#bindConfigModal input[name="ip_verify"]:checked').val()), + ip_rebind_enabled: parseInt($('#bindConfigModal input[name="ip_rebind_enabled"]:checked').val()), + ip_rebind_limit: parseInt($('#bindConfigModal input[name="ip_rebind_limit"]:checked').val()), + ip_free_count: parseInt($('#bindConfigModal input[name="ip_free_count"]').val()) || 0, + ip_rebind_count: parseInt($('#bindConfigModal input[name="ip_rebind_count"]').val()) || 0, + ip_rebind_deduct: parseInt($('#bindConfigModal input[name="ip_rebind_deduct"]').val()) || 0 + }; + + // 验证数据 + if (isNaN(formData.machine_verify) || formData.machine_verify < 0 || formData.machine_verify > 1) { + layer.msg('请选择机器验证选项', { icon: 2 }); + return; + } + if (isNaN(formData.machine_rebind_enabled) || formData.machine_rebind_enabled < 0 || formData.machine_rebind_enabled > 1) { + layer.msg('请选择机器重绑选项', { icon: 2 }); + return; + } + if (isNaN(formData.machine_rebind_limit) || formData.machine_rebind_limit < 0 || formData.machine_rebind_limit > 1) { + layer.msg('请选择机器重绑限制', { icon: 2 }); + return; + } + if (isNaN(formData.ip_verify) || formData.ip_verify < 0 || formData.ip_verify > 3) { + layer.msg('请选择IP地址验证选项', { icon: 2 }); + return; + } + if (isNaN(formData.ip_rebind_enabled) || formData.ip_rebind_enabled < 0 || formData.ip_rebind_enabled > 1) { + layer.msg('请选择IP地址重绑选项', { icon: 2 }); + return; + } + if (isNaN(formData.ip_rebind_limit) || formData.ip_rebind_limit < 0 || formData.ip_rebind_limit > 1) { + layer.msg('请选择IP地址重绑限制', { icon: 2 }); + return; + } + + // 发送更新请求 + $.ajax({ + url: '/admin/api/apps/update_bind_config', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(formData), + success: function (res) { + if (res.code === 0) { + layer.msg('绑定设置更新成功', { icon: 1 }); + layer.close(index); + table.reload('appsTable'); + } else { + layer.msg(res.msg || '更新绑定设置失败', { icon: 2 }); + } + }, + error: function () { + layer.msg('网络错误,请稍后重试', { icon: 2 }); + } + }); + }, + btn2: function (index) { + layer.close(index); + }, + success: function () { + // 重新渲染表单 + form.render(); + } + }); + }, + error: function () { + layer.msg('获取绑定设置失败,请稍后重试', { icon: 2 }); + } + }); + } else if (menudata.id === 'register_settings') { + // 注册设置 + $.ajax({ + url: '/admin/api/apps/get_register_config?uuid=' + obj.data.uuid, + type: 'GET', + success: function (config) { + // 填充表单数据 + $('#registerConfigModal input[name="register_enabled"][value="' + config.register_enabled + '"]').prop('checked', true); + $('#registerConfigModal input[name="register_limit_enabled"][value="' + config.register_limit_enabled + '"]').prop('checked', true); + $('#registerConfigModal input[name="register_limit_time"][value="' + config.register_limit_time + '"]').prop('checked', true); + $('#registerConfigModal input[name="register_count"]').val(config.register_count); + $('#registerConfigModal input[name="trial_enabled"][value="' + config.trial_enabled + '"]').prop('checked', true); + $('#registerConfigModal input[name="trial_limit_time"][value="' + config.trial_limit_time + '"]').prop('checked', true); + $('#registerConfigModal input[name="trial_duration"]').val(config.trial_duration); + + // 打开静态弹窗 + var registerConfigIndex = layer.open({ + type: 1, + title: '注册设置 - ' + obj.data.name, + area: ['550px', '500px'], + content: $('#registerConfigModal'), + btn: ['保存', '取消'], + yes: function (index, layero) { + var formData = { + uuid: obj.data.uuid, + register_enabled: parseInt($('#registerConfigModal input[name="register_enabled"]:checked').val()), + register_limit_enabled: parseInt($('#registerConfigModal input[name="register_limit_enabled"]:checked').val()), + register_limit_time: parseInt($('#registerConfigModal input[name="register_limit_time"]:checked').val()), + register_count: parseInt($('#registerConfigModal input[name="register_count"]').val()) || 1, + trial_enabled: parseInt($('#registerConfigModal input[name="trial_enabled"]:checked').val()), + trial_limit_time: parseInt($('#registerConfigModal input[name="trial_limit_time"]:checked').val()), + trial_duration: parseInt($('#registerConfigModal input[name="trial_duration"]').val()) || 0 + }; + + // 验证数据 + if (isNaN(formData.register_enabled) || formData.register_enabled < 0 || formData.register_enabled > 1) { + layer.msg('请选择账号注册选项', { icon: 2 }); + return; + } + if (isNaN(formData.register_limit_enabled) || formData.register_limit_enabled < 0 || formData.register_limit_enabled > 1) { + layer.msg('请选择注册限制选项', { icon: 2 }); + return; + } + if (isNaN(formData.register_limit_time) || formData.register_limit_time < 0 || formData.register_limit_time > 1) { + layer.msg('请选择限制时间选项', { icon: 2 }); + return; + } + if (isNaN(formData.register_count) || formData.register_count < 1) { + layer.msg('注册次数必须大于0', { icon: 2 }); + return; + } + if (isNaN(formData.trial_enabled) || formData.trial_enabled < 0 || formData.trial_enabled > 1) { + layer.msg('请选择领取试用选项', { icon: 2 }); + return; + } + if (isNaN(formData.trial_limit_time) || formData.trial_limit_time < 0 || formData.trial_limit_time > 1) { + layer.msg('请选择试用限制时间选项', { icon: 2 }); + return; + } + if (isNaN(formData.trial_duration) || formData.trial_duration < 0) { + layer.msg('试用时间不能小于0', { icon: 2 }); + return; + } + + // 发送更新请求 + $.ajax({ + url: '/admin/api/apps/update_register_config', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(formData), + success: function (res) { + if (res.code === 0) { + layer.msg('注册设置更新成功', { icon: 1 }); + layer.close(index); + table.reload('appsTable'); + } else { + layer.msg(res.msg || '更新注册设置失败', { icon: 2 }); + } + }, + error: function () { + layer.msg('网络错误,请稍后重试', { icon: 2 }); + } + }); + }, + btn2: function (index) { + layer.close(index); + }, + success: function () { + // 重新渲染表单 + form.render(); + } + }); + }, + error: function () { + layer.msg('获取注册设置失败,请稍后重试', { icon: 2 }); + } + }); } }, - error: function(xhr) { - layer.msg(xhr.responseText || '删除失败', {icon: 2}); + align: 'right', // 右对齐弹出 + style: 'box-shadow: 1px 1px 10px rgb(0 0 0 / 12%);' // 设置额外样式 + }); + } + }); + + // 批量删除 + $('#btnBatchDeleteApps').on('click', function () { + const checkStatus = table.checkStatus('appsTable'); + const data = checkStatus.data; + + if (data.length === 0) { + layer.msg('请选择要删除的应用', { icon: 2 }); + return; + } + + layer.confirm('确定删除选中的 ' + data.length + ' 个应用吗?', { icon: 3, title: '提示' }, function (index) { + const ids = data.map(item => item.id); + $.ajax({ + url: '/admin/api/apps/batch_delete', + type: 'POST', + data: JSON.stringify({ ids: ids }), + contentType: 'application/json', + success: function (res) { + if (res.code === 0) { + layer.msg(res.msg, { icon: 1 }); + appsTable.reload(); + } else { + layer.msg(res.msg || '批量删除失败', { icon: 2 }); + } + }, + error: function (xhr) { + layer.msg(xhr.responseText || '批量删除失败', { icon: 2 }); } }); layer.close(index); }); - } else if (obj.event === 'more') { - // 更多操作下拉菜单 - dropdown.render({ - elem: this, // 使用 this 而不是查找元素 - show: true, // 外部事件触发即显示 - data: [ - { - title: '应用数据', - id: 'app_data' - }, - { - title: '程序公告', - id: 'announcement' - }, - { - title: '多开配置', - id: 'multi_instance' - }, - { - title: '绑定设置', - id: 'bind_settings' - }, - { - title: '注册设置', - id: 'register_settings' - }, - { - title: '重置密钥', - id: 'reset_secret' - } - ], - click: function(menudata, othis) { - if (menudata.id === 'app_data') { - // 应用数据 - // 先获取当前应用数据内容 - $.ajax({ - url: '/admin/api/apps/get_app_data?uuid=' + obj.data.uuid, - type: 'GET', - success: function(res) { - var currentAppData = ''; - if (res.code === 0 && res.data && res.data.app_data) { - currentAppData = res.data.app_data; - } - - // 显示编辑弹窗 - layer.open({ - type: 1, - title: '编辑应用数据 - ' + obj.data.name, - area: ['600px', '400px'], - content: '
' + - '' + - '
', - btn: ['保存', '取消'], - yes: function(index, layero) { - var appDataContent = $('#appDataEditor').val(); - - // 发送更新请求 - $.ajax({ - url: '/admin/api/apps/update_app_data', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ - uuid: obj.data.uuid, - app_data: appDataContent - }), - success: function(res) { - if (res.code === 0) { - layer.msg('应用数据更新成功!', { - icon: 1, - time: 2000 - }); - layer.close(index); - } else { - layer.msg(res.msg || '更新应用数据失败', {icon: 2}); - } - }, - error: function() { - layer.msg('网络错误,请稍后重试', {icon: 2}); - } - }); - }, - btn2: function(index) { - layer.close(index); - } - }); - }, - error: function() { - layer.msg('获取应用数据失败,请稍后重试', {icon: 2}); - } - }); - } else if (menudata.id === 'announcement') { - // 程序公告 - // 先获取当前公告内容 - $.ajax({ - url: '/admin/api/apps/get_announcement?uuid=' + obj.data.uuid, - type: 'GET', - success: function(res) { - var currentAnnouncement = ''; - if (res.code === 0 && res.data && res.data.announcement) { - currentAnnouncement = res.data.announcement; - } - - // 显示编辑弹窗 - layer.open({ - type: 1, - title: '编辑程序公告 - ' + obj.data.name, - area: ['600px', '400px'], - content: '
' + - '' + - '
', - btn: ['保存', '取消'], - yes: function(index, layero) { - var announcementContent = $('#announcementEditor').val(); - - // 发送更新请求 - $.ajax({ - url: '/admin/api/apps/update_announcement', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ - uuid: obj.data.uuid, - announcement: announcementContent - }), - success: function(res) { - if (res.code === 0) { - layer.msg('程序公告更新成功!', { - icon: 1, - time: 2000 - }); - layer.close(index); - } else { - layer.msg(res.msg || '更新程序公告失败', {icon: 2}); - } - }, - error: function() { - layer.msg('网络错误,请稍后重试', {icon: 2}); - } - }); - }, - btn2: function(index) { - layer.close(index); - } - }); - }, - error: function() { - layer.msg('获取程序公告失败,请稍后重试', {icon: 2}); - } - }); - } else if (menudata.id === 'multi_instance') { - // 多开配置 - $.ajax({ - url: '/admin/api/apps/get_multi_config?uuid=' + obj.data.uuid, - type: 'GET', - success: function(config) { - // 填充表单数据 - $('input[name="login_type"][value="' + config.login_type + '"]').prop('checked', true); - $('input[name="multi_open_scope"][value="' + config.multi_open_scope + '"]').prop('checked', true); - $('input[name="clean_interval"]').val(config.clean_interval); - $('input[name="check_interval"]').val(config.check_interval); - $('input[name="multi_open_count"]').val(config.multi_open_count); - - // 打开静态弹窗 - var multiConfigIndex = layer.open({ - type: 1, - title: '多开配置 - ' + obj.data.name, - area: ['550px', '450px'], - content: $('#multiConfigModal'), - btn: ['保存', '取消'], - yes: function(index, layero) { - var formData = { - uuid: obj.data.uuid, - login_type: parseInt($('input[name="login_type"]:checked').val()), - multi_open_scope: parseInt($('input[name="multi_open_scope"]:checked').val()), - clean_interval: parseInt($('input[name="clean_interval"]').val()), - check_interval: parseInt($('input[name="check_interval"]').val()), - multi_open_count: parseInt($('input[name="multi_open_count"]').val()) - }; - - // 验证数据 - if (isNaN(formData.login_type) || formData.login_type < 0 || formData.login_type > 1) { - layer.msg('请选择登录方式', {icon: 2}); - return; - } - if (isNaN(formData.multi_open_scope) || formData.multi_open_scope < 0 || formData.multi_open_scope > 2) { - layer.msg('请选择多开范围', {icon: 2}); - return; - } - if (isNaN(formData.clean_interval) || formData.clean_interval < 1) { - layer.msg('清理间隔必须大于0', {icon: 2}); - return; - } - if (isNaN(formData.check_interval) || formData.check_interval < 1) { - layer.msg('校验间隔必须大于0', {icon: 2}); - return; - } - if (isNaN(formData.multi_open_count) || formData.multi_open_count < 1) { - layer.msg('多开数量必须大于0', {icon: 2}); - return; - } - - // 发送更新请求 - $.ajax({ - url: '/admin/api/apps/update_multi_config', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify(formData), - success: function(res) { - if (res.message) { - layer.msg('多开配置更新成功', {icon: 1}); - layer.close(index); - table.reload('appsTable'); - } else { - layer.msg(res.msg || '更新多开配置失败', {icon: 2}); - } - }, - error: function() { - layer.msg('网络错误,请稍后重试', {icon: 2}); - } - }); - }, - btn2: function(index) { - layer.close(index); - }, - success: function() { - // 重新渲染表单 - form.render(); - } - }); - }, - error: function() { - layer.msg('获取多开配置失败,请稍后重试', {icon: 2}); - } - }); - } else if (menudata.id === 'reset_secret') { - // 重置密钥 - layer.confirm('确定重置该应用的密钥吗?重置后原密钥将失效!', {icon: 3, title: '提示'}, function(index) { - // 发送重置密钥请求 - $.ajax({ - url: '/admin/api/apps/reset_secret', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ - uuid: obj.data.uuid - }), - success: function(res) { - if (res.code === 0) { - layer.msg('密钥重置成功!', { - icon: 1, - time: 2000 // 显示2秒 - }); - // 刷新表格数据 - table.reload('appsTable'); - } else { - layer.msg(res.msg || '重置密钥失败', {icon: 2}); - } - }, - error: function(xhr) { - let errorMsg = '重置密钥失败'; - if (xhr.responseText) { - try { - const errorRes = JSON.parse(xhr.responseText); - errorMsg = errorRes.msg || errorMsg; - } catch (e) { - errorMsg = xhr.responseText; - } - } - layer.msg(errorMsg, {icon: 2}); - } - }); - layer.close(index); - }); - } else if (menudata.id === 'bind_settings') { - // 绑定设置 - $.ajax({ - url: '/admin/api/apps/get_bind_config?uuid=' + obj.data.uuid, - type: 'GET', - success: function(config) { - // 填充表单数据 - $('#bindConfigModal input[name="machine_verify"][value="' + config.machine_verify + '"]').prop('checked', true); - $('#bindConfigModal input[name="machine_rebind_enabled"][value="' + config.machine_rebind_enabled + '"]').prop('checked', true); - $('#bindConfigModal input[name="machine_rebind_limit"][value="' + config.machine_rebind_limit + '"]').prop('checked', true); - $('#bindConfigModal input[name="machine_free_count"]').val(config.machine_free_count); - $('#bindConfigModal input[name="machine_rebind_count"]').val(config.machine_rebind_count); - $('#bindConfigModal input[name="machine_rebind_deduct"]').val(config.machine_rebind_deduct); - $('#bindConfigModal input[name="ip_verify"][value="' + config.ip_verify + '"]').prop('checked', true); - $('#bindConfigModal input[name="ip_rebind_enabled"][value="' + config.ip_rebind_enabled + '"]').prop('checked', true); - $('#bindConfigModal input[name="ip_rebind_limit"][value="' + config.ip_rebind_limit + '"]').prop('checked', true); - $('#bindConfigModal input[name="ip_free_count"]').val(config.ip_free_count); - $('#bindConfigModal input[name="ip_rebind_count"]').val(config.ip_rebind_count); - $('#bindConfigModal input[name="ip_rebind_deduct"]').val(config.ip_rebind_deduct); - - // 打开静态弹窗 - var bindConfigIndex = layer.open({ - type: 1, - title: '绑定设置 - ' + obj.data.name, - area: ['650px', '600px'], - content: $('#bindConfigModal'), - btn: ['保存', '取消'], - yes: function(index, layero) { - var formData = { - uuid: obj.data.uuid, - machine_verify: parseInt($('#bindConfigModal input[name="machine_verify"]:checked').val()), - machine_rebind_enabled: parseInt($('#bindConfigModal input[name="machine_rebind_enabled"]:checked').val()), - machine_rebind_limit: parseInt($('#bindConfigModal input[name="machine_rebind_limit"]:checked').val()), - machine_free_count: parseInt($('#bindConfigModal input[name="machine_free_count"]').val()) || 0, - machine_rebind_count: parseInt($('#bindConfigModal input[name="machine_rebind_count"]').val()) || 0, - machine_rebind_deduct: parseInt($('#bindConfigModal input[name="machine_rebind_deduct"]').val()) || 0, - ip_verify: parseInt($('#bindConfigModal input[name="ip_verify"]:checked').val()), - ip_rebind_enabled: parseInt($('#bindConfigModal input[name="ip_rebind_enabled"]:checked').val()), - ip_rebind_limit: parseInt($('#bindConfigModal input[name="ip_rebind_limit"]:checked').val()), - ip_free_count: parseInt($('#bindConfigModal input[name="ip_free_count"]').val()) || 0, - ip_rebind_count: parseInt($('#bindConfigModal input[name="ip_rebind_count"]').val()) || 0, - ip_rebind_deduct: parseInt($('#bindConfigModal input[name="ip_rebind_deduct"]').val()) || 0 - }; - - // 验证数据 - if (isNaN(formData.machine_verify) || formData.machine_verify < 0 || formData.machine_verify > 1) { - layer.msg('请选择机器验证选项', {icon: 2}); - return; - } - if (isNaN(formData.machine_rebind_enabled) || formData.machine_rebind_enabled < 0 || formData.machine_rebind_enabled > 1) { - layer.msg('请选择机器重绑选项', {icon: 2}); - return; - } - if (isNaN(formData.machine_rebind_limit) || formData.machine_rebind_limit < 0 || formData.machine_rebind_limit > 1) { - layer.msg('请选择机器重绑限制', {icon: 2}); - return; - } - if (isNaN(formData.ip_verify) || formData.ip_verify < 0 || formData.ip_verify > 3) { - layer.msg('请选择IP地址验证选项', {icon: 2}); - return; - } - if (isNaN(formData.ip_rebind_enabled) || formData.ip_rebind_enabled < 0 || formData.ip_rebind_enabled > 1) { - layer.msg('请选择IP地址重绑选项', {icon: 2}); - return; - } - if (isNaN(formData.ip_rebind_limit) || formData.ip_rebind_limit < 0 || formData.ip_rebind_limit > 1) { - layer.msg('请选择IP地址重绑限制', {icon: 2}); - return; - } - - // 发送更新请求 - $.ajax({ - url: '/admin/api/apps/update_bind_config', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify(formData), - success: function(res) { - if (res.code === 0) { - layer.msg('绑定设置更新成功', {icon: 1}); - layer.close(index); - table.reload('appsTable'); - } else { - layer.msg(res.msg || '更新绑定设置失败', {icon: 2}); - } - }, - error: function() { - layer.msg('网络错误,请稍后重试', {icon: 2}); - } - }); - }, - btn2: function(index) { - layer.close(index); - }, - success: function() { - // 重新渲染表单 - form.render(); - } - }); - }, - error: function() { - layer.msg('获取绑定设置失败,请稍后重试', {icon: 2}); - } - }); - } else if (menudata.id === 'register_settings') { - // 注册设置 - $.ajax({ - url: '/admin/api/apps/get_register_config?uuid=' + obj.data.uuid, - type: 'GET', - success: function(config) { - // 填充表单数据 - $('#registerConfigModal input[name="register_enabled"][value="' + config.register_enabled + '"]').prop('checked', true); - $('#registerConfigModal input[name="register_limit_enabled"][value="' + config.register_limit_enabled + '"]').prop('checked', true); - $('#registerConfigModal input[name="register_limit_time"][value="' + config.register_limit_time + '"]').prop('checked', true); - $('#registerConfigModal input[name="register_count"]').val(config.register_count); - $('#registerConfigModal input[name="trial_enabled"][value="' + config.trial_enabled + '"]').prop('checked', true); - $('#registerConfigModal input[name="trial_limit_time"][value="' + config.trial_limit_time + '"]').prop('checked', true); - $('#registerConfigModal input[name="trial_duration"]').val(config.trial_duration); - - // 打开静态弹窗 - var registerConfigIndex = layer.open({ - type: 1, - title: '注册设置 - ' + obj.data.name, - area: ['550px', '500px'], - content: $('#registerConfigModal'), - btn: ['保存', '取消'], - yes: function(index, layero) { - var formData = { - uuid: obj.data.uuid, - register_enabled: parseInt($('#registerConfigModal input[name="register_enabled"]:checked').val()), - register_limit_enabled: parseInt($('#registerConfigModal input[name="register_limit_enabled"]:checked').val()), - register_limit_time: parseInt($('#registerConfigModal input[name="register_limit_time"]:checked').val()), - register_count: parseInt($('#registerConfigModal input[name="register_count"]').val()) || 1, - trial_enabled: parseInt($('#registerConfigModal input[name="trial_enabled"]:checked').val()), - trial_limit_time: parseInt($('#registerConfigModal input[name="trial_limit_time"]:checked').val()), - trial_duration: parseInt($('#registerConfigModal input[name="trial_duration"]').val()) || 0 - }; - - // 验证数据 - if (isNaN(formData.register_enabled) || formData.register_enabled < 0 || formData.register_enabled > 1) { - layer.msg('请选择账号注册选项', {icon: 2}); - return; - } - if (isNaN(formData.register_limit_enabled) || formData.register_limit_enabled < 0 || formData.register_limit_enabled > 1) { - layer.msg('请选择注册限制选项', {icon: 2}); - return; - } - if (isNaN(formData.register_limit_time) || formData.register_limit_time < 0 || formData.register_limit_time > 1) { - layer.msg('请选择限制时间选项', {icon: 2}); - return; - } - if (isNaN(formData.register_count) || formData.register_count < 1) { - layer.msg('注册次数必须大于0', {icon: 2}); - return; - } - if (isNaN(formData.trial_enabled) || formData.trial_enabled < 0 || formData.trial_enabled > 1) { - layer.msg('请选择领取试用选项', {icon: 2}); - return; - } - if (isNaN(formData.trial_limit_time) || formData.trial_limit_time < 0 || formData.trial_limit_time > 1) { - layer.msg('请选择试用限制时间选项', {icon: 2}); - return; - } - if (isNaN(formData.trial_duration) || formData.trial_duration < 0) { - layer.msg('试用时间不能小于0', {icon: 2}); - return; - } - - // 发送更新请求 - $.ajax({ - url: '/admin/api/apps/update_register_config', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify(formData), - success: function(res) { - if (res.code === 0) { - layer.msg('注册设置更新成功', {icon: 1}); - layer.close(index); - table.reload('appsTable'); - } else { - layer.msg(res.msg || '更新注册设置失败', {icon: 2}); - } - }, - error: function() { - layer.msg('网络错误,请稍后重试', {icon: 2}); - } - }); - }, - btn2: function(index) { - layer.close(index); - }, - success: function() { - // 重新渲染表单 - form.render(); - } - }); - }, - error: function() { - layer.msg('获取注册设置失败,请稍后重试', {icon: 2}); - } - }); - } - }, - align: 'right', // 右对齐弹出 - style: 'box-shadow: 1px 1px 10px rgb(0 0 0 / 12%);' // 设置额外样式 - }); - } - }); + }); + + // 批量启用 + $('#btnBatchEnableApps').on('click', function () { + const checkStatus = table.checkStatus('appsTable'); + const data = checkStatus.data; + + if (data.length === 0) { + layer.msg('请选择要启用的应用', { icon: 2 }); + return; + } - // 批量删除 - $('#btnBatchDeleteApps').on('click', function() { - const checkStatus = table.checkStatus('appsTable'); - const data = checkStatus.data; - - if (data.length === 0) { - layer.msg('请选择要删除的应用', {icon: 2}); - return; - } - - layer.confirm('确定删除选中的 ' + data.length + ' 个应用吗?', {icon: 3, title: '提示'}, function(index) { const ids = data.map(item => item.id); $.ajax({ - url: '/admin/api/apps/batch_delete', + url: '/admin/api/apps/batch_update_status', type: 'POST', - data: JSON.stringify({ids: ids}), + data: JSON.stringify({ ids: ids, status: 1 }), contentType: 'application/json', - success: function(res) { + success: function (res) { if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); + layer.msg(res.msg, { icon: 1 }); appsTable.reload(); } else { - layer.msg(res.msg || '批量删除失败', {icon: 2}); + layer.msg(res.msg || '批量启用失败', { icon: 2 }); } }, - error: function(xhr) { - layer.msg(xhr.responseText || '批量删除失败', {icon: 2}); + error: function (xhr) { + layer.msg(xhr.responseText || '批量启用失败', { icon: 2 }); } }); - layer.close(index); }); - }); - // 批量启用 - $('#btnBatchEnableApps').on('click', function() { - const checkStatus = table.checkStatus('appsTable'); - const data = checkStatus.data; - - if (data.length === 0) { - layer.msg('请选择要启用的应用', {icon: 2}); - return; - } - - const ids = data.map(item => item.id); - $.ajax({ - url: '/admin/api/apps/batch_update_status', - type: 'POST', - data: JSON.stringify({ids: ids, status: 1}), - contentType: 'application/json', - success: function(res) { - if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); - appsTable.reload(); - } else { - layer.msg(res.msg || '批量启用失败', {icon: 2}); - } - }, - error: function(xhr) { - layer.msg(xhr.responseText || '批量启用失败', {icon: 2}); + // 批量禁用 + $('#btnBatchDisableApps').on('click', function () { + const checkStatus = table.checkStatus('appsTable'); + const data = checkStatus.data; + + if (data.length === 0) { + layer.msg('请选择要禁用的应用', { icon: 2 }); + return; } - }); - }); - // 批量禁用 - $('#btnBatchDisableApps').on('click', function() { - const checkStatus = table.checkStatus('appsTable'); - const data = checkStatus.data; - - if (data.length === 0) { - layer.msg('请选择要禁用的应用', {icon: 2}); - return; - } - - const ids = data.map(item => item.id); - $.ajax({ - url: '/admin/api/apps/batch_update_status', - type: 'POST', - data: JSON.stringify({ids: ids, status: 0}), - contentType: 'application/json', - success: function(res) { - if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); - appsTable.reload(); - } else { - layer.msg(res.msg || '批量禁用失败', {icon: 2}); + const ids = data.map(item => item.id); + $.ajax({ + url: '/admin/api/apps/batch_update_status', + type: 'POST', + data: JSON.stringify({ ids: ids, status: 0 }), + contentType: 'application/json', + success: function (res) { + if (res.code === 0) { + layer.msg(res.msg, { icon: 1 }); + appsTable.reload(); + } else { + layer.msg(res.msg || '批量禁用失败', { icon: 2 }); + } + }, + error: function (xhr) { + layer.msg(xhr.responseText || '批量禁用失败', { icon: 2 }); } - }, - error: function(xhr) { - layer.msg(xhr.responseText || '批量禁用失败', {icon: 2}); - } + }); }); - }); - // Tips提示功能已移至admin.js统一管理 + // Tips提示功能已移至admin.js统一管理 }); }); diff --git a/web/template/admin/dashboard.html b/web/template/admin/dashboard.html index 4cc6c85..8086002 100644 --- a/web/template/admin/dashboard.html +++ b/web/template/admin/dashboard.html @@ -38,59 +38,54 @@
- - - {{ end }} \ No newline at end of file diff --git a/web/template/admin/layout.html b/web/template/admin/layout.html index fbfef73..7e698ce 100644 --- a/web/template/admin/layout.html +++ b/web/template/admin/layout.html @@ -1,73 +1,75 @@ - - - - {{ .Title }} - {{ .SystemName }} - - - - - - - -
-
- - -
- + @@ -185,58 +209,63 @@ + \ No newline at end of file diff --git a/web/template/admin/settings.html b/web/template/admin/settings.html index 6f48ee3..92d5b0e 100644 --- a/web/template/admin/settings.html +++ b/web/template/admin/settings.html @@ -1,7 +1,6 @@ {{ define "settings.html" }}

系统设置

-
基本信息设置
@@ -34,7 +33,7 @@
- +
系统配置
@@ -61,7 +60,8 @@
- + 秒(300-86400秒)
@@ -70,7 +70,7 @@
- +
页脚与备案
@@ -103,13 +103,14 @@
- +
- +
@@ -129,159 +130,159 @@ } } - waitForLayui(function() { - layui.use(['jquery', 'form', 'layer', 'util'], function() { + waitForLayui(function () { + layui.use(['jquery', 'form', 'layer', 'util'], function () { const { $, form, layer, util } = layui; - // 缓存上次加载的设置值,用于“重置”恢复 - let originalSettings = {}; + // 缓存上次加载的设置值,用于“重置”恢复 + let originalSettings = {}; - /** - * 加载后台所有设置并回填到三个表单 - * - 从 /admin/api/settings 获取 name:value 映射 - * - 处理开关型字段(maintenance_mode) - * - 渲染 layui 组件 - */ - const loadSettings = async () => { - try { - const res = await fetch('/admin/api/settings', { - method: 'GET', - 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'); + /** + * 加载后台所有设置并回填到三个表单 + * - 从 /admin/api/settings 获取 name:value 映射 + * - 处理开关型字段(maintenance_mode) + * - 渲染 layui 组件 + */ + const loadSettings = async () => { + try { + const res = await fetch('/admin/api/settings', { + method: 'GET', + 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 }); + } + }; - /** - * 处理“重置”点击 - * - 恢复为上次加载的 originalSettings - */ - const handleReset = () => { - fillForms(originalSettings); - layer.msg('已恢复到上次加载的值', { icon: 1, time: 800 }); - }; + /** + * 将 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 || ''); - // 事件绑定 - $('#saveAllBtn').off('click').on('click', handleSaveAll); - $('#resetBtn').off('click').on('click', handleReset); + // 系统配置 + 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'); - // 初始化:加载设置 - 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(); }); }); diff --git a/web/template/admin/user.html b/web/template/admin/user.html index a37f83d..c469981 100644 --- a/web/template/admin/user.html +++ b/web/template/admin/user.html @@ -1,472 +1,357 @@ {{ define "user.html" }} - - -
-
    -
  • 个人资料
  • -
  • 修改密码
  • -
  • 修改用户名
  • -
-
- -
-
-
- 个人资料 -
-
-
-
- -
- +
+

个人资料

+
+
    +
  • 个人资料
  • +
  • 修改密码
  • +
  • 修改用户名
  • d +
+
+ +
+
+
个人资料
+
+ +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
- + +
-
- -
-
-
- 修改密码 -
-
-
-
- -
- + +
+
+
修改密码
+
+ +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
-
- - +
+
+ + +
-
- + +
-
- -
-
-
- 修改用户名 -
-
-
-
- -
- + +
+
+
修改用户名
+
+ +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
-
- - +
+
+ + +
-
- + +
-
- + })() + +
{{ end }} \ No newline at end of file diff --git a/web/template/admin/variables.html b/web/template/admin/variables.html index 19e8972..92edf6f 100644 --- a/web/template/admin/variables.html +++ b/web/template/admin/variables.html @@ -3,7 +3,8 @@

变量管理

- +
@@ -62,7 +63,8 @@
- +
@@ -90,47 +92,47 @@ } } - waitForLayui(function() { - layui.use(['table', 'form', 'layer', 'element'], function() { - const table = layui.table; - const form = layui.form; - const layer = layui.layer; - const $ = layui.$; + waitForLayui(function () { + layui.use(['table', 'form', 'layer', 'element'], function () { + const table = layui.table; + const form = layui.form; + const layer = layui.layer; + const $ = layui.$; - // 自定义验证规则 - form.verify({ - alias: function(value) { - if (!value) return '别名不能为空'; - // 检查是否以英文字母开头,且只包含数字和英文字母 - if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) { - return '别名必须以英文字母开头,只能包含数字和英文字母'; + // 自定义验证规则 + form.verify({ + alias: function (value) { + if (!value) return '别名不能为空'; + // 检查是否以英文字母开头,且只包含数字和英文字母 + if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) { + return '别名必须以英文字母开头,只能包含数字和英文字母'; + } } - } - }); + }); - // 格式化时间函数 - function formatDateTime(dateStr) { - if (!dateStr) return '-'; - return new Date(dateStr).toLocaleString(); - } + // 格式化时间函数 + function formatDateTime(dateStr) { + if (!dateStr) return '-'; + return new Date(dateStr).toLocaleString(); + } // 加载应用列表到下拉框 function loadApps() { $.ajax({ url: '/admin/variable/apps', type: 'GET', - success: function(res) { + success: function (res) { if (res.code === 0 && res.data) { let options = ''; - res.data.forEach(function(app) { + res.data.forEach(function (app) { options += ''; }); $('select[name="app_uuid"]').html(options); form.render('select'); } }, - error: function() { - layer.msg('加载应用列表失败', {icon: 2}); + error: function () { + layer.msg('加载应用列表失败', { icon: 2 }); } }); } @@ -143,7 +145,7 @@ elem: '#variablesTable', id: 'variablesTable', url: '/admin/variable/list', - parseData: function(res) { + parseData: function (res) { return { code: res.code, msg: res.msg || '', @@ -160,20 +162,20 @@ limit: 20, limits: [10, 20, 50, 100], loading: true, - done: function(res, curr, count) { + done: function (res, curr, count) { // 表格渲染完成后的回调 }, cols: [[ - {type: 'checkbox', width: 50}, - {field: 'id', title: 'ID', width: 80, sort: true}, - {field: 'app_name', title: '应用名称', minWidth: 120}, - {field: 'number', title: '变量编号', width: 180}, - {field: 'alias', title: '变量别名', minWidth: 150}, + { type: 'checkbox', width: 50 }, + { field: 'id', title: 'ID', width: 80, sort: true }, + { field: 'app_name', title: '应用名称', minWidth: 120 }, + { field: 'number', title: '变量编号', width: 180 }, + { field: 'alias', title: '变量别名', minWidth: 150 }, { - field: 'data', - title: '变量数据', + field: 'data', + title: '变量数据', minWidth: 200, - templet: function(d) { + templet: function (d) { // 限制显示长度,避免内容过长影响布局 if (d.data && d.data.length > 50) { return '' + d.data.substring(0, 50) + '...'; @@ -182,10 +184,10 @@ } }, { - field: 'remark', - title: '备注', + field: 'remark', + title: '备注', minWidth: 150, - templet: function(d) { + templet: function (d) { // 限制显示长度,避免内容过长影响布局 if (d.remark && d.remark.length > 30) { return '' + d.remark.substring(0, 30) + '...'; @@ -194,32 +196,32 @@ } }, { - field: 'created_at', - title: '创建时间', + field: 'created_at', + title: '创建时间', width: 180, - templet: function(d) { + templet: function (d) { 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) { - variablesTable.reload({ - where: { - app_uuid: data.value, - search: $('input[name="search"]').val() - }, - page: { - curr: 1 - } - }); - }); + form.on('select(appSelect)', function (data) { + variablesTable.reload({ + where: { + app_uuid: data.value, + search: $('input[name="search"]').val() + }, + page: { + curr: 1 + } + }); + }); // 搜索功能 - $('#btnSearchVariables').on('click', function() { + $('#btnSearchVariables').on('click', function () { variablesTable.reload({ where: { app_uuid: $('select[name="app_uuid"]').val(), @@ -232,7 +234,7 @@ }); // 重置搜索 - $('#btnResetVariables').on('click', function() { + $('#btnResetVariables').on('click', function () { $('#variableFilterForm')[0].reset(); form.render(); variablesTable.reload({ @@ -244,72 +246,72 @@ }); // 新增变量 - $('#btnAddVariable').on('click', function() { + $('#btnAddVariable').on('click', function () { console.log('新增变量按钮被点击'); $('#variableForm')[0].reset(); $('input[name="id"]').val(''); - + // 重新加载应用列表到表单中 loadApps(); - + layer.open({ type: 1, title: '新增变量', content: $('#variableFormLayer'), area: ['500px', '460px'], btn: ['创建', '取消'], - yes: function(index, layero) { + yes: function (index, layero) { // 手动收集表单数据 var formData = {}; - $('#variableForm').find('input, select, textarea').each(function() { + $('#variableForm').find('input, select, textarea').each(function () { var $this = $(this); var name = $this.attr('name'); if (name && name !== 'id') { formData[name] = $this.val(); } }); - + console.log('新增变量 - 收集到的表单数据:', formData); - + // 验证必填字段 if (!formData.app_uuid || formData.app_uuid.trim() === '') { - layer.msg('应用UUID不能为空', {icon: 2}); + layer.msg('应用UUID不能为空', { icon: 2 }); return; } if (!formData.alias || formData.alias.trim() === '') { - layer.msg('请输入变量别名', {icon: 2}); + layer.msg('请输入变量别名', { icon: 2 }); return; } if (!formData.data || formData.data.trim() === '') { - layer.msg('请输入变量数据', {icon: 2}); + layer.msg('请输入变量数据', { icon: 2 }); return; } - + console.log('新增变量 - 发送的JSON数据:', JSON.stringify(formData)); - + $.ajax({ url: '/admin/variable/create', type: 'POST', data: JSON.stringify(formData), contentType: 'application/json', - success: function(res) { + success: function (res) { if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); + layer.msg(res.msg, { icon: 1 }); layer.close(index); variablesTable.reload(); } else { - layer.msg(res.msg || '操作失败', {icon: 2}); + layer.msg(res.msg || '操作失败', { icon: 2 }); } }, - error: function(xhr) { - layer.msg(xhr.responseText || '操作失败', {icon: 2}); + error: function (xhr) { + layer.msg(xhr.responseText || '操作失败', { icon: 2 }); } }); }, - btn2: function(index) { + btn2: function (index) { layer.close(index); }, - success: function() { + success: function () { form.render(); }, shadeClose: false @@ -317,32 +319,32 @@ }); // 批量删除 - $('#btnBatchDeleteVariables').on('click', function() { + $('#btnBatchDeleteVariables').on('click', function () { const checkStatus = table.checkStatus('variablesTable'); const data = checkStatus.data; - + if (data.length === 0) { - layer.msg('请选择要删除的变量', {icon: 2}); + layer.msg('请选择要删除的变量', { icon: 2 }); 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); $.ajax({ url: '/admin/variable/batch_delete', type: 'POST', - data: JSON.stringify({ids: ids}), + data: JSON.stringify({ ids: ids }), contentType: 'application/json', - success: function(res) { + success: function (res) { if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); + layer.msg(res.msg, { icon: 1 }); variablesTable.reload(); } else { - layer.msg(res.msg || '批量删除失败', {icon: 2}); + layer.msg(res.msg || '批量删除失败', { icon: 2 }); } }, - error: function(xhr) { - layer.msg(xhr.responseText || '批量删除失败', {icon: 2}); + error: function (xhr) { + layer.msg(xhr.responseText || '批量删除失败', { icon: 2 }); } }); layer.close(index); @@ -350,107 +352,107 @@ }); // 表格工具栏事件 - table.on('tool(variablesTableFilter)', function(obj) { + table.on('tool(variablesTableFilter)', function (obj) { const data = obj.data; - + if (obj.event === 'edit') { // 编辑 console.log('编辑按钮被点击', data); $('#variableForm')[0].reset(); $('input[name="uuid"]').val(data.uuid); - + // 重新加载应用列表,然后设置选中值 loadApps(); - setTimeout(function() { + setTimeout(function () { $('select[name="app_uuid"]').val(data.app_uuid); form.render('select'); }, 100); - + $('input[name="alias"]').val(data.alias); $('textarea[name="data"]').val(data.data); $('textarea[name="remark"]').val(data.remark); - + layer.open({ type: 1, title: '编辑变量', content: $('#variableFormLayer'), area: ['500px', '460px'], btn: ['保存', '取消'], - yes: function(index, layero) { + yes: function (index, layero) { // 手动收集表单数据 var formData = {}; - $('#variableForm').find('input, select, textarea').each(function() { + $('#variableForm').find('input, select, textarea').each(function () { var $this = $(this); var name = $this.attr('name'); if (name && name !== 'id') { formData[name] = $this.val(); } }); - + console.log('编辑变量 - 收集到的表单数据:', formData); - + // 验证必填字段 if (!formData.app_uuid || formData.app_uuid.trim() === '') { - layer.msg('应用UUID不能为空', {icon: 2}); + layer.msg('应用UUID不能为空', { icon: 2 }); return; } if (!formData.alias || formData.alias.trim() === '') { - layer.msg('请输入变量别名', {icon: 2}); + layer.msg('请输入变量别名', { icon: 2 }); return; } if (!formData.data || formData.data.trim() === '') { - layer.msg('请输入变量数据', {icon: 2}); + layer.msg('请输入变量数据', { icon: 2 }); return; } - + console.log('编辑变量 - 发送的JSON数据:', JSON.stringify(formData)); - + $.ajax({ url: '/admin/variable/update', type: 'POST', data: JSON.stringify(formData), contentType: 'application/json', - success: function(res) { + success: function (res) { if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); + layer.msg(res.msg, { icon: 1 }); layer.close(index); variablesTable.reload(); } else { - layer.msg(res.msg || '操作失败', {icon: 2}); + layer.msg(res.msg || '操作失败', { icon: 2 }); } }, - error: function(xhr) { - layer.msg(xhr.responseText || '操作失败', {icon: 2}); + error: function (xhr) { + layer.msg(xhr.responseText || '操作失败', { icon: 2 }); } }); }, - btn2: function(index) { + btn2: function (index) { layer.close(index); }, - success: function() { + success: function () { form.render(); }, shadeClose: false }); - + } else if (obj.event === 'del') { // 删除 - layer.confirm('确定删除该变量吗?', {icon: 3, title: '提示'}, function(index) { + layer.confirm('确定删除该变量吗?', { icon: 3, title: '提示' }, function (index) { $.ajax({ url: '/admin/variable/delete', type: 'POST', - data: JSON.stringify({id: data.id}), + data: JSON.stringify({ id: data.id }), contentType: 'application/json', - success: function(res) { + success: function (res) { if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); + layer.msg(res.msg, { icon: 1 }); variablesTable.reload(); } else { - layer.msg(res.msg || '删除失败', {icon: 2}); + layer.msg(res.msg || '删除失败', { icon: 2 }); } }, - error: function(xhr) { - layer.msg(xhr.responseText || '删除失败', {icon: 2}); + error: function (xhr) { + layer.msg(xhr.responseText || '删除失败', { icon: 2 }); } }); layer.close(index); diff --git a/web/template/index.html b/web/template/index.html index 2a794a1..734a2c4 100644 --- a/web/template/index.html +++ b/web/template/index.html @@ -1,5 +1,6 @@ + {{.SystemName}} - 生活就像愤怒的小鸟,失败后总有几只猪在笑。 @@ -17,9 +18,10 @@ - + - - -
- -
-
-
-

系统提醒

-
-
-
-
🚫 未授权,拒绝访问
-
-
-
💬 如有问题,请联系网站管理员
-
-
- - {{if or .ICPRecord .PSBRecord}}
{{if .ICPRecord}}{{.ICPRecord}}{{end}}{{if and .ICPRecord .PSBRecord}} {{end}}{{if .PSBRecord}}{{.PSBRecord}}{{end}}
{{end}} -
-
- - - + + + }; + + // 启动粒子系统 + initParticles(); + addMouseInteraction(); + animate(); + + \ No newline at end of file