mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
Fix the authentication mechanism
Fix filter search
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,9 +18,10 @@ import (
|
|||||||
// LoginPageHandler 管理员登录页渲染处理器
|
// LoginPageHandler 管理员登录页渲染处理器
|
||||||
// - 如果已登录则重定向到 /admin
|
// - 如果已登录则重定向到 /admin
|
||||||
// - 否则渲染 web/template/admin/login.html 模板
|
// - 否则渲染 web/template/admin/login.html 模板
|
||||||
|
// - 自动清理失效的JWT Cookie,避免刷新时的问题
|
||||||
func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// 已登录直接跳转到后台布局
|
// 使用带清理功能的JWT校验,避免失效Cookie在登录页面造成问题
|
||||||
if IsAdminAuthenticated(r) {
|
if IsAdminAuthenticatedWithCleanup(w, r) {
|
||||||
http.Redirect(w, r, "/admin", http.StatusFound)
|
http.Redirect(w, r, "/admin", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -118,6 +118,20 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// - 确保令牌完全失效
|
// - 确保令牌完全失效
|
||||||
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// 清理JWT Cookie
|
// 清理JWT Cookie
|
||||||
|
clearInvalidJWTCookie(w)
|
||||||
|
|
||||||
|
// 可选:将JWT令牌加入黑名单(需要Redis或数据库支持)
|
||||||
|
// 这里可以实现JWT黑名单机制
|
||||||
|
|
||||||
|
utils.JsonResponse(w, http.StatusOK, true, "已退出登录", map[string]interface{}{
|
||||||
|
"redirect": "/admin/login",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearInvalidJWTCookie 清理失效的JWT Cookie
|
||||||
|
// - 统一的Cookie清理函数,确保一致性
|
||||||
|
// - 在JWT校验失败时自动调用,提升安全性和用户体验
|
||||||
|
func clearInvalidJWTCookie(w http.ResponseWriter) {
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: "admin_session",
|
Name: "admin_session",
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -128,13 +142,6 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
Expires: time.Unix(0, 0), // 确保过期
|
Expires: time.Unix(0, 0), // 确保过期
|
||||||
}
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
// 可选:将JWT令牌加入黑名单(需要Redis或数据库支持)
|
|
||||||
// 这里可以实现JWT黑名单机制
|
|
||||||
|
|
||||||
utils.JsonResponse(w, http.StatusOK, true, "已退出登录", map[string]interface{}{
|
|
||||||
"redirect": "/admin/login",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT密钥(生产环境应从配置文件或环境变量读取)
|
// JWT密钥(生产环境应从配置文件或环境变量读取)
|
||||||
@@ -142,9 +149,10 @@ var jwtSecret = []byte(viper.GetString("security.jwt_secret"))
|
|||||||
|
|
||||||
// JWTClaims JWT载荷结构
|
// JWTClaims JWT载荷结构
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
UserID uint `json:"user_id"`
|
UserUUID string `json:"user_uuid"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role int `json:"role"`
|
Role int `json:"role"`
|
||||||
|
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,16 +161,20 @@ type JWTClaims struct {
|
|||||||
// - 设置24小时过期时间
|
// - 设置24小时过期时间
|
||||||
// - 使用HMAC-SHA256签名
|
// - 使用HMAC-SHA256签名
|
||||||
func generateJWTToken(user models.User) (string, error) {
|
func generateJWTToken(user models.User) (string, error) {
|
||||||
|
// 生成密码哈希摘要(使用SHA256)
|
||||||
|
passwordHashDigest := utils.GenerateSHA256Hash(user.Password)
|
||||||
|
|
||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
UserID: user.ID,
|
UserUUID: user.UUID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
|
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
Issuer: "凌动技术",
|
Issuer: "凌动技术",
|
||||||
Subject: strconv.Itoa(int(user.ID)),
|
Subject: user.UUID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +225,96 @@ func IsAdminAuthenticated(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选:进一步验证用户是否仍然存在且有效
|
// 验证用户是否仍然存在于数据库中
|
||||||
// 这里可以添加数据库查询来验证用户状态
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
|
||||||
|
// 记录安全事件:用户不存在但持有有效JWT令牌
|
||||||
|
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected - User not found: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
|
||||||
|
if user.Username != claims.Username {
|
||||||
|
// 记录安全事件:用户名不匹配
|
||||||
|
fmt.Printf("[SECURITY WARNING] Username mismatch detected - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
|
||||||
|
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
|
||||||
|
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
|
||||||
|
if claims.PasswordHash != currentPasswordHash {
|
||||||
|
// 记录安全事件:密码哈希不匹配,可能密码已被修改
|
||||||
|
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
|
||||||
|
// - 当JWT校验失败时,自动清理失效的Cookie
|
||||||
|
// - 适用于API接口等需要清理失效令牌的场景
|
||||||
|
func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
cookie, err := r.Cookie("admin_session")
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析并验证JWT令牌
|
||||||
|
claims, err := parseJWTToken(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
// JWT解析失败,清理失效Cookie
|
||||||
|
clearInvalidJWTCookie(w)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户角色(只允许管理员角色=0)
|
||||||
|
if claims.Role != 0 {
|
||||||
|
clearInvalidJWTCookie(w)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户是否仍然存在于数据库中
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
|
||||||
|
// 记录安全事件并清理失效Cookie
|
||||||
|
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected - User not found: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
clearInvalidJWTCookie(w)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
|
||||||
|
if user.Username != claims.Username {
|
||||||
|
// 记录安全事件并清理失效Cookie
|
||||||
|
fmt.Printf("[SECURITY WARNING] Username mismatch detected - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
|
||||||
|
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
|
||||||
|
clearInvalidJWTCookie(w)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
|
||||||
|
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
|
||||||
|
if claims.PasswordHash != currentPasswordHash {
|
||||||
|
// 记录安全事件并清理失效Cookie
|
||||||
|
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
clearInvalidJWTCookie(w)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -238,6 +338,37 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
|||||||
return nil, fmt.Errorf("权限不足")
|
return nil, fmt.Errorf("权限不足")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证用户是否仍然存在于数据库中
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("数据库连接失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
|
||||||
|
// 记录安全事件:用户不存在但持有有效JWT令牌
|
||||||
|
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected in GetCurrentAdminUser - User not found: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
return nil, fmt.Errorf("用户不存在或权限已变更")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
|
||||||
|
if user.Username != claims.Username {
|
||||||
|
// 记录安全事件:用户名不匹配
|
||||||
|
fmt.Printf("[SECURITY WARNING] Username mismatch detected in GetCurrentAdminUser - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
|
||||||
|
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
|
||||||
|
return nil, fmt.Errorf("用户信息已变更,请重新登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
|
||||||
|
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
|
||||||
|
if claims.PasswordHash != currentPasswordHash {
|
||||||
|
// 记录安全事件:密码哈希不匹配,可能密码已被修改
|
||||||
|
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected in GetCurrentAdminUser - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
return nil, fmt.Errorf("密码已变更,请重新登录")
|
||||||
|
}
|
||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,13 +391,44 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW
|
|||||||
return nil, false, fmt.Errorf("权限不足")
|
return nil, false, fmt.Errorf("权限不足")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证用户是否仍然存在于数据库中
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("数据库连接失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
|
||||||
|
// 记录安全事件:用户不存在但持有有效JWT令牌
|
||||||
|
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected in GetCurrentAdminUserWithRefresh - User not found: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
return nil, false, fmt.Errorf("用户不存在或权限已变更")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
|
||||||
|
if user.Username != claims.Username {
|
||||||
|
// 记录安全事件:用户名不匹配
|
||||||
|
fmt.Printf("[SECURITY WARNING] Username mismatch detected in GetCurrentAdminUserWithRefresh - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
|
||||||
|
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
|
||||||
|
return nil, false, fmt.Errorf("用户信息已变更,请重新登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
|
||||||
|
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
|
||||||
|
if claims.PasswordHash != currentPasswordHash {
|
||||||
|
// 记录安全事件:密码哈希不匹配,可能密码已被修改
|
||||||
|
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected in GetCurrentAdminUserWithRefresh - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
|
||||||
|
claims.UserUUID, claims.Username, r.RemoteAddr)
|
||||||
|
return nil, false, fmt.Errorf("密码已变更,请重新登录")
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否需要刷新令牌(根据配置的阈值)
|
// 检查是否需要刷新令牌(根据配置的阈值)
|
||||||
refreshed := false
|
refreshed := false
|
||||||
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour
|
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour
|
||||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
||||||
// 生成新的JWT令牌
|
// 生成新的JWT令牌
|
||||||
user := models.User{
|
user := models.User{
|
||||||
ID: claims.UserID,
|
UUID: claims.UserUUID,
|
||||||
Username: claims.Username,
|
Username: claims.Username,
|
||||||
Role: claims.Role,
|
Role: claims.Role,
|
||||||
}
|
}
|
||||||
@@ -301,6 +463,9 @@ func AdminAuthRequired(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
// 尝试获取用户信息并自动刷新令牌
|
// 尝试获取用户信息并自动刷新令牌
|
||||||
claims, refreshed, err := GetCurrentAdminUserWithRefresh(w, r)
|
claims, refreshed, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 自动清理失效的JWT Cookie,提升安全性和用户体验
|
||||||
|
clearInvalidJWTCookie(w)
|
||||||
|
|
||||||
// 中文注释:区分普通页面请求与AJAX/JSON请求
|
// 中文注释:区分普通页面请求与AJAX/JSON请求
|
||||||
// - 对 AJAX/JSON:直接返回 401 JSON,便于前端处理(如提示重新登录)
|
// - 对 AJAX/JSON:直接返回 401 JSON,便于前端处理(如提示重新登录)
|
||||||
// - 对普通页面:保持原有重定向到登录页
|
// - 对普通页面:保持原有重定向到登录页
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import (
|
|||||||
// AdminIndexHandler /admin 与 /admin/ 根路径入口
|
// AdminIndexHandler /admin 与 /admin/ 根路径入口
|
||||||
// - 未登录:重定向到 /admin/login
|
// - 未登录:重定向到 /admin/login
|
||||||
// - 已登录:渲染后台布局页(或重定向到 /admin/layout)
|
// - 已登录:渲染后台布局页(或重定向到 /admin/layout)
|
||||||
|
// - 自动清理失效的JWT Cookie
|
||||||
func AdminIndexHandler(w http.ResponseWriter, r *http.Request) {
|
func AdminIndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if IsAdminAuthenticated(r) {
|
if IsAdminAuthenticatedWithCleanup(w, r) {
|
||||||
// 直接渲染布局页,保持URL为 /admin
|
// 直接渲染布局页,保持URL为 /admin
|
||||||
AdminLayoutHandler(w, r)
|
AdminLayoutHandler(w, r)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package admin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"networkDev/database"
|
"networkDev/database"
|
||||||
"networkDev/models"
|
"networkDev/models"
|
||||||
@@ -16,7 +17,7 @@ func UserFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserProfileQueryHandler 查询当前登录管理员的基本信息
|
// UserProfileQueryHandler 查询当前登录管理员的基本信息
|
||||||
// - 返回 id/username/role 三个字段
|
// - 返回 uuid/username/role/created_at 四个字段
|
||||||
// - 自动刷新接近过期的JWT令牌
|
// - 自动刷新接近过期的JWT令牌
|
||||||
func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
@@ -30,10 +31,24 @@ func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询用户完整信息以获取创建时间
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
utils.JsonResponse(w, http.StatusOK, true, "ok", map[string]interface{}{
|
utils.JsonResponse(w, http.StatusOK, true, "ok", map[string]interface{}{
|
||||||
"id": claims.UserID,
|
"uuid": user.UUID,
|
||||||
"username": claims.Username,
|
"username": user.Username,
|
||||||
"role": claims.Role,
|
"role": user.Role,
|
||||||
|
"created_at": user.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +106,7 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 查询当前用户
|
// 查询当前用户
|
||||||
var user models.User
|
var user models.User
|
||||||
if dbErr := db.First(&user, claims.UserID).Error; dbErr != nil {
|
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil {
|
||||||
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -105,30 +120,59 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 生成新的密码盐值
|
// 生成新的密码盐值
|
||||||
newSalt, err := utils.GenerateRandomSalt()
|
newSalt, err := utils.GenerateRandomSalt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 添加详细错误日志
|
||||||
|
fmt.Printf("生成密码盐失败: %v\n", err)
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Printf("成功生成新盐值,长度: %d\n", len(newSalt))
|
||||||
|
|
||||||
// 使用新盐值生成密码哈希
|
// 使用新盐值生成密码哈希
|
||||||
hash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
hash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 添加详细错误日志
|
||||||
|
fmt.Printf("生成密码哈希失败: %v, 密码长度: %d, 盐值长度: %d\n", err, len(body.NewPassword), len(newSalt))
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Printf("成功生成密码哈希,长度: %d\n", len(hash))
|
||||||
|
|
||||||
// 更新密码和盐值
|
// 更新密码和盐值
|
||||||
if err := db.Model(&models.User{}).Where("id = ?", claims.UserID).Updates(map[string]interface{}{
|
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Updates(map[string]interface{}{
|
||||||
"password": hash,
|
"password": hash,
|
||||||
"password_salt": newSalt,
|
"password_salt": newSalt,
|
||||||
}).Error; err != nil {
|
}).Error; dbErr != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选:安全起见,通知前端跳转到登录页
|
// 重新查询用户信息(包含新密码)
|
||||||
utils.JsonResponse(w, http.StatusOK, true, "密码修改成功,请重新登录", map[string]interface{}{
|
var updatedUser models.User
|
||||||
"redirect": "/admin/login",
|
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&updatedUser).Error; dbErr != nil {
|
||||||
})
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询用户信息失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新生成JWT令牌(包含新的密码哈希摘要)
|
||||||
|
newToken, err := generateJWTToken(updatedUser)
|
||||||
|
if err != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新Cookie
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "admin_session",
|
||||||
|
Value: newToken,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: false, // 生产环境应设置为true(HTTPS)
|
||||||
|
MaxAge: 24 * 60 * 60, // 24小时
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
|
// 密码修改成功,已重新生成JWT令牌
|
||||||
|
utils.JsonResponse(w, http.StatusOK, true, "密码修改成功", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserProfileUpdateHandler 修改当前登录管理员的用户名
|
// UserProfileUpdateHandler 修改当前登录管理员的用户名
|
||||||
@@ -173,9 +217,9 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查唯一性:排除当前用户ID
|
// 检查唯一性:排除当前用户UUID
|
||||||
var cnt int64
|
var cnt int64
|
||||||
if dbErr := db.Model(&models.User{}).Where("username = ? AND id <> ?", username, claims.UserID).Count(&cnt).Error; dbErr != nil {
|
if dbErr := db.Model(&models.User{}).Where("username = ? AND uuid <> ?", username, claims.UserUUID).Count(&cnt).Error; dbErr != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查用户名唯一性失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查用户名唯一性失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -199,7 +243,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// 查询当前用户并校验旧密码
|
// 查询当前用户并校验旧密码
|
||||||
var user models.User
|
var user models.User
|
||||||
if dbErr := db.First(&user, claims.UserID).Error; dbErr != nil {
|
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil {
|
||||||
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -210,13 +254,13 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行更新
|
// 执行更新
|
||||||
if dbErr := db.Model(&models.User{}).Where("id = ?", claims.UserID).Update("username", username).Error; dbErr != nil {
|
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Update("username", username).Error; dbErr != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新用户名失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新用户名失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新签发JWT并写入Cookie
|
// 重新签发JWT并写入Cookie
|
||||||
newUser := models.User{ID: claims.UserID, Username: username, Role: claims.Role}
|
newUser := models.User{UUID: claims.UserUUID, Username: username, Role: claims.Role}
|
||||||
token, err := generateJWTToken(newUser)
|
token, err := generateJWTToken(newUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
||||||
|
|||||||
@@ -40,8 +40,13 @@ func VariableListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 获取应用UUID参数(用于按应用筛选变量)
|
// 获取应用UUID参数(用于按应用筛选变量)
|
||||||
appUUID := strings.TrimSpace(r.URL.Query().Get("app_uuid"))
|
appUUID := strings.TrimSpace(r.URL.Query().Get("app_uuid"))
|
||||||
|
|
||||||
// 获取别名搜索参数
|
// 获取搜索关键词参数(支持编号、别名、数据的综合搜索)
|
||||||
alias := strings.TrimSpace(r.URL.Query().Get("alias"))
|
search := strings.TrimSpace(r.URL.Query().Get("search"))
|
||||||
|
|
||||||
|
// 兼容旧的别名搜索参数
|
||||||
|
if search == "" {
|
||||||
|
search = strings.TrimSpace(r.URL.Query().Get("alias"))
|
||||||
|
}
|
||||||
|
|
||||||
// 构建查询
|
// 构建查询
|
||||||
db, err := database.GetDB()
|
db, err := database.GetDB()
|
||||||
@@ -59,9 +64,10 @@ func VariableListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
query = query.Where("app_uuid = ?", appUUID)
|
query = query.Where("app_uuid = ?", appUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果指定了别名搜索,则按别名模糊搜索
|
// 如果指定了搜索关键词,则在编号、别名、数据、备注中进行模糊搜索
|
||||||
if alias != "" {
|
if search != "" {
|
||||||
query = query.Where("alias LIKE ?", "%"+alias+"%")
|
query = query.Where("number LIKE ? OR alias LIKE ? OR data LIKE ? OR remark LIKE ?",
|
||||||
|
"%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取总数
|
// 获取总数
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// SeedDefaultAdmin 初始化默认管理员账号
|
// SeedDefaultAdmin 初始化默认管理员账号
|
||||||
// - 如果用户名为 admin 的用户已存在,则跳过
|
// - 如果用户名为 admin 的用户已存在,则跳过
|
||||||
// - 如不存在,则创建用户名为 admin、密码为 admin123(以 bcrypt 哈希存储)、角色 Role=0 的管理员
|
// - 如不存在,则创建用户名为 admin、密码为 admin123(以 bcrypt 哈希存储)、角色 Role=0 的管理员
|
||||||
// - 根据需求:默认 admin 用户的 ID 固定为 10000
|
// - ID和UUID将自动生成
|
||||||
func SeedDefaultAdmin() error {
|
func SeedDefaultAdmin() error {
|
||||||
db, err := GetDB()
|
db, err := GetDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,9 +38,8 @@ func SeedDefaultAdmin() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建默认管理员(指定固定 ID=10000)
|
// 创建默认管理员(ID和UUID将自动生成)
|
||||||
admin := models.User{
|
admin := models.User{
|
||||||
ID: 10000,
|
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Password: hash,
|
Password: hash,
|
||||||
PasswordSalt: salt,
|
PasswordSalt: salt,
|
||||||
@@ -49,6 +48,6 @@ func SeedDefaultAdmin() error {
|
|||||||
if err := db.Create(&admin).Error; err != nil {
|
if err := db.Create(&admin).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logrus.WithField("username", "admin").WithField("id", admin.ID).Info("默认管理员创建成功")
|
logrus.WithField("username", "admin").WithField("uuid", admin.UUID).Info("默认管理员创建成功")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
// User 用户表模型
|
// User 用户表模型
|
||||||
// 说明:PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64
|
// 说明:PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `gorm:"primaryKey;comment:用户ID,自增主键"`
|
ID uint `gorm:"primaryKey;comment:用户ID,自增主键"`
|
||||||
|
UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"`
|
||||||
Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"`
|
Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"`
|
||||||
Password string `gorm:"size:255;not null;comment:密码哈希值"`
|
Password string `gorm:"size:255;not null;comment:密码哈希值"`
|
||||||
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"`
|
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"`
|
||||||
@@ -14,3 +20,12 @@ type User struct {
|
|||||||
CreatedAt time.Time `gorm:"comment:创建时间"`
|
CreatedAt time.Time `gorm:"comment:创建时间"`
|
||||||
UpdatedAt time.Time `gorm:"comment:更新时间"`
|
UpdatedAt time.Time `gorm:"comment:更新时间"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate 在创建记录前自动生成UUID
|
||||||
|
func (user *User) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
// 生成UUID
|
||||||
|
if user.UUID == "" {
|
||||||
|
user.UUID = strings.ToUpper(uuid.New().String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -299,8 +299,8 @@ func HashPasswordWithSalt(password, salt string) (string, error) {
|
|||||||
// 将密码和盐值组合
|
// 将密码和盐值组合
|
||||||
combined := password + salt
|
combined := password + salt
|
||||||
|
|
||||||
// 使用bcrypt进行哈希(成本因子12,平衡安全性和性能)
|
// 使用bcrypt进行哈希(成本因子10,平衡安全性和性能)
|
||||||
hashed, err := bcrypt.GenerateFromPassword([]byte(combined), 12)
|
hashed, err := bcrypt.GenerateFromPassword([]byte(combined), 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -321,3 +321,10 @@ func VerifyPasswordWithSalt(password, salt, hashedPassword string) bool {
|
|||||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(combined))
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(combined))
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateSHA256Hash 生成字符串的SHA256哈希值
|
||||||
|
// 用于生成密码哈希摘要,用于JWT验证
|
||||||
|
func GenerateSHA256Hash(input string) string {
|
||||||
|
hash := sha256.Sum256([]byte(input))
|
||||||
|
return fmt.Sprintf("%x", hash)
|
||||||
|
}
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ layui.use(['table', 'form', 'layer', 'dropdown'], function() {
|
|||||||
cols: [[
|
cols: [[
|
||||||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||||||
{ field: 'app_name', title: '应用名称', minWidth: 150 },
|
{ field: 'app_name', title: '应用名称', minWidth: 150 },
|
||||||
|
{ field: 'uuid', title: 'UUID', minWidth: 335 },
|
||||||
{ field: 'api_type_name', title: '接口类型', minWidth: 120 },
|
{ field: 'api_type_name', title: '接口类型', minWidth: 120 },
|
||||||
{
|
{
|
||||||
field: 'status_name',
|
field: 'status_name',
|
||||||
|
|||||||
@@ -1,63 +1,251 @@
|
|||||||
{{ define "user.html" }}
|
{{ define "user.html" }}
|
||||||
<div class="layui-card">
|
<style>
|
||||||
<div class="layui-card-header">个人资料</div>
|
/* 基础模块样式 */
|
||||||
<div class="layui-card-body">
|
.user-module {
|
||||||
<form class="layui-form" id="accountForm" lay-filter="accountForm" onsubmit="return false">
|
margin-bottom: 20px;
|
||||||
<!-- 按照要求纵向排序:ID、角色、用户名、旧密码、新密码、确认新密码 -->
|
}
|
||||||
<div class="layui-form-item">
|
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-id">ID</label>
|
|
||||||
<div class="layui-input-block">
|
|
||||||
<input type="text" name="id" disabled readonly class="layui-input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layui-form-item">
|
.user-module .layui-card-header {
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-role">角色</label>
|
font-weight: 600;
|
||||||
<div class="layui-input-block">
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
<!-- 角色禁用与只读,仅作展示用途,显示中文标签"管理员/普通成员" -->
|
}
|
||||||
<input type="text" name="role" disabled readonly class="layui-input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layui-form-item">
|
.module-tabs {
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-username">用户名</label>
|
margin-bottom: 20px;
|
||||||
<div class="layui-input-block">
|
}
|
||||||
<input type="text" name="username" placeholder="请输入用户名(不修改可留空或保持不变)" autocomplete="off" class="layui-input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layui-form-item">
|
.module-tabs .layui-tab-title li {
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-old-password">旧密码</label>
|
font-weight: 500;
|
||||||
<div class="layui-input-block">
|
}
|
||||||
<!-- 不修改密码时可留空 -->
|
|
||||||
<input type="password" name="old_password" placeholder="不修改可留空" autocomplete="off" class="layui-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layui-form-item">
|
.readonly-field {
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-new-password">新密码</label>
|
cursor: not-allowed !important;
|
||||||
<div class="layui-input-block">
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
<!-- 不修改密码时可留空 -->
|
}
|
||||||
<input type="password" name="new_password" placeholder="不修改可留空(至少6位)" autocomplete="off" class="layui-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layui-form-item">
|
/* 浅色模式样式 */
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-confirm-password">确认密码</label>
|
:root {
|
||||||
<div class="layui-input-block">
|
--user-card-header-bg: #f8f9fa;
|
||||||
<!-- 不修改密码时可留空 -->
|
--user-card-header-color: #333;
|
||||||
<input type="password" name="confirm_password" placeholder="不修改可留空" autocomplete="off" class="layui-input">
|
--user-readonly-bg: #f5f5f5;
|
||||||
</div>
|
--user-readonly-color: #666;
|
||||||
</div>
|
--user-card-bg: #ffffff;
|
||||||
|
--user-card-border: #e6e6e6;
|
||||||
|
--user-input-bg: #ffffff;
|
||||||
|
--user-input-border: #d9d9d9;
|
||||||
|
--user-input-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="layui-form-item">
|
/* 深色模式样式 */
|
||||||
<div class="layui-input-block">
|
@media (prefers-color-scheme: dark) {
|
||||||
<button class="layui-btn" lay-submit lay-filter="submitAccount">保存更改</button>
|
:root {
|
||||||
<!-- 将原先 type="reset" 改为自定义按钮,避免浏览器重置成初始空值 -->
|
--user-card-header-bg: #2f2f2f;
|
||||||
<button type="button" id="btnReset" class="layui-btn layui-btn-primary">重置</button>
|
--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 class="layui-form-item">
|
||||||
|
<label class="layui-form-label">用户组</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="role" disabled readonly class="layui-input readonly-field" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">用户名</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="username" disabled readonly class="layui-input readonly-field" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">创建时间</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="created_at" disabled readonly class="layui-input readonly-field" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码模块 -->
|
||||||
|
<div class="layui-tab-item">
|
||||||
|
<div class="layui-card user-module">
|
||||||
|
<div class="layui-card-header">
|
||||||
|
<i class="layui-icon layui-icon-password"></i> 修改密码
|
||||||
|
</div>
|
||||||
|
<div class="layui-card-body">
|
||||||
|
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">当前密码</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="password" name="old_password" placeholder="请输入当前密码" autocomplete="off" class="layui-input" lay-verify="required" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">新密码</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="password" name="new_password" placeholder="请输入新密码(至少6位)" autocomplete="off" class="layui-input" lay-verify="required" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">确认密码</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="password" name="confirm_password" placeholder="请再次输入新密码" autocomplete="off" class="layui-input" lay-verify="required" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<button class="layui-btn" lay-submit lay-filter="submitPassword">
|
||||||
|
<i class="layui-icon layui-icon-ok"></i> 修改密码
|
||||||
|
</button>
|
||||||
|
<button type="button" id="resetPasswordBtn" class="layui-btn layui-btn-primary">
|
||||||
|
<i class="layui-icon layui-icon-refresh"></i> 重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改用户名模块 -->
|
||||||
|
<div class="layui-tab-item">
|
||||||
|
<div class="layui-card user-module">
|
||||||
|
<div class="layui-card-header">
|
||||||
|
<i class="layui-icon layui-icon-edit"></i> 修改用户名
|
||||||
|
</div>
|
||||||
|
<div class="layui-card-body">
|
||||||
|
<form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false">
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">当前用户名</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="current_username" disabled readonly class="layui-input readonly-field" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">新用户名</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="new_username" placeholder="请输入新用户名" autocomplete="off" class="layui-input" lay-verify="required" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">当前密码</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="password" name="password" placeholder="请输入当前密码以确认身份" autocomplete="off" class="layui-input" lay-verify="required" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<button class="layui-btn" lay-submit lay-filter="submitUsername">
|
||||||
|
<i class="layui-icon layui-icon-ok"></i> 修改用户名
|
||||||
|
</button>
|
||||||
|
<button type="button" id="resetUsernameBtn" class="layui-btn layui-btn-primary">
|
||||||
|
<i class="layui-icon layui-icon-refresh"></i> 重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,15 +253,19 @@
|
|||||||
// 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突
|
// 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突
|
||||||
(() => {
|
(() => {
|
||||||
// 工具方法:将数值角色转为中文标签
|
// 工具方法:将数值角色转为中文标签
|
||||||
// 0 => 管理员,1 => 普通成员
|
|
||||||
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 ? '管理员' : '普通成员'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果未加载 layui,则按需加载(兼容用户直接访问片段页 /admin/user)
|
// 格式化时间
|
||||||
// 说明:当 window.layui 不存在时,动态引入 Layui 的 CSS 和 JS,加载完成后再执行页面逻辑
|
const formatTime = (timeStr) => {
|
||||||
|
if (!timeStr) return ''
|
||||||
|
const date = new Date(timeStr)
|
||||||
|
return date.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果未加载 layui,则按需加载
|
||||||
const ensureLayui = () => new Promise((resolve) => {
|
const ensureLayui = () => new Promise((resolve) => {
|
||||||
if (window.layui) return resolve(window.layui)
|
if (window.layui) return resolve(window.layui)
|
||||||
const css = document.createElement('link')
|
const css = document.createElement('link')
|
||||||
@@ -88,166 +280,190 @@
|
|||||||
|
|
||||||
// 在确保 Layui 可用后再执行页面逻辑
|
// 在确保 Layui 可用后再执行页面逻辑
|
||||||
ensureLayui().then(() => {
|
ensureLayui().then(() => {
|
||||||
layui.use(['form', 'layer'], () => {
|
layui.use(['form', 'layer', 'element'], () => {
|
||||||
const form = layui.form
|
const form = layui.form
|
||||||
const layer = layui.layer
|
const layer = layui.layer
|
||||||
|
const element = layui.element
|
||||||
|
|
||||||
// 记录初始用户名,用于判断是否需要更新
|
// 全局变量
|
||||||
let initialUsername = ''
|
let userProfile = null
|
||||||
// 缓存最近一次加载到表单中的资料,用于“重置”恢复
|
|
||||||
let lastProfile = null
|
|
||||||
|
|
||||||
// 加载个人资料:填充ID/用户名/角色(角色显示中文标签并禁用)
|
// 加载个人资料
|
||||||
// 返回:无;副作用:设置 initialUsername、lastProfile 与表单值
|
|
||||||
const loadProfile = async () => {
|
const loadProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/admin/api/user/profile')
|
const res = await fetch('/admin/api/user/profile')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const ok = (data.success === true) || (data.code === 0)
|
const ok = (data.success === true) || (data.code === 0)
|
||||||
if (!ok) throw new Error(data.message || data.msg || '加载失败')
|
if (!ok) throw new Error(data.message || data.msg || '加载失败')
|
||||||
const payload = data.data || {}
|
|
||||||
initialUsername = payload.username || ''
|
userProfile = data.data || {}
|
||||||
// 将角色转换为中文展示,并缓存为最近一次加载的“默认值”
|
|
||||||
const display = { ...payload, role: roleToText(payload.role) }
|
// 填充个人资料表单
|
||||||
lastProfile = display
|
const profileData = {
|
||||||
form.val('accountForm', display)
|
...userProfile,
|
||||||
|
role: roleToText(userProfile.role),
|
||||||
|
created_at: formatTime(userProfile.created_at)
|
||||||
|
}
|
||||||
|
form.val('profileForm', profileData)
|
||||||
|
|
||||||
|
// 填充用户名修改表单的当前用户名
|
||||||
|
form.val('usernameForm', { current_username: userProfile.username })
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
|
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验密码表单:当任一密码字段填写时,要求三个字段均填写且有效
|
// 修改密码模块
|
||||||
// 返回:{ ok: boolean, msg?: string }
|
const PasswordModule = {
|
||||||
const validatePassword = (fields) => {
|
validate: (fields) => {
|
||||||
const oldPwd = (fields.old_password || '').trim()
|
const { old_password, new_password, confirm_password } = fields
|
||||||
const newPwd = (fields.new_password || '').trim()
|
|
||||||
const confirmPwd = (fields.confirm_password || '').trim()
|
if (!old_password || !new_password || !confirm_password) {
|
||||||
const anyFilled = !!(oldPwd || newPwd || confirmPwd)
|
return { ok: false, msg: '请填写完整的密码信息' }
|
||||||
if (!anyFilled) return { ok: true }
|
}
|
||||||
if (!oldPwd || !newPwd || !confirmPwd) return { ok: false, msg: '请完整填写旧密码/新密码/确认新密码' }
|
|
||||||
if (newPwd.length < 6) return { ok: false, msg: '新密码长度不能少于6位' }
|
if (new_password.length < 6) {
|
||||||
if (newPwd !== confirmPwd) return { ok: false, msg: '两次输入的新密码不一致' }
|
return { ok: false, msg: '新密码长度不能少于6位' }
|
||||||
if (oldPwd === newPwd) return { ok: false, msg: '新密码不能与旧密码相同' }
|
}
|
||||||
return { ok: true }
|
|
||||||
}
|
if (new_password !== confirm_password) {
|
||||||
|
return { ok: false, msg: '两次输入的新密码不一致' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old_password === new_password) {
|
||||||
|
return { ok: false, msg: '新密码不能与当前密码相同' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
},
|
||||||
|
|
||||||
// 更新用户名:传输 username 与 old_password(当仅修改用户名时必须提供当前密码;同时修改密码时沿用同一 old_password)
|
submit: async (fields) => {
|
||||||
// 返回:Promise<void>
|
const validation = PasswordModule.validate(fields)
|
||||||
const updateUsername = async (username, oldPassword) => {
|
if (!validation.ok) {
|
||||||
const payload = { username }
|
layer.msg(validation.msg, { icon: 2 })
|
||||||
if (oldPassword) payload.old_password = oldPassword
|
|
||||||
const res = await fetch('/admin/api/user/profile/update', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
const ok = (data.success === true) || (data.code === 0)
|
|
||||||
if (!ok) throw new Error(data.message || data.msg || '保存资料失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新密码:仅传输旧/新/确认三个字段
|
|
||||||
// 返回:Promise<any> 后端响应数据,用于可能的重定向处理
|
|
||||||
const updatePassword = async (fields) => {
|
|
||||||
const payload = {
|
|
||||||
old_password: fields.old_password,
|
|
||||||
new_password: fields.new_password,
|
|
||||||
confirm_password: fields.confirm_password
|
|
||||||
}
|
|
||||||
const res = await fetch('/admin/api/user/password', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
const ok = (data.success === true) || (data.code === 0)
|
|
||||||
if (!ok) throw new Error(data.message || data.msg || '修改密码失败')
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交综合更新:
|
|
||||||
// 规则:
|
|
||||||
// - 用户名:仅当与 initialUsername 不同且非空时更新
|
|
||||||
// - 密码:当任一密码字段填写时,要求完整校验并更新;若均未填则不更新
|
|
||||||
// - 若两者均无改动,则提示“未修改任何内容”
|
|
||||||
form.on('submit(submitAccount)', async (obj) => {
|
|
||||||
const fields = obj.field
|
|
||||||
const desiredUsername = (fields.username || '').trim()
|
|
||||||
const needUpdateUsername = desiredUsername && desiredUsername !== initialUsername
|
|
||||||
|
|
||||||
// 判定密码相关输入:
|
|
||||||
// - wantChangePassword:输入了新密码或确认密码,视为尝试修改密码(将要求三个字段都填写)
|
|
||||||
// - onlyOldProvided:仅输入了旧密码,用于支持“仅修改用户名需要当前密码”的场景
|
|
||||||
const hasOld = !!(fields.old_password && fields.old_password.trim())
|
|
||||||
const hasNewOrConfirm = !!((fields.new_password && fields.new_password.trim()) || (fields.confirm_password && fields.confirm_password.trim()))
|
|
||||||
const wantChangePassword = hasNewOrConfirm
|
|
||||||
const onlyOldProvided = hasOld && !hasNewOrConfirm
|
|
||||||
|
|
||||||
if (!needUpdateUsername && !wantChangePassword) {
|
|
||||||
layer.msg('未修改任何内容', { icon: 0 })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改密码场景:需进行严格校验(旧/新/确认均必填)
|
|
||||||
if (wantChangePassword) {
|
|
||||||
const pwdCheck = validatePassword(fields)
|
|
||||||
if (!pwdCheck.ok) {
|
|
||||||
layer.msg(pwdCheck.msg, { icon: 2 })
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 仅修改用户名:要求输入当前密码
|
try {
|
||||||
if (needUpdateUsername && !wantChangePassword && !hasOld) {
|
const res = await fetch('/admin/api/user/password', {
|
||||||
layer.msg('修改用户名需要输入当前密码', { icon: 2 })
|
method: 'POST',
|
||||||
return false
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
body: JSON.stringify({
|
||||||
|
old_password: fields.old_password,
|
||||||
try {
|
new_password: fields.new_password,
|
||||||
// 始终先更新用户名,再更新密码(避免改密后跳转导致无法继续)
|
confirm_password: fields.confirm_password
|
||||||
if (needUpdateUsername) {
|
})
|
||||||
await updateUsername(desiredUsername, hasOld ? fields.old_password : '')
|
|
||||||
initialUsername = desiredUsername
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wantChangePassword) {
|
|
||||||
const pwdResp = await updatePassword(fields)
|
|
||||||
// 修改密码后通常需要重新登录,优先使用后端返回的 redirect,否则默认登录页
|
|
||||||
const redirect = pwdResp && pwdResp.data && pwdResp.data.redirect ? pwdResp.data.redirect : '/admin/login'
|
|
||||||
layer.msg('密码修改成功,即将跳转到登录页', { icon: 1, time: 1200 }, () => {
|
|
||||||
window.location.href = redirect
|
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
// 未修改密码,仅修改资料
|
const data = await res.json()
|
||||||
await loadProfile()
|
const ok = (data.success === true) || (data.code === 0)
|
||||||
layer.msg('保存成功', { icon: 1 })
|
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 })
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
layer.msg(e.message || '保存失败', { icon: 2 })
|
return false
|
||||||
}
|
},
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 绑定“重置”按钮:将表单恢复为最近一次加载到表单中的资料
|
reset: () => {
|
||||||
// 逻辑:
|
document.getElementById('passwordForm').reset()
|
||||||
// - 如有 lastProfile,直接回填;
|
layer.msg('表单已重置', { icon: 1 })
|
||||||
// - 回填时同时清空三个密码字段;
|
}
|
||||||
// - 如暂无缓存(极小概率),则重新请求资料
|
|
||||||
const bindReset = () => {
|
|
||||||
const btn = document.getElementById('btnReset')
|
|
||||||
if (!btn) return
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
if (lastProfile) {
|
|
||||||
form.val('accountForm', { ...lastProfile, old_password: '', new_password: '', confirm_password: '' })
|
|
||||||
layer.msg('已恢复为当前资料', { icon: 1 })
|
|
||||||
} else {
|
|
||||||
loadProfile()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修改用户名模块
|
||||||
|
const UsernameModule = {
|
||||||
|
validate: (fields) => {
|
||||||
|
const { new_username, password } = fields
|
||||||
|
|
||||||
|
if (!new_username || !password) {
|
||||||
|
return { ok: false, msg: '请填写新用户名和当前密码' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_username === userProfile?.username) {
|
||||||
|
return { ok: false, msg: '新用户名不能与当前用户名相同' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_username.length < 3) {
|
||||||
|
return { ok: false, msg: '用户名长度不能少于3位' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
submit: async (fields) => {
|
||||||
|
const validation = UsernameModule.validate(fields)
|
||||||
|
if (!validation.ok) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
UsernameModule.reset()
|
||||||
|
|
||||||
|
} 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定表单提交事件
|
||||||
|
form.on('submit(submitPassword)', (obj) => {
|
||||||
|
return PasswordModule.submit(obj.field)
|
||||||
|
})
|
||||||
|
|
||||||
|
form.on('submit(submitUsername)', (obj) => {
|
||||||
|
return UsernameModule.submit(obj.field)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 绑定重置按钮
|
||||||
|
document.getElementById('resetPasswordBtn')?.addEventListener('click', PasswordModule.reset)
|
||||||
|
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
|
||||||
|
|
||||||
// 初始化加载
|
// 初始化加载
|
||||||
bindReset()
|
|
||||||
loadProfile()
|
loadProfile()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
<label class="layui-form-label">搜索</label>
|
<label class="layui-form-label">搜索</label>
|
||||||
<div class="layui-input-inline">
|
<div class="layui-input-inline">
|
||||||
<input type="text" name="search" placeholder="变量别名/数据" autocomplete="off" class="layui-input" />
|
<input type="text" name="search" placeholder="变量编号/别名/数据/备注" autocomplete="off" class="layui-input" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
variablesTable.reload({
|
variablesTable.reload({
|
||||||
where: {
|
where: {
|
||||||
app_uuid: data.value,
|
app_uuid: data.value,
|
||||||
alias: $('input[name="search"]').val()
|
search: $('input[name="search"]').val()
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
curr: 1
|
curr: 1
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
variablesTable.reload({
|
variablesTable.reload({
|
||||||
where: {
|
where: {
|
||||||
app_uuid: $('select[name="app_uuid"]').val(),
|
app_uuid: $('select[name="app_uuid"]').val(),
|
||||||
alias: $('input[name="search"]').val()
|
search: $('input[name="search"]').val()
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
curr: 1
|
curr: 1
|
||||||
|
|||||||
Reference in New Issue
Block a user