mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
Fix the new authentication issue
This commit is contained in:
@@ -16,8 +16,8 @@ import (
|
|||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Host string `json:"host" mapstructure:"host"` // 服务器监听地址
|
Host string `json:"host" mapstructure:"host"` // 服务器监听地址
|
||||||
Port int `json:"port" mapstructure:"port"` // 服务器监听端口
|
Port int `json:"port" mapstructure:"port"` // 服务器监听端口
|
||||||
Mode string `json:"mode" mapstructure:"mode"` // 运行模式(debug/release)
|
|
||||||
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
|
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
|
||||||
|
DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseConfig 数据库配置结构体
|
// DatabaseConfig 数据库配置结构体
|
||||||
@@ -80,7 +80,7 @@ type CookieConfig struct {
|
|||||||
type SecurityConfig struct {
|
type SecurityConfig struct {
|
||||||
JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
|
JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
|
||||||
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥
|
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥
|
||||||
JWTRefreshThresholdHours int `json:"jwt_refresh_threshold_hours" mapstructure:"jwt_refresh_threshold_hours"` // JWT令牌刷新阈值(小时)
|
JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值(小时)
|
||||||
Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置
|
Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +99,8 @@ func GetDefaultAppConfig() *AppConfig {
|
|||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Mode: "debug",
|
|
||||||
Dist: "",
|
Dist: "",
|
||||||
|
DevMode: false,
|
||||||
},
|
},
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
Type: "sqlite",
|
Type: "sqlite",
|
||||||
@@ -134,7 +134,7 @@ func GetDefaultAppConfig() *AppConfig {
|
|||||||
Security: SecurityConfig{
|
Security: SecurityConfig{
|
||||||
JWTSecret: "",
|
JWTSecret: "",
|
||||||
EncryptionKey: "",
|
EncryptionKey: "",
|
||||||
JWTRefreshThresholdHours: 6,
|
JWTRefresh: 6,
|
||||||
Cookie: CookieConfig{
|
Cookie: CookieConfig{
|
||||||
Secure: true,
|
Secure: true,
|
||||||
SameSite: "Lax",
|
SameSite: "Lax",
|
||||||
|
|||||||
@@ -75,12 +75,6 @@ func validateServerConfig(config *ServerConfig) error {
|
|||||||
return fmt.Errorf("无效的端口号: %d,端口号必须在1-65535之间", config.Port)
|
return fmt.Errorf("无效的端口号: %d,端口号必须在1-65535之间", config.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证运行模式
|
|
||||||
validModes := []string{"debug", "release", "test"}
|
|
||||||
if !contains(validModes, config.Mode) {
|
|
||||||
return fmt.Errorf("无效的运行模式: %s,支持的模式: %s", config.Mode, strings.Join(validModes, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +194,7 @@ func validateSecurityConfig(config *SecurityConfig) error {
|
|||||||
return errors.New("加密密钥长度不能少于16个字符")
|
return errors.New("加密密钥长度不能少于16个字符")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.JWTRefreshThresholdHours < 1 || config.JWTRefreshThresholdHours > 23 {
|
if config.JWTRefresh < 1 || config.JWTRefresh > 23 {
|
||||||
return errors.New("JWT令牌刷新阈值必须在1-23小时之间")
|
return errors.New("JWT令牌刷新阈值必须在1-23小时之间")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,13 +185,15 @@ func clearInvalidJWTCookie(w http.ResponseWriter) {
|
|||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT密钥(生产环境应从配置文件或环境变量读取)
|
// getJWTSecret 动态获取当前的JWT密钥
|
||||||
var jwtSecret = []byte(viper.GetString("security.jwt_secret"))
|
// 修复安全漏洞:确保每次都从最新配置中获取密钥,而不是使用启动时的全局变量
|
||||||
|
func getJWTSecret() []byte {
|
||||||
|
return []byte(viper.GetString("security.jwt_secret"))
|
||||||
|
}
|
||||||
|
|
||||||
// JWTClaims JWT载荷结构
|
// JWTClaims JWT载荷结构
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IsAdmin bool `json:"is_admin"` // 是否为管理员
|
|
||||||
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
@@ -206,7 +208,6 @@ func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
|
|||||||
|
|
||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
Username: adminUser.Username,
|
Username: adminUser.Username,
|
||||||
IsAdmin: true, // 管理员
|
|
||||||
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
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)),
|
||||||
@@ -218,7 +219,7 @@ func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
return token.SignedString(jwtSecret)
|
return token.SignedString(getJWTSecret())
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseJWTToken 解析并验证JWT令牌
|
// parseJWTToken 解析并验证JWT令牌
|
||||||
@@ -230,7 +231,7 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) {
|
|||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
}
|
}
|
||||||
return jwtSecret, nil
|
return getJWTSecret(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -244,11 +245,48 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) {
|
|||||||
return nil, fmt.Errorf("invalid token")
|
return nil, fmt.Errorf("invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getJWTCookie 获取JWT cookie的通用函数
|
||||||
|
func getJWTCookie(r *http.Request) (*http.Cookie, error) {
|
||||||
|
return r.Cookie("admin_session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAdminPasswordHash 验证管理员密码哈希的通用函数
|
||||||
|
func validateAdminPasswordHash(claims *JWTClaims, r *http.Request) bool {
|
||||||
|
// 【安全修复】验证数据库中的当前密码哈希
|
||||||
|
// 这确保了密码修改后,旧的JWT令牌会失效
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[SECURITY WARNING] Database connection failed during auth - Username=%s, IP=%s\n",
|
||||||
|
claims.Username, r.RemoteAddr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前数据库中的管理员密码
|
||||||
|
var adminPassword models.Settings
|
||||||
|
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil {
|
||||||
|
fmt.Printf("[SECURITY WARNING] Admin password not found in database - Username=%s, IP=%s\n",
|
||||||
|
claims.Username, r.RemoteAddr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成当前数据库密码的哈希摘要
|
||||||
|
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
|
||||||
|
|
||||||
|
// 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致
|
||||||
|
if claims.PasswordHash != currentPasswordHash {
|
||||||
|
fmt.Printf("[SECURITY WARNING] Password hash mismatch - JWT token invalidated - Username=%s, IP=%s\n",
|
||||||
|
claims.Username, r.RemoteAddr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// IsAdminAuthenticated 判断管理员是否已认证(导出)
|
// IsAdminAuthenticated 判断管理员是否已认证(导出)
|
||||||
// - 检查admin_session Cookie中的JWT令牌
|
// - 检查admin_session Cookie中的JWT令牌
|
||||||
// - 验证令牌签名、过期时间和用户角色
|
// - 验证令牌签名、过期时间和用户角色
|
||||||
func IsAdminAuthenticated(r *http.Request) bool {
|
func IsAdminAuthenticated(r *http.Request) bool {
|
||||||
cookie, err := r.Cookie("admin_session")
|
cookie, err := getJWTCookie(r)
|
||||||
if err != nil || cookie.Value == "" {
|
if err != nil || cookie.Value == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -259,27 +297,17 @@ func IsAdminAuthenticated(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户角色(只允许管理员)
|
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
|
||||||
if !claims.IsAdmin {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
// 验证密码哈希
|
||||||
// 只需要验证JWT中的信息即可
|
return validateAdminPasswordHash(claims, r)
|
||||||
if !claims.IsAdmin {
|
|
||||||
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
|
|
||||||
claims.Username, r.RemoteAddr)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
|
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
|
||||||
// - 当JWT校验失败时,自动清理失效的Cookie
|
// - 当JWT校验失败时,自动清理失效的Cookie
|
||||||
// - 适用于API接口等需要清理失效令牌的场景
|
// - 适用于API接口等需要清理失效令牌的场景
|
||||||
func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) bool {
|
func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) bool {
|
||||||
cookie, err := r.Cookie("admin_session")
|
cookie, err := getJWTCookie(r)
|
||||||
if err != nil || cookie.Value == "" {
|
if err != nil || cookie.Value == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -292,17 +320,10 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户角色(只允许管理员)
|
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
|
||||||
if !claims.IsAdmin {
|
|
||||||
clearInvalidJWTCookie(w)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
// 验证密码哈希
|
||||||
// 只需要验证JWT中的信息即可
|
if !validateAdminPasswordHash(claims, r) {
|
||||||
if !claims.IsAdmin {
|
|
||||||
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
|
|
||||||
claims.Username, r.RemoteAddr)
|
|
||||||
clearInvalidJWTCookie(w)
|
clearInvalidJWTCookie(w)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -315,7 +336,7 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo
|
|||||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
||||||
// - 返回用户ID、用户名和角色
|
// - 返回用户ID、用户名和角色
|
||||||
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
||||||
cookie, err := r.Cookie("admin_session")
|
cookie, err := getJWTCookie(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("未找到会话信息")
|
return nil, fmt.Errorf("未找到会话信息")
|
||||||
}
|
}
|
||||||
@@ -325,15 +346,7 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
|||||||
return nil, fmt.Errorf("无效的会话信息")
|
return nil, fmt.Errorf("无效的会话信息")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !claims.IsAdmin {
|
// 注释:由于这是管理员专用函数,不需要额外的角色验证
|
||||||
return nil, fmt.Errorf("权限不足")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
|
||||||
// 只需要验证JWT中的信息即可
|
|
||||||
if !claims.IsAdmin {
|
|
||||||
return nil, fmt.Errorf("无效的管理员令牌")
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
@@ -343,7 +356,7 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
|||||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
||||||
// - 返回用户ID、用户名、角色和是否刷新了令牌
|
// - 返回用户ID、用户名、角色和是否刷新了令牌
|
||||||
func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JWTClaims, bool, error) {
|
func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JWTClaims, bool, error) {
|
||||||
cookie, err := r.Cookie("admin_session")
|
cookie, err := getJWTCookie(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("未找到会话信息")
|
return nil, false, fmt.Errorf("未找到会话信息")
|
||||||
}
|
}
|
||||||
@@ -353,19 +366,16 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW
|
|||||||
return nil, false, fmt.Errorf("无效的会话信息")
|
return nil, false, fmt.Errorf("无效的会话信息")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !claims.IsAdmin {
|
// 注释:由于这是管理员专用函数,不需要额外的角色验证
|
||||||
return nil, false, fmt.Errorf("权限不足")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于管理员,不需要验证数据库中的用户记录,因为管理员信息存储在settings中
|
// 验证密码哈希
|
||||||
// 只需要验证JWT中的信息即可
|
if !validateAdminPasswordHash(claims, r) {
|
||||||
if !claims.IsAdmin {
|
return nil, false, fmt.Errorf("会话已失效,请重新登录")
|
||||||
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")) * time.Hour
|
||||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
||||||
// 为管理员生成新的JWT令牌
|
// 为管理员生成新的JWT令牌
|
||||||
adminUser := models.User{
|
adminUser := models.User{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"networkDev/utils"
|
"networkDev/utils"
|
||||||
|
|
||||||
"github.com/mojocn/base64Captcha"
|
"github.com/mojocn/base64Captcha"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 全局验证码存储器
|
// 全局验证码存储器
|
||||||
@@ -91,6 +92,11 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 这个函数将在登录处理中被调用
|
// 这个函数将在登录处理中被调用
|
||||||
// 支持大小写不敏感匹配
|
// 支持大小写不敏感匹配
|
||||||
func VerifyCaptcha(r *http.Request, captchaValue string) bool {
|
func VerifyCaptcha(r *http.Request, captchaValue string) bool {
|
||||||
|
// 检查是否为开发模式,如果是则跳过验证码验证
|
||||||
|
if viper.GetBool("server.dev_mode") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// 从cookie中获取验证码ID
|
// 从cookie中获取验证码ID
|
||||||
cookie, err := r.Cookie("captcha_id")
|
cookie, err := r.Cookie("captcha_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"networkDev/database"
|
"networkDev/database"
|
||||||
|
"networkDev/models"
|
||||||
"networkDev/services"
|
"networkDev/services"
|
||||||
"networkDev/utils"
|
"networkDev/utils"
|
||||||
"networkDev/utils/timeutil"
|
"networkDev/utils/timeutil"
|
||||||
@@ -10,7 +11,24 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AdminIndexHandler /admin 与 /admin/ 根路径入口
|
// formatDBType 格式化数据库类型显示
|
||||||
|
// 将配置文件中的小写类型转换为友好的显示格式
|
||||||
|
func formatDBType(dbType string) string {
|
||||||
|
switch dbType {
|
||||||
|
case "mysql":
|
||||||
|
return "MySQL"
|
||||||
|
case "sqlite":
|
||||||
|
return "SQLite"
|
||||||
|
case "postgresql", "postgres":
|
||||||
|
return "PostgreSQL"
|
||||||
|
case "sqlserver":
|
||||||
|
return "SQL Server"
|
||||||
|
default:
|
||||||
|
return "SQLite" // 默认显示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口
|
||||||
// - 未登录:重定向到 /admin/login
|
// - 未登录:重定向到 /admin/login
|
||||||
// - 已登录:渲染后台布局页(或重定向到 /admin/layout)
|
// - 已登录:渲染后台布局页(或重定向到 /admin/layout)
|
||||||
// - 自动清理失效的JWT Cookie
|
// - 自动清理失效的JWT Cookie
|
||||||
@@ -71,10 +89,10 @@ func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DashboardFragmentHandler 仪表盘片段渲染
|
// DashboardFragmentHandler 仪表盘片段渲染
|
||||||
// - 展示系统信息:版本、运行模式、数据库类型、启动时长
|
// - 展示系统信息:版本、开发模式、数据库类型、启动时长
|
||||||
func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
version := "1.0.0"
|
version := "1.0.0"
|
||||||
mode := viper.GetString("server.mode")
|
mode := viper.GetBool("server.dev_mode")
|
||||||
dbType := viper.GetString("database.type")
|
dbType := viper.GetString("database.type")
|
||||||
if dbType == "" {
|
if dbType == "" {
|
||||||
dbType = "sqlite"
|
dbType = "sqlite"
|
||||||
@@ -84,7 +102,7 @@ func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"Version": version,
|
"Version": version,
|
||||||
"Mode": mode,
|
"Mode": mode,
|
||||||
"DBType": dbType,
|
"DBType": formatDBType(dbType),
|
||||||
"Uptime": uptime,
|
"Uptime": uptime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +118,7 @@ func SystemInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
version := "1.0.0"
|
version := "1.0.0"
|
||||||
mode := viper.GetString("server.mode")
|
mode := viper.GetBool("server.dev_mode")
|
||||||
dbType := viper.GetString("database.type")
|
dbType := viper.GetString("database.type")
|
||||||
if dbType == "" {
|
if dbType == "" {
|
||||||
dbType = "sqlite"
|
dbType = "sqlite"
|
||||||
@@ -110,9 +128,64 @@ func SystemInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"version": version,
|
"version": version,
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
"db_type": dbType,
|
"db_type": formatDBType(dbType),
|
||||||
"uptime": uptime,
|
"uptime": uptime,
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.JsonResponse(w, http.StatusOK, true, "ok", data)
|
utils.JsonResponse(w, http.StatusOK, true, "ok", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DashboardStatsHandler 仪表盘统计数据API接口
|
||||||
|
// - 返回应用统计数据的JSON数据,包括全部/启用/禁用/变量数量
|
||||||
|
func DashboardStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库连接
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计应用数据
|
||||||
|
var totalApps int64
|
||||||
|
var enabledApps int64
|
||||||
|
var disabledApps int64
|
||||||
|
var totalVariables int64
|
||||||
|
|
||||||
|
// 统计全部应用数量
|
||||||
|
if err := db.Model(&models.App{}).Count(&totalApps).Error; err != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计应用数量失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计启用应用数量
|
||||||
|
if err := db.Model(&models.App{}).Where("status = ?", 1).Count(&enabledApps).Error; err != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计启用应用数量失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计禁用应用数量
|
||||||
|
if err := db.Model(&models.App{}).Where("status = ?", 0).Count(&disabledApps).Error; err != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计禁用应用数量失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计变量数量
|
||||||
|
if err := db.Model(&models.Variable{}).Count(&totalVariables).Error; err != nil {
|
||||||
|
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计变量数量失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"total_apps": totalApps,
|
||||||
|
"enabled_apps": enabledApps,
|
||||||
|
"disabled_apps": disabledApps,
|
||||||
|
"total_variables": totalVariables,
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.JsonResponse(w, http.StatusOK, true, "ok", data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,11 +81,7 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认是管理员
|
// 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户
|
||||||
if !claims.IsAdmin {
|
|
||||||
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取数据库连接
|
// 获取数据库连接
|
||||||
db, err := database.GetDB()
|
db, err := database.GetDB()
|
||||||
@@ -176,7 +172,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
_, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||||
return
|
return
|
||||||
@@ -207,11 +203,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认当前用户是管理员
|
// 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户
|
||||||
if !claims.IsAdmin {
|
|
||||||
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有管理员相关设置
|
// 获取所有管理员相关设置
|
||||||
var adminSettings []models.Settings
|
var adminSettings []models.Settings
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func initDefaultAdmin(db *gorm.DB) error {
|
|||||||
|
|
||||||
// 如果密码已设置,跳过初始化
|
// 如果密码已设置,跳过初始化
|
||||||
if passwordSetting.Value != "" {
|
if passwordSetting.Value != "" {
|
||||||
logrus.Info("管理员密码已设置,跳过默认密码初始化")
|
logrus.Debug("管理员密码已设置,跳过默认密码初始化")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
|
|||||||
// 系统信息API(用于仪表盘定时刷新)
|
// 系统信息API(用于仪表盘定时刷新)
|
||||||
mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler))
|
mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler))
|
||||||
|
|
||||||
|
// 仪表盘统计数据API
|
||||||
|
mux.HandleFunc("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(adminctl.DashboardStatsHandler))
|
||||||
|
|
||||||
// 个人资料API
|
// 个人资料API
|
||||||
mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler))
|
mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler))
|
||||||
mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler)))
|
mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler)))
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{{ define "apis.html" }}
|
{{ define "apis.html" }}
|
||||||
<section>
|
<section>
|
||||||
<h2>接口管理</h2>d
|
<h2>接口管理</h2>
|
||||||
<div class="layui-card" style="margin-top:12px">
|
<div class="layui-panel" style="margin-top:12px">
|
||||||
<div class="layui-card-header">筛选</div>d
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form layui-form-pane" id="apiFilterForm" lay-filter="apiFilterForm">
|
<form class="layui-form layui-form-pane" id="apiFilterForm" lay-filter="apiFilterForm">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<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">
|
||||||
<select name="app_uuid" lay-filter="appSelect" lay-search="">
|
<select name="app_uuid" lay-filter="appSelect" lay-search="">
|
||||||
<option value="">请选择应用</option>e
|
<option value="">请选择应用</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="layui-card" style="margin-top:12px">
|
<div class="layui-panel" style="margin-top:12px">
|
||||||
<div class="layui-card-header">接口列表</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">接口列表</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<table id="apisTable" lay-filter="apisTableFilter"></table>
|
<table id="apisTable" lay-filter="apisTableFilter"></table>
|
||||||
<script type="text/html" id="tpl-apis-ops">
|
<script type="text/html" id="tpl-apis-ops">
|
||||||
<div style="white-space: nowrap;">
|
<div style="white-space: nowrap;">
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
批量禁用</button>
|
批量禁用</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="layui-card" style="margin-top:12px">
|
<div class="layui-panel" style="margin-top:12px">
|
||||||
<div class="layui-card-header">筛选</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form layui-form-pane" id="appFilterForm" lay-filter="appFilterForm">
|
<form class="layui-form layui-form-pane" id="appFilterForm" lay-filter="appFilterForm">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="layui-card" style="margin-top:12px">
|
<div class="layui-panel" style="margin-top:12px">
|
||||||
<div class="layui-card-header">应用列表</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">应用列表</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<table id="appsTable" lay-filter="appsTableFilter"></table>
|
<table id="appsTable" lay-filter="appsTableFilter"></table>
|
||||||
<script type="text/html" id="tpl-apps-ops">
|
<script type="text/html" id="tpl-apps-ops">
|
||||||
<div style="white-space: nowrap;">
|
<div style="white-space: nowrap;">
|
||||||
|
|||||||
@@ -2,37 +2,66 @@
|
|||||||
<section>
|
<section>
|
||||||
<h2>系统信息</h2>
|
<h2>系统信息</h2>
|
||||||
<div class="layui-row layui-col-space15" style="margin-top:12px">
|
<div class="layui-row layui-col-space15" style="margin-top:12px">
|
||||||
<div class="layui-col-md6">
|
<!-- 系统信息面板 -->
|
||||||
<div class="layui-card">
|
<div class="layui-col-md8">
|
||||||
<div class="layui-card-header">基本信息</div>
|
<div class="layui-panel">
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<div class="system-info-grid">
|
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">系统信息</h3>
|
||||||
<div class="system-info-item">
|
<table class="layui-table" lay-skin="nob">
|
||||||
<div class="system-info-label">版本</div>
|
<tbody>
|
||||||
<div class="system-info-value">{{ .Version }}</div>
|
<tr>
|
||||||
</div>
|
<td style="width: 120px; font-weight: bold;">程序版本</td>
|
||||||
<div class="system-info-item">
|
<td><span style="font-size: 18px; font-weight: bold; color: var(--lay-color-normal);">{{ .Version }}</span></td>
|
||||||
<div class="system-info-label">运行模式</div>
|
</tr>
|
||||||
<div class="system-info-value">{{ .Mode }}</div>
|
<tr>
|
||||||
|
<td style="font-weight: bold;">存储方案</td>
|
||||||
|
<td><span style="font-size: 18px; font-weight: bold; color: var(--lay-color-info);">{{ .DBType }}</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight: bold;">开发模式</td>
|
||||||
|
<td>
|
||||||
|
{{ if .Mode }}
|
||||||
|
<span style="font-size: 18px; font-weight: bold; color: var(--lay-color-danger);">开启</span>
|
||||||
|
{{ else }}
|
||||||
|
<span style="font-size: 18px; font-weight: bold; color: var(--lay-color-success);">关闭</span>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight: bold;">运行时长</td>
|
||||||
|
<td><span id="uptime-display" style="font-size: 18px; font-weight: bold; color: var(--lay-color-normal);">{{ .Uptime }}</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<!-- 应用统计面板 -->
|
||||||
<div class="layui-col-md6">
|
<div class="layui-col-md4">
|
||||||
<div class="layui-card">
|
<div class="layui-panel">
|
||||||
<div class="layui-card-header">运行状态</div>
|
<div style="padding: 20px;">
|
||||||
<div class="layui-card-body">
|
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">应用统计</h3>
|
||||||
<div class="system-info-grid">
|
<table class="layui-table" lay-skin="nob">
|
||||||
<div class="system-info-item">
|
<tbody>
|
||||||
<div class="system-info-label">数据库</div>
|
<tr>
|
||||||
<div class="system-info-value">{{ .DBType }}</div>
|
<td style="width: 120px; font-weight: bold;">全部应用</td>
|
||||||
</div>
|
<td><span id="total-apps" style="font-size: 18px; font-weight: bold;">0</span></td>
|
||||||
<div class="system-info-item">
|
</tr>
|
||||||
<div class="system-info-label">运行时长</div>
|
<tr>
|
||||||
<div class="system-info-value">{{ .Uptime }}</div>
|
<td style="font-weight: bold;">启用应用</td>
|
||||||
</div>
|
<td><span id="enabled-apps" style="font-size: 18px; font-weight: bold; color: var(--lay-color-success);">0</span></td>
|
||||||
</div>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight: bold;">禁用应用</td>
|
||||||
|
<td><span id="disabled-apps" style="font-size: 18px; font-weight: bold; color: var(--lay-color-danger);">0</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight: bold;">变量数量</td>
|
||||||
|
<td><span id="total-variables" style="font-size: 18px; font-weight: bold; color: var(--lay-color-info);">0</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,12 +100,7 @@
|
|||||||
const data = res.data;
|
const data = res.data;
|
||||||
// 更新运行时长
|
// 更新运行时长
|
||||||
if (data.uptime) {
|
if (data.uptime) {
|
||||||
$('.system-info-item').each(function () {
|
$('#uptime-display').text(data.uptime);
|
||||||
const label = $(this).find('.system-info-label').text();
|
|
||||||
if (label === '运行时长') {
|
|
||||||
$(this).find('.system-info-value').text(data.uptime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).fail(() => {
|
}).fail(() => {
|
||||||
@@ -84,8 +108,30 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 立即刷新一次系统信息
|
// 函数:刷新应用统计数据
|
||||||
|
// 说明:请求后台获取应用统计信息并更新页面显示
|
||||||
|
const refreshAppStats = () => {
|
||||||
|
$.get('/admin/api/dashboard/stats', (res) => {
|
||||||
|
if (res && res.code === 0 && res.data) {
|
||||||
|
const data = res.data;
|
||||||
|
$('#total-apps').text(data.total_apps || 0);
|
||||||
|
$('#enabled-apps').text(data.enabled_apps || 0);
|
||||||
|
$('#disabled-apps').text(data.disabled_apps || 0);
|
||||||
|
$('#total-variables').text(data.total_variables || 0);
|
||||||
|
}
|
||||||
|
}).fail(() => {
|
||||||
|
console.log('获取应用统计失败');
|
||||||
|
// 显示默认值
|
||||||
|
$('#total-apps').text('0');
|
||||||
|
$('#enabled-apps').text('0');
|
||||||
|
$('#disabled-apps').text('0');
|
||||||
|
$('#total-variables').text('0');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 立即刷新一次系统信息和应用统计
|
||||||
refreshSystemInfo();
|
refreshSystemInfo();
|
||||||
|
refreshAppStats();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
<section>
|
<section>
|
||||||
<h2>系统设置</h2>
|
<h2>系统设置</h2>
|
||||||
<!-- 基本信息设置 -->
|
<!-- 基本信息设置 -->
|
||||||
<div class="layui-card" style="margin-top: 16px;">
|
<div class="layui-panel" style="margin-top: 16px;">
|
||||||
<div class="layui-card-header">基本信息设置</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">基本信息设置</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form" id="basicForm">
|
<form class="layui-form" id="basicForm">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="site-title">站点标题</label>
|
<label class="layui-form-label" style="cursor: pointer;" data-tips="site-title">站点标题</label>
|
||||||
@@ -35,9 +35,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 系统配置设置 -->
|
<!-- 系统配置设置 -->
|
||||||
<div class="layui-card" style="margin-top: 16px;">
|
<div class="layui-panel" style="margin-top: 16px;">
|
||||||
<div class="layui-card-header">系统配置</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">系统配置</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form" id="systemForm">
|
<form class="layui-form" id="systemForm">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label>
|
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label>
|
||||||
@@ -72,9 +72,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 页脚与备案信息 -->
|
<!-- 页脚与备案信息 -->
|
||||||
<div class="layui-card" style="margin-top: 16px;">
|
<div class="layui-panel" style="margin-top: 16px;">
|
||||||
<div class="layui-card-header">页脚与备案</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">页脚与备案</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form" id="footerForm">
|
<form class="layui-form" id="footerForm">
|
||||||
<div class="layui-form-item layui-form-text">
|
<div class="layui-form-item layui-form-text">
|
||||||
<label class="layui-form-label" style="cursor: pointer;" data-tips="footer-text">页脚文本</label>
|
<label class="layui-form-label" style="cursor: pointer;" data-tips="footer-text">页脚文本</label>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
<div class="layui-tab-content">
|
<div class="layui-tab-content">
|
||||||
<!-- 修改密码模块 -->
|
<!-- 修改密码模块 -->
|
||||||
<div class="layui-tab-item layui-show">
|
<div class="layui-tab-item layui-show">
|
||||||
<div class="layui-card" style="margin-top: 16px;">
|
<div class="layui-panel" style="margin-top: 16px;">
|
||||||
<div class="layui-card-header">修改密码</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改密码</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
|
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label">当前密码</label>
|
<label class="layui-form-label">当前密码</label>
|
||||||
@@ -51,9 +51,9 @@
|
|||||||
|
|
||||||
<!-- 修改用户名模块 -->
|
<!-- 修改用户名模块 -->
|
||||||
<div class="layui-tab-item">
|
<div class="layui-tab-item">
|
||||||
<div class="layui-card" style="margin-top: 16px;">
|
<div class="layui-panel" style="margin-top: 16px;">
|
||||||
<div class="layui-card-header">修改用户名</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改用户名</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false">
|
<form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label">当前用户名</label>
|
<label class="layui-form-label">当前用户名</label>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
批量删除</button>
|
批量删除</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="layui-card" style="margin-top:12px">
|
<div class="layui-panel" style="margin-top:12px">
|
||||||
<div class="layui-card-header">筛选</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<form class="layui-form layui-form-pane" id="variableFilterForm" lay-filter="variableFilterForm">
|
<form class="layui-form layui-form-pane" id="variableFilterForm" lay-filter="variableFilterForm">
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
@@ -35,9 +35,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="layui-card" style="margin-top:12px">
|
<div class="layui-panel" style="margin-top:12px">
|
||||||
<div class="layui-card-header">变量列表</div>
|
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">变量列表</h3>
|
||||||
<div class="layui-card-body">
|
<div style="padding: 20px;">
|
||||||
<table id="variablesTable" lay-filter="variablesTableFilter"></table>
|
<table id="variablesTable" lay-filter="variablesTableFilter"></table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
web/template/layui-theme-dark
Submodule
1
web/template/layui-theme-dark
Submodule
Submodule web/template/layui-theme-dark added at a89e6787f4
Reference in New Issue
Block a user