diff --git a/VERSION b/VERSION
index 943f9cb..53adb84 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.7.1
+1.8.2
diff --git a/config.yaml b/config.yaml
index 875d5aa..508eae0 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,7 +1,7 @@
base:
name: FileCodeBox
description: 开箱即用的文件快传系统
- keywords: ""
+ keywords: FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件
port: 12345
host: 0.0.0.0
datapath: /Users/zhangyi/zy/FileCodeBox/data
@@ -44,8 +44,6 @@ mcp:
enablemcpserver: 0
mcpport: ""
mcphost: ""
-notifytitle: ""
-notifycontent: ""
ui:
themes_select: themes/2025
background: ""
@@ -54,15 +52,9 @@ ui:
User-agent: *
Disallow: /
show_admin_addr: 0
- opacity: 0
-themes_select: themes/2025
-robots_text: |-
- User-agent: *
- Disallow: /
-page_explain: 请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。
-show_admin_addr: 0
-opacity: 0
-background: ""
+ opacity: 1
+notify_title: ""
+notify_content: ""
sys_start: ""
upload_minute: 0
upload_count: 0
diff --git a/docs/swagger-enhanced.yaml b/docs/swagger-enhanced.yaml
index f555c50..d4d8c13 100644
--- a/docs/swagger-enhanced.yaml
+++ b/docs/swagger-enhanced.yaml
@@ -2,7 +2,7 @@ swagger: "2.0"
info:
title: "FileCodeBox API"
description: "FileCodeBox 是一个用于文件分享和代码片段管理的 Web 应用程序"
- version: "1.7.1"
+ version: "1.8.2"
termsOfService: "http://swagger.io/terms/"
contact:
name: "API Support"
@@ -69,7 +69,7 @@ paths:
example: "2025-09-11T10:00:00Z"
version:
type: "string"
- example: "1.7.1"
+ example: "1.8.2"
uptime:
type: "string"
example: "2h30m15s"
diff --git a/docs/swagger.json b/docs/swagger.json
index 8e31f36..0a5f130 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -13,7 +13,7 @@
"name": "MIT",
"url": "https://github.com/zy84338719/filecodebox/blob/main/LICENSE"
},
- "version": "1.7.1"
+ "version": "1.8.2"
},
"host": "localhost:12345",
"basePath": "/",
@@ -553,7 +553,7 @@
},
"version": {
"type": "string",
- "example": "1.7.1"
+ "example": "1.8.2"
}
}
},
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 40e3092..76a5289 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -12,7 +12,7 @@ definitions:
example: 2h30m15s
type: string
version:
- example: 1.7.1
+ example: 1.8.2
type: string
type: object
handlers.SystemConfig:
@@ -57,7 +57,7 @@ info:
url: https://github.com/zy84338719/filecodebox/blob/main/LICENSE
termsOfService: http://swagger.io/terms/
title: FileCodeBox API
- version: "1.7.1"
+ version: "1.8.2"
paths:
/api/config:
get:
diff --git a/internal/config/manager.go b/internal/config/manager.go
index d749d2b..79b043a 100644
--- a/internal/config/manager.go
+++ b/internal/config/manager.go
@@ -3,7 +3,6 @@ package config
import (
"errors"
"os"
- "strconv"
"strings"
"gopkg.in/yaml.v3"
@@ -56,15 +55,17 @@ func NewConfigManager() *ConfigManager {
func InitManager() *ConfigManager {
cm := NewConfigManager()
- // 尝试加载 YAML 配置文件
+ var sources []ConfigSource
+
if configPath := os.Getenv("CONFIG_PATH"); configPath != "" {
- _ = cm.LoadFromYAML(configPath)
+ sources = append(sources, YAMLFileSource{Path: configPath})
} else if _, err := os.Stat("./config.yaml"); err == nil {
- _ = cm.LoadFromYAML("./config.yaml")
+ sources = append(sources, YAMLFileSource{Path: "./config.yaml"})
}
- // 应用环境变量覆盖
- cm.applyEnvironmentOverrides()
+ sources = append(sources, NewDefaultEnvSource())
+
+ _ = cm.ApplySources(sources...)
return cm
}
@@ -135,24 +136,24 @@ func (cm *ConfigManager) mergeSimpleFields(fileCfg *ConfigManager) {
}
}
-// LoadFromYAML 从 YAML 文件加载配置
-func (cm *ConfigManager) LoadFromYAML(path string) error {
- b, err := os.ReadFile(path)
- if err != nil {
- return err
- }
-
- var fileCfg ConfigManager
- if err := yaml.Unmarshal(b, &fileCfg); err != nil {
- return err
+// ApplySources processes a group of configuration sources and collects errors.
+func (cm *ConfigManager) ApplySources(sources ...ConfigSource) error {
+ var errs []error
+ for _, source := range sources {
+ if source == nil {
+ continue
+ }
+ if err := source.Apply(cm); err != nil {
+ errs = append(errs, err)
+ }
}
- // 按模块合并配置
- cm.mergeConfigModules(&fileCfg)
- cm.mergeUserConfig(fileCfg.User)
- cm.mergeSimpleFields(&fileCfg)
+ return errors.Join(errs...)
+}
- return nil
+// LoadFromYAML 从 YAML 文件加载配置
+func (cm *ConfigManager) LoadFromYAML(path string) error {
+ return cm.ApplySources(YAMLFileSource{Path: path})
}
// ReloadConfig 重新加载配置(仅支持环境变量,保持端口不变)
@@ -190,30 +191,8 @@ func (cm *ConfigManager) PersistYAML() error {
// applyEnvironmentOverrides 应用环境变量覆盖配置
func (cm *ConfigManager) applyEnvironmentOverrides() {
- // 基础配置环境变量
- if port := os.Getenv("PORT"); port != "" {
- if n, err := strconv.Atoi(port); err == nil {
- cm.Base.Port = n
- }
- }
- if dataPath := os.Getenv("DATA_PATH"); dataPath != "" {
- cm.Base.DataPath = dataPath
- }
-
- // MCP 配置环境变量
- if enableMCP := os.Getenv("ENABLE_MCP_SERVER"); enableMCP != "" {
- if enableMCP == "true" || enableMCP == "1" {
- cm.MCP.EnableMCPServer = 1
- } else {
- cm.MCP.EnableMCPServer = 0
- }
- }
- if mcpPort := os.Getenv("MCP_PORT"); mcpPort != "" {
- cm.MCP.MCPPort = mcpPort
- }
- if mcpHost := os.Getenv("MCP_HOST"); mcpHost != "" {
- cm.MCP.MCPHost = mcpHost
- }
+ // 收集错误以便在调用者中统一处理,保持现有签名
+ _ = NewDefaultEnvSource().Apply(cm)
}
// Save 保存配置(已废弃,请使用 config.yaml 和环境变量)
diff --git a/internal/config/manager_test.go b/internal/config/manager_test.go
index 9be60a8..5109ffc 100644
--- a/internal/config/manager_test.go
+++ b/internal/config/manager_test.go
@@ -66,3 +66,18 @@ func TestEnvOverride(t *testing.T) {
t.Fatalf("expected PORT env to override to 9090, got %d", cm.Base.Port)
}
}
+
+func TestApplySourcesAggregatesErrors(t *testing.T) {
+ cm := NewConfigManager()
+ src := NewDefaultEnvSource()
+ src.lookup = func(key string) string {
+ if key == "ENABLE_MCP_SERVER" {
+ return "definitely-not-bool"
+ }
+ return ""
+ }
+
+ if err := cm.ApplySources(src); err == nil {
+ t.Fatalf("expected aggregated error when environment value invalid")
+ }
+}
diff --git a/internal/config/source.go b/internal/config/source.go
new file mode 100644
index 0000000..4d8465b
--- /dev/null
+++ b/internal/config/source.go
@@ -0,0 +1,143 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+)
+
+// ConfigSource represents a configuration input that can mutate the manager state.
+type ConfigSource interface {
+ Apply(*ConfigManager) error
+}
+
+// ConfigSourceFunc allows plain functions to be used as ConfigSource.
+type ConfigSourceFunc func(*ConfigManager) error
+
+// Apply executes the underlying function.
+func (f ConfigSourceFunc) Apply(cm *ConfigManager) error { return f(cm) }
+
+// YAMLFileSource loads configuration values from a YAML file.
+type YAMLFileSource struct {
+ Path string
+}
+
+// Apply reads and merges YAML content into the manager.
+func (s YAMLFileSource) Apply(cm *ConfigManager) error {
+ if strings.TrimSpace(s.Path) == "" {
+ return errors.New("config: YAML path is empty")
+ }
+
+ data, err := os.ReadFile(s.Path)
+ if err != nil {
+ return err
+ }
+
+ var fileCfg ConfigManager
+ if err := yaml.Unmarshal(data, &fileCfg); err != nil {
+ return fmt.Errorf("config: unmarshal %s: %w", s.Path, err)
+ }
+
+ cm.mergeConfigModules(&fileCfg)
+ cm.mergeUserConfig(fileCfg.User)
+ cm.mergeSimpleFields(&fileCfg)
+ return nil
+}
+
+type envOverride struct {
+ key string
+ apply func(string, *ConfigManager) error
+}
+
+// EnvSource mutates configuration using environment variables.
+type EnvSource struct {
+ overrides []envOverride
+ lookup func(string) string
+}
+
+// NewDefaultEnvSource returns the built-in environment overrides.
+func NewDefaultEnvSource() EnvSource {
+ return EnvSource{
+ overrides: []envOverride{
+ {key: "PORT", apply: applyPortOverride},
+ {key: "DATA_PATH", apply: applyDataPathOverride},
+ {key: "ENABLE_MCP_SERVER", apply: applyMCPEnabledOverride},
+ {key: "MCP_PORT", apply: applyMCPPortOverride},
+ {key: "MCP_HOST", apply: applyMCPHostOverride},
+ },
+ }
+}
+
+// Apply applies every configured override.
+func (s EnvSource) Apply(cm *ConfigManager) error {
+ lookup := s.lookup
+ if lookup == nil {
+ lookup = os.Getenv
+ }
+
+ var errs []error
+ for _, override := range s.overrides {
+ if value := lookup(override.key); value != "" {
+ if err := override.apply(value, cm); err != nil {
+ errs = append(errs, fmt.Errorf("%s: %w", override.key, err))
+ }
+ }
+ }
+
+ return errors.Join(errs...)
+}
+
+func applyPortOverride(val string, cm *ConfigManager) error {
+ port, err := strconv.Atoi(val)
+ if err != nil {
+ return fmt.Errorf("invalid port %q", val)
+ }
+ cm.Base.Port = port
+ return nil
+}
+
+func applyDataPathOverride(val string, cm *ConfigManager) error {
+ if strings.TrimSpace(val) == "" {
+ return errors.New("data path cannot be blank")
+ }
+ cm.Base.DataPath = val
+ return nil
+}
+
+func applyMCPEnabledOverride(val string, cm *ConfigManager) error {
+ enabled, err := parseBool(val)
+ if err != nil {
+ return err
+ }
+ if enabled {
+ cm.MCP.EnableMCPServer = 1
+ } else {
+ cm.MCP.EnableMCPServer = 0
+ }
+ return nil
+}
+
+func applyMCPPortOverride(val string, cm *ConfigManager) error {
+ cm.MCP.MCPPort = val
+ return nil
+}
+
+func applyMCPHostOverride(val string, cm *ConfigManager) error {
+ cm.MCP.MCPHost = val
+ return nil
+}
+
+func parseBool(val string) (bool, error) {
+ switch strings.ToLower(strings.TrimSpace(val)) {
+ case "1", "true", "t", "yes", "y":
+ return true, nil
+ case "0", "false", "f", "no", "n":
+ return false, nil
+ default:
+ return false, fmt.Errorf("invalid boolean value %q", val)
+ }
+}
diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go
index 19e01bf..21ecb21 100644
--- a/internal/handlers/admin.go
+++ b/internal/handlers/admin.go
@@ -137,6 +137,9 @@ func (h *AdminHandler) GetConfig(c *gin.Context) {
SysStart: &cfg.SysStart,
},
}
+
+ resp.NotifyTitle = &cfg.NotifyTitle
+ resp.NotifyContent = &cfg.NotifyContent
common.SuccessResponse(c, resp)
}
diff --git a/internal/handlers/api.go b/internal/handlers/api.go
index d1c57ae..c504726 100644
--- a/internal/handlers/api.go
+++ b/internal/handlers/api.go
@@ -29,7 +29,7 @@ func NewAPIHandler(manager *config.ConfigManager) *APIHandler {
type HealthResponse struct {
Status string `json:"status" example:"ok"`
Timestamp string `json:"timestamp" example:"2025-09-11T10:00:00Z"`
- Version string `json:"version" example:"1.7.1"`
+ Version string `json:"version" example:"1.8.2"`
Uptime string `json:"uptime" example:"2h30m15s"`
}
diff --git a/internal/models/service/system.go b/internal/models/service/system.go
index 4fcb9bc..c9155ba 100644
--- a/internal/models/service/system.go
+++ b/internal/models/service/system.go
@@ -19,7 +19,7 @@ var (
GitBranch = "unknown"
// Version 应用版本号
- Version = "1.7.1"
+ Version = "1.8.2"
)
// BuildInfo 构建信息结构体
diff --git a/internal/models/web/admin.go b/internal/models/web/admin.go
index 4cab9b6..76a7f7d 100644
--- a/internal/models/web/admin.go
+++ b/internal/models/web/admin.go
@@ -160,6 +160,10 @@ type AdminConfigRequest struct {
// 系统运行时特有字段(不属于配置模块的字段)
SysStart *string `json:"sys_start,omitempty"`
+
+ // 顶层通知字段保留与历史配置结构兼容
+ NotifyTitle *string `json:"notify_title,omitempty"`
+ NotifyContent *string `json:"notify_content,omitempty"`
}
// CountResponse 通用计数响应
diff --git a/internal/routes/admin.go b/internal/routes/admin.go
index 724c54a..9bf90ee 100644
--- a/internal/routes/admin.go
+++ b/internal/routes/admin.go
@@ -1,9 +1,6 @@
package routes
import (
- "os"
- "path/filepath"
-
"github.com/zy84338719/filecodebox/internal/config"
"github.com/zy84338719/filecodebox/internal/handlers"
"github.com/zy84338719/filecodebox/internal/middleware"
@@ -56,116 +53,36 @@ func SetupAdminRoutes(
// 将管理后台静态资源与前端入口注册为公开路由,允许未认证用户加载登录页面和相关静态资源
// 注意:API 路由仍然放在受保护的 authGroup 中
- themeDir := "./" + cfg.UI.ThemesSelect
-
- // css
- adminGroup.GET("/css/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "admin", "css", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
+ serveFile := func(parts ...string) func(*gin.Context) {
+ return func(c *gin.Context) {
+ rel := c.Param("filepath")
+ joined := append(parts, rel)
+ path, err := static.ResolveThemeFile(cfg, joined...)
+ if err != nil {
+ c.Status(404)
+ return
+ }
+ c.File(path)
}
- c.File(p)
- })
+ }
- // HEAD for css
- adminGroup.HEAD("/css/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "admin", "css", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
+ // css
+ adminGroup.GET("/css/*filepath", serveFile("admin", "css"))
+ adminGroup.HEAD("/css/*filepath", serveFile("admin", "css"))
// js
- adminGroup.GET("/js/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "admin", "js", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
-
- // HEAD for js
- adminGroup.HEAD("/js/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "admin", "js", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
+ adminGroup.GET("/js/*filepath", serveFile("admin", "js"))
+ adminGroup.HEAD("/js/*filepath", serveFile("admin", "js"))
// templates
- adminGroup.GET("/templates/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "admin", "templates", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
-
- // HEAD for templates
- adminGroup.HEAD("/templates/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "admin", "templates", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
+ adminGroup.GET("/templates/*filepath", serveFile("admin", "templates"))
+ adminGroup.HEAD("/templates/*filepath", serveFile("admin", "templates"))
// assets and components
- adminGroup.GET("/assets/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "assets", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
-
- // HEAD for assets
- adminGroup.HEAD("/assets/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "assets", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
-
- adminGroup.GET("/components/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "components", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
-
- // HEAD for components
- adminGroup.HEAD("/components/*filepath", func(c *gin.Context) {
- fp := c.Param("filepath")
- p := filepath.Join(themeDir, "components", fp)
- if _, err := os.Stat(p); err != nil {
- c.Status(404)
- return
- }
- c.File(p)
- })
+ adminGroup.GET("/assets/*filepath", serveFile("assets"))
+ adminGroup.HEAD("/assets/*filepath", serveFile("assets"))
+ adminGroup.GET("/components/*filepath", serveFile("components"))
+ adminGroup.HEAD("/components/*filepath", serveFile("components"))
// 管理前端入口公开:允许未认证用户加载登录页面
adminGroup.GET("/", func(c *gin.Context) {
diff --git a/internal/services/admin/config.go b/internal/services/admin/config.go
index e4ccc80..a424c98 100644
--- a/internal/services/admin/config.go
+++ b/internal/services/admin/config.go
@@ -2,6 +2,7 @@ package admin
import (
"fmt"
+ "strings"
"github.com/zy84338719/filecodebox/internal/config"
"github.com/zy84338719/filecodebox/internal/models/web"
@@ -22,6 +23,12 @@ func (s *Service) UpdateConfig(configData map[string]interface{}) error {
// UpdateConfigFromRequest 从结构化请求更新配置
func (s *Service) UpdateConfigFromRequest(configRequest *web.AdminConfigRequest) error {
// 直接更新配置管理器的各个模块,不使用 map 转换
+ ensureUI := func() *config.UIConfig {
+ if s.manager.UI == nil {
+ s.manager.UI = &config.UIConfig{}
+ }
+ return s.manager.UI
+ }
// 处理基础配置
if configRequest.Base != nil {
@@ -152,24 +159,20 @@ func (s *Service) UpdateConfigFromRequest(configRequest *web.AdminConfigRequest)
// 处理 UI 配置
if configRequest.UI != nil {
uiConfig := configRequest.UI
- if uiConfig.ThemesSelect != "" {
- s.manager.UI.ThemesSelect = uiConfig.ThemesSelect
- }
- if uiConfig.Background != "" {
- s.manager.UI.Background = uiConfig.Background
- }
- if uiConfig.PageExplain != "" {
- s.manager.UI.PageExplain = uiConfig.PageExplain
- }
- if uiConfig.RobotsText != "" {
- s.manager.UI.RobotsText = uiConfig.RobotsText
- }
- if uiConfig.ShowAdminAddr != 0 {
- s.manager.UI.ShowAdminAddr = uiConfig.ShowAdminAddr
- }
- if uiConfig.Opacity != 0 {
- s.manager.UI.Opacity = uiConfig.Opacity
+ ui := ensureUI()
+ if strings.TrimSpace(uiConfig.ThemesSelect) != "" {
+ ui.ThemesSelect = uiConfig.ThemesSelect
}
+ ui.PageExplain = uiConfig.PageExplain
+ ui.Opacity = uiConfig.Opacity
+ }
+
+ // 顶层通知字段
+ if configRequest.NotifyTitle != nil {
+ s.manager.NotifyTitle = *configRequest.NotifyTitle
+ }
+ if configRequest.NotifyContent != nil {
+ s.manager.NotifyContent = *configRequest.NotifyContent
}
// 处理系统运行时字段
@@ -177,8 +180,10 @@ func (s *Service) UpdateConfigFromRequest(configRequest *web.AdminConfigRequest)
s.manager.SysStart = *configRequest.SysStart
}
- // 保存配置到数据库和文件
- return s.manager.Save()
+ if err := s.manager.PersistYAML(); err != nil {
+ return fmt.Errorf("persist config: %w", err)
+ }
+ return nil
}
// GetFullConfig 获取完整配置 - 返回配置管理器结构体
diff --git a/internal/static/assets.go b/internal/static/assets.go
index 32e53aa..0d78119 100644
--- a/internal/static/assets.go
+++ b/internal/static/assets.go
@@ -2,6 +2,7 @@ package static
import (
"fmt"
+ "html"
"net/http"
"os"
"path/filepath"
@@ -11,14 +12,102 @@ import (
"github.com/zy84338719/filecodebox/internal/config"
)
+const defaultThemeDir = "themes/2025"
+
+func themeCandidates(cfg *config.ConfigManager) []string {
+ var candidates []string
+ seen := make(map[string]struct{})
+ add := func(path string) {
+ path = strings.TrimSpace(path)
+ if path == "" {
+ return
+ }
+ if _, ok := seen[path]; ok {
+ return
+ }
+ seen[path] = struct{}{}
+ candidates = append(candidates, path)
+ }
+
+ if cfg != nil && cfg.UI != nil {
+ add(cfg.UI.ThemesSelect)
+ }
+ add(defaultThemeDir)
+ return candidates
+}
+
+func themeDirExists(dir string) bool {
+ info, err := os.Stat(dir)
+ return err == nil && info.IsDir()
+}
+
+func firstExistingThemeDir(cfg *config.ConfigManager) string {
+ for _, candidate := range themeCandidates(cfg) {
+ if filepath.IsAbs(candidate) {
+ if themeDirExists(candidate) {
+ return candidate
+ }
+ continue
+ }
+ if themeDirExists(candidate) {
+ return candidate
+ }
+ }
+ return defaultThemeDir
+}
+
+func resolveThemeFilePath(cfg *config.ConfigManager, parts ...string) (string, error) {
+ var firstErr error
+ for _, candidate := range themeCandidates(cfg) {
+ pathParts := append([]string{candidate}, parts...)
+ path := filepath.Join(pathParts...)
+ info, err := os.Stat(path)
+ if err == nil {
+ if !info.IsDir() {
+ return path, nil
+ }
+ continue
+ }
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ if firstErr == nil {
+ firstErr = os.ErrNotExist
+ }
+ return "", firstErr
+}
+
+func loadThemeFile(cfg *config.ConfigManager, parts ...string) ([]byte, error) {
+ path, err := resolveThemeFilePath(cfg, parts...)
+ if err != nil {
+ return nil, err
+ }
+ return os.ReadFile(path)
+}
+
+// ResolveThemeFile returns the concrete filesystem path for a theme file, applying fallbacks.
+func ResolveThemeFile(cfg *config.ConfigManager, parts ...string) (string, error) {
+ return resolveThemeFilePath(cfg, parts...)
+}
+
+// ThemePath returns the resolved theme directory joined with optional relative parts.
+func ThemePath(cfg *config.ConfigManager, parts ...string) string {
+ root := firstExistingThemeDir(cfg)
+ if len(parts) == 0 {
+ return root
+ }
+ pathParts := append([]string{root}, parts...)
+ return filepath.Join(pathParts...)
+}
+
// RegisterStaticRoutes registers public-facing static routes (assets, css, js, components)
func RegisterStaticRoutes(router *gin.Engine, cfg *config.ConfigManager) {
- themeDir := fmt.Sprintf("./%s", cfg.UI.ThemesSelect)
-
- router.Static("/assets", fmt.Sprintf("%s/assets", themeDir))
- router.Static("/css", fmt.Sprintf("%s/css", themeDir))
- router.Static("/js", fmt.Sprintf("%s/js", themeDir))
- router.Static("/components", fmt.Sprintf("%s/components", themeDir))
+ themeDir := firstExistingThemeDir(cfg)
+ router.Static("/assets", filepath.Join(themeDir, "assets"))
+ router.Static("/css", filepath.Join(themeDir, "css"))
+ router.Static("/js", filepath.Join(themeDir, "js"))
+ router.Static("/components", filepath.Join(themeDir, "components"))
}
// Note: admin static routes are intentionally not registered here.
@@ -29,11 +118,12 @@ func RegisterStaticRoutes(router *gin.Engine, cfg *config.ConfigManager) {
// ServeIndex serves the main index page with basic template replacements.
func ServeIndex(c *gin.Context, cfg *config.ConfigManager) {
- indexPath := filepath.Join(".", cfg.UI.ThemesSelect, "index.html")
-
- content, err := os.ReadFile(indexPath)
+ content, err := loadThemeFile(cfg, "index.html")
if err != nil {
- c.String(http.StatusNotFound, "Index file not found")
+ html := fallbackIndexHTML(cfg)
+ c.Header("Cache-Control", "no-cache")
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(http.StatusOK, html)
return
}
@@ -58,11 +148,12 @@ func ServeIndex(c *gin.Context, cfg *config.ConfigManager) {
// ServeSetup serves the setup page with template replacements.
func ServeSetup(c *gin.Context, cfg *config.ConfigManager) {
- setupPath := filepath.Join(".", cfg.UI.ThemesSelect, "setup.html")
-
- content, err := os.ReadFile(setupPath)
+ content, err := loadThemeFile(cfg, "setup.html")
if err != nil {
- c.String(http.StatusNotFound, "Setup page not found")
+ html := fallbackSetupHTML(cfg)
+ c.Header("Cache-Control", "no-cache")
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(http.StatusOK, html)
return
}
@@ -81,11 +172,12 @@ func ServeSetup(c *gin.Context, cfg *config.ConfigManager) {
// ServeAdminPage serves the admin index page
func ServeAdminPage(c *gin.Context, cfg *config.ConfigManager) {
- adminPath := filepath.Join(".", cfg.UI.ThemesSelect, "admin", "index.html")
-
- content, err := os.ReadFile(adminPath)
+ content, err := loadThemeFile(cfg, "admin", "index.html")
if err != nil {
- c.String(http.StatusNotFound, "Admin page not found")
+ html := fallbackAdminHTML(cfg)
+ c.Header("Cache-Control", "no-cache")
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(http.StatusOK, html)
return
}
@@ -96,9 +188,7 @@ func ServeAdminPage(c *gin.Context, cfg *config.ConfigManager) {
// ServeUserPage serves user-facing static pages (login/register/dashboard/etc.)
func ServeUserPage(c *gin.Context, cfg *config.ConfigManager, pageName string) {
- userPagePath := filepath.Join(".", cfg.UI.ThemesSelect, pageName)
-
- content, err := os.ReadFile(userPagePath)
+ content, err := loadThemeFile(cfg, pageName)
if err != nil {
c.String(http.StatusNotFound, "User page not found: "+pageName)
return
@@ -115,3 +205,118 @@ func ServeUserPage(c *gin.Context, cfg *config.ConfigManager, pageName string) {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, html)
}
+
+func fallbackBaseName(cfg *config.ConfigManager) string {
+ if cfg != nil && cfg.Base != nil {
+ if name := strings.TrimSpace(cfg.Base.Name); name != "" {
+ return name
+ }
+ }
+ return "FileCodeBox"
+}
+
+func fallbackBaseDescription(cfg *config.ConfigManager) string {
+ if cfg != nil && cfg.Base != nil {
+ if desc := strings.TrimSpace(cfg.Base.Description); desc != "" {
+ return desc
+ }
+ }
+ return "A lightweight file sharing service"
+}
+
+func fallbackPageExplain(cfg *config.ConfigManager) string {
+ if cfg != nil && cfg.UI != nil {
+ if explain := strings.TrimSpace(cfg.UI.PageExplain); explain != "" {
+ return explain
+ }
+ }
+ return "Service is running, but the selected theme assets were not found."
+}
+
+func fallbackIndexHTML(cfg *config.ConfigManager) string {
+ name := html.EscapeString(fallbackBaseName(cfg))
+ desc := html.EscapeString(fallbackBaseDescription(cfg))
+ explain := html.EscapeString(fallbackPageExplain(cfg))
+
+ return fmt.Sprintf(`
+
+
+
+
+%s
+
+
+
+
+
%s
+
%s
+
%s
+
The configured theme directory is missing; static assets will load once it is restored.
+
+
+`, name, name, desc, explain)
+}
+
+func fallbackSetupHTML(cfg *config.ConfigManager) string {
+ name := html.EscapeString(fallbackBaseName(cfg))
+ desc := html.EscapeString(fallbackBaseDescription(cfg))
+
+ return fmt.Sprintf(`
+
+
+
+
+%s - Setup
+
+
+
+
+
%s 初始化
+
%s
+
主题资源尚未就绪,请先完成配置文件中的 ui.themes_select 目录部署。
+
+- 确认主题目录已随构建产物一并分发
+- 或在配置中切换到有效的主题路径
+- 之后重新刷新本页面即可完成初始化流程
+
+
+
+`, name, name, desc)
+}
+
+func fallbackAdminHTML(cfg *config.ConfigManager) string {
+ name := html.EscapeString(fallbackBaseName(cfg))
+
+ return fmt.Sprintf(`
+
+
+
+
+%s Admin
+
+
+
+
+
Admin theme missing
+
Static assets for the admin console are unavailable. Restore the configured theme directory to load the full interface.
+
+
+`, name)
+}
diff --git a/main.go b/main.go
index e087245..b0db491 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,7 @@
package main
// @title FileCodeBox API
-// @version 1.7.1
+// @version 1.8.2
// @description FileCodeBox 是一个用于文件分享和代码片段管理的 Web 应用程序
// @termsOfService http://swagger.io/terms/
diff --git a/themes/2025/admin/css/admin-modern.css b/themes/2025/admin/css/admin-modern.css
new file mode 100644
index 0000000..a17e8c4
--- /dev/null
+++ b/themes/2025/admin/css/admin-modern.css
@@ -0,0 +1,1665 @@
+:root {
+ --admin-bg-top: #eef2ff;
+ --admin-bg-bottom: #ffffff;
+ --admin-surface: #ffffff;
+ --admin-surface-subtle: #f7f8ff;
+ --admin-surface-muted: rgba(79, 70, 229, 0.08);
+ --admin-border: rgba(15, 23, 42, 0.08);
+ --admin-text: #0f172a;
+ --admin-muted: rgba(51, 65, 85, 0.88);
+ --admin-subtle: rgba(94, 106, 129, 0.7);
+ --admin-accent: #4f46e5;
+ --admin-accent-strong: #6366f1;
+ --admin-accent-soft: rgba(79, 70, 229, 0.12);
+ --admin-danger: #ef4444;
+ --admin-shadow-card: 0 18px 40px rgba(15, 23, 42, 0.08);
+ --admin-shadow-soft: 0 12px 24px rgba(79, 70, 229, 0.12);
+ --admin-placeholder-bg: #f8f9ff;
+ --admin-inverse-text: #f8fafc;
+ --admin-heading: #0f172a;
+ --admin-interactive-bg: rgba(79, 70, 229, 0.08);
+ --admin-interactive-bg-hover: rgba(79, 70, 229, 0.16);
+ --admin-icon-default: rgba(79, 70, 229, 0.7);
+ --admin-overlay: rgba(15, 23, 42, 0.55);
+ --admin-toggle-bg: rgba(148, 163, 184, 0.16);
+ --admin-ghost-bg: rgba(99, 102, 241, 0.12);
+ --admin-ghost-bg-hover: rgba(99, 102, 241, 0.2);
+ --admin-danger-soft: rgba(248, 113, 113, 0.12);
+ --admin-danger-soft-hover: rgba(248, 113, 113, 0.18);
+ --admin-danger-strong: #b91c1c;
+ --admin-nav-active-start: rgba(99, 102, 241, 0.78);
+ --admin-nav-active-end: rgba(129, 140, 248, 0.64);
+ --admin-nav-active-shadow: 0 16px 28px rgba(99, 102, 241, 0.26);
+ --admin-primary-btn-start: rgba(99, 102, 241, 0.82);
+ --admin-primary-btn-end: rgba(129, 140, 248, 0.68);
+ --admin-primary-btn-shadow: 0 12px 24px rgba(99, 102, 241, 0.22);
+ --admin-form-hint: rgba(71, 85, 105, 0.72);
+ --admin-stat-bg: rgba(255, 255, 255, 0.9);
+ --admin-stat-border: rgba(99, 102, 241, 0.16);
+ --admin-stat-highlight: rgba(99, 102, 241, 0.14);
+ --admin-stat-glow: 0 24px 45px rgba(79, 70, 229, 0.18);
+ --admin-stat-hover-glow: 0 28px 52px rgba(79, 70, 229, 0.24);
+ --admin-stat-icon-bg: rgba(79, 70, 229, 0.12);
+ --admin-stat-icon-color: #4338ca;
+ --admin-stat-trend-bg: rgba(79, 70, 229, 0.12);
+ --admin-stat-trend-text: #4338ca;
+ --admin-panel-bg: rgba(255, 255, 255, 0.95);
+ --admin-panel-border: rgba(99, 102, 241, 0.16);
+ --admin-panel-shadow: 0 24px 45px rgba(15, 23, 42, 0.08);
+ --admin-panel-highlight: rgba(99, 102, 241, 0.06);
+ --admin-action-bg: rgba(255, 255, 255, 0.92);
+ --admin-action-border: rgba(99, 102, 241, 0.12);
+ --admin-action-hover: rgba(79, 70, 229, 0.12);
+ --admin-action-glow: 0 18px 32px rgba(15, 23, 42, 0.08);
+ --admin-action-icon-bg: rgba(79, 70, 229, 0.12);
+ --admin-action-icon-color: #4338ca;
+ --admin-action-icon-shadow: 0 12px 26px rgba(79, 70, 229, 0.18);
+ --admin-status-bg: rgba(255, 255, 255, 0.92);
+ --admin-status-border: rgba(99, 102, 241, 0.14);
+ --admin-status-hover-border: rgba(79, 70, 229, 0.25);
+ --admin-status-dot-online: #34d399;
+ --admin-status-dot-warning: #facc15;
+ --admin-status-dot-offline: #f87171;
+}
+
+.admin-theme-dark {
+ --admin-bg-top: #0f172a;
+ --admin-bg-bottom: #111c44;
+ --admin-surface: rgba(17, 24, 39, 0.92);
+ --admin-surface-subtle: rgba(15, 23, 42, 0.72);
+ --admin-surface-muted: rgba(99, 102, 241, 0.22);
+ --admin-border: rgba(148, 163, 184, 0.2);
+ --admin-text: #e2e8f0;
+ --admin-muted: rgba(203, 213, 225, 0.88);
+ --admin-subtle: rgba(148, 163, 184, 0.68);
+ --admin-accent: #818cf8;
+ --admin-accent-strong: #6366f1;
+ --admin-accent-soft: rgba(99, 102, 241, 0.3);
+ --admin-danger: #f87171;
+ --admin-shadow-card: 0 24px 55px rgba(15, 23, 42, 0.45);
+ --admin-shadow-soft: 0 18px 36px rgba(99, 102, 241, 0.32);
+ --admin-placeholder-bg: rgba(15, 23, 42, 0.65);
+ --admin-inverse-text: #0f172a;
+ --admin-heading: #f8fafc;
+ --admin-interactive-bg: rgba(129, 140, 248, 0.18);
+ --admin-interactive-bg-hover: rgba(129, 140, 248, 0.32);
+ --admin-icon-default: rgba(226, 232, 240, 0.8);
+ --admin-overlay: rgba(15, 23, 42, 0.7);
+ --admin-toggle-bg: rgba(51, 65, 85, 0.6);
+ --admin-ghost-bg: rgba(99, 102, 241, 0.24);
+ --admin-ghost-bg-hover: rgba(129, 140, 248, 0.36);
+ --admin-danger-soft: rgba(248, 113, 113, 0.2);
+ --admin-danger-soft-hover: rgba(248, 113, 113, 0.28);
+ --admin-danger-strong: #fecaca;
+ --admin-nav-active-start: rgba(99, 102, 241, 0.92);
+ --admin-nav-active-end: rgba(129, 140, 248, 0.78);
+ --admin-nav-active-shadow: 0 18px 36px rgba(99, 102, 241, 0.38);
+ --admin-primary-btn-start: rgba(99, 102, 241, 0.94);
+ --admin-primary-btn-end: rgba(129, 140, 248, 0.82);
+ --admin-primary-btn-shadow: 0 14px 28px rgba(99, 102, 241, 0.42);
+ --admin-form-hint: rgba(148, 163, 184, 0.72);
+ --admin-stat-bg: rgba(17, 24, 39, 0.92);
+ --admin-stat-border: rgba(129, 140, 248, 0.32);
+ --admin-stat-highlight: rgba(99, 102, 241, 0.28);
+ --admin-stat-glow: 0 30px 56px rgba(2, 6, 23, 0.65);
+ --admin-stat-hover-glow: 0 36px 64px rgba(30, 41, 59, 0.72);
+ --admin-stat-icon-bg: rgba(99, 102, 241, 0.24);
+ --admin-stat-icon-color: #e0e7ff;
+ --admin-stat-trend-bg: rgba(99, 102, 241, 0.22);
+ --admin-stat-trend-text: #c7d2fe;
+ --admin-panel-bg: rgba(17, 24, 39, 0.9);
+ --admin-panel-border: rgba(99, 102, 241, 0.28);
+ --admin-panel-shadow: 0 26px 56px rgba(2, 6, 23, 0.7);
+ --admin-panel-highlight: rgba(79, 70, 229, 0.12);
+ --admin-action-bg: rgba(30, 41, 59, 0.85);
+ --admin-action-border: rgba(129, 140, 248, 0.22);
+ --admin-action-hover: rgba(99, 102, 241, 0.22);
+ --admin-action-glow: 0 24px 44px rgba(2, 6, 23, 0.6);
+ --admin-action-icon-bg: rgba(99, 102, 241, 0.24);
+ --admin-action-icon-color: #e0e7ff;
+ --admin-action-icon-shadow: 0 18px 38px rgba(2, 6, 23, 0.65);
+ --admin-status-bg: rgba(17, 24, 39, 0.88);
+ --admin-status-border: rgba(99, 102, 241, 0.28);
+ --admin-status-hover-border: rgba(129, 140, 248, 0.38);
+ --admin-status-dot-online: #34d399;
+ --admin-status-dot-warning: #facc15;
+ --admin-status-dot-offline: #f87171;
+}
+
+body.admin-modern-body {
+ margin: 0;
+ min-height: 100vh;
+ background: linear-gradient(220deg, var(--admin-bg-top) 0%, var(--admin-bg-bottom) 60%, var(--admin-surface) 100%);
+ color: var(--admin-text);
+ font-family: "Inter", "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+.admin-modern-body *,
+.admin-modern-body *::before,
+.admin-modern-body *::after {
+ box-sizing: border-box;
+}
+
+.admin-modern-body h1,
+.admin-modern-body h2,
+.admin-modern-body h3,
+.admin-modern-body h4,
+.admin-modern-body h5,
+.admin-modern-body h6 {
+ color: var(--admin-text);
+ font-weight: 600;
+ letter-spacing: 0.01em;
+ margin-top: 0;
+}
+
+.admin-modern-body p,
+.admin-modern-body span,
+.admin-modern-body label,
+.admin-modern-body small,
+.admin-modern-body li {
+ color: var(--admin-muted);
+ line-height: 1.6;
+}
+
+.admin-modern-body small {
+ font-size: 13px;
+}
+
+.admin-modern-body a {
+ color: var(--admin-accent);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.admin-modern-body a:hover {
+ color: #312e81;
+}
+
+.admin-shell {
+ display: flex;
+ min-height: 100vh;
+ position: relative;
+}
+
+.admin-sidebar {
+ width: 260px;
+ background: var(--admin-surface);
+ border-right: 1px solid var(--admin-border);
+ display: flex;
+ flex-direction: column;
+ padding: 32px 24px 28px;
+ gap: 32px;
+ z-index: 30;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ box-shadow: var(--admin-shadow-card);
+}
+
+.sidebar-brand {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+
+.sidebar-brand .logo-icon {
+ width: 42px;
+ height: 42px;
+ border-radius: 14px;
+ background: var(--admin-accent-soft);
+ padding: 10px;
+}
+
+.brand-text {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.brand-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--admin-text);
+ letter-spacing: 0.02em;
+}
+
+.brand-subtitle {
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ color: var(--admin-subtle);
+}
+
+.sidebar-section,
+.sidebar-footer {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.sidebar-label {
+ font-size: 12px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--admin-subtle);
+ margin-bottom: 4px;
+}
+
+.tab-btn.nav-link {
+ width: 100%;
+ flex: none;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ border: none;
+ outline: none;
+ background: var(--admin-interactive-bg);
+ color: var(--admin-text);
+ border-radius: 14px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: transform 0.2s ease, background 0.2s ease, box-shadow 0.3s ease;
+ justify-content: flex-start;
+ text-transform: none;
+ letter-spacing: normal;
+ min-width: auto;
+ white-space: nowrap;
+}
+
+.tab-btn.nav-link i {
+ font-size: 16px;
+ color: var(--admin-icon-default);
+ transition: color 0.2s ease;
+}
+
+.tab-btn.nav-link:hover {
+ background: var(--admin-interactive-bg-hover);
+ transform: translateX(4px);
+}
+
+.tab-btn.nav-link.active {
+ background: linear-gradient(135deg, var(--admin-nav-active-start) 0%, var(--admin-nav-active-end) 100%);
+ color: #fff;
+ box-shadow: var(--admin-nav-active-shadow);
+}
+
+.tab-btn.nav-link.active i {
+ color: #fff;
+}
+
+.tab-btn.nav-link::before,
+.tab-btn.nav-link::after {
+ display: none !important;
+}
+
+.sidebar-action {
+ background: var(--admin-interactive-bg);
+ border: none;
+ color: var(--admin-text);
+ border-radius: 12px;
+ padding: 12px 16px;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ transition: background 0.2s ease, transform 0.2s ease;
+}
+
+.sidebar-action:hover {
+ background: var(--admin-interactive-bg-hover);
+ transform: translateX(4px);
+}
+
+.sidebar-action.logout {
+ background: var(--admin-danger-soft);
+ color: var(--admin-danger-strong);
+}
+
+.sidebar-action.logout:hover {
+ background: var(--admin-danger-soft-hover);
+}
+
+.sidebar-overlay {
+ position: fixed;
+ inset: 0;
+ background: var(--admin-overlay);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease;
+ z-index: 20;
+}
+
+.sidebar-overlay.active {
+ opacity: 0.85;
+ pointer-events: auto;
+}
+
+.admin-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ backdrop-filter: blur(18px);
+}
+
+.admin-header {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ padding: 32px 32px 8px;
+}
+
+.header-toggle {
+ display: none;
+ width: 44px;
+ height: 44px;
+ border-radius: 14px;
+ border: none;
+ background: var(--admin-toggle-bg);
+ color: var(--admin-text);
+ font-size: 18px;
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+}
+
+.headline {
+ flex: 1;
+}
+
+.headline h1 {
+ margin: 0;
+ font-size: 28px;
+ font-weight: 600;
+ color: var(--admin-heading);
+}
+
+.headline p {
+ margin: 8px 0 0;
+ max-width: 520px;
+ color: var(--admin-muted);
+ font-size: 14px;
+ line-height: 1.6;
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.header-btn-ghost {
+ background: var(--admin-ghost-bg);
+ color: var(--admin-accent);
+ box-shadow: none;
+}
+
+.header-btn-ghost:hover {
+ background: var(--admin-ghost-bg-hover);
+ box-shadow: none;
+}
+
+.header-metrics {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ max-width: 360px;
+}
+
+.metric-chip {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ border-radius: 12px;
+ padding: 10px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 95px;
+ box-shadow: 0 12px 22px rgba(99, 102, 241, 0.12);
+}
+
+.metric-chip .chip-label {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: var(--admin-subtle);
+}
+
+.metric-chip .chip-value {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--admin-text);
+}
+
+.header-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ border: none;
+ padding: 10px 18px;
+ border-radius: 12px;
+ background: linear-gradient(135deg, var(--admin-accent) 0%, var(--admin-accent-strong) 100%);
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 14px 28px rgba(99, 102, 241, 0.35);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.header-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 16px 34px rgba(99, 102, 241, 0.42);
+}
+
+.admin-main {
+ flex: 1;
+ padding: 16px 32px 40px;
+ overflow-y: auto;
+}
+
+.tab-panels {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+.tab-panels .tab-content {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ border-radius: 26px;
+ padding: 32px;
+ box-shadow: 0 20px 45px rgba(15, 23, 42, 0.08);
+ display: none;
+}
+
+.tab-panels .tab-content.active {
+ display: block;
+}
+
+/* 让嵌套面板更舒展 */
+.tab-content > .dashboard-header,
+.tab-content > .maintenance-container,
+.tab-content > .files-container,
+.tab-content > .users-container,
+.tab-content > .storage-container,
+.tab-content > form {
+ margin-top: 8px;
+}
+
+/* 响应式 */
+@media (max-width: 1180px) {
+ .admin-header {
+ flex-wrap: wrap;
+ align-items: flex-start;
+ }
+
+ .header-actions {
+ width: 100%;
+ justify-content: flex-start;
+ order: 3;
+ }
+
+ .header-metrics {
+ order: 2;
+ max-width: none;
+ width: 100%;
+ justify-content: flex-start;
+ }
+
+ .headline {
+ order: 1;
+ }
+}
+
+@media (max-width: 1024px) {
+ .admin-shell {
+ flex-direction: column;
+ }
+
+ .admin-sidebar {
+ position: fixed;
+ inset: 0 auto 0 0;
+ transform: translateX(-105%);
+ max-width: 260px;
+ height: 100vh;
+ }
+
+ .admin-sidebar.sidebar-open {
+ transform: translateX(0);
+ }
+
+ .header-toggle {
+ display: flex;
+ order: -1;
+ }
+
+ .admin-main {
+ padding: 16px 20px 32px;
+ }
+}
+
+@media (max-width: 768px) {
+ .admin-header {
+ padding: 24px 20px 4px;
+ gap: 16px;
+ }
+
+ .header-metrics {
+ gap: 10px;
+ }
+
+ .headline h1 {
+ font-size: 22px;
+ }
+
+ .tab-panels .tab-content {
+ padding: 24px;
+ border-radius: 20px;
+ }
+}
+
+@media (max-width: 520px) {
+ .sidebar-action,
+ .tab-btn.nav-link {
+ font-size: 13px;
+ }
+
+ .admin-main {
+ padding: 16px;
+ }
+}
+
+@media (max-width: 640px) {
+ .admin-modern-body .stats-grid {
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 18px;
+ margin: 24px 0 28px;
+ }
+
+ .admin-modern-body .stat-card {
+ padding: 20px;
+ border-radius: 20px;
+ }
+
+ .admin-modern-body .stat-number {
+ font-size: 28px;
+ }
+
+ .admin-modern-body .dashboard-actions,
+ .admin-modern-body .dashboard-status {
+ padding: 24px;
+ border-radius: 20px;
+ }
+
+ .admin-modern-body .action-cards {
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 16px;
+ }
+
+ .admin-modern-body .action-card {
+ padding: 18px 20px;
+ border-radius: 18px;
+ }
+
+ .admin-modern-body .status-item {
+ padding: 16px 18px 16px 54px;
+ border-radius: 18px;
+ }
+}
+
+/* Dashboard + cards refinement */
+.admin-modern-body .dashboard-header {
+ position: relative;
+ z-index: 0;
+ background: var(--admin-panel-bg);
+ border: 1px solid var(--admin-panel-border);
+ border-radius: 22px;
+ box-shadow: var(--admin-panel-shadow);
+ padding: 28px 28px 24px;
+ overflow: hidden;
+}
+
+.admin-modern-body .dashboard-header::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(circle at top right, var(--admin-panel-highlight) 0%, transparent 60%);
+ pointer-events: none;
+ z-index: 0;
+}
+
+.admin-modern-body .dashboard-header h3 {
+ position: relative;
+ z-index: 1;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: var(--admin-heading);
+}
+
+.admin-modern-body .dashboard-header h3 i {
+ color: var(--admin-accent);
+}
+
+.admin-modern-body .dashboard-header .dashboard-subtitle {
+ position: relative;
+ z-index: 1;
+ margin: 12px 0 0;
+ color: var(--admin-muted);
+}
+
+.admin-modern-body .stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 24px;
+ margin: 32px 0 36px;
+}
+
+.admin-modern-body .stat-card {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ gap: 18px;
+ padding: 24px;
+ border-radius: 22px;
+ background: var(--admin-stat-bg);
+ border: 1px solid var(--admin-stat-border);
+ box-shadow: var(--admin-stat-glow);
+ overflow: hidden;
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
+}
+
+.admin-modern-body .stat-card::before {
+ content: '';
+ position: absolute;
+ inset: -45% -35% 0 auto;
+ width: 70%;
+ height: 70%;
+ background: radial-gradient(circle at top right, var(--admin-stat-highlight) 0%, transparent 65%);
+ opacity: 0.65;
+ pointer-events: none;
+ transition: opacity 0.25s ease;
+}
+
+.admin-modern-body .stat-card:hover {
+ transform: translateY(-4px);
+ border-color: var(--admin-accent-soft);
+ box-shadow: var(--admin-stat-hover-glow);
+}
+
+.admin-modern-body .stat-card:hover::before {
+ opacity: 1;
+}
+
+.admin-modern-body .stat-icon {
+ position: relative;
+ z-index: 1;
+ width: 54px;
+ height: 54px;
+ border-radius: 18px;
+ display: grid;
+ place-items: center;
+ background: var(--admin-stat-icon-bg);
+ color: var(--admin-stat-icon-color);
+ font-size: 20px;
+}
+
+.admin-modern-body .stat-content {
+ position: relative;
+ z-index: 1;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.admin-modern-body .stat-number {
+ font-size: 32px;
+ font-weight: 700;
+ color: var(--admin-heading);
+ letter-spacing: -0.01em;
+ font-variant-numeric: tabular-nums;
+}
+
+.admin-modern-body .stat-label {
+ font-size: 14px;
+ text-transform: uppercase;
+ letter-spacing: 0.16em;
+ color: var(--admin-subtle);
+}
+
+.admin-modern-body .stat-trend {
+ margin-top: 8px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ border-radius: 999px;
+ background: var(--admin-stat-trend-bg);
+ color: var(--admin-stat-trend-text);
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.admin-modern-body .stat-trend i {
+ font-size: 12px;
+}
+
+.admin-modern-body .maintenance-card,
+.admin-modern-body .files-container,
+.admin-modern-body .users-container,
+.admin-modern-body .storage-container,
+.admin-modern-body .maintenance-container {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ box-shadow: var(--admin-shadow-card);
+ border-radius: 18px;
+}
+
+.admin-modern-body .dashboard-actions {
+ padding: 28px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ background: var(--admin-action-bg);
+ border: 1px solid var(--admin-action-border);
+ box-shadow: var(--admin-action-glow);
+ border-radius: 22px;
+}
+
+.admin-modern-body .dashboard-actions h4 {
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--admin-heading);
+}
+
+.admin-modern-body .dashboard-actions h4 i {
+ color: var(--admin-accent);
+ font-size: 18px;
+}
+
+.admin-modern-body .action-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 18px;
+}
+
+.admin-modern-body .action-card {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ padding: 20px 22px;
+ border-radius: 20px;
+ background: var(--admin-surface);
+ border: 1px solid transparent;
+ box-shadow: none;
+ color: var(--admin-text);
+ overflow: hidden;
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
+}
+
+.admin-modern-body .action-card::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(140deg, var(--admin-panel-highlight) 0%, transparent 70%);
+ opacity: 0.14;
+ pointer-events: none;
+ transition: opacity 0.25s ease;
+}
+
+.admin-modern-body .action-card::after {
+ content: "\f061";
+ font-family: "Font Awesome 6 Free";
+ font-weight: 900;
+ font-size: 14px;
+ color: var(--admin-accent);
+ margin-left: auto;
+ transition: transform 0.25s ease, color 0.25s ease;
+}
+
+.admin-modern-body .action-card:hover {
+ transform: translateY(-3px);
+ border-color: var(--admin-action-border);
+ box-shadow: var(--admin-action-glow);
+ background: var(--admin-action-bg);
+}
+
+.admin-modern-body .action-card:hover::before {
+ opacity: 0.32;
+}
+
+.admin-modern-body .action-card:hover::after {
+ transform: translateX(4px);
+ color: var(--admin-accent-strong);
+}
+
+.admin-modern-body .action-icon {
+ width: 52px;
+ height: 52px;
+ border-radius: 16px;
+ display: grid;
+ place-items: center;
+ background: var(--admin-action-icon-bg);
+ color: var(--admin-action-icon-color);
+ box-shadow: var(--admin-action-icon-shadow);
+ font-size: 20px;
+}
+
+.admin-modern-body .action-content {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.admin-modern-body .action-content h5 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--admin-heading);
+}
+
+.admin-modern-body .action-content p {
+ margin: 0;
+ font-size: 13px;
+ color: var(--admin-muted);
+ line-height: 1.5;
+}
+
+.admin-modern-body .dashboard-status {
+ padding: 28px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ background: var(--admin-status-bg);
+ border: 1px solid var(--admin-status-border);
+ box-shadow: var(--admin-action-glow);
+ border-radius: 22px;
+}
+
+.admin-modern-body .dashboard-status h4 {
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--admin-heading);
+}
+
+.admin-modern-body .dashboard-status h4 i {
+ color: var(--admin-accent);
+ font-size: 18px;
+}
+
+.admin-modern-body .status-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 18px;
+}
+
+.admin-modern-body .status-item {
+ position: relative;
+ padding: 18px 20px 18px 58px;
+ border-radius: 18px;
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-status-border);
+ overflow: hidden;
+ transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
+}
+
+.admin-modern-body .status-item::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(130deg, var(--admin-panel-highlight) 0%, transparent 70%);
+ opacity: 0.18;
+ pointer-events: none;
+ transition: opacity 0.25s ease;
+}
+
+.admin-modern-body .status-item:hover {
+ transform: translateY(-2px);
+ border-color: var(--admin-status-hover-border);
+ box-shadow: var(--admin-action-glow);
+}
+
+.admin-modern-body .status-item:hover::before {
+ opacity: 0.28;
+}
+
+.admin-modern-body .status-indicator {
+ position: absolute;
+ left: 20px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.08);
+}
+
+.admin-modern-body .status-indicator.online {
+ background: var(--admin-status-dot-online);
+}
+
+.admin-modern-body .status-indicator.warning {
+ background: var(--admin-status-dot-warning);
+}
+
+.admin-modern-body .status-indicator.error {
+ background: var(--admin-status-dot-offline);
+}
+
+.admin-modern-body .status-label {
+ font-size: 13px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--admin-subtle);
+ margin-bottom: 6px;
+}
+
+.admin-modern-body .status-value {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--admin-heading);
+}
+
+/* Forms & tables */
+.admin-modern-body .form-control,
+.admin-modern-body input[type="text"],
+.admin-modern-body input[type="number"],
+.admin-modern-body input[type="password"],
+.admin-modern-body input[type="datetime-local"],
+.admin-modern-body textarea,
+.admin-modern-body select {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ color: var(--admin-text);
+ border-radius: 12px;
+ transition: border 0.2s ease, box-shadow 0.2s ease;
+}
+
+.admin-modern-body .form-control:focus,
+.admin-modern-body input:focus,
+.admin-modern-body textarea:focus,
+.admin-modern-body select:focus {
+ border-color: rgba(99, 102, 241, 0.45);
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.12);
+ outline: none;
+}
+
+.admin-modern-body .form-text {
+ color: var(--admin-form-hint);
+}
+
+.admin-modern-body .config-action-bar {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+ padding: 20px;
+ margin-top: 32px;
+ background: rgba(15, 23, 42, 0.5);
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ border-radius: 18px;
+}
+
+.admin-modern-body .config-action-bar .btn {
+ min-width: 160px;
+}
+
+.admin-modern-body .config-action-bar .btn i {
+ margin-right: 6px;
+}
+
+.admin-modern-body .btn,
+.admin-modern-body button.btn,
+.admin-modern-body .logout-btn,
+.admin-modern-body .user-btn {
+ background: linear-gradient(135deg, var(--admin-primary-btn-start) 0%, var(--admin-primary-btn-end) 100%);
+ color: #fff;
+ border: none;
+ border-radius: 12px;
+ padding: 10px 18px;
+ font-weight: 600;
+ box-shadow: var(--admin-primary-btn-shadow);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 42px;
+ margin: 0;
+}
+
+.admin-modern-body .btn:hover,
+.admin-modern-body button.btn:hover,
+.admin-modern-body .logout-btn:hover,
+.admin-modern-body .user-btn:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 18px 36px rgba(129, 140, 248, 0.45);
+}
+
+.admin-modern-body .btn.btn-danger,
+.admin-modern-body button.btn.btn-danger {
+ background: linear-gradient(135deg, rgba(248, 113, 113, 0.75), rgba(239, 68, 68, 0.65));
+ box-shadow: 0 12px 22px rgba(239, 68, 68, 0.25);
+}
+
+.admin-modern-body .btn.btn-secondary,
+.admin-modern-body button.btn.btn-secondary {
+ background: rgba(148, 163, 184, 0.22);
+ color: #1f2937;
+ box-shadow: 0 14px 22px rgba(148, 163, 184, 0.2);
+}
+
+.admin-modern-body .btn.btn-secondary:hover,
+.admin-modern-body button.btn.btn-secondary:hover {
+ background: rgba(148, 163, 184, 0.3);
+}
+
+.admin-modern-body table {
+ background: #ffffff;
+ color: var(--admin-text);
+ width: 100%;
+ border-spacing: 0;
+ border-collapse: collapse;
+}
+
+.admin-modern-body thead {
+ background: var(--admin-interactive-bg);
+}
+
+.admin-modern-body tbody tr:hover {
+ background: var(--admin-interactive-bg);
+}
+
+/* Modal glass */
+.admin-modern-body .modal .modal-content {
+ background: #ffffff;
+ border: 1px solid var(--admin-border);
+ box-shadow: 0 30px 60px rgba(148, 163, 184, 0.25);
+ border-radius: 18px;
+}
+
+.admin-modern-body .modal .modal-header {
+ border-bottom-color: rgba(99, 102, 241, 0.12);
+}
+
+.admin-modern-body .modal .modal-title {
+ color: var(--admin-text);
+}
+
+.admin-modern-body .modal .modal-form label {
+ color: rgba(15, 23, 42, 0.75);
+}
+
+/* Scrollbar */
+.admin-modern-body ::-webkit-scrollbar {
+ width: 8px;
+}
+
+.admin-modern-body ::-webkit-scrollbar-track {
+ background: #e5e7ff;
+}
+
+.admin-modern-body ::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, rgba(99, 102, 241, 0.6), rgba(129, 140, 248, 0.5));
+ border-radius: 999px;
+}
+
+.admin-modern-body ::selection {
+ background: rgba(129, 140, 248, 0.45);
+}
+
+/* Section refinements */
+.admin-modern-body .section-header {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ margin-bottom: 16px;
+}
+
+.admin-modern-body .section-header h3 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--admin-heading);
+}
+
+.admin-modern-body .section-header p {
+ margin: 0;
+ color: rgba(71, 85, 105, 0.68);
+}
+
+.admin-modern-body .section-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(129, 140, 248, 0.1));
+ color: var(--admin-accent);
+ box-shadow: 0 8px 18px rgba(79, 70, 229, 0.18);
+}
+
+
+.admin-modern-body .config-section,
+.admin-modern-body .maintenance-card,
+.admin-modern-body .dashboard-actions,
+.admin-modern-body .dashboard-status,
+.admin-modern-body .quick-actions,
+.admin-modern-body .storage-container .config-section,
+.admin-modern-body .mcp-container .config-section,
+.admin-modern-body .maintenance-container {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ border-radius: 18px;
+ padding: 24px;
+ box-shadow: var(--admin-shadow-card);
+ margin-bottom: 28px;
+}
+
+.admin-modern-body .maintenance-card,
+.admin-modern-body .config-section {
+ padding: 24px;
+}
+
+.admin-modern-body .maintenance-card p {
+ color: rgba(226, 232, 240, 0.65);
+ margin: 0 0 16px;
+}
+
+.admin-modern-body .maintenance-card .btn {
+ justify-content: flex-start;
+ width: 100%;
+}
+
+.admin-modern-body .quick-action-buttons {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.admin-modern-body .row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.admin-modern-body .row .col-6 {
+ flex: 1 1 calc(50% - 20px);
+ min-width: 260px;
+}
+
+.admin-modern-body .form-group {
+ margin-bottom: 18px;
+}
+
+.admin-modern-body .form-group small,
+.admin-modern-body .form-group .form-text {
+ display: block;
+ margin-top: 6px;
+}
+
+.admin-modern-body form label {
+ color: rgba(226, 232, 240, 0.75);
+ font-weight: 500;
+}
+
+.admin-modern-body .checkbox-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.admin-modern-body .checkbox-label input[type="checkbox"] {
+ accent-color: rgba(129, 140, 248, 0.85);
+ transform: scale(1.1);
+}
+
+.admin-modern-body .form-text i {
+ color: var(--admin-accent);
+ margin-right: 6px;
+}
+
+.admin-modern-body table {
+ border-collapse: collapse;
+}
+
+.admin-modern-body tbody tr td,
+.admin-modern-body thead tr th {
+ border-bottom: 1px solid rgba(148, 163, 184, 0.12);
+ padding: 12px 16px;
+}
+
+.admin-modern-body table thead th {
+ font-weight: 600;
+ color: rgba(226, 232, 240, 0.85);
+}
+
+/* User modal refinement */
+.admin-modern-body #user-modal .modal-content {
+ background: rgba(15, 23, 42, 0.9);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ border-radius: 22px;
+ box-shadow: 0 30px 65px rgba(15, 23, 42, 0.65);
+ overflow: hidden;
+}
+
+.admin-modern-body #user-modal .modal-header {
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.85) 0%, rgba(129, 140, 248, 0.72) 100%);
+ padding: 26px 32px;
+}
+
+.admin-modern-body #user-modal .modal-header h3 {
+ color: var(--admin-heading);
+ font-size: 22px;
+}
+
+.admin-modern-body #user-modal .close {
+ background: rgba(15, 23, 42, 0.15);
+ color: var(--admin-heading);
+}
+
+.admin-modern-body #user-modal .modal-body {
+ background: transparent;
+}
+
+.admin-modern-body #user-modal .form-group label {
+ color: rgba(226, 232, 240, 0.85);
+}
+
+.admin-modern-body #user-modal .form-control {
+ background: var(--admin-overlay);
+ border: 1px solid rgba(148, 163, 184, 0.25);
+ box-shadow: none;
+}
+
+.admin-modern-body #user-modal .form-control:focus {
+ background: rgba(15, 23, 42, 0.65);
+ border-color: rgba(99, 102, 241, 0.5);
+}
+
+.admin-modern-body #user-modal .form-text {
+ color: var(--admin-form-hint);
+}
+.admin-modern-body #current-storage-display,
+.admin-modern-body #storage-actions,
+.admin-modern-body #mcp-status-display,
+.admin-modern-body #mcp-config-options,
+.admin-modern-body .quick-actions,
+.admin-modern-body .dashboard-actions,
+.admin-modern-body .dashboard-status {
+ background: #ffffff !important;
+ border: 1px solid var(--admin-border) !important;
+ border-radius: 18px !important;
+ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
+}
+
+.admin-modern-body #storage-actions {
+ padding: 20px;
+}
+
+.admin-modern-body #storage-actions p {
+ color: var(--admin-icon-default);
+}
+
+.admin-modern-body #mcp-config-options {
+ margin-top: 25px !important;
+ padding: 24px !important;
+ border-left: none !important;
+}
+
+.admin-modern-body #mcp-config-options .form-control {
+ border: 1px solid rgba(148, 163, 184, 0.3) !important;
+ border-radius: 12px !important;
+ padding: 12px !important;
+}
+
+.admin-modern-body #mcp-config-options .form-control:focus {
+ border-color: rgba(99, 102, 241, 0.55) !important;
+}
+.admin-modern-body .placeholder-block {
+ text-align: center;
+ padding: 40px;
+ color: var(--admin-muted);
+ background: var(--admin-placeholder-bg);
+ border-radius: 14px;
+}
+
+.admin-modern-body .muted-text {
+ color: rgba(71, 85, 105, 0.65) !important;
+ margin: 0;
+}
+
+.admin-modern-body .guide-panel {
+ background: var(--admin-surface-subtle);
+ border-radius: 16px;
+ border: 1px solid rgba(99, 102, 241, 0.12);
+ padding: 24px;
+ box-shadow: var(--admin-shadow-soft);
+}
+
+.admin-modern-body .guide-panel h4 {
+ color: var(--admin-text);
+ margin-bottom: 16px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.admin-modern-body .guide-panel h4 i {
+ color: var(--admin-accent);
+}
+
+.admin-modern-body .guide-callout {
+ background: var(--admin-surface);
+ border-radius: 12px;
+ padding: 16px;
+ border-left: 4px solid var(--admin-accent-soft);
+ margin-bottom: 18px;
+}
+
+.admin-modern-body .guide-callout p {
+ margin: 0;
+}
+
+.admin-modern-body .guide-callout p + p {
+ margin-top: 8px;
+ font-size: 13px;
+}
+
+.admin-modern-body .guide-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 14px;
+}
+
+.admin-modern-body .guide-card,
+.admin-modern-body .guide-grid > div {
+ background: var(--admin-surface) !important;
+ border-radius: 14px;
+ padding: 16px;
+ border-left: 4px solid var(--admin-accent-soft) !important;
+ color: var(--admin-muted);
+}
+
+.admin-modern-body .guide-card strong,
+.admin-modern-body .guide-grid > div strong {
+ display: block;
+ margin-bottom: 6px;
+ color: var(--admin-text) !important;
+}
+
+.admin-modern-body .guide-grid p {
+ margin: 4px 0 0;
+ font-size: 13px;
+ color: var(--admin-muted);
+}
+
+/* Tab metrics */
+.admin-modern-body .tab-metrics {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 16px;
+ margin-bottom: 24px;
+}
+
+.admin-modern-body .metric-card {
+ background: rgba(15, 23, 42, 0.58);
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ border-radius: 18px;
+ padding: 18px;
+ box-shadow: 0 18px 45px rgba(15, 23, 42, 0.45);
+}
+
+.admin-modern-body .metric-label {
+ color: rgba(226, 232, 240, 0.72);
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-bottom: 8px;
+}
+
+.admin-modern-body .metric-value {
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--admin-heading);
+}
+
+/* Toolbar shell */
+.admin-modern-body .toolbar-shell,
+.admin-modern-body .user-toolbar,
+.admin-modern-body .files-toolbar {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ border-radius: 16px;
+ padding: 18px 20px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: var(--admin-shadow-card);
+ margin-bottom: 20px;
+}
+
+.admin-modern-body .toolbar-left,
+.admin-modern-body .toolbar-right,
+.admin-modern-body .user-toolbar .search-section,
+.admin-modern-body .user-toolbar .action-section {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.admin-modern-body .search-box,
+.admin-modern-body .files-search-box {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.admin-modern-body .search-input,
+.admin-modern-body .files-search-input {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ color: var(--admin-text);
+ border-radius: 12px;
+ padding: 10px 14px;
+ min-width: 240px;
+}
+
+.admin-modern-body .filter-row,
+.admin-modern-body .user-filters {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-bottom: 18px;
+}
+
+.admin-modern-body .filter-label {
+ font-size: 13px;
+ color: var(--admin-icon-default);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.admin-modern-body .filter-tag {
+ padding: 6px 12px;
+ border-radius: 999px;
+ background: var(--admin-toggle-bg);
+ color: var(--admin-text);
+ cursor: pointer;
+ font-size: 13px;
+ transition: background 0.2s ease, color 0.2s ease;
+}
+
+.admin-modern-body .filter-tag.active {
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.75), rgba(129, 140, 248, 0.65));
+ box-shadow: 0 12px 24px rgba(99, 102, 241, 0.35);
+}
+
+.admin-modern-body .bulk-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ border-radius: 12px;
+ background: var(--admin-surface-muted);
+ border: 1px solid var(--admin-border);
+ margin-bottom: 18px;
+}
+
+.admin-modern-body .bulk-info {
+ color: rgba(71, 85, 105, 0.75);
+}
+
+.admin-modern-body .bulk-buttons {
+ display: flex;
+ gap: 8px;
+}
+
+.admin-modern-body .view-toggle {
+ display: inline-flex;
+ background: var(--admin-interactive-bg);
+ border-radius: 999px;
+ border: 1px solid rgba(99, 102, 241, 0.15);
+ overflow: hidden;
+}
+
+.admin-modern-body .view-toggle-btn {
+ background: transparent;
+ border: none;
+ color: rgba(71, 85, 105, 0.8);
+ padding: 8px 16px;
+ font-size: 13px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.admin-modern-body .view-toggle-btn.active {
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.16), rgba(129, 140, 248, 0.12));
+ color: var(--admin-text);
+}
+
+.admin-modern-body .table-shell,
+.admin-modern-body .users-table-container,
+.admin-modern-body .files-table-container {
+ background: var(--admin-surface);
+ border: 1px solid var(--admin-border);
+ border-radius: 16px;
+ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
+ overflow: hidden;
+}
+
+.admin-modern-body .users-table,
+.admin-modern-body .files-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.admin-modern-body .users-table thead,
+.admin-modern-body .files-table thead {
+ background: var(--admin-interactive-bg);
+}
+
+.admin-modern-body .users-table th,
+.admin-modern-body .files-table th,
+.admin-modern-body .users-table td,
+.admin-modern-body .files-table td {
+ padding: 12px 16px;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.12);
+}
+
+.admin-modern-body .pagination-container {
+ background: rgba(15, 23, 42, 0.45);
+ border: 1px solid rgba(148, 163, 184, 0.14);
+ border-radius: 14px;
+ padding: 12px 18px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.admin-modern-body .pagination-info {
+ color: var(--admin-icon-default);
+}
+.admin-modern-body .user-toolbar .dropdown {
+ position: relative;
+}
+
+.admin-modern-body .user-toolbar .dropdown-menu {
+ background: rgba(15, 23, 42, 0.9);
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ border-radius: 12px;
+ padding: 10px 0;
+ box-shadow: 0 20px 35px rgba(15, 23, 42, 0.45);
+}
+
+.admin-modern-body .user-toolbar .dropdown-menu a {
+ color: rgba(226, 232, 240, 0.8);
+}
+
+.admin-modern-body .form-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 18px;
+}
+
+.admin-modern-body .form-grid.two-cols {
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+}
+
+.admin-modern-body .form-actions {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.admin-modern-body .form-actions .btn + .btn {
+ margin-left: 12px;
+}
+
+.admin-modern-body .form-grid .span-2 {
+ grid-column: span 2;
+}
+
+@media (max-width: 768px) {
+ .admin-modern-body .form-grid .span-2 {
+ grid-column: span 1;
+ }
+}
+
+.admin-modern-body .storage-actions {
+ margin-top: 20px;
+ text-align: center;
+}
+
+.admin-modern-body .storage-actions p {
+ text-align: center;
+}
+
+.admin-modern-body .placeholder-block i {
+ font-size: 24px;
+ margin-bottom: 10px;
+ color: var(--admin-accent);
+}
diff --git a/themes/2025/admin/index.html b/themes/2025/admin/index.html
index 86cc098..603bf36 100644
--- a/themes/2025/admin/index.html
+++ b/themes/2025/admin/index.html
@@ -19,74 +19,107 @@
+
-
-
-
-
-
+
-
-