修改项目为前后端分离方案

This commit is contained in:
2026-03-28 23:30:02 +08:00
parent d8536354d4
commit 7a7d3aeaaa
77 changed files with 1447 additions and 23765 deletions

2
.gitignore vendored
View File

@@ -40,7 +40,7 @@ log/
*.swo *.swo
*~ *~
node.txt node.txt
模板 frontend
# IDE 和编辑器 # IDE 和编辑器
.vscode/settings.json .vscode/settings.json

View File

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

195
README.md
View File

@@ -1,6 +1,6 @@
# NetworkAuth开发中 # NetworkAuth网络授权服务
一个基于 Go 语言开发的网络应用管理系统提供应用程序管理、API接口管理、变量管理、用户认证等功能的 Web 管理平台 网络授权服务 (NetworkAuth) 是一个专注于应用鉴权、接口管理和动态逻辑分发的后端系统。它基于 Go 语言开发提供应用程序管理、API接口管理、变量管理、用户认证等核心服务
## 功能特性 ## 功能特性
@@ -11,115 +11,46 @@
- **函数管理**: 支持自定义函数代码管理,可绑定特定应用或全局使用 - **函数管理**: 支持自定义函数代码管理,可绑定特定应用或全局使用
- **用户管理**: 完整的用户认证和权限管理系统 - **用户管理**: 完整的用户认证和权限管理系统
- **系统设置**: 灵活的系统配置和参数管理 - **系统设置**: 灵活的系统配置和参数管理
- **系统安装**: 提供可视化的安装向导,轻松完成数据库和管理员配置 - **系统初始化**: 提供引导式的数据表初始化和默认设置注入
- **仪表盘**: 实时系统状态监控和统计数据展示
- **日志审计**: 详细的登录日志和操作日志记录,保障系统安全 - **日志审计**: 详细的登录日志和操作日志记录,保障系统安全
### 🔧 技术特性 ### 🔧 技术特性
- **RESTful API**: 标准的 REST API 接口设计 - **RESTful API**: 标准的 REST API 接口设计
- **JWT 认证**: 基于 JWT 的安全认证机制 - **JWT 认证**: 基于 JWT 的安全认证机制
- **多种加密算法**: 支持 RC4、RSA、RSA动态、易加密等多种加密方式 - **多种加密算法**: 支持 RC4、RSA、RSA动态、易加密等多种加密方式
- **数据库支持**: 支持 MySQL 和 SQLite 数据库 - **数据库支持**: 兼容 MySQL 和 SQLite 数据库 (通过 GORM)
- **Redis 缓存**: 集成 Redis 缓存提升性能(可选) - **Redis 缓存**: 集成 Redis 缓存提升性能(可选)
- **Excel 导出**: 支持数据导出为 Excel 文件 - **日志系统**: 完整的日志记录和管理,支持日志切割 (Logrus + Lumberjack)
- **日志系统**: 完整的日志记录和管理,支持日志切割
- **配置管理**: 基于 Viper 的灵活配置系统 - **配置管理**: 基于 Viper 的灵活配置系统
- **命令行工具**: 基于 Cobra 的强悍 CLI 管理工具
### 🎨 界面特性
- **响应式设计**: 支持多种设备和屏幕尺寸
- **现代化 UI**: 基于 LayUI 的现代化管理界面
- **主题支持**: 支持明暗主题切换
- **实时更新**: 支持数据的实时刷新和更新
- **片段化加载**: 采用 AJAX 片段加载提升用户体验
## 技术栈 ## 技术栈
- **后端**: Go 1.25.0 - **语言**: Go 1.25.0
- **Web 框架**: Gin + 自定义路由 - **Web 框架**: Gin
- **数据库**: GORM + MySQL/SQLite - **数据库 ORM**: GORM
- **缓存**: Redis可选 - **缓存**: Redis可选
- **认证**: JWT + 验证码 - **认证**: JWT + 验证码
- **日志**: Logrus + Lumberjack - **日志**: Logrus + Lumberjack
- **配置**: Viper - **配置管理**: Viper
- **前端**: LayUI + JavaScript - **命令行**: Cobra
- **工具**: Excelize (Excel导出)
- **加密**: 自定义加密工具包 - **加密**: 自定义加密工具包
## 项目结构 ## 项目结构
``` ```
networkDev/ NetworkAuth/
├── cmd/ # 命令行工具 ├── cmd/ # Cobra 命令行工具定义
│ ├── root.go # 根命令定义 ├── config/ # 配置文件模型与校验逻辑
│ └── server.go # 服务器启动命令 ├── constants/ # 全局常量定义 (版本号、状态码等)
├── config/ # 配置文件和配置管理 ├── controllers/ # 控制器层 (处理 HTTP 请求)
│ ├── config.go # 配置加载和验证 ├── database/ # 数据库连接、迁移与默认数据填充
│ ├── security.go # 安全配置 ├── middleware/ # Gin 中间件 (日志、认证、维护模式等)
│ └── validator.go # 配置验证器 ├── models/ # GORM 数据模型定义
├── constants/ # 常量定义 ├── server/ # HTTP 服务器路由注册
│ └── status.go # 状态常量 ├── services/ # 核心业务逻辑层
├── controllers/ # 控制器层 ├── utils/ # 通用工具函数 (加密、日志、时间等)
│ ├── admin/ # 管理后台控制器 └── main.go # 项目入口
│ │ ├── api.go # API接口管理
│ │ ├── app.go # 应用管理
│ │ ├── auth.go # 认证管理
│ │ ├── captcha.go # 验证码管理
│ │ ├── function.go # 函数管理
│ │ ├── handlers.go # 通用处理器
│ │ ├── login_log.go # 登录日志
│ │ ├── operation_log.go # 操作日志
│ │ ├── profile.go # 个人资料
│ │ ├── settings.go # 系统设置
│ │ ├── user.go # 用户管理
│ │ └── variable.go # 变量管理
│ ├── default/ # 默认控制器
│ ├── install/ # 安装向导控制器
│ └── base.go # 基础控制器
├── database/ # 数据库相关
│ ├── database.go # 数据库连接
│ ├── migrate.go # 数据库迁移
│ └── settings.go # 默认设置初始化
├── middleware/ # 中间件
│ ├── devmode.go # 开发模式中间件
│ ├── install.go # 安装检查中间件
│ ├── logging.go # 日志中间件
│ └── maintenance.go # 维护模式中间件
├── models/ # 数据模型
│ ├── api.go # API接口模型
│ ├── app.go # 应用模型
│ ├── function.go # 函数模型
│ ├── login_log.go # 登录日志模型
│ ├── operation_log.go # 操作日志模型
│ ├── settings.go # 系统设置模型
│ ├── user.go # 用户模型
│ └── variable.go # 变量模型
├── server/ # 服务器路由配置
│ ├── admin.go # 管理后台路由
│ ├── default.go # 默认路由
│ ├── install.go # 安装路由
│ └── routes.go # 路由注册
├── services/ # 业务逻辑层
│ ├── log_cleanup.go # 日志清理服务
│ ├── operation_log.go # 操作日志服务
│ ├── query.go # 查询服务
│ └── settings.go # 设置服务
├── utils/ # 工具函数
│ ├── encrypt/ # 加密工具包
│ ├── excel/ # Excel工具
│ ├── logger/ # 日志工具
│ ├── timeutil/ # 时间工具
│ ├── cookie.go # Cookie工具
│ ├── crypto.go # 加密工具
│ ├── csrf.go # CSRF防护
│ ├── database.go # 数据库工具
│ └── errors.go # 错误处理
└── web/ # Web 资源
├── assets/ # 资源文件
├── static/ # 静态资源
└── template/ # 模板文件
├── admin/ # 管理后台模板
├── default/ # 默认模板
└── install/ # 安装向导模板
``` ```
## 快速开始 ## 快速开始
@@ -128,14 +59,14 @@ networkDev/
- Go 1.25.0 或更高版本 - Go 1.25.0 或更高版本
- MySQL 5.7+ 或 SQLite 3 - MySQL 5.7+ 或 SQLite 3
- Redis (可选,用于缓存) - Redis (可选)
### 安装步骤 ### 安装与运行
1. **克隆项目** 1. **克隆项目**
```bash ```bash
git clone <repository-url> git clone https://github.com/skyle1995/NetworkAuth.git
cd networkDev cd NetworkAuth
``` ```
2. **安装依赖** 2. **安装依赖**
@@ -143,96 +74,56 @@ networkDev/
go mod download go mod download
``` ```
3. **运行项目** 3. **运行服务器**
```bash ```bash
# 直接运行 # 直接运行
./networkDev server
# 或使用 go run
go run main.go server go run main.go server
# 或者编译后运行
go build -o networkauth main.go
./networkauth server
``` ```
4. **系统初始化**
打开浏览器访问: `http://localhost:8080/install`
根据安装向导提示,配置数据库连接和管理员账号即可完成初始化。
### 命令行工具 ### 命令行工具
项目基于 Cobra CLI 框架,提供了丰富的命令行工具支持 项目基于 Cobra CLI 框架,提供了丰富的命令行工具:
```bash ```bash
# 查看帮助信息 # 查看帮助信息
./networkDev --help ./networkauth --help
# 启动服务器 # 启动服务器
./networkDev server ./networkauth server
# 指定配置文件启动 # 指定配置文件启动
./networkDev --config ./config.json server ./networkauth --config ./config.json server
# 指定端口启动 (覆盖配置文件) # 指定端口启动 (覆盖配置文件)
./networkDev server -p 8080 ./networkauth server -p 8080
``` ```
## API 文档
### 认证接口
- `POST /admin/api/auth/login` - 用户登录
- `POST /admin/api/auth/logout` - 用户登出
- `GET /admin/api/auth/captcha` - 获取验证码
### 应用管理接口
- `GET /admin/api/apps/list` - 获取应用列表
- `POST /admin/api/apps/create` - 创建应用
- `POST /admin/api/apps/update` - 更新应用
- `POST /admin/api/apps/delete` - 删除应用
- `POST /admin/api/apps/batch_delete` - 批量删除应用
### 变量管理接口
- `GET /admin/variable/list` - 获取变量列表
- `POST /admin/variable/create` - 创建变量
- `POST /admin/variable/update` - 更新变量
- `POST /admin/variable/delete` - 删除变量
- `POST /admin/variable/batch_delete` - 批量删除变量
### 函数管理接口
- `GET /admin/function/list` - 获取函数列表
- `POST /admin/function/create` - 创建函数
- `POST /admin/function/update` - 更新函数
- `POST /admin/function/delete` - 删除函数
- `POST /admin/function/batch_delete` - 批量删除函数
### 系统管理接口
- `GET /admin/api/settings` - 获取系统设置
- `POST /admin/api/settings/update` - 更新系统设置
- `GET /admin/api/logs` - 获取操作日志
- `GET /admin/api/login_logs` - 获取登录日志
## 部署 ## 部署
### Docker 部署 ### Docker 部署
```bash ```bash
# 构建镜像 # 构建镜像
docker build -t networkdev . docker build -t networkauth .
# 运行容器 # 运行容器
docker run -d -p 8080:8080 networkdev docker run -d -p 8080:8080 networkauth
``` ```
### 生产环境部署 ### 生产环境部署
1. 编译生产版本 1. 编译生产版本
```bash ```bash
go build -o networkdev main.go go build -o networkauth main.go
``` ```
2. 配置生产环境配置文件 2. 准备配置文件(可参考默认配置)。
3. 使用进程管理工具(如 systemd 或 supervisor管理后端服务进程。
3. 使用进程管理工具(如 systemd管理服务
## 许可证 ## 许可证
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。

View File

@@ -61,11 +61,12 @@ func setupLogrusForNonHTTP() {
// 设置输出目标(稍后会根据配置文件调整) // 设置输出目标(稍后会根据配置文件调整)
logrus.SetOutput(os.Stdout) logrus.SetOutput(os.Stdout)
// 初始化配置(优先使用命令行参数,否则默认 config.json
// 注意:如果文件不存在,配置系统将在内存中生成默认配置
if cfgFile != "" { if cfgFile != "" {
// 使用命令行指定的配置文件
config.Init(cfgFile) config.Init(cfgFile)
} else { } else {
// 使用默认配置文件路径
config.Init("./config.json") config.Init("./config.json")
} }

View File

@@ -1,201 +1,196 @@
package cmd package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"NetworkAuth/database" "NetworkAuth/database"
"NetworkAuth/middleware" "NetworkAuth/middleware"
"NetworkAuth/server" "NetworkAuth/server"
"NetworkAuth/services" "NetworkAuth/services"
"NetworkAuth/utils" "NetworkAuth/utils"
"NetworkAuth/utils/logger" "NetworkAuth/utils/logger"
"NetworkAuth/web"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin" "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus" "github.com/spf13/cobra"
"github.com/spf13/cobra" "github.com/spf13/viper"
"github.com/spf13/viper" )
)
// serverCmd 代表服务器命令
// serverCmd 代表服务器命令 var serverCmd = &cobra.Command{
var serverCmd = &cobra.Command{ Use: "server",
Use: "server", Short: "启动 NetworkAuth 系统服务器",
Short: "启动网络授权服务", Long: `启动 NetworkAuth 系统 HTTP 服务器,监听配置文件中指定的端口,提供 Web 管理界面和 API 服务。`,
Long: `启动 NetworkAuth HTTP 服务器,监听配置文件中指定的端口,提供 Web 管理界面和 API 服务。`, Run: runServer,
Run: runServer, }
}
func init() {
func init() { // 将服务器命令添加到根命令
// 将服务器命令添加到根命令 rootCmd.AddCommand(serverCmd)
rootCmd.AddCommand(serverCmd)
// 添加服务器特定的标志
// 添加服务器特定的标志 serverCmd.Flags().StringP("host", "H", "", "服务器监听地址 (覆盖配置文件)")
serverCmd.Flags().StringP("host", "H", "", "服务器监听地址 (覆盖配置文件)") serverCmd.Flags().IntP("port", "p", 0, "服务器监听端口 (覆盖配置文件)")
serverCmd.Flags().IntP("port", "p", 0, "服务器监听端口 (覆盖配置文件)") }
}
// runServer 运行HTTP服务器
// runServer 运行HTTP服务器 func runServer(cmd *cobra.Command, args []string) {
func runServer(cmd *cobra.Command, args []string) { // 获取配置
// 获取配置 host := getServerHost(cmd)
host := getServerHost(cmd) port := getServerPort(cmd)
port := getServerPort(cmd) addr := fmt.Sprintf("%s:%d", host, port)
addr := fmt.Sprintf("%s:%d", host, port)
// 获取全局日志实例
// 获取全局日志实例 logger := logger.GetLogger()
logger := logger.GetLogger() logger.LogServerStart(host, port)
logger.LogServerStart(host, port)
// 重定向 Gin 框架内部日志到 Logrus
// 重定向 Gin 框架内部日志到 Logrus // 这将捕获 [GIN-debug] 路由注册日志和其他框架级输出
// 这将捕获 [GIN-debug] 路由注册日志和其他框架级输出 gin.DefaultWriter = logger.WriterLevel(logrus.DebugLevel)
gin.DefaultWriter = logger.WriterLevel(logrus.DebugLevel) gin.DefaultErrorWriter = logger.WriterLevel(logrus.ErrorLevel)
gin.DefaultErrorWriter = logger.WriterLevel(logrus.ErrorLevel)
// 设置 Gin 模式
// 设置 Gin 模式 if !viper.GetBool("server.dev_mode") {
if !viper.GetBool("server.dev_mode") { gin.SetMode(gin.ReleaseMode)
gin.SetMode(gin.ReleaseMode) }
}
// 初始化Redis如果配置存在失败不致命
// 初始化Redis如果配置存在失败不致命 utils.InitRedis()
utils.InitRedis()
// 初始化数据库(根据 viper 配置选择 SQLite 或 MySQL
// 初始化数据库(根据 viper 配置选择 SQLite 或 MySQL // 如果初始化失败(例如 MySQL 连不上),则打印错误并退出
// 如果初始化失败则回退并退出 db, err := database.Init()
db, err := database.Init() if err != nil {
if err != nil { logrus.WithError(err).Fatal("数据库初始化失败,请检查配置或确认是否已安装")
logrus.WithError(err).Fatal("数据库初始化失败") }
}
if db != nil {
if db != nil { // 执行自动迁移(确保表结构存在)
// 执行自动迁移(确保表结构存在) if err := database.AutoMigrate(); err != nil {
if err := database.AutoMigrate(); err != nil { logrus.WithError(err).Fatal("数据库自动迁移失败")
logrus.WithError(err).Fatal("数据库自动迁移失败") }
} // 初始化默认系统设置
// 初始化默认系统设置 if err := database.SeedDefaultSettings(); err != nil {
if err := database.SeedDefaultSettings(); err != nil { logrus.WithError(err).Fatal("默认系统设置初始化失败")
logrus.WithError(err).Fatal("默认系统设置初始化失败") }
}
// 初始化加密管理器
// 初始化加密管理器 // 从数据库设置中获取加密密钥
// 从数据库设置中获取加密密钥 encryptionKey := services.GetSettingsService().GetEncryptionKey()
encryptionKey := services.GetSettingsService().GetEncryptionKey() if err := utils.InitEncryption(encryptionKey); err != nil {
if err := utils.InitEncryption(encryptionKey); err != nil { logrus.WithError(err).Fatal("加密管理器初始化失败")
logrus.WithError(err).Fatal("加密管理器初始化失败") }
}
// 启动日志清理定时任务
// 启动日志清理定时任务 services.StartLogCleanupTask()
services.StartLogCleanupTask() } else {
} else { logrus.Info("系统处于未初始化状态,跳过数据库自动迁移和设置加载")
logrus.Info("系统处于未初始化状态,跳过数据库自动迁移和设置加载") }
}
// 创建HTTP服务器
// 创建HTTP服务器 server := createHTTPServer(addr)
server := createHTTPServer(addr)
// 启动服务器
// 启动服务器 startServer(server)
startServer(server) }
}
// getServerHost 获取服务器监听地址
// getServerHost 获取服务器监听地址 func getServerHost(cmd *cobra.Command) string {
func getServerHost(cmd *cobra.Command) string { if host, _ := cmd.Flags().GetString("host"); host != "" {
if host, _ := cmd.Flags().GetString("host"); host != "" { return host
return host }
} return viper.GetString("server.host")
return viper.GetString("server.host") }
}
// getServerPort 获取服务器监听端口
// getServerPort 获取服务器监听端口 func getServerPort(cmd *cobra.Command) int {
func getServerPort(cmd *cobra.Command) int { if port, _ := cmd.Flags().GetInt("port"); port != 0 {
if port, _ := cmd.Flags().GetInt("port"); port != 0 { return port
return port }
} return viper.GetInt("server.port")
return viper.GetInt("server.port") }
}
// createHTTPServer 创建HTTP服务器
// createHTTPServer 创建HTTP服务器 func createHTTPServer(addr string) *http.Server {
func createHTTPServer(addr string) *http.Server { // 创建 Gin 引擎
// 创建 Gin 引擎 r := gin.New()
r := gin.New()
// 使用默认的 Recovery 中间件
// 使用默认的 Recovery 中间件 r.Use(gin.Recovery())
r.Use(gin.Recovery())
// 启用 CORS 中间件,支持前后端分离
// 添加日志中间件 r.Use(middleware.CorsMiddleware())
// 默认为 true只有显式设置为 false 才关闭
enableAccessLog := true // 添加日志中间件
if viper.IsSet("server.access_log") { // 默认为 true只有显式设置为 false 才关闭
enableAccessLog = viper.GetBool("server.access_log") enableAccessLog := true
} if viper.IsSet("server.access_log") {
if enableAccessLog { enableAccessLog = viper.GetBool("server.access_log")
r.Use(middleware.WrapHandler()) }
} if enableAccessLog {
r.Use(middleware.WrapHandler())
// 添加安装检查中间件 }
r.Use(middleware.InstallCheckMiddleware())
// 添加开发模式中间件(统一管理开发模式功能)
// 添加维护模式中间件 r.Use(middleware.DevModeMiddleware())
r.Use(middleware.MaintenanceMiddleware())
// 添加安装检查中间件
// 添加开发模式中间件(统一管理开发模式功能:模板热重载等) r.Use(middleware.InstallCheckMiddleware())
r.Use(middleware.DevModeMiddleware(r))
// 添加维护模式中间件
// 加载并设置 HTML 模板 r.Use(middleware.MaintenanceMiddleware())
if tmpl, err := web.ParseTemplates(); err == nil {
r.SetHTMLTemplate(tmpl) // 注册路由
} else { registerRoutes(r)
logrus.WithError(err).Error("HTML模板加载失败")
} return &http.Server{
Addr: addr,
// 注册路由 Handler: r,
registerRoutes(r) }
}
return &http.Server{
Addr: addr, // registerRoutes 注册HTTP路由
Handler: r, func registerRoutes(r *gin.Engine) {
} // 使用server包中的路由注册函数
} server.RegisterRoutes(r)
}
// registerRoutes 注册HTTP路由
func registerRoutes(r *gin.Engine) { // startServer 启动服务器并处理优雅关闭
// 使用server包中的路由注册函数 func startServer(server *http.Server) {
server.RegisterRoutes(r) // 获取全局日志实例
} logger := logger.GetLogger()
// startServer 启动服务器并处理优雅关闭 // 创建一个通道来接收操作系统信号
func startServer(server *http.Server) { sigChan := make(chan os.Signal, 1)
// 获取全局日志实例 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
logger := logger.GetLogger()
// 在goroutine中启动服务器
// 创建一个通道来接收操作系统信号 go func() {
sigChan := make(chan os.Signal, 1) logger.WithField("addr", server.Addr).Info("HTTP服务器已启动")
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.LogError(err, "服务器启动失败")
// 在goroutine中启动服务器 os.Exit(1)
go func() { }
logger.WithField("addr", server.Addr).Info("HTTP服务器已启动") }()
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.LogError(err, "服务器启动失败") // 等待中断信号
os.Exit(1) <-sigChan
} logger.Info("收到关闭信号,正在优雅关闭服务器...")
}()
// 创建一个带超时的上下文
// 等待中断信号 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
<-sigChan defer cancel()
logger.Info("收到关闭信号,正在优雅关闭服务器...")
// 优雅关闭服务器
// 创建一个带超时的上下文 if err := server.Shutdown(ctx); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) logger.LogError(err, "服务器关闭时出错")
defer cancel() } else {
logger.LogServerStop()
// 优雅关闭服务器 }
if err := server.Shutdown(ctx); err != nil { }
logger.LogError(err, "服务器关闭时出错")
} else {
logger.LogServerStop()
}
}

View File

@@ -3,14 +3,15 @@ package config
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "os"
"io/fs"
"path/filepath" "path/filepath"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var currentConfigFilePath string
// ============================================================================ // ============================================================================
// 结构体定义 // 结构体定义
// ============================================================================ // ============================================================================
@@ -127,63 +128,67 @@ func GetDefaultAppConfig() *AppConfig {
// Init 初始化配置文件 // Init 初始化配置文件
func Init(cfgFilePath string) { func Init(cfgFilePath string) {
currentConfigFilePath = cfgFilePath
viper.SetConfigFile(cfgFilePath) viper.SetConfigFile(cfgFilePath)
viper.SetConfigType("json") viper.SetConfigType("json")
viper.AddConfigPath(".") viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil { // 检查配置文件是否存在,如果不存在则使用内存默认配置,并创建默认配置文件
var pathError *fs.PathError if _, err := os.Stat(cfgFilePath); os.IsNotExist(err) {
if errors.As(err, &pathError) { log.WithField("file", cfgFilePath).Info("配置文件不存在,将在本地生成默认配置")
log.Warn("未找到配置文件,使用默认配置在内存中运行(需通过安装页面初始化)") defaultConfig := GetDefaultAppConfig()
// 使用默认配置 configBytes, err := json.MarshalIndent(defaultConfig, "", " ")
defaultConfig := GetDefaultAppConfig() if err != nil {
// 将配置结构体转换为JSON
configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ")
if marshalErr != nil {
log.WithFields(
log.Fields{
"err": marshalErr,
},
).Fatal("序列化默认配置失败")
return
}
// 将配置加载到viper中但不写入文件
err = viper.ReadConfig(bytes.NewBuffer(configBytes))
if err != nil {
log.WithFields(
log.Fields{
"err": err,
},
).Error("读取默认配置失败")
} else {
log.Info("已成功在内存中加载默认配置")
}
// 不在这里写入文件了,安装完成后通过 UpdateConfig 写入
} else {
log.WithFields( log.WithFields(
log.Fields{ log.Fields{
"err": err, "err": err,
}, },
).Fatal("配置文件解析错误") ).Fatal("默认配置序列化错误")
} }
} else {
// 只显示配置文件名,不显示完整路径 // 创建默认配置文件
configFile := viper.ConfigFileUsed() if err := os.WriteFile(cfgFilePath, configBytes, 0644); err != nil {
if configFile != "" {
// 统一使用 filepath.Clean 和 filepath.Base 处理路径展示
cleanPath := filepath.Clean(configFile)
log.WithFields( log.WithFields(
log.Fields{ log.Fields{
"file": cleanPath, "err": err,
}, },
).Info("使用配置文件") ).Fatal("创建默认配置文件失败")
} }
// 将配置加载到viper中
err = viper.ReadConfig(bytes.NewBuffer(configBytes))
if err != nil {
log.WithFields(
log.Fields{
"err": err,
},
).Error("读取默认配置失败")
} else {
log.Info("已成功在内存中加载默认配置")
}
// 明确设置当前配置路径为待保存的路径,以便后续安装时保存
currentConfigFilePath = cfgFilePath
return
} }
if err := viper.ReadInConfig(); err != nil {
log.WithFields(
log.Fields{
"err": err,
},
).Fatal("配置文件解析错误")
}
// 统一使用 filepath.Clean 和 filepath.Base 处理路径展示
cleanPath := filepath.Clean(cfgFilePath)
log.WithFields(
log.Fields{
"file": cleanPath,
},
).Info("使用配置文件")
// 验证配置 // 验证配置
if _, err := ValidateConfig(); err != nil { if _, err := ValidateConfig(); err != nil {
log.WithFields( log.WithFields(
@@ -194,6 +199,32 @@ func Init(cfgFilePath string) {
} }
} }
func SaveConfig(appConfig *AppConfig) error {
if err := ValidateConfigValue(appConfig); err != nil {
return err
}
if currentConfigFilePath == "" {
currentConfigFilePath = "./config.json"
}
if err := os.MkdirAll(filepath.Dir(currentConfigFilePath), 0755); err != nil {
return err
}
configBytes, err := json.MarshalIndent(appConfig, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(currentConfigFilePath, configBytes, 0644); err != nil {
return err
}
viper.SetConfigFile(currentConfigFilePath)
viper.SetConfigType("json")
if err := viper.ReadInConfig(); err != nil {
return err
}
syncViperConfig(appConfig)
return nil
}
// UpdateConfig 更新配置文件 // UpdateConfig 更新配置文件
// 接收一个回调函数,在回调函数中修改配置对象,然后保存到文件 // 接收一个回调函数,在回调函数中修改配置对象,然后保存到文件
func UpdateConfig(updateFn func(*AppConfig)) error { func UpdateConfig(updateFn func(*AppConfig)) error {
@@ -206,18 +237,16 @@ func UpdateConfig(updateFn func(*AppConfig)) error {
// 2. 执行更新回调 // 2. 执行更新回调
updateFn(&currentConfig) updateFn(&currentConfig)
// 3. 将更新后的配置写回 Viper return SaveConfig(&currentConfig)
// 注意:这里需要手动设置回 viper否则 viper.WriteConfig() 写入的还是旧配置 }
// 也可以直接序列化 currentConfig 写入文件
// 更新 Server 配置 func syncViperConfig(currentConfig *AppConfig) {
viper.Set("server.host", currentConfig.Server.Host) viper.Set("server.host", currentConfig.Server.Host)
viper.Set("server.port", currentConfig.Server.Port) viper.Set("server.port", currentConfig.Server.Port)
viper.Set("server.dist", currentConfig.Server.Dist) viper.Set("server.dist", currentConfig.Server.Dist)
viper.Set("server.dev_mode", currentConfig.Server.DevMode) viper.Set("server.dev_mode", currentConfig.Server.DevMode)
viper.Set("server.access_log", currentConfig.Server.AccessLog) viper.Set("server.access_log", currentConfig.Server.AccessLog)
// 更新 Database 配置
viper.Set("database.type", currentConfig.Database.Type) viper.Set("database.type", currentConfig.Database.Type)
viper.Set("database.mysql.host", currentConfig.Database.MySQL.Host) viper.Set("database.mysql.host", currentConfig.Database.MySQL.Host)
viper.Set("database.mysql.port", currentConfig.Database.MySQL.Port) viper.Set("database.mysql.port", currentConfig.Database.MySQL.Port)
@@ -229,27 +258,14 @@ func UpdateConfig(updateFn func(*AppConfig)) error {
viper.Set("database.mysql.max_open_conns", currentConfig.Database.MySQL.MaxOpenConns) viper.Set("database.mysql.max_open_conns", currentConfig.Database.MySQL.MaxOpenConns)
viper.Set("database.sqlite.path", currentConfig.Database.SQLite.Path) viper.Set("database.sqlite.path", currentConfig.Database.SQLite.Path)
// 更新 Redis 配置
viper.Set("redis.host", currentConfig.Redis.Host) viper.Set("redis.host", currentConfig.Redis.Host)
viper.Set("redis.port", currentConfig.Redis.Port) viper.Set("redis.port", currentConfig.Redis.Port)
viper.Set("redis.password", currentConfig.Redis.Password) viper.Set("redis.password", currentConfig.Redis.Password)
viper.Set("redis.db", currentConfig.Redis.DB) viper.Set("redis.db", currentConfig.Redis.DB)
// 更新 Log 配置
viper.Set("log.level", currentConfig.Log.Level) viper.Set("log.level", currentConfig.Log.Level)
viper.Set("log.file", currentConfig.Log.File) viper.Set("log.file", currentConfig.Log.File)
viper.Set("log.max_size", currentConfig.Log.MaxSize) viper.Set("log.max_size", currentConfig.Log.MaxSize)
viper.Set("log.max_backups", currentConfig.Log.MaxBackups) viper.Set("log.max_backups", currentConfig.Log.MaxBackups)
viper.Set("log.max_age", currentConfig.Log.MaxAge) viper.Set("log.max_age", currentConfig.Log.MaxAge)
// 4. 保存到文件
if err := viper.WriteConfig(); err != nil {
// 如果配置文件不存在(比如只用了默认配置没写文件),则尝试 SafeWriteConfig
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return viper.SafeWriteConfig()
}
return err
}
return nil
} }

View File

@@ -27,7 +27,7 @@ func ValidateConfig() (*AppConfig, error) {
} }
// 验证配置 // 验证配置
if err := validateConfig(&config); err != nil { if err := ValidateConfigValue(&config); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err) return nil, fmt.Errorf("配置验证失败: %w", err)
} }
@@ -35,12 +35,8 @@ func ValidateConfig() (*AppConfig, error) {
return &config, nil return &config, nil
} }
// ============================================================================ // ValidateConfigValue 验证配置
// 私有函数 func ValidateConfigValue(config *AppConfig) error {
// ============================================================================
// validateConfig 验证配置
func validateConfig(config *AppConfig) error {
// 验证服务器配置 // 验证服务器配置
if err := validateServerConfig(&config.Server); err != nil { if err := validateServerConfig(&config.Server); err != nil {
return fmt.Errorf("服务器配置错误: %w", err) return fmt.Errorf("服务器配置错误: %w", err)

View File

@@ -7,5 +7,5 @@ package constants
// 应用程序版本信息 // 应用程序版本信息
const ( const (
// AppVersion 应用程序版本号 // AppVersion 应用程序版本号
AppVersion = "1.0.3" AppVersion = "2.0.1"
) )

View File

@@ -21,17 +21,6 @@ import (
// 创建基础控制器实例 // 创建基础控制器实例
var apiBaseController = controllers.NewBaseController() var apiBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// APIFragmentHandler 接口列表页面片段处理器
func APIFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "apis.html", gin.H{
"Title": "接口设置",
})
}
// ============================================================================ // ============================================================================
// API处理器 // API处理器
// ============================================================================ // ============================================================================
@@ -122,18 +111,12 @@ func APIListHandler(c *gin.Context) {
responseAPIs = append(responseAPIs, responseAPI) responseAPIs = append(responseAPIs, responseAPI)
} }
// 计算分页信息 // 返回结果
totalPages := (total + int64(limit) - 1) / int64(limit)
response := gin.H{ response := gin.H{
"success": true, "code": 0,
"data": gin.H{ "msg": "success",
"apis": responseAPIs, "count": total,
"total": total, "data": responseAPIs,
"page": page,
"limit": limit,
"total_pages": totalPages,
},
} }
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)

View File

@@ -21,17 +21,6 @@ import (
var appBaseController = controllers.NewBaseController() var appBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// AppsFragmentHandler 应用列表页面片段处理器
func AppsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "apps.html", gin.H{
"Title": "应用程序",
})
}
// ============================================================================ // ============================================================================
// API处理器 // API处理器
// ============================================================================ // ============================================================================
@@ -359,6 +348,7 @@ func AppCreateHandler(c *gin.Context) {
} }
// 提交事务 // 提交事务
if err := tx.Commit().Error; err != nil { if err := tx.Commit().Error; err != nil {
logrus.WithError(err).Error("Failed to commit transaction") logrus.WithError(err).Error("Failed to commit transaction")
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@@ -531,6 +521,28 @@ func AppDeleteHandler(c *gin.Context) {
return return
} }
// 删除相关的变量记录
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Variable{}).Error; err != nil {
tx.Rollback()
logrus.WithError(err).Error("Failed to delete related variables")
c.JSON(http.StatusInternalServerError, gin.H{
"code": 1,
"msg": "删除相关变量失败",
})
return
}
// 删除相关的函数记录
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Function{}).Error; err != nil {
tx.Rollback()
logrus.WithError(err).Error("Failed to delete related functions")
c.JSON(http.StatusInternalServerError, gin.H{
"code": 1,
"msg": "删除相关函数失败",
})
return
}
// 删除应用 // 删除应用
if err := tx.Delete(&app).Error; err != nil { if err := tx.Delete(&app).Error; err != nil {
tx.Rollback() tx.Rollback()
@@ -569,7 +581,7 @@ func AppDeleteHandler(c *gin.Context) {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"app_id": app.ID, "app_id": app.ID,
"app_uuid": app.UUID, "app_uuid": app.UUID,
}).Debug("Successfully deleted app and related APIs") }).Debug("Successfully deleted app and related APIs, Variables and Functions")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"code": 0, "code": 0,

View File

@@ -24,54 +24,30 @@ import (
var authBaseController = controllers.NewBaseController() var authBaseController = controllers.NewBaseController()
// ============================================================================ // ============================================================================
// 页面处理器 // API处理器
// ============================================================================ // ============================================================================
// LoginPageHandler 管理员登录页渲染处理器 // CSRFTokenHandler 获取CSRF令牌接口
// - 如果已登录则重定向到 /admin func CSRFTokenHandler(c *gin.Context) {
// - 否则渲染 web/template/admin/login.html 模板
// - 自动清理失效的JWT Cookie避免刷新时的问题
func LoginPageHandler(c *gin.Context) {
// 使用带清理功能的JWT校验避免失效Cookie在登录页面造成问题
if IsAdminAuthenticatedWithCleanup(c) {
c.Redirect(http.StatusFound, "/admin")
return
}
// 获取或生成CSRF令牌
var token string
// 尝试从Cookie获取 // 尝试从Cookie获取
var token string
if cookie, err := c.Cookie(CSRFCookieName); err == nil && cookie != "" { if cookie, err := c.Cookie(CSRFCookieName); err == nil && cookie != "" {
token = cookie token = cookie
} else { } else {
// 生成新的CSRF令牌并设置到Cookie
newToken, err := utils.GenerateCSRFToken() newToken, err := utils.GenerateCSRFToken()
if err != nil { if err != nil {
c.HTML(http.StatusInternalServerError, "error.html", gin.H{ authBaseController.HandleInternalError(c, "生成CSRF令牌失败", err)
"Error": "生成CSRF令牌失败",
})
return return
} }
token = newToken token = newToken
setCSRFToken(c, token) setCSRFToken(c, token)
} }
// 准备模板数据 authBaseController.HandleSuccess(c, "success", gin.H{
data := authBaseController.GetDefaultTemplateData() "csrf_token": token,
if sysName, ok := data["SystemName"].(string); ok && sysName != "" { })
data["Title"] = sysName + " - 管理员登录"
} else {
data["Title"] = "管理员登录"
}
data["CSRFToken"] = token
c.HTML(http.StatusOK, "login.html", data)
} }
// ============================================================================
// API处理器
// ============================================================================
// LoginHandler 管理员登录接口 // LoginHandler 管理员登录接口
// - 接收JSON: {username, password, captcha, csrf_token} // - 接收JSON: {username, password, captcha, csrf_token}
// - 验证CSRF令牌 // - 验证CSRF令牌
@@ -112,35 +88,30 @@ func LoginHandler(c *gin.Context) {
return return
} }
// 获取系统设置服务 // 从数据库中查找对应的用户
settingsService := services.GetSettingsService() db, err := database.GetDB()
adminUsername := settingsService.GetString("admin_username", "admin") if err != nil {
adminPasswordHash := settingsService.GetString("admin_password", "") recordLoginLog(c, body.Username, 0, "数据库连接失败")
adminPasswordSalt := settingsService.GetString("admin_password_salt", "") authBaseController.HandleInternalError(c, "数据库连接失败", err)
// 验证密码为空的情况(首次登录需要初始化)
if adminPasswordHash == "" || adminPasswordSalt == "" {
recordLoginLog(c, body.Username, 0, "管理员账号未初始化")
authBaseController.HandleInternalError(c, "管理员账号未初始化,请联系系统管理员", nil)
return return
} }
// 验证用户名 var user models.User
if body.Username != adminUsername { if err := db.Where("username = ? AND role = ?", body.Username, 0).First(&user).Error; err != nil {
recordLoginLog(c, body.Username, 0, "用户名错误") recordLoginLog(c, body.Username, 0, "用户不存在或非管理员")
authBaseController.HandleValidationError(c, "用户不存在或密码错误") authBaseController.HandleValidationError(c, "用户不存在或密码错误")
return return
} }
// 验证密码(使用盐值校验) // 验证密码(使用盐值校验)
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPasswordHash) { if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
recordLoginLog(c, body.Username, 0, "密码错误") recordLoginLog(c, body.Username, 0, "密码错误")
authBaseController.HandleValidationError(c, "用户不存在或密码错误") authBaseController.HandleValidationError(c, "用户不存在或密码错误")
return return
} }
// 生成JWT令牌 // 生成JWT令牌
token, err := generateJWTTokenForAdmin(body.Username, adminPasswordHash) token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID)
if err != nil { if err != nil {
recordLoginLog(c, body.Username, 0, "生成令牌失败") recordLoginLog(c, body.Username, 0, "生成令牌失败")
authBaseController.HandleInternalError(c, "生成令牌失败", err) authBaseController.HandleInternalError(c, "生成令牌失败", err)
@@ -149,6 +120,7 @@ func LoginHandler(c *gin.Context) {
// 设置JWT CookieHttpOnly安全 // 设置JWT CookieHttpOnly安全
// 使用系统配置的Cookie参数 // 使用系统配置的Cookie参数
settingsService := services.GetSettingsService()
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig() secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite) cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly) c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
@@ -156,6 +128,9 @@ func LoginHandler(c *gin.Context) {
recordLoginLog(c, body.Username, 1, "登录成功") recordLoginLog(c, body.Username, 1, "登录成功")
authBaseController.HandleSuccess(c, "登录成功", gin.H{ authBaseController.HandleSuccess(c, "登录成功", gin.H{
"redirect": "/admin", "redirect": "/admin",
"avatar": user.Avatar,
"nickname": user.Nickname,
"username": user.Username,
}) })
} }
@@ -282,7 +257,7 @@ type JWTClaims struct {
// - 包含管理员用户名信息和密码哈希 // - 包含管理员用户名信息和密码哈希
// - 设置过期时间 // - 设置过期时间
// - 使用HMAC-SHA256签名 // - 使用HMAC-SHA256签名
func generateJWTTokenForAdmin(username, passwordHash string) (string, error) { func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string) (string, error) {
// 生成密码哈希摘要使用SHA256 // 生成密码哈希摘要使用SHA256
// 注意:传入的 passwordHash 已经是数据库存的 Hash这里我们再次 Hash 还是直接用? // 注意:传入的 passwordHash 已经是数据库存的 Hash这里我们再次 Hash 还是直接用?
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password) // atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
@@ -292,9 +267,6 @@ func generateJWTTokenForAdmin(username, passwordHash string) (string, error) {
// 所以这里也应该对数据库里的值进行 Hash。 // 所以这里也应该对数据库里的值进行 Hash。
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash) passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
// 获取虚拟管理员UUID (NetworkAuth 项目默认为 admin-uuid-001)
adminUUID := services.GetSettingsService().GetString("admin_uuid", "admin-uuid-001")
claims := JWTClaims{ claims := JWTClaims{
Username: username, Username: username,
UUID: adminUUID, UUID: adminUUID,
@@ -352,16 +324,16 @@ func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
return false return false
} }
// 获取当前数据库中的管理员密码 // 获取当前数据库中的管理员用户
var adminPassword models.Settings var adminUser models.User
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil { if err := db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser).Error; err != nil {
fmt.Printf("[SECURITY WARNING] Admin password not found in database - Username=%s, IP=%s\n", fmt.Printf("[SECURITY WARNING] Admin user not found in database - Username=%s, IP=%s\n",
claims.Username, c.ClientIP()) claims.Username, c.ClientIP())
return false return false
} }
// 生成当前数据库密码的哈希摘要 // 生成当前数据库密码的哈希摘要
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value) currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
// 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致 // 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致
if claims.PasswordHash != currentPasswordHash { if claims.PasswordHash != currentPasswordHash {
@@ -417,12 +389,13 @@ func IsAdminAuthenticatedHttp(r *http.Request) bool {
return false return false
} }
var adminPassword models.Settings var adminUser models.User
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil { if err := db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser).Error; err != nil {
return false return false
} }
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value) // 验证密码哈希
currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
if claims.PasswordHash != currentPasswordHash { if claims.PasswordHash != currentPasswordHash {
return false return false
} }
@@ -518,11 +491,11 @@ func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
if time.Until(claims.ExpiresAt.Time) < refreshThreshold { if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
// 获取当前的 PasswordHash // 获取当前的 PasswordHash
db, _ := database.GetDB() db, _ := database.GetDB()
var adminPassword models.Settings var adminUser models.User
db.Where("name = ?", "admin_password").First(&adminPassword) db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser)
// 使用新的有效期生成令牌 // 使用新的有效期生成令牌
newToken, err := generateJWTTokenForAdmin(claims.Username, adminPassword.Value) newToken, err := generateJWTTokenForAdmin(claims.Username, adminUser.Password, claims.UUID)
if err == nil { if err == nil {
tokenToSet = newToken tokenToSet = newToken
refreshed = true refreshed = true
@@ -553,19 +526,12 @@ func AdminAuthRequired() gin.HandlerFunc {
// 自动清理失效的JWT Cookie提升安全性和用户体验 // 自动清理失效的JWT Cookie提升安全性和用户体验
clearInvalidJWTCookie(c) clearInvalidJWTCookie(c)
// 中文注释区分普通页面请求与AJAX/JSON请求 // API 请求直接返回 401 JSON
accept := c.GetHeader("Accept") c.JSON(http.StatusUnauthorized, gin.H{
xrw := strings.ToLower(strings.TrimSpace(c.GetHeader("X-Requested-With"))) "success": false,
if strings.Contains(accept, "application/json") || xrw == "xmlhttprequest" { "message": "未登录或会话已过期",
c.JSON(http.StatusUnauthorized, gin.H{ "data": nil,
"success": false, })
"message": "未登录或会话已过期",
"data": nil,
})
c.Abort()
return
}
c.Redirect(http.StatusFound, "/admin/login")
c.Abort() c.Abort()
return return
} }

View File

@@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha" "github.com/mojocn/base64Captcha"
"github.com/sirupsen/logrus"
) )
// ============================================================================ // ============================================================================
@@ -100,8 +101,10 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
// 从cookie中获取验证码ID // 从cookie中获取验证码ID
captchaId, err := c.Cookie("captcha_id") captchaId, err := c.Cookie("captcha_id")
if err != nil || captchaId == "" { if err != nil || captchaId == "" {
logrus.WithError(err).Warn("验证码验证失败无法从Cookie获取captcha_id")
return false return false
} }
logrus.Infof("VerifyCaptcha: received captchaId=%s, captchaValue=%s", captchaId, captchaValue)
// 先尝试原始值验证 // 先尝试原始值验证
if store.Verify(captchaId, captchaValue, false) { if store.Verify(captchaId, captchaValue, false) {

View File

@@ -6,9 +6,7 @@ import (
"NetworkAuth/middleware" "NetworkAuth/middleware"
"NetworkAuth/models" "NetworkAuth/models"
"NetworkAuth/services" "NetworkAuth/services"
"NetworkAuth/utils"
"NetworkAuth/utils/timeutil" "NetworkAuth/utils/timeutil"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -42,86 +40,12 @@ func formatDBType(dbType string) string {
} }
} }
// ============================================================================
// 页面处理器
// ============================================================================
// AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口
// - 未登录:重定向到 /admin/login
// - 已登录:渲染后台布局页(或重定向到 /admin/layout
// - 自动清理失效的JWT Cookie
func AdminIndexHandler(c *gin.Context) {
if IsAdminAuthenticatedWithCleanup(c) {
// 直接渲染布局页保持URL为 /admin
AdminLayoutHandler(c)
return
}
c.Redirect(http.StatusFound, "/admin/login")
}
// AdminLayoutHandler 后台布局页渲染
// - 渲染 layout.html包含顶部导航、侧边栏与动态内容容器
func AdminLayoutHandler(c *gin.Context) {
// 获取或生成CSRF令牌
var token string
if existingToken := utils.GetCSRFTokenFromCookie(c); existingToken != "" {
// 重用现有的Cookie令牌
token = existingToken
} else {
// 生成新的CSRF令牌并设置到Cookie
newToken, err := utils.GenerateCSRFToken()
if err != nil {
handlersBaseController.HandleInternalError(c, "生成CSRF令牌失败", err)
return
}
token = newToken
utils.SetCSRFToken(c, token)
}
// 准备模板数据
data := handlersBaseController.GetDefaultTemplateData()
data["CSRFToken"] = token
// 从数据库读取站点标题,如果失败则使用默认值
settingsSvc := services.GetSettingsService()
data["Title"] = settingsSvc.GetString("site_title", "后台管理")
// 合并其他数据(如果有的话)
extraData := gin.H{}
for key, value := range extraData {
data[key] = value
}
c.HTML(http.StatusOK, "layout.html", data)
}
// DashboardFragmentHandler 仪表盘片段渲染
// - 展示系统信息:版本、开发模式、数据库类型、启动时长
func DashboardFragmentHandler(c *gin.Context) {
version := constants.AppVersion
mode := middleware.IsDevModeFromContext(c)
dbType := viper.GetString("database.type")
if dbType == "" {
dbType = "sqlite"
}
uptime := timeutil.GetServerUptimeString()
data := gin.H{
"Version": version,
"Mode": mode,
"DBType": formatDBType(dbType),
"Uptime": uptime,
}
c.HTML(http.StatusOK, "dashboard.html", data)
}
// ============================================================================ // ============================================================================
// API处理器 // API处理器
// ============================================================================ // ============================================================================
// SystemInfoHandler 系统信息API接口 // SystemInfoHandler 系统信息API接口
// - 返回系统运行状态的JSON数据用于前端定时刷新 // 返回系统运行状态的JSON数据用于前端定时刷新
func SystemInfoHandler(c *gin.Context) { func SystemInfoHandler(c *gin.Context) {
version := constants.AppVersion version := constants.AppVersion
mode := middleware.IsDevModeFromContext(c) mode := middleware.IsDevModeFromContext(c)
@@ -130,19 +54,21 @@ func SystemInfoHandler(c *gin.Context) {
dbType = "sqlite" dbType = "sqlite"
} }
uptime := timeutil.GetServerUptimeString() uptime := timeutil.GetServerUptimeString()
uptimeSeconds := int64(timeutil.GetServerUptime().Seconds())
data := gin.H{ data := gin.H{
"version": version, "version": version,
"mode": mode, "mode": mode,
"db_type": formatDBType(dbType), "db_type": formatDBType(dbType),
"uptime": uptime, "uptime": uptime,
"uptime_seconds": uptimeSeconds,
} }
handlersBaseController.HandleSuccess(c, "ok", data) handlersBaseController.HandleSuccess(c, "ok", data)
} }
// DashboardStatsHandler 仪表盘统计数据API接口 // DashboardStatsHandler 仪表盘统计数据API接口
// - 返回应用统计数据的JSON数据包括全部/启用/禁用/变量数量 // - 返回应用统计数据的JSON数据包括全部/启用/变量数量
func DashboardStatsHandler(c *gin.Context) { func DashboardStatsHandler(c *gin.Context) {
// 获取数据库连接 // 获取数据库连接
db, ok := handlersBaseController.GetDB(c) db, ok := handlersBaseController.GetDB(c)
@@ -152,8 +78,7 @@ func DashboardStatsHandler(c *gin.Context) {
// 统计应用数据 // 统计应用数据
var totalApps int64 var totalApps int64
var enabledApps int64 var totalFunctions int64
var disabledApps int64
var totalVariables int64 var totalVariables int64
// 统计全部应用数量 // 统计全部应用数量
@@ -162,15 +87,9 @@ func DashboardStatsHandler(c *gin.Context) {
return return
} }
// 统计启用应用数量 // 统计函数数量
if err := db.Model(&models.App{}).Where("status = ?", 1).Count(&enabledApps).Error; err != nil { if err := db.Model(&models.Function{}).Count(&totalFunctions).Error; err != nil {
handlersBaseController.HandleInternalError(c, "统计启用应用数量失败", err) handlersBaseController.HandleInternalError(c, "统计函数数量失败", err)
return
}
// 统计禁用应用数量
if err := db.Model(&models.App{}).Where("status = ?", 0).Count(&disabledApps).Error; err != nil {
handlersBaseController.HandleInternalError(c, "统计禁用应用数量失败", err)
return return
} }
@@ -182,8 +101,7 @@ func DashboardStatsHandler(c *gin.Context) {
data := gin.H{ data := gin.H{
"total_apps": totalApps, "total_apps": totalApps,
"enabled_apps": enabledApps, "total_functions": totalFunctions,
"disabled_apps": disabledApps,
"total_variables": totalVariables, "total_variables": totalVariables,
} }

View File

@@ -20,17 +20,6 @@ import (
// 创建基础控制器实例 // 创建基础控制器实例
var functionBaseController = controllers.NewBaseController() var functionBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// FunctionFragmentHandler 公共函数列表页面片段处理器
func FunctionFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "functions.html", gin.H{
"Title": "公共函数",
})
}
// ============================================================================ // ============================================================================
// API处理器 // API处理器
// ============================================================================ // ============================================================================

View File

@@ -4,7 +4,6 @@ import (
"NetworkAuth/controllers" "NetworkAuth/controllers"
"NetworkAuth/models" "NetworkAuth/models"
"NetworkAuth/services" "NetworkAuth/services"
"net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -45,13 +44,6 @@ func RecordLoginLog(c *gin.Context, username string, status int, message string)
} }
} }
// LoginLogsFragmentHandler 登录日志页面片段处理器
func LoginLogsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "login_logs.html", gin.H{
"Title": "登录日志",
})
}
// ============================================================================ // ============================================================================
// API处理器 // API处理器
// ============================================================================ // ============================================================================
@@ -61,7 +53,7 @@ func LoginLogsListHandler(c *gin.Context) {
// 获取分页参数 // 获取分页参数
page, limit := loginLogBaseController.GetPaginationParams(c) page, limit := loginLogBaseController.GetPaginationParams(c)
// 构建查询 // 获取数据库连接
db, ok := loginLogBaseController.GetDB(c) db, ok := loginLogBaseController.GetDB(c)
if !ok { if !ok {
return return
@@ -132,17 +124,19 @@ func LoginLogsClearHandler(c *gin.Context) {
} }
// 记录操作日志 // 记录操作日志
// 由于 NetworkAuth 中没有 SystemAdminUser 全局变量,这里暂时使用 "admin" var operator, operatorUUID string
operator := "admin" if claims, _, err := GetCurrentAdminUserWithRefresh(c); err == nil && claims != nil {
// 尝试从上下文获取用户名(如果中间件设置了的话) operator = claims.Username
// if user, exists := c.Get("username"); exists { operatorUUID = claims.UUID
// operator = user.(string) } else {
// } operator = "admin"
operatorUUID = "00000000-0000-0000-0000-000000000000"
}
log := models.OperationLog{ log := models.OperationLog{
OperationType: "清空登录日志", OperationType: "清空登录日志",
Operator: operator, Operator: operator,
OperatorUUID: "", // NetworkAuth 中暂时无法获取 UUID OperatorUUID: operatorUUID,
Details: "管理员清空了所有登录日志", Details: "管理员清空了所有登录日志",
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }

View File

@@ -4,7 +4,6 @@ import (
"NetworkAuth/controllers" "NetworkAuth/controllers"
"NetworkAuth/models" "NetworkAuth/models"
"NetworkAuth/services" "NetworkAuth/services"
"net/http"
"strings" "strings"
"time" "time"
@@ -19,17 +18,6 @@ import (
var logBaseController = controllers.NewBaseController() var logBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// LogsFragmentHandler 日志操作页面片段处理器
func LogsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "operation_logs.html", gin.H{
"Title": "操作日志",
})
}
// ============================================================================ // ============================================================================
// API处理器 // API处理器
// ============================================================================ // ============================================================================
@@ -92,11 +80,19 @@ func LogsClearHandler(c *gin.Context) {
} }
// 记录操作日志 (因为刚刚清空了,这条将是第一条) // 记录操作日志 (因为刚刚清空了,这条将是第一条)
operator := "admin" var operator, operatorUUID string
if claims, _, err := GetCurrentAdminUserWithRefresh(c); err == nil && claims != nil {
operator = claims.Username
operatorUUID = claims.UUID
} else {
operator = "admin"
operatorUUID = "00000000-0000-0000-0000-000000000000"
}
log := models.OperationLog{ log := models.OperationLog{
OperationType: "清空日志", OperationType: "清空日志",
Operator: operator, Operator: operator,
OperatorUUID: "", OperatorUUID: operatorUUID,
Details: "管理员清空了所有操作日志", Details: "管理员清空了所有操作日志",
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }

View File

@@ -5,38 +5,41 @@ import (
"NetworkAuth/models" "NetworkAuth/models"
"NetworkAuth/services" "NetworkAuth/services"
"NetworkAuth/utils" "NetworkAuth/utils"
"net/http" "fmt"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
// ProfileFragmentHandler 个人资料片段渲染 // ProfileQueryHandler 获取当前登录管理员的用户名和昵称等信息
// - 渲染个人资料与修改密码表单 // - 返回 JSON: {username, nickname, avatar}
func ProfileFragmentHandler(c *gin.Context) { // - 从数据库获取最新信息
c.HTML(http.StatusOK, "profile.html", map[string]interface{}{}) func ProfileQueryHandler(c *gin.Context) {
} claims, _, err := GetCurrentAdminUserWithRefresh(c)
// ProfileInfoHandler 查询当前登录管理员的基本信息
// - 返回 username 字段
func ProfileInfoHandler(c *gin.Context) {
_, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{ authBaseController.HandleValidationError(c, "未登录或会话已过期")
"code": 1,
"msg": "未登录或会话已过期",
"data": nil,
})
return return
} }
// 获取最新设置 // 获取最新设置
settingsService := services.GetSettingsService() db, ok := authBaseController.GetDB(c)
username := settingsService.GetString("admin_username", "admin") if !ok {
return
}
var adminUser models.User
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
authBaseController.HandleInternalError(c, "获取管理员信息失败", err)
return
}
username := adminUser.Username
nickname := adminUser.Nickname
avatar := adminUser.Avatar
authBaseController.HandleSuccess(c, "ok", map[string]interface{}{ authBaseController.HandleSuccess(c, "ok", gin.H{
"username": username, "username": username,
"nickname": nickname,
"avatar": avatar,
}) })
} }
@@ -44,31 +47,34 @@ func ProfileInfoHandler(c *gin.Context) {
// - 接收 JSON: {old_password, new_password, confirm_password} // - 接收 JSON: {old_password, new_password, confirm_password}
// - 校验旧密码正确性、新密码与确认一致性 // - 校验旧密码正确性、新密码与确认一致性
// - 成功后更新密码哈希 // - 成功后更新密码哈希
// - 自动刷新接近过期的JWT令牌
func ProfilePasswordUpdateHandler(c *gin.Context) { func ProfilePasswordUpdateHandler(c *gin.Context) {
_, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "未登录或会话已过期",
"data": nil,
})
return
}
var body struct { var body struct {
OldPassword string `json:"old_password"` OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"` NewPassword string `json:"new_password"`
ConfirmPassword string `json:"confirm_password"` ConfirmPassword string `json:"confirm_password"`
} }
if !authBaseController.BindJSON(c, &body) { if !authBaseController.BindJSON(c, &body) {
return return
} }
// 基础校验 // 获取当前用户信息用于日志记录
if body.OldPassword == "" || body.NewPassword == "" || body.ConfirmPassword == "" { claims, _, err := GetCurrentAdminUserWithRefresh(c)
authBaseController.HandleValidationError(c, "旧密码/新密码/确认密码均不能为空") if err != nil {
authBaseController.HandleValidationError(c, "未登录或会话已过期")
return return
} }
// 基础校验
if !authBaseController.ValidateRequired(c, map[string]interface{}{
"旧密码": body.OldPassword,
"新密码": body.NewPassword,
"确认密码": body.ConfirmPassword,
}) {
return
}
if len(body.NewPassword) < 6 { if len(body.NewPassword) < 6 {
authBaseController.HandleValidationError(c, "新密码长度不能少于6位") authBaseController.HandleValidationError(c, "新密码长度不能少于6位")
return return
@@ -82,10 +88,29 @@ func ProfilePasswordUpdateHandler(c *gin.Context) {
return return
} }
// 获取当前密码设置 // 注释由于使用了AdminAuthRequired中间件已确保是管理员用户
settingsService := services.GetSettingsService()
currentHash := settingsService.GetString("admin_password", "") // 获取数据库连接
currentSalt := settingsService.GetString("admin_password_salt", "") db, ok := authBaseController.GetDB(c)
if !ok {
return
}
// 从数据库获取当前管理员信息
var adminUser models.User
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
authBaseController.HandleInternalError(c, "获取管理员信息失败", err)
return
}
currentHash := adminUser.Password
currentSalt := adminUser.PasswordSalt
// 检查必要的设置是否存在
if currentHash == "" || currentSalt == "" {
authBaseController.HandleInternalError(c, "管理员密码设置不完整", nil)
return
}
// 校验旧密码 // 校验旧密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) { if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) {
@@ -93,91 +118,58 @@ func ProfilePasswordUpdateHandler(c *gin.Context) {
return return
} }
// 生成新盐值和哈希 // 生成新的密码盐值
newSalt, err := utils.GenerateRandomSalt() newSalt, err := utils.GenerateRandomSalt()
if err != nil { if err != nil {
authBaseController.HandleInternalError(c, "生成盐失败", err) authBaseController.HandleInternalError(c, "生成密码盐失败", err)
return return
} }
// 生成新密码哈希
newHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt) newHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
if err != nil { if err != nil {
authBaseController.HandleInternalError(c, "生成密码哈希失败", err) authBaseController.HandleInternalError(c, "生成密码哈希失败", err)
return return
} }
// 更新数据库 // 更新数据库
db, ok := authBaseController.GetDB(c) err = db.Transaction(func(tx *gorm.DB) error {
if !ok { // 更新密码和盐值
return return tx.Model(&models.User{}).Where("uuid = ?", claims.UUID).Updates(map[string]interface{}{
} "password": newHash,
"password_salt": newSalt,
}).Error
})
// 更新 admin_password if err != nil {
if err := updateSetting(db, "admin_password", newHash); err != nil {
authBaseController.HandleInternalError(c, "更新密码失败", err) authBaseController.HandleInternalError(c, "更新密码失败", err)
return return
} }
// 更新 admin_password_salt
if err := updateSetting(db, "admin_password_salt", newSalt); err != nil {
authBaseController.HandleInternalError(c, "更新盐值失败", err)
return
}
// 刷新缓存
settingsService.RefreshCache()
// 清除相关缓存键
_ = utils.RedisDel(c.Request.Context(), "setting:admin_password", "setting:admin_password_salt")
// 获取当前用户名
currentUsername := settingsService.GetString("admin_username", "admin")
// 重新签发JWT并写入Cookie
token, err := generateJWTTokenForAdmin(currentUsername, newHash)
if err != nil {
authBaseController.HandleInternalError(c, "生成新令牌失败", err)
return
}
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
// 记录操作日志 // 记录操作日志
operator := c.GetString("admin_username") services.RecordOperationLog("修改密码", claims.Username, claims.UUID, "管理员修改了登录密码")
if operator == "" {
operator = "unknown"
}
operatorUUID := c.GetString("admin_uuid")
services.RecordOperationLog( authBaseController.HandleSuccess(c, "密码修改成功,请重新登录", gin.H{
"修改密码", "redirect": "/admin/login",
operator, })
operatorUUID,
"管理员修改了登录密码",
)
authBaseController.HandleSuccess(c, "密码修改成功", nil)
} }
// ProfileUpdateHandler 修改当前登录管理员的用户名 // ProfileUpdateHandler 修改当前登录管理员的资料(用户名、昵称、头像)
// - 接收 JSON: {username} // - 接收 JSON: {username, nickname, avatar, old_password}
// - 校验用户名非空、长度 // - 校验旧密码正确性
// - 更新数据库后重新签发JWT并写入 Cookie保持前端展示的一致性 // - 更新数据库后重新签发JWT并写入 Cookie保持前端展示的一致性
// - 自动刷新接近过期的JWT令牌
func ProfileUpdateHandler(c *gin.Context) { func ProfileUpdateHandler(c *gin.Context) {
_, _, err := GetCurrentAdminUserWithRefresh(c) claims, _, err := GetCurrentAdminUserWithRefresh(c)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{ authBaseController.HandleValidationError(c, "未登录或会话已过期")
"code": 1,
"msg": "未登录或会话已过期",
"data": nil,
})
return return
} }
var body struct { var body struct {
Username string `json:"username"` Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
OldPassword string `json:"old_password"` OldPassword string `json:"old_password"`
} }
if !authBaseController.BindJSON(c, &body) { if !authBaseController.BindJSON(c, &body) {
@@ -185,6 +177,9 @@ func ProfileUpdateHandler(c *gin.Context) {
} }
username := strings.TrimSpace(body.Username) username := strings.TrimSpace(body.Username)
nickname := strings.TrimSpace(body.Nickname)
avatar := strings.TrimSpace(body.Avatar)
if username == "" { if username == "" {
authBaseController.HandleValidationError(c, "用户名不能为空") authBaseController.HandleValidationError(c, "用户名不能为空")
return return
@@ -193,75 +188,86 @@ func ProfileUpdateHandler(c *gin.Context) {
authBaseController.HandleValidationError(c, "用户名长度不能超过64字符") authBaseController.HandleValidationError(c, "用户名长度不能超过64字符")
return return
} }
if len(nickname) > 64 {
settingsService := services.GetSettingsService() authBaseController.HandleValidationError(c, "昵称长度不能超过64字符")
currentUsername := settingsService.GetString("admin_username", "admin") return
}
// 如果未变化则直接返回成功 if len(avatar) > 255 {
if strings.EqualFold(username, currentUsername) { authBaseController.HandleValidationError(c, "头像URL长度不能超过255字符")
authBaseController.HandleSuccess(c, "保存成功", map[string]interface{}{
"username": username,
})
return return
} }
// 修改用户名需要进行当前密码校验
if strings.TrimSpace(body.OldPassword) == "" {
authBaseController.HandleValidationError(c, "修改用户名需要提供当前密码")
return
}
currentHash := settingsService.GetString("admin_password", "")
currentSalt := settingsService.GetString("admin_password_salt", "")
// 校验旧密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) {
authBaseController.HandleValidationError(c, "当前密码不正确")
return
}
// 更新数据库
db, ok := authBaseController.GetDB(c) db, ok := authBaseController.GetDB(c)
if !ok { if !ok {
return return
} }
if err := updateSetting(db, "admin_username", username); err != nil { // 注释由于使用了AdminAuthRequired中间件已确保是管理员用户
authBaseController.HandleInternalError(c, "更新用户名失败", err)
// 从数据库获取当前管理员信息
var adminUser models.User
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
authBaseController.HandleInternalError(c, "获取管理员信息失败", err)
return return
} }
// 刷新缓存 adminUsername := adminUser.Username
settingsService.RefreshCache() adminNickname := adminUser.Nickname
_ = utils.RedisDel(c.Request.Context(), "setting:admin_username") adminAvatar := adminUser.Avatar
adminPassword := adminUser.Password
adminPasswordSalt := adminUser.PasswordSalt
// 检查必要的设置是否存在
if adminUsername == "" || adminPassword == "" || adminPasswordSalt == "" {
authBaseController.HandleInternalError(c, "管理员设置不完整", nil)
return
}
// 如果用户名、昵称和头像都未变化则直接返回成功(无需校验旧密码)
if strings.EqualFold(username, adminUsername) && nickname == adminNickname && avatar == adminAvatar {
authBaseController.HandleSuccess(c, "保存成功", gin.H{
"username": username,
"nickname": nickname,
"avatar": avatar,
})
return
}
// 如果只修改昵称或头像,不需要验证密码
if !strings.EqualFold(username, adminUsername) {
// 修改用户名需要进行当前密码校验
if strings.TrimSpace(body.OldPassword) == "" {
authBaseController.HandleValidationError(c, "修改账号需要提供当前密码")
return
}
// 使用盐值验证当前密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
authBaseController.HandleValidationError(c, "当前密码不正确")
return
}
}
// 更新管理员资料
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UUID).Updates(map[string]interface{}{
"username": username,
"nickname": nickname,
"avatar": avatar,
}).Error; dbErr != nil {
authBaseController.HandleInternalError(c, "更新管理员资料失败", dbErr)
return
}
// 获取当前管理员并刷新Token这会生成包含新用户名的Token并更新Cookie
_, _, _ = GetCurrentAdminUserWithRefresh(c)
// 记录操作日志 // 记录操作日志
operator := c.GetString("admin_username") services.RecordOperationLog("修改资料", claims.Username, claims.UUID, fmt.Sprintf("管理员修改资料为 用户名: %s, 昵称: %s, 头像: %s", username, nickname, avatar))
if operator == "" {
operator = "unknown"
}
operatorUUID := c.GetString("admin_uuid")
services.RecordOperationLog( authBaseController.HandleSuccess(c, "保存成功", gin.H{
"修改账号",
operator,
operatorUUID,
"管理员修改了用户名为: "+username,
)
// 重新签发JWT并写入Cookie
token, err := generateJWTTokenForAdmin(username, currentHash)
if err != nil {
authBaseController.HandleInternalError(c, "生成新令牌失败", err)
return
}
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
authBaseController.HandleSuccess(c, "用户名修改成功", map[string]interface{}{
"username": username, "username": username,
"nickname": nickname,
"avatar": avatar,
}) })
} }

View File

@@ -13,12 +13,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// SettingsFragmentHandler 设置片段渲染
// - 渲染设置表单通过前端JS调用API加载/保存)
func SettingsFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "settings.html", map[string]interface{}{})
}
// SubAccountSimpleListHandler 子账号简单列表API处理器 (Mock) // SubAccountSimpleListHandler 子账号简单列表API处理器 (Mock)
func SubAccountSimpleListHandler(c *gin.Context) { func SubAccountSimpleListHandler(c *gin.Context) {
// Mock implementation for NetworkAuth which has no subaccounts // Mock implementation for NetworkAuth which has no subaccounts
@@ -67,6 +61,11 @@ func SettingsUpdateHandler(c *gin.Context) {
return return
} }
var categoryStr string
if category, ok := directBody["category"].(string); ok {
categoryStr = category
}
// 提取设置数据 // 提取设置数据
var settingsData map[string]string var settingsData map[string]string
@@ -87,6 +86,9 @@ func SettingsUpdateHandler(c *gin.Context) {
// 直接字段格式 // 直接字段格式
settingsData = make(map[string]string) settingsData = make(map[string]string)
for k, v := range directBody { for k, v := range directBody {
if k == "category" {
continue // 忽略 category 字段,不保存到设置表
}
if str, ok := v.(string); ok { if str, ok := v.(string); ok {
settingsData[k] = str settingsData[k] = str
} else if v != nil { } else if v != nil {
@@ -119,59 +121,6 @@ func SettingsUpdateHandler(c *gin.Context) {
// 批量处理设置项 // 批量处理设置项
for k, v := range settingsData { for k, v := range settingsData {
// 特殊处理 admin_password
if k == "admin_password" {
// 如果密码为空,跳过更新(保留原密码)
if v == "" {
continue
}
// 记录操作日志
// 由于 NetworkAuth 中没有 SystemAdminUser 全局变量,这里暂时使用 "admin"
// operator := "admin"
// 尝试从上下文获取用户名(如果中间件设置了的话)
// if user, exists := c.Get("username"); exists {
// operator = user.(string)
// }
// 生成随机盐值
salt, err := utils.GenerateRandomSalt()
if err != nil {
authBaseController.HandleInternalError(c, "生成盐值失败", err)
return
}
// 使用盐值哈希密码
hash, err := utils.HashPasswordWithSalt(v, salt)
if err != nil {
authBaseController.HandleInternalError(c, "密码哈希失败", err)
return
}
// 更新 salt 设置项(如果不存在则创建)
var saltSetting models.Settings
if err := db.Where("name = ?", "admin_password_salt").First(&saltSetting).Error; err != nil {
saltSetting = models.Settings{Name: "admin_password_salt", Value: salt}
if err := db.Create(&saltSetting).Error; err != nil {
logrus.WithError(err).Error("创建admin_password_salt失败")
authBaseController.HandleInternalError(c, "保存盐值失败", err)
return
}
} else {
if err := db.Model(&saltSetting).Update("value", salt).Error; err != nil {
logrus.WithError(err).Error("更新admin_password_salt失败")
authBaseController.HandleInternalError(c, "更新盐值失败", err)
return
}
}
// 将盐值相关的缓存键加入清理列表
keysToDel = append(keysToDel, "setting:admin_password_salt")
// 将当前处理的值替换为哈希后的密码
v = hash
}
var s models.Settings var s models.Settings
if err := db.Where("name = ?", k).First(&s).Error; err != nil { if err := db.Where("name = ?", k).First(&s).Error; err != nil {
// 不存在则创建 // 不存在则创建
@@ -201,6 +150,23 @@ func SettingsUpdateHandler(c *gin.Context) {
// 刷新内存中的设置缓存,保证后续读取一致 // 刷新内存中的设置缓存,保证后续读取一致
services.GetSettingsService().RefreshCache() services.GetSettingsService().RefreshCache()
// 获取当前操作人信息
claims, _, err := GetCurrentAdminUserWithRefresh(c)
var operator, operatorUUID string
if err == nil && claims != nil {
operator = claims.Username
operatorUUID = claims.UUID
} else {
operator = "system"
}
// 记录操作日志
logType := "系统设置"
if categoryStr != "" {
logType = fmt.Sprintf("系统设置-%s", categoryStr)
}
services.RecordOperationLog(logType, operator, operatorUUID, fmt.Sprintf("管理员更新了系统设置,包含 %d 个配置项", len(settingsData)))
authBaseController.HandleSuccess(c, "保存成功", nil) authBaseController.HandleSuccess(c, "保存成功", nil)
} }
@@ -253,3 +219,24 @@ func SettingsGenerateKeyHandler(c *gin.Context) {
authBaseController.HandleSuccess(c, "生成成功", map[string]string{"key": key}) authBaseController.HandleSuccess(c, "生成成功", map[string]string{"key": key})
} }
// SettingsPublicHandler 公开设置查询API
// - 仅返回允许公开的设置项以及所有前端平台配置
func SettingsPublicHandler(c *gin.Context) {
db, ok := authBaseController.GetDB(c)
if !ok {
return
}
var list []models.Settings
// 查询公开的基本信息、维护模式和所有前端平台配置
if err := db.Where("name IN ? OR name LIKE ?", []string{"site_title", "site_description", "site_keywords", "site_logo", "contact_email", "maintenance_mode"}, "platform_%").Find(&list).Error; err != nil {
authBaseController.HandleInternalError(c, "查询失败", err)
return
}
res := map[string]string{}
for _, s := range list {
res[s.Name] = s.Value
}
authBaseController.HandleSuccess(c, "ok", res)
}

View File

@@ -20,17 +20,6 @@ import (
// 创建基础控制器实例 // 创建基础控制器实例
var variableBaseController = controllers.NewBaseController() var variableBaseController = controllers.NewBaseController()
// ============================================================================
// 页面处理器
// ============================================================================
// VariableFragmentHandler 公共变量列表页面片段处理器
func VariableFragmentHandler(c *gin.Context) {
c.HTML(http.StatusOK, "variables.html", gin.H{
"Title": "公共变量",
})
}
// ============================================================================ // ============================================================================
// API处理器 // API处理器
// ============================================================================ // ============================================================================

View File

@@ -237,8 +237,7 @@ func (bc *BaseController) BindURI(c *gin.Context, obj interface{}) bool {
func (bc *BaseController) GetDefaultTemplateData() gin.H { func (bc *BaseController) GetDefaultTemplateData() gin.H {
settings := services.GetSettingsService() settings := services.GetSettingsService()
return gin.H{ return gin.H{
"Title": settings.GetString("site_title", "NetworkAuth"), "Title": settings.GetString("site_title", "NetworkAuth 系统"),
"SystemName": settings.GetString("site_title", "NetworkAuth"),
"FooterText": settings.GetString("footer_text", "Copyright © 2026 NetworkAuth. All Rights Reserved."), "FooterText": settings.GetString("footer_text", "Copyright © 2026 NetworkAuth. All Rights Reserved."),
"ICPRecord": settings.GetString("icp_record", ""), "ICPRecord": settings.GetString("icp_record", ""),
"ICPRecordLink": settings.GetString("icp_record_link", "https://beian.miit.gov.cn"), "ICPRecordLink": settings.GetString("icp_record_link", "https://beian.miit.gov.cn"),

View File

@@ -3,30 +3,26 @@ package default_ctrl
import ( import (
"NetworkAuth/services" "NetworkAuth/services"
"net/http" "net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// RootHandler 根路径处理器 // RootHandler 根路径处理器
// 使用模板渲染服务器信息页面 // 返回服务器信息 JSON
func RootHandler(c *gin.Context) { func RootHandler(c *gin.Context) {
// 获取设置服务 // 获取设置服务
settings := services.GetSettingsService() settings := services.GetSettingsService()
// 传递模板数据 // 传递数据
data := map[string]interface{}{ data := gin.H{
"Title": settings.GetString("site_title", "NetworkAuth Server"), "title": settings.GetString("site_title", "NetworkAuth Server"),
"Keywords": settings.GetString("site_keywords", ""), "description": settings.GetString("site_description", ""),
"Description": settings.GetString("site_description", ""), "status": "running",
"SystemName": "系统提醒", // 对应 H1 "message": "NetworkAuth API Server is running",
"WarningText": "🚫 未授权,拒绝访问",
"InfoText": "💬 如有问题,请联系网站管理员",
"FooterText": settings.GetString("footer_text", "Copyright © 2026 NetworkAuth. All Rights Reserved."),
"ICPRecord": settings.GetString("icp_record", ""),
"ICPRecordLink": settings.GetString("icp_record_link", "https://beian.miit.gov.cn"),
"CurrentYear": time.Now().Year(),
} }
c.HTML(http.StatusOK, "index.html", data) c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": data,
})
} }

View File

@@ -6,20 +6,15 @@ import (
"NetworkAuth/models" "NetworkAuth/models"
"NetworkAuth/services" "NetworkAuth/services"
"NetworkAuth/utils" "NetworkAuth/utils"
"fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
) )
// InstallPageHandler 渲染安装页面
func InstallPageHandler(c *gin.Context) {
// 由于前端是通过模板渲染的,我们返回一个安装页面
c.HTML(http.StatusOK, "install.html", gin.H{
"title": "NetworkAuth 系统初始化",
})
}
// InstallSubmitHandler 处理安装表单提交 // InstallSubmitHandler 处理安装表单提交
func InstallSubmitHandler(c *gin.Context) { func InstallSubmitHandler(c *gin.Context) {
var req struct { var req struct {
@@ -58,7 +53,24 @@ func InstallSubmitHandler(c *gin.Context) {
return return
} }
// 2. 重新初始化数据库连接并执行迁移 // 2. 使用新配置尝试连接数据库
var testDB *gorm.DB
if req.DbType == "mysql" {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
req.DbUser, req.DbPass, req.DbHost, req.DbPort, req.DbName)
testDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接 MySQL 数据库失败,请检查配置是否正确: " + err.Error()})
return
}
sqlDB, err := testDB.DB()
if err != nil || sqlDB.Ping() != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接 MySQL 数据库失败,无法 Ping 通,请检查配置是否正确"})
return
}
}
// 3. 重新初始化全局数据库连接并执行迁移
db, err := database.ReInit() db, err := database.ReInit()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接数据库失败: " + err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接数据库失败: " + err.Error()})
@@ -66,7 +78,7 @@ func InstallSubmitHandler(c *gin.Context) {
} }
if db == nil { if db == nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "获取数据库实例失败"}) c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "获取数据库实例失败,请检查数据库配置是否正确"})
return return
} }
@@ -91,17 +103,55 @@ func InstallSubmitHandler(c *gin.Context) {
return return
} }
// 4. 更新设置表
settingsToUpdate := map[string]string{
"site_title": req.SiteTitle,
"admin_username": strings.TrimSpace(req.AdminUsername),
"admin_password": adminPasswordHash,
"admin_password_salt": adminSalt,
"is_installed": "1", // 标记为已安装
}
// 开启事务进行更新 // 开启事务进行更新
tx := db.Begin() tx := db.Begin()
// 更新或创建超级管理员账号
var adminUser models.User
if err := tx.Where("uuid = ?", "00000000-0000-0000-0000-000000000000").First(&adminUser).Error; err != nil {
// 如果不存在则创建
adminUser = models.User{
UUID: "00000000-0000-0000-0000-000000000000",
Username: strings.TrimSpace(req.AdminUsername),
Password: adminPasswordHash,
PasswordSalt: adminSalt,
Nickname: "管理员",
Avatar: "",
Role: 0,
Status: 1,
Remark: "系统默认超级管理员",
}
// 使用 Select("Role") 确保 Role 字段值为0时是零值被显式插入避免使用数据库默认值 1
if err := tx.Select("UUID", "Username", "Password", "PasswordSalt", "Nickname", "Avatar", "Role", "Status", "Remark").Create(&adminUser).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "创建管理员账号失败"})
return
}
} else {
// 存在则更新
adminUser.Username = strings.TrimSpace(req.AdminUsername)
adminUser.Password = adminPasswordHash
adminUser.PasswordSalt = adminSalt
adminUser.Nickname = "管理员"
adminUser.Role = 0
if err := tx.Save(&adminUser).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "更新管理员账号失败"})
return
}
// 确保角色被更新为0GORM的Save可能忽略零值所以额外Update一次
tx.Model(&adminUser).Update("Role", 0)
}
// 如果是新创建的,再额外确保一次 Role 为 0避免 default 标签导致的零值问题
tx.Model(&adminUser).Update("Role", 0)
// 4. 更新设置表
settingsToUpdate := map[string]string{
"site_title": req.SiteTitle,
"is_installed": "1", // 标记为已安装
}
for name, value := range settingsToUpdate { for name, value := range settingsToUpdate {
// 先尝试更新,如果没有该记录,则忽略(因为 AutoMigrate 已经创建了默认记录) // 先尝试更新,如果没有该记录,则忽略(因为 AutoMigrate 已经创建了默认记录)
if err := tx.Model(&models.Settings{}).Where("name = ?", name).Update("value", value).Error; err != nil { if err := tx.Model(&models.Settings{}).Where("name = ?", name).Update("value", value).Error; err != nil {
@@ -113,10 +163,7 @@ func InstallSubmitHandler(c *gin.Context) {
tx.Commit() tx.Commit()
// 5. 更新内存缓存 // 5. 更新内存缓存
settingsService := services.GetSettingsService() services.ResetSettingsService()
for name, value := range settingsToUpdate {
settingsService.Set(name, value)
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "安装成功"}) c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "安装成功"})
} }

View File

@@ -1,6 +1,7 @@
package database package database
import ( import (
appconfig "NetworkAuth/config"
"NetworkAuth/utils" "NetworkAuth/utils"
"context" "context"
"fmt" "fmt"
@@ -40,7 +41,7 @@ var (
func Init() (*gorm.DB, error) { func Init() (*gorm.DB, error) {
var initErr error var initErr error
once.Do(func() { once.Do(func() {
initErr = performInit() initErr = performInitFromViper()
}) })
return dbInstance, initErr return dbInstance, initErr
} }
@@ -57,83 +58,101 @@ func GetDB() (*gorm.DB, error) {
// ReInit 重新初始化数据库连接 // ReInit 重新初始化数据库连接
// 用于在修改配置后重新连接数据库 // 用于在修改配置后重新连接数据库
func ReInit() (*gorm.DB, error) { func ReInit() (*gorm.DB, error) {
// 如果已有连接,尝试关闭它 closeCurrentDB()
if dbInstance != nil {
if healthCheckCancel != nil {
healthCheckCancel()
healthCheckCancel = nil
}
if sqlDB, err := dbInstance.DB(); err == nil {
sqlDB.Close()
}
}
dbInstance = nil
// 重新执行初始化逻辑(不经过 once.Do // 在 ReInit 时,强制从 viper 重新读取配置并连接,忽略"系统尚未安装"的检查
return dbInstance, performInit() // 因为这是安装过程触发的
var cfg appconfig.AppConfig
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
if err := performInitWithConfig(&cfg); err != nil {
return nil, err
}
if dbInstance == nil {
return nil, fmt.Errorf("数据库实例初始化后为空")
}
return dbInstance, nil
} }
func performInit() error { func InitWithAppConfig(cfg *appconfig.AppConfig) (*gorm.DB, error) {
// 检查是否已经有配置文件(通过检查文件是否存在) closeCurrentDB()
if err := performInitWithConfig(cfg); err != nil {
return nil, err
}
return dbInstance, nil
}
func performInitFromViper() error {
configFile := viper.ConfigFileUsed() configFile := viper.ConfigFileUsed()
// 如果 viper 没有使用配置文件(可能是因为没找到文件而使用了默认配置),
// 或者配置文件路径为空,我们应该假设处于未安装状态。
// 但 viper.ConfigFileUsed() 在 ReadInConfig 成功后会返回文件名。
// 如果 ReadInConfig 失败因为文件不存在viper 可能会返回空或者我们在 config.go 中设置的路径。
// 在 config.go 中,如果文件不存在,我们加载了默认配置但没有写文件。
// 此时 viper.ConfigFileUsed() 可能是空的或者我们设置的路径。
// 让我们检查该路径对应的文件是否存在。
if configFile == "" { if configFile == "" {
configFile = "config.json" configFile = "config.json"
} }
_, err := os.Stat(configFile) // 从 viper 中读取配置
isConfigExists := !os.IsNotExist(err) var cfg appconfig.AppConfig
if err := viper.Unmarshal(&cfg); err != nil {
// 如果配置文件不存在,说明还没有经过安装初始化,暂时不连接数据库 return err
if !isConfigExists {
logrus.Info("尚未初始化配置,跳过数据库连接")
return nil
} }
var initErr error // 检查数据库类型,如果文件或配置不存在,说明系统尚未安装,跳过数据库连接
dbType := viper.GetString("database.type") switch cfg.Database.Type {
switch dbType { case "sqlite":
dbPath := cfg.Database.SQLite.Path
if dbPath == "" {
dbPath = "./database.db"
}
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
logrus.Info("SQLite 数据库文件不存在,系统尚未安装,跳过数据库连接")
return nil
}
case "mysql": case "mysql":
initErr = initMySQL() // 只有在明确配置了 host 并且不是安装请求时才去连接 MySQL
// 我们通过检查是否已有有效配置来判断,比如检查 database 是否为空
if cfg.Database.MySQL.Database == "" {
logrus.Info("MySQL 数据库名称未配置,说明系统尚未安装,跳过数据库连接")
return nil
}
}
return performInitWithConfig(&cfg)
}
func performInitWithConfig(cfg *appconfig.AppConfig) error {
if cfg == nil {
return fmt.Errorf("应用配置不能为空")
}
if err := appconfig.ValidateConfigValue(cfg); err != nil {
return err
}
var initErr error
switch cfg.Database.Type {
case "mysql":
initErr = initMySQL(&cfg.Database.MySQL, cfg.Log.Level)
if initErr != nil {
logrus.WithError(initErr).Error("MySQL 数据库连接失败,请检查配置或重新安装")
// 既然 MySQL 连不上,说明系统无法正常工作,直接返回错误,由外层决定是否退出
return initErr
}
default: default:
initErr = initSQLite() initErr = initSQLite(&cfg.Database.SQLite, cfg.Log.Level)
} }
if initErr != nil || dbInstance == nil {
// 如果数据库初始化成功,配置连接池和启动健康检查 return initErr
if initErr == nil && dbInstance != nil {
// 加载数据库配置
var configPrefix string
if dbType == "mysql" {
configPrefix = "database.mysql"
} else {
configPrefix = "database.sqlite"
}
dbConfig := utils.LoadDatabaseConfig(configPrefix)
// 验证配置
if err := utils.ValidateDatabaseConfig(dbConfig); err != nil {
logrus.WithError(err).Warn("数据库配置验证失败,使用默认配置")
dbConfig = utils.GetDefaultDatabaseConfig()
}
// 配置连接池
if err := utils.ConfigureConnectionPool(dbInstance, dbConfig); err != nil {
logrus.WithError(err).Error("配置数据库连接池失败")
}
// 启动健康检查
healthCheckCancel = utils.StartHealthCheck(dbInstance, dbConfig)
} }
return initErr dbConfig := buildPoolConfig(cfg)
if err := utils.ValidateDatabaseConfig(dbConfig); err != nil {
logrus.WithError(err).Warn("数据库配置验证失败,使用默认配置")
dbConfig = utils.GetDefaultDatabaseConfig()
}
if err := utils.ConfigureConnectionPool(dbInstance, dbConfig); err != nil {
logrus.WithError(err).Error("配置数据库连接池失败")
}
healthCheckCancel = utils.StartHealthCheck(dbInstance, dbConfig)
return nil
} }
// SetDB 设置全局 *gorm.DB 实例(用于测试) // SetDB 设置全局 *gorm.DB 实例(用于测试)
@@ -145,16 +164,38 @@ func SetDB(db *gorm.DB) {
// 私有函数 // 私有函数
// ============================================================================ // ============================================================================
// initSQLite 初始化 SQLite 数据库 func closeCurrentDB() {
// 使用 viper 中的 database.sqlite.path 作为数据库文件路径 if healthCheckCancel != nil {
func initSQLite() error { healthCheckCancel()
path := viper.GetString("database.sqlite.path") healthCheckCancel = nil
if path == "" {
path = "./recharge.db"
} }
dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path) if dbInstance != nil {
if sqlDB, err := dbInstance.DB(); err == nil {
sqlDB.Close()
}
}
dbInstance = nil
}
func buildPoolConfig(cfg *appconfig.AppConfig) *utils.DatabaseConfig {
dbConfig := utils.GetDefaultDatabaseConfig()
if cfg.Database.Type == "mysql" {
if cfg.Database.MySQL.MaxIdleConns > 0 {
dbConfig.MaxIdleConns = cfg.Database.MySQL.MaxIdleConns
}
if cfg.Database.MySQL.MaxOpenConns > 0 {
dbConfig.MaxOpenConns = cfg.Database.MySQL.MaxOpenConns
}
return dbConfig
}
dbConfig.MaxIdleConns = 1
dbConfig.MaxOpenConns = 1
return dbConfig
}
func buildGormLogger(level string) gLogger.Interface {
var logLevel gLogger.LogLevel var logLevel gLogger.LogLevel
switch viper.GetString("logger.level") { switch level {
case "debug": case "debug":
logLevel = gLogger.Info logLevel = gLogger.Info
case "error": case "error":
@@ -162,55 +203,45 @@ func initSQLite() error {
default: default:
logLevel = gLogger.Warn logLevel = gLogger.Warn
} }
gl := gLogger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), gLogger.Config{SlowThreshold: 2 * time.Second, LogLevel: logLevel, IgnoreRecordNotFoundError: true, Colorful: false}) return gLogger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), gLogger.Config{SlowThreshold: 2 * time.Second, LogLevel: logLevel, IgnoreRecordNotFoundError: true, Colorful: false})
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: gl}) }
func initSQLite(sqliteConfig *appconfig.SQLiteConfig, logLevel string) error {
path := sqliteConfig.Path
if path == "" {
path = "./database.db"
}
dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path)
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)})
if err != nil { if err != nil {
logrus.WithError(err).Error("SQLite 初始化失败") logrus.WithError(err).Error("SQLite 初始化失败")
return err return err
} }
// SQLite 连接池配置SQLite 对连接池支持有限,但仍可设置基本参数)
if sqlDB, err := db.DB(); err == nil { if sqlDB, err := db.DB(); err == nil {
// SQLite 通常使用单连接,但可以设置一些基本参数 sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxOpenConns(1) // SQLite 建议使用单连接
sqlDB.SetMaxIdleConns(1) sqlDB.SetMaxIdleConns(1)
} }
dbInstance = db dbInstance = db
logrus.WithField("path", path).Info("SQLite 连接已建立") logrus.WithField("path", path).Info("SQLite 连接已建立")
return nil return nil
} }
// initMySQL 初始化 MySQL 数据库 func initMySQL(mysqlConfig *appconfig.MySQLConfig, logLevel string) error {
// 从 viper 读取 database.mysql.* 配置构建 DSN host := mysqlConfig.Host
func initMySQL() error { port := mysqlConfig.Port
host := viper.GetString("database.mysql.host") user := mysqlConfig.Username
port := viper.GetInt("database.mysql.port") pass := mysqlConfig.Password
user := viper.GetString("database.mysql.username") dbname := mysqlConfig.Database
pass := viper.GetString("database.mysql.password") charset := mysqlConfig.Charset
dbname := viper.GetString("database.mysql.database")
charset := viper.GetString("database.mysql.charset")
if charset == "" { if charset == "" {
charset = "utf8mb4" charset = "utf8mb4"
} }
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", user, pass, host, port, dbname, charset) dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", user, pass, host, port, dbname, charset)
var logLevel gLogger.LogLevel db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)})
switch viper.GetString("logger.level") {
case "debug":
logLevel = gLogger.Info
case "error":
logLevel = gLogger.Error
default:
logLevel = gLogger.Warn
}
gl := gLogger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), gLogger.Config{SlowThreshold: 2 * time.Second, LogLevel: logLevel, IgnoreRecordNotFoundError: true, Colorful: false})
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: gl})
if err != nil { if err != nil {
logrus.WithError(err).Error("MySQL 初始化失败") logrus.WithError(err).Error("MySQL 初始化失败")
return err return err
} }
dbInstance = db dbInstance = db
logrus.WithField("host", host).WithField("database", dbname).Info("MySQL 连接已建立") logrus.WithField("host", host).WithField("database", dbname).Info("MySQL 连接已建立")
return nil return nil

View File

@@ -18,11 +18,11 @@ func AutoMigrate() error {
&models.Settings{}, &models.Settings{},
&models.OperationLog{}, &models.OperationLog{},
&models.LoginLog{}, &models.LoginLog{},
&models.User{},
&models.App{}, &models.App{},
&models.API{}, &models.API{},
&models.Function{},
&models.Variable{}, &models.Variable{},
&models.User{}, &models.Function{},
); err != nil { ); err != nil {
logrus.WithError(err).Error("AutoMigrate 执行失败") logrus.WithError(err).Error("AutoMigrate 执行失败")
return err return err

View File

@@ -1,232 +1,329 @@
package database package database
import ( import (
"NetworkAuth/config" "NetworkAuth/config"
"NetworkAuth/models" "NetworkAuth/models"
"NetworkAuth/utils"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus" )
)
// ============================================================================
// ============================================================================ // 公共函数
// 公共函数 // ============================================================================
// ============================================================================
// SeedDefaultSettings 初始化默认系统设置
// SeedDefaultSettings 初始化默认系统设置 // - 检查各项设置是否已存在,如不存在则创建默认值
// - 检查各项设置是否已存在,如不存在则创建默认值 // - 包含站点基本信息、SEO设置等常用配置项
// - 包含站点基本信息、SEO设置等常用配置项 func SeedDefaultSettings() error {
func SeedDefaultSettings() error { db, err := GetDB()
db, err := GetDB() if err != nil {
if err != nil { return err
return err }
}
// 生成安全的随机密钥
// 生成安全的随机密钥 jwtSecret, err := config.GenerateSecureJWTSecret()
jwtSecret, err := config.GenerateSecureJWTSecret() if err != nil {
if err != nil { return err
return err }
} encryptionKey, err := config.GenerateSecureEncryptionKey()
encryptionKey, err := config.GenerateSecureEncryptionKey() if err != nil {
if err != nil { return err
return err }
}
isInstalledDefault := "0"
// 生成默认管理员密码admin123的盐值和哈希
// 这样可以确保admin_password和admin_password_salt在初始化时就有值 // 定义默认设置项
adminSalt, err := utils.GenerateRandomSalt() var defaultSettings []models.Settings
if err != nil {
return err // ===== 系统安装状态 =====
} defaultSettings = append(defaultSettings, []models.Settings{
adminPasswordHash, err := utils.HashPasswordWithSalt("admin123", adminSalt) {
if err != nil { Name: "is_installed",
return err Value: isInstalledDefault,
} Description: "系统是否已初始化安装0=未安装1=已安装",
},
// 检查是否已有 admin_password如果有说明是旧版本升级应该把 is_installed 默认设为 1 }...)
var adminPwdCount int64
db.Model(&models.Settings{}).Where("name = ?", "admin_password").Count(&adminPwdCount) // ===== 系统和安全相关默认项 =====
isInstalledDefault := "0" defaultSettings = append(defaultSettings, []models.Settings{
if adminPwdCount > 0 { {
isInstalledDefault = "1" Name: "maintenance_mode",
} Value: "0",
Description: "维护模式0=关闭维护模式1=开启维护模式",
// 定义默认设置项 },
defaultSettings := []models.Settings{ {
// ===== 系统安装状态 ===== Name: "encryption_key",
{ Value: encryptionKey,
Name: "is_installed", Description: "数据加密密钥",
Value: isInstalledDefault, },
Description: "系统是否已初始化安装0=未安装1=已安装", {
}, Name: "jwt_secret",
// ===== 管理员账号相关默认项 ===== Value: jwtSecret,
{ Description: "JWT签名密钥",
Name: "admin_username", },
Value: "admin", {
Description: "管理员用户名", Name: "jwt_refresh",
}, Value: "6",
{ Description: "JWT令牌刷新阈值小时",
Name: "admin_password", },
Value: adminPasswordHash, {
Description: "管理员密码哈希值", Name: "jwt_expire",
}, Value: "24",
{ Description: "JWT令牌有效期小时",
Name: "admin_password_salt", },
Value: adminSalt, {
Description: "管理员密码加密盐值", Name: "session_timeout",
}, Value: "3600",
// ===== 系统和安全相关默认项 ===== Description: "会话超时时间默认1小时",
{ },
Name: "maintenance_mode", {
Value: "0", Name: "max_upload_size",
Description: "维护模式0=关闭维护模式1=开启维护模式", Value: "10",
}, Description: "文件上传最大尺寸",
{ },
Name: "encryption_key", {
Value: encryptionKey, Name: "max_upload_size_unit",
Description: "数据加密密钥", Value: "MB",
}, Description: "文件上传大小单位B/KB/MB/GB",
{ },
Name: "jwt_secret", }...)
Value: jwtSecret,
Description: "JWT签名密钥", // ===== 日志清理策略默认项 =====
}, defaultSettings = append(defaultSettings, []models.Settings{
{ {
Name: "jwt_refresh", Name: "login_log_cleanup_days",
Value: "6", Value: "30",
Description: "JWT令牌刷新阈值小时", Description: "登录日志保留天数0表示不按天清理",
}, },
{ {
Name: "jwt_expire", Name: "login_log_cleanup_limit",
Value: "24", Value: "10000",
Description: "JWT令牌有效期小时", Description: "登录日志保留条数0表示不按数量清理",
}, },
{ {
Name: "session_timeout", Name: "operation_log_cleanup_days",
Value: "3600", Value: "30",
Description: "会话超时时间默认1小时", Description: "操作日志保留天数0表示不按天清理",
}, },
{ {
Name: "max_upload_size", Name: "operation_log_cleanup_limit",
Value: "10485760", Value: "10000",
Description: "文件上传最大尺寸字节默认10MB", Description: "操作日志保留条数0表示不按数量清理",
}, },
{ }...)
Name: "default_user_role",
Value: "1", // ===== Cookie相关默认项 =====
Description: "新用户默认角色0=管理员1=普通用户", defaultSettings = append(defaultSettings, []models.Settings{
}, {
// ===== 日志清理策略默认项 ===== Name: "cookie_secure",
{ Value: "true",
Name: "login_log_cleanup_days", Description: "Cookie Secure属性是否只在HTTPS下发送",
Value: "30", },
Description: "登录日志保留天数0表示不按天清理", {
}, Name: "cookie_same_site",
{ Value: "Lax",
Name: "login_log_cleanup_limit", Description: "Cookie SameSite属性Strict/Lax/None",
Value: "10000", },
Description: "登录日志保留条数0表示不按数量清理", {
}, Name: "cookie_domain",
{ Value: "",
Name: "operation_log_cleanup_days", Description: "Cookie域名",
Value: "30", },
Description: "操作日志保留天数0表示不按天清理", {
}, Name: "cookie_max_age",
{ Value: "86400",
Name: "operation_log_cleanup_limit", Description: "Cookie最大存活时间",
Value: "10000", },
Description: "操作日志保留条数0表示不按数量清理", }...)
},
// ===== Cookie相关默认项 ===== // ===== 站点基本信息默认项 =====
{ defaultSettings = append(defaultSettings, []models.Settings{
Name: "cookie_secure", {
Value: "true", Name: "site_title",
Description: "Cookie Secure属性是否只在HTTPS下发送", Value: "NetworkAuth",
}, Description: "网站标题,显示在浏览器标题栏和页面顶部",
{ },
Name: "cookie_same_site", {
Value: "Lax", Name: "site_keywords",
Description: "Cookie SameSite属性Strict/Lax/None", Value: "NetworkAuth,网络授权服务,GoLang,Web服务",
}, Description: "网站关键词用于SEO优化多个关键词用逗号分隔",
{ },
Name: "cookie_domain", {
Value: "", Name: "site_description",
Description: "Cookie域名", Value: "网络授权服务 (NetworkAuth) 是一个专注于应用鉴权、接口管理和动态逻辑分发的后端系统",
}, Description: "网站描述用于SEO优化和社交媒体分享",
{ },
Name: "cookie_max_age", {
Value: "86400", Name: "site_logo",
Description: "Cookie最大存活时间", Value: "/logo.svg",
}, Description: "网站Logo图片路径",
// ===== 站点基本信息默认项 ===== },
{ {
Name: "site_title", Name: "contact_email",
Value: "NetworkAuth", Value: "admin@example.com",
Description: "网站标题,显示在浏览器标题栏和页面顶部", Description: "联系邮箱,用于客服和业务咨询",
}, },
{ }...)
Name: "site_keywords",
Value: "NetworkAuth,鉴权,API管理,GoLang", // ===== 页脚与备案相关默认项 =====
Description: "网站关键词用于SEO优化多个关键词用逗号分隔", defaultSettings = append(defaultSettings, []models.Settings{
}, {
{ Name: "footer_text",
Name: "site_description", Value: "Copyright © 2026 NetworkAuth. All Rights Reserved.",
Value: "NetworkAuth 网络授权服务,专注于应用鉴权与接口管理", Description: "页脚展示的版权或说明信息",
Description: "网站描述用于SEO优化和社交媒体分享", },
}, {
{ Name: "icp_record",
Name: "site_logo", Value: "",
Value: "/static/logo.png", Description: "ICP备案号留空则不显示",
Description: "网站Logo图片路径", },
}, {
{ Name: "icp_record_link",
Name: "contact_email", Value: "https://beian.miit.gov.cn",
Value: "admin@example.com", Description: "工信部ICP备案查询链接留空则不显示",
Description: "联系邮箱,用于客服和业务咨询", },
}, {
// ===== 页脚与备案相关默认项 ===== Name: "psb_record",
{ Value: "",
Name: "footer_text", Description: "公安备案号,留空则不显示",
Value: "Copyright © 2026 NetworkAuth. All Rights Reserved.", },
Description: "页脚展示的版权或说明信息", {
}, Name: "psb_record_link",
{ Value: "",
Name: "icp_record", Description: "公安备案查询链接,留空则不显示",
Value: "", },
Description: "ICP备案号留空则不显示", }...)
},
{ // ===== 前端平台配置相关默认项 =====
Name: "icp_record_link", defaultSettings = append(defaultSettings, []models.Settings{
Value: "https://beian.miit.gov.cn", {
Description: "工信部ICP备案查询链接留空则不显示", Name: "platform_fixed_header",
}, Value: "1",
{ Description: "是否固定页头 (0 = 关闭1 = 开启)",
Name: "psb_record", },
Value: "", {
Description: "公安备案号,留空则不显示", Name: "platform_hidden_side_bar",
}, Value: "0",
{ Description: "是否隐藏侧边栏 (0 = 关闭1 = 开启)",
Name: "psb_record_link", },
Value: "", {
Description: "公安备案查询链接,留空则不显示", Name: "platform_multi_tags_cache",
}, Value: "0",
} Description: "是否开启多标签页缓存 (0 = 关闭1 = 开启)",
},
// 逐个检查并创建不存在的设置项 {
for _, setting := range defaultSettings { Name: "platform_keep_alive",
var count int64 Value: "1",
if err := db.Model(&models.Settings{}).Where("name = ?", setting.Name).Count(&count).Error; err != nil { Description: "是否开启组件缓存 (0 = 关闭1 = 开启)",
return err },
} {
Name: "platform_layout",
if count == 0 { Value: "vertical",
if err := db.Create(&setting).Error; err != nil { Description: "布局模式 (vertical/horizontal/mix/comprehensive)",
logrus.WithError(err).WithField("name", setting.Name).Error("创建默认设置失败") },
return err {
} Name: "platform_theme",
logrus.WithField("name", setting.Name).WithField("value", setting.Value).Debug("创建默认设置项") Value: "light",
} Description: "主题配色 (light/dark)",
} },
{
logrus.Info("默认系统设置初始化完成") Name: "platform_dark_mode",
return nil Value: "0",
} Description: "是否开启暗黑模式 (0 = 关闭1 = 开启)",
},
{
Name: "platform_overall_style",
Value: "light",
Description: "整体风格",
},
{
Name: "platform_grey",
Value: "0",
Description: "是否开启灰色模式 (0 = 关闭1 = 开启)",
},
{
Name: "platform_weak",
Value: "0",
Description: "是否开启色弱模式 (0 = 关闭1 = 开启)",
},
{
Name: "platform_hide_tabs",
Value: "0",
Description: "是否隐藏标签页 (0 = 关闭1 = 开启)",
},
{
Name: "platform_hide_footer",
Value: "0",
Description: "是否隐藏页脚 (0 = 关闭1 = 开启)",
},
{
Name: "platform_stretch",
Value: "0",
Description: "是否开启页面宽度拉伸 (0 = 关闭1 = 开启)",
},
{
Name: "platform_sidebar_status",
Value: "1",
Description: "侧边栏状态 (0 = 折叠1 = 展开)",
},
{
Name: "platform_ep_theme_color",
Value: "#409EFF",
Description: "Element Plus 主题色",
},
{
Name: "platform_show_logo",
Value: "1",
Description: "是否显示Logo (0 = 关闭1 = 开启)",
},
{
Name: "platform_show_model",
Value: "smart",
Description: "显示模式 (smart等)",
},
{
Name: "platform_menu_arrow_icon_no_transition",
Value: "0",
Description: "菜单箭头图标是否取消过渡动画 (0 = 关闭1 = 开启)",
},
{
Name: "platform_caching_async_routes",
Value: "0",
Description: "是否缓存异步路由 (0 = 关闭1 = 开启)",
},
{
Name: "platform_tooltip_effect",
Value: "light",
Description: "提示框效果 (light/dark)",
},
{
Name: "platform_responsive_storage_name_space",
Value: "responsive-",
Description: "响应式存储命名空间",
},
{
Name: "platform_menu_search_history",
Value: "6",
Description: "菜单搜索历史最大记录数",
},
}...)
// 逐个检查并创建不存在的设置项
for _, setting := range defaultSettings {
var count int64
if err := db.Model(&models.Settings{}).Where("name = ?", setting.Name).Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := db.Create(&setting).Error; err != nil {
logrus.WithError(err).WithField("name", setting.Name).Error("创建系统设置失败")
return err
}
logrus.WithField("name", setting.Name).WithField("value", setting.Value).Debug("创建系统设置项")
}
}
logrus.Info("系统设置初始化完成")
return nil
}

21
go.mod
View File

@@ -3,6 +3,7 @@ module NetworkAuth
go 1.25.0 go 1.25.0
require ( require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
@@ -13,7 +14,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1 github.com/spf13/viper v1.20.1
github.com/xuri/excelize/v2 v2.10.1 github.com/xuri/excelize/v2 v2.10.1
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.49.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.30.1 gorm.io/gorm v1.30.1
@@ -21,7 +22,7 @@ require (
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -29,7 +30,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
@@ -37,7 +38,7 @@ require (
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -49,7 +50,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -69,12 +70,12 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.25.0 // indirect
golang.org/x/image v0.25.0 // indirect golang.org/x/image v0.25.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.35.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect

42
go.sum
View File

@@ -4,8 +4,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
@@ -26,8 +26,10 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
@@ -48,8 +50,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@@ -89,8 +91,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -162,15 +164,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
@@ -187,8 +189,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -208,8 +210,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -227,8 +229,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -236,8 +238,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

21
middleware/cors.go Normal file
View File

@@ -0,0 +1,21 @@
package middleware
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// CorsMiddleware 处理跨域请求
// 允许 Vue 等前端分离架构在开发和生产环境下访问后端 API
func CorsMiddleware() gin.HandlerFunc {
return cors.New(cors.Config{
AllowOriginFunc: func(origin string) bool { return true }, // 允许所有来源
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization", "X-CSRF-Token", "Accept"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
})
}

View File

@@ -1,8 +1,6 @@
package middleware package middleware
import ( import (
"NetworkAuth/web"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -13,8 +11,6 @@ import (
// DevModeConfig 开发模式配置 // DevModeConfig 开发模式配置
type DevModeConfig struct { type DevModeConfig struct {
// 是否启用模板热重载
EnableTemplateReload bool
// 是否跳过验证码验证 // 是否跳过验证码验证
SkipCaptcha bool SkipCaptcha bool
// 是否显示详细错误信息 // 是否显示详细错误信息
@@ -29,7 +25,7 @@ type DevModeConfig struct {
// DevModeMiddleware 开发模式中间件 // DevModeMiddleware 开发模式中间件
// 统一管理所有开发模式相关的功能 // 统一管理所有开发模式相关的功能
func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc { func DevModeMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// 检查是否为开发模式 // 检查是否为开发模式
if IsDevMode() { if IsDevMode() {
@@ -37,12 +33,6 @@ func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc {
c.Set("dev_mode", true) c.Set("dev_mode", true)
c.Set("dev_config", GetDevModeConfig()) c.Set("dev_config", GetDevModeConfig())
// 如果启用了模板热重载,则重新加载模板
config := GetDevModeConfig()
if config.EnableTemplateReload {
reloadTemplates(engine)
}
// 设置开发模式相关的响应头 // 设置开发模式相关的响应头
c.Header("X-Dev-Mode", "true") c.Header("X-Dev-Mode", "true")
} else { } else {
@@ -69,10 +59,9 @@ func GetDevModeConfig() DevModeConfig {
} }
return DevModeConfig{ return DevModeConfig{
EnableTemplateReload: true, // 开发模式下默认启用模板热重载 SkipCaptcha: true, // 开发模式下默认跳过验证码
SkipCaptcha: true, // 开发模式下默认跳过验证码 ShowDetailedErrors: true, // 开发模式下显示详细错误
ShowDetailedErrors: true, // 开发模式下显示详细错误 EnableDebugLog: true, // 开发模式下启用调试日志
EnableDebugLog: true, // 开发模式下启用调试日志
} }
} }
@@ -107,10 +96,3 @@ func ShouldSkipCaptcha(c *gin.Context) bool {
// ============================================================================ // ============================================================================
// 私有函数 // 私有函数
// ============================================================================ // ============================================================================
// reloadTemplates 重新加载模板(内部函数)
func reloadTemplates(engine *gin.Engine) {
if tmpl, err := web.ParseTemplates(); err == nil {
engine.SetHTMLTemplate(tmpl)
}
}

View File

@@ -3,9 +3,10 @@ package middleware
import ( import (
"NetworkAuth/services" "NetworkAuth/services"
"net/http" "net/http"
"strings" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/viper"
) )
// InstallCheckMiddleware 检查系统是否已安装 // InstallCheckMiddleware 检查系统是否已安装
@@ -13,38 +14,52 @@ func InstallCheckMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
path := c.Request.URL.Path path := c.Request.URL.Path
// 放行静态资源和favicon isInstallRoute := path == "/api/install" || path == "/api/install/"
if strings.HasPrefix(path, "/static/") || strings.HasPrefix(path, "/assets/") || path == "/favicon.ico" {
c.Next()
return
}
// 检查是否为安装相关的路由
isInstallRoute := path == "/install" || path == "/api/install"
// 获取系统的安装状态 // 获取系统的安装状态
// 在没有数据库的时候GetSettingsService().GetString 会返回默认值 "0" isInstalledStr := services.GetSettingsService().GetString("is_installed", "0")
isInstalled := services.GetSettingsService().GetString("is_installed", "0") == "1" isInstalled := isInstalledStr == "1"
// 如果未安装且当前不是访问安装页面,则重定向到安装页面 // 如果设置服务没获取到(因为未连接数据库),再结合文件判断
if !isInstalled && !isInstallRoute { if !isInstalled {
// 对于 API 请求,返回 JSON 提示 // 检查数据库文件是否存在(如果是 sqlite
if strings.HasPrefix(path, "/api/") || strings.Contains(path, "/api/") { dbType := viper.GetString("database.type")
c.JSON(http.StatusForbidden, gin.H{ switch dbType {
"code": 403, case "sqlite":
"msg": "系统未初始化,请先完成安装", dbPath := viper.GetString("database.sqlite.path")
}) if dbPath == "" {
c.Abort() dbPath = "./database.db"
return }
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
isInstalled = false
} else {
isInstalled = true
}
case "mysql":
// 如果是 mysql 且配置了 database我们认为是已安装
dbName := viper.GetString("database.mysql.database")
if dbName != "" {
isInstalled = true
}
} }
c.Redirect(http.StatusTemporaryRedirect, "/install") }
// 如果未安装且不是访问安装接口,则返回 403 JSON
if !isInstalled && !isInstallRoute {
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"msg": "系统未初始化,请先完成安装",
})
c.Abort() c.Abort()
return return
} }
// 如果已安装但尝试访问安装页面,则重定向到首页或后台 // 如果已安装但尝试访问安装接口,则返回 403 JSON
if isInstalled && isInstallRoute { if isInstalled && isInstallRoute {
c.Redirect(http.StatusTemporaryRedirect, "/admin") c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"msg": "系统已安装,请勿重复初始化",
})
c.Abort() c.Abort()
return return
} }

View File

@@ -36,7 +36,7 @@ func NewLoggingMiddleware(logger *logger.Logger) *LoggingMiddleware {
// ============================================================================ // ============================================================================
// Handler 返回Gin中间件函数用于记录HTTP请求日志 // Handler 返回Gin中间件函数用于记录HTTP请求日志
// 记录格式参考了更灵活的 NetworkAuth 实现,支持配置开关和日志级别检查 // 记录格式参考了更灵活的NetworkAuth实现支持配置开关和日志级别检查
func (lm *LoggingMiddleware) Handler() gin.HandlerFunc { func (lm *LoggingMiddleware) Handler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// 检查是否启用了访问日志 // 检查是否启用了访问日志

View File

@@ -19,81 +19,21 @@ func MaintenanceMiddleware() gin.HandlerFunc {
return return
} }
// 白名单检查(路径前缀匹配)
path := c.Request.URL.Path path := c.Request.URL.Path
// 1. 允许静态资源 // 允许管理员后台相关接口(以便管理员登录关闭维护模式)
if strings.HasPrefix(path, "/static/") || strings.HasPrefix(path, "/assets/") || path == "/favicon.ico" {
c.Next()
return
}
// 2. 允许管理员后台相关接口(以便管理员登录关闭维护模式)
// 包括登录页、登录接口、API接口、CSRF Token等 // 包括登录页、登录接口、API接口、CSRF Token等
if strings.HasPrefix(path, "/admin") { if strings.HasPrefix(path, "/api/admin") {
c.Next() c.Next()
return return
} }
// 3. 检查请求类型 // 返回 503 JSON
// AJAX/JSON 请求返回 503 JSON c.JSON(http.StatusServiceUnavailable, gin.H{
accept := c.GetHeader("Accept") "code": 503,
xrw := strings.ToLower(strings.TrimSpace(c.GetHeader("X-Requested-With"))) "success": false,
if strings.Contains(accept, "application/json") || xrw == "xmlhttprequest" || strings.HasPrefix(path, "/api/") { "msg": "系统正在维护中,请稍后再试",
c.JSON(http.StatusServiceUnavailable, gin.H{ })
"code": 503,
"success": false,
"msg": "系统正在维护中,请稍后再试",
})
c.Abort()
return
}
// 4. 普通页面请求渲染维护页面
c.Header("Content-Type", "text/html; charset=utf-8")
c.Status(http.StatusServiceUnavailable)
c.Writer.WriteString(maintenanceHTML)
c.Abort() c.Abort()
} }
} }
// 简单的维护页面 HTML
const maintenanceHTML = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统维护中</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: #333;
}
.container {
text-align: center;
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
max-width: 500px;
width: 90%;
}
h1 { font-size: 24px; margin-bottom: 16px; color: #1890ff; }
p { font-size: 16px; color: #666; line-height: 1.6; }
.icon { font-size: 64px; margin-bottom: 24px; color: #faad14; }
</style>
</head>
<body>
<div class="container">
<div class="icon">⚠️</div>
<h1>系统维护中</h1>
<p>为了提供更好的服务,系统正在进行升级维护。<br>请稍后访问,给您带来的不便敬请谅解。</p>
</div>
</body>
</html>`

View File

@@ -9,7 +9,7 @@ type OperationLog struct {
ID uint `gorm:"primarykey" json:"id"` ID uint `gorm:"primarykey" json:"id"`
// 操作信息 // 操作信息
OperationType string `gorm:"type:varchar(50);index;comment:操作方式" json:"operation_type"` OperationType string `gorm:"type:varchar(50);index;comment:操作方式" json:"operation_type"` // 如:入库成功、凭证分配等
// 操作人信息 // 操作人信息
Operator string `gorm:"type:varchar(100);index;comment:操作账号" json:"operator"` Operator string `gorm:"type:varchar(100);index;comment:操作账号" json:"operator"`

View File

@@ -13,15 +13,21 @@ import (
// ============================================================================ // ============================================================================
// User 用户表模型 // User 用户表模型
// 此表只存储普通用户管理员账号存储在settings表中 // 存储所有账号包括超级管理员Role=0和子账号Role=1等
// CreatedAt/UpdatedAt 由 GORM 自动维护 // CreatedAt/UpdatedAt 由 GORM 自动维护
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"` 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:账号名,唯一索引" json:"username"`
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:密码加密盐值"`
CreatedAt time.Time `gorm:"comment:创建时间"` Status int `gorm:"not null;default:1;comment:状态0禁用1启用" json:"status"`
Role int `gorm:"not null;default:2;comment:角色类型0超级管理员1代理成员2普通成员" json:"role"`
Permissions string `gorm:"size:255;comment:权限列表,逗号分隔" json:"permissions"`
Nickname string `gorm:"size:64;comment:用户昵称" json:"nickname"`
Remark string `gorm:"size:255;comment:备注信息" json:"remark"`
Avatar string `gorm:"size:255;comment:用户头像URL" json:"avatar"`
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"comment:更新时间"` UpdatedAt time.Time `gorm:"comment:更新时间"`
} }

View File

@@ -1,167 +1,108 @@
package server package server
import ( import (
adminctl "NetworkAuth/controllers/admin" adminctl "NetworkAuth/controllers/admin"
"NetworkAuth/utils"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin" )
)
// RegisterAdminRoutes 注册管理员后台相关路由
// RegisterAdminRoutes 注册管理员后台相关路由 func RegisterAdminRoutes(rg *gin.RouterGroup) {
// - /admin/login: 支持GET渲染登录页、POST提交登录 admin := rg.Group("/admin")
// - /admin/logout: 管理员退出登录
// - /admin/dashboard: 管理员仪表盘 // Admin 认证相关路由
// - /admin/fragment/*: 布局内动态片段加载 admin.GET("/captcha", adminctl.CaptchaHandler)
// - /admin/api/*: 各种业务API admin.GET("/csrf", adminctl.CSRFTokenHandler)
func RegisterAdminRoutes(r *gin.Engine) { admin.POST("/login", adminctl.LoginHandler)
// /admin 根与前缀统一入口:根据是否登录跳转
r.GET("/admin", adminctl.AdminIndexHandler) // 公开设置API
r.GET("/admin/", adminctl.AdminIndexHandler) admin.GET("/settings/public", adminctl.SettingsPublicHandler)
// Admin 认证相关路由 // 退出登录
r.GET("/admin/login", adminctl.LoginPageHandler) admin.POST("/logout", adminctl.LogoutHandler)
r.POST("/admin/login", adminctl.LoginHandler)
// 需要认证的路由组
// 退出登录 authorized := admin.Group("/")
r.GET("/admin/logout", adminctl.LogoutHandler) authorized.Use(adminctl.AdminAuthRequired())
r.POST("/admin/logout", adminctl.LogoutHandler) {
// 系统信息API
// 验证码生成路由(无需认证) authorized.GET("/system/info", adminctl.SystemInfoHandler)
r.GET("/admin/captcha", adminctl.CaptchaHandler) authorized.GET("/dashboard/stats", adminctl.DashboardStatsHandler)
authorized.GET("/dashboard/login-logs", adminctl.DashboardLoginLogsHandler)
// CSRF令牌获取API无需认证但需要在登录页面等地方获取
r.GET("/admin/api/csrf-token", func(c *gin.Context) { // 个人资料API
// 生成新的CSRF令牌 authorized.GET("/profile", adminctl.ProfileQueryHandler)
token, err := utils.GenerateCSRFToken() authorized.POST("/profile/update", adminctl.ProfileUpdateHandler)
if err != nil { authorized.POST("/profile/password", adminctl.ProfilePasswordUpdateHandler)
c.JSON(500, gin.H{"success": false, "message": "生成CSRF令牌失败"})
return // 设置API
} authorized.GET("/settings", adminctl.SettingsQueryHandler)
authorized.POST("/settings/update", adminctl.SettingsUpdateHandler)
// 设置令牌到Cookie和响应头 authorized.POST("/settings/generate-key", adminctl.SettingsGenerateKeyHandler)
utils.SetCSRFToken(c, token)
// 操作日志API
// 返回令牌给前端 authorized.GET("/logs", adminctl.LogsListHandler)
c.JSON(200, gin.H{ authorized.POST("/logs/clear", adminctl.LogsClearHandler)
"code": 0, // 统一使用 code 0 表示成功
"success": true, // 登录日志API
"message": "CSRF令牌生成成功", authorized.GET("/login_logs", adminctl.LoginLogsListHandler)
"data": gin.H{ authorized.POST("/login_logs/clear", adminctl.LoginLogsClearHandler)
"csrf_token": token,
}, // 子账号相关API (Mock)
}) authorized.GET("/subaccounts/simple", adminctl.SubAccountSimpleListHandler)
})
// 应用管理API
// 需要认证的路由组 appsGroup := authorized.Group("/apps")
authorized := r.Group("/admin") {
authorized.Use(adminctl.AdminAuthRequired()) appsGroup.GET("/list", adminctl.AppsListHandler)
{ appsGroup.GET("/simple", adminctl.AppsSimpleListHandler)
// 后台布局页 appsGroup.POST("/create", adminctl.AppCreateHandler)
authorized.GET("/layout", adminctl.AdminLayoutHandler) appsGroup.POST("/update", adminctl.AppUpdateHandler)
appsGroup.POST("/delete", adminctl.AppDeleteHandler)
// 片段路由 appsGroup.POST("/batch_delete", adminctl.AppsBatchDeleteHandler)
authorized.GET("/dashboard", adminctl.DashboardFragmentHandler) appsGroup.POST("/batch_update_status", adminctl.AppsBatchUpdateStatusHandler)
authorized.GET("/profile", adminctl.ProfileFragmentHandler) appsGroup.POST("/update_status", adminctl.AppUpdateStatusHandler)
authorized.GET("/settings", adminctl.SettingsFragmentHandler) appsGroup.POST("/reset_secret", adminctl.AppResetSecretHandler)
authorized.GET("/operation_logs", adminctl.LogsFragmentHandler) appsGroup.GET("/get_app_data", adminctl.AppGetAppDataHandler)
authorized.GET("/login_logs", adminctl.LoginLogsFragmentHandler) appsGroup.POST("/update_app_data", adminctl.AppUpdateAppDataHandler)
authorized.GET("/apps", adminctl.AppsFragmentHandler) appsGroup.GET("/get_announcement", adminctl.AppGetAnnouncementHandler)
authorized.GET("/apis", adminctl.APIFragmentHandler) appsGroup.POST("/update_announcement", adminctl.AppUpdateAnnouncementHandler)
authorized.GET("/variables", adminctl.VariableFragmentHandler) appsGroup.GET("/get_multi_config", adminctl.AppGetMultiConfigHandler)
authorized.GET("/functions", adminctl.FunctionFragmentHandler) appsGroup.POST("/update_multi_config", adminctl.AppUpdateMultiConfigHandler)
appsGroup.GET("/get_bind_config", adminctl.AppGetBindConfigHandler)
// 系统信息API appsGroup.POST("/update_bind_config", adminctl.AppUpdateBindConfigHandler)
authorized.GET("/api/system/info", adminctl.SystemInfoHandler) appsGroup.GET("/get_register_config", adminctl.AppGetRegisterConfigHandler)
// 仪表盘数据 appsGroup.POST("/update_register_config", adminctl.AppUpdateRegisterConfigHandler)
authorized.GET("/api/dashboard/stats", adminctl.DashboardStatsHandler) }
authorized.GET("/api/dashboard/login-logs", adminctl.DashboardLoginLogsHandler)
// API接口管理API
// API 路由组 apisGroup := authorized.Group("/apis")
api := authorized.Group("/api") {
{ apisGroup.GET("/list", adminctl.APIListHandler)
// 个人资料API apisGroup.POST("/update", adminctl.APIUpdateHandler)
profileGroup := api.Group("/profile") apisGroup.POST("/update_status", adminctl.APIUpdateStatusHandler)
{ apisGroup.GET("/types", adminctl.APIGetTypesHandler)
profileGroup.GET("/info", adminctl.ProfileInfoHandler) apisGroup.POST("/generate_keys", adminctl.APIGenerateKeysHandler)
profileGroup.POST("/update", adminctl.ProfileUpdateHandler) }
profileGroup.POST("/password", adminctl.ProfilePasswordUpdateHandler)
} // 变量管理API
variableGroup := authorized.Group("/variable")
// 系统设置API {
settingsGroup := api.Group("/settings") variableGroup.GET("/list", adminctl.VariableListHandler)
{ variableGroup.POST("/create", adminctl.VariableCreateHandler)
settingsGroup.GET("", adminctl.SettingsQueryHandler) variableGroup.POST("/update", adminctl.VariableUpdateHandler)
settingsGroup.POST("/update", adminctl.SettingsUpdateHandler) variableGroup.POST("/delete", adminctl.VariableDeleteHandler)
settingsGroup.POST("/generate-key", adminctl.SettingsGenerateKeyHandler) variableGroup.POST("/batch_delete", adminctl.VariablesBatchDeleteHandler)
} }
// 操作日志API // 函数管理API
logsGroup := api.Group("/logs") functionGroup := authorized.Group("/function")
{ {
logsGroup.GET("", adminctl.LogsListHandler) functionGroup.GET("/list", adminctl.FunctionListHandler)
logsGroup.POST("/clear", adminctl.LogsClearHandler) functionGroup.POST("/create", adminctl.FunctionCreateHandler)
} functionGroup.POST("/update", adminctl.FunctionUpdateHandler)
functionGroup.POST("/delete", adminctl.FunctionDeleteHandler)
// 登录日志API functionGroup.POST("/batch_delete", adminctl.FunctionsBatchDeleteHandler)
loginLogsGroup := api.Group("/login_logs") }
{ }
loginLogsGroup.GET("", adminctl.LoginLogsListHandler) }
loginLogsGroup.POST("/clear", adminctl.LoginLogsClearHandler)
}
// 应用管理API
appsGroup := api.Group("/apps")
{
appsGroup.GET("/list", adminctl.AppsListHandler)
appsGroup.GET("/simple", adminctl.AppsSimpleListHandler)
appsGroup.POST("/create", adminctl.AppCreateHandler)
appsGroup.POST("/update", adminctl.AppUpdateHandler)
appsGroup.POST("/delete", adminctl.AppDeleteHandler)
appsGroup.POST("/batch_delete", adminctl.AppsBatchDeleteHandler)
appsGroup.POST("/batch_update_status", adminctl.AppsBatchUpdateStatusHandler)
appsGroup.POST("/update_status", adminctl.AppUpdateStatusHandler)
appsGroup.POST("/reset_secret", adminctl.AppResetSecretHandler)
appsGroup.GET("/get_app_data", adminctl.AppGetAppDataHandler)
appsGroup.POST("/update_app_data", adminctl.AppUpdateAppDataHandler)
appsGroup.GET("/get_announcement", adminctl.AppGetAnnouncementHandler)
appsGroup.POST("/update_announcement", adminctl.AppUpdateAnnouncementHandler)
appsGroup.GET("/get_multi_config", adminctl.AppGetMultiConfigHandler)
appsGroup.POST("/update_multi_config", adminctl.AppUpdateMultiConfigHandler)
appsGroup.GET("/get_bind_config", adminctl.AppGetBindConfigHandler)
appsGroup.POST("/update_bind_config", adminctl.AppUpdateBindConfigHandler)
appsGroup.GET("/get_register_config", adminctl.AppGetRegisterConfigHandler)
appsGroup.POST("/update_register_config", adminctl.AppUpdateRegisterConfigHandler)
}
// API接口管理API
apisGroup := api.Group("/apis")
{
apisGroup.GET("/list", adminctl.APIListHandler)
apisGroup.POST("/update", adminctl.APIUpdateHandler)
apisGroup.POST("/update_status", adminctl.APIUpdateStatusHandler)
apisGroup.GET("/types", adminctl.APIGetTypesHandler)
apisGroup.POST("/generate_keys", adminctl.APIGenerateKeysHandler)
}
}
// 变量管理API
variableGroup := authorized.Group("/variable")
{
variableGroup.GET("/list", adminctl.VariableListHandler)
variableGroup.POST("/create", adminctl.VariableCreateHandler)
variableGroup.POST("/update", adminctl.VariableUpdateHandler)
variableGroup.POST("/delete", adminctl.VariableDeleteHandler)
variableGroup.POST("/batch_delete", adminctl.VariablesBatchDeleteHandler)
}
// 函数管理API
functionGroup := authorized.Group("/function")
{
functionGroup.GET("/list", adminctl.FunctionListHandler)
functionGroup.POST("/create", adminctl.FunctionCreateHandler)
functionGroup.POST("/update", adminctl.FunctionUpdateHandler)
functionGroup.POST("/delete", adminctl.FunctionDeleteHandler)
functionGroup.POST("/batch_delete", adminctl.FunctionsBatchDeleteHandler)
}
}
}

View File

@@ -1,18 +1,16 @@
package server package server
import ( import (
default_ctrl "NetworkAuth/controllers/default" defaultctrl "NetworkAuth/controllers/default"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ============================================================================
// 路由注册函数
// ============================================================================
// RegisterDefaultRoutes 注册默认路由 // RegisterDefaultRoutes 注册默认路由
// 包含根路径,用于默认主页功能 // 包含根路径、健康检查、API信息等基础端点
func RegisterDefaultRoutes(r *gin.Engine) { func RegisterDefaultRoutes(rg *gin.RouterGroup) {
// 根路径 - 主页 homeGroup := rg.Group("/home")
r.GET("/", default_ctrl.RootHandler)
// 根路径
homeGroup.GET("", defaultctrl.RootHandler)
} }

View File

@@ -7,10 +7,9 @@ import (
) )
// RegisterInstallRoutes 注册安装相关的路由 // RegisterInstallRoutes 注册安装相关的路由
func RegisterInstallRoutes(r *gin.Engine) { func RegisterInstallRoutes(rg *gin.RouterGroup) {
// 安装向导页面 installGroup := rg.Group("/install")
r.GET("/install", install.InstallPageHandler)
// 提交安装表单 // 提交安装表单
r.POST("/api/install", install.InstallSubmitHandler) installGroup.POST("", install.InstallSubmitHandler)
} }

View File

@@ -1,48 +1,15 @@
package server package server
import ( import (
"NetworkAuth/web" "github.com/gin-gonic/gin"
"io/fs" )
"log"
"net/http" // RegisterRoutes 聚合注册所有路由
func RegisterRoutes(r *gin.Engine) {
"github.com/gin-gonic/gin" // 所有路由基于 /api
) apiGroup := r.Group("/api")
// RegisterRoutes 聚合注册所有路由 RegisterInstallRoutes(apiGroup)
func RegisterRoutes(r *gin.Engine) { RegisterDefaultRoutes(apiGroup)
registerStaticRoutes(r) RegisterAdminRoutes(apiGroup)
registerFaviconRoute(r) }
RegisterInstallRoutes(r)
RegisterDefaultRoutes(r)
RegisterAdminRoutes(r)
}
// registerStaticRoutes 注册静态资源路由
// 静态资源服务,将 /static/ 和 /assets/ 映射到嵌入的文件系统
func registerStaticRoutes(r *gin.Engine) {
if fsys, err := web.GetStaticFS(); err == nil {
// 为 /static/ 路径创建子文件系统
if staticSubFS, staticErr := fs.Sub(fsys, "static"); staticErr == nil {
r.StaticFS("/static", http.FS(staticSubFS))
} else {
log.Printf("创建静态资源子文件系统失败: %v", staticErr)
}
// 为 /assets/ 路径创建子文件系统
if assetsSubFS, assetsErr := fs.Sub(fsys, "assets"); assetsErr == nil {
r.StaticFS("/assets", http.FS(assetsSubFS))
} else {
log.Printf("创建资产资源子文件系统失败: %v", assetsErr)
}
} else {
log.Printf("初始化静态资源文件系统失败: %v", err)
}
}
// registerFaviconRoute 注册favicon路由
func registerFaviconRoute(r *gin.Engine) {
// 将 /favicon.ico 重定向到 /assets/favicon.svg
r.GET("/favicon.ico", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/assets/favicon.svg")
})
}

View File

@@ -1,10 +1,10 @@
package services package services
import ( import (
"NetworkAuth/models"
"NetworkAuth/utils"
"context" "context"
"fmt" "fmt"
"NetworkAuth/models"
"NetworkAuth/utils"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -104,7 +104,11 @@ func FindEntitiesByCondition(model interface{}, result interface{}, condition st
// 返回: 是否存在和错误 // 返回: 是否存在和错误
func CheckEntityExists(model interface{}, condition string, db *gorm.DB, args ...interface{}) (bool, error) { func CheckEntityExists(model interface{}, condition string, db *gorm.DB, args ...interface{}) (bool, error) {
var count int64 var count int64
err := db.Model(model).Where(condition, args...).Count(&count).Error query := db.Model(model)
if condition != "" {
query = query.Where(condition, args...)
}
err := query.Count(&count).Error
return count > 0, err return count > 0, err
} }

View File

@@ -42,6 +42,14 @@ func GetSettingsService() *SettingsService {
return settingsService return settingsService
} }
// ResetSettingsService 充置设置服务单例,主要用于重新加载设置(比如安装后)
func ResetSettingsService() {
settingsService = &SettingsService{
cache: make(map[string]string),
}
settingsService.loadAllSettings()
}
// ============================================================================ // ============================================================================
// 私有函数 // 私有函数
// ============================================================================ // ============================================================================
@@ -58,6 +66,12 @@ func (s *SettingsService) loadAllSettings() {
return return
} }
// 检查 settings 表是否存在,如果不存在则不查询
if !db.Migrator().HasTable(&models.Settings{}) {
logrus.Info("settings 表不存在,跳过加载设置")
return
}
var settings []models.Settings var settings []models.Settings
if err := db.Find(&settings).Error; err != nil { if err := db.Find(&settings).Error; err != nil {
logrus.WithError(err).Error("加载设置失败") logrus.WithError(err).Error("加载设置失败")

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#2563eb"/>
<stop offset="100%" stop-color="#60a5fa"/>
</linearGradient>
</defs>
<rect x="8" y="8" width="112" height="112" rx="20" fill="url(#g)"/>
<path d="M64 28 L86 64 L64 100 L42 64 Z" fill="#fff" opacity="0.95"/>
<circle cx="64" cy="64" r="10" fill="#2563eb"/>
</svg>

Before

Width:  |  Height:  |  Size: 491 B

View File

@@ -1,355 +0,0 @@
{
"Default": {
"--color-white": "#FFFFFF",
"--color-black": "#000000",
"--lay-color-white": "#FAFAFA",
"--lay-color-black": "#333333",
"--lay-color-red-1": "#FFF1E8",
"--lay-color-red-2": "#FFD7C0",
"--lay-color-red-3": "#FFBB99",
"--lay-color-red-4": "#FF9C71",
"--lay-color-red-5": "#FF7A4A",
"--lay-color-red-6": "#FF5722",
"--lay-color-red-7": "#D23B15",
"--lay-color-red-8": "#A6250B",
"--lay-color-red-9": "#791404",
"--lay-color-red-10": "#4D0800",
"--lay-color-blue-1": "#E8F9FF",
"--lay-color-blue-2": "#C0ECFF",
"--lay-color-blue-3": "#97DCFF",
"--lay-color-blue-4": "#6FCAFF",
"--lay-color-blue-5": "#46B5FF",
"--lay-color-blue-6": "#1E9FFF",
"--lay-color-blue-7": "#1379D2",
"--lay-color-blue-8": "#0A58A6",
"--lay-color-blue-9": "#043A79",
"--lay-color-blue-10": "#00214D",
"--lay-color-lightblue-1": "#E8FDFF",
"--lay-color-lightblue-2": "#C1F4FB",
"--lay-color-lightblue-3": "#9CEAF7",
"--lay-color-lightblue-4": "#77DDF4",
"--lay-color-lightblue-5": "#53CEF0",
"--lay-color-lightblue-6": "#31BDEC",
"--lay-color-lightblue-7": "#1F95C4",
"--lay-color-lightblue-8": "#10709C",
"--lay-color-lightblue-9": "#064E74",
"--lay-color-lightblue-10": "#002F4D",
"--lay-color-layuigreen-1": "#E8FFF9",
"--lay-color-layuigreen-2": "#B5F1E3",
"--lay-color-layuigreen-3": "#87E3D1",
"--lay-color-layuigreen-4": "#5DD6C1",
"--lay-color-layuigreen-5": "#37C8B5",
"--lay-color-layuigreen-6": "#16BAAA",
"--lay-color-layuigreen-7": "#0E9F95",
"--lay-color-layuigreen-8": "#08837F",
"--lay-color-layuigreen-9": "#036868",
"--lay-color-layuigreen-10": "#004A4D",
"--lay-color-green-1": "#E8FFF2",
"--lay-color-green-2": "#B5F1D1",
"--lay-color-green-3": "#86E2B4",
"--lay-color-green-4": "#5CD49C",
"--lay-color-green-5": "#37C588",
"--lay-color-green-6": "#16B777",
"--lay-color-green-7": "#0E9C68",
"--lay-color-green-8": "#088259",
"--lay-color-green-9": "#036749",
"--lay-color-green-10": "#004D38",
"--lay-color-orange-1": "#FFFCE8",
"--lay-color-orange-2": "#FFF5BA",
"--lay-color-orange-3": "#FFEA8B",
"--lay-color-orange-4": "#FFDC5D",
"--lay-color-orange-5": "#FFCB2E",
"--lay-color-orange-6": "#FFB800",
"--lay-color-orange-7": "#D29000",
"--lay-color-orange-8": "#A66C00",
"--lay-color-orange-9": "#794B00",
"--lay-color-orange-10": "#4D2D00",
"--lay-color-cyan-1": "#E8F6FF",
"--lay-color-cyan-2": "#B9CEDD",
"--lay-color-cyan-3": "#8FA7BB",
"--lay-color-cyan-4": "#6A829A",
"--lay-color-cyan-5": "#4A5F78",
"--lay-color-cyan-6": "#2F4056",
"--lay-color-cyan-7": "#223654",
"--lay-color-cyan-8": "#162C51",
"--lay-color-cyan-9": "#0B214F",
"--lay-color-cyan-10": "#00174D",
"--lay-color-purple-1": "#FDE8FF",
"--lay-color-purple-2": "#EDBEF4",
"--lay-color-purple-3": "#DC97E8",
"--lay-color-purple-4": "#C972DD",
"--lay-color-purple-5": "#B651D1",
"--lay-color-purple-6": "#A233C6",
"--lay-color-purple-7": "#8120A8",
"--lay-color-purple-8": "#631289",
"--lay-color-purple-9": "#48076B",
"--lay-color-purple-10": "#2F004D",
"--lay-color-black-1": "#E8F8FF",
"--lay-color-black-2": "#BFD0D8",
"--lay-color-black-3": "#98A8B1",
"--lay-color-black-4": "#73818A",
"--lay-color-black-5": "#505B63",
"--lay-color-black-6": "#2F363C",
"--lay-color-black-7": "#23303C",
"--lay-color-black-8": "#18293C",
"--lay-color-black-9": "#0C213C",
"--lay-color-black-10": "#00183C",
"--lay-color-gray-1": "#FAFAFA",
"--lay-color-gray-2": "#F6F6F6",
"--lay-color-gray-3": "#EEEEEE",
"--lay-color-gray-4": "#E2E2E2",
"--lay-color-gray-5": "#DDDDDD",
"--lay-color-gray-6": "#D2D2D2",
"--lay-color-gray-7": "#CCCCCC",
"--lay-color-gray-8": "#C2C2C2",
"--lay-color-gray-9": "#AAAAAA",
"--lay-color-gray-10": "#939393",
"--lay-color-gray-11": "#858585",
"--lay-color-gray-12": "#7b7b7b",
"--lay-color-gray-13": "#686868",
"--lay-color-primary": "var(--lay-color-layuigreen-6)",
"--lay-color-primary-hover": "var(--lay-color-layuigreen-5)",
"--lay-color-primary-active": "var(--lay-color-layuigreen-7)",
"--lay-color-primary-disabled": "var(--lay-color-layuigreen-3)",
"--lay-color-primary-light": "var(--lay-color-layuigreen-4)",
"--lay-color-secondary": "var(--lay-color-green-6)",
"--lay-color-secondary-hover": "var(--lay-color-green-5)",
"--lay-color-secondary-active": "var(--lay-color-green-7)",
"--lay-color-secondary-disabled": "var(--lay-color-green-3)",
"--lay-color-secondary-light": "var(--lay-color-green-4)",
"--lay-color-info": "var(--lay-color-lightblue-6)",
"--lay-color-info-hover": "var(--lay-color-lightblue-5)",
"--lay-color-info-active": "var(--lay-color-lightblue-7)",
"--lay-color-info-disabled": "var(--lay-color-lightblue-3)",
"--lay-color-info-light": "var(--lay-color-lightblue-4)",
"--lay-color-normal": "var(--lay-color-blue-6)",
"--lay-color-normal-hover": "var(--lay-color-blue-5)",
"--lay-color-normal-active": "var(--lay-color-blue-7)",
"--lay-color-normal-disabled": "var(--lay-color-blue-3)",
"--lay-color-normal-light": "var(--lay-color-blue-4)",
"--lay-color-warning": "var(--lay-color-orange-6)",
"--lay-color-warning-hover": "var(--lay-color-orange-5)",
"--lay-color-warning-active": "var(--lay-color-orange-7)",
"--lay-color-warning-disabled": "var(--lay-color-orange-3)",
"--lay-color-warning-light": "var(--lay-color-orange-4)",
"--lay-color-success": "var(--lay-color-green-6)",
"--lay-color-success-hover": "var(--lay-color-green-5)",
"--lay-color-success-active": "var(--lay-color-green-7)",
"--lay-color-success-disabled": "var(--lay-color-green-3)",
"--lay-color-success-light": "var(--lay-color-green-4)",
"--lay-color-danger": "var(--lay-color-red-6)",
"--lay-color-danger-hover": "var(--lay-color-red-5)",
"--lay-color-danger-active": "var(--lay-color-red-7)",
"--lay-color-danger-disabled": "var(--lay-color-red-3)",
"--lay-color-danger-light": "var(--lay-color-red-4)",
"--lay-color-bg-1": "#17171A",
"--lay-color-bg-2": "#232324",
"--lay-color-bg-3": "#2a2a2b",
"--lay-color-bg-4": "#313132",
"--lay-color-bg-5": "#373739",
"--lay-color-bg-white": "#f6f6f6",
"--lay-color-text-1": "rgba(255,255,255,.9)",
"--lay-color-text-2": "rgba(255,255,255,.7)",
"--lay-color-text-3": "rgba(255,255,255,.5)",
"--lay-color-text-4": "rgba(255,255,255,.3)",
"--lay-color-border-1": "#2e2e30",
"--lay-color-border-2": "#484849",
"--lay-color-border-3": "#5f5f60",
"--lay-color-border-4": "#929293",
"--lay-color-fill-1": "rgba(255,255,255,.04)",
"--lay-color-fill-2": "rgba(255,255,255,.08)",
"--lay-color-fill-3": "rgba(255,255,255,.12)",
"--lay-color-fill-4": "rgba(255,255,255,.16)",
"--lay-color-hover": "var(--lay-color-fill-3)",
"--lay-color-active": "var(--lay-color-fill-3)",
"--lay-shadow-1": "0 4px 6px rgba(0, 0, 0, 6%), 0 1px 10px rgba(0, 0, 0, 8%), 0 2px 4px rgba(0, 0, 0, 12%)",
"--lay-shadow-2": "0 8px 10px rgba(0, 0, 0, 12%), 0 3px 14px rgba(0, 0, 0, 10%), 0 5px 5px rgba(0, 0, 0, 16%)",
"--lay-shadow-3": "0 16px 24px rgba(0, 0, 0, 14%), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%)"
},
"ColorPaletteDark": {
"--lay-color-red-1": "#4D0800",
"--lay-color-red-2": "#791505",
"--lay-color-red-3": "#A62A11",
"--lay-color-red-4": "#D24622",
"--lay-color-red-5": "#FF6839",
"--lay-color-red-6": "#FF7948",
"--lay-color-red-7": "#FF9C71",
"--lay-color-red-8": "#FFBC9A",
"--lay-color-red-9": "#FFD9C3",
"--lay-color-red-10": "#FFF3EB",
"--lay-color-blue-1": "#00214D",
"--lay-color-blue-2": "#033A79",
"--lay-color-blue-3": "#0F5AA6",
"--lay-color-blue-4": "#1F7FD2",
"--lay-color-blue-5": "#35A9FF",
"--lay-color-blue-6": "#44B4FF",
"--lay-color-blue-7": "#70CAFF",
"--lay-color-blue-8": "#9BDDFF",
"--lay-color-blue-9": "#C6EEFF",
"--lay-color-blue-10": "#F2FCFF",
"--lay-color-lightblue-1": "#002F4D",
"--lay-color-lightblue-2": "#044D74",
"--lay-color-lightblue-3": "#12719C",
"--lay-color-lightblue-4": "#2797C4",
"--lay-color-lightblue-5": "#42C1EC",
"--lay-color-lightblue-6": "#56CFF0",
"--lay-color-lightblue-7": "#79DDF4",
"--lay-color-lightblue-8": "#9DEAF7",
"--lay-color-lightblue-9": "#C3F4FB",
"--lay-color-lightblue-10": "#EAFDFF",
"--lay-color-layuigreen-1": "#004A4D",
"--lay-color-layuigreen-2": "#046868",
"--lay-color-layuigreen-3": "#0E837F",
"--lay-color-layuigreen-4": "#1C9F96",
"--lay-color-layuigreen-5": "#2EBAAC",
"--lay-color-layuigreen-6": "#40C8B6",
"--lay-color-layuigreen-7": "#64D6C2",
"--lay-color-layuigreen-8": "#8CE3D2",
"--lay-color-layuigreen-9": "#B9F1E4",
"--lay-color-layuigreen-10": "#EAFFFA",
"--lay-color-green-1": "#004D38",
"--lay-color-green-2": "#046749",
"--lay-color-green-3": "#0E825B",
"--lay-color-green-4": "#1C9C6D",
"--lay-color-green-5": "#2EB780",
"--lay-color-green-6": "#3FC58B",
"--lay-color-green-7": "#64D4A0",
"--lay-color-green-8": "#8CE2B7",
"--lay-color-green-9": "#BAF1D3",
"--lay-color-green-10": "#EBFFF4",
"--lay-color-orange-1": "#4D2D00",
"--lay-color-orange-2": "#794C04",
"--lay-color-orange-3": "#A66F0A",
"--lay-color-orange-4": "#D29613",
"--lay-color-orange-5": "#FFC11F",
"--lay-color-orange-6": "#FFC926",
"--lay-color-orange-7": "#FFDB57",
"--lay-color-orange-8": "#FFE987",
"--lay-color-orange-9": "#FFF5B8",
"--lay-color-orange-10": "#FFFCE8",
"--lay-color-cyan-1": "#00174D",
"--lay-color-cyan-2": "#0B214F",
"--lay-color-cyan-3": "#162C51",
"--lay-color-cyan-4": "#233754",
"--lay-color-cyan-5": "#304056",
"--lay-color-cyan-6": "#546478",
"--lay-color-cyan-7": "#75879A",
"--lay-color-cyan-8": "#99ABBB",
"--lay-color-cyan-9": "#C2D2DD",
"--lay-color-cyan-10": "#EFF9FF",
"--lay-color-purple-1": "#2F004D",
"--lay-color-purple-2": "#47056B",
"--lay-color-purple-3": "#631389",
"--lay-color-purple-4": "#8326A8",
"--lay-color-purple-5": "#A53FC6",
"--lay-color-purple-6": "#B755D1",
"--lay-color-purple-7": "#CA77DD",
"--lay-color-purple-8": "#DD9BE8",
"--lay-color-purple-9": "#EEC3F4",
"--lay-color-purple-10": "#FDEDFF"
},
"ColorPaletteLight": {
"--lay-color-red-1": "#FFF1E8",
"--lay-color-red-2": "#FFD7C0",
"--lay-color-red-3": "#FFBB99",
"--lay-color-red-4": "#FF9C71",
"--lay-color-red-5": "#FF7A4A",
"--lay-color-red-6": "#FF5722",
"--lay-color-red-7": "#D23B15",
"--lay-color-red-8": "#A6250B",
"--lay-color-red-9": "#791404",
"--lay-color-red-10": "#4D0800",
"--lay-color-blue-1": "#E8F9FF",
"--lay-color-blue-2": "#C0ECFF",
"--lay-color-blue-3": "#97DCFF",
"--lay-color-blue-4": "#6FCAFF",
"--lay-color-blue-5": "#46B5FF",
"--lay-color-blue-6": "#1E9FFF",
"--lay-color-blue-7": "#1379D2",
"--lay-color-blue-8": "#0A58A6",
"--lay-color-blue-9": "#043A79",
"--lay-color-blue-10": "#00214D",
"--lay-color-lightblue-1": "#E8FDFF",
"--lay-color-lightblue-2": "#C1F4FB",
"--lay-color-lightblue-3": "#9CEAF7",
"--lay-color-lightblue-4": "#77DDF4",
"--lay-color-lightblue-5": "#53CEF0",
"--lay-color-lightblue-6": "#31BDEC",
"--lay-color-lightblue-7": "#1F95C4",
"--lay-color-lightblue-8": "#10709C",
"--lay-color-lightblue-9": "#064E74",
"--lay-color-lightblue-10": "#002F4D",
"--lay-color-layuigreen-1": "#E8FFF9",
"--lay-color-layuigreen-2": "#B5F1E3",
"--lay-color-layuigreen-3": "#87E3D1",
"--lay-color-layuigreen-4": "#5DD6C1",
"--lay-color-layuigreen-5": "#37C8B5",
"--lay-color-layuigreen-6": "#16BAAA",
"--lay-color-layuigreen-7": "#0E9F95",
"--lay-color-layuigreen-8": "#08837F",
"--lay-color-layuigreen-9": "#036868",
"--lay-color-layuigreen-10": "#004A4D",
"--lay-color-green-1": "#E8FFF2",
"--lay-color-green-2": "#B5F1D1",
"--lay-color-green-3": "#86E2B4",
"--lay-color-green-4": "#5CD49C",
"--lay-color-green-5": "#37C588",
"--lay-color-green-6": "#16B777",
"--lay-color-green-7": "#0E9C68",
"--lay-color-green-8": "#088259",
"--lay-color-green-9": "#036749",
"--lay-color-green-10": "#004D38",
"--lay-color-orange-1": "#FFFCE8",
"--lay-color-orange-2": "#FFF5BA",
"--lay-color-orange-3": "#FFEA8B",
"--lay-color-orange-4": "#FFDC5D",
"--lay-color-orange-5": "#FFCB2E",
"--lay-color-orange-6": "#FFB800",
"--lay-color-orange-7": "#D29000",
"--lay-color-orange-8": "#A66C00",
"--lay-color-orange-9": "#794B00",
"--lay-color-orange-10": "#4D2D00",
"--lay-color-cyan-1": "#E8F6FF",
"--lay-color-cyan-2": "#B9CEDD",
"--lay-color-cyan-3": "#8FA7BB",
"--lay-color-cyan-4": "#6A829A",
"--lay-color-cyan-5": "#4A5F78",
"--lay-color-cyan-6": "#2F4056",
"--lay-color-cyan-7": "#223654",
"--lay-color-cyan-8": "#162C51",
"--lay-color-cyan-9": "#0B214F",
"--lay-color-cyan-10": "#00174D",
"--lay-color-purple-1": "#FDE8FF",
"--lay-color-purple-2": "#EDBEF4",
"--lay-color-purple-3": "#DC97E8",
"--lay-color-purple-4": "#C972DD",
"--lay-color-purple-5": "#B651D1",
"--lay-color-purple-6": "#A233C6",
"--lay-color-purple-7": "#8120A8",
"--lay-color-purple-8": "#631289",
"--lay-color-purple-9": "#48076B",
"--lay-color-purple-10": "#2F004D"
},
"editable": {
"--lay-color-bg-1": "#17171A",
"--lay-color-bg-2": "#232324",
"--lay-color-bg-3": "#2a2a2b",
"--lay-color-bg-4": "#313132",
"--lay-color-bg-5": "#373739",
"--lay-color-bg-white": "#f6f6f6",
"--lay-color-text-1": "rgba(255,255,255,.9)",
"--lay-color-text-2": "rgba(255,255,255,.7)",
"--lay-color-text-3": "rgba(255,255,255,.5)",
"--lay-color-text-4": "rgba(255,255,255,.3)",
"--lay-color-border-1": "#2e2e30",
"--lay-color-border-2": "#484849",
"--lay-color-border-3": "#5f5f60",
"--lay-color-border-4": "#929293",
"--lay-color-fill-1": "rgba(255,255,255,.04)",
"--lay-color-fill-2": "rgba(255,255,255,.08)",
"--lay-color-fill-3": "rgba(255,255,255,.12)",
"--lay-color-fill-4": "rgba(255,255,255,.16)",
"--lay-color-hover": "var(--lay-color-fill-3)",
"--lay-color-active": "var(--lay-color-fill-3)"
}
}

View File

@@ -1,67 +0,0 @@
package web
import (
"embed"
"html/template"
"io/fs"
"log"
"os"
"path/filepath"
"github.com/spf13/viper"
)
// TemplatesFS 嵌入模板的文件系统
//
//go:embed template/*/*.html
var templatesFS embed.FS
// StaticFS 嵌入静态资源的文件系统(包含 CSS/JS 的 static 与 图片/字体等资源的 assets
//
//go:embed static/* assets/*
var staticFS embed.FS
// getDistRootFS 获取基于 server.dist 的本地文件系统
// 当 server.dist 非空且路径存在时,返回对应的本地只读 FS否则返回 nil
func getDistRootFS() fs.FS {
// 从配置中读取 server.dist
distPath := viper.GetString("server.dist")
if distPath == "" {
return nil
}
// 归一化路径,兼容相对/绝对
absPath := distPath
if !filepath.IsAbs(distPath) {
if p, err := filepath.Abs(distPath); err == nil {
absPath = p
}
}
// 检查目录是否存在
if info, err := os.Stat(absPath); err == nil && info.IsDir() {
return os.DirFS(absPath)
}
log.Printf("server.dist 路径无效或不可访问:%s将回退使用嵌入资源", distPath)
return nil
}
// ParseTemplates 解析模板
// 优先从 server.dist 指定目录加载(当配置非空且有效),否则回退到嵌入模板
func ParseTemplates() (*template.Template, error) { // Go 顶级函数不支持箭头写法
if distFS := getDistRootFS(); distFS != nil {
// 期望 dist 目录下存在 template 与 template/admin 结构
// 如:{dist}/template/*.html 与 {dist}/template/admin/*.html
return template.ParseFS(distFS, "template/*/*.html")
}
// 默认:使用嵌入模板
return template.ParseFS(templatesFS, "template/*/*.html")
}
// GetStaticFS 返回静态资源文件系统(包含 static 与 assets 目录)
// 优先使用 server.dist 指定的本地目录;否则回退到嵌入静态资源
func GetStaticFS() (fs.FS, error) { // Go 顶级函数不支持箭头写法
if distFS := getDistRootFS(); distFS != nil {
// 直接返回以 dist 根为起点的 FSroutes 中会再基于此 FS Sub 出 static 与 assets
return distFS, nil
}
return staticFS, nil
}

View File

@@ -1,93 +0,0 @@
wc-include{padding: 15px;display: block;}
#app {display: none;}
.layui-layout-right .layui-nav-bar {background-color: unset !important;}
.layui-layout-admin .layui-side {top: 0 !important;z-index: 1001;}
.layui-layout-admin .layui-logo {position: relative !important;height: 60px !important;top: -2px !important;}
.layui-side,
.layui-header,
.layui-body,
.layui-footer {transition: left 0.3s;}
.collapse .layui-layout-admin .layui-side,
.collapse .layui-layout-admin .layui-header {left: -200px;}
.collapse .layui-layout-admin .layui-footer,
.collapse .layui-layout-admin .layui-body {left: 0px;}
::view-transition-old(root),
::view-transition-new(root) {animation: none;mix-blend-mode: normal;}
::view-transition-old(root) {z-index: 9999;}
::view-transition-new(root) {z-index: 1;}
.dark::view-transition-old(root) {z-index: 1;}
.dark::view-transition-new(root) {z-index: 9999;}
/* 以下为自定义样式 */
.system-info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 20px; }
.system-info-item { padding: 16px; border-radius: 8px; background: var(--card); border: 1px solid var(--border); }
.system-info-label { font-size: 14px; color: var(--muted); margin-bottom: 8px; }
.system-info-value { font-size: 16px; font-weight: 600; color: var(--fg); }
/* ===================== 滚动条美化与布局约束(右侧滑块条) ===================== */
/*
作用:
1. 统一 Admin 布局下内容区(.layui-body为局部滚动容器只在头部与页脚之间滚动
2. 美化 .layui-body 的滚动条样式,增强可用性与观感
3. 不影响登录页等非 Admin 布局页面(仅在 .layui-layout-admin 作用域内生效)
*/
:root {
/* 头部与页脚的高度变量,便于后续维护/调整 */
--admin-header-h: 60px;
--admin-footer-h: 0px; /* 当前页脚未启用,如启用可改为 44px 等 */
}
/* Admin 主容器占满视口,高度锁定,避免出现浏览器右侧全局滚动条 */
.layui-layout-admin {
position: relative;
height: 100vh;
overflow: hidden;
}
/* 头部/页脚高度同步到变量,确保与内容区上下边界垂直齐平 */
.layui-layout-admin .layui-header {
height: var(--admin-header-h);
line-height: var(--admin-header-h);
}
.layui-layout-admin .layui-footer {
height: var(--admin-footer-h);
line-height: var(--admin-footer-h);
}
/* 内容区设为局部滚动容器,顶部/底部与头部/页脚精确对齐 */
.layui-layout-admin .layui-body {
/* 仅约束垂直方向,左右定位保持与 Layui 默认一致,兼容现有折叠动画 */
top: var(--admin-header-h) !important;
bottom: var(--admin-footer-h) !important;
overflow: auto;
/* Firefox 滚动条样式(细滚动条+自定义颜色) */
scrollbar-width: thin; /* 细滚动条 */
scrollbar-color: var(--lay-color-secondary) var(--lay-color-bg-3); /* thumb 与 track 颜色 */
}
/* WebKit 滚动条样式Chrome/Edge/Safari */
.layui-layout-admin .layui-body::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.layui-layout-admin .layui-body::-webkit-scrollbar-track {
background: var(--lay-color-bg-2);
border-left: 1px solid var(--lay-color-border-2);
}
.layui-layout-admin .layui-body::-webkit-scrollbar-thumb {
/* 渐变+内边透明边框,获得圆润质感 */
background: linear-gradient(180deg, var(--lay-color-gray-7), var(--lay-color-gray-9));
border-radius: 8px;
border: 2px solid transparent;
background-clip: padding-box;
}
.layui-layout-admin .layui-body::-webkit-scrollbar-thumb:hover {
background: var(--lay-color-secondary); /* 悬停高亮,强化可交互性 */
}
.layui-layout-admin .layui-body::-webkit-scrollbar-corner {
background: transparent;
}
/* ===================== END 滚动条美化与布局约束 ===================== */

View File

@@ -1,463 +0,0 @@
const VERSION = '2.9.20'; // Using local version
const layuicss = '/static/lib/layui/css/layui.css';
const layuijs = '/static/lib/layui/layui.js';
const rootPath = (function (src) {
src = (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') ? document.currentScript.src : document.scripts[document.scripts.length - 1].src;
return src.substring(0, src.lastIndexOf('/') + 1);
})();
// CSRF令牌管理
const CSRFManager = {
// 缓存的CSRF令牌
token: null,
// 获取CSRF令牌
async getToken() {
if (this.token) {
return this.token;
}
try {
const response = await fetch('/admin/api/csrf-token', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (data.code === 0 && data.data && data.data.csrf_token) {
this.token = data.data.csrf_token;
return this.token;
}
}
} catch (error) {
console.error('获取CSRF令牌失败:', error);
}
return null;
},
// 清除缓存的令牌
clearToken() {
this.token = null;
},
// 为fetch请求添加CSRF令牌
async addCSRFHeader(headers = {}) {
const token = await this.getToken();
if (token) {
headers['X-CSRF-Token'] = token;
}
return headers;
}
};
// 增强的fetch函数自动添加CSRF令牌
window.fetchWithCSRF = async function(url, options = {}) {
const headers = await CSRFManager.addCSRFHeader(options.headers || {});
return fetch(url, {
...options,
headers
});
};
const app = document.querySelector('#app')
addLink({ href: layuicss }).then(() => {
app.style.display = 'block';
});
addLink({ id: 'layui_theme_css', href: `/static/src/layui-theme-dark-selector.css` });
loadScript(layuijs, function () {
layui
.config({
base: '/static/lib/',
})
.extend({
drawer: 'drawer/drawer',
});
layui.use(['drawer', 'colorMode', 'jquery', 'layer'], async function () {
const { $, element, form, layer, util, dropdown, drawer, colorMode } = layui;
// --- CSRF Setup for jQuery ---
// Ensure token is loaded
await CSRFManager.getToken();
$.ajaxSetup({
beforeSend: function(xhr) {
if (CSRFManager.token) {
xhr.setRequestHeader('X-CSRF-Token', CSRFManager.token);
}
},
complete: function(xhr) {
if (xhr.status === 401) {
window.location.href = '/admin/login';
}
}
});
// -----------------------------
const APPERANCE_KEY = 'layui-theme-demo-prefer-dark';
const theme = colorMode.init({
selector: 'html',
attribute: 'class',
initialValue: 'dark',
modes: {
light: '',
dark: 'dark',
},
storageKey: APPERANCE_KEY,
onChanged(mode, defaultHandler) {
const isAppearanceTransition = document.startViewTransition && !window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
const isDark = mode === 'dark';
$('#change-theme').attr('class', `layui-icon layui-icon-${isDark ? 'moon' : 'light'}`);
if (!isAppearanceTransition) {
defaultHandler();
} else {
rippleViewTransition(isDark, function () {
defaultHandler();
});
}
},
});
routerTo({path: location.hash.slice(1) || 'dashboard'});
dropdown.render({
elem: '#change-theme',
align: 'center',
data: [
{
title: '深色模式',
id: 'dark',
icon: 'layui-icon-moon',
},
{
title: '浅色模式',
id: 'light',
icon: 'layui-icon-light',
},
{
title: '跟随系统',
id: 'auto',
icon: 'layui-icon-console',
},
],
templet(d) {
return `
<span style="display: flex;">
<i class="layui-icon ${d.icon}" style="margin-right: 8px"></i>
${d.title}
</span>`.trim();
},
click(obj) {
const { id: mode } = obj;
theme.setMode(mode);
},
});
util.event('lay-header-event', {
menuLeft() {
$('body').toggleClass('collapse');
},
menuRight() {
drawer.open({
area: '600px',
url: './static/tpl/theme.html',
hideOnClose: true,
id: 'drawer-theme-tpl',
shade: 0.01,
});
},
});
element.on('nav(nav-side)', function (elem) {
var path = elem.data('path');
if (path) {
routerTo({path});
if ($(window).width() <= 768) {
$('body').toggleClass('collapse', false);
}
}
});
$('#layuiv').text(layui.v);
/*
* 后台通用脚本
* 说明:统一处理全局的退出登录逻辑
*/
// 绑定退出登录按钮事件
const bindLogout = () => {
const btn = document.getElementById('logout-btn');
if (!btn) return;
btn.addEventListener('click', (e) => {
e.preventDefault();
handleLogout();
});
};
// 执行退出登录
const handleLogout = () => {
layer.confirm('确定要退出登录吗?', {
icon: 3,
title: '提示'
}, (index) => {
layer.close(index);
const loadIndex = layer.load(2, {
content: '正在退出登录...'
});
fetchWithCSRF('/admin/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
const ok = data && (data.code === 0 || data.success);
const msg = (data && (data.msg || data.message)) || (ok ? '退出登录成功' : '退出登录失败');
if (ok) {
layer.msg(msg, {
icon: 1,
time: 1000
}, () => {
const redirect = (data && data.data && data.data.redirect) || '/admin/login';
window.location.href = redirect;
});
} else {
layer.msg(msg, { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
console.error('登出请求失败:', error);
layer.msg('网络错误,请重试', { icon: 2 });
});
});
};
(() => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindLogout);
} else {
bindLogout();
}
})();
// 刷新页面功能处理
const handleRefresh = () => {
layer.confirm('确定要刷新内容吗?', {
icon: 3,
title: '提示'
}, (index) => {
layer.close(index);
let currentPath = window.location.hash.replace('#', '') || 'dashboard';
const loadIndex = layer.load(2, {
content: '正在刷新...'
});
setTimeout(() => {
routerTo({ path: currentPath });
layer.close(loadIndex);
}, 500);
});
};
$('#refresh-btn').on('click', handleRefresh);
// 统一的Tips提示功能
$(document).off('click', '[data-tips]').on('click', '[data-tips]', function() {
var tipsType = $(this).data('tips');
var tipsContent = getTipsContent(tipsType);
layer.tips(tipsContent, this, {
tips: [2, '#16b777'],
time: 3000
});
});
function getTipsContent(type) {
var tips = {
'user-username': '用户名:用于登录的用户名,可以修改但需要保证唯一性',
'user-old-password': '旧密码:修改密码时需要输入当前密码进行验证,不修改密码时可留空',
'user-new-password': '新密码要设置的新密码长度至少6位不修改密码时可留空',
'site-title': '站点标题:网站的主标题,显示在浏览器标题栏和搜索引擎结果中',
'site-keywords': '关键词网站的SEO关键词用于搜索引擎优化多个关键词用逗号分隔',
'site-description': '站点描述网站的简要描述用于SEO和搜索引擎结果展示',
'site-logo': '站点Logo网站的标志图片路径建议使用SVG格式',
'maintenance-mode': '维护模式:开启后网站将进入维护模式,普通用户无法访问',
'default-user-role': '默认角色新注册用户的默认权限级别0为管理员1为普通成员',
'session-timeout': '会话超时:用户登录会话的有效时间,单位为秒,超时后需要重新登录',
'footer-text': '页脚文本:显示在网站底部的版权信息或其他文本',
'icp-record': 'ICP备案网站的ICP备案号中国大陆网站必须显示',
'icp-record-link': 'ICP备案链接ICP备案号对应的查询链接通常指向工信部备案网站',
'psb-record': '公安备案:网站的公安备案号,部分地区要求显示',
'psb-record-link': '公安备案链接:公安备案号对应的查询链接,通常指向公安部备案网站',
'app-name': '应用名称:设置应用的显示名称,用户在客户端看到的应用标识',
'app-version': '应用版本:当前应用的版本号,用于版本控制和更新检测',
'app-status': '应用状态:控制应用是否可用,禁用后用户无法使用该应用',
'force-update': '强制更新:开启后用户必须更新到最新版本才能使用',
'download-type': '更新方式:设置应用的更新下载方式,支持不同的分发渠道',
'download-url': '下载地址:应用安装包的下载链接地址',
'login-type': '登录方式:设置用户登录验证的方式,如账号密码、卡密等',
'multi-open-scope': '多开范围:设置多开功能的作用范围,如全局或特定应用',
'clean-interval': '清理间隔:系统自动清理无效会话的时间间隔(分钟)',
'check-interval': '校验间隔:系统检查用户状态的时间间隔(分钟)',
'multi-open-count': '多开数量:允许用户同时运行的应用实例数量',
'machine-verify': '机器码验证:控制是否启用机器码验证功能,用于限制软件在特定设备上运行',
'machine-rebind': '机器码重绑:允许用户重新绑定机器码,当设备更换或重装系统时使用',
'machine-rebind-limit': '重绑限制:设置重绑的时间限制,每天表示每天可重绑,永久表示不限制重绑时间',
'machine-free-count': '免费次数:用户可以免费重绑机器码的次数',
'machine-rebind-count': '重绑次数:用户总共可以重绑机器码的次数限制',
'machine-rebind-deduct': '重绑扣除:每次重绑机器码时扣除的时间(分钟)',
'ip-verify': 'IP地址验证控制是否启用IP地址验证关闭/开启/开启(市)/开启(省)分别对应不同的验证级别',
'ip-rebind': 'IP地址重绑允许用户重新绑定IP地址当网络环境变化时使用',
'ip-rebind-limit': '重绑限制设置IP重绑的时间限制每天表示每天可重绑永久表示不限制重绑时间',
'ip-free-count': '免费次数用户可以免费重绑IP地址的次数',
'ip-rebind-count': '重绑次数用户总共可以重绑IP地址的次数限制',
'ip-rebind-deduct': '重绑扣除每次重绑IP地址时扣除的时间分钟',
'register-enabled': '账号注册:控制是否允许新用户注册账号',
'register-limit': '注册限制:设置注册的限制规则,如时间限制等',
'register-limit-time': '限制时间:注册限制的时间周期,每天或永久',
'register-count': '注册次数:在限制时间内允许注册的账号数量',
'trial-enabled': '领取试用:控制是否允许用户领取试用时间',
'trial-limit-time': '限制时间:试用领取的时间限制周期',
'trial-time': '试用时间:用户可以领取的试用时长(分钟)',
'submit-algorithm': '提交算法:客户端向服务器提交数据时使用的加密算法<br/>• 不加密:数据明文传输,适用于内网环境<br/>• RC4对称加密速度快适用于一般场景<br/>• RSA非对称加密安全性高适用于敏感数据<br/>• RSA动态动态生成密钥的RSA加密安全性最高<br/>• 易加密自定义对称加密算法使用15-30位整数密钥数组',
'submit-keys': '提交密钥:用于加密客户端提交数据的密钥<br/>• RC416位十六进制密钥用于对称加密<br/>• RSA公钥用于客户端加密私钥用于服务器解密<br/>• 易加密15-30位整数数组逗号分隔<br/>• 密钥由系统自动生成,确保安全性',
'return-algorithm': '返回算法:服务器向客户端返回数据时使用的加密算法<br/>• 不加密:数据明文传输,适用于内网环境<br/>• RC4对称加密速度快适用于一般场景<br/>• RSA非对称加密安全性高适用于敏感数据<br/>• RSA动态动态生成密钥的RSA加密安全性最高<br/>• 易加密自定义对称加密算法使用15-30位整数密钥数组',
'return-keys': '返回密钥:用于加密服务器返回数据的密钥<br/>• RC416位十六进制密钥用于对称加密<br/>• RSA公钥用于服务器加密私钥用于客户端解密<br/>• 易加密15-30位整数数组逗号分隔<br/>• 密钥由系统自动生成,确保安全性',
'api-status': '接口状态控制当前API接口是否可用<br/>• 启用:接口正常工作,客户端可以调用<br/>• 禁用:接口暂停服务,客户端调用将返回错误',
'variable-alias': '变量别名:变量的唯一标识符,必须以英文字母开头,只能包含数字和英文字母,用于在代码中引用该变量',
'variable-app': '关联应用:选择变量所属的应用,选择"全局变量"表示该变量可在所有应用中使用',
'variable-data': '变量数据存储的具体数据内容可以是文本、数字、JSON等格式根据实际需要填写',
'variable-remark': '备注:对该变量的说明和描述,帮助理解变量的用途和使用场景,可选填写',
'function-alias': '函数别名:函数的唯一标识符,必须以英文字母开头,只能包含数字和英文字母,用于在代码中调用该函数',
'function-app': '关联应用:选择函数所属的应用,选择"全局函数"表示该函数可在所有应用中使用',
'function-code': '函数代码存储的JavaScript代码内容使用Goja引擎执行支持ES5语法和部分ES6特性',
'function-remark': '备注:对该函数的说明和描述,帮助理解函数的功能和使用场景,可选填写'
};
return tips[type] || '暂无说明';
}
function routerTo({
elem = '#router-view',
path = 'dashboard',
prefix = '/admin/', //路由前缀
suffix = '', //路由后缀
} = {}) {
var routerView = $(elem);
var url = prefix + path + suffix;
var loadTimer = setTimeout(() => {
layer.load(2);
}, 100);
history.replaceState({}, '', `#${path}`);
routerView.attr('src', url)
routerView.off('load').on('load',function(){
element.render();
form.render();
clearTimeout(loadTimer);
layer.closeLast('loading');
})
// 选中, 展开菜单
$('#ws-nav-side')
.find("[data-path='" + path + "']")
.parent('dd')
.addClass('layui-this')
.closest('.layui-nav-item')
.addClass('layui-nav-itemed');
}
});
});
function rippleViewTransition(isDark, callback) {
// 移植自 https://github.com/vuejs/vitepress/pull/2347
// 支持 Chrome 111+
// 兼容 jQuery 3 下隐式 event 全局对象不可用的问题
if (!window.event) {
window.event = new MouseEvent('click', {
clientX: document.documentElement.clientWidth,
clientY: 60,
});
}
const x = event.clientX;
const y = event.clientY;
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
const transition = document.startViewTransition(function () {
callback && callback();
});
transition.ready.then(function () {
var clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
document.documentElement.animate(
{
clipPath: isDark ? clipPath : [...clipPath].reverse(),
},
{
duration: 300,
easing: 'ease-in',
pseudoElement: isDark ? '::view-transition-new(root)' : '::view-transition-old(root)',
}
);
});
}
function addStyle(id, cssStr) {
const el = document.getElementById(id) || document.createElement('style');
if (!el.isConnected) {
el.type = 'text/css';
el.id = id;
document.head.appendChild(el);
}
el.textContent = cssStr;
}
function addLink(opt) {
return new Promise((resolve) => {
const link = Object.assign(document.createElement('link'), {
rel: 'stylesheet',
onload: () => resolve({ ...opt, status: 'success' }),
onerror: () => resolve({ ...opt, status: 'error' }), // 为了在 Promise.all 的使用场景
...opt,
});
document.head.appendChild(link);
});
}
function loadScript(url, callback) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = 'async';
script.src = url;
document.body.appendChild(script);
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState == 'complete' || script.readyState == 'loaded') {
script.onreadystatechange = null;
callback && callback();
}
};
} else {
script.onload = function () {
callback && callback();
};
}
}

View File

@@ -1,83 +0,0 @@
# ColorMode 模块WIP
开箱即用的主题切换(深色/浅色/自定义)模块,具有自动数据持久性。
**基本使用**
```js
layui.use(['colorMode'], function () {
var colorMode = layui.colorMode
var theme = colorMode.init()
}
);
```
**配置**
模块仅处理 DOM 属性更改,以便在 CSS 中应用正确的选择器,不会处理实际的样式,主题或 CSS。
默认情况下,使用 auto 模式(与用户的浏览器首选项匹配),将类 dark 应用于 html 标签时启用深色模式,返回一个对象,用来获取和改变主题。
```js
var theme = colorMode.init()
theme.mode() // 'dark' | 'light'
theme.setMode('dark') // 设置为深色模式并持久化到 localstorage
theme.setMode('auto') // 设置为 auto 模式
```
也可以自定义以使其适用于大多数场景
```js
var theme = colorMode.init({
selector: 'body',
attribute: 'theme-mode',
initialValue: 'light',
modes: {
auto: '',
light: 'light',
dark: 'dark',
contrast: 'dark contrast',
},
storage: localStorage,
storageKey: 'xxx-theme-mode',
disableTransition: true,
})
```
如果上述配置仍不能满足您的需求,可以使用 onChanged 选项完全控制处理更新的方式
```js
var theme = colorMode.init({
onChanged: function(mode, defaultHandler){
// 自定义更新方式
}
})
```
**API**
```ts
/**
* @typedef {object} initOptions
* @prop {string} [selector='html'] - 应用于目标元素的 CSS 选择器
* @prop {string} [attribute='class'] - 应用于目标元素的 HTML 属性
* @prop {string} [initialValue='auto'] - 初始颜色模式
* @prop {Object.<string, string>} [modes]- 颜色模式。value 为添加到 HTML 属性上的值
* @prop {(mode: string, defaultHandler: () => void) => void} [onChanged] - 用于处理更新的自定义处理程序指定时默认行为将被覆盖。mode 为颜色模式defaultHandler 为默认处理程序
* @prop {Storage} [storage=localStorage] - 将数据持久化到 localStorage/sessionStorage 的键。传递 `null` 以禁用持久性
* @prop {string | null} [storageKey='color-scheme'] - 持久化使用的 key
* @prop {boolean} [disableTransition=true] - 禁用切换时的过渡 {@link https://paco.me/writing/disable-theme-transitions}
*
*/
/**
*
* @param {initOptions} options
* @returns {{ mode: () => string; setMode: (mode: string) => void;}}
*/
colorMode.init(options)
```

View File

@@ -1,191 +0,0 @@
/**
* WIP
* 移植自 https://github.com/vueuse/vueuse/tree/main/packages/core/useColorMode
*/
// @ts-ignore
layui.define(['jquery'], function (exports) {
'use strict';
/** @type {jQuery}*/
var $ = layui.jquery;
var MOD_NAME = 'colorMode';
var defaultWindow = window;
var document = defaultWindow.document;
var colorMode = {
/**
* @typedef {object} initOptions
* @prop {string} [selector="html"] - 应用于目标元素的 CSS 选择器
* @prop {string} [attribute="class"] - 应用于目标元素的 HTML 属性
* @prop {string} [initialValue='auto'] - 初始颜色模式
* @prop {Object.<string, string>} [modes]- 颜色模式。value 为添加到 HTML 属性上的值
* @prop {(mode: string, defaultHandler: (window?: Window) => void) => void} [onChanged] - 用于处理更新的自定义处理程序,指定时,默认行为将被覆盖。
* @prop {Storage} [storage=localStorage] - 将数据持久化到 localStorage/sessionStorage 的键。传递 `null` 以禁用持久性
* @prop {string | null} [storageKey='color-scheme'] - 持久化使用的 key
* @prop {boolean} [disableTransition=true] - 禁用切换时的过渡 {@link https://paco.me/writing/disable-theme-transitions}
*
*/
/**
*
* @param {initOptions} options
* @returns {{mode: () => string; setMode: (mode: string, window?: Window) => void; }}
*/
init: function (options) {
var defaults = {
selector: 'html',
attribute: 'class',
initialValue: 'auto',
modes: {
auto: '',
light: 'light',
dark: 'dark',
},
storage: localStorage,
storageKey: 'color-scheme',
disableTransition: true,
};
var opts = $.extend(true, {}, defaults, options);
// 当前颜色模式
var state;
// 系统颜色模式
var system;
// 初始化 storage
var store =
opts.storageKey == null
? opts.initialValue
: (function () {
var v = opts.storage.getItem(opts.storageKey);
if (!v) {
opts.storage.setItem(opts.storageKey, opts.initialValue);
return opts.initialValue;
}
return v;
})();
/**
* 更新 HTML 属性值
* @param {String} selector
* @param {String} attribute
* @param {String} value
* @param {Window} win
*/
var updateHTMLAttrs = function (selector, attribute, value, win) {
win = win || defaultWindow;
var document = win.document;
var el = typeof selector === 'string' ? document.querySelector(selector) : undefined;
if (!el) return;
/**@type HTMLStyleElement */
var style;
if (opts.disableTransition) {
style = document.createElement('style');
style.appendChild(
document.createTextNode(
'*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}'
)
);
document.head.appendChild(style);
}
if (attribute === 'class') {
var current = value.split(/\s/g);
$.each(opts.modes, function (_, modeval) {
$.each((modeval || '').split(/\s/g), function (_, v) {
if (!v) return;
if (current.indexOf(v) !== -1) {
el.classList.add(v);
} else {
el.classList.remove(v);
}
});
});
} else {
el.setAttribute(attribute, value);
}
if (opts.disableTransition) {
// 调用 getComputedStyle 强制浏览器重绘
// @ts-expect-error 未使用的变量
var _ = window.getComputedStyle(style).opacity;
document.head.removeChild(style);
}
};
/**
* 更新状态
* @param {String} mode - 颜色模式
*/
var updateState = function (mode) {
store = opts.storageKey == null ? mode : opts.storage.getItem(opts.storageKey);
state = store === 'auto' ? system : store;
};
var prefersColorScheme = function () {
var isSupported = window && 'matchMedia' in window && typeof window.matchMedia === 'function';
if (!isSupported) {
system = 'light';
onChanged(system);
return;
}
var darkThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
var update = function () {
var preferredDark = darkThemeMediaQuery.matches;
system = preferredDark ? 'dark' : 'light';
onChanged(system);
};
update();
if ('addEventListener' in darkThemeMediaQuery) {
darkThemeMediaQuery.addEventListener('change', update);
} else {
// @ts-ignore 已弃用
darkThemeMediaQuery.addListener(update);
}
};
prefersColorScheme();
function defaultOnChanged(win) {
updateHTMLAttrs(opts.selector, opts.attribute, opts.modes[state], win);
}
function onChanged(mode, win) {
updateState(mode);
if (opts.onChanged) {
opts.onChanged(state, defaultOnChanged);
} else {
defaultOnChanged(win);
}
}
return {
setMode: function (mode, win) {
if (opts.storageKey) {
opts.storage.setItem(opts.storageKey, mode);
}
onChanged(mode, win);
},
mode: function () {
return state;
},
};
},
addStyle: function (id, cssStr) {
var el = /** @type {HTMLStyleElement} */ (document.getElementById(id) || document.createElement('style'));
if (!el.isConnected) {
el.type = 'text/css';
el.id = id;
document.head.appendChild(el);
}
el.textContent = cssStr;
},
};
exports(MOD_NAME, colorMode);
});

View File

@@ -1,317 +0,0 @@
.layer-drawer.layui-layer {
border-radius: 0 !important;
overflow: auto;
}
.layer-drawer.layui-layer.position-absolute {
position: absolute !important;
}
.layer-drawer-anim,
.layer-drawer-anim.layui-anim {
-webkit-animation-duration: .3s;
animation-duration: .3s;
-webkit-animation-timing-function: cubic-bezier(0.7, 0.3, 0.1, 1);
animation-timing-function: cubic-bezier(0.7, 0.3, 0.1, 1);
}
/* right to left */
@keyframes layer-rl {
from {
-webkit-transform: translate3d(100%, 0, 0);
-ms-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
opacity: 1;
}
to {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@-webkit-keyframes layer-rl {
from {
-webkit-transform: translate3d(100%, 0, 0);
-ms-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
opacity: 1;
}
to {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
.layer-anim-rl {
-webkit-animation-name: layer-rl;
animation-name: layer-rl;
}
/* right to left close */
@keyframes layer-rl-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(100%, 0, 0);
-ms-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
@-webkit-keyframes layer-rl-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(100%, 0, 0);
-ms-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
.layer-anim-rl-close,
.layer-anim-rl.layer-anim-close {
-webkit-animation-name: layer-rl-close;
animation-name: layer-rl-close;
}
/* left to right */
@-webkit-keyframes layer-lr {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1
}
to {
-webkit-transform: translate3d(-100%, 0, 0);
-ms-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
opacity: 1
}
}
@keyframes layer-lr {
from {
-webkit-transform: translate3d(-100%, 0, 0);
-ms-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
opacity: 1
}
to {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1
}
}
.layer-anim-lr {
-webkit-animation-name: layer-lr;
animation-name: layer-lr
}
/* left to right close */
@-webkit-keyframes layer-lr-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(-100%, 0, 0);
-ms-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
@keyframes layer-lr-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(-100%, 0, 0);
-ms-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
.layer-anim-lr-close,
.layer-anim-lr.layer-anim-close {
-webkit-animation-name: layer-lr-close;
animation-name: layer-lr-close
}
/* top to bottom */
@-webkit-keyframes layer-tb {
from {
-webkit-transform: translate3d(0, -100%, 0);
-ms-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
opacity: 1;
animation-timing-function: cubic-bezier(0.7, 0.3, 0.1, 1);
}
to {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1;
animation-timing-function: cubic-bezier(0.7, 0.3, 0.1, 1);
}
}
@keyframes layer-tb {
from {
-webkit-transform: translate3d(0, -100%, 0);
-ms-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
opacity: 1;
animation-timing-function: cubic-bezier(0.7, 0.3, 0.1, 1);
}
to {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1;
animation-timing-function: cubic-bezier(0.7, 0.3, 0.1, 1);
}
}
.layer-anim-tb {
-webkit-animation-name: layer-tb;
animation-name: layer-tb
}
/* top to bottom close */
@-webkit-keyframes layer-tb-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(0, -100%, 0);
-ms-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
}
@keyframes layer-tb-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(0, -100%, 0);
-ms-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
}
.layer-anim-tb-close,
.layer-anim-tb.layer-anim-close {
-webkit-animation-name: layer-tb-close;
animation-name: layer-tb-close
}
/* bottom to top */
@-webkit-keyframes layer-bt {
from {
-webkit-transform: translate3d(0, 100%, 0);
-ms-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
opacity: 1
}
to {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1
}
}
@keyframes layer-bt {
from {
-webkit-transform: translate3d(0, 100%, 0);
-ms-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
opacity: 1
}
to {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1
}
}
.layer-anim-bt {
-webkit-animation-name: layer-bt;
animation-name: layer-bt
}
/* bottom to top close */
@-webkit-keyframes layer-bt-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(0, 100%, 0);
-ms-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
}
@keyframes layer-bt-close {
from {
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
to {
-webkit-transform: translate3d(0, 100%, 0);
-ms-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
}
.layer-anim-bt-close,
.layer-anim-bt.layer-anim-close {
-webkit-animation-name: layer-bt-close;
animation-name: layer-bt-close
}

View File

@@ -1,200 +0,0 @@
/**
* 抽屉模块
*/
layui.define(['jquery', 'layer'], function (exports) {
('use strict');
var MOD_NAME = 'drawer';
var $ = layui.jquery;
var layer = layui.layer;
layui.link(layui.cache.base + 'drawer/drawer.css');
var drawer = new (function () {
this.open = function (option) {
return layerDrawer(option);
};
this.title = layer.title;
this.style = layer.style;
this.close = layer.close;
this.closeAll = layer.closeAll;
})();
/**
*
* 封装 layer.open
*
* @param {object} option, `type`, `anim`, `move`, `fixed`, `skin`,`maxWidth`, `maxHeight`, `moveOut`, `moveEnd` 不可用,其它参数和 layer.open 一致, 新增 `iframe`和 `url`参数
* @returns {number} 原生 layer 的 index
*/
function layerDrawer(option) {
var opt = normalizeOption(option);
if (opt.target) appendToTarget(opt);
if (opt.url) loadFragment(opt);
if (opt.shade) {
$('<style/>')
.attr('id', 'layer-drawer')
.html('.layui-layer-shade{opacity: 0;transition: opacity .35s cubic-bezier(0.34, 0.69, 0.1, 1);}') // fadeIn
.appendTo('head');
option.end = Aspect(option.end, undefined, function (layero, index) {
$('#layer-drawer').remove();
});
}
return layer.open(opt);
}
/**
* 加载 HTML 片段到 layer content
* @param {object} option 设置选项
*/
function loadFragment(option) {
option.success = Aspect(option.success, function (layero) {
$.ajax({
url: option.url,
dataType: 'html',
success: function (result) {
layero.children('.layui-layer-content').html(result);
},
});
});
}
/**
*将 layer 附加到指定节点
* @param {object} opt 设置选项
*/
function appendToTarget(opt) {
var targetDOM = $(opt.target);
var contentDOM = opt.content;
contentDOM.appendTo(targetDOM);
opt.skin = getDrawerAnimationClass(opt.offset, true);
opt.offset = calcOffset(opt.offset, opt.area, targetDOM);
// 处理关闭后偶现 DOM 仍显示的问题
opt.end = Aspect(opt.end, function () {
contentDOM.css('display', 'none');
});
if (opt.shade) {
opt.success = Aspect(opt.success, function (layero, index) {
var shadeDOM = $('#layui-layer-shade' + index);
shadeDOM.css('position', 'absolute');
shadeDOM.appendTo(layero.parent());
});
}
}
/**
* 规范化 layer.open 选项
* @param {object} option layer.open 的选项
* @returns 规范化后的选项
*/
function normalizeOption(option) {
option.type = option.iframe ? 2 : 1;
option.anim = -1;
option.move = false;
option.fixed = true;
option.content = option.iframe ? option.iframe : option.content;
if (option.offset === undefined) option.offset = 'r';
option.area = calcDrawerArea(option.offset, option.area);
option.skin = getDrawerAnimationClass(option.offset);
if (option.title === undefined) option.title = false;
if (option.closeBtn === undefined) option.closeBtn = false;
if (option.shade === undefined) option.shade = 0.3;
if (option.shadeClose === undefined) option.shadeClose = true;
if (option.resize === undefined) option.resize = false;
if (option.success === undefined) option.success = function () {}; // 处理遮罩需要
if (option.end === undefined) option.end = function () {};
return option;
}
/**
* 计算抽屉宽高
* @param {string} offset 抽屉方向 l = 左, r = 右, t = 上, b = 下
* @param {string[] | string} drawerArea 抽屉大小,字符串数组格式[width, height]["200px","100%"],字符串格式:"30%" "200px"。
* @returns{string[]} 抽屉宽高数组
*/
function calcDrawerArea(offset, drawerArea) {
if (drawerArea instanceof Array) {
return drawerArea;
}
drawerArea = drawerArea === undefined || drawerArea === 'auto' ? '30%' : drawerArea;
if (offset === 'l' || offset === 'r') {
return [drawerArea, '100%'];
} else if (offset === 't' || offset === 'b') {
return ['100%', drawerArea];
}
}
/**
* 获取抽屉动画类,指定挂载容器时需要设置绝对定位
* @param {string} offset 抽屉方向 l = 左, r = 右, t = 上, b = 下
* @param {boolean} [isAbsolute] 是否是绝对定位
* @returns {string} 抽屉入场动画类
*/
function getDrawerAnimationClass(offset, isAbsolute) {
var prefixClass = 'layer-drawer layer-drawer-anim layui-anim layer-anim-';
var suffix = 'rl';
if (offset === 'l') {
suffix = 'lr';
} else if (offset === 'r') {
suffix = 'rl';
} else if (offset === 't') {
suffix = 'tb';
} else if (offset === 'b') {
suffix = 'bt';
}
return prefixClass + suffix + (isAbsolute ? ' position-absolute ' : '');
}
/**
* 指定挂载容器重新计算 offset
*
* layer 源码中使用窗口宽高计算位置,所以此
* @param {string} offset 位置
* @param {string | string[]} area 范围大小
* @param {*} targetEl 挂载节点
* @returns 包含抽屉位置信息的数组,[top, left]
*/
function calcOffset(offset, area, targetEl) {
// https://gitee.com/layui/layui/blob/main/src/modules/layer.js#L560
if (offset === undefined || offset === 'l' || offset === 't') {
offset = 'lt';
} else if (offset === 'r') {
// https://gitee.com/layui/layui/blob/main/src/modules/layer.js#L554
area = area instanceof Array ? area[0] : area;
var left = /%$/.test(area)
? targetEl.innerWidth() * (1 - window.parseFloat(area) / 100)
: targetEl.innerWidth() - window.parseFloat(area);
offset = ['0', left];
} else if (offset === 'b') {
area = area instanceof Array ? area[1] : area;
var top = /%$/.test(area)
? targetEl.innerHeight() * (1 - window.parseFloat(area) / 100)
: targetEl.innerHeight() - window.parseFloat(area);
offset = [top, '0'];
}
return offset;
}
/**
* 简易的切面
* @param {Function} target 被通知的对象,原函数
* @param {Function | undefined} [before] 前置通知
* @param {Function | undefined} [after] 后置通知
* @returns 代理函数
*/
function Aspect(target, before, after) {
function proxyFunc() {
if (before && typeof before === 'function') {
before.apply(this, arguments);
}
target.apply(this, arguments);
if (after && typeof after === 'function') {
after.apply(this, arguments);
}
}
return proxyFunc;
}
exports(MOD_NAME, drawer);
});

File diff suppressed because one or more lines are too long

View File

@@ -1,142 +0,0 @@
/**
* @typedef {object} IncludeFile
*
* @prop {boolean} ok
* @prop {number} status
* @prop {string} html
*/
/** @type {Map<string,IncludeFile | Promise<IncludeFile>>} */
const includeFiles = new Map();
/**
*
* @param {string} src
* @param {'cors' | 'no-cors' | 'same-origin'} [mode='cors']
*
* @returns {Promise<IncludeFile>}
*/
export function requestInclude(src, mode = 'cors'){
const prev = includeFiles.get(src);
if (prev !== undefined) {
return Promise.resolve(prev);
}
const fileDataPromise = fetch(src, { mode: mode }).then(async response => {
const res = {
ok: response.ok,
status: response.status,
html: await response.text()
};
includeFiles.set(src, res);
return res;
});
includeFiles.set(src, fileDataPromise);
return fileDataPromise;
}
class HtmlImport extends HTMLElement {
constructor () {
super();
}
static get observedAttributes () {
return ['src', 'mode', 'allow-scripts'];
}
get src() {
return this.getAttribute('src') || '';
}
set src(value) {
this.setAttribute('src', value);
}
get mode() {
return this.getAttribute('mode') || 'cors';
}
set mode(value) {
this.setAttribute('mode', value);
}
get allowScripts() {
return this.hasAttribute('allow-scripts');
}
set allowScripts(value) {
this.toggleAttribute('allow-scripts', value);
}
/**
* 执行 innerHTML 中的 <script></script>
* @param {HTMLScriptElement} scripts
*/
async executeScript(scripts) {
const execQueue = function (script) {
const newScript = document.createElement('script');
[...script.attributes].forEach(attr => newScript.setAttribute(attr.name, attr.value));
newScript.textContent = script.textContent;
script.parentNode && script.parentNode.replaceChild(newScript, script);
return script.src ? new Promise((resolve) => {
newScript.async = false;
newScript.addEventListener('load', e => resolve(e));
newScript.addEventListener('error', e => resolve(e));
}) : Promise.resolve();
};
// 按 <script> 顺序执行,确保上下文关联
for (const script of scripts) {
await execQueue(script);
// console.log(`${script.src||script} loaded`, Date.now());
}
}
async handleSrcChange() {
try {
const src = this.src;
const file = await requestInclude(src, this.mode);
if (src !== this.src) {
return;
}
if (!file.ok) {
this.emit('error', { detail: { status: file.status } });
return;
}
this.innerHTML = file.html;
if (this.allowScripts) {
await this.executeScript(this.querySelectorAll('script'));
}
this.emit('load');
} catch {
this.emit('error', { detail: { status: -1 } });
}
}
attributeChangedCallback (name) {
if (name == 'src') {
this.handleSrcChange();
}
}
emit(name, options) {
const event = new CustomEvent(name, {
bubbles: true,
cancelable: false,
composed: true,
detail: {},
...options
});
this.dispatchEvent(event);
return event;
}
}
if (!customElements.get('wc-include')) {
customElements.define('wc-include', HtmlImport);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,202 +0,0 @@
:root{
/* =====色板===== */
/*常量,不随明暗主题变化*/
--color-white: #FFFFFF;
--color-black: #000000;
--lay-color-white: #FAFAFA;
--lay-color-black: #333333;
--lay-color-red-1: #FFF1E8;
--lay-color-red-2: #FFD7C0;
--lay-color-red-3: #FFBB99;
--lay-color-red-4: #FF9C71;
--lay-color-red-5: #FF7A4A;
--lay-color-red-6: #FF5722;
--lay-color-red-7: #D23B15;
--lay-color-red-8: #A6250B;
--lay-color-red-9: #791404;
--lay-color-red-10: #4D0800;
--lay-color-blue-1: #E8F9FF;
--lay-color-blue-2: #C0ECFF;
--lay-color-blue-3: #97DCFF;
--lay-color-blue-4: #6FCAFF;
--lay-color-blue-5: #46B5FF;
--lay-color-blue-6: #1E9FFF;
--lay-color-blue-7: #1379D2;
--lay-color-blue-8: #0A58A6;
--lay-color-blue-9: #043A79;
--lay-color-blue-10: #00214D;
--lay-color-lightblue-1: #E8FDFF;
--lay-color-lightblue-2: #C1F4FB;
--lay-color-lightblue-3: #9CEAF7;
--lay-color-lightblue-4: #77DDF4;
--lay-color-lightblue-5: #53CEF0;
--lay-color-lightblue-6: #31BDEC;
--lay-color-lightblue-7: #1F95C4;
--lay-color-lightblue-8: #10709C;
--lay-color-lightblue-9: #064E74;
--lay-color-lightblue-10: #002F4D;
--lay-color-layuigreen-1: #E8FFF9;
--lay-color-layuigreen-2: #B5F1E3;
--lay-color-layuigreen-3: #87E3D1;
--lay-color-layuigreen-4: #5DD6C1;
--lay-color-layuigreen-5: #37C8B5;
--lay-color-layuigreen-6: #16BAAA;
--lay-color-layuigreen-7: #0E9F95;
--lay-color-layuigreen-8: #08837F;
--lay-color-layuigreen-9: #036868;
--lay-color-layuigreen-10: #004A4D;
--lay-color-green-1: #E8FFF2;
--lay-color-green-2: #B5F1D1;
--lay-color-green-3: #86E2B4;
--lay-color-green-4: #5CD49C;
--lay-color-green-5: #37C588;
--lay-color-green-6: #16B777;
--lay-color-green-7: #0E9C68;
--lay-color-green-8: #088259;
--lay-color-green-9: #036749;
--lay-color-green-10: #004D38;
--lay-color-orange-1: #FFFCE8;
--lay-color-orange-2: #FFF5BA;
--lay-color-orange-3: #FFEA8B;
--lay-color-orange-4: #FFDC5D;
--lay-color-orange-5: #FFCB2E;
--lay-color-orange-6: #FFB800;
--lay-color-orange-7: #D29000;
--lay-color-orange-8: #A66C00;
--lay-color-orange-9: #794B00;
--lay-color-orange-10: #4D2D00;
--lay-color-cyan-1: #E8F6FF;
--lay-color-cyan-2: #B9CEDD;
--lay-color-cyan-3: #8FA7BB;
--lay-color-cyan-4: #6A829A;
--lay-color-cyan-5: #4A5F78;
--lay-color-cyan-6: #2F4056;
--lay-color-cyan-7: #223654;
--lay-color-cyan-8: #162C51;
--lay-color-cyan-9: #0B214F;
--lay-color-cyan-10: #00174D;
--lay-color-purple-1: #FDE8FF;
--lay-color-purple-2: #EDBEF4;
--lay-color-purple-3: #DC97E8;
--lay-color-purple-4: #C972DD;
--lay-color-purple-5: #B651D1;
--lay-color-purple-6: #A233C6;
--lay-color-purple-7: #8120A8;
--lay-color-purple-8: #631289;
--lay-color-purple-9: #48076B;
--lay-color-purple-10: #2F004D;
--lay-color-black-1: #E8F8FF;
--lay-color-black-2: #BFD0D8;
--lay-color-black-3: #98A8B1;
--lay-color-black-4: #73818A;
--lay-color-black-5: #505B63;
--lay-color-black-6: #2F363C;
--lay-color-black-7: #23303C;
--lay-color-black-8: #18293C;
--lay-color-black-9: #0C213C;
--lay-color-black-10: #00183C;
--lay-color-gray-1: #FAFAFA;
--lay-color-gray-2: #F6F6F6;
--lay-color-gray-3: #EEEEEE;
--lay-color-gray-4: #E2E2E2;
--lay-color-gray-5: #DDDDDD;
--lay-color-gray-6: #D2D2D2;
--lay-color-gray-7: #CCCCCC;
--lay-color-gray-8: #C2C2C2;
--lay-color-gray-9: #AAAAAA;
--lay-color-gray-10: #939393;
--lay-color-gray-11: #858585;
--lay-color-gray-12: #7b7b7b;
--lay-color-gray-13: #686868;
/* =====语义===== */
/* 主色 */
--lay-color-primary: var(--lay-color-layuigreen-6);
--lay-color-primary-hover: var(--lay-color-layuigreen-5);
--lay-color-primary-active: var(--lay-color-layuigreen-7);
--lay-color-primary-disabled: var(--lay-color-layuigreen-3);
--lay-color-primary-light: var(--lay-color-layuigreen-4);
/* 次色 */
--lay-color-secondary: var(--lay-color-green-6);
--lay-color-secondary-hover: var(--lay-color-green-5);
--lay-color-secondary-active: var(--lay-color-green-7);
--lay-color-secondary-disabled: var(--lay-color-green-3);
--lay-color-secondary-light: var(--lay-color-green-4);
/* 引导 */
--lay-color-info: var(--lay-color-lightblue-6);
--lay-color-info-hover: var(--lay-color-lightblue-5);
--lay-color-info-active: var(--lay-color-lightblue-7);
--lay-color-info-disabled: var(--lay-color-lightblue-3);
--lay-color-info-light: var(--lay-color-lightblue-4);
/* 百搭 */
--lay-color-normal: var(--lay-color-blue-6);
--lay-color-normal-hover: var(--lay-color-blue-5);
--lay-color-normal-active: var(--lay-color-blue-7);
--lay-color-normal-disabled: var(--lay-color-blue-3);
--lay-color-normal-light: var(--lay-color-blue-4);
/* 警示 */
--lay-color-warning: var(--lay-color-orange-6);
--lay-color-warning-hover: var(--lay-color-orange-5);
--lay-color-warning-active: var(--lay-color-orange-7);
--lay-color-warning-disabled: var(--lay-color-orange-3);
--lay-color-warning-light: var(--lay-color-orange-4);
/* 成功 */
--lay-color-success: var(--lay-color-green-6);
--lay-color-success-hover: var(--lay-color-green-5);
--lay-color-success-active: var(--lay-color-green-7);
--lay-color-success-disabled: var(--lay-color-green-3);
--lay-color-success-light: var(--lay-color-green-4);
/* 错误 */
--lay-color-danger: var(--lay-color-red-6);
--lay-color-danger-hover: var(--lay-color-red-5);
--lay-color-danger-active: var(--lay-color-red-7);
--lay-color-danger-disabled: var(--lay-color-red-3);
--lay-color-danger-light: var(--lay-color-red-4);
--lay-color-bg-1: #17171A; /*整体背景*/
--lay-color-bg-2: #232324; /*一级容器背景,卡片,面板*/
--lay-color-bg-3: #2a2a2b; /*二级容器背景*/
--lay-color-bg-4: #313132; /*三级容器背景*/
--lay-color-bg-5: #373739; /*下拉弹出框、Tooltip 背景颜色*/
--lay-color-bg-white: #f6f6f6; /*白色背景*/
--lay-color-text-1: rgba(255,255,255,.9); /*强调/正文标题*/
--lay-color-text-2: rgba(255,255,255,.7); /*次强调/语句*/
--lay-color-text-3: rgba(255,255,255,.5); /*次要信息*/
--lay-color-text-4: rgba(255,255,255,.3);/*禁用状态文字 */
--lay-color-border-1: #2e2e30;
--lay-color-border-2: #484849;
--lay-color-border-3: #5f5f60;
--lay-color-border-4: #929293;
--lay-color-fill-1: rgba(255,255,255,.04);/*浅/禁用*/
--lay-color-fill-2: rgba(255,255,255,.08);/*常规/白底悬浮*/
--lay-color-fill-3: rgba(255,255,255,.12); /*深/灰底悬浮*/
--lay-color-fill-4: rgba(255,255,255,.16);/*重/特殊场景*/
--lay-color-hover: var(--lay-color-fill-3); /*bg*/
--lay-color-active: var(--lay-color-fill-3); /*bg*/
--lay-shadow-1: 0 4px 6px rgba(0, 0, 0, 6%), 0 1px 10px rgba(0, 0, 0, 8%), 0 2px 4px rgba(0, 0, 0, 12%);/*基础/下层投影 卡片面板*/
--lay-shadow-2: 0 8px 10px rgba(0, 0, 0, 12%), 0 3px 14px rgba(0, 0, 0, 10%), 0 5px 5px rgba(0, 0, 0, 16%);/*中层投影 下拉菜单,选择器*/
--lay-shadow-3: 0 16px 24px rgba(0, 0, 0, 14%), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%);/*上层投影 弹窗*/
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,738 +0,0 @@
:root{
/* =====色板===== */
/*常量,不随明暗主题变化*/
--color-white: #FFFFFF;
--color-black: #000000;
--lay-color-white: #FAFAFA;
--lay-color-black: #333333;
--lay-color-red-1: #FFF1E8;
--lay-color-red-2: #FFD7C0;
--lay-color-red-3: #FFBB99;
--lay-color-red-4: #FF9C71;
--lay-color-red-5: #FF7A4A;
--lay-color-red-6: #FF5722;
--lay-color-red-7: #D23B15;
--lay-color-red-8: #A6250B;
--lay-color-red-9: #791404;
--lay-color-red-10: #4D0800;
--lay-color-blue-1: #E8F9FF;
--lay-color-blue-2: #C0ECFF;
--lay-color-blue-3: #97DCFF;
--lay-color-blue-4: #6FCAFF;
--lay-color-blue-5: #46B5FF;
--lay-color-blue-6: #1E9FFF;
--lay-color-blue-7: #1379D2;
--lay-color-blue-8: #0A58A6;
--lay-color-blue-9: #043A79;
--lay-color-blue-10: #00214D;
--lay-color-lightblue-1: #E8FDFF;
--lay-color-lightblue-2: #C1F4FB;
--lay-color-lightblue-3: #9CEAF7;
--lay-color-lightblue-4: #77DDF4;
--lay-color-lightblue-5: #53CEF0;
--lay-color-lightblue-6: #31BDEC;
--lay-color-lightblue-7: #1F95C4;
--lay-color-lightblue-8: #10709C;
--lay-color-lightblue-9: #064E74;
--lay-color-lightblue-10: #002F4D;
--lay-color-layuigreen-1: #E8FFF9;
--lay-color-layuigreen-2: #B5F1E3;
--lay-color-layuigreen-3: #87E3D1;
--lay-color-layuigreen-4: #5DD6C1;
--lay-color-layuigreen-5: #37C8B5;
--lay-color-layuigreen-6: #16BAAA;
--lay-color-layuigreen-7: #0E9F95;
--lay-color-layuigreen-8: #08837F;
--lay-color-layuigreen-9: #036868;
--lay-color-layuigreen-10: #004A4D;
--lay-color-green-1: #E8FFF2;
--lay-color-green-2: #B5F1D1;
--lay-color-green-3: #86E2B4;
--lay-color-green-4: #5CD49C;
--lay-color-green-5: #37C588;
--lay-color-green-6: #16B777;
--lay-color-green-7: #0E9C68;
--lay-color-green-8: #088259;
--lay-color-green-9: #036749;
--lay-color-green-10: #004D38;
--lay-color-orange-1: #FFFCE8;
--lay-color-orange-2: #FFF5BA;
--lay-color-orange-3: #FFEA8B;
--lay-color-orange-4: #FFDC5D;
--lay-color-orange-5: #FFCB2E;
--lay-color-orange-6: #FFB800;
--lay-color-orange-7: #D29000;
--lay-color-orange-8: #A66C00;
--lay-color-orange-9: #794B00;
--lay-color-orange-10: #4D2D00;
--lay-color-cyan-1: #E8F6FF;
--lay-color-cyan-2: #B9CEDD;
--lay-color-cyan-3: #8FA7BB;
--lay-color-cyan-4: #6A829A;
--lay-color-cyan-5: #4A5F78;
--lay-color-cyan-6: #2F4056;
--lay-color-cyan-7: #223654;
--lay-color-cyan-8: #162C51;
--lay-color-cyan-9: #0B214F;
--lay-color-cyan-10: #00174D;
--lay-color-purple-1: #FDE8FF;
--lay-color-purple-2: #EDBEF4;
--lay-color-purple-3: #DC97E8;
--lay-color-purple-4: #C972DD;
--lay-color-purple-5: #B651D1;
--lay-color-purple-6: #A233C6;
--lay-color-purple-7: #8120A8;
--lay-color-purple-8: #631289;
--lay-color-purple-9: #48076B;
--lay-color-purple-10: #2F004D;
--lay-color-black-1: #E8F8FF;
--lay-color-black-2: #BFD0D8;
--lay-color-black-3: #98A8B1;
--lay-color-black-4: #73818A;
--lay-color-black-5: #505B63;
--lay-color-black-6: #2F363C;
--lay-color-black-7: #23303C;
--lay-color-black-8: #18293C;
--lay-color-black-9: #0C213C;
--lay-color-black-10: #00183C;
--lay-color-gray-1: #FAFAFA;
--lay-color-gray-2: #F6F6F6;
--lay-color-gray-3: #EEEEEE;
--lay-color-gray-4: #E2E2E2;
--lay-color-gray-5: #DDDDDD;
--lay-color-gray-6: #D2D2D2;
--lay-color-gray-7: #CCCCCC;
--lay-color-gray-8: #C2C2C2;
--lay-color-gray-9: #AAAAAA;
--lay-color-gray-10: #939393;
--lay-color-gray-11: #858585;
--lay-color-gray-12: #7b7b7b;
--lay-color-gray-13: #686868;
/* =====语义===== */
/* 主色 */
--lay-color-primary: var(--lay-color-layuigreen-6);
--lay-color-primary-hover: var(--lay-color-layuigreen-5);
--lay-color-primary-active: var(--lay-color-layuigreen-7);
--lay-color-primary-disabled: var(--lay-color-layuigreen-3);
--lay-color-primary-light: var(--lay-color-layuigreen-4);
/* 次色 */
--lay-color-secondary: var(--lay-color-green-6);
--lay-color-secondary-hover: var(--lay-color-green-5);
--lay-color-secondary-active: var(--lay-color-green-7);
--lay-color-secondary-disabled: var(--lay-color-green-3);
--lay-color-secondary-light: var(--lay-color-green-4);
/* 引导 */
--lay-color-info: var(--lay-color-lightblue-6);
--lay-color-info-hover: var(--lay-color-lightblue-5);
--lay-color-info-active: var(--lay-color-lightblue-7);
--lay-color-info-disabled: var(--lay-color-lightblue-3);
--lay-color-info-light: var(--lay-color-lightblue-4);
/* 百搭 */
--lay-color-normal: var(--lay-color-blue-6);
--lay-color-normal-hover: var(--lay-color-blue-5);
--lay-color-normal-active: var(--lay-color-blue-7);
--lay-color-normal-disabled: var(--lay-color-blue-3);
--lay-color-normal-light: var(--lay-color-blue-4);
/* 警示 */
--lay-color-warning: var(--lay-color-orange-6);
--lay-color-warning-hover: var(--lay-color-orange-5);
--lay-color-warning-active: var(--lay-color-orange-7);
--lay-color-warning-disabled: var(--lay-color-orange-3);
--lay-color-warning-light: var(--lay-color-orange-4);
/* 成功 */
--lay-color-success: var(--lay-color-green-6);
--lay-color-success-hover: var(--lay-color-green-5);
--lay-color-success-active: var(--lay-color-green-7);
--lay-color-success-disabled: var(--lay-color-green-3);
--lay-color-success-light: var(--lay-color-green-4);
/* 错误 */
--lay-color-danger: var(--lay-color-red-6);
--lay-color-danger-hover: var(--lay-color-red-5);
--lay-color-danger-active: var(--lay-color-red-7);
--lay-color-danger-disabled: var(--lay-color-red-3);
--lay-color-danger-light: var(--lay-color-red-4);
--lay-color-bg-1: #17171A; /*整体背景*/
--lay-color-bg-2: #232324; /*一级容器背景,卡片,面板*/
--lay-color-bg-3: #2a2a2b; /*二级容器背景*/
--lay-color-bg-4: #313132; /*三级容器背景*/
--lay-color-bg-5: #373739; /*下拉弹出框、Tooltip 背景颜色*/
--lay-color-bg-white: #f6f6f6; /*白色背景*/
--lay-color-text-1: rgba(255,255,255,.9); /*强调/正文标题*/
--lay-color-text-2: rgba(255,255,255,.7); /*次强调/语句*/
--lay-color-text-3: rgba(255,255,255,.5); /*次要信息*/
--lay-color-text-4: rgba(255,255,255,.3);/*禁用状态文字 */
--lay-color-border-1: #2e2e30;
--lay-color-border-2: #484849;
--lay-color-border-3: #5f5f60;
--lay-color-border-4: #929293;
--lay-color-fill-1: rgba(255,255,255,.04);/*浅/禁用*/
--lay-color-fill-2: rgba(255,255,255,.08);/*常规/白底悬浮*/
--lay-color-fill-3: rgba(255,255,255,.12); /*深/灰底悬浮*/
--lay-color-fill-4: rgba(255,255,255,.16);/*重/特殊场景*/
--lay-color-hover: var(--lay-color-fill-3); /*bg*/
--lay-color-active: var(--lay-color-fill-3); /*bg*/
--lay-shadow-1: 0 4px 6px rgba(0, 0, 0, 6%), 0 1px 10px rgba(0, 0, 0, 8%), 0 2px 4px rgba(0, 0, 0, 12%);/*基础/下层投影 卡片面板*/
--lay-shadow-2: 0 8px 10px rgba(0, 0, 0, 12%), 0 3px 14px rgba(0, 0, 0, 10%), 0 5px 5px rgba(0, 0, 0, 16%);/*中层投影 下拉菜单,选择器*/
--lay-shadow-3: 0 16px 24px rgba(0, 0, 0, 14%), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%);/*上层投影 弹窗*/
}
blockquote,body,button,dd,div,dl,dt,form,h1,h2,h3,h4,h5,h6,input,li,ol,p,pre,td,textarea,th,ul{-webkit-tap-highlight-color: rgba(0, 0, 0, 0)} /*danger: 勿改*/
body{color:var(--lay-color-text-2);background-color: var(--lay-color-bg-1); color-scheme: dark;}
hr{border-bottom:1px solid var(--lay-color-border-2)!important}
a{color:var(--lay-color-text-1);}
a:hover{color:var(--lay-color-text-3)}
/* 三角形 */
.layui-edge{border-color:transparent}
.layui-edge-top{border-bottom-color:var(--lay-color-border-4)}
.layui-edge-right{border-left-color:var(--lay-color-border-4)}
.layui-edge-bottom{border-top-color:var(--lay-color-border-4)}
.layui-edge-left{border-right-color:var(--lay-color-border-4)}
/* 禁用文字 */
.layui-disabled,.layui-disabled:hover{color:var(--lay-color-text-4)!important}
/* 图标 */
.layui-icon{-moz-osx-font-smoothing:grayscale}
/* admin 布局 */
.layui-layout-admin .layui-header{background-color:var(--lay-color-bg-2)}
.layui-layout-admin .layui-footer{box-shadow:-1px 0 4px rgb(0 0 0 / 12%);background-color:var(--lay-color-bg-2)}
.layui-layout-admin .layui-logo{color:var(--lay-color-primary);box-shadow:0 1px 2px 0 rgb(0 0 0 / 15%)}
/* 引用 */
.layui-elem-quote{border-left:5px solid var(--lay-color-secondary);background-color:var(--lay-color-fill-1)}
.layui-quote-nm{border-color: var(--lay-color-fill-1)}
/* 进度条 */
.layui-progress{background-color: var(--lay-color-bg-3)}
.layui-progress-bar{background-color:var( --lay-color-secondary)}
.layui-progress-text{color:var(--lay-color-text-2)}
.layui-progress-big .layui-progress-text{color: var(--lay-color-text-1)}
/* 折叠面板 */
.layui-colla-title{color: var(--lay-color-text-1);background-color: var(--lay-color-bg-2)}
.layui-colla-content{color:var(--lay-color-text-2)}
/* 卡片面板 */
.layui-card{background-color: var(--lay-color-bg-2);box-shadow:var(--lay-shadow-1)}
.layui-card-header{border-bottom:1px solid var(--lay-color-border-2);color:var(--lay-color-text-1)}
/* 常规面板 */
.layui-panel{box-shadow:var(--lay-shadow-1);background-color: var( --lay-color-bg-2);color: var(--lay-color-text-1)}
.layui-menu-body-panel{box-shadow: var(--lay-shadow-2)}
/* 窗口面板 */
.layui-panel-window{border-top:5px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-2)}
/* 背景颜色 */
.layui-bg-red{background-color:var(--lay-color-red-6)!important;color: var(--lay-color-white)!important}
.layui-bg-orange{background-color:var(--lay-color-orange-6)!important;color: var(--lay-color-white)!important}
.layui-bg-green{background-color:var(--lay-color-layuigreen-6)!important;color: var(--lay-color-white)!important}
.layui-bg-cyan{background-color:var(--lay-color-cyan-6)!important;color: var(--lay-color-white)!important}
.layui-bg-blue{background-color: var(--lay-color-blue-6)!important;color: var(--lay-color-white)!important}
.layui-bg-black{background-color:var(--lay-color-black-6)!important;color: var(--lay-color-white)!important}
.layui-bg-purple{background-color: var(--lay-color-purple-6)!important; color: var(--lay-color-white)!important;}
.layui-bg-gray{background-color:var(--lay-color-gray-1)!important;color: var(--lay-color-black-6)!important}
/* 徽章 */
.layui-badge-rim,.layui-border,.layui-colla-content,.layui-colla-item,.layui-collapse,.layui-elem-field,.layui-form-pane .layui-form-item[pane],.layui-form-pane .layui-form-label,.layui-input,.layui-input-split,.layui-panel,.layui-select,.layui-tab-bar,.layui-tab-card,.layui-tab-title,.layui-tab-title .layui-this:after,.layui-textarea{border-color: var(--lay-color-border-1)}
/* 边框颜色 */
.layui-border{color:var(--lay-color-text-1)!important}
.layui-border-red{border-color:var(--lay-color-red-6)!important;color:var(--lay-color-red-6)!important}
.layui-border-orange{border-color:var(--lay-color-orange-6)!important;color:var(--lay-color-orange-6)!important}
.layui-border-green{border-color:var(--lay-color-layuigreen-6)!important;color:var(--lay-color-layuigreen-6)!important}
.layui-border-cyan{border-color:var(--lay-color-cyan-6)!important;color:var(--lay-color-cyan-6)!important}
.layui-border-blue{border-color: var(--lay-color-blue-6)!important;color: var(--lay-color-blue-6)!important}
.layui-border-purple{border-color: var(--lay-color-purple-6)!important; color: var(--lay-color-purple-6)!important;}
.layui-border-black{border-color:var(--lay-color-black-6)!important;color:var(--lay-color-text-1)!important}
/* 文本区域 */
.layui-text{color:var(--lay-color-text-2)}
.layui-text-em,.layui-word-aux{color: var(--lay-color-text-3)!important}
.layui-text a:not(.layui-btn){color:var(--lay-color-lightblue-6)}
.layui-text blockquote:not(.layui-elem-quote){border-left:5px solid var(--lay-color-border-4)}
/* 字体颜色 */
.layui-font-red{color:var(--lay-color-red-6)!important}
.layui-font-orange{color:var(--lay-color-orange-6)!important}
.layui-font-green{color:var(--lay-color-layuigreen-6)!important}
.layui-font-cyan{color:var(--lay-color-cyan-6)!important}
.layui-font-blue{color:var(--lay-color-lightblue-6)!important}
.layui-font-black{color:var(--lay-color-black)!important}
.layui-font-purple{color:var(--lay-color-purple-6)!important;}
.layui-font-gray{color:var(--lay-color-gray-7)!important}
/* 按钮 */
.layui-btn{border:1px solid transparent;background-color:var(--lay-color-primary);color: var(--lay-color-text-1)}
.layui-btn:hover{color: var(--lay-color-text-2)}
.layui-btn-primary{border-color:var(--lay-color-border-2);color:var(--lay-color-text-1);background-color: var(--lay-color-bg-4)}
.layui-btn-primary:hover{border-color: transparent;color:var(--lay-color-text-2)}
.layui-btn-normal{background-color: var(--lay-color-normal)}
.layui-btn-warm{background-color:var(--lay-color-warning)}
.layui-btn-danger{background-color:var(--lay-color-danger)}
.layui-btn-checked{background-color:var(--lay-color-success)}
.layui-btn-disabled,.layui-btn-disabled:active,.layui-btn-disabled:hover{border-color: var(--lay-color-border-2)!important;background-color: var(--lay-color-bg-2)!important;color: var(--lay-color-text-4)!important}
.layui-btn-group .layui-btn{border-left:1px solid var(--lay-color-border-2)}
.layui-btn-group .layui-btn-primary:hover{border-color:var(--lay-color-border-2);color:var(--lay-color-primary)}
.layui-btn-group .layui-btn-primary:first-child{border-left:1px solid var(--lay-color-gray-5)}
/*表单*/
.layui-input,.layui-select,.layui-textarea{background-color: var(--lay-color-fill-2);color: var(--lay-color-text-2)}
.layui-input:hover,.layui-textarea:hover{border-color: var(--lay-color-border-2)!important}
.layui-input:focus,.layui-textarea:focus{border-color: var(--lay-color-secondary-hover)!important;background-color: var(--lay-color-bg-2);box-shadow: 0 0 0 3px rgba(22, 183, 119, 0.08);}
.layui-input[disabled],.layui-select[disabled],.layui-textarea[disabled],.layui-input.layui-disabled,.layui-textarea.layui-disabled{background-color: var(--lay-color-fill-1);color: var(--lay-color-text-4);border-color: var(--lay-color-border-1)!important;box-shadow: 0 0 0 0;}
.layui-form-danger+.layui-form-select .layui-input,.layui-form-danger:focus{border-color:var(--lay-color-danger)!important;box-shadow: 0 0 0 3px rgba(255, 87, 34, 0.08);}
/* 输入框点缀 */
.layui-input-prefix .layui-icon,.layui-input-split .layui-icon,.layui-input-suffix .layui-icon{color: var(--lay-color-gray-8)}
.layui-input-wrap .layui-input:hover+.layui-input-split{border-color: var(--lay-color-border-2)}
.layui-input-wrap .layui-input[disabled]:hover+.layui-input-split{border-color: var(--lay-color-border-1)}
.layui-input-wrap .layui-input:focus+.layui-input-split{border-color: var(--lay-color-secondary-hover)}
.layui-input-wrap .layui-input.layui-form-danger:focus + .layui-input-split{border-color: var(--lay-color-danger);}
.layui-input-affix .layui-icon{color: var(--lay-color-text-2)}
.layui-input-affix .layui-icon-clear{color:var(--lay-color-text-2)}
.layui-input-affix .layui-icon:hover{color:var(--lay-color-text-3)}
/* 数字输入框动态点缀 */
.layui-input-wrap .layui-input-number .layui-icon-up{border-bottom-color:var(--lay-color-border-1)}
.layui-input-wrap .layui-input[type="number"].layui-input-number-out-of-range{color:var(--lay-color-danger)}
/* 下拉选择 */
.layui-form-select{color:var(--lay-color-text-2)}
.layui-form-select .layui-edge{border-top-color:var(--lay-color-gray-8)}
.layui-form-select dl{border:1px solid var( --lay-color-border-2);background-color: var(--lay-color-bg-5);box-shadow:var(--lay-shadow-2)}
.layui-form-select dl dt{color:var(--lay-color-gray-8)}
.layui-form-select dl dd:hover{background-color:var(--lay-color-active)}
.layui-form-select dl dd.layui-select-tips{color:var(--lay-color-text-2)}
.layui-form-select dl dd.layui-this{background-color: var(--lay-color-active);color: var(--lay-color-text-1)}
.layui-form-select dl dd.layui-disabled,.layui-form-select dl dd:hover.layui-disabled{background-color: var(--lay-color-bg-5)}
.layui-select-none{color:var(--lay-color-black-8)}
.layui-select-disabled .layui-disabled{border-color:var(--lay-color-border-1)!important}
.layui-select-disabled .layui-edge{border-top-color:var(--lay-color-gray-6)}
/* 复选框 */
.layui-form-checkbox{background-color:var(--lay-color-fill-2)}
.layui-form-checkbox>div{background-color:var(--lay-color-fill-3);color:var(--lay-color-text-2)}
.layui-form-checkbox:hover>div{background-color: var(--lay-color-active)}
.layui-form-checkbox>i{background-color: var(--lay-color-fill-1);border-top-color:var(--lay-color-border-1);border-right-color:var(--lay-color-border-1);border-bottom-color:var(--lay-color-border-1);border-left-color:initial;color:var(--lay-color-text-1)}
.layui-form-checkbox:hover>i{border-color:var(--lay-color-border-2);color:var(--lay-color-text-4)}
.layui-form-checked,.layui-form-checked:hover{border-color:var(--lay-color-secondary-active)}
.layui-form-checked>div,.layui-form-checked:hover>div{background-color:var(--lay-color-secondary)}
.layui-form-checked>i,.layui-form-checked:hover>i{color:var(--lay-color-secondary-hover)}
.layui-form-checkbox.layui-checkbox-disabled>div{background-color: var(--lay-color-fill-3) !important;}
/* 复选框-默认风格 */
.layui-form-checkbox[lay-skin=primary]{background-image:none;background-color:initial;border-color:initial!important}
.layui-form-checkbox[lay-skin=primary]>div{background-image:none;background-color:initial;color:var(--lay-color-text-2)}
.layui-form-checkbox[lay-skin=primary]>i{border-color:var(--lay-color-border-1);background-color:var(--lay-color-fill-2)}
.layui-form-checkbox[lay-skin=primary]:hover>i{border-color:var(--lay-color-secondary-hover);color:var(--lay-color-text-1)}
.layui-form-checked[lay-skin=primary]>i{background-color:var(--lay-color-secondary);color:var(--lay-color-text-1);border-color:var(--lay-color-secondary-active)!important}
.layui-checkbox-disabled[lay-skin=primary] >div{background:none!important;color:var(--lay-color-text-4)!important}
.layui-form-checked.layui-checkbox-disabled[lay-skin=primary]>i{background-color:var(--lay-color-fill-1)!important;border-color:var(--lay-color-border-2)!important}
.layui-checkbox-disabled[lay-skin=primary]:hover>i{border-color:var(--lay-color-border-1)}
.layui-form-checkbox[lay-skin="primary"]>.layui-icon-indeterminate:before{background-color: var(--lay-color-secondary-hover);opacity: 1;}
.layui-form-checkbox[lay-skin="primary"]:hover>.layui-icon-indeterminate:before{opacity: 1;}
.layui-form-checkbox[lay-skin="primary"]>.layui-icon-indeterminate{border-color: var(--lay-color-secondary-hover);}
/* 复选框-开关风格 */
.layui-form-switch{border-color:var(--lay-color-border-2);background-color:var(--lay-color-fill-2)}
.layui-form-switch>i{background-color:var(--lay-color-gray-4)}
.layui-form-switch.layui-checkbox-disabled>i{background-color:var(--lay-color-gray-7);}
.layui-form-switch>div{color:var(--lay-color-gray-8)!important}
.layui-form-onswitch{border-color:var(--lay-color-secondary-active);background-color:var(--lay-color-secondary)}
.layui-form-onswitch>i{background-color:var(--lay-color-gray-4)}
.layui-form-onswitch>div{color:var(--lay-color-text-1)!important}
.layui-checkbox-disabled{border-color:var(--lay-color-border-2)!important}
.layui-checkbox-disabled>div{background-color:var(--lay-color-fill-3)!important;color: var(--lay-color-text-4)!important;}
.layui-checkbox-disabled>i{border-color:var(--lay-color-border-2)!important}
.layui-checkbox-disabled:hover>i{color:var(--lay-color-text-1)!important}
.layui-form-switch.layui-checkbox-disabled>div{background-color:initial!important;color: var(--lay-color-text-3)!important;}
/*复选框背景优化*/
.layui-form-checkbox>i:before{opacity:0;filter:alpha(opacity=0)}
.layui-form-checkbox:hover>i:before{opacity:1;filter:alpha(opacity=100)}
.layui-form-checked.layui-checkbox-disabled:hover>i:before,.layui-form-checked:hover>i:before,.layui-form-checked>i:before{opacity:1;filter:alpha(opacity=100)}
.layui-form-checkbox[lay-skin=primary]:hover>i:before{opacity:0;filter:alpha(opacity=0)}
.layui-form-checked[lay-skin=primary]:hover>i:before{opacity:1;filter:alpha(opacity=100)}
.layui-checkbox-disabled:hover>i:before{opacity:0;filter:alpha(opacity=0)}
/*单选框*/
.layui-form-radio>i{color:var(--lay-color-gray-8)}
.layui-form-radio:hover>*,.layui-form-radioed,.layui-form-radioed>i{color:var(--lay-color-secondary)}
.layui-radio-disabled>i{color:var(--lay-color-text-4)!important}
.layui-radio-disabled>*{color:var(--lay-color-text-4)!important}
/* 表单方框风格 */
.layui-form-pane .layui-form-label{background-color:var(--lay-color-bg-2)}
/** 分页 **/
.layui-laypage a,.layui-laypage button,.layui-laypage input,.layui-laypage select,.layui-laypage span{border:1px solid var(--lay-color-border-2)}
.layui-laypage a,.layui-laypage span{background-color: var(--lay-color-bg-2);color: var(--lay-color-text-2)}
.layui-laypage a[data-page]{color:var(--lay-color-text-2)}
.layui-laypage a:hover{color: var(--lay-color-primary)}
.layui-laypage .layui-laypage-spr{color:var(--lay-color-text-3)}
.layui-laypage .layui-laypage-curr em{color: var(--lay-color-white)}
.layui-laypage .layui-laypage-curr .layui-laypage-em{background-color: var(--lay-color-primary)}
.layui-laypage .layui-laypage-skip{color:var(--lay-color-text-3)}
.layui-laypage button,.layui-laypage input{background-color: var(--lay-color-bg-2)}
.layui-laypage input:focus,.layui-laypage select:focus{border-color: var(--lay-color-primary)!important}
/** 流加载 **/
.layui-flow-more{color:var(--lay-color-text-1)}
.layui-flow-more a cite{background-color: var(--lay-color-bg-4);color: var(--lay-color-text-1)}
.layui-flow-more a i{color:var(--lay-color-text-2)}
/** 表格 **/
.layui-table{background-color: var(--lay-color-bg-2);color: var(--lay-color-text-2)}
.layui-table-mend{background-color: var(--lay-color-bg-2)}
.layui-table-click,.layui-table-hover,.layui-table[lay-even] tbody tr:nth-child(even){background-color:var(--lay-color-fill-3)}
.layui-table-checked{background-color: var(--lay-color-fill-2);color: var(--lay-color-text-1)}
.layui-table-checked.layui-table-hover,.layui-table-checked.layui-table-click{background-color: var(--lay-color-fill-3);}
.layui-table td,.layui-table th,.layui-table-col-set,.layui-table-fixed-r,.layui-table-grid-down,.layui-table-header,.layui-table-mend,.layui-table-page,.layui-table-tips-main,.layui-table-tool,.layui-table-total,.layui-table-view,.layui-table[lay-skin=line],.layui-table[lay-skin=row]{border-color: var(--lay-color-border-2)}
.layui-table-view:after {background-color: var(--lay-color-border-2);}
.layui-table-view .layui-table td[data-edit]:hover:after{border:1px solid var(--lay-color-primary-active)}
.layui-table-loading-icon .layui-icon{color:var(--lay-color-gray-8);}
.layui-table-page{background-color: var(--lay-color-bg-2);}
.layui-table-page .layui-laypage a,
.layui-table-page .layui-laypage span{border: none;}
.layui-table-tool{background-color: var(--lay-color-bg-2);}
.layui-table-tool .layui-inline[lay-event]{color:var(--lay-color-text-3);border:1px solid var(--lay-color-border-2)}
.layui-table-tool .layui-inline[lay-event]:hover{border:1px solid var(--lay-color-border-3)}
.layui-table-tool-panel{color: var(--lay-color-text-1); border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-5);box-shadow:var(--lay-shadow-2)}
.layui-table-tool-panel li:hover{background-color:var(--lay-color-active)}
.layui-table-col-set{background-color: var(--lay-color-white)}
.layui-table-sort .layui-table-sort-asc{border-bottom-color:var(--lay-color-gray-8)}
.layui-table-sort .layui-table-sort-asc:hover{border-bottom-color:var(--lay-color-gray-11)}
.layui-table-sort .layui-table-sort-desc{border-top-color:var(--lay-color-gray-8)}
.layui-table-sort .layui-table-sort-desc:hover{border-top-color:var(--lay-color-gray-11)}
.layui-table-sort[lay-sort=asc] .layui-table-sort-asc{border-bottom-color:var(--lay-color-gray-13)}
.layui-table-sort[lay-sort=desc] .layui-table-sort-desc{border-top-color:var(--lay-color-gray-13)}
.layui-table-cell .layui-table-link{color: var(--lay-color-lightblue-5)}
.layui-table-body .layui-none{color:var(--lay-color-gray-8)}
.layui-table-fixed-l{box-shadow:1px 0 8px rgba(0,0,0,1)}
.layui-table-fixed-r{box-shadow:-1px 0 8px rgba(0,0,0,1)}
.layui-table-edit{box-shadow:var(--lay-shadow-1);background-color: var(--lay-color-bg-2)}
.layui-table-edit:focus{border-color:var(--lay-color-secondary)!important}
select.layui-table-edit{border-color:var(--lay-color-border-2)}
.layui-table-grid-down{background-color: var(--lay-color-bg-5);color:var(--lay-color-gray-8)}
.layui-table-grid-down:hover{background-color:var(--lay-color-bg-5)}
/* 单元格多行展开风格 */
.layui-table-cell-c{background-color: var(--lay-color-gray-13);color: var(--lay-color-text-1); border-color: var(--lay-color-border-3);}
.layui-table-cell-c:hover{border-color: var(--lay-color-secondary-hover);}
/* 单元格 TIPS 展开风格 */
body .layui-table-tips .layui-layer-content{box-shadow:var(--lay-shadow-3)}
.layui-table-tips-main{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-3)}
.layui-table-tips-c{background-color:var(--lay-color-gray-13);color: var(--lay-color-text-1)}
.layui-table-tips-c:hover{background-color:var(--lay-color-gray-10)}
/** 文件上传 **/
.layui-upload-choose{color:var(--lay-color-gray-8)}
.layui-upload-drag{border:1px dashed var( --lay-color-border-2);background-color: var(--lay-color-bg-4);color: var(--lay-color-text-2)}
.layui-upload-drag .layui-icon{color: var(--lay-color-primary)}
.layui-upload-drag[lay-over]{border-color: var(--lay-color-primary)}
/* 基础菜单元素 */
.layui-menu{background-color: var(--lay-color-bg-2)}
.layui-menu li{color: var(--lay-color-text-1)}
.layui-menu li:hover{background-color: var(--lay-color-bg-5)}
.layui-menu li.layui-disabled,.layui-menu li.layui-disabled *{color:var(--lay-color-text-4)!important}
.layui-menu .layui-menu-item-group>.layui-menu-body-title{color: var(--lay-color-text-3)}
.layui-menu .layui-menu-item-none{color: var(--lay-color-text-3);}
.layui-menu .layui-menu-item-divider{border-bottom:1px solid var(--lay-color-border-2)}
.layui-menu .layui-menu-item-group:hover,
.layui-menu .layui-menu-item-none:hover,
.layui-menu .layui-menu-item-divider:hover{background: none;}
.layui-menu .layui-menu-item-up>.layui-menu-body-title{color: var(--lay-color-text-1)}
.layui-menu .layui-menu-item-down:hover>.layui-menu-body-title>.layui-icon,.layui-menu .layui-menu-item-up>.layui-menu-body-title:hover>.layui-icon{color: var(--lay-color-text-1)}
.layui-menu .layui-menu-item-checked,.layui-menu .layui-menu-item-checked2{background-color:var(--lay-color-active)!important;color:var(--lay-color-secondary)}
.layui-menu .layui-menu-item-checked a,.layui-menu .layui-menu-item-checked2 a{color:var(--lay-color-secondary)}
.layui-menu .layui-menu-item-checked:after{border-right:3px solid var(--lay-color-secondary)}
.layui-menu-body-title a{color: var(--lay-color-text-1)}
.layui-menu-lg .layui-menu-body-title a:hover,.layui-menu-lg li:hover{color:var(--lay-color-secondary)}
/* 下拉菜单 */
.layui-dropdown{background-color: var(--lay-color-bg-5)}
.layui-dropdown.layui-panel,.layui-dropdown .layui-panel{background-color: var(--lay-color-bg-5);box-shadow: var(--lay-shadow-2)}
.layui-dropdown.layui-panel .layui-menu{background-color: var(--lay-color-bg-5)}
/** 导航菜单 **/
.layui-nav{background-color:var(--lay-color-black-6);color: var(--lay-color-white)}
.layui-nav .layui-nav-item a{color: var(--lay-color-text-1);}
.layui-nav .layui-this:after,.layui-nav-bar{background-color:var(--lay-color-secondary)}
.layui-nav .layui-nav-item a:hover,.layui-nav .layui-this a{color: var(--lay-color-text-1)}
.layui-nav-child{box-shadow:var(--lay-shadow-2);border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-5)}
.layui-nav .layui-nav-child a{color: var(--lay-color-text-1)}
.layui-nav .layui-nav-child a:hover{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-nav-child dd.layui-this{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-nav-tree .layui-nav-child dd.layui-this,.layui-nav-tree .layui-nav-child dd.layui-this a,.layui-nav-tree .layui-this,.layui-nav-tree .layui-this>a,.layui-nav-tree .layui-this>a:hover{background-color: var(--lay-color-primary);color: var(--lay-color-white)}
.layui-nav-itemed>a,.layui-nav-tree .layui-nav-title a,.layui-nav-tree .layui-nav-title a:hover{color: var(--lay-color-white)!important}
.layui-nav-tree .layui-nav-bar{background-color:var(--lay-color-primary)}
.layui-nav-tree .layui-nav-child{background: none; background-color:rgba(0, 0, 0, .3); border: none; box-shadow: none;}
.layui-nav-tree .layui-nav-child a{color: var(--lay-color-white);color: var(--lay-color-text-1)}
.layui-nav-tree .layui-nav-child a:hover{background: none; color: var(--lay-color-white)}
.layui-nav.layui-bg-gray,.layui-nav-tree.layui-bg-gray{background-color: var(--lay-color-bg-2) !important;color: var(--lay-color-text-1);}
.layui-nav-tree.layui-bg-gray .layui-nav-child{background-color: rgba(0, 0, 0, .3) !important;}
.layui-nav-tree.layui-bg-gray a,.layui-nav.layui-bg-gray .layui-nav-item a{color: var(--lay-color-text-1)}
.layui-nav.layui-bg-gray .layui-nav-child{background-color: var(--lay-color-bg-5);}
.layui-nav-tree.layui-bg-gray .layui-nav-itemed>a{color: var(--lay-color-text-1)!important}
.layui-nav.layui-bg-gray .layui-this a{color:var(--lay-color-secondary)}
.layui-nav-tree.layui-bg-gray .layui-nav-child dd.layui-this,.layui-nav-tree.layui-bg-gray .layui-nav-child dd.layui-this a,.layui-nav-tree.layui-bg-gray .layui-this,.layui-nav-tree.layui-bg-gray .layui-this>a{color:var(--lay-color-secondary)!important}
.layui-nav-tree.layui-bg-gray .layui-nav-bar{background-color:var(--lay-color-secondary)}
/** 面包屑 **/
.layui-breadcrumb a{color:var(--lay-color-gray-7)!important}
.layui-breadcrumb a:hover{color:var(--lay-color-secondary)!important}
.layui-breadcrumb a cite{color:var(--lay-color-gray-8)}
.layui-breadcrumb span[lay-separator]{color:var(--lay-color-gray-7)}
/** Tab 选项卡 **/
.layui-tab .layui-tab-title:after{border-bottom-color: var(--lay-color-border-1);}
.layui-tab-title .layui-this{color: var(--lay-color-text-2)}
.layui-tab-title .layui-this:after{border-bottom-color: var(--lay-color-bg-1)}
.layui-tab-bar{background-color: var(--lay-color-bg-3)}
.layui-tab-more li.layui-this:after{border-bottom-color:var(--lay-color-border-1)}
.layui-tab-title li .layui-tab-close{color:var(--lay-color-gray-8)}
.layui-tab-title li .layui-tab-close:hover{background-color:var(--lay-color-danger);color: var(--lay-color-white)}
.layui-tab-brief>.layui-tab-title .layui-this{color:var( --lay-color-primary)}
.layui-tab-brief>.layui-tab-more li.layui-this:after,.layui-tab-brief>.layui-tab-title .layui-this:after{border-bottom:2px solid var(--lay-color-secondary)}
.layui-tab-card{box-shadow: var(--lay-shadow-1)}
.layui-tab-card>.layui-tab-title{background-color: var(--lay-color-bg-2)}
.layui-tab-card>.layui-tab-title .layui-this{background-color: var(--lay-color-bg-1)}
.layui-tab-card>.layui-tab-title .layui-this:after{border-bottom-color: var(--lay-color-bg-1)}
.layui-tab-card>.layui-tab-more .layui-this{color:var(--lay-color-secondary)}
/** tabs 标签页 **/
.layui-tabs-header:after,
.layui-tabs-scroll:after{border-bottom-color: var(--lay-color-border-1);}
.layui-tabs-card>.layui-tabs-header .layui-this{background-color: transparent;}
.layui-tabs-card>.layui-tabs-header .layui-this:after{border-color: var(--lay-color-border-1); border-bottom-color: var(--lay-color-bg-1);}
.layui-tabs-card.layui-panel>.layui-tabs-header .layui-this:after{border-bottom-color: var(--lay-color-bg-2);}
.layui-tabs-bar .layui-icon{background-color: var(--lay-color-bg-1); color: var(--lay-color-text-2); border-color: var(--lay-color-border-1); box-shadow: 2px 0 5px 0 rgb(0 0 0 / 32%);}
.layui-tabs-bar .layui-icon-next{box-shadow: -2px 0 5px 0 rgb(0 0 0 / 32%);}
/*时间线*/
.layui-timeline-axis{background-color: var(--lay-color-bg-4);color:var(--lay-color-secondary)}
.layui-timeline-axis:hover{color:var(--lay-color-red-6)}
.layui-timeline-item:before{background-color: var(--lay-color-bg-3)}
/*徽章*/
.layui-badge,.layui-badge-dot,.layui-badge-rim{background-color:var(--lay-color-red-6);color: var(--lay-color-white)}
.layui-badge-rim{background-color: var(--lay-color-white);color:var(--lay-color-black-6)}
/* carousel 轮播 */
.layui-carousel{background-color:var(--lay-color-gray-2)}
.layui-carousel>[carousel-item]:before{color:var(--lay-color-gray-8);-moz-osx-font-smoothing:grayscale}
.layui-carousel>[carousel-item]>*{background-color:var(--lay-color-gray-2)}
.layui-carousel-arrow{background-color:rgba(0,0,0,.2);color: var(--lay-color-white)}
.layui-carousel-arrow:hover,.layui-carousel-ind ul:hover{background-color:var(--lay-color-black)}
.layui-carousel[lay-indicator=outside] .layui-carousel-ind ul{background-color:var(--lay-color-black)}
.layui-carousel-ind ul{background-color:rgba(0,0,0,.2)}
.layui-carousel-ind ul li{background-color:var(--lay-color-gray-3);background-color: var(--lay-color-text-3)}
.layui-carousel-ind ul li:hover{background-color: var(--lay-color-white)}
.layui-carousel-ind ul li.layui-this{background-color: var(--lay-color-white)}
/** fixbar **/
.layui-fixbar li{background-color:var(--lay-color-black-5);color: var(--lay-color-text-1)}
/** 表情面板 **/
body .layui-util-face .layui-layer-content{background-color: var(--lay-color-bg-5);color:var(--lay-color-text-2)}
.layui-util-face ul{border:1px solid var(--lay-color-border-3);background-color: var(--lay-color-bg-5);box-shadow:var(--lay-shadow-2)}
.layui-util-face ul li{border:1px solid var(--lay-color-border-2)}
.layui-util-face ul li:hover{border:1px solid var(--lay-color-red-7);background: var(--lay-color-text-1)}
/** 代码文本修饰 **/
.layui-code{border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-white);color: var(--lay-color-text-2)}
/** 穿梭框 **/
.layui-transfer-box,.layui-transfer-header,.layui-transfer-search{border-color: var(--lay-color-border-2)}
.layui-transfer-box{background-color: var(--lay-color-bg-2)}
.layui-transfer-search .layui-icon-search{color:var(--lay-color-gray-8)}
.layui-transfer-active .layui-btn{background-color:var( --lay-color-secondary);border-color:var( --lay-color-secondary);color: var(--lay-color-white)}
.layui-transfer-active .layui-btn-disabled{background-color:var(--lay-color-gray-2);border-color:var(--lay-color-gray-3);color:var(--lay-color-gray-8)}
.layui-transfer-data li:hover{background-color:var(--lay-color-active)}
/* chrome 105 */
.layui-transfer-data li:hover:has([lay-filter="layTransferCheckbox"][disabled]){background-color:var(--lay-color-bg-2)}
.layui-transfer-data .layui-none{color:var(--lay-color-gray-7)}
/** 评分组件 **/
.layui-rate li i.layui-icon{color:var(--lay-color-orange-6)}
/** 颜色选择器 **/
.layui-colorpicker{border:1px solid var(--lay-color-border-1)}
.layui-colorpicker:hover{border-color: var(--lay-color-border-2)}
.layui-colorpicker-trigger-span{border:1px solid var(--lay-color-border-1)}
.layui-colorpicker-trigger-i{color: var(--lay-color-white)}
.layui-colorpicker-trigger-i.layui-icon-close{color:var(--lay-color-black-7)}
.layui-colorpicker-main{background: var(--lay-color-bg-2);border:1px solid var( --lay-color-border-2);box-shadow:var(--lay-shadow-2)}
.layui-colorpicker-basis-white{background:linear-gradient(90deg, #fff,hsla(0,0%,100%,0))} /* danger: 勿改*/
.layui-colorpicker-basis-black{background:linear-gradient(0deg,#000,transparent)} /* danger: 勿改*/
.layui-colorpicker-basis-cursor{border:1px solid var(--lay-color-white)}
.layui-colorpicker-side{background:linear-gradient(linear-gradient(#F00, #FF0, #0F0, #0FF, #00F, #F0F, #F00))} /* danger: 勿改*/
.layui-colorpicker-side-slider{box-shadow:var(--lay-shadow-1);background: var(--lay-color-white);border:1px solid var(--lay-color-gray-2)}
.layui-colorpicker-alpha-slider{box-shadow:var(--lay-shadow-1);background: var(--lay-color-white);border:1px solid var(--lay-color-gray-2)}
.layui-colorpicker-pre.layui-this{box-shadow:var(--lay-shadow-1)}
.layui-colorpicker-pre.selected{box-shadow:var(--lay-shadow-1)}
.layui-colorpicker-main-input input.layui-input{color: var(--lay-color-text-2)}
/** 滑块 **/
.layui-slider{background: var( --lay-color-bg-5)}
.layui-slider-step{background: var(--lay-color-fill-4)}
.layui-slider-wrap-btn{background: var(--lay-color-bg-4)}
.layui-slider-tips{color: var(--lay-color-text-1);background:var(--lay-color-black);box-shadow: var(--lay-shadow-3)}
.layui-slider-tips:after{border-color:var(--lay-color-black) transparent transparent transparent}
.layui-slider-input{border:1px solid var(--lay-color-border-1)}
.layui-slider-input-btn{border-left:1px solid var(--lay-color-border-1)}
.layui-slider-input-btn i{color:var(--lay-color-gray-9)}
.layui-slider-input-btn i:first-child{border-bottom:1px solid var(--lay-color-border-1)}
.layui-slider-input-btn i:hover{color:var(--lay-color-primary)}
/** 树组件 **/
.layui-tree-line .layui-tree-set .layui-tree-set:after{border-top:1px dotted var(--lay-color-gray-7)}
.layui-tree-entry:hover{background-color: var(--lay-color-bg-4)}
.layui-tree-line .layui-tree-entry:hover{background-color:var(--lay-color-black)}
.layui-tree-line .layui-tree-entry:hover .layui-tree-txt{color:var(--lay-color-text-3)}
.layui-tree-entry:hover:has(span.layui-tree-txt.layui-disabled){background-color: transparent !important}
.layui-tree-line .layui-tree-set:before{border-left:1px dotted var(--lay-color-gray-7)}
.layui-tree-iconClick{color:var(--lay-color-gray-7)}
.layui-tree-icon{border:1px solid var(--lay-color-gray-8)}
.layui-tree-icon .layui-icon{color:var(--lay-color-text-1)}
.layui-tree-iconArrow:after{border-color:transparent transparent transparent var(--lay-color-gray-7)}
.layui-tree-txt{color:var(--lay-color-text-2)}
.layui-tree-search{color:var(--lay-color-black-7)}
.layui-tree-btnGroup .layui-icon:hover{color:var(--lay-color-text-2)}
.layui-tree-editInput{background-color:var(--lay-color-fill-2)}
.layui-tree-emptyText{color:var(--lay-color-text-2)}
/*code 不处理*/
.layui-code-view{border:1px solid var(--lay-color-border-1);}
.layui-code-view:not(.layui-code-hl){background-color: var(--lay-color-bg-2);color: var(--lay-color-text-2);}
.layui-code-header{border-bottom: 1px solid var(--lay-color-border-1); background-color: var(--lay-color-bg-2)}
.layui-code-header > .layui-code-header-about{color: var(--lay-color-text-2);}
.layui-code-view:not(.layui-code-hl) .layui-code-ln-side{border-color: var(--lay-color-border-1); background-color: var(--lay-color-bg-2);}
.layui-code-nowrap > .layui-code-ln-side{background: none !important;}
.layui-code-fixbar > span{color: var(--lay-color-text-3);}
.layui-code-fixbar > span:hover{color: var(--lay-color-secondary-hover);}
.layui-code-theme-dark,
.layui-code-theme-dark > .layui-code-header{border-color: rgb(126 122 122 / 15%); background-color: #1f1f1f;}
.layui-code-theme-dark{border-width: 1px; color: #ccc;}
.layui-code-theme-dark > .layui-code-ln-side{border-right-color: #2a2a2a; background: none; color: #6e7681;}
.layui-code-view.layui-code-hl > .layui-code-ln-side{background-color: transparent;}
.layui-code-theme-dark.layui-code-hl,
.layui-code-theme-dark.layui-code-hl > .layui-code-ln-side{border-color: rgb(126 122 122 / 15%);}
.layui-code-full{background-color: var(--lay-color-bg-1)}
/*日期选择器*/
.layui-laydate-header i{color:var(--lay-color-gray-8)}
.laydate-day-holidays:before{color:var(--lay-color-red-6)}
.layui-laydate .layui-this .laydate-day-holidays:before{color: var(--lay-color-white)}
.layui-laydate-footer span{border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-5)}
.layui-laydate-footer span:hover{color:var(--lay-color-secondary)}
.layui-laydate-footer span.layui-laydate-preview{border-color:transparent!important;}
.layui-laydate-footer span.layui-laydate-preview:hover{color:var(--lay-color-text-1) !important}
.layui-laydate-shortcut+.layui-laydate-main{border-left:1px solid var(--lay-color-border-2)}
.layui-laydate .layui-laydate-list{background-color: var(--lay-color-bg-5)}
.layui-laydate-hint{color:var(--lay-color-danger)}
.layui-laydate-range .laydate-main-list-1 .layui-laydate-content,.layui-laydate-range .laydate-main-list-1 .layui-laydate-header{border-left:1px solid var(--lay-color-border-2)}
.layui-laydate,.layui-laydate-hint{border-color: var(--lay-color-border-2);box-shadow:var(--lay-shadow-3);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-laydate{box-shadow: var(--lay-shadow-2)}
.layui-laydate-hint{border-color:var(--lay-color-border-1)}
.layui-laydate-header{border-bottom:1px solid var( --lay-color-border-2)}
.layui-laydate-header i:hover,.layui-laydate-header span:hover{color:var(--lay-color-secondary)}
.layui-laydate-content th{color: var(--lay-color-text-1)}
.layui-laydate-content td{color: var(--lay-color-text-1)}
.layui-laydate-content td.laydate-day-now{color:var(--lay-color-secondary)}
.layui-laydate-content td.laydate-day-now:after{border:1px solid var(--lay-color-secondary)}
.layui-laydate-linkage .layui-laydate-content td.laydate-selected>div{background-color:var(--lay-color-green-8);}
.layui-laydate-linkage .laydate-selected:hover>div{background-color:var(--lay-color-green-8)!important}
.layui-laydate-content td>div:hover,.layui-laydate-list li:hover,.layui-laydate-shortcut>li:hover{background-color: var(--lay-color-fill-2);color: var(--lay-color-text-2)}
.layui-laydate-content td.laydate-disabled>div:hover{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-4)}
.laydate-time-list li ol{border:1px solid var(--lay-color-border-2)}
.laydate-time-list>li:hover{background: 0 0;}
.layui-laydate-content .laydate-day-next,.layui-laydate-content .laydate-day-prev{color: var(--lay-color-text-3)}
.layui-laydate-linkage .laydate-selected.laydate-day-next>div,.layui-laydate-linkage .laydate-selected.laydate-day-prev>div{background: none!important}
.layui-laydate-footer{border-top:1px solid var(--lay-color-border-2)}
.layui-laydate-hint{color:var(--lay-color-danger)}
.laydate-day-mark::after{background-color:var(--lay-color-secondary)}
.layui-laydate-footer span[lay-type=date]{color:var(--lay-color-secondary)}
.layui-laydate .layui-this,.layui-laydate .layui-this>div{background-color:var(--lay-color-secondary)!important;color: var(--lay-color-white)!important}
.layui-laydate .laydate-disabled,.layui-laydate .laydate-disabled:hover{color: var(--lay-color-text-4)!important}
.layui-laydate .layui-this.laydate-disabled,.layui-laydate .layui-this.laydate-disabled>div{background-color: var(--lay-color-fill-1) !important;color: var(--lay-color-text-4) !important;}
.laydate-theme-molv .layui-laydate-header{background-color:var(--lay-color-primary)}
.laydate-theme-molv .layui-laydate-header i,.laydate-theme-molv .layui-laydate-header span{color:var(--lay-color-gray-2)}
.laydate-theme-molv .layui-laydate-header i:hover,.laydate-theme-molv .layui-laydate-header span:hover{color: var(--lay-color-white)}
.laydate-theme-molv .layui-laydate-content{border:1px solid var(--lay-color-border-2)}
.laydate-theme-molv .layui-this, .laydate-theme-molv .layui-this>div{background-color: var(--lay-color-primary) !important;}
.laydate-theme-molv .layui-laydate-footer{border:1px solid var(--lay-color-border-2)}
.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li,.laydate-theme-grid .layui-laydate-content td,.laydate-theme-grid .layui-laydate-content thead{border:1px solid var(--lay-color-border-2)}
.layui-laydate-linkage.laydate-theme-grid .laydate-selected,.layui-laydate-linkage.laydate-theme-grid .laydate-selected:hover{background-color:var(--lay-color-gray-3)!important;color:var(--lay-color-primary)!important}
.layui-laydate-linkage.laydate-theme-grid .laydate-selected.laydate-day-next,.layui-laydate-linkage.laydate-theme-grid .laydate-selected.laydate-day-prev{color:var(--lay-color-gray-6)!important}
.layui-laydate.laydate-theme-circle .layui-laydate-content table td.layui-this{background-color:transparent!important}
/*layer*/
.layui-layer{background-color: var(--lay-color-bg-3);box-shadow:var(--lay-shadow-3)}
.layui-layer-border{border:1px solid var(--lay-color-border-2);box-shadow:var(--lay-shadow-3)}
.layui-layer-move{background-color: var(--lay-color-bg-5)}
.layui-layer-title{border-bottom:1px solid var(--lay-color-border-2);color: var(--lay-color-text-1)}
.layui-layer-setwin span{color: var(--lay-color-text-1)}
.layui-layer-setwin .layui-layer-min:before{border-bottom-color:var(--lay-color-text-1)}
.layui-layer-setwin .layui-layer-min:hover:before{border-bottom-color:var(--lay-color-info-hover)}
.layui-layer-setwin .layui-layer-max:after,.layui-layer-setwin .layui-layer-max:before{border:1px solid var(--lay-color-text-3)}
.layui-layer-setwin .layui-layer-max:hover:after,.layui-layer-setwin .layui-layer-max:hover:before{border-color:var(--lay-color-info-hover)}
.layui-layer-setwin .layui-layer-maxmin:after,.layui-layer-setwin .layui-layer-maxmin:before{background-color: var(--lay-color-bg-5)}
.layui-layer-setwin .layui-layer-close2{color:var(--lay-color-text-1);background-color:var(--lay-color-gray-10)}
.layui-layer-setwin .layui-layer-close2:hover{background-color:var(--lay-color-normal)}
.layui-layer-btn a{border:1px solid var(--lay-color-border-2);background-color: var( --lay-color-bg-3);color: var(--lay-color-text-2)}
.layui-layer-btn .layui-layer-btn0{border-color: transparent;background-color: var(--lay-color-normal);color: var(--lay-color-text-1)}
.layui-layer-dialog .layui-layer-content .layui-layer-face{color:var(--lay-color-gray-9)}
.layui-layer-dialog .layui-layer-content .layui-icon-tips{color:var(--lay-color-warning)}
.layui-layer-dialog .layui-layer-content .layui-icon-success{color: var(--lay-color-success)}
.layui-layer-dialog .layui-layer-content .layui-icon-error{top: 19px; color: var(--lay-color-danger)}
.layui-layer-dialog .layui-layer-content .layui-icon-question{color: var(--lay-color-warning);}
.layui-layer-dialog .layui-layer-content .layui-icon-lock{color: var(--lay-color-gray-10)}
.layui-layer-dialog .layui-layer-content .layui-icon-face-cry{color:var(--lay-color-danger)}
.layui-layer-dialog .layui-layer-content .layui-icon-face-smile{color:var(--lay-color-success)}
.layui-layer-rim{border:6px solid var(--lay-color-gray-8);border:6px solid var(--lay-color-border-2)}
.layui-layer-msg{border:1px solid var( --lay-color-border-1)}
.layui-layer-hui{background-color: var(--lay-color-bg-3);color: var(--lay-color-text-1)}
.layui-layer-hui .layui-layer-close{color: var(--lay-color-white)}
.layui-layer-loading-icon{color:var(--lay-color-gray-9)}
.layui-layer-loading-2:after,.layui-layer-loading-2:before{border:3px solid var(--lay-color-gray-6)}
.layui-layer-loading-2:after{border-color:transparent;border-left-color: var(--lay-color-normal)}
.layui-layer-tips .layui-layer-content{box-shadow: var(--lay-shadow-3);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-tips i.layui-layer-TipsG{border-color:transparent}
.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{border-right-color:var(--lay-color-black)}
.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{border-bottom-color:var(--lay-color-black)}
.layui-layer-lan .layui-layer-title{background:var(--lay-color-blue-5);color: var(--lay-color-text-1)}
.layui-layer-lan .layui-layer-btn{border-top:1px solid var(--lay-color-border-3)}
.layui-layer-lan .layui-layer-btn a{background: var(--lay-color-white);border-color:var(--lay-color-border-3);color: var(--lay-color-black-7)}
.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background: var(--lay-color-gray-7)}
.layui-layer-molv .layui-layer-title{background:var(--lay-color-layuigreen-6);color: var(--lay-color-text-1)}
.layui-layer-molv .layui-layer-btn a{background:var(--lay-color-layuigreen-6);border-color:var(--lay-color-layuigreen-6)}
.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:var(--lay-color-gray-7)}
.layui-layer-win10{border-color: var(--lay-color-border-2)}
.layui-layer-win10 .layui-layer-btn{background-color: var(--lay-color-bg-2);border-color: var(--lay-color-border-2)}
.layui-layer-win10.layui-layer-dialog .layui-layer-content{color: var(--lay-color-blue-7)}
.layui-layer-win10 .layui-layer-btn .layui-layer-btn0{border-color: var(--lay-color-blue-9);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-win10 .layui-layer-btn .layui-layer-btn1{border-color: var(--lay-color-border-2);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-win10 .layui-layer-btn a:hover{background-color: var(--lay-color-blue-10);border-color: var(--lay-color-blue-8)}
.layui-layer-prompt .layui-layer-input{border:1px solid var(--lay-color-border-2);color: var(--lay-color-text-2)}
.layui-layer-tab{box-shadow:var(--lay-shadow-3)}
.layui-layer-tab .layui-layer-title span.layui-this{border-left:1px solid var(--lay-color-border-2);border-right:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-3)}
.layui-layer-photos{background: none; box-shadow: none;}
.layui-layer-photos-prev,.layui-layer-photos-next{color:var(--lay-color-gray-9)}
.layui-layer-photos-prev:hover,.layui-layer-photos-next:hover{color:var(--lay-color-text-1)}
.layui-layer-photos-toolbar{background-color:#333;background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-photos-toolbar *{color: var(--lay-color-text-1)}
.layui-layer-photos-toolbar a:hover{color: var(--lay-color-text-2)}
.layui-layer-photos-header > span:hover{background-color: var(--lay-color-fill-2)}
.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{border-right-color: var(--lay-color-bg-5)}
.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{border-bottom-color: var(--lay-color-bg-5)}
.layui-layer-prompt .layui-layer-input{border:1px solid var(--lay-color-border-2);color:var(--lay-color-text-1);background-color:var(--lay-color-black)}
.layui-layer-prompt .layui-layer-input:focus{outline:0}
/*fix style*/
.layui-layer-loading{background:0 0;box-shadow:0 0}
.layui-btn-primary{border-color:transparent}
.layui-btn-group .layui-btn:first-child{border-left:none}
.layui-btn-group .layui-btn-primary:hover{border-top-color:transparent; border-bottom-color: transparent;}
.layui-menu li:hover{background-color:var(--lay-color-fill-2)}
.layui-nav-child dd.layui-this{background-color:var(--lay-color-fill-2)}
.layui-nav .layui-nav-child a:hover{background-color:var(--lay-color-fill-2)}
.layui-nav .layui-nav-item a:hover,.layui-nav .layui-this a{background-color: var(--lay-color-fill-2)}
.layui-nav-child dd.layui-this{background-color: var(--lay-color-fill-2)}
.layui-tab-card>.layui-tab-title .layui-this:after{border-bottom-color:var(--lay-color-bg-1)}
.layui-form-select dl dd:hover{background-color:var(--lay-color-fill-2)}
.layui-form-select dl dd.layui-this{background-color:var(--lay-color-fill-2)}
.layui-laypage button{color:var(--lay-color-text-1)}
.layui-table[lay-even] tbody tr:nth-child(even){background-color:var(--lay-color-fill-4)}
.layui-menu .layui-menu-item-checked,.layui-menu .layui-menu-item-checked2{background-color:var(--lay-color-fill-2)!important}
.layui-input-split{background-color: var(--lay-color-bg-2);}
.layui-input-wrap .layui-input-prefix.layui-input-split{border-width: 1px;}
.layui-input-wrap .layui-input-split:has(+.layui-input:hover) {border-color: var(--lay-color-border-2);}
.layui-input-wrap .layui-input-split:has(+.layui-input:focus) {border-color: var(--lay-color-secondary-hover);}
.layui-layer-tab .layui-layer-title span:first-child{border-left: none !important;}
.layui-slider-input.layui-input,
.layui-slider-input .layui-input {background-color: var(--lay-color-bg-2);}
/*# sourceMappingURL=layui-theme-dark.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -1,534 +0,0 @@
blockquote,body,button,dd,div,dl,dt,form,h1,h2,h3,h4,h5,h6,input,li,ol,p,pre,td,textarea,th,ul{-webkit-tap-highlight-color: rgba(0, 0, 0, 0)} /*danger: 勿改*/
body{color:var(--lay-color-text-2);background-color: var(--lay-color-bg-1); color-scheme: dark;}
hr{border-bottom:1px solid var(--lay-color-border-2)!important}
a{color:var(--lay-color-text-1);}
a:hover{color:var(--lay-color-text-3)}
/* 三角形 */
.layui-edge{border-color:transparent}
.layui-edge-top{border-bottom-color:var(--lay-color-border-4)}
.layui-edge-right{border-left-color:var(--lay-color-border-4)}
.layui-edge-bottom{border-top-color:var(--lay-color-border-4)}
.layui-edge-left{border-right-color:var(--lay-color-border-4)}
/* 禁用文字 */
.layui-disabled,.layui-disabled:hover{color:var(--lay-color-text-4)!important}
/* 图标 */
.layui-icon{-moz-osx-font-smoothing:grayscale}
/* admin 布局 */
.layui-layout-admin .layui-header{background-color:var(--lay-color-bg-2)}
.layui-layout-admin .layui-footer{box-shadow:-1px 0 4px rgb(0 0 0 / 12%);background-color:var(--lay-color-bg-2)}
.layui-layout-admin .layui-logo{color:var(--lay-color-primary);box-shadow:0 1px 2px 0 rgb(0 0 0 / 15%)}
/* 引用 */
.layui-elem-quote{border-left:5px solid var(--lay-color-secondary);background-color:var(--lay-color-fill-1)}
.layui-quote-nm{border-color: var(--lay-color-fill-1)}
/* 进度条 */
.layui-progress{background-color: var(--lay-color-bg-3)}
.layui-progress-bar{background-color:var( --lay-color-secondary)}
.layui-progress-text{color:var(--lay-color-text-2)}
.layui-progress-big .layui-progress-text{color: var(--lay-color-text-1)}
/* 折叠面板 */
.layui-colla-title{color: var(--lay-color-text-1);background-color: var(--lay-color-bg-2)}
.layui-colla-content{color:var(--lay-color-text-2)}
/* 卡片面板 */
.layui-card{background-color: var(--lay-color-bg-2);box-shadow:var(--lay-shadow-1)}
.layui-card-header{border-bottom:1px solid var(--lay-color-border-2);color:var(--lay-color-text-1)}
/* 常规面板 */
.layui-panel{box-shadow:var(--lay-shadow-1);background-color: var( --lay-color-bg-2);color: var(--lay-color-text-1)}
.layui-menu-body-panel{box-shadow: var(--lay-shadow-2)}
/* 窗口面板 */
.layui-panel-window{border-top:5px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-2)}
/* 背景颜色 */
.layui-bg-red{background-color:var(--lay-color-red-6)!important;color: var(--lay-color-white)!important}
.layui-bg-orange{background-color:var(--lay-color-orange-6)!important;color: var(--lay-color-white)!important}
.layui-bg-green{background-color:var(--lay-color-layuigreen-6)!important;color: var(--lay-color-white)!important}
.layui-bg-cyan{background-color:var(--lay-color-cyan-6)!important;color: var(--lay-color-white)!important}
.layui-bg-blue{background-color: var(--lay-color-blue-6)!important;color: var(--lay-color-white)!important}
.layui-bg-black{background-color:var(--lay-color-black-6)!important;color: var(--lay-color-white)!important}
.layui-bg-purple{background-color: var(--lay-color-purple-6)!important; color: var(--lay-color-white)!important;}
.layui-bg-gray{background-color:var(--lay-color-gray-1)!important;color: var(--lay-color-black-6)!important}
/* 徽章 */
.layui-badge-rim,.layui-border,.layui-colla-content,.layui-colla-item,.layui-collapse,.layui-elem-field,.layui-form-pane .layui-form-item[pane],.layui-form-pane .layui-form-label,.layui-input,.layui-input-split,.layui-panel,.layui-select,.layui-tab-bar,.layui-tab-card,.layui-tab-title,.layui-tab-title .layui-this:after,.layui-textarea{border-color: var(--lay-color-border-1)}
/* 边框颜色 */
.layui-border{color:var(--lay-color-text-1)!important}
.layui-border-red{border-color:var(--lay-color-red-6)!important;color:var(--lay-color-red-6)!important}
.layui-border-orange{border-color:var(--lay-color-orange-6)!important;color:var(--lay-color-orange-6)!important}
.layui-border-green{border-color:var(--lay-color-layuigreen-6)!important;color:var(--lay-color-layuigreen-6)!important}
.layui-border-cyan{border-color:var(--lay-color-cyan-6)!important;color:var(--lay-color-cyan-6)!important}
.layui-border-blue{border-color: var(--lay-color-blue-6)!important;color: var(--lay-color-blue-6)!important}
.layui-border-purple{border-color: var(--lay-color-purple-6)!important; color: var(--lay-color-purple-6)!important;}
.layui-border-black{border-color:var(--lay-color-black-6)!important;color:var(--lay-color-text-1)!important}
/* 文本区域 */
.layui-text{color:var(--lay-color-text-2)}
.layui-text-em,.layui-word-aux{color: var(--lay-color-text-3)!important}
.layui-text a:not(.layui-btn){color:var(--lay-color-lightblue-6)}
.layui-text blockquote:not(.layui-elem-quote){border-left:5px solid var(--lay-color-border-4)}
/* 字体颜色 */
.layui-font-red{color:var(--lay-color-red-6)!important}
.layui-font-orange{color:var(--lay-color-orange-6)!important}
.layui-font-green{color:var(--lay-color-layuigreen-6)!important}
.layui-font-cyan{color:var(--lay-color-cyan-6)!important}
.layui-font-blue{color:var(--lay-color-lightblue-6)!important}
.layui-font-black{color:var(--lay-color-black)!important}
.layui-font-purple{color:var(--lay-color-purple-6)!important;}
.layui-font-gray{color:var(--lay-color-gray-7)!important}
/* 按钮 */
.layui-btn{border:1px solid transparent;background-color:var(--lay-color-primary);color: var(--lay-color-text-1)}
.layui-btn:hover{color: var(--lay-color-text-2)}
.layui-btn-primary{border-color:var(--lay-color-border-2);color:var(--lay-color-text-1);background-color: var(--lay-color-bg-4)}
.layui-btn-primary:hover{border-color: transparent;color:var(--lay-color-text-2)}
.layui-btn-normal{background-color: var(--lay-color-normal)}
.layui-btn-warm{background-color:var(--lay-color-warning)}
.layui-btn-danger{background-color:var(--lay-color-danger)}
.layui-btn-checked{background-color:var(--lay-color-success)}
.layui-btn-disabled,.layui-btn-disabled:active,.layui-btn-disabled:hover{border-color: var(--lay-color-border-2)!important;background-color: var(--lay-color-bg-2)!important;color: var(--lay-color-text-4)!important}
.layui-btn-group .layui-btn{border-left:1px solid var(--lay-color-border-2)}
.layui-btn-group .layui-btn-primary:hover{border-color:var(--lay-color-border-2);color:var(--lay-color-primary)}
.layui-btn-group .layui-btn-primary:first-child{border-left:1px solid var(--lay-color-gray-5)}
/*表单*/
.layui-input,.layui-select,.layui-textarea{background-color: var(--lay-color-fill-2);color: var(--lay-color-text-2)}
.layui-input:hover,.layui-textarea:hover{border-color: var(--lay-color-border-2)!important}
.layui-input:focus,.layui-textarea:focus{border-color: var(--lay-color-secondary-hover)!important;background-color: var(--lay-color-bg-2);box-shadow: 0 0 0 3px rgba(22, 183, 119, 0.08);}
.layui-input[disabled],.layui-select[disabled],.layui-textarea[disabled],.layui-input.layui-disabled,.layui-textarea.layui-disabled{background-color: var(--lay-color-fill-1);color: var(--lay-color-text-4);border-color: var(--lay-color-border-1)!important;box-shadow: 0 0 0 0;}
.layui-form-danger+.layui-form-select .layui-input,.layui-form-danger:focus{border-color:var(--lay-color-danger)!important;box-shadow: 0 0 0 3px rgba(255, 87, 34, 0.08);}
/* 输入框点缀 */
.layui-input-prefix .layui-icon,.layui-input-split .layui-icon,.layui-input-suffix .layui-icon{color: var(--lay-color-gray-8)}
.layui-input-wrap .layui-input:hover+.layui-input-split{border-color: var(--lay-color-border-2)}
.layui-input-wrap .layui-input[disabled]:hover+.layui-input-split{border-color: var(--lay-color-border-1)}
.layui-input-wrap .layui-input:focus+.layui-input-split{border-color: var(--lay-color-secondary-hover)}
.layui-input-wrap .layui-input.layui-form-danger:focus + .layui-input-split{border-color: var(--lay-color-danger);}
.layui-input-affix .layui-icon{color: var(--lay-color-text-2)}
.layui-input-affix .layui-icon-clear{color:var(--lay-color-text-2)}
.layui-input-affix .layui-icon:hover{color:var(--lay-color-text-3)}
/* 数字输入框动态点缀 */
.layui-input-wrap .layui-input-number .layui-icon-up{border-bottom-color:var(--lay-color-border-1)}
.layui-input-wrap .layui-input[type="number"].layui-input-number-out-of-range{color:var(--lay-color-danger)}
/* 下拉选择 */
.layui-form-select{color:var(--lay-color-text-2)}
.layui-form-select .layui-edge{border-top-color:var(--lay-color-gray-8)}
.layui-form-select dl{border:1px solid var( --lay-color-border-2);background-color: var(--lay-color-bg-5);box-shadow:var(--lay-shadow-2)}
.layui-form-select dl dt{color:var(--lay-color-gray-8)}
.layui-form-select dl dd:hover{background-color:var(--lay-color-active)}
.layui-form-select dl dd.layui-select-tips{color:var(--lay-color-text-2)}
.layui-form-select dl dd.layui-this{background-color: var(--lay-color-active);color: var(--lay-color-text-1)}
.layui-form-select dl dd.layui-disabled,.layui-form-select dl dd:hover.layui-disabled{background-color: var(--lay-color-bg-5)}
.layui-select-none{color:var(--lay-color-black-8)}
.layui-select-disabled .layui-disabled{border-color:var(--lay-color-border-1)!important}
.layui-select-disabled .layui-edge{border-top-color:var(--lay-color-gray-6)}
/* 复选框 */
.layui-form-checkbox{background-color:var(--lay-color-fill-2)}
.layui-form-checkbox>div{background-color:var(--lay-color-fill-3);color:var(--lay-color-text-2)}
.layui-form-checkbox:hover>div{background-color: var(--lay-color-active)}
.layui-form-checkbox>i{background-color: var(--lay-color-fill-1);border-top-color:var(--lay-color-border-1);border-right-color:var(--lay-color-border-1);border-bottom-color:var(--lay-color-border-1);border-left-color:initial;color:var(--lay-color-text-1)}
.layui-form-checkbox:hover>i{border-color:var(--lay-color-border-2);color:var(--lay-color-text-4)}
.layui-form-checked,.layui-form-checked:hover{border-color:var(--lay-color-secondary-active)}
.layui-form-checked>div,.layui-form-checked:hover>div{background-color:var(--lay-color-secondary)}
.layui-form-checked>i,.layui-form-checked:hover>i{color:var(--lay-color-secondary-hover)}
.layui-form-checkbox.layui-checkbox-disabled>div{background-color: var(--lay-color-fill-3) !important;}
/* 复选框-默认风格 */
.layui-form-checkbox[lay-skin=primary]{background-image:none;background-color:initial;border-color:initial!important}
.layui-form-checkbox[lay-skin=primary]>div{background-image:none;background-color:initial;color:var(--lay-color-text-2)}
.layui-form-checkbox[lay-skin=primary]>i{border-color:var(--lay-color-border-1);background-color:var(--lay-color-fill-2)}
.layui-form-checkbox[lay-skin=primary]:hover>i{border-color:var(--lay-color-secondary-hover);color:var(--lay-color-text-1)}
.layui-form-checked[lay-skin=primary]>i{background-color:var(--lay-color-secondary);color:var(--lay-color-text-1);border-color:var(--lay-color-secondary-active)!important}
.layui-checkbox-disabled[lay-skin=primary] >div{background:none!important;color:var(--lay-color-text-4)!important}
.layui-form-checked.layui-checkbox-disabled[lay-skin=primary]>i{background-color:var(--lay-color-fill-1)!important;border-color:var(--lay-color-border-2)!important}
.layui-checkbox-disabled[lay-skin=primary]:hover>i{border-color:var(--lay-color-border-1)}
.layui-form-checkbox[lay-skin="primary"]>.layui-icon-indeterminate:before{background-color: var(--lay-color-secondary-hover);opacity: 1;}
.layui-form-checkbox[lay-skin="primary"]:hover>.layui-icon-indeterminate:before{opacity: 1;}
.layui-form-checkbox[lay-skin="primary"]>.layui-icon-indeterminate{border-color: var(--lay-color-secondary-hover);}
/* 复选框-开关风格 */
.layui-form-switch{border-color:var(--lay-color-border-2);background-color:var(--lay-color-fill-2)}
.layui-form-switch>i{background-color:var(--lay-color-gray-4)}
.layui-form-switch.layui-checkbox-disabled>i{background-color:var(--lay-color-gray-7);}
.layui-form-switch>div{color:var(--lay-color-gray-8)!important}
.layui-form-onswitch{border-color:var(--lay-color-secondary-active);background-color:var(--lay-color-secondary)}
.layui-form-onswitch>i{background-color:var(--lay-color-gray-4)}
.layui-form-onswitch>div{color:var(--lay-color-text-1)!important}
.layui-checkbox-disabled{border-color:var(--lay-color-border-2)!important}
.layui-checkbox-disabled>div{background-color:var(--lay-color-fill-3)!important;color: var(--lay-color-text-4)!important;}
.layui-checkbox-disabled>i{border-color:var(--lay-color-border-2)!important}
.layui-checkbox-disabled:hover>i{color:var(--lay-color-text-1)!important}
.layui-form-switch.layui-checkbox-disabled>div{background-color:initial!important;color: var(--lay-color-text-3)!important;}
/*复选框背景优化*/
.layui-form-checkbox>i:before{opacity:0;filter:alpha(opacity=0)}
.layui-form-checkbox:hover>i:before{opacity:1;filter:alpha(opacity=100)}
.layui-form-checked.layui-checkbox-disabled:hover>i:before,.layui-form-checked:hover>i:before,.layui-form-checked>i:before{opacity:1;filter:alpha(opacity=100)}
.layui-form-checkbox[lay-skin=primary]:hover>i:before{opacity:0;filter:alpha(opacity=0)}
.layui-form-checked[lay-skin=primary]:hover>i:before{opacity:1;filter:alpha(opacity=100)}
.layui-checkbox-disabled:hover>i:before{opacity:0;filter:alpha(opacity=0)}
/*单选框*/
.layui-form-radio>i{color:var(--lay-color-gray-8)}
.layui-form-radio:hover>*,.layui-form-radioed,.layui-form-radioed>i{color:var(--lay-color-secondary)}
.layui-radio-disabled>i{color:var(--lay-color-text-4)!important}
.layui-radio-disabled>*{color:var(--lay-color-text-4)!important}
/* 表单方框风格 */
.layui-form-pane .layui-form-label{background-color:var(--lay-color-bg-2)}
/** 分页 **/
.layui-laypage a,.layui-laypage button,.layui-laypage input,.layui-laypage select,.layui-laypage span{border:1px solid var(--lay-color-border-2)}
.layui-laypage a,.layui-laypage span{background-color: var(--lay-color-bg-2);color: var(--lay-color-text-2)}
.layui-laypage a[data-page]{color:var(--lay-color-text-2)}
.layui-laypage a:hover{color: var(--lay-color-primary)}
.layui-laypage .layui-laypage-spr{color:var(--lay-color-text-3)}
.layui-laypage .layui-laypage-curr em{color: var(--lay-color-white)}
.layui-laypage .layui-laypage-curr .layui-laypage-em{background-color: var(--lay-color-primary)}
.layui-laypage .layui-laypage-skip{color:var(--lay-color-text-3)}
.layui-laypage button,.layui-laypage input{background-color: var(--lay-color-bg-2)}
.layui-laypage input:focus,.layui-laypage select:focus{border-color: var(--lay-color-primary)!important}
/** 流加载 **/
.layui-flow-more{color:var(--lay-color-text-1)}
.layui-flow-more a cite{background-color: var(--lay-color-bg-4);color: var(--lay-color-text-1)}
.layui-flow-more a i{color:var(--lay-color-text-2)}
/** 表格 **/
.layui-table{background-color: var(--lay-color-bg-2);color: var(--lay-color-text-2)}
.layui-table-mend{background-color: var(--lay-color-bg-2)}
.layui-table-click,.layui-table-hover,.layui-table[lay-even] tbody tr:nth-child(even){background-color:var(--lay-color-fill-3)}
.layui-table-checked{background-color: var(--lay-color-fill-2);color: var(--lay-color-text-1)}
.layui-table-checked.layui-table-hover,.layui-table-checked.layui-table-click{background-color: var(--lay-color-fill-3);}
.layui-table td,.layui-table th,.layui-table-col-set,.layui-table-fixed-r,.layui-table-grid-down,.layui-table-header,.layui-table-mend,.layui-table-page,.layui-table-tips-main,.layui-table-tool,.layui-table-total,.layui-table-view,.layui-table[lay-skin=line],.layui-table[lay-skin=row]{border-color: var(--lay-color-border-2)}
.layui-table-view:after {background-color: var(--lay-color-border-2);}
.layui-table-view .layui-table td[data-edit]:hover:after{border:1px solid var(--lay-color-primary-active)}
.layui-table-loading-icon .layui-icon{color:var(--lay-color-gray-8);}
.layui-table-page{background-color: var(--lay-color-bg-2);}
.layui-table-page .layui-laypage a,
.layui-table-page .layui-laypage span{border: none;}
.layui-table-tool{background-color: var(--lay-color-bg-2);}
.layui-table-tool .layui-inline[lay-event]{color:var(--lay-color-text-3);border:1px solid var(--lay-color-border-2)}
.layui-table-tool .layui-inline[lay-event]:hover{border:1px solid var(--lay-color-border-3)}
.layui-table-tool-panel{color: var(--lay-color-text-1); border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-5);box-shadow:var(--lay-shadow-2)}
.layui-table-tool-panel li:hover{background-color:var(--lay-color-active)}
.layui-table-col-set{background-color: var(--lay-color-white)}
.layui-table-sort .layui-table-sort-asc{border-bottom-color:var(--lay-color-gray-8)}
.layui-table-sort .layui-table-sort-asc:hover{border-bottom-color:var(--lay-color-gray-11)}
.layui-table-sort .layui-table-sort-desc{border-top-color:var(--lay-color-gray-8)}
.layui-table-sort .layui-table-sort-desc:hover{border-top-color:var(--lay-color-gray-11)}
.layui-table-sort[lay-sort=asc] .layui-table-sort-asc{border-bottom-color:var(--lay-color-gray-13)}
.layui-table-sort[lay-sort=desc] .layui-table-sort-desc{border-top-color:var(--lay-color-gray-13)}
.layui-table-cell .layui-table-link{color: var(--lay-color-lightblue-5)}
.layui-table-body .layui-none{color:var(--lay-color-gray-8)}
.layui-table-fixed-l{box-shadow:1px 0 8px rgba(0,0,0,1)}
.layui-table-fixed-r{box-shadow:-1px 0 8px rgba(0,0,0,1)}
.layui-table-edit{box-shadow:var(--lay-shadow-1);background-color: var(--lay-color-bg-2)}
.layui-table-edit:focus{border-color:var(--lay-color-secondary)!important}
select.layui-table-edit{border-color:var(--lay-color-border-2)}
.layui-table-grid-down{background-color: var(--lay-color-bg-5);color:var(--lay-color-gray-8)}
.layui-table-grid-down:hover{background-color:var(--lay-color-bg-5)}
/* 单元格多行展开风格 */
.layui-table-cell-c{background-color: var(--lay-color-gray-13);color: var(--lay-color-text-1); border-color: var(--lay-color-border-3);}
.layui-table-cell-c:hover{border-color: var(--lay-color-secondary-hover);}
/* 单元格 TIPS 展开风格 */
body .layui-table-tips .layui-layer-content{box-shadow:var(--lay-shadow-3)}
.layui-table-tips-main{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-3)}
.layui-table-tips-c{background-color:var(--lay-color-gray-13);color: var(--lay-color-text-1)}
.layui-table-tips-c:hover{background-color:var(--lay-color-gray-10)}
/** 文件上传 **/
.layui-upload-choose{color:var(--lay-color-gray-8)}
.layui-upload-drag{border:1px dashed var( --lay-color-border-2);background-color: var(--lay-color-bg-4);color: var(--lay-color-text-2)}
.layui-upload-drag .layui-icon{color: var(--lay-color-primary)}
.layui-upload-drag[lay-over]{border-color: var(--lay-color-primary)}
/* 基础菜单元素 */
.layui-menu{background-color: var(--lay-color-bg-2)}
.layui-menu li{color: var(--lay-color-text-1)}
.layui-menu li:hover{background-color: var(--lay-color-bg-5)}
.layui-menu li.layui-disabled,.layui-menu li.layui-disabled *{color:var(--lay-color-text-4)!important}
.layui-menu .layui-menu-item-group>.layui-menu-body-title{color: var(--lay-color-text-3)}
.layui-menu .layui-menu-item-none{color: var(--lay-color-text-3);}
.layui-menu .layui-menu-item-divider{border-bottom:1px solid var(--lay-color-border-2)}
.layui-menu .layui-menu-item-group:hover,
.layui-menu .layui-menu-item-none:hover,
.layui-menu .layui-menu-item-divider:hover{background: none;}
.layui-menu .layui-menu-item-up>.layui-menu-body-title{color: var(--lay-color-text-1)}
.layui-menu .layui-menu-item-down:hover>.layui-menu-body-title>.layui-icon,.layui-menu .layui-menu-item-up>.layui-menu-body-title:hover>.layui-icon{color: var(--lay-color-text-1)}
.layui-menu .layui-menu-item-checked,.layui-menu .layui-menu-item-checked2{background-color:var(--lay-color-active)!important;color:var(--lay-color-secondary)}
.layui-menu .layui-menu-item-checked a,.layui-menu .layui-menu-item-checked2 a{color:var(--lay-color-secondary)}
.layui-menu .layui-menu-item-checked:after{border-right:3px solid var(--lay-color-secondary)}
.layui-menu-body-title a{color: var(--lay-color-text-1)}
.layui-menu-lg .layui-menu-body-title a:hover,.layui-menu-lg li:hover{color:var(--lay-color-secondary)}
/* 下拉菜单 */
.layui-dropdown{background-color: var(--lay-color-bg-5)}
.layui-dropdown.layui-panel,.layui-dropdown .layui-panel{background-color: var(--lay-color-bg-5);box-shadow: var(--lay-shadow-2)}
.layui-dropdown.layui-panel .layui-menu{background-color: var(--lay-color-bg-5)}
/** 导航菜单 **/
.layui-nav{background-color:var(--lay-color-black-6);color: var(--lay-color-white)}
.layui-nav .layui-nav-item a{color: var(--lay-color-text-1);}
.layui-nav .layui-this:after,.layui-nav-bar{background-color:var(--lay-color-secondary)}
.layui-nav .layui-nav-item a:hover,.layui-nav .layui-this a{color: var(--lay-color-text-1)}
.layui-nav-child{box-shadow:var(--lay-shadow-2);border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-5)}
.layui-nav .layui-nav-child a{color: var(--lay-color-text-1)}
.layui-nav .layui-nav-child a:hover{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-nav-child dd.layui-this{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-nav-tree .layui-nav-child dd.layui-this,.layui-nav-tree .layui-nav-child dd.layui-this a,.layui-nav-tree .layui-this,.layui-nav-tree .layui-this>a,.layui-nav-tree .layui-this>a:hover{background-color: var(--lay-color-primary);color: var(--lay-color-white)}
.layui-nav-itemed>a,.layui-nav-tree .layui-nav-title a,.layui-nav-tree .layui-nav-title a:hover{color: var(--lay-color-white)!important}
.layui-nav-tree .layui-nav-bar{background-color:var(--lay-color-primary)}
.layui-nav-tree .layui-nav-child{background: none; background-color:rgba(0, 0, 0, .3); border: none; box-shadow: none;}
.layui-nav-tree .layui-nav-child a{color: var(--lay-color-white);color: var(--lay-color-text-1)}
.layui-nav-tree .layui-nav-child a:hover{background: none; color: var(--lay-color-white)}
.layui-nav.layui-bg-gray,.layui-nav-tree.layui-bg-gray{background-color: var(--lay-color-bg-2) !important;color: var(--lay-color-text-1);}
.layui-nav-tree.layui-bg-gray .layui-nav-child{background-color: rgba(0, 0, 0, .3) !important;}
.layui-nav-tree.layui-bg-gray a,.layui-nav.layui-bg-gray .layui-nav-item a{color: var(--lay-color-text-1)}
.layui-nav.layui-bg-gray .layui-nav-child{background-color: var(--lay-color-bg-5);}
.layui-nav-tree.layui-bg-gray .layui-nav-itemed>a{color: var(--lay-color-text-1)!important}
.layui-nav.layui-bg-gray .layui-this a{color:var(--lay-color-secondary)}
.layui-nav-tree.layui-bg-gray .layui-nav-child dd.layui-this,.layui-nav-tree.layui-bg-gray .layui-nav-child dd.layui-this a,.layui-nav-tree.layui-bg-gray .layui-this,.layui-nav-tree.layui-bg-gray .layui-this>a{color:var(--lay-color-secondary)!important}
.layui-nav-tree.layui-bg-gray .layui-nav-bar{background-color:var(--lay-color-secondary)}
/** 面包屑 **/
.layui-breadcrumb a{color:var(--lay-color-gray-7)!important}
.layui-breadcrumb a:hover{color:var(--lay-color-secondary)!important}
.layui-breadcrumb a cite{color:var(--lay-color-gray-8)}
.layui-breadcrumb span[lay-separator]{color:var(--lay-color-gray-7)}
/** Tab 选项卡 **/
.layui-tab .layui-tab-title:after{border-bottom-color: var(--lay-color-border-1);}
.layui-tab-title .layui-this{color: var(--lay-color-text-2)}
.layui-tab-title .layui-this:after{border-bottom-color: var(--lay-color-bg-1)}
.layui-tab-bar{background-color: var(--lay-color-bg-3)}
.layui-tab-more li.layui-this:after{border-bottom-color:var(--lay-color-border-1)}
.layui-tab-title li .layui-tab-close{color:var(--lay-color-gray-8)}
.layui-tab-title li .layui-tab-close:hover{background-color:var(--lay-color-danger);color: var(--lay-color-white)}
.layui-tab-brief>.layui-tab-title .layui-this{color:var( --lay-color-primary)}
.layui-tab-brief>.layui-tab-more li.layui-this:after,.layui-tab-brief>.layui-tab-title .layui-this:after{border-bottom:2px solid var(--lay-color-secondary)}
.layui-tab-card{box-shadow: var(--lay-shadow-1)}
.layui-tab-card>.layui-tab-title{background-color: var(--lay-color-bg-2)}
.layui-tab-card>.layui-tab-title .layui-this{background-color: var(--lay-color-bg-1)}
.layui-tab-card>.layui-tab-title .layui-this:after{border-bottom-color: var(--lay-color-bg-1)}
.layui-tab-card>.layui-tab-more .layui-this{color:var(--lay-color-secondary)}
/** tabs 标签页 **/
.layui-tabs-header:after,
.layui-tabs-scroll:after{border-bottom-color: var(--lay-color-border-1);}
.layui-tabs-card>.layui-tabs-header .layui-this{background-color: transparent;}
.layui-tabs-card>.layui-tabs-header .layui-this:after{border-color: var(--lay-color-border-1); border-bottom-color: var(--lay-color-bg-1);}
.layui-tabs-card.layui-panel>.layui-tabs-header .layui-this:after{border-bottom-color: var(--lay-color-bg-2);}
.layui-tabs-bar .layui-icon{background-color: var(--lay-color-bg-1); color: var(--lay-color-text-2); border-color: var(--lay-color-border-1); box-shadow: 2px 0 5px 0 rgb(0 0 0 / 32%);}
.layui-tabs-bar .layui-icon-next{box-shadow: -2px 0 5px 0 rgb(0 0 0 / 32%);}
/*时间线*/
.layui-timeline-axis{background-color: var(--lay-color-bg-4);color:var(--lay-color-secondary)}
.layui-timeline-axis:hover{color:var(--lay-color-red-6)}
.layui-timeline-item:before{background-color: var(--lay-color-bg-3)}
/*徽章*/
.layui-badge,.layui-badge-dot,.layui-badge-rim{background-color:var(--lay-color-red-6);color: var(--lay-color-white)}
.layui-badge-rim{background-color: var(--lay-color-white);color:var(--lay-color-black-6)}
/* carousel 轮播 */
.layui-carousel{background-color:var(--lay-color-gray-2)}
.layui-carousel>[carousel-item]:before{color:var(--lay-color-gray-8);-moz-osx-font-smoothing:grayscale}
.layui-carousel>[carousel-item]>*{background-color:var(--lay-color-gray-2)}
.layui-carousel-arrow{background-color:rgba(0,0,0,.2);color: var(--lay-color-white)}
.layui-carousel-arrow:hover,.layui-carousel-ind ul:hover{background-color:var(--lay-color-black)}
.layui-carousel[lay-indicator=outside] .layui-carousel-ind ul{background-color:var(--lay-color-black)}
.layui-carousel-ind ul{background-color:rgba(0,0,0,.2)}
.layui-carousel-ind ul li{background-color:var(--lay-color-gray-3);background-color: var(--lay-color-text-3)}
.layui-carousel-ind ul li:hover{background-color: var(--lay-color-white)}
.layui-carousel-ind ul li.layui-this{background-color: var(--lay-color-white)}
/** fixbar **/
.layui-fixbar li{background-color:var(--lay-color-black-5);color: var(--lay-color-text-1)}
/** 表情面板 **/
body .layui-util-face .layui-layer-content{background-color: var(--lay-color-bg-5);color:var(--lay-color-text-2)}
.layui-util-face ul{border:1px solid var(--lay-color-border-3);background-color: var(--lay-color-bg-5);box-shadow:var(--lay-shadow-2)}
.layui-util-face ul li{border:1px solid var(--lay-color-border-2)}
.layui-util-face ul li:hover{border:1px solid var(--lay-color-red-7);background: var(--lay-color-text-1)}
/** 代码文本修饰 **/
.layui-code{border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-white);color: var(--lay-color-text-2)}
/** 穿梭框 **/
.layui-transfer-box,.layui-transfer-header,.layui-transfer-search{border-color: var(--lay-color-border-2)}
.layui-transfer-box{background-color: var(--lay-color-bg-2)}
.layui-transfer-search .layui-icon-search{color:var(--lay-color-gray-8)}
.layui-transfer-active .layui-btn{background-color:var( --lay-color-secondary);border-color:var( --lay-color-secondary);color: var(--lay-color-white)}
.layui-transfer-active .layui-btn-disabled{background-color:var(--lay-color-gray-2);border-color:var(--lay-color-gray-3);color:var(--lay-color-gray-8)}
.layui-transfer-data li:hover{background-color:var(--lay-color-active)}
/* chrome 105 */
.layui-transfer-data li:hover:has([lay-filter="layTransferCheckbox"][disabled]){background-color:var(--lay-color-bg-2)}
.layui-transfer-data .layui-none{color:var(--lay-color-gray-7)}
/** 评分组件 **/
.layui-rate li i.layui-icon{color:var(--lay-color-orange-6)}
/** 颜色选择器 **/
.layui-colorpicker{border:1px solid var(--lay-color-border-1)}
.layui-colorpicker:hover{border-color: var(--lay-color-border-2)}
.layui-colorpicker-trigger-span{border:1px solid var(--lay-color-border-1)}
.layui-colorpicker-trigger-i{color: var(--lay-color-white)}
.layui-colorpicker-trigger-i.layui-icon-close{color:var(--lay-color-black-7)}
.layui-colorpicker-main{background: var(--lay-color-bg-2);border:1px solid var( --lay-color-border-2);box-shadow:var(--lay-shadow-2)}
.layui-colorpicker-basis-white{background:linear-gradient(90deg, #fff,hsla(0,0%,100%,0))} /* danger: 勿改*/
.layui-colorpicker-basis-black{background:linear-gradient(0deg,#000,transparent)} /* danger: 勿改*/
.layui-colorpicker-basis-cursor{border:1px solid var(--lay-color-white)}
.layui-colorpicker-side{background:linear-gradient(linear-gradient(#F00, #FF0, #0F0, #0FF, #00F, #F0F, #F00))} /* danger: 勿改*/
.layui-colorpicker-side-slider{box-shadow:var(--lay-shadow-1);background: var(--lay-color-white);border:1px solid var(--lay-color-gray-2)}
.layui-colorpicker-alpha-slider{box-shadow:var(--lay-shadow-1);background: var(--lay-color-white);border:1px solid var(--lay-color-gray-2)}
.layui-colorpicker-pre.layui-this{box-shadow:var(--lay-shadow-1)}
.layui-colorpicker-pre.selected{box-shadow:var(--lay-shadow-1)}
.layui-colorpicker-main-input input.layui-input{color: var(--lay-color-text-2)}
/** 滑块 **/
.layui-slider{background: var( --lay-color-bg-5)}
.layui-slider-step{background: var(--lay-color-fill-4)}
.layui-slider-wrap-btn{background: var(--lay-color-bg-4)}
.layui-slider-tips{color: var(--lay-color-text-1);background:var(--lay-color-black);box-shadow: var(--lay-shadow-3)}
.layui-slider-tips:after{border-color:var(--lay-color-black) transparent transparent transparent}
.layui-slider-input{border:1px solid var(--lay-color-border-1)}
.layui-slider-input-btn{border-left:1px solid var(--lay-color-border-1)}
.layui-slider-input-btn i{color:var(--lay-color-gray-9)}
.layui-slider-input-btn i:first-child{border-bottom:1px solid var(--lay-color-border-1)}
.layui-slider-input-btn i:hover{color:var(--lay-color-primary)}
/** 树组件 **/
.layui-tree-line .layui-tree-set .layui-tree-set:after{border-top:1px dotted var(--lay-color-gray-7)}
.layui-tree-entry:hover{background-color: var(--lay-color-bg-4)}
.layui-tree-line .layui-tree-entry:hover{background-color:var(--lay-color-black)}
.layui-tree-line .layui-tree-entry:hover .layui-tree-txt{color:var(--lay-color-text-3)}
.layui-tree-entry:hover:has(span.layui-tree-txt.layui-disabled){background-color: transparent !important}
.layui-tree-line .layui-tree-set:before{border-left:1px dotted var(--lay-color-gray-7)}
.layui-tree-iconClick{color:var(--lay-color-gray-7)}
.layui-tree-icon{border:1px solid var(--lay-color-gray-8)}
.layui-tree-icon .layui-icon{color:var(--lay-color-text-1)}
.layui-tree-iconArrow:after{border-color:transparent transparent transparent var(--lay-color-gray-7)}
.layui-tree-txt{color:var(--lay-color-text-2)}
.layui-tree-search{color:var(--lay-color-black-7)}
.layui-tree-btnGroup .layui-icon:hover{color:var(--lay-color-text-2)}
.layui-tree-editInput{background-color:var(--lay-color-fill-2)}
.layui-tree-emptyText{color:var(--lay-color-text-2)}
/*code 不处理*/
.layui-code-view{border:1px solid var(--lay-color-border-1);}
.layui-code-view:not(.layui-code-hl){background-color: var(--lay-color-bg-2);color: var(--lay-color-text-2);}
.layui-code-header{border-bottom: 1px solid var(--lay-color-border-1); background-color: var(--lay-color-bg-2)}
.layui-code-header > .layui-code-header-about{color: var(--lay-color-text-2);}
.layui-code-view:not(.layui-code-hl) .layui-code-ln-side{border-color: var(--lay-color-border-1); background-color: var(--lay-color-bg-2);}
.layui-code-nowrap > .layui-code-ln-side{background: none !important;}
.layui-code-fixbar > span{color: var(--lay-color-text-3);}
.layui-code-fixbar > span:hover{color: var(--lay-color-secondary-hover);}
.layui-code-theme-dark,
.layui-code-theme-dark > .layui-code-header{border-color: rgb(126 122 122 / 15%); background-color: #1f1f1f;}
.layui-code-theme-dark{border-width: 1px; color: #ccc;}
.layui-code-theme-dark > .layui-code-ln-side{border-right-color: #2a2a2a; background: none; color: #6e7681;}
.layui-code-view.layui-code-hl > .layui-code-ln-side{background-color: transparent;}
.layui-code-theme-dark.layui-code-hl,
.layui-code-theme-dark.layui-code-hl > .layui-code-ln-side{border-color: rgb(126 122 122 / 15%);}
.layui-code-full{background-color: var(--lay-color-bg-1)}
/*日期选择器*/
.layui-laydate-header i{color:var(--lay-color-gray-8)}
.laydate-day-holidays:before{color:var(--lay-color-red-6)}
.layui-laydate .layui-this .laydate-day-holidays:before{color: var(--lay-color-white)}
.layui-laydate-footer span{border:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-5)}
.layui-laydate-footer span:hover{color:var(--lay-color-secondary)}
.layui-laydate-footer span.layui-laydate-preview{border-color:transparent!important;}
.layui-laydate-footer span.layui-laydate-preview:hover{color:var(--lay-color-text-1) !important}
.layui-laydate-shortcut+.layui-laydate-main{border-left:1px solid var(--lay-color-border-2)}
.layui-laydate .layui-laydate-list{background-color: var(--lay-color-bg-5)}
.layui-laydate-hint{color:var(--lay-color-danger)}
.layui-laydate-range .laydate-main-list-1 .layui-laydate-content,.layui-laydate-range .laydate-main-list-1 .layui-laydate-header{border-left:1px solid var(--lay-color-border-2)}
.layui-laydate,.layui-laydate-hint{border-color: var(--lay-color-border-2);box-shadow:var(--lay-shadow-3);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-laydate{box-shadow: var(--lay-shadow-2)}
.layui-laydate-hint{border-color:var(--lay-color-border-1)}
.layui-laydate-header{border-bottom:1px solid var( --lay-color-border-2)}
.layui-laydate-header i:hover,.layui-laydate-header span:hover{color:var(--lay-color-secondary)}
.layui-laydate-content th{color: var(--lay-color-text-1)}
.layui-laydate-content td{color: var(--lay-color-text-1)}
.layui-laydate-content td.laydate-day-now{color:var(--lay-color-secondary)}
.layui-laydate-content td.laydate-day-now:after{border:1px solid var(--lay-color-secondary)}
.layui-laydate-linkage .layui-laydate-content td.laydate-selected>div{background-color:var(--lay-color-green-8);}
.layui-laydate-linkage .laydate-selected:hover>div{background-color:var(--lay-color-green-8)!important}
.layui-laydate-content td>div:hover,.layui-laydate-list li:hover,.layui-laydate-shortcut>li:hover{background-color: var(--lay-color-fill-2);color: var(--lay-color-text-2)}
.layui-laydate-content td.laydate-disabled>div:hover{background-color: var(--lay-color-bg-5);color: var(--lay-color-text-4)}
.laydate-time-list li ol{border:1px solid var(--lay-color-border-2)}
.laydate-time-list>li:hover{background: 0 0;}
.layui-laydate-content .laydate-day-next,.layui-laydate-content .laydate-day-prev{color: var(--lay-color-text-3)}
.layui-laydate-linkage .laydate-selected.laydate-day-next>div,.layui-laydate-linkage .laydate-selected.laydate-day-prev>div{background: none!important}
.layui-laydate-footer{border-top:1px solid var(--lay-color-border-2)}
.layui-laydate-hint{color:var(--lay-color-danger)}
.laydate-day-mark::after{background-color:var(--lay-color-secondary)}
.layui-laydate-footer span[lay-type=date]{color:var(--lay-color-secondary)}
.layui-laydate .layui-this,.layui-laydate .layui-this>div{background-color:var(--lay-color-secondary)!important;color: var(--lay-color-white)!important}
.layui-laydate .laydate-disabled,.layui-laydate .laydate-disabled:hover{color: var(--lay-color-text-4)!important}
.layui-laydate .layui-this.laydate-disabled,.layui-laydate .layui-this.laydate-disabled>div{background-color: var(--lay-color-fill-1) !important;color: var(--lay-color-text-4) !important;}
.laydate-theme-molv .layui-laydate-header{background-color:var(--lay-color-primary)}
.laydate-theme-molv .layui-laydate-header i,.laydate-theme-molv .layui-laydate-header span{color:var(--lay-color-gray-2)}
.laydate-theme-molv .layui-laydate-header i:hover,.laydate-theme-molv .layui-laydate-header span:hover{color: var(--lay-color-white)}
.laydate-theme-molv .layui-laydate-content{border:1px solid var(--lay-color-border-2)}
.laydate-theme-molv .layui-this, .laydate-theme-molv .layui-this>div{background-color: var(--lay-color-primary) !important;}
.laydate-theme-molv .layui-laydate-footer{border:1px solid var(--lay-color-border-2)}
.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li,.laydate-theme-grid .layui-laydate-content td,.laydate-theme-grid .layui-laydate-content thead{border:1px solid var(--lay-color-border-2)}
.layui-laydate-linkage.laydate-theme-grid .laydate-selected,.layui-laydate-linkage.laydate-theme-grid .laydate-selected:hover{background-color:var(--lay-color-gray-3)!important;color:var(--lay-color-primary)!important}
.layui-laydate-linkage.laydate-theme-grid .laydate-selected.laydate-day-next,.layui-laydate-linkage.laydate-theme-grid .laydate-selected.laydate-day-prev{color:var(--lay-color-gray-6)!important}
.layui-laydate.laydate-theme-circle .layui-laydate-content table td.layui-this{background-color:transparent!important}
/*layer*/
.layui-layer{background-color: var(--lay-color-bg-3);box-shadow:var(--lay-shadow-3)}
.layui-layer-border{border:1px solid var(--lay-color-border-2);box-shadow:var(--lay-shadow-3)}
.layui-layer-move{background-color: var(--lay-color-bg-5)}
.layui-layer-title{border-bottom:1px solid var(--lay-color-border-2);color: var(--lay-color-text-1)}
.layui-layer-setwin span{color: var(--lay-color-text-1)}
.layui-layer-setwin .layui-layer-min:before{border-bottom-color:var(--lay-color-text-1)}
.layui-layer-setwin .layui-layer-min:hover:before{border-bottom-color:var(--lay-color-info-hover)}
.layui-layer-setwin .layui-layer-max:after,.layui-layer-setwin .layui-layer-max:before{border:1px solid var(--lay-color-text-3)}
.layui-layer-setwin .layui-layer-max:hover:after,.layui-layer-setwin .layui-layer-max:hover:before{border-color:var(--lay-color-info-hover)}
.layui-layer-setwin .layui-layer-maxmin:after,.layui-layer-setwin .layui-layer-maxmin:before{background-color: var(--lay-color-bg-5)}
.layui-layer-setwin .layui-layer-close2{color:var(--lay-color-text-1);background-color:var(--lay-color-gray-10)}
.layui-layer-setwin .layui-layer-close2:hover{background-color:var(--lay-color-normal)}
.layui-layer-btn a{border:1px solid var(--lay-color-border-2);background-color: var( --lay-color-bg-3);color: var(--lay-color-text-2)}
.layui-layer-btn .layui-layer-btn0{border-color: transparent;background-color: var(--lay-color-normal);color: var(--lay-color-text-1)}
.layui-layer-dialog .layui-layer-content .layui-layer-face{color:var(--lay-color-gray-9)}
.layui-layer-dialog .layui-layer-content .layui-icon-tips{color:var(--lay-color-warning)}
.layui-layer-dialog .layui-layer-content .layui-icon-success{color: var(--lay-color-success)}
.layui-layer-dialog .layui-layer-content .layui-icon-error{top: 19px; color: var(--lay-color-danger)}
.layui-layer-dialog .layui-layer-content .layui-icon-question{color: var(--lay-color-warning);}
.layui-layer-dialog .layui-layer-content .layui-icon-lock{color: var(--lay-color-gray-10)}
.layui-layer-dialog .layui-layer-content .layui-icon-face-cry{color:var(--lay-color-danger)}
.layui-layer-dialog .layui-layer-content .layui-icon-face-smile{color:var(--lay-color-success)}
.layui-layer-rim{border:6px solid var(--lay-color-gray-8);border:6px solid var(--lay-color-border-2)}
.layui-layer-msg{border:1px solid var( --lay-color-border-1)}
.layui-layer-hui{background-color: var(--lay-color-bg-3);color: var(--lay-color-text-1)}
.layui-layer-hui .layui-layer-close{color: var(--lay-color-white)}
.layui-layer-loading-icon{color:var(--lay-color-gray-9)}
.layui-layer-loading-2:after,.layui-layer-loading-2:before{border:3px solid var(--lay-color-gray-6)}
.layui-layer-loading-2:after{border-color:transparent;border-left-color: var(--lay-color-normal)}
.layui-layer-tips .layui-layer-content{box-shadow: var(--lay-shadow-3);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-tips i.layui-layer-TipsG{border-color:transparent}
.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{border-right-color:var(--lay-color-black)}
.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{border-bottom-color:var(--lay-color-black)}
.layui-layer-lan .layui-layer-title{background:var(--lay-color-blue-5);color: var(--lay-color-text-1)}
.layui-layer-lan .layui-layer-btn{border-top:1px solid var(--lay-color-border-3)}
.layui-layer-lan .layui-layer-btn a{background: var(--lay-color-white);border-color:var(--lay-color-border-3);color: var(--lay-color-black-7)}
.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background: var(--lay-color-gray-7)}
.layui-layer-molv .layui-layer-title{background:var(--lay-color-layuigreen-6);color: var(--lay-color-text-1)}
.layui-layer-molv .layui-layer-btn a{background:var(--lay-color-layuigreen-6);border-color:var(--lay-color-layuigreen-6)}
.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:var(--lay-color-gray-7)}
.layui-layer-win10{border-color: var(--lay-color-border-2)}
.layui-layer-win10 .layui-layer-btn{background-color: var(--lay-color-bg-2);border-color: var(--lay-color-border-2)}
.layui-layer-win10.layui-layer-dialog .layui-layer-content{color: var(--lay-color-blue-7)}
.layui-layer-win10 .layui-layer-btn .layui-layer-btn0{border-color: var(--lay-color-blue-9);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-win10 .layui-layer-btn .layui-layer-btn1{border-color: var(--lay-color-border-2);background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-win10 .layui-layer-btn a:hover{background-color: var(--lay-color-blue-10);border-color: var(--lay-color-blue-8)}
.layui-layer-prompt .layui-layer-input{border:1px solid var(--lay-color-border-2);color: var(--lay-color-text-2)}
.layui-layer-tab{box-shadow:var(--lay-shadow-3)}
.layui-layer-tab .layui-layer-title span.layui-this{border-left:1px solid var(--lay-color-border-2);border-right:1px solid var(--lay-color-border-2);background-color: var(--lay-color-bg-3)}
.layui-layer-photos{background: none; box-shadow: none;}
.layui-layer-photos-prev,.layui-layer-photos-next{color:var(--lay-color-gray-9)}
.layui-layer-photos-prev:hover,.layui-layer-photos-next:hover{color:var(--lay-color-text-1)}
.layui-layer-photos-toolbar{background-color:#333;background-color: var(--lay-color-bg-5);color: var(--lay-color-text-1)}
.layui-layer-photos-toolbar *{color: var(--lay-color-text-1)}
.layui-layer-photos-toolbar a:hover{color: var(--lay-color-text-2)}
.layui-layer-photos-header > span:hover{background-color: var(--lay-color-fill-2)}
.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{border-right-color: var(--lay-color-bg-5)}
.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{border-bottom-color: var(--lay-color-bg-5)}
.layui-layer-prompt .layui-layer-input{border:1px solid var(--lay-color-border-2);color:var(--lay-color-text-1);background-color:var(--lay-color-black)}
.layui-layer-prompt .layui-layer-input:focus{outline:0}
/*fix style*/
.layui-layer-loading{background:0 0;box-shadow:0 0}
.layui-btn-primary{border-color:transparent}
.layui-btn-group .layui-btn:first-child{border-left:none}
.layui-btn-group .layui-btn-primary:hover{border-top-color:transparent; border-bottom-color: transparent;}
.layui-menu li:hover{background-color:var(--lay-color-fill-2)}
.layui-nav-child dd.layui-this{background-color:var(--lay-color-fill-2)}
.layui-nav .layui-nav-child a:hover{background-color:var(--lay-color-fill-2)}
.layui-nav .layui-nav-item a:hover,.layui-nav .layui-this a{background-color: var(--lay-color-fill-2)}
.layui-nav-child dd.layui-this{background-color: var(--lay-color-fill-2)}
.layui-tab-card>.layui-tab-title .layui-this:after{border-bottom-color:var(--lay-color-bg-1)}
.layui-form-select dl dd:hover{background-color:var(--lay-color-fill-2)}
.layui-form-select dl dd.layui-this{background-color:var(--lay-color-fill-2)}
.layui-laypage button{color:var(--lay-color-text-1)}
.layui-table[lay-even] tbody tr:nth-child(even){background-color:var(--lay-color-fill-4)}
.layui-menu .layui-menu-item-checked,.layui-menu .layui-menu-item-checked2{background-color:var(--lay-color-fill-2)!important}
.layui-input-split{background-color: var(--lay-color-bg-2);}
.layui-input-wrap .layui-input-prefix.layui-input-split{border-width: 1px;}
.layui-input-wrap .layui-input-split:has(+.layui-input:hover) {border-color: var(--lay-color-border-2);}
.layui-input-wrap .layui-input-split:has(+.layui-input:focus) {border-color: var(--lay-color-secondary-hover);}
.layui-layer-tab .layui-layer-title span:first-child{border-left: none !important;}
.layui-slider-input.layui-input,
.layui-slider-input .layui-input {background-color: var(--lay-color-bg-2);}

View File

@@ -1,240 +0,0 @@
<style>
:root{
--ms-color-bg: #FFF;
}
:root.dark{
--ms-color-bg: #000;
}
.ms-color-palette{
font-size:0;
}
.ms-color-palette .ms-color-gradient:nth-child(10n){
margin-right: 20px;
}
.ms-color-palette .ms-color-gradient:nth-child(20n){
display: table-column;
}
.ms-color-gradient{
display: inline-block;
position: relative;
width: 20px;
height:20px;
margin-bottom: 15px;
margin-right: 5px;
transition-duration:0.1s;
}
.ms-color-gradient:hover{
transform:scale(1.5);
z-index: 999;
}
.ms-color-edit-picker>div{
border-color: transparent !important;
}
.ms-color-edit-picker i{
display: none;
}
.ms-color-edit-picker span{
border-color: #5f5f60;
}
.ms-color-edit-picker .layui-colorpicker-trigger-bgcolor{
display: inline;
}
</style>
<div style="padding: 20px;background-color: var(--ms-color-bg);">
<div style="display: flex;justify-content: space-between;align-items: center;">
<div style="display: flex;" >
<label style="display: flex; position: relative; top: 1px; margin-right: 10px;" title="降低饱和度,提高亮度,暗色下更舒适">
深色色板
<input name="colorpicker" type="checkbox" style="height: 20px; width: 20px;">
</label>
<label title="类名/属性名例如 .dark,[theme-mode='dark']">
自定义主题类/属性选择器
<input name="theme-prefix" type="input" style="height: 18px; width: 150px;">
</label>
<i id="theme-prefix-tips" class="layui-icon layui-icon-tips" style="position: relative; top: 3.5px;margin-left: 2px;"></i>
</div>
<div style="display: flex;" >
<span title="重置"><i id="resetTheme" style="font-size: 23px;" class="layui-icon layui-icon-refresh"></i></span>
<span title="下载"><i id="downloadCSS" style="font-size: 23px;margin-left: 10px;" class="layui-icon layui-icon-download-circle"></i></span>
</div>
</div>
<hr>
<div class="ms-color-palette">
<!-- 色板 tpl 生成 -->
</div>
<div class="ms-color-edit">
<!-- 编辑区 tpl 生成 -->
</div>
</div>
<template id="tpl-color-palette">
{{# layui.each(d.ColorPaletteLight, function(key, val){ }}
<div class="ms-color-gradient" style="background-color: var({{- key }});" title="{{- key.replace('--lay-color-','') }}"></div>
{{# }) }}
</template>
<template id="tpl-color-editable">
{{# layui.each(d.editable, function(key, val){ }}
<div style="display: flex; align-items: center; height:30px">
<div>{{= key }}</div>
<div style="flex: 1 1 auto;"></div>
<input type="text" name="color" value="{{= val }}" placeholder=""
style="text-align: right;height:28px;width: 150px;background-color: transparent;border-color: transparent;">
<div class="ms-color-edit-picker" lay-options="{color: '{{= val }}', format: '{{- /^rgb/.test(val) ? 'rgb':'hex' }}' }" data-key="{{= key}}"></div>
</div>
{{# }) }}
</template>
<template id="tpl-theme-prefix-example">
<pre class="layui-code code-demo" lay-options="{}" style="margin: 0; padding: 0;">
/** .dark通过改变 html 标签的类名切换主题*/
:root{ :root.dark{
--color-bg: #000; --color-bg: #000;
} ==> }
.lay-card{ .dark .lay-card{}
color: #FFF; color: #FFF;
} }
/** js */
// 设置为暗色主题
document.documentElement.classList.add('dark')
// 恢复亮色主题
document.documentElement.classList.remove('dark')
// 切换亮/暗主题
document.documentElement.classList.toggle('dark')
----------------------------------------------------------
/** [theme-mode='dark'],通过改变 html 标签上 theme-mode 的属性切换主题*/
:root{ :root[theme-mode='dark']{
--color-bg: #000; --color-bg: #000;
} ==> }
.lay-card{ [theme-mode='dark'] .lay-card{}
color: #FFF; color: #FFF;
} }
/** js */
// 设置为暗色主题
document.documentElement.setAttribute('theme-mode', 'dark')
// 恢复亮色主题
document.documentElement.removeAttribute('theme-mode');
</pre>
</template>
<script src="static/lib/less.js"></script>
<script>
layui.use(async ()=> {
const {jquery:$,laytpl,colorpicker,layer} = layui;
const originalData=await (await fetch('./assets/themes.json')).json()
let customTheme = {};
laytpl($('#tpl-color-palette').html()).render(originalData,function(str) {
$('.ms-color-palette').html(str);
});
laytpl($('#tpl-color-editable').html()).render(originalData,function(str) {
$('.ms-color-edit').html(str);
renderColorPicker();
});
$('input[name=colorpicker]').on('click',function(){
applyColorPalette(this.checked)
})
$('#resetTheme').on('click',function(){
resetTheme()
$('input[name=colorpicker]').prop('checked',false)
})
$('#downloadCSS').on('click',function(){
dropdownCSS()
})
$('#theme-prefix-tips').hover(function(){
layer.tips($('#tpl-theme-prefix-example').html(),this,{
tips: 3,
time: false,
area: ['700px','auto'],
success: function(layero){
layui.code({elem: '.code-demo'});
layero.find('.layui-layer-content').css({padding:0,margin:0})
layero.css('top','50px') //阻止反转
}
});
},function(){
layer.closeLast('tips');
})
function renderColorPicker(){
colorpicker.render({
elem: '.ms-color-edit-picker',
alpha: true,
change: function(color) {
const elem=this.elem
elem.prev().val(color)
applyTheme(elem.data('key'),color)
},
done: function(color) {
const elem=this.elem
elem.prev().val(color)
applyTheme(elem.data('key'),color)
},
close: function(color){
const elem=this.elem
elem.prev().val(color)
applyTheme(elem.data('key'),color)
}
});
}
function resetTheme() {
customTheme={}
addStyle('demo-customTheme',getCSS(customTheme))
renderColorPicker()
}
function applyTheme(key,val){
customTheme = {...customTheme,...{[key]:val} }
addStyle('demo-customTheme',getCSS(customTheme))
}
function applyColorPalette(isDark=false){
customTheme={...customTheme, ...originalData[isDark ? 'ColorPaletteDark' : 'ColorPaletteLight']}
addStyle('demo-customTheme',getCSS(customTheme))
}
function getCSS(cssVarsObj){
return `:root {\n ${
Object.entries(cssVarsObj)
.map(([key,val])=> ` ${key}: ${val};`)
.join('\n')
}\n}`
}
async function dropdownCSS(){
const hasPrefix = $('input[name="theme-prefix"]').val()
const overrideCSS = await(await fetch('./static/src/override.css')).text()
const varsCSS = getCSS({...originalData.Default, ...customTheme})
let finalCSS=`
${hasPrefix
? varsCSS.replace(':root',`:root${hasPrefix}`)
: varsCSS}\n
${hasPrefix
? `${hasPrefix}{${overrideCSS}}`
: overrideCSS}`;
// css-next 插件太大,暂时用 less
finalCSS = (await window.less.render(finalCSS)).css
const alink=document.createElement("a")
alink.download='layui-theme-dark-custom.css'
alink.href=URL.createObjectURL(new Blob([finalCSS]))
document.body.appendChild(alink)
alink.click()
document.body.removeChild(alink)
}
function addStyle(id,cssStr) {
var el=document.getElementById(id)||document.createElement('style')
if(!el.isConnected) {
el.type='text/css';
el.id=id;
document.head.appendChild(el);
}
el.textContent=cssStr;
}
})
</script>

View File

@@ -1,661 +0,0 @@
{{ define "apis.html" }}
<section>
<h2>接口设置</h2>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="apiFilterForm" lay-filter="apiFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">应用</label>
<div class="layui-input-inline">
<select name="app_uuid" lay-filter="appSelect" lay-search="">
<option value="">全部应用</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">类型</label>
<div class="layui-input-inline">
<select name="api_type" lay-filter="apiTypeSelect">
<option value="">请选择接口类型</option>
</select>
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn layui-btn-primary" id="btnResetAPIs">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">接口列表</h3>
<div style="padding: 20px;">
<table id="apisTable" lay-filter="apisTableFilter"></table>
<script type="text/html" id="tpl-apis-ops">
<div style="white-space: nowrap;">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
</div>
</script>
<script type="text/html" id="tpl-apis-status">
{{`{{# if(d.status === 1) { }}`}}
<span class="layui-badge layui-bg-green">启用</span>
{{`{{# } else { }}`}}
<span class="layui-badge">禁用</span>
{{`{{# } }}`}}
</script>
</div>
</div>
<!-- 隐藏的表单弹层内容:编辑接口 -->
<div id="apiFormModal" style="display:none;padding:16px">
<form class="layui-form layui-form-pane" id="apiForm">
<input type="hidden" name="uuid" />
<!-- 关联应用与接口类型为固定项,移除编辑能力 -->
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="submit-algorithm">提交算法</label>
<div class="layui-input-block">
<select name="submit_algorithm" lay-verify="required" lay-filter="submitAlgorithm">
<option value="0">不加密</option>
<option value="1">RC4</option>
<option value="2">RSA</option>
<option value="3">RSA动态</option>
<option value="4">易加密</option>
</select>
</div>
</div>
<!-- 提交密钥/证书输入区:根据算法动态显示 -->
<div class="layui-form-item" id="submitKeysContainer" style="display:none">
<label class="layui-form-label" style="cursor: pointer;" data-tips="submit-keys">提交密钥</label>
<div class="layui-input-block">
<div id="submit-rc4" style="display:none">
<input type="text" name="submit_private_key" placeholder="RC4密钥16位十六进制大写" autocomplete="off"
class="layui-input" readonly />
</div>
<div id="submit-rsa" style="display:none">
<textarea name="submit_public_key" placeholder="RSA 公钥PEM 明文)" class="layui-textarea" rows="4"
readonly></textarea>
<textarea name="submit_private_key" placeholder="RSA 私钥PEM 明文)" class="layui-textarea" rows="6"
style="margin-top:8px;" readonly></textarea>
</div>
<div id="submit-easy" style="display:none">
<input type="text" name="submit_private_key" placeholder="易加密密钥15-30位整数数组逗号分隔" autocomplete="off"
class="layui-input" readonly />
</div>
<div style="margin-top:8px;">
<button type="button" class="layui-btn layui-btn-sm" id="btnGenSubmitKeys">重新生成</button>
<span class="layui-word-aux" style="margin-left:8px;">自动生成密钥,也可手动重新生成</span>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="return-algorithm">返回算法</label>
<div class="layui-input-block">
<select name="return_algorithm" lay-verify="required" lay-filter="returnAlgorithm">
<option value="0">不加密</option>
<option value="1">RC4</option>
<option value="2">RSA</option>
<option value="3">RSA动态</option>
<option value="4">易加密</option>
</select>
</div>
</div>
<!-- 返回密钥/证书输入区:根据算法动态显示 -->
<div class="layui-form-item" id="returnKeysContainer" style="display:none">
<label class="layui-form-label" style="cursor: pointer;" data-tips="return-keys">返回密钥</label>
<div class="layui-input-block">
<div id="return-rc4" style="display:none">
<input type="text" name="return_private_key" placeholder="RC4密钥16位十六进制大写" autocomplete="off"
class="layui-input" readonly />
</div>
<div id="return-rsa" style="display:none">
<textarea name="return_public_key" placeholder="RSA 公钥PEM 明文)" class="layui-textarea" rows="4"
readonly></textarea>
<textarea name="return_private_key" placeholder="RSA 私钥PEM 明文)" class="layui-textarea" rows="6"
style="margin-top:8px;" readonly></textarea>
</div>
<div id="return-easy" style="display:none">
<input type="text" name="return_private_key" placeholder="易加密密钥15-30位整数数组逗号分隔" autocomplete="off"
class="layui-input" readonly />
</div>
<div style="margin-top:8px;">
<button type="button" class="layui-btn layui-btn-sm" id="btnGenReturnKeys">重新生成</button>
<span class="layui-word-aux" style="margin-left:8px;">自动生成密钥,也可手动重新生成</span>
</div>
</div>
</div>
<div class="layui-form-item" pane>
<label class="layui-form-label" style="cursor: pointer;" data-tips="api-status">接口状态</label>
<div class="layui-input-block">
<input type="checkbox" name="status" lay-skin="switch" lay-text="启用|禁用" checked>
</div>
</div>
</form>
</div>
</section>
<script>
layui.use(['table', 'form', 'layer', 'dropdown'], function () {
const $ = layui.$;
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
var dropdown = layui.dropdown;
// 格式化时间函数
function formatDateTime(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
// 当前选中的应用UUID
var currentAppUUID = '';
// 复制到剪贴板函数
window.copyToClipboard = function (text) {
if (navigator.clipboard && window.isSecureContext) {
// 使用现代 Clipboard API
navigator.clipboard.writeText(text).then(function () {
layer.msg('接口地址已复制到剪贴板', { icon: 1, time: 2000 });
}).catch(function (err) {
console.error('复制失败:', err);
fallbackCopyTextToClipboard(text);
});
} else {
// 降级方案
fallbackCopyTextToClipboard(text);
}
};
// 降级复制方案
function fallbackCopyTextToClipboard(text) {
var textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand('copy');
if (successful) {
layer.msg('接口地址已复制到剪贴板', { icon: 1, time: 2000 });
} else {
layer.msg('复制失败,请手动复制', { icon: 2, time: 3000 });
}
} catch (err) {
console.error('复制失败:', err);
layer.msg('复制失败,请手动复制', { icon: 2, time: 3000 });
}
document.body.removeChild(textArea);
}
// 初始化接口表格
var apisTable = table.render({
elem: '#apisTable',
url: '/admin/api/apis/list',
parseData: function (res) {
return {
code: res.success ? 0 : 1,
msg: res.message || '',
count: res.data ? res.data.total : 0,
data: res.data ? res.data.apis : []
};
},
request: {
pageName: 'page',
limitName: 'limit'
},
method: 'GET',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
done: function (res, curr, count) {
// 表格渲染完成后的回调
},
cols: [[
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'app_name', title: '应用名称', minWidth: 150 },
{ field: 'api_type_name', title: '接口类型', minWidth: 120 },
{ field: 'uuid', title: 'UUID', minWidth: 335 },
{
field: 'status_name',
title: '状态',
width: 100,
templet: (d) => {
const checked = d.status === 1 ? 'checked' : '';
return `<input type="checkbox" ${checked} lay-skin="switch" lay-text="启用|禁用" lay-filter="api-status-switch" data-id="${d.id}">`;
}
},
{
field: 'submit_algorithm',
title: '提交算法',
width: 120,
templet: (d) => {
const algorithm = d.algorithm_names ? d.algorithm_names.submit : '-';
if (algorithm && algorithm.length > 10) {
return '<span title="' + algorithm + '">' + algorithm.substring(0, 10) + '...</span>';
}
return algorithm;
}
},
{
field: 'return_algorithm',
title: '返回算法',
width: 120,
templet: (d) => {
const algorithm = d.algorithm_names ? d.algorithm_names.return : '-';
if (algorithm && algorithm.length > 10) {
return '<span title="' + algorithm + '">' + algorithm.substring(0, 10) + '...</span>';
}
return algorithm;
}
},
{
field: 'created_at',
title: '创建时间',
width: 160,
templet: (d) => formatDateTime(d.created_at)
},
{
field: 'updated_at',
title: '修改时间',
width: 160,
templet: (d) => formatDateTime(d.updated_at)
},
{ fixed: 'right', title: '操作', toolbar: '#tpl-apis-ops', width: 80, align: 'center' }
]]
});
// 加载应用列表到筛选器
function loadApps() {
$.ajax({
url: '/admin/api/apps/simple',
type: 'GET',
success: function (res) {
if (res.code === 0 && res.data) {
var filterSelect = $('select[name="app_uuid"]').eq(0);
// 清空现有选项(保留默认选项)
filterSelect.find('option:not(:first)').remove();
// 添加应用选项(不默认选中,保持“请选择应用”以显示全部接口)
res.data.forEach(function (app) {
var option = '<option value="' + app.uuid + '">' + app.name + '(ID:' + app.id + ')' + '</option>';
filterSelect.append(option);
});
// 仅刷新下拉,不触发表格按应用过滤,默认显示全部接口
form.render('select');
}
},
error: function () {
layer.msg('加载应用列表失败', { icon: 2 });
}
});
}
// 加载接口类型列表到筛选器
function loadAPITypes() {
$.ajax({
url: '/admin/api/apis/types',
type: 'GET',
success: function (res) {
if (res.code === 0 && res.data) {
var typeSelect = $('select[name="api_type"]').eq(0);
// 清空现有选项(保留默认选项)
typeSelect.find('option:not(:first)').remove();
// 添加接口类型选项
res.data.forEach(function (type) {
var option = '<option value="' + type.value + '">' + type.name + '</option>';
typeSelect.append(option);
});
// 刷新下拉
form.render('select');
}
},
error: function () {
layer.msg('加载接口类型列表失败', { icon: 2 });
}
});
}
// 初始化加载应用列表和接口类型列表
loadApps();
loadAPITypes();
// 监听应用选择变化
form.on('select(appSelect)', function (data) {
currentAppUUID = data.value;
apisTable.reload({
where: {
app_uuid: currentAppUUID,
api_type: $('select[name="api_type"]').val()
},
page: {
curr: 1
}
});
});
// 监听接口类型选择变化
form.on('select(apiTypeSelect)', function (data) {
apisTable.reload({
where: {
app_uuid: $('select[name="app_uuid"]').val(),
api_type: data.value
},
page: {
curr: 1
}
});
});
// 重置筛选
$('#btnResetAPIs').on('click', function () {
$('#apiFilterForm')[0].reset();
form.render();
apisTable.reload({
where: {},
page: {
curr: 1
}
});
});
// 算法联动与一键生成
function refreshSubmitKeysUI(row) {
var algo = parseInt($('select[name="submit_algorithm"]').val());
if (algo === 0) {
$('#submitKeysContainer').hide();
$('#submit-rc4').hide();
$('#submit-rsa').hide();
$('#submit-easy').hide();
$('[name="submit_public_key"],[name="submit_private_key"]').val('');
return;
}
$('#submitKeysContainer').show();
if (algo === 1) { // RC4
$('#submit-rc4').show();
$('#submit-rsa').hide();
$('#submit-easy').hide();
if (row && row.submit_private_key) {
$('[name="submit_private_key"]').val(row.submit_private_key);
}
} else if (algo === 4) { // 易加密
$('#submit-rc4').hide();
$('#submit-rsa').hide();
$('#submit-easy').show();
if (row && row.submit_private_key) {
$('[name="submit_private_key"]').val(row.submit_private_key);
}
} else { // RSA & RSA动态
$('#submit-rc4').hide();
$('#submit-rsa').show();
$('#submit-easy').hide();
if (row && (row.submit_public_key || row.submit_private_key)) {
$('[name="submit_public_key"]').val(row.submit_public_key || '');
$('[name="submit_private_key"]').val(row.submit_private_key || '');
}
}
}
function refreshReturnKeysUI(row) {
var algo = parseInt($('select[name="return_algorithm"]').val());
if (algo === 0) {
$('#returnKeysContainer').hide();
$('#return-rc4').hide();
$('#return-rsa').hide();
$('#return-easy').hide();
$('[name="return_public_key"],[name="return_private_key"]').val('');
return;
}
$('#returnKeysContainer').show();
if (algo === 1) { // RC4
$('#return-rc4').show();
$('#return-rsa').hide();
$('#return-easy').hide();
if (row && row.return_private_key) {
$('[name="return_private_key"]').val(row.return_private_key);
}
} else if (algo === 4) { // 易加密
$('#return-rc4').hide();
$('#return-rsa').hide();
$('#return-easy').show();
if (row && row.return_private_key) {
$('[name="return_private_key"]').val(row.return_private_key);
}
} else { // RSA & RSA动态
$('#return-rc4').hide();
$('#return-rsa').show();
$('#return-easy').hide();
if (row && (row.return_public_key || row.return_private_key)) {
$('[name="return_public_key"]').val(row.return_public_key || '');
$('[name="return_private_key"]').val(row.return_private_key || '');
}
}
}
// 自动生成密钥函数
function autoGenerateKeys(side, algorithm) {
if (algorithm === 0) return; // 不加密不需要生成密钥
var prefix = side === 'submit' ? 'submit' : 'return';
// 先清空所有相关输入框
$('[name="' + prefix + '_public_key"]').val('');
$('[name="' + prefix + '_private_key"]').val('');
$.ajax({
url: '/admin/api/apis/generate_keys',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ side: side, algorithm: algorithm }),
success: function (res) {
if (res.code === 0 && res.data) {
if (algorithm === 1) { // RC4
$('[name="' + prefix + '_private_key"]').val(res.data.private_key || '');
} else if (algorithm === 4) { // 易加密
$('[name="' + prefix + '_private_key"]').val(res.data.private_key || '');
} else { // RSA & RSA动态
$('[name="' + prefix + '_public_key"]').val(res.data.public_key || '');
$('[name="' + prefix + '_private_key"]').val(res.data.private_key || '');
}
layer.msg('已自动生成' + (side === 'submit' ? '提交' : '返回') + '密钥', { icon: 1, time: 1500 });
} else {
layer.msg(res.msg || '自动生成密钥失败', { icon: 2 });
}
},
error: function () {
layer.msg('自动生成密钥失败', { icon: 2 });
}
});
}
form.on('select(submitAlgorithm)', function (data) {
refreshSubmitKeysUI();
var algo = parseInt(data.value);
autoGenerateKeys('submit', algo);
});
form.on('select(returnAlgorithm)', function (data) {
refreshReturnKeysUI();
var algo = parseInt(data.value);
autoGenerateKeys('return', algo);
});
$('#btnGenSubmitKeys').on('click', function () {
var algo = parseInt($('select[name="submit_algorithm"]').val());
if (algo === 0) { layer.msg('请选择加密算法', { icon: 0 }); return; }
// 先清空所有相关输入框
$('[name="submit_public_key"]').val('');
$('[name="submit_private_key"]').val('');
$.ajax({
url: '/admin/api/apis/generate_keys',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ side: 'submit', algorithm: algo }),
success: function (res) {
if (res.code === 0 && res.data) {
if (algo === 1) { // RC4
$('[name="submit_private_key"]').val(res.data.private_key || '');
} else if (algo === 4) { // 易加密
$('[name="submit_private_key"]').val(res.data.private_key || '');
} else { // RSA
$('[name="submit_public_key"]').val(res.data.public_key || '');
$('[name="submit_private_key"]').val(res.data.private_key || '');
}
} else {
layer.msg(res.msg || '生成失败', { icon: 2 });
}
},
error: function () { layer.msg('生成失败', { icon: 2 }); }
});
});
$('#btnGenReturnKeys').on('click', function () {
var algo = parseInt($('select[name="return_algorithm"]').val());
if (algo === 0) { layer.msg('请选择加密算法', { icon: 0 }); return; }
// 先清空所有相关输入框
$('[name="return_public_key"]').val('');
$('[name="return_private_key"]').val('');
$.ajax({
url: '/admin/api/apis/generate_keys',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ side: 'return', algorithm: algo }),
success: function (res) {
if (res.code === 0 && res.data) {
if (algo === 1) { // RC4
$('[name="return_private_key"]').val(res.data.private_key || '');
} else if (algo === 4) { // 易加密
$('[name="return_private_key"]').val(res.data.private_key || '');
} else { // RSA
$('[name="return_public_key"]').val(res.data.public_key || '');
$('[name="return_private_key"]').val(res.data.private_key || '');
}
} else {
layer.msg(res.msg || '生成失败', { icon: 2 });
}
},
error: function () { layer.msg('生成失败', { icon: 2 }); }
});
});
// 监听表格工具条
table.on('tool(apisTableFilter)', function (obj) {
var data = obj.data;
if (obj.event === 'edit') {
// 编辑接口
$('#apiForm')[0].reset();
$('input[name="uuid"]').val(data.uuid);
$('select[name="submit_algorithm"]').val(data.submit_algorithm);
$('select[name="return_algorithm"]').val(data.return_algorithm);
$('input[name="status"]').prop('checked', data.status === 1);
// 根据现有算法与密钥填充/显示输入区
refreshSubmitKeysUI(data);
refreshReturnKeysUI(data);
layer.open({
type: 1,
title: '编辑接口',
content: $('#apiFormModal'),
area: ['500px', '520px'],
btn: ['保存', '取消'],
yes: function (index, layero) {
// 手动收集表单数据
var formData = {};
$('#apiForm').find('input, select, textarea').each(function () {
var $this = $(this);
var name = $this.attr('name');
if (name) {
if ($this.attr('type') === 'checkbox') {
if ($this.attr('lay-skin') === 'switch') {
formData[name] = $this.prop('checked') ? 1 : 0;
} else {
formData[name] = $this.prop('checked') ? $this.val() : '';
}
} else if ($this.attr('type') === 'radio') {
if ($this.prop('checked')) {
formData[name] = $this.val();
}
} else {
formData[name] = $this.val();
}
}
});
// 转换数值类型
formData.submit_algorithm = parseInt(formData.submit_algorithm);
formData.return_algorithm = parseInt(formData.return_algorithm);
$.ajax({
url: '/admin/api/apis/update',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(formData),
success: function (res) {
if (res.code === 0) {
layer.msg('接口更新成功', { icon: 1 });
layer.close(index);
apisTable.reload();
} else {
layer.msg(res.msg || '更新失败', { icon: 2 });
}
},
error: function () {
layer.msg('网络错误,请稍后重试', { icon: 2 });
}
});
},
btn2: function (index) {
layer.close(index);
},
success: function () {
form.render();
},
shadeClose: false
});
}
});
// 接口状态switch开关事件监听
form.on('switch(api-status-switch)', function(data) {
const apiId = data.elem.getAttribute('data-id');
const status = data.elem.checked ? 1 : 0;
$.ajax({
url: '/admin/api/apis/update_status',
type: 'POST',
data: JSON.stringify({ id: parseInt(apiId), status: status }),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg || '状态更新成功', { icon: 1 });
} else {
layer.msg(res.msg || '状态更新失败', { icon: 2 });
// 如果更新失败,恢复开关状态
data.elem.checked = !data.elem.checked;
form.render('checkbox');
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '状态更新失败', { icon: 2 });
// 如果更新失败,恢复开关状态
data.elem.checked = !data.elem.checked;
form.render('checkbox');
}
});
});
});
</script>
{{ end }}

File diff suppressed because it is too large Load Diff

View File

@@ -1,181 +0,0 @@
{{ define "dashboard.html" }}
<section>
<h2>系统信息</h2>
<div class="layui-row layui-col-space15" style="margin-top:12px">
<!-- 系统信息面板 -->
<div class="layui-col-md8">
<div class="layui-panel">
<div style="padding: 20px;">
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">系统信息</h3>
<table class="layui-table" lay-skin="nob">
<tbody>
<tr>
<td style="width: 120px; font-weight: bold;">程序版本</td>
<td style="height: 20px; vertical-align: middle;">
<span class="layui-badge layui-bg-blue" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">v{{ .Version }}</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">存储方案</td>
<td style="height: 20px; vertical-align: middle;">
<span class="layui-badge layui-bg-cyan" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .DBType }}</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">开发模式</td>
<td style="height: 20px; vertical-align: middle;">
{{ if .Mode }}
<span class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">开启</span>
{{ else }}
<span class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">关闭</span>
{{ end }}
</td>
</tr>
<tr>
<td style="font-weight: bold;">运行时长</td>
<td style="height: 20px; vertical-align: middle;">
<span id="uptime-display" class="layui-badge layui-bg-gray" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .Uptime }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 系统统计面板 (预留) -->
<div class="layui-col-md4">
<div class="layui-panel">
<div style="padding: 20px;">
<h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">系统统计</h3>
<table class="layui-table" lay-skin="nob">
<tbody>
<tr>
<td style="width: 120px; font-weight: bold;">应用总数</td>
<td style="height: 20px; vertical-align: middle;">
<span id="total-apps" class="layui-badge" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">启用应用</td>
<td style="height: 20px; vertical-align: middle;">
<span id="enabled-apps" class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">禁用应用</td>
<td style="height: 20px; vertical-align: middle;">
<span id="disabled-apps" class="layui-badge layui-bg-gray" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">变量总数</td>
<td style="height: 20px; vertical-align: middle;">
<span id="total-variables" class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<h2 style="margin-top: 20px;">最近登录日志</h2>
<div class="layui-panel" style="margin-top:12px">
<div style="padding: 20px;">
<table id="loginLogsTable" lay-filter="loginLogsTable"></table>
</div>
</div>
</section>
<script>
// 仪表盘脚本
layui.use(['jquery', 'table', 'util'], () => {
const $ = layui.$;
const table = layui.table;
const util = layui.util;
// 刷新基本信息和运行状态
const refreshSystemInfo = () => {
$.get('/admin/api/system/info', (res) => {
if (res && res.code === 0 && res.data) {
const data = res.data;
if (data.uptime) {
const uptimeElement = $('#uptime-display');
uptimeElement.text(data.uptime);
if (!uptimeElement.hasClass('layui-badge')) {
uptimeElement.addClass('layui-badge layui-bg-gray');
uptimeElement.css({
'font-size': '14px',
'padding': '2px 8px',
'line-height': '1.2'
});
}
}
}
}).fail(() => {
console.log('获取系统信息失败');
});
};
// 刷新系统统计数据
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(() => {
$('#total-apps').text('0');
$('#enabled-apps').text('0');
$('#disabled-apps').text('0');
$('#total-variables').text('0');
});
};
// 立即刷新一次
refreshSystemInfo();
refreshAppStats();
// 渲染登录日志表格
table.render({
elem: '#loginLogsTable',
url: '/admin/api/dashboard/login-logs',
page: true,
limit: 10,
limits: [10, 20, 30, 50],
cols: [[
{field: 'created_at', title: '登录时间', width: 180, templet: (d) => {
return util.toDateString(d.created_at);
}},
{field: 'username', title: '用户名', width: 150},
{field: 'ip', title: '登录IP', width: 150},
{field: 'status', title: '状态', width: 100, align: 'center', templet: (d) => {
return d.status === 1 ?
'<span class="layui-badge layui-bg-green">成功</span>' :
'<span class="layui-badge layui-bg-red">失败</span>';
}},
{field: 'message', title: '详情', minWidth: 150},
{field: 'user_agent', title: 'User Agent', minWidth: 200, templet: (d) => {
return '<div title="'+d.user_agent+'" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">'+d.user_agent+'</div>';
}}
]],
response: {
statusCode: 0
},
parseData: (res) => {
return {
"code": res.code,
"msg": res.msg,
"count": res.data ? res.data.total : 0,
"data": res.data ? res.data.list : []
};
}
});
});
</script>
{{ end }}

View File

@@ -1,489 +0,0 @@
{{ define "functions.html" }}
<section>
<h2>公共函数</h2>
<div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddFunction"><i class="layui-icon layui-icon-add-1"></i> 新增函数</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteFunctions"><i class="layui-icon layui-icon-delete"></i>
批量删除</button>
</div>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="functionFilterForm" lay-filter="functionFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">应用筛选</label>
<div class="layui-input-inline">
<select name="filter_app_uuid" lay-search lay-filter="appSelect">
<option value="">全部应用</option>
<option value="0">全局函数</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">搜索</label>
<div class="layui-input-inline">
<input type="text" name="search" placeholder="函数编号/别名/代码/备注" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnSearchFunctions">查询</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetFunctions">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">函数列表</h3>
<div style="padding: 20px;">
<table id="functionsTable" lay-filter="functionsTableFilter"></table>
</div>
</div>
<!-- 表格操作模板 -->
<script type="text/html" id="tpl-functions-ops">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<!-- 隐藏的表单弹层内容:新增/编辑函数 -->
<div id="functionFormLayer" style="display:none;padding:20px">
<form class="layui-form layui-form-pane" lay-filter="functionForm" id="functionForm">
<input type="hidden" name="uuid">
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="function-alias">函数别名</label>
<div class="layui-input-block">
<input type="text" name="alias" lay-verify="required|alias" placeholder="请输入函数别名(英文开头,只能包含数字和英文字母)"
autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="function-app">关联应用</label>
<div class="layui-input-block">
<select name="app_uuid" lay-search>
<option value="0">全局函数</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="function-code">函数代码</label>
<div class="layui-input-block">
<textarea name="code" placeholder="请输入函数代码" lay-verify="required" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="function-remark">备注</label>
<div class="layui-input-block">
<textarea name="remark" placeholder="请输入备注信息" class="layui-textarea"></textarea>
</div>
</div>
</form>
</div>
<script>
// 等待layui加载完成
function waitForLayui(callback) {
if (typeof layui !== 'undefined') {
callback();
} else {
setTimeout(() => waitForLayui(callback), 100);
}
}
waitForLayui(function () {
layui.use(['table', 'form', 'layer', 'element'], function () {
const table = layui.table;
const form = layui.form;
const layer = layui.layer;
const $ = layui.$;
// 全局应用列表
let appsList = [];
// 自定义验证规则
form.verify({
alias: function (value) {
if (!value) return '别名不能为空';
// 检查是否以英文字母开头,且只包含数字和英文字母
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) {
return '别名必须以英文字母开头,只能包含数字和英文字母';
}
}
});
// 格式化时间函数
function formatDateTime(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
// 根据应用UUID获取应用名称和ID并添加颜色徽章
function getAppName(appUUID) {
if (appUUID === '0') {
return '<span class="layui-badge layui-bg-blue">全局函数</span>';
}
const app = appsList.find(app => app.uuid === appUUID);
if (app) {
return '<span class="layui-badge layui-bg-green">' + app.name + '(ID:' + app.id + ')' + '</span>';
} else {
return '<span class="layui-badge">未知应用</span>';
}
}
// 加载应用列表
function loadAppList() {
$.ajax({
url: '/admin/api/apps/simple',
type: 'GET',
success: function (res) {
if (res.code === 0 && res.data) {
// 保存应用列表到全局变量
appsList = res.data;
const filterSelect = $('form[lay-filter="functionFilterForm"] select[name="filter_app_uuid"]');
const formSelect = $('#functionForm select[name="app_uuid"]');
// 清空现有选项(保留默认选项:全部应用和全局函数)
filterSelect.find('option:not([value=""]):not([value="0"])').remove();
formSelect.find('option:not([value=""]):not([value="0"])').remove();
// 添加应用选项
res.data.forEach(function(app) {
const option = '<option value="' + app.uuid + '">' + app.name + '(ID:' + app.id + ')' + '</option>';
filterSelect.append(option);
formSelect.append(option);
});
// 重新渲染表单
form.render('select');
}
},
error: function(xhr) {
console.log('加载应用列表失败:', xhr.responseText);
}
});
}
// 页面加载时获取应用列表
loadAppList();
// 渲染表格
const functionsTable = table.render({
elem: '#functionsTable',
id: 'functionsTable',
url: '/admin/function/list',
parseData: function (res) {
return {
code: res.code,
msg: res.msg || '',
count: res.count || 0,
data: res.data || []
};
},
request: {
pageName: 'page',
limitName: 'page_size'
},
method: 'GET',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
done: function (res, curr, count) {
// 表格渲染完成后的回调
},
cols: [[
{ type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'number', title: '函数编号', width: 180 },
{
field: 'app_uuid',
title: '关联应用',
minWidth: 180,
templet: function (d) {
return getAppName(d.app_uuid);
}
},
{ field: 'alias', title: '函数别名', minWidth: 150 },
{
field: 'code',
title: '函数代码',
minWidth: 200,
templet: function (d) {
// 限制显示长度,避免内容过长影响布局
if (d.code && d.code.length > 50) {
return '<span title="' + d.code + '">' + d.code.substring(0, 50) + '...</span>';
}
return d.code || '-';
}
},
{
field: 'remark',
title: '备注',
minWidth: 150,
templet: function (d) {
// 限制显示长度,避免内容过长影响布局
if (d.remark && d.remark.length > 30) {
return '<span title="' + d.remark + '">' + d.remark.substring(0, 30) + '...</span>';
}
return d.remark || '-';
}
},
{
field: 'created_at',
title: '创建时间',
width: 180,
templet: function (d) {
return formatDateTime(d.created_at);
}
},
{ title: '操作', width: 120, align: 'center', toolbar: '#tpl-functions-ops', fixed: 'right' }
]]
});
// 搜索功能
$('#btnSearchFunctions').on('click', function () {
const searchData = {
search: $('input[name="search"]').val()
};
// 添加应用筛选
const appUUID = $('select[name="filter_app_uuid"]').val();
if (appUUID) {
searchData.app_uuid = appUUID;
}
functionsTable.reload({
where: searchData,
page: {
curr: 1
}
});
});
// 重置搜索
$('#btnResetFunctions').on('click', function () {
$('#functionFilterForm')[0].reset();
form.render();
functionsTable.reload({
where: {},
page: {
curr: 1
}
});
});
// 监听应用选择变化,实现联动筛选
form.on('select(appSelect)', function (data) {
const searchData = {
search: $('input[name="search"]').val()
};
// 添加应用筛选
if (data.value) {
searchData.app_uuid = data.value;
}
functionsTable.reload({
where: searchData,
page: {
curr: 1
}
});
});
// 新增函数
$('#btnAddFunction').on('click', function () {
$('#functionForm')[0].reset();
$('input[name="id"]').val('');
// 确保新增模式下别名输入框是启用的
$('input[name="alias"]').prop('disabled', false);
layer.open({
type: 1,
title: '新增函数',
content: $('#functionFormLayer'),
area: ['500px', '435px'],
btn: ['创建', '取消'],
yes: function (index, layero) {
// 手动收集表单数据
var formData = {};
$('#functionForm').find('input, select, textarea').each(function () {
var $this = $(this);
var name = $this.attr('name');
if (name && name !== 'id') {
formData[name] = $this.val();
}
});
// 验证必填字段
if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入函数别名', { icon: 2 });
return;
}
if (!formData.code || formData.code.trim() === '') {
layer.msg('请输入函数代码', { icon: 2 });
return;
}
$.ajax({
url: '/admin/function/create',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
layer.close(index);
functionsTable.reload();
} else {
layer.msg(res.msg || '操作失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', { icon: 2 });
}
});
},
btn2: function (index) {
layer.close(index);
},
success: function () {
form.render();
},
shadeClose: false
});
});
// 批量删除
$('#btnBatchDeleteFunctions').on('click', function () {
const checkStatus = table.checkStatus('functionsTable');
const data = checkStatus.data;
if (data.length === 0) {
layer.msg('请选择要删除的函数', { icon: 2 });
return;
}
layer.confirm('确定删除选中的 ' + data.length + ' 个函数吗?', { icon: 3, title: '提示' }, function (index) {
const ids = data.map(item => item.id);
$.ajax({
url: '/admin/function/batch_delete',
type: 'POST',
data: JSON.stringify({ ids: ids }),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
functionsTable.reload();
} else {
layer.msg(res.msg || '批量删除失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '批量删除失败', { icon: 2 });
}
});
layer.close(index);
});
});
// 表格工具栏事件
table.on('tool(functionsTableFilter)', function (obj) {
const data = obj.data;
if (obj.event === 'edit') {
// 编辑
$('#functionForm')[0].reset();
$('input[name="uuid"]').val(data.uuid);
$('input[name="alias"]').val(data.alias);
// 在编辑模式下禁用别名输入框
$('input[name="alias"]').prop('disabled', true);
$('select[name="app_uuid"]').val(data.app_uuid || '0');
$('textarea[name="code"]').val(data.code);
$('textarea[name="remark"]').val(data.remark);
layer.open({
type: 1,
title: '编辑函数',
content: $('#functionFormLayer'),
area: ['500px', '435px'],
btn: ['保存', '取消'],
yes: function (index, layero) {
// 手动收集表单数据
var formData = {};
$('#functionForm').find('input, select, textarea').each(function () {
var $this = $(this);
var name = $this.attr('name');
// 编辑模式下排除alias字段避免修改别名
if (name && name !== 'id' && name !== 'alias') {
formData[name] = $this.val();
}
});
// 验证必填字段编辑模式下不验证alias
if (!formData.code || formData.code.trim() === '') {
layer.msg('请输入函数代码', { icon: 2 });
return;
}
$.ajax({
url: '/admin/function/update',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
layer.close(index);
functionsTable.reload();
} else {
layer.msg(res.msg || '操作失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', { icon: 2 });
}
});
},
btn2: function (index) {
layer.close(index);
},
success: function () {
form.render();
},
shadeClose: false
});
} else if (obj.event === 'del') {
// 删除
layer.confirm('确定删除该函数吗?', { icon: 3, title: '提示' }, function (index) {
$.ajax({
url: '/admin/function/delete',
type: 'POST',
data: JSON.stringify({ id: data.id }),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
functionsTable.reload();
} else {
layer.msg(res.msg || '删除失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '删除失败', { icon: 2 });
}
});
layer.close(index);
});
}
});
});
});
</script>
</section>
{{ end }}

View File

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

View File

@@ -1,280 +0,0 @@
{{/* 管理员登录页面模板使用layui构建的登录界面 */}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{{ .Title }}</title>
<!-- 站点图标 -->
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link href="/static/lib/layui/css/layui.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.demo-login-container {
width: 400px;
margin: 21px auto 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
text-align: center;
color: #fff;
}
.login-header h1 {
margin: 0;
font-size: 24px;
font-weight: 300;
}
.login-header p {
margin: 8px 0 0;
opacity: 0.8;
font-size: 14px;
}
.login-form {
padding: 30px 20px;
}
/* 调整表单项间距 */
.login-form .layui-form-item {
margin-bottom: 20px;
}
/* 最后一个表单项不需要底部边距 */
.login-form .layui-form-item:last-child {
margin-bottom: 0;
}
.demo-login-other .layui-icon {
position: relative;
display: inline-block;
margin: 0 2px;
top: 2px;
font-size: 26px;
}
.login-footer {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
border-top: 1px solid #f0f0f0;
}
/* 响应式设计 - 移动端适配 */
@media (max-width: 768px) {
.demo-login-container {
width: 90%;
margin: 10px auto;
border-radius: 4px;
}
.login-header {
padding: 25px 15px;
}
.login-header h1 {
font-size: 20px;
}
.login-form {
padding: 25px 15px;
}
/* 移动端表单项间距调整 */
.login-form .layui-form-item {
margin-bottom: 18px;
}
}
@media (max-width: 480px) {
.demo-login-container {
width: 95%;
margin: 5px auto;
border-radius: 0;
min-height: calc(100vh - 10px);
display: flex;
flex-direction: column;
}
.login-header {
padding: 20px 15px;
}
.login-header h1 {
font-size: 18px;
}
.login-form {
padding: 20px 15px;
}
/* 小屏幕表单项间距调整 */
.login-form .layui-form-item {
margin-bottom: 16px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
}
</style>
</head>
<body>
<form class="layui-form">
<div class="demo-login-container">
<div class="login-header">
<h1>{{ .SystemName }}</h1>
<p>管理员登录</p>
</div>
<div class="login-form">
<!-- CSRF令牌隐藏字段 -->
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}" id="csrf-token">
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="username" value="" lay-verify="required" placeholder="用户名"
lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-password"></i>
</div>
<input type="password" name="password" value="" lay-verify="required" placeholder="密 码"
lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
</div>
</div>
<div class="layui-form-item">
<div class="layui-row">
<div class="layui-col-xs7">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-vercode"></i>
</div>
<input type="text" name="captcha" value="" lay-verify="required" placeholder="验证码"
lay-reqtext="请填写验证码" autocomplete="off" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-xs5">
<div style="margin-left: 5px; text-align: right;">
<img id="captcha-img" src="/admin/captcha"
onclick="this.src='/admin/captcha?t='+ new Date().getTime();"
style="cursor: pointer; height: 38px; border-radius: 4px; width: 100%;"
title="点击刷新验证码">
</div>
</div>
</div>
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-login">立即登录</button>
</div>
</div>
<div class="login-footer">
<p>{{ .FooterText }}</p>
</div>
</div>
</form>
<script src="/static/lib/layui/layui.js"></script>
<script>
layui.use(function () {
var form = layui.form;
var layer = layui.layer;
// 登录提交回调:向 /admin/login 发送请求,并依据 code===0 判断成功与否
form.on('submit(demo-login)', function (data) {
var loadIndex = layer.msg('登录中...', {
icon: 16,
shade: 0.01,
time: 0
});
// 获取CSRF令牌
var csrfToken = document.getElementById('csrf-token').value;
// 发送登录请求
fetch('/admin/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data.field)
})
.then(response => response.text())
.then(text => {
try {
return JSON.parse(text);
} catch (e) {
console.error('Non-JSON response:', text);
throw new Error('服务器响应格式错误');
}
})
.then(result => {
layer.close(loadIndex);
// 根据统一接口code === 0 表示成功
const isOk = result && result.code === 0;
if (isOk) {
layer.msg('登录成功', {
icon: 1,
time: 1500
}, function () {
const redirect = (result.data && result.data.redirect) || '/admin';
window.location.href = redirect;
});
} else {
const msg = (result && (result.msg || result.message)) || '登录失败,请检查用户名和密码';
layer.msg(msg, { icon: 2 });
// 登录失败时刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
}
})
.catch(error => {
layer.close(loadIndex);
console.error('登录错误:', error);
var msg = error.message || '网络错误,请稍后重试';
layer.msg(msg, { icon: 2 });
// 网络错误时也刷新验证码
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
});
return false; // 阻止表单跳转
});
});
</script>
</body>
</html>

View File

@@ -1,171 +0,0 @@
{{ define "login_logs.html" }}
<section>
<h2>登录日志</h2>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="loginLogFilterForm" lay-filter="loginLogFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">日期范围</label>
<div class="layui-input-inline" style="width: 200px;">
<input type="text" class="layui-input" id="loginTimeRange" placeholder=" - " autocomplete="off">
<input type="hidden" name="login_start_time" id="login_start_time">
<input type="hidden" name="login_end_time" id="login_end_time">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value="">全部</option>
<option value="1">成功</option>
<option value="0">失败</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">用户名</label>
<div class="layui-input-inline">
<input type="text" name="username" placeholder="请输入用户名" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">登录IP</label>
<div class="layui-input-inline">
<input type="text" name="ip" placeholder="请输入IP地址" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnSearchLoginLogs">搜索</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetLoginLogs">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">日志列表</h3>
<div style="padding: 20px;">
<script type="text/html" id="loginLogsToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="clearLogs">
<i class="layui-icon layui-icon-delete"></i>
</button>
</div>
</script>
<table id="loginLogsTable" lay-filter="loginLogsTableFilter"></table>
</div>
</div>
</section>
<script>
layui.use(['table', 'form', 'laydate', 'util', 'jquery'], function(){
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
var util = layui.util;
var $ = layui.jquery;
// 日期范围选择器
laydate.render({
elem: '#loginTimeRange',
range: true,
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
done: function(value, date, endDate){
if(value) {
const dates = value.split(' - ');
$('#login_start_time').val(dates[0]);
$('#login_end_time').val(dates[1]);
} else {
$('#login_start_time').val('');
$('#login_end_time').val('');
}
}
});
// 渲染表格
var loginLogsTable = table.render({
elem: '#loginLogsTable',
url: '/admin/api/login_logs',
toolbar: '#loginLogsToolbar',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
cols: [[
{field: 'username', title: '用户名', width: 150},
{field: 'ip', title: '登录IP', width: 150},
{field: 'status', title: '状态', width: 100, align: 'center', templet: function(d){
return d.status === 1 ?
'<span class="layui-badge layui-bg-green">成功</span>' :
'<span class="layui-badge layui-bg-red">失败</span>';
}},
{field: 'message', title: '详情', minWidth: 150},
{field: 'user_agent', title: 'User Agent', minWidth: 200, templet: function(d){
return '<div title="'+d.user_agent+'" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">'+d.user_agent+'</div>';
}},
{field: 'created_at', title: '登录时间', width: 180, templet: function(d){
return util.toDateString(d.created_at);
}}
]],
response: {
statusCode: 0
},
parseData: function(res){
return {
"code": res.code,
"msg": res.msg,
"count": res.data ? res.data.total : 0,
"data": res.data ? res.data.list : []
};
}
});
// 搜索按钮
$('#btnSearchLoginLogs').on('click', function(){
const formData = form.val('loginLogFilterForm');
loginLogsTable.reload({
where: {
status: formData.status,
username: formData.username,
ip: formData.ip,
start_time: $('#login_start_time').val(),
end_time: $('#login_end_time').val()
},
page: {curr: 1}
});
});
// 头工具栏事件
table.on('toolbar(loginLogsTableFilter)', function(obj){
switch(obj.event){
case 'clearLogs':
layer.confirm('确定要清空所有登录日志吗?此操作不可恢复!', {icon: 3, title:'警告'}, function(index){
$.post('/admin/api/login_logs/clear', function(res){
if(res.code === 0){
layer.msg('登录日志已清空', {icon: 1});
loginLogsTable.reload({page: {curr: 1}});
} else {
layer.msg(res.msg || '清空失败', {icon: 2});
}
});
layer.close(index);
});
break;
};
});
// 重置按钮
$('#btnResetLoginLogs').on('click', function(){
$('#loginLogFilterForm')[0].reset();
$('#login_start_time').val('');
$('#login_end_time').val('');
$('#loginTimeRange').val('');
form.render('select');
$('#btnSearchLoginLogs').click();
});
});
</script>
{{ end }}

View File

@@ -1,155 +0,0 @@
{{ define "operation_logs.html" }}
<section>
<h2>日志操作</h2>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="operationLogFilterForm" lay-filter="operationLogFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">日期范围</label>
<div class="layui-input-inline" style="width: 200px;">
<input type="text" class="layui-input" id="operationTimeRange" placeholder=" - " autocomplete="off">
<input type="hidden" name="operation_start_time" id="operation_start_time">
<input type="hidden" name="operation_end_time" id="operation_end_time">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">操作方式</label>
<div class="layui-input-inline">
<input type="text" name="operation_type" placeholder="请输入操作方式" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">操作账号</label>
<div class="layui-input-inline">
<input type="text" name="operator" placeholder="请输入操作账号" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnSearchOperationLogs">搜索</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetOperationLogs">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">日志列表</h3>
<div style="padding: 20px;">
<script type="text/html" id="operationLogsToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="clearLogs">
<i class="layui-icon layui-icon-delete"></i>
</button>
</div>
</script>
<table id="operationLogsTable" lay-filter="operationLogsTableFilter"></table>
</div>
</div>
</section>
<script>
layui.use(['table', 'form', 'laydate', 'util', 'jquery'], function(){
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
var util = layui.util;
var $ = layui.jquery;
// 日期范围选择器
laydate.render({
elem: '#operationTimeRange',
range: true,
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
done: function(value, date, endDate){
if(value) {
const dates = value.split(' - ');
$('#operation_start_time').val(dates[0]);
$('#operation_end_time').val(dates[1]);
} else {
$('#operation_start_time').val('');
$('#operation_end_time').val('');
}
}
});
// 渲染表格
var operationLogsTable = table.render({
elem: '#operationLogsTable',
url: '/admin/api/logs',
toolbar: '#operationLogsToolbar',
page: true,
limit: 20,
limits: [10, 20, 50, 100, 200, 500, 1000],
cols: [[
{field: 'operator', title: '操作账号', minWidth: 150},
{field: 'operation_type', title: '操作方式', minWidth: 150},
{field: 'details', title: '日志内容', minWidth: 200},
{field: 'created_at', title: '创建时间', width: 180, templet: function(d){
return util.toDateString(d.created_at);
}}
]],
response: {
statusName: 'code',
statusCode: 0,
msgName: 'msg',
countName: 'count', // 解析数据长度的字段名称
dataName: 'data' // 解析数据列表的字段名称
},
parseData: function(res) { // 将原始数据格式解析成 table 组件所规定的数据格式
return {
"code": res.code, // 解析接口状态
"msg": res.msg, // 解析提示文本
"count": res.data ? res.data.total : 0, // 解析数据长度
"data": res.data ? res.data.list : [] // 解析数据列表
};
}
});
// 搜索按钮
$('#btnSearchOperationLogs').on('click', function(){
const formData = form.val('operationLogFilterForm');
operationLogsTable.reload({
where: {
operation_type: formData.operation_type,
operator: formData.operator,
start_time: $('#operation_start_time').val(),
end_time: $('#operation_end_time').val()
},
page: {curr: 1}
});
});
// 头工具栏事件
table.on('toolbar(operationLogsTableFilter)', function(obj){
switch(obj.event){
case 'clearLogs':
layer.confirm('确定要清空所有日志吗?此操作不可恢复!', {icon: 3, title:'警告'}, function(index){
$.post('/admin/api/logs/clear', function(res){
if(res.code === 0){
layer.msg('日志已清空', {icon: 1});
operationLogsTable.reload({page: {curr: 1}});
} else {
layer.msg(res.msg || '清空失败', {icon: 2});
}
});
layer.close(index);
});
break;
};
});
// 重置按钮
$('#btnResetOperationLogs').on('click', function(){
$('#operationLogFilterForm')[0].reset();
$('#operation_start_time').val('');
$('#operation_end_time').val('');
$('#operationTimeRange').val('');
$('#btnSearchOperationLogs').click();
});
});
</script>
{{ end }}

View File

@@ -1,340 +0,0 @@
{{ define "profile.html" }}
<section>
<h2>账户管理</h2>
<div class="layui-tab layui-tab-brief" lay-filter="userTabs" style="margin-top: 16px;">
<ul class="layui-tab-title">
<li class="layui-this">修改密码</li>
<li>修改用户名</li>
</ul>
<div class="layui-tab-content">
<!-- 修改密码模块 -->
<div class="layui-tab-item layui-show">
<div class="layui-panel" style="margin-top: 16px;">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改密码</h3>
<div style="padding: 20px;">
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-old-password">当前密码</label>
<div class="layui-input-block">
<div class="layui-input-wrap">
<input type="password" name="old_password" placeholder="请输入当前密码" autocomplete="off"
class="layui-input" lay-verify="required" lay-affix="eye" />
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="user-new-password">新的密码</label>
<div class="layui-input-block">
<div class="layui-input-wrap">
<input type="password" name="new_password" placeholder="请输入新密码至少6位" autocomplete="off"
class="layui-input" lay-verify="required" lay-affix="eye" />
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<div class="layui-input-wrap">
<input type="password" name="confirm_password" placeholder="请再次输入新密码" autocomplete="off"
class="layui-input" lay-verify="required" lay-affix="eye" />
</div>
</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-panel" style="margin-top: 16px;">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改用户名</h3>
<div style="padding: 20px;">
<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" style="cursor: pointer;" data-tips="user-username">新用户名</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" style="cursor: pointer;" data-tips="user-old-password">当前密码</label>
<div class="layui-input-block">
<div class="layui-input-wrap">
<input type="password" name="password" placeholder="请输入当前密码以确认身份" autocomplete="off"
class="layui-input" lay-verify="required" lay-affix="eye" />
</div>
</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>
<script>
// 使用自执行函数创建局部作用域,避免与其他页面脚本发生全局命名冲突
(() => {
// 工具方法:将数值角色转为中文标签
const roleToText = (role) => {
const r = typeof role === 'string' ? parseInt(role, 10) : role
return r === 0 ? '管理员' : '子账号'
}
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return ''
const date = new Date(timeStr)
return date.toLocaleString('zh-CN')
}
// 如果未加载 layui则按需加载
const ensureLayui = () => new Promise((resolve) => {
if (window.layui) return resolve(window.layui)
const css = document.createElement('link')
css.rel = 'stylesheet'
css.href = 'https://unpkg.com/layui@2.10.1/dist/css/layui.css'
document.head.appendChild(css)
const script = document.createElement('script')
script.src = 'https://unpkg.com/layui@2.10.1/dist/layui.js'
script.onload = () => resolve(window.layui)
document.head.appendChild(script)
})
// 在确保 Layui 可用后再执行页面逻辑
ensureLayui().then(() => {
layui.use(['form', 'layer', 'element'], () => {
const form = layui.form
const layer = layui.layer
const element = layui.element
// 全局变量
let currentUsername = null
// 获取当前用户名
const getCurrentUsername = async () => {
try {
const res = await fetch('/admin/api/profile/info')
let data
try {
const text = await res.text()
data = JSON.parse(text)
} catch (e) {
throw new Error('服务器响应格式错误')
}
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '获取用户信息失败')
currentUsername = data.data.username
// 填充用户名修改表单的当前用户名
form.val('usernameForm', { current_username: currentUsername })
} catch (e) {
layer.msg(e.message || '获取用户信息失败', { icon: 2 })
}
}
// 修改密码模块
const PasswordModule = {
validate: (fields) => {
const { old_password, new_password, confirm_password } = fields
if (!old_password || !new_password || !confirm_password) {
return { ok: false, msg: '请填写完整的密码信息' }
}
if (new_password.length < 6) {
return { ok: false, msg: '新密码长度不能少于6位' }
}
if (new_password !== confirm_password) {
return { ok: false, msg: '两次输入的新密码不一致' }
}
if (old_password === new_password) {
return { ok: false, msg: '新密码不能与当前密码相同' }
}
return { ok: true }
},
submit: async (fields) => {
const validation = PasswordModule.validate(fields)
if (!validation.ok) {
layer.msg(validation.msg, { icon: 2 })
return false
}
try {
const res = await fetch('/admin/api/profile/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
old_password: fields.old_password,
new_password: fields.new_password,
confirm_password: fields.confirm_password
})
})
let data
try {
const text = await res.text()
data = JSON.parse(text)
} catch (e) {
throw new Error('服务器响应格式错误')
}
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '修改密码失败')
// 检查是否需要跳转
if (data.data?.redirect) {
layer.msg('密码修改成功,即将跳转到登录页', { icon: 1, time: 1500 }, () => {
window.location.href = data.data.redirect
})
} else {
// 密码修改成功,不跳转,重置表单
layer.msg('密码修改成功', { icon: 1 })
document.getElementById('passwordForm').reset()
}
} catch (e) {
layer.msg(e.message || '修改密码失败', { icon: 2 })
}
return false
},
reset: () => {
document.getElementById('passwordForm').reset()
layer.msg('表单已重置', { icon: 1 })
}
}
// 修改用户名模块
const UsernameModule = {
validate: (fields) => {
const { new_username, password } = fields
if (!new_username || !password) {
return { ok: false, msg: '请填写新用户名和当前密码' }
}
if (new_username === currentUsername) {
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/profile/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: fields.new_username,
old_password: fields.password
})
})
let data
try {
const text = await res.text()
data = JSON.parse(text)
} catch (e) {
throw new Error('服务器响应格式错误')
}
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '修改用户名失败')
layer.msg('用户名修改成功', { icon: 1 })
// 重新获取当前用户名
await getCurrentUsername()
// 清空表单(不显示重置提示)
form.val('usernameForm', {
new_username: '',
password: '',
current_username: currentUsername || ''
})
} catch (e) {
layer.msg(e.message || '修改用户名失败', { icon: 2 })
}
return false
},
reset: () => {
form.val('usernameForm', {
new_username: '',
password: '',
current_username: currentUsername || ''
})
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)
// 初始化加载
getCurrentUsername()
})
})
})()
</script>
</section>
{{ end }}

View File

@@ -1,508 +0,0 @@
{{ define "settings.html" }}
<section>
<h2>系统设置</h2>
<!-- 系统配置设置 -->
<div class="layui-panel" style="margin-top: 16px;">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">安全配置</h3>
<div style="padding: 20px;">
<form class="layui-form" id="systemForm">
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label>
<div class="layui-input-block">
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
<input type="checkbox" name="maintenance_mode" lay-skin="switch" lay-text="开启|关闭" title="开启|关闭">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="encryption-key">加密密钥</label>
<div class="layui-input-block">
<div style="display: flex; gap: 10px;">
<input type="text" name="encryption_key" placeholder="请输入数据加密密钥" class="layui-input" readonly>
<button type="button" class="layui-btn layui-btn-primary" id="generateEncBtn">生成</button>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="jwt-secret">JWT密钥</label>
<div class="layui-input-block">
<div style="display: flex; gap: 10px;">
<input type="text" name="jwt_secret" placeholder="请输入JWT签名密钥" class="layui-input" readonly>
<button type="button" class="layui-btn layui-btn-primary" id="generateJwtBtn">生成</button>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="jwt-refresh">JWT刷新</label>
<div class="layui-input-block">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="number" name="jwt_refresh" placeholder="6" min="1" lay-affix="number" class="layui-input"
style="width: 120px;" />
<span class="layui-form-mid">小时至少1小时</span>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="jwt-expire">JWT有效期</label>
<div class="layui-input-block">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="number" name="jwt_expire" placeholder="24" min="1" lay-affix="number" class="layui-input"
style="width: 120px;" />
<span class="layui-form-mid">小时至少1小时</span>
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_system">保存安全配置</button>
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="system">重置</button>
</div>
</div>
</form>
</div>
</div>
<!-- Cookie 设置 -->
<div class="layui-panel" style="margin-top: 16px;">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">Cookie 设置</h3>
<div style="padding: 20px;">
<form class="layui-form" id="cookieForm">
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-secure">Secure</label>
<div class="layui-input-block">
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
<input type="checkbox" name="cookie_secure" lay-skin="switch" lay-text="开启|关闭" title="开启|关闭">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-samesite">Same</label>
<div class="layui-input-block">
<select name="cookie_same_site">
<option value="Strict">Strict</option>
<option value="Lax">Lax</option>
<option value="None">None</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-domain">Domain</label>
<div class="layui-input-block">
<input type="text" name="cookie_domain" placeholder="留空则默认为当前域名" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="cookie-max-age">MaxAge</label>
<div class="layui-input-block">
<div style="display: flex; align-items: center; gap: 10px;">
<input type="number" name="cookie_max_age" placeholder="86400" min="0" lay-affix="number" class="layui-input"
style="width: 120px;" />
<span class="layui-form-mid"></span>
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_cookie">保存Cookie设置</button>
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="cookie">重置</button>
</div>
</div>
</form>
</div>
</div>
<!-- 日志清理设置 -->
<div class="layui-panel" style="margin-top: 16px;">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">日志清理设置</h3>
<div style="padding: 20px;">
<form class="layui-form" id="logCleanupForm">
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="login-log-cleanup">登录日志</label>
<div class="layui-input-block">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="layui-form-mid">保留</span>
<input type="number" name="login_log_cleanup_days" placeholder="30" min="0" lay-affix="number" class="layui-input" style="width: 80px;" />
<span class="layui-form-mid">天,且保留最近</span>
<input type="number" name="login_log_cleanup_limit" placeholder="10000" min="0" lay-affix="number" class="layui-input" style="width: 100px;" />
<span class="layui-form-mid">0为不限制</span>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="operation-log-cleanup">操作日志</label>
<div class="layui-input-block">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="layui-form-mid">保留</span>
<input type="number" name="operation_log_cleanup_days" placeholder="30" min="0" lay-affix="number" class="layui-input" style="width: 80px;" />
<span class="layui-form-mid">天,且保留最近</span>
<input type="number" name="operation_log_cleanup_limit" placeholder="10000" min="0" lay-affix="number" class="layui-input" style="width: 100px;" />
<span class="layui-form-mid">0为不限制</span>
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_cleanup">保存清理策略</button>
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="cleanup">重置</button>
</div>
</div>
</form>
</div>
</div>
<!-- 基本信息设置 -->
<div class="layui-panel" style="margin-top: 16px;">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">基本信息设置</h3>
<div style="padding: 20px;">
<form class="layui-form" id="basicForm">
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="site-title">站点标题</label>
<div class="layui-input-block">
<input type="text" name="site_title" lay-verify="required" placeholder="请输入站点标题" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="site-keywords">关键词</label>
<div class="layui-input-block">
<input type="text" name="site_keywords" placeholder="请输入站点关键词,多个关键词用逗号分隔" class="layui-input" />
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label" style="cursor: pointer;" data-tips="site-description">站点描述</label>
<div class="layui-input-block">
<textarea name="site_description" placeholder="请输入站点描述" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="site-logo">站点Logo</label>
<div class="layui-input-block">
<input type="text" name="site_logo" placeholder="/assets/logo.svg" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_basic">保存基本信息</button>
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="basic">重置</button>
</div>
</div>
</form>
</div>
</div>
<!-- 页脚与备案信息 -->
<div class="layui-panel" style="margin-top: 16px;">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">页脚与备案</h3>
<div style="padding: 20px;">
<form class="layui-form" id="footerForm">
<div class="layui-form-item layui-form-text">
<label class="layui-form-label" style="cursor: pointer;" data-tips="footer-text">页脚文本</label>
<div class="layui-input-block">
<textarea name="footer_text" placeholder="© 2025 凌动技术 保留所有权利" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="icp-record">ICP备案</label>
<div class="layui-input-block">
<input type="text" name="icp_record" placeholder="京ICP备12345678号" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="icp-record-link">备案链接</label>
<div class="layui-input-block">
<input type="url" name="icp_record_link" placeholder="https://beian.miit.gov.cn" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="psb-record">公安备案</label>
<div class="layui-input-block">
<input type="text" name="psb_record" placeholder="京公网安备11010802012345号" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="psb-record-link">备案链接</label>
<div class="layui-input-block">
<input type="url" name="psb_record_link" placeholder="http://www.beian.gov.cn/portal/registerSystemInfo"
class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn layui-btn-sm" lay-submit lay-filter="save_footer">保存页脚备案</button>
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm reset-btn" data-type="footer">重置</button>
</div>
</div>
</form>
</div>
</div>
</section>
<script>
// 等待layui加载完成
function waitForLayui(callback) {
if (typeof layui !== 'undefined') {
callback();
} else {
setTimeout(() => waitForLayui(callback), 100);
}
}
waitForLayui(function () {
layui.use(['jquery', 'form', 'layer', 'util'], function () {
const { $, form, layer, util } = layui;
// 缓存上次加载的设置值,用于“重置”恢复
let originalSettings = {};
/**
* 加载后台所有设置并回填到三个表单
* - 从 /admin/api/settings 获取 name:value 映射
* - 处理开关型字段maintenance_mode
* - 渲染 layui 组件
*/
const loadSettings = async () => {
try {
const res = await fetch('/admin/api/settings', {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
if (data.code !== 0) {
layer.msg(data.msg || '加载设置失败', { icon: 2 });
return;
}
originalSettings = data.data || {};
fillForms(originalSettings);
} catch (err) {
console.error('获取设置失败:', err);
layer.msg('网络错误,无法加载设置', { icon: 2 });
}
};
/**
* 将 settings 数据回填到各表单控件
* - 拆分为独立的填充函数,便于局部重置
*/
const fillSystem = (settings) => {
const maintenanceChecked = (settings.maintenance_mode || '0') === '1';
$('[name="maintenance_mode"]').prop('checked', maintenanceChecked);
$('[name="jwt_secret"]').val(settings.jwt_secret || '');
$('[name="encryption_key"]').val(settings.encryption_key || '');
$('[name="jwt_refresh"]').val(settings.jwt_refresh || '6');
$('[name="jwt_expire"]').val(settings.jwt_expire || '24');
};
const fillCookie = (settings) => {
const cookieSecureChecked = (settings.cookie_secure || 'true') === 'true' || settings.cookie_secure === '1';
$('[name="cookie_secure"]').prop('checked', cookieSecureChecked);
$('[name="cookie_same_site"]').val(settings.cookie_same_site || 'Lax');
$('[name="cookie_domain"]').val(settings.cookie_domain || '');
$('[name="cookie_max_age"]').val(settings.cookie_max_age || '86400');
};
const fillCleanup = (settings) => {
$('[name="login_log_cleanup_days"]').val(settings.login_log_cleanup_days || '30');
$('[name="login_log_cleanup_limit"]').val(settings.login_log_cleanup_limit || '10000');
$('[name="operation_log_cleanup_days"]').val(settings.operation_log_cleanup_days || '30');
$('[name="operation_log_cleanup_limit"]').val(settings.operation_log_cleanup_limit || '10000');
};
const fillBasic = (settings) => {
$('[name="site_title"]').val(settings.site_title || '');
$('[name="site_keywords"]').val(settings.site_keywords || '');
$('[name="site_description"]').val(settings.site_description || '');
$('[name="site_logo"]').val(settings.site_logo || '');
};
const fillFooter = (settings) => {
$('[name="footer_text"]').val(settings.footer_text || '');
$('[name="icp_record"]').val(settings.icp_record || '');
$('[name="icp_record_link"]').val(settings.icp_record_link || '');
$('[name="psb_record"]').val(settings.psb_record || '');
$('[name="psb_record_link"]').val(settings.psb_record_link || '');
};
const fillForms = (settings = {}) => {
fillBasic(settings);
fillSystem(settings);
fillCookie(settings);
fillCleanup(settings);
fillFooter(settings);
// 渲染 layui 组件
form.render();
};
/**
* 收集某个表单下所有可用控件的值
* - 统一将 checkbox 转为 "1"/"0"
* - 其他控件转为字符串,避免后端类型不一致
*/
const collectForm = (selector) => {
const obj = {};
const $form = $(selector);
$form.find('input, textarea, select').each(function () {
const $el = $(this);
const name = $el.attr('name');
if (!name) return; // 无 name 不纳入
const type = ($el.attr('type') || '').toLowerCase();
let value = '';
if (type === 'checkbox') {
value = $el.prop('checked') ? '1' : '0';
} else {
value = ($el.val() ?? '').toString();
}
obj[name] = value;
});
return obj;
};
/**
* 汇总三个表单的字段为一个扁平对象
*/
const collectAllSettings = () => {
return {
...collectForm('#basicForm'),
...collectForm('#systemForm'),
...collectForm('#cookieForm'),
...collectForm('#footerForm'),
...collectForm('#logCleanupForm'),
};
};
/**
* 提交设置到后端
* @param {Object} payload - 要保存的设置对象
* @param {HTMLElement} btnElem - 触发保存的按钮元素(用于禁用/恢复)
* @param {String} successMsg - 成功提示信息
*/
const submitSettings = (payload, btnElem, successMsg = '保存成功') => {
const $btn = $(btnElem);
$btn.prop('disabled', true).addClass('layui-btn-disabled');
const loadIdx = layer.msg('正在保存...', {
icon: 16,
time: 0,
shade: 0.1
});
fetch('/admin/api/settings/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
})
.then(resp => resp.json())
.then(res => {
if (res.code === 0) {
layer.msg(res.msg || successMsg, { icon: 1, time: 1000 });
// 更新本地缓存,合并新保存的设置
originalSettings = { ...originalSettings, ...payload };
} else {
layer.msg(res.msg || '保存失败', { icon: 2 });
}
})
.catch(err => {
console.error('保存设置失败:', err);
var msg = '网络错误,保存失败';
if (err.response && err.response.data && err.response.data.msg) {
msg = err.response.data.msg;
} else if (err.message) {
msg = err.message;
}
layer.msg(msg, { icon: 2 });
})
.finally(() => {
layer.close(loadIdx);
$btn.prop('disabled', false).removeClass('layui-btn-disabled');
});
};
/**
* 绑定各个分块的保存按钮
*/
form.on('submit(save_system)', function(data){
submitSettings(collectForm('#systemForm'), data.elem, '安全配置已保存');
return false;
});
form.on('submit(save_cookie)', function(data){
submitSettings(collectForm('#cookieForm'), data.elem, 'Cookie设置已保存');
return false;
});
form.on('submit(save_cleanup)', function(data){
submitSettings(collectForm('#logCleanupForm'), data.elem, '清理策略已保存');
return false;
});
form.on('submit(save_basic)', function(data){
submitSettings(collectForm('#basicForm'), data.elem, '基本信息已保存');
return false;
});
form.on('submit(save_footer)', function(data){
submitSettings(collectForm('#footerForm'), data.elem, '页脚备案已保存');
return false;
});
/**
* 处理各个分块的重置按钮
*/
$(document).on('click', '.reset-btn', function() {
const type = $(this).data('type');
switch (type) {
case 'system':
fillSystem(originalSettings);
break;
case 'cookie':
fillCookie(originalSettings);
break;
case 'cleanup':
fillCleanup(originalSettings);
break;
case 'basic':
fillBasic(originalSettings);
break;
case 'footer':
fillFooter(originalSettings);
break;
}
form.render();
layer.msg('已恢复该部分默认值', { icon: 1, time: 800 });
});
/**
* 生成安全密钥
*/
const generateKey = async (type) => {
try {
const loadIdx = layer.load(2);
const res = await fetch(`/admin/api/settings/generate_key?type=${type}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
layer.close(loadIdx);
if (data.code === 0) {
if (type === 'jwt') {
$('[name="jwt_secret"]').val(data.data.key);
} else if (type === 'encryption') {
$('[name="encryption_key"]').val(data.data.key);
}
layer.msg('生成成功', { icon: 1 });
} else {
layer.msg(data.msg || '生成失败', { icon: 2 });
}
} catch (err) {
console.error('生成密钥失败:', err);
layer.closeAll('loading');
layer.msg('网络错误', { icon: 2 });
}
};
$('#generateJwtBtn').on('click', () => generateKey('jwt'));
$('#generateEncBtn').on('click', () => generateKey('encryption'));
// 初始化:加载设置
loadSettings();
});
});
</script>
{{ end }}

View File

@@ -1,489 +0,0 @@
{{ define "variables.html" }}
<section>
<h2>公共变量</h2>
<div class="layui-btn-container" style="margin:12px 0">
<button class="layui-btn" id="btnAddVariable"><i class="layui-icon layui-icon-add-1"></i> 新增变量</button>
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteVariables"><i class="layui-icon layui-icon-delete"></i>
批量删除</button>
</div>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="variableFilterForm" lay-filter="variableFilterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">应用筛选</label>
<div class="layui-input-inline">
<select name="filter_app_uuid" lay-search lay-filter="appSelect">
<option value="">全部应用</option>
<option value="0">全局变量</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">搜索</label>
<div class="layui-input-inline">
<input type="text" name="search" placeholder="变量编号/别名/数据/备注" autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnSearchVariables">查询</button>
<button type="button" class="layui-btn layui-btn-primary" id="btnResetVariables">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-panel" style="margin-top:12px">
<h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">变量列表</h3>
<div style="padding: 20px;">
<table id="variablesTable" lay-filter="variablesTableFilter"></table>
</div>
</div>
<!-- 表格操作模板 -->
<script type="text/html" id="tpl-variables-ops">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<!-- 隐藏的表单弹层内容:新增/编辑变量 -->
<div id="variableFormLayer" style="display:none;padding:20px">
<form class="layui-form layui-form-pane" lay-filter="variableForm" id="variableForm">
<input type="hidden" name="uuid">
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="variable-alias">变量别名</label>
<div class="layui-input-block">
<input type="text" name="alias" lay-verify="required|alias" placeholder="请输入变量别名(英文开头,只能包含数字和英文字母)"
autocomplete="off" class="layui-input" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="variable-app">关联应用</label>
<div class="layui-input-block">
<select name="app_uuid" lay-search>
<option value="0">全局变量</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="variable-data">变量数据</label>
<div class="layui-input-block">
<textarea name="data" placeholder="请输入变量数据" lay-verify="required" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="variable-remark">备注</label>
<div class="layui-input-block">
<textarea name="remark" placeholder="请输入备注信息" class="layui-textarea"></textarea>
</div>
</div>
</form>
</div>
<script>
// 等待layui加载完成
function waitForLayui(callback) {
if (typeof layui !== 'undefined') {
callback();
} else {
setTimeout(() => waitForLayui(callback), 100);
}
}
waitForLayui(function () {
layui.use(['table', 'form', 'layer', 'element'], function () {
const table = layui.table;
const form = layui.form;
const layer = layui.layer;
const $ = layui.$;
// 全局应用列表
let appsList = [];
// 自定义验证规则
form.verify({
alias: function (value) {
if (!value) return '别名不能为空';
// 检查是否以英文字母开头,且只包含数字和英文字母
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) {
return '别名必须以英文字母开头,只能包含数字和英文字母';
}
}
});
// 格式化时间函数
function formatDateTime(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
// 根据应用UUID获取应用名称和ID并添加颜色徽章
function getAppName(appUUID) {
if (appUUID === '0') {
return '<span class="layui-badge layui-bg-blue">全局变量</span>';
}
const app = appsList.find(app => app.uuid === appUUID);
if (app) {
return '<span class="layui-badge layui-bg-green">' + app.name + '(ID:' + app.id + ')' + '</span>';
} else {
return '<span class="layui-badge">未知应用</span>';
}
}
// 加载应用列表
function loadAppList() {
$.ajax({
url: '/admin/api/apps/simple',
type: 'GET',
success: function (res) {
if (res.code === 0 && res.data) {
// 保存应用列表到全局变量
appsList = res.data;
const filterSelect = $('form[lay-filter="variableFilterForm"] select[name="filter_app_uuid"]');
const formSelect = $('#variableForm select[name="app_uuid"]');
// 清空现有选项(保留默认选项:全部应用和全局变量)
filterSelect.find('option:not([value=""]):not([value="0"])').remove();
formSelect.find('option:not([value=""]):not([value="0"])').remove();
// 添加应用选项
res.data.forEach(function(app) {
const option = '<option value="' + app.uuid + '">' + app.name + '(ID:' + app.id + ')' + '</option>';
filterSelect.append(option);
formSelect.append(option);
});
// 重新渲染表单
form.render('select');
}
},
error: function(xhr) {
console.log('加载应用列表失败:', xhr.responseText);
}
});
}
// 页面加载时获取应用列表
loadAppList();
// 渲染表格
const variablesTable = table.render({
elem: '#variablesTable',
id: 'variablesTable',
url: '/admin/variable/list',
parseData: function (res) {
return {
code: res.code,
msg: res.msg || '',
count: res.count || 0,
data: res.data || []
};
},
request: {
pageName: 'page',
limitName: 'page_size'
},
method: 'GET',
page: true,
limit: 20,
limits: [10, 20, 50, 100],
loading: true,
done: function (res, curr, count) {
// 表格渲染完成后的回调
},
cols: [[
{ type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'number', title: '变量编号', width: 180 },
{
field: 'app_uuid',
title: '关联应用',
minWidth: 180,
templet: function (d) {
return getAppName(d.app_uuid);
}
},
{ field: 'alias', title: '变量别名', minWidth: 150 },
{
field: 'data',
title: '变量数据',
minWidth: 200,
templet: function (d) {
// 限制显示长度,避免内容过长影响布局
if (d.data && d.data.length > 50) {
return '<span title="' + d.data + '">' + d.data.substring(0, 50) + '...</span>';
}
return d.data || '-';
}
},
{
field: 'remark',
title: '备注',
minWidth: 150,
templet: function (d) {
// 限制显示长度,避免内容过长影响布局
if (d.remark && d.remark.length > 30) {
return '<span title="' + d.remark + '">' + d.remark.substring(0, 30) + '...</span>';
}
return d.remark || '-';
}
},
{
field: 'created_at',
title: '创建时间',
width: 180,
templet: function (d) {
return formatDateTime(d.created_at);
}
},
{ title: '操作', width: 120, align: 'center', toolbar: '#tpl-variables-ops', fixed: 'right' }
]]
});
// 搜索功能
$('#btnSearchVariables').on('click', function () {
const searchData = {
search: $('input[name="search"]').val()
};
// 添加应用筛选
const appUUID = $('select[name="filter_app_uuid"]').val();
if (appUUID) {
searchData.app_uuid = appUUID;
}
variablesTable.reload({
where: searchData,
page: {
curr: 1
}
});
});
// 重置搜索
$('#btnResetVariables').on('click', function () {
$('#variableFilterForm')[0].reset();
form.render();
variablesTable.reload({
where: {},
page: {
curr: 1
}
});
});
// 监听应用选择变化,实现联动筛选
form.on('select(appSelect)', function (data) {
const searchData = {
search: $('input[name="search"]').val()
};
// 添加应用筛选
if (data.value) {
searchData.app_uuid = data.value;
}
variablesTable.reload({
where: searchData,
page: {
curr: 1
}
});
});
// 新增变量
$('#btnAddVariable').on('click', function () {
$('#variableForm')[0].reset();
$('input[name="id"]').val('');
// 确保新增模式下别名输入框是启用的
$('input[name="alias"]').prop('disabled', false);
layer.open({
type: 1,
title: '新增变量',
content: $('#variableFormLayer'),
area: ['500px', '435px'],
btn: ['创建', '取消'],
yes: function (index, layero) {
// 手动收集表单数据
var formData = {};
$('#variableForm').find('input, select, textarea').each(function () {
var $this = $(this);
var name = $this.attr('name');
if (name && name !== 'id') {
formData[name] = $this.val();
}
});
// 验证必填字段
if (!formData.alias || formData.alias.trim() === '') {
layer.msg('请输入变量别名', { icon: 2 });
return;
}
if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', { icon: 2 });
return;
}
$.ajax({
url: '/admin/variable/create',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
layer.close(index);
variablesTable.reload();
} else {
layer.msg(res.msg || '操作失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', { icon: 2 });
}
});
},
btn2: function (index) {
layer.close(index);
},
success: function () {
form.render();
},
shadeClose: false
});
});
// 批量删除
$('#btnBatchDeleteVariables').on('click', function () {
const checkStatus = table.checkStatus('variablesTable');
const data = checkStatus.data;
if (data.length === 0) {
layer.msg('请选择要删除的变量', { icon: 2 });
return;
}
layer.confirm('确定删除选中的 ' + data.length + ' 个变量吗?', { icon: 3, title: '提示' }, function (index) {
const ids = data.map(item => item.id);
$.ajax({
url: '/admin/variable/batch_delete',
type: 'POST',
data: JSON.stringify({ ids: ids }),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
variablesTable.reload();
} else {
layer.msg(res.msg || '批量删除失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '批量删除失败', { icon: 2 });
}
});
layer.close(index);
});
});
// 表格工具栏事件
table.on('tool(variablesTableFilter)', function (obj) {
const data = obj.data;
if (obj.event === 'edit') {
// 编辑
$('#variableForm')[0].reset();
$('input[name="uuid"]').val(data.uuid);
$('input[name="alias"]').val(data.alias);
// 在编辑模式下禁用别名输入框
$('input[name="alias"]').prop('disabled', true);
$('select[name="app_uuid"]').val(data.app_uuid || '0');
$('textarea[name="data"]').val(data.data);
$('textarea[name="remark"]').val(data.remark);
layer.open({
type: 1,
title: '编辑变量',
content: $('#variableFormLayer'),
area: ['500px', '435px'],
btn: ['保存', '取消'],
yes: function (index, layero) {
// 手动收集表单数据
var formData = {};
$('#variableForm').find('input, select, textarea').each(function () {
var $this = $(this);
var name = $this.attr('name');
// 编辑模式下排除alias字段避免修改别名
if (name && name !== 'id' && name !== 'alias') {
formData[name] = $this.val();
}
});
// 验证必填字段编辑模式下不验证alias
if (!formData.data || formData.data.trim() === '') {
layer.msg('请输入变量数据', { icon: 2 });
return;
}
$.ajax({
url: '/admin/variable/update',
type: 'POST',
data: JSON.stringify(formData),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
layer.close(index);
variablesTable.reload();
} else {
layer.msg(res.msg || '操作失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '操作失败', { icon: 2 });
}
});
},
btn2: function (index) {
layer.close(index);
},
success: function () {
form.render();
},
shadeClose: false
});
} else if (obj.event === 'del') {
// 删除
layer.confirm('确定删除该变量吗?', { icon: 3, title: '提示' }, function (index) {
$.ajax({
url: '/admin/variable/delete',
type: 'POST',
data: JSON.stringify({ id: data.id }),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
variablesTable.reload();
} else {
layer.msg(res.msg || '删除失败', { icon: 2 });
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '删除失败', { icon: 2 });
}
});
layer.close(index);
});
}
});
});
});
</script>
</section>
{{ end }}

View File

@@ -1,374 +0,0 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title>{{ .Title }}</title>
<!-- 站 点 协 议 -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta http-equiv="content-language" content="zh-cn">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
{{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
{{ if .Keywords }}<meta name="keywords" content="{{ .Keywords }}">{{ end }}
<!-- 站 点 图 标 -->
<link href='/favicon.ico' rel='icon' type='image/x-icon'>
<link href="/favicon.ico" rel="shortcut icon">
<link href="/favicon.ico" rel="bookmark">
<!-- 样 式 文 件 -->
<link rel="stylesheet" href="/static/lib/layui/css/layui.css"/>
<style>
html, body {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
font-family: 'Microsoft YaHei', Arial, sans-serif;
}
body {
background-color: #000000 !important;
}
.layui-container {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.body-background {
width: 420px;
min-height: 350px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.logo-title {
text-align: center;
letter-spacing: 3px;
padding: 0 0 0 0;
margin-bottom: 5px;
}
.logo-title h1 {
color: #2550dd;
font-size: 28px;
font-weight: 600;
margin: 0;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes glow {
from {
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
}
to {
text-shadow: 0 0 30px rgba(0, 212, 255, 0.8), 0 0 40px rgba(0, 212, 255, 0.6);
}
}
.box-form {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(240, 248, 255, 0.9));
border: 2px solid rgba(0, 212, 255, 0.3);
border-radius: 15px;
padding: 30px 25px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
.box-form::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.1), transparent);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
.box-form .layui-form-item {
margin-bottom: 20px;
position: relative;
}
.warning-text {
font-size: 24px;
color: #ff4757;
font-weight: 600;
text-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
margin: 15px 0;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.info-text {
color: #3742fa;
font-size: 16px;
font-weight: 500;
margin: 15px 0;
text-shadow: 0 1px 2px rgba(55, 66, 250, 0.2);
}
.body_box {
text-align: center;
}
.body_footer {
padding-top: 15px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.body_beian {
padding-top: 8px;
}
.body_beian a {
color: rgba(0, 212, 255, 0.8);
text-decoration: none;
font-size: 13px;
transition: all 0.3s ease;
}
.body_beian a:hover {
color: #00d4ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
#canvas {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
hr {
border: none;
height: 2px;
background: linear-gradient(90deg, transparent, #00d4ff, transparent);
margin: 20px 0;
border-radius: 1px;
}
</style>
</head>
<body>
<!-- 代 码 结 构 -->
<div class="layui-container">
<canvas id="canvas"></canvas>
<div class="body-background body_box">
<div class="layui-form box-form body_box">
<div class="layui-form-item logo-title">
<h1><strong>{{ .SystemName }}</strong></h1>
</div>
<hr>
<div class="layui-form-item">
<div class="warning-text">{{ .WarningText }}</div>
</div>
<div class="layui-form-item">
<div class="info-text">{{ .InfoText }}</div>
</div>
</div>
<div class="body_footer">{{ .FooterText }}</div>
{{ if .ICPRecord }}
<div class="body_beian"><a href="{{ .ICPRecordLink }}" target="_blank">{{ .ICPRecord }}</a></div>
{{ end }}
</div>
</div>
<!-- 资 源 引 入 -->
<script src="/static/lib/jquery/jquery.min.js" type="text/javascript"></script>
<script>
// 设置版权年份 (保留此逻辑以防 FooterText 中使用了 id="currentYear")
if(document.getElementById('currentYear')) {
document.getElementById('currentYear').textContent = new Date().getFullYear();
}
// 获取canvas元素和绘图上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 设置canvas尺寸为全屏
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 粒子类
class Particle {
constructor() {
this.reset();
}
// 重置粒子位置和属性
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 2;
this.vy = (Math.random() - 0.5) * 2;
this.size = Math.random() * 3 + 1;
this.opacity = Math.random() * 0.8 + 0.2;
this.color = this.getRandomColor();
}
// 获取随机颜色
getRandomColor() {
const colors = [
'#00FF00', '#0080FF', '#FF0080', '#FFFF00',
'#FF8000', '#8000FF', '#00FFFF', '#FF4000'
];
return colors[Math.floor(Math.random() * colors.length)];
}
// 更新粒子位置
update() {
this.x += this.vx;
this.y += this.vy;
// 边界检测,粒子超出边界时重置
if (this.x < 0 || this.x > canvas.width ||
this.y < 0 || this.y > canvas.height) {
this.reset();
}
// 随机改变透明度
this.opacity += (Math.random() - 0.5) * 0.02;
this.opacity = Math.max(0.1, Math.min(1, this.opacity));
}
// 绘制粒子
draw() {
ctx.save();
ctx.globalAlpha = this.opacity;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// 创建粒子数组
const particles = [];
const particleCount = 150;
// 初始化粒子
const initParticles = () => {
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle());
}
};
// 绘制连线
const drawConnections = () => {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 如果距离小于100像素绘制连线
if (distance < 100) {
ctx.save();
ctx.globalAlpha = (100 - distance) / 100 * 0.3;
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
ctx.restore();
}
}
}
};
// 动画循环
const animate = () => {
// 清除画布,使用半透明黑色创建拖尾效果
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 更新和绘制所有粒子
particles.forEach(particle => {
particle.update();
particle.draw();
});
// 绘制粒子间的连线
drawConnections();
requestAnimationFrame(animate);
};
// 鼠标交互效果
const addMouseInteraction = () => {
let mouseX = 0;
let mouseY = 0;
canvas.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
// 鼠标附近的粒子会被吸引
particles.forEach(particle => {
const dx = mouseX - particle.x;
const dy = mouseY - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 150) {
particle.vx += dx * 0.0001;
particle.vy += dy * 0.0001;
}
});
});
// 点击时添加新粒子
canvas.addEventListener('click', (e) => {
for (let i = 0; i < 5; i++) {
const newParticle = new Particle();
newParticle.x = e.clientX + (Math.random() - 0.5) * 20;
newParticle.y = e.clientY + (Math.random() - 0.5) * 20;
particles.push(newParticle);
}
// 限制粒子数量
if (particles.length > particleCount + 50) {
particles.splice(0, 5);
}
});
};
// 启动粒子系统
initParticles();
addMouseInteraction();
animate();
</script>
</body>
</html>

View File

@@ -1,209 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{{ .title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="/static/lib/layui/css/layui.css">
<style>
body {
background-color: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 40px 0;
}
.install-box {
width: 600px;
background: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.install-header {
text-align: center;
margin-bottom: 30px;
}
.install-header h2 {
color: #333;
font-weight: 500;
}
.install-header p {
color: #999;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="install-box">
<div class="install-header">
<h2>系统初始化</h2>
<p>欢迎使用,请完成以下初始化设置</p>
</div>
<form class="layui-form" lay-filter="install-form">
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
<legend style="font-size: 14px;">1. 数据库配置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">数据库类型</label>
<div class="layui-input-block">
<input type="radio" name="db_type" value="sqlite" title="SQLite (默认)" lay-filter="db_type" checked>
<input type="radio" name="db_type" value="mysql" title="MySQL" lay-filter="db_type">
</div>
</div>
<div id="mysql-config" style="display: none;">
<div class="layui-form-item">
<label class="layui-form-label">主机地址</label>
<div class="layui-input-block">
<input type="text" name="db_host" value="127.0.0.1" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">端口号</label>
<div class="layui-input-block">
<input type="number" name="db_port" value="3306" lay-affix="number" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">数据库名</label>
<div class="layui-input-block">
<input type="text" name="db_name" value="networkauth" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="db_user" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" name="db_pass" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend style="font-size: 14px;">2. 站点信息</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">站点标题</label>
<div class="layui-input-block">
<input type="text" name="site_title" lay-verify="required" value="NetworkAuth" autocomplete="off" class="layui-input">
</div>
</div>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend style="font-size: 14px;">3. 管理员设置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">管理员账号</label>
<div class="layui-input-block">
<input type="text" name="admin_username" lay-verify="required" placeholder="设置管理员账号" value="admin" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">管理员密码</label>
<div class="layui-input-block">
<input type="password" name="admin_password" lay-verify="required|pass" placeholder="设置管理员密码至少6位" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="password" name="confirm_password" lay-verify="required|confirmPass" placeholder="请再次输入管理员密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item" style="margin-top: 40px; text-align: center;">
<button class="layui-btn layui-btn-normal layui-btn-lg" style="width: 200px;" lay-submit lay-filter="install-submit">立即初始化</button>
</div>
</form>
</div>
<script src="/static/lib/layui/layui.js"></script>
<script>
layui.use(['form', 'layer', 'jquery'], () => {
const form = layui.form;
const layer = layui.layer;
const $ = layui.jquery;
// 监听数据库类型切换
form.on('radio(db_type)', (data) => {
if (data.value === 'mysql') {
$('#mysql-config').slideDown();
$('#mysql-config input').attr('lay-verify', 'required');
} else {
$('#mysql-config').slideUp();
$('#mysql-config input').removeAttr('lay-verify');
}
});
// 自定义验证规则
form.verify({
pass: [
/^[\S]{6,20}$/,
'密码必须6到20位且不能出现空格'
],
confirmPass: (value) => {
const pass = $('input[name="admin_password"]').val();
if(value !== pass){
return '两次输入的密码不一致';
}
}
});
// 监听提交
form.on('submit(install-submit)', (data) => {
const loading = layer.load(2, {shade: [0.1, '#fff']});
// 处理 db_port 转换为整数
const payload = { ...data.field };
if (payload.db_port) {
payload.db_port = parseInt(payload.db_port, 10) || 3306;
}
$.ajax({
url: '/api/install',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload),
success: (res) => {
layer.close(loading);
if (res.code === 0) {
layer.msg('系统初始化成功!正在跳转登录...', {
icon: 1,
time: 2000
}, () => {
window.location.href = '/admin/login';
});
} else {
layer.msg(res.msg || '初始化失败', {icon: 2});
}
},
error: (xhr) => {
layer.close(loading);
const res = xhr.responseJSON;
layer.msg(res ? res.msg : '请求失败,请重试', {icon: 2});
}
});
return false; // 阻止表单跳转
});
});
</script>
</body>
</html>