全局 Caddyfile 配置
+修改这些配置将会重写您的主 Caddyfile 并触发 + Caddy 重载。
+ + +diff --git a/Caddyfile b/Caddyfile index 2dbe81a..373891c 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,95 +1,95 @@ { - debug - admin :2019 - http_port 80 - https_port 443 - metrics - order ja4h_header first - order webdav before file_server - order cache before rewrite - cache { - cache_name CaddyCache - } - log { - level INFO - output file ./log/caddy.log { - roll_size 10MB - roll_keep 10 - } - } + admin localhost:2019 + http_port 80 + https_port 443 + metrics + + order ja4h_header first + order webdav before file_server + order cache before rewrite + + cache { + cache_name CaddyCache + } + + log { + level INFO + output file ./log/caddy.log { + roll_size 10MB + roll_keep 10 + roll_keep_for 24h + } + } } (log) { - log { - format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` { - time_format "02/Jan/2006:15:04:05 -0700" - } - output file ./log/{args[0]}/access.log { - roll_size 10MB - roll_keep 10 - roll_keep_for 24h - } - } + log { + format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` { + time_format "02/Jan/2006:15:04:05 -0700" + } + output file ./log/{args[0]}/access.log { + roll_size 10MB + roll_keep 10 + roll_keep_for 24h + } + } } (error_page) { - handle_errors { - rewrite * /{err.status_code}.html - root * ./pages/errors - file_server - } + handle_errors { + rewrite * /{err.status_code}.html + root * ./pages/errors + file_server + } } (encode) { - encode { - zstd + encode { + zstd br gzip minimum_length 512 - } + } } (cache) { - cache { - allowed_http_verbs GET - stale {args[0]} - ttl {args[1]} - } + cache { + allowed_http_verbs GET + stale {args[0]} + ttl {args[1]} + } } (header_realip_cf) { - header_up X-Real-IP {remote_host} + header_up X-Real-IP {remote_host} header_up X-Real-IP {http.request.header.CF-Connecting-IP} header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} header_up X-Forwarded-Proto {http.request.header.CF-Visitor} } (header_realip) { - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} } (tls) { - tls { - dns {args[0]} {args[1]} - } } (rate_limit) { - route /* { - rate_limit {remote.ip} {args[0]}r/m 10000 429 - } + route /* { + rate_limit {remote.ip} {args[0]}r/m 10000 429 + } } (route_nocache) { - route {args[0]} { - rate_limit {remote.ip} {args[1]}r/m 10000 429 - cache { - stale 0s - ttl 0s - } - } + route {args[0]} { + rate_limit {remote.ip} {args[1]}r/m 10000 429 + cache { + stale 0s + ttl 0s + } + } } -import ./config.d/* \ No newline at end of file +import ./config.d/* diff --git a/api/api.go b/api/api.go index 70501ee..2ac1320 100644 --- a/api/api.go +++ b/api/api.go @@ -46,6 +46,8 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) { glbr.GET("/tls/providers", func(c *touka.Context) { c.JSON(200, gen.ProviderList) }) + glbr.PUT("/config", PutGlobalConfig(cdb, cfg)) + glbr.GET("/config", GetGlobalConfig(cdb)) } } diff --git a/api/config.go b/api/config.go index c58b5b1..447ab3a 100644 --- a/api/config.go +++ b/api/config.go @@ -35,7 +35,7 @@ func PutConfig(cdb *db.ConfigDB, cfg *config.Config) touka.HandlerFunc { var config gen.CaddyUniConfig err := c.ShouldBindJSON(&config) if err != nil { - c.JSON(500, touka.H{"error": err.Error()}) + c.JSON(400, touka.H{"error": err.Error()}) return } diff --git a/api/global.go b/api/global.go index 778f64e..356a4ba 100644 --- a/api/global.go +++ b/api/global.go @@ -1 +1,87 @@ package api + +import ( + "caddydash/config" + "caddydash/db" + "caddydash/gen" + "os" + + "github.com/infinite-iroha/touka" +) + +func PutGlobalConfig(cdb *db.ConfigDB, cfg *config.Config) touka.HandlerFunc { + return func(c *touka.Context) { + var config gen.CaddyGlobalConfig + err := c.ShouldBindJSON(&config) + if err != nil { + c.JSON(400, touka.H{"error": err.Error()}) + return + } + + var paramsGob []byte + + paramsGob, err = gen.EncodeGobConfig(config) + if err != nil { + c.Warnf("encode gob config error: %v", err) + c.JSON(500, touka.H{"error": err.Error()}) + return + } + + // 取出数据库内的tmpl + tmplContent, err := cdb.GetGlobalTemplate("caddyfile") + if err != nil { + c.Warnf("get global template error: %v", err) + c.JSON(500, touka.H{"error": err.Error()}) + return + } + + renderedContent, err := gen.RenderGlobalConfig(paramsGob, tmplContent) + if err != nil { + c.Warnf("render global config error: %v", err) + c.JSON(500, touka.H{"error": err.Error()}) + return + } + + //回写条目到数据库 + err = cdb.SaveGlobalConfig(db.GlobalConfig{ + Filename: "caddyfile", + Params: paramsGob, + TmplContent: tmplContent, + RenderedContent: renderedContent, + }) + if err != nil { + c.Warnf("save global config error: %v", err) + c.JSON(500, touka.H{"error": err.Error()}) + return + } + + err = os.WriteFile(cfg.Server.CaddyDir+"Caddyfile", renderedContent, 0644) + if err != nil { + c.Warnf("write Caddyfile error: %v", err) + c.JSON(500, touka.H{"error": err.Error()}) + return + } + + c.JSON(200, touka.H{"message": "global config saved"}) + return + } +} + +// GetGlobalConfig 检出已有配置 +func GetGlobalConfig(cdb *db.ConfigDB) touka.HandlerFunc { + return func(c *touka.Context) { + globalConfig, err := cdb.GetGlobalConfig("caddyfile") + if err != nil { + c.JSON(500, touka.H{"error": err.Error()}) + return + } + + var config gen.CaddyGlobalConfig + err = gen.DecodeGobConfig(globalConfig.Params, &config) + if err != nil { + c.JSON(500, touka.H{"error": err.Error()}) + return + } + c.JSON(200, config) + } +} diff --git a/apic/run.go b/apic/run.go index 4f02026..4c3e4fc 100644 --- a/apic/run.go +++ b/apic/run.go @@ -105,7 +105,7 @@ func StopCaddy() touka.HandlerFunc { return } client := c.GetHTTPC() - rb := client.NewRequestBuilder("POST", "http://127.0.0.1:2019/stop") + rb := client.NewRequestBuilder("POST", "http://localhost:2019/stop") resp, err := rb.Execute() if err != nil { c.JSON(500, map[string]string{"error": err.Error()}) @@ -139,7 +139,7 @@ func RestartCaddy(cfg *config.Config) touka.HandlerFunc { // StopCaddy client := c.GetHTTPC() - rb := client.NewRequestBuilder("POST", "http://127.0.0.1:2019/stop") + rb := client.NewRequestBuilder("POST", "http://localhost:2019/stop") resp, err := rb.Execute() if err != nil { c.JSON(500, map[string]string{"error": err.Error()}) diff --git a/config/config.go b/config/config.go index 844e294..2a9ac66 100644 --- a/config/config.go +++ b/config/config.go @@ -8,7 +8,6 @@ import ( type Config struct { Server ServerConfig - Tmpl TmplConfig DB DatabaseConfig `toml:"database"` } @@ -26,14 +25,6 @@ type ServerConfig struct { CaddyDir string `toml:"caddyDir"` } -/* -[tmpl] -path = "./tmpl" -*/ -type TmplConfig struct { - Path string `toml:"path"` -} - /* [database] filepath = "sqlite.db" @@ -100,9 +91,6 @@ func DefaultConfig() *Config { Debug: false, CaddyDir: "./", }, - Tmpl: TmplConfig{ - Path: "./tmpl", - }, DB: DatabaseConfig{ Filepath: "caddydash.db", }, diff --git a/config/config.toml b/config/config.toml index 86f84b9..2523b0b 100644 --- a/config/config.toml +++ b/config/config.toml @@ -4,8 +4,5 @@ port = 8080 debug = false caddyDir = "./" -[tmpl] -path = "/data/github/WJQSERVER/caddydash/tmpl" - [database] filepath = "test.db" \ No newline at end of file diff --git a/db/db.go b/db/db.go index 6015c89..86d49b0 100644 --- a/db/db.go +++ b/db/db.go @@ -44,6 +44,15 @@ type UsersTable struct { UpdatedAt int64 `json:"updated_at"` } +// GlobalConfig 全局CaddyFile配置专用table +type GlobalConfig struct { + Filename string `json:"filename"` + Params []byte `json:"params"` + TmplContent []byte `json:"tmpl_content"` + RenderedContent []byte `json:"rendered_content"` + UpdatedAt int64 `json:"updated_at"` +} + // ConfigDB type ConfigDB struct { DB *sql.DB @@ -124,7 +133,6 @@ func (cdb *ConfigDB) createTables() error { return fmt.Errorf("failed to create 'rendered_configs' table: %w", err) } - // // 4. 创建 users 表 _, err = tx.Exec(` CREATE TABLE IF NOT EXISTS users ( @@ -137,6 +145,19 @@ func (cdb *ConfigDB) createTables() error { return fmt.Errorf("failed to create 'users' table: %w", err) } + // 5. 创建 global_configs 表 + _, err = tx.Exec(` + CREATE TABLE IF NOT EXISTS global_configs ( + filename TEXT PRIMARY KEY, + params BLOB NOT NULL DEFAULT '', + tmpl_content BLOB NOT NULL DEFAULT '', + rendered_content BLOB NOT NULL DEFAULT '', + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + );`) + if err != nil { + return fmt.Errorf("failed to create 'global_configs' table: %w", err) + } + // 提交事务 if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction for table creation: %w", err) diff --git a/db/operation.go b/db/operation.go index a148b2f..9bb107d 100644 --- a/db/operation.go +++ b/db/operation.go @@ -1,13 +1,9 @@ package db import ( - "bytes" "database/sql" - "encoding/gob" "errors" "fmt" - "strings" - "text/template" ) // 用户校验操作 @@ -413,92 +409,119 @@ func (cdb *ConfigDB) RangeAllReandered() ([]RenderedConfigEntry, error) { return renderedConfigs, nil } -// --- GOB 编码/解码辅助函数 --- +// --- 全局配置操作 (Global_Configs Table) --- +/* + _, err = tx.Exec(` + CREATE TABLE IF NOT EXISTS global_configs ( + filename TEXT PRIMARY KEY, + params BLOB NOT NULL, + tmpl_content BLOB NOT NULL, + rendered_content BLOB NOT NULL, + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + );`) +*/ -// gobEncode 将 Go 值编码为 GOB 格式的字节切片. -// 'data' 必须是可被 GOB 编码的值; 例如基本类型, 切片, 映射, 结构体. -func gobEncode(data interface{}) ([]byte, error) { - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - if err := enc.Encode(data); err != nil { - return nil, fmt.Errorf("db: failed to GOB encode data: %w", err) - } - return buf.Bytes(), nil -} - -// gobDecode 将 GOB 格式的字节切片解码到 Go 值. -// 'data' 是 GOB 编码的字节切片. -// 'valuePtr' 必须是指向目标 Go 值的指针; 其类型必须与编码时的数据类型兼容. -func gobDecode(data []byte, valuePtr interface{}) error { - buf := bytes.NewBuffer(data) - dec := gob.NewDecoder(buf) - if err := dec.Decode(valuePtr); err != nil { - return fmt.Errorf("db: failed to GOB decode data: %w", err) +// SaveGlobalConfig 在 'global_configs' 表中保存或更新全局配置. +func (cdb *ConfigDB) SaveGlobalConfig(entry GlobalConfig) error { + insertSQL := ` + INSERT INTO global_configs (filename, params, tmpl_content, rendered_content) + VALUES (?, ?, ?, ?) + ON CONFLICT(filename) DO UPDATE SET + params = EXCLUDED.params, + tmpl_content = EXCLUDED.tmpl_content, + rendered_content = EXCLUDED.rendered_content, + updated_at = strftime('%s', 'now'); + ` + _, err := cdb.DB.Exec(insertSQL, entry.Filename, entry.Params, entry.TmplContent, entry.RenderedContent) + if err != nil { + return fmt.Errorf("db: failed to save global config for '%s': %w", entry.Filename, err) } return nil } -// --- 业务逻辑: 渲染并保存 (由使用者调用) --- -// RenderAndSaveConfig 从数据库获取模板和参数; 渲染后保存到 'rendered_configs' 表. -// 这是一个组合操作; 通常由应用程序逻辑在需要时调用. -// filename: 要渲染的配置文件的唯一标识. -// templateParser: 一个实现了 TemplateParser 接口的模板解析器实例. -// dynamicParams: 运行时动态提供的参数; 它们会覆盖存储在数据库中的同名参数. -func (cdb *ConfigDB) RenderAndSaveConfig(filename string, dynamicParams map[string]interface{}) error { - // 1. 获取模板内容. - tmplEntry, err := cdb.GetTemplate(filename) - if err != nil { - return fmt.Errorf("db: failed to get template '%s' for rendering: %w", filename, err) - } +// GetGlobalConfig 从 'global_configs' 表中获取全局配置. +func (cdb *ConfigDB) GetGlobalConfig(filename string) (*GlobalConfig, error) { + querySQL := `SELECT filename, params, tmpl_content, rendered_content, updated_at FROM global_configs WHERE filename = ?;` + row := cdb.DB.QueryRow(querySQL, filename) - // 2. 获取存储的参数. - paramsEntry, err := cdb.GetParams(filename) - var storedParams map[string]interface{} + entry := &GlobalConfig{} + err := row.Scan(&entry.Filename, &entry.Params, &entry.TmplContent, &entry.RenderedContent, &entry.UpdatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { - storedParams = make(map[string]interface{}) // 参数不存在; 使用空 map. - } else { - return fmt.Errorf("db: failed to get parameters for '%s': %w", filename, err) - } - } else { - // 解码 GOB 参数到 map. - if err := gobDecode(paramsEntry.ParamsGOB, &storedParams); err != nil { - return fmt.Errorf("db: failed to decode stored parameters for '%s': %w", filename, err) + return nil, fmt.Errorf("db: global config '%s' not found: %w", filename, err) } + return nil, fmt.Errorf("db: failed to get global config '%s': %w", filename, err) } + return entry, nil +} - // 合并参数: 动态传入的参数覆盖存储的参数. - if dynamicParams != nil { - for k, v := range dynamicParams { - storedParams[k] = v - } - } - - // 3. 渲染模板. - var parsedTmpl *template.Template - var parseErr error - - // 使用传入的 templateParser 实例来解析模板内容. - // 注意: templateParser.Parse 是在提供的实例上调用; 以解析特定内容. - //parsedTmpl, parseErr = templateParser.Parse(string(tmplEntry.Content)) - parsedTmpl, parseErr = template.New(tmplEntry.Filename).Parse(string(tmplEntry.Content)) - - if parseErr != nil { - return fmt.Errorf("db: failed to parse template content for '%s': %w", tmplEntry.Filename, parseErr) - } - - var renderedContentBuilder strings.Builder - if err := parsedTmpl.Execute(&renderedContentBuilder, storedParams); err != nil { - return fmt.Errorf("db: failed to render template '%s': %w", tmplEntry.Filename, err) - } - - // 4. 保存渲染结果. - renderedEntry := RenderedConfigEntry{ - Filename: filename, - RenderedContent: []byte(renderedContentBuilder.String()), - } - if err := cdb.SaveRenderedConfig(renderedEntry); err != nil { - return fmt.Errorf("db: failed to save rendered config for '%s': %w", filename, err) +// DeleteGlobalConfig 从 'global_configs' 表中删除全局配置. +func (cdb *ConfigDB) DeleteGlobalConfig(filename string) error { + _, err := cdb.DB.Exec(`DELETE FROM global_configs WHERE filename = ?;`, filename) + if err != nil { + return fmt.Errorf("db: failed to delete global config for '%s': %w", filename, err) } return nil } + +// SaveGlobalParams +func (cdb *ConfigDB) SaveGlobalParams(filename string, params []byte) error { + insertSQL := ` + INSERT INTO global_configs (filename, params) + VALUES (?, ?) + ON CONFLICT(filename) DO UPDATE SET + params = EXCLUDED.params, + updated_at = strftime('%s', 'now'); + ` + _, err := cdb.DB.Exec(insertSQL, filename, params) + if err != nil { + return fmt.Errorf("db: failed to save global params for '%s': %w", filename, err) + } + return nil +} + +// SaveGlobalRenderedContent +func (cdb *ConfigDB) SaveGlobalRenderedContent(filename string, renderedContent []byte) error { + insertSQL := ` + INSERT INTO global_configs (filename, rendered_content) + VALUES (?, ?) + ON CONFLICT(filename) DO UPDATE SET + rendered_content = EXCLUDED.rendered_content, + updated_at = strftime('%s', 'now'); + ` + _, err := cdb.DB.Exec(insertSQL, filename, renderedContent) + if err != nil { + return fmt.Errorf("db: failed to save global rendered content for '%s': %w", filename, err) + } + return nil +} + +// SaveGlobalTemplate +func (cdb *ConfigDB) SaveGlobalTemplate(filename string, tmplContent []byte) error { + insertSQL := ` + INSERT INTO global_configs (filename, tmpl_content) + VALUES (?, ?) + ON CONFLICT(filename) DO UPDATE SET + tmpl_content = EXCLUDED.tmpl_content, + updated_at = strftime('%s', 'now'); + ` + _, err := cdb.DB.Exec(insertSQL, filename, tmplContent) + if err != nil { + return fmt.Errorf("db: failed to save global template for '%s': %w", filename, err) + } + return nil +} + +// GetGlobalTemplate +func (cdb *ConfigDB) GetGlobalTemplate(filename string) ([]byte, error) { + querySQL := `SELECT tmpl_content FROM global_configs WHERE filename = ?;` + var tmplContent []byte + err := cdb.DB.QueryRow(querySQL, filename).Scan(&tmplContent) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("db: global template '%s' not found: %w", filename, err) + } + return nil, fmt.Errorf("db: failed to get global template '%s': %w", filename, err) + } + return tmplContent, nil +} diff --git a/db/utils.go b/db/utils.go new file mode 100644 index 0000000..c9006d7 --- /dev/null +++ b/db/utils.go @@ -0,0 +1,101 @@ +package db + +import ( + "bytes" + "database/sql" + "encoding/gob" + "errors" + "fmt" + "strings" + "text/template" +) + +// --- GOB 编码/解码辅助函数 --- + +// gobEncode 将 Go 值编码为 GOB 格式的字节切片. +// 'data' 必须是可被 GOB 编码的值; 例如基本类型, 切片, 映射, 结构体. +func gobEncode(data interface{}) ([]byte, error) { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + if err := enc.Encode(data); err != nil { + return nil, fmt.Errorf("db: failed to GOB encode data: %w", err) + } + return buf.Bytes(), nil +} + +// gobDecode 将 GOB 格式的字节切片解码到 Go 值. +// 'data' 是 GOB 编码的字节切片. +// 'valuePtr' 必须是指向目标 Go 值的指针; 其类型必须与编码时的数据类型兼容. +func gobDecode(data []byte, valuePtr interface{}) error { + buf := bytes.NewBuffer(data) + dec := gob.NewDecoder(buf) + if err := dec.Decode(valuePtr); err != nil { + return fmt.Errorf("db: failed to GOB decode data: %w", err) + } + return nil +} + +// --- 业务逻辑: 渲染并保存 (由使用者调用) --- +// RenderAndSaveConfig 从数据库获取模板和参数; 渲染后保存到 'rendered_configs' 表. +// 这是一个组合操作; 通常由应用程序逻辑在需要时调用. +// filename: 要渲染的配置文件的唯一标识. +// templateParser: 一个实现了 TemplateParser 接口的模板解析器实例. +// dynamicParams: 运行时动态提供的参数; 它们会覆盖存储在数据库中的同名参数. +func (cdb *ConfigDB) RenderAndSaveConfig(filename string, dynamicParams map[string]interface{}) error { + // 1. 获取模板内容. + tmplEntry, err := cdb.GetTemplate(filename) + if err != nil { + return fmt.Errorf("db: failed to get template '%s' for rendering: %w", filename, err) + } + + // 2. 获取存储的参数. + paramsEntry, err := cdb.GetParams(filename) + var storedParams map[string]interface{} + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + storedParams = make(map[string]interface{}) // 参数不存在; 使用空 map. + } else { + return fmt.Errorf("db: failed to get parameters for '%s': %w", filename, err) + } + } else { + // 解码 GOB 参数到 map. + if err := gobDecode(paramsEntry.ParamsGOB, &storedParams); err != nil { + return fmt.Errorf("db: failed to decode stored parameters for '%s': %w", filename, err) + } + } + + // 合并参数: 动态传入的参数覆盖存储的参数. + if dynamicParams != nil { + for k, v := range dynamicParams { + storedParams[k] = v + } + } + + // 3. 渲染模板. + var parsedTmpl *template.Template + var parseErr error + + // 使用传入的 templateParser 实例来解析模板内容. + // 注意: templateParser.Parse 是在提供的实例上调用; 以解析特定内容. + //parsedTmpl, parseErr = templateParser.Parse(string(tmplEntry.Content)) + parsedTmpl, parseErr = template.New(tmplEntry.Filename).Parse(string(tmplEntry.Content)) + + if parseErr != nil { + return fmt.Errorf("db: failed to parse template content for '%s': %w", tmplEntry.Filename, parseErr) + } + + var renderedContentBuilder strings.Builder + if err := parsedTmpl.Execute(&renderedContentBuilder, storedParams); err != nil { + return fmt.Errorf("db: failed to render template '%s': %w", tmplEntry.Filename, err) + } + + // 4. 保存渲染结果. + renderedEntry := RenderedConfigEntry{ + Filename: filename, + RenderedContent: []byte(renderedContentBuilder.String()), + } + if err := cdb.SaveRenderedConfig(renderedEntry); err != nil { + return fmt.Errorf("db: failed to save rendered config for '%s': %w", filename, err) + } + return nil +} diff --git a/frontend/css/style.css b/frontend/css/style.css index 34b3b5c..c330e81 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -13,6 +13,9 @@ --checking-color: #f59e0b; --text-color: #1f2937; --text-color-secondary: #6b7280; + --scrollbar-track-color: transparent; + --scrollbar-thumb-color: #d1d5db; + --scrollbar-thumb-hover-color: #9ca3af; --border-color: #e5e7eb; --border-radius-large: 12px; --border-radius-small: 8px; @@ -20,14 +23,23 @@ --sidebar-width: 260px; --transition-speed: 0.3s; } + [data-theme="dark"] { --bg-color: #111827; --surface-color: #1f2937; --text-color: #f9fafb; --text-color-secondary: #9ca3af; --border-color: #374151; + --scrollbar-thumb-color: #4b5563; + --scrollbar-thumb-hover-color: #6b7280; } -* { margin: 0; padding: 0; box-sizing: border-box; } + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + body { font-family: var(--font-family); background-color: var(--bg-color); @@ -36,15 +48,46 @@ body { overflow: hidden; transition: background-color var(--transition-speed), color var(--transition-speed); } -.hidden { display: none !important; } -/* --- 登录页 --- */ +.hidden { + display: none !important; +} + +/* --- 新增: 自定义滚动条样式 --- */ +/* 适用于 Webkit 内核浏览器 (Chrome, Safari, Edge) */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--scrollbar-track-color); +} + +::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb-color); + border-radius: 10px; + border: 2px solid transparent; + background-clip: content-box; +} + +::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-hover-color); +} + +/* 适用于 Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); +} + .login-page-body { display: flex; align-items: center; justify-content: center; height: 100vh; } + .login-container { width: 100%; max-width: 400px; @@ -52,25 +95,37 @@ body { background-color: var(--surface-color); border-radius: var(--border-radius-large); border: 1px solid var(--border-color); - box-shadow: 0 10px 25px rgba(0,0,0,0.05); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05); text-align: center; } -.login-header { margin-bottom: 32px; } -.login-header .fa-rocket, .login-header .fa-magic-wand-sparkles { + +.login-header { + margin-bottom: 32px; +} + +.login-header .fa-rocket, +.login-header .fa-magic-wand-sparkles { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 16px; } + .login-header h1 { font-size: 1.75rem; font-weight: 700; margin-bottom: 8px; } -.login-header p { color: var(--text-color-secondary); } -#login-form .form-group, #init-form .form-group { + +.login-header p { + color: var(--text-color-secondary); +} + +#login-form .form-group, +#init-form .form-group { text-align: left; margin-bottom: 20px; } + .btn-login { margin-top: 16px; width: 100%; @@ -79,7 +134,6 @@ body { padding-right: 24px; } -/* --- 通知系统 --- */ .toast-container { position: fixed; top: 20px; @@ -89,6 +143,7 @@ body { flex-direction: column; gap: 12px; } + .toast { display: flex; align-items: center; @@ -97,69 +152,138 @@ body { background-color: var(--surface-color); border: 1px solid var(--border-color); border-radius: var(--border-radius-small); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); width: 320px; opacity: 0; transform: translateX(100%); transition: opacity 0.3s ease, transform 0.3s ease; } -.toast.show { opacity: 1; transform: translateX(0); } -.toast-icon { font-size: 1.25rem; } -.toast-message { flex-grow: 1; font-size: 0.9rem; font-weight: 500; } -.toast-close { - background: none; border: none; color: var(--text-color-secondary); - cursor: pointer; font-size: 1.1rem; padding: 4px; + +.toast.show { + opacity: 1; + transform: translateX(0); +} + +.toast-icon { + font-size: 1.25rem; +} + +.toast-message { + flex-grow: 1; + font-size: 0.9rem; + font-weight: 500; +} + +.toast-close { + background: none; + border: none; + color: var(--text-color-secondary); + cursor: pointer; + font-size: 1.1rem; + padding: 4px; +} + +.toast.success .toast-icon { + color: var(--success-color); +} + +.toast.error .toast-icon { + color: var(--danger-color); +} + +.toast.info .toast-icon { + color: var(--primary-color); } -.toast.success .toast-icon { color: var(--success-color); } -.toast.error .toast-icon { color: var(--danger-color); } -.toast.info .toast-icon { color: var(--primary-color); } #dialog-container { - position: fixed; top: 0; left: 0; width: 100%; height: 100%; - z-index: 1000; display: flex; align-items: center; justify-content: center; - background-color: rgba(0,0,0,0.3); - opacity: 0; visibility: hidden; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.3); + opacity: 0; + visibility: hidden; transition: opacity 0.2s ease, visibility 0.2s; } + #dialog-container.active { - opacity: 1; visibility: visible; + opacity: 1; + visibility: visible; } + .dialog-box { - background-color: var(--surface-color); border-radius: var(--border-radius-large); - padding: 24px; width: 90%; max-width: 400px; - box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center; + background-color: var(--surface-color); + border-radius: var(--border-radius-large); + padding: 24px; + width: 90%; + max-width: 400px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + text-align: center; transform: scale(0.95); transition: transform 0.2s ease; } + #dialog-container.active .dialog-box { transform: scale(1); } -.dialog-message { margin: 16px 0 24px; font-size: 1rem; color: var(--text-color-secondary); } -.dialog-actions { display: flex; justify-content: center; gap: 12px; } -.dialog-actions .btn { width: auto; } + +.dialog-message { + margin: 16px 0 24px; + font-size: 1rem; + color: var(--text-color-secondary); +} + +.dialog-actions { + display: flex; + justify-content: center; + gap: 12px; +} + +.dialog-actions .btn { + width: auto; +} #modal-container { - position: fixed; top: 0; left: 0; width: 100%; height: 100%; - z-index: 1500; display: flex; align-items: center; justify-content: center; - background-color: rgba(0,0,0,0.4); - opacity: 0; visibility: hidden; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1500; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.4); + opacity: 0; + visibility: hidden; transition: opacity 0.2s ease, visibility 0.2s; } + #modal-container.active { - opacity: 1; visibility: visible; + opacity: 1; + visibility: visible; } + #modal-container .modal-box { background-color: var(--surface-color); border-radius: var(--border-radius-large); - width: 90%; max-width: 500px; + width: 90%; + max-width: 500px; text-align: left; - box-shadow: 0 10px 25px rgba(0,0,0,0.1); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); transform: scale(0.95) translateY(-10px); transition: transform 0.25s ease; } + #modal-container.active .modal-box { transform: scale(1) translateY(0); } + .modal-header { display: flex; justify-content: space-between; @@ -167,41 +291,72 @@ body { padding: 16px 24px; border-bottom: 1px solid var(--border-color); } + .modal-header h3 { margin: 0; font-size: 1.2rem; } + .modal-content { padding: 24px; max-height: 60vh; overflow-y: auto; } + ul.preset-list { list-style: none; padding: 0; margin: 0; } + ul.preset-list li { padding: 12px 16px; border-radius: var(--border-radius-small); cursor: pointer; transition: background-color 0.2s ease; } + ul.preset-list li:hover { background-color: var(--bg-color); } + ul.preset-list li p { font-size: 0.85rem; color: var(--text-color-secondary); margin-top: 4px; } -/* --- 主应用布局 --- */ -.app-container { display: flex; height: 100vh; } -.main-content { flex-grow: 1; padding: 24px 32px; overflow-y: auto; } -#view-container { position: relative; } -.view { animation: fadeIn 0.5s ease; } -@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } +.app-container { + display: flex; + height: 100vh; +} + +.main-content { + flex-grow: 1; + padding: 24px 32px; + overflow-y: auto; +} + +#view-container { + position: relative; +} + +.view { + animation: fadeIn 0.5s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + .sidebar { width: var(--sidebar-width); background-color: var(--surface-color); @@ -212,21 +367,58 @@ ul.preset-list li p { flex-shrink: 0; transition: transform var(--transition-speed) ease, background-color var(--transition-speed), border-color var(--transition-speed); } + .sidebar-header { - display: flex; align-items: center; gap: 12px; - margin-bottom: 32px; font-size: 1.5rem; color: var(--primary-color); + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 32px; + font-size: 1.5rem; + color: var(--primary-color); } -.sidebar-header h1 { font-size: 1.25rem; font-weight: 600; color: var(--text-color); } -.sidebar-nav { flex-grow: 1; } + +.sidebar-header h1 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); +} + +.sidebar-nav { + flex-grow: 1; +} + .sidebar-nav ul li a { - display: flex; align-items: center; gap: 16px; padding: 12px; - border-radius: var(--border-radius-small); color: var(--text-color-secondary); - font-weight: 500; transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 16px; + padding: 12px; + border-radius: var(--border-radius-small); + color: var(--text-color-secondary); + font-weight: 500; + transition: all 0.2s ease; } -.sidebar-nav ul li a:hover { background-color: var(--bg-color); color: var(--text-color); } -.sidebar-nav ul li a.active { background-color: var(--primary-color); color: white; } -.sidebar-nav ul li a.active i { color: white; } -.sidebar-bottom { margin-top: auto; padding-top: 16px; border-top: 1px solid var(--border-color); transition: border-color var(--transition-speed); } + +.sidebar-nav ul li a:hover { + background-color: var(--bg-color); + color: var(--text-color); +} + +.sidebar-nav ul li a.active { + background-color: var(--primary-color); + color: white; +} + +.sidebar-nav ul li a.active i { + color: white; +} + +.sidebar-bottom { + margin-top: auto; + padding-top: 16px; + border-top: 1px solid var(--border-color); + transition: border-color var(--transition-speed); +} + .theme-switcher { display: flex; justify-content: space-around; @@ -237,21 +429,56 @@ ul.preset-list li p { margin-bottom: 16px; transition: background-color var(--transition-speed); } -.theme-switcher i { color: var(--text-color-secondary); } -.switch { position: relative; display: inline-block; width: 44px; height: 24px; } -.switch input { opacity: 0; width: 0; height: 0; } + +.theme-switcher i { + color: var(--text-color-secondary); +} + +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + .slider { - position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; - background-color: #ccc; border-radius: 24px; + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + border-radius: 24px; transition: var(--transition-speed); } + .slider:before { - position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; - background-color: white; border-radius: 50%; + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; transition: var(--transition-speed); } -input:checked + .slider { background-color: var(--primary-color); } -input:checked + .slider:before { transform: translateX(20px); } + +input:checked+.slider { + background-color: var(--primary-color); +} + +input:checked+.slider:before { + transform: translateX(20px); +} + .caddy-control-panel { margin-top: 16px; padding: 12px; @@ -259,69 +486,112 @@ input:checked + .slider:before { transform: translateX(20px); } border-radius: var(--border-radius-small); transition: background-color var(--transition-speed); } + #caddy-action-button-container { display: flex; flex-direction: column; gap: 8px; } + .caddy-status { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; } + .status-dot { width: 10px; height: 10px; border-radius: 50%; transition: background-color var(--transition-speed); } -.status-dot.running { background-color: var(--success-color); } -.status-dot.stopped { background-color: var(--danger-color); } -.status-dot.checking { background-color: var(--checking-color); } -.status-dot.error { background-color: var(--text-color-secondary); } + +.status-dot.running { + background-color: var(--success-color); +} + +.status-dot.stopped { + background-color: var(--danger-color); +} + +.status-dot.checking { + background-color: var(--checking-color); +} + +.status-dot.error { + background-color: var(--text-color-secondary); +} + .status-text { font-weight: 500; font-size: 0.875rem; color: var(--text-color-secondary); } + .logout-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-color); transition: border-color var(--transition-speed); } + .main-header { - display: flex; align-items: center; margin-bottom: 24px; + display: flex; + align-items: center; + margin-bottom: 24px; } -.main-header h2 { font-size: 1.75rem; font-weight: 700; flex-grow: 1; } -#menu-toggle-btn { display: none; margin-right: 16px; } + +.main-header h2 { + font-size: 1.75rem; + font-weight: 700; + flex-grow: 1; +} + +#menu-toggle-btn { + display: none; + margin-right: 16px; +} + .card-panel { background-color: var(--surface-color); border-radius: var(--border-radius-large); padding: 24px; margin-bottom: 24px; border: 1px solid var(--border-color); - box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.03); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); transition: background-color var(--transition-speed), border-color var(--transition-speed); } + .form-panel-header { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; } + .form-panel-header h3 { flex-grow: 1; } + .btn { - display: inline-flex; align-items: center; justify-content: center; gap: 8px; - padding: 10px 18px; border: 1px solid transparent; - border-radius: var(--border-radius-small); font-weight: 600; - cursor: pointer; transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 18px; + border: 1px solid transparent; + border-radius: var(--border-radius-small); + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; width: 100%; } -.btn:active { transform: scale(0.97); } + +.btn:active { + transform: scale(0.97); +} + .btn:disabled { background-color: #e5e7eb; color: #9ca3af; @@ -329,27 +599,82 @@ input:checked + .slider:before { transform: translateX(20px); } border-color: transparent; transform: none; } + [data-theme="dark"] .btn:disabled { background-color: #374151; color: #6b7280; } -.btn-primary { background-color: var(--primary-color); color: white; } -.btn-primary:hover:not(:disabled) { background-color: var(--primary-color-hover); } -.btn-secondary { background-color: var(--surface-color); color: var(--text-color); border-color: var(--border-color); } -.btn-secondary:hover:not(:disabled) { border-color: #ced4da; background-color: var(--bg-color); } -.btn-danger { background-color: var(--danger-color); color: white; } -.btn-danger:hover:not(:disabled) { background-color: var(--danger-color-hover); } -.btn-success { background-color: var(--success-color); color: white; } -.btn-success:hover:not(:disabled) { background-color: var(--success-color-hover); } -.btn-warning { background-color: var(--warning-color); color: white; } -.btn-warning:hover:not(:disabled) { background-color: var(--warning-color-hover); } -.btn-small { padding: 6px 12px; font-size: 0.875rem; width: auto; } -.btn-icon { - background: none; border: none; color: var(--text-color-secondary); cursor: pointer; - width: 40px; height: 40px; font-size: 1.1rem; border-radius: 50%; + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background-color: var(--primary-color-hover); +} + +.btn-secondary { + background-color: var(--surface-color); + color: var(--text-color); + border-color: var(--border-color); +} + +.btn-secondary:hover:not(:disabled) { + border-color: #ced4da; + background-color: var(--bg-color); +} + +.btn-danger { + background-color: var(--danger-color); + color: white; +} + +.btn-danger:hover:not(:disabled) { + background-color: var(--danger-color-hover); +} + +.btn-success { + background-color: var(--success-color); + color: white; +} + +.btn-success:hover:not(:disabled) { + background-color: var(--success-color-hover); +} + +.btn-warning { + background-color: var(--warning-color); + color: white; +} + +.btn-warning:hover:not(:disabled) { + background-color: var(--warning-color-hover); +} + +.btn-small { + padding: 6px 12px; + font-size: 0.875rem; width: auto; } -.btn-icon:hover { background-color: var(--bg-color); color: var(--text-color); } + +.btn-icon { + background: none; + border: none; + color: var(--text-color-secondary); + cursor: pointer; + width: 40px; + height: 40px; + font-size: 1.1rem; + border-radius: 50%; + width: auto; +} + +.btn-icon:hover { + background-color: var(--bg-color); + color: var(--text-color); +} + .btn-link { background: none; border: none; @@ -359,77 +684,192 @@ input:checked + .slider:before { transform: translateX(20px); } padding: 4px 8px; width: auto; } + .btn-link:hover { text-decoration: underline; } -.main-header .btn-primary { width: auto; } -.form-actions .btn { width: auto; } -.config-list-container { display: flex; flex-direction: column; gap: 12px; } + +.main-header .btn-primary { + width: auto; +} + +.form-actions .btn { + width: auto; +} + +.config-list-container { + display: flex; + flex-direction: column; + gap: 12px; +} + .config-item { - display: flex; align-items: center; padding: 12px 16px; background-color: var(--bg-color); - border: 1px solid var(--border-color); border-radius: var(--border-radius-small); + display: flex; + align-items: center; + padding: 12px 16px; + background-color: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-small); transition: all 0.2s ease; } -.config-item:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); } -.config-item-name { font-weight: 500; flex-grow: 1; } -.config-item-actions { display: flex; gap: 8px; } -.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; margin-bottom: 24px; } -.form-group label, fieldset legend { - display: block; font-weight: 500; margin-bottom: 8px; color: var(--text-color-secondary); font-size: 0.875rem; + +.config-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); } + +.config-item-name { + font-weight: 500; + flex-grow: 1; +} + +.config-item-actions { + display: flex; + gap: 8px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 24px; + margin-bottom: 24px; +} + +.form-group label, +fieldset legend { + display: block; + font-weight: 500; + margin-bottom: 8px; + color: var(--text-color-secondary); + font-size: 0.875rem; +} + .form-group input { - width: 100%; padding: 12px; background-color: var(--surface-color); border: 1px solid var(--border-color); - border-radius: var(--border-radius-small); color: var(--text-color); font-size: 1rem; + width: 100%; + padding: 12px; + background-color: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-small); + color: var(--text-color); + font-size: 1rem; transition: border-color 0.2s, box-shadow 0.2s, background-color var(--transition-speed); } + .form-group input:focus { - outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); } -fieldset { border: none; padding: 0; margin: 0 0 24px 0; } + +/* 新增: 重置数字输入框的默认样式 */ +input[type="number"] { + -moz-appearance: textfield; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +fieldset { + border: none; + padding: 0; + margin: 0 0 24px 0; +} + .sub-fieldset { border: 1px solid var(--border-color); border-radius: var(--border-radius-small); padding: 16px; margin-top: 20px; } + .sub-legend-group { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } + .sub-legend-group .sub-legend { margin-bottom: 0; } + .sub-legend { font-weight: 500; color: var(--text-color-secondary); font-size: 0.875rem; margin-bottom: 12px; } -.checkbox-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-top: 8px; } + +.checkbox-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + margin-top: 8px; +} + .custom-checkbox { - position: relative; display: inline-flex; align-items: center; cursor: pointer; gap: 12px; + position: relative; + display: inline-flex; + align-items: center; + cursor: pointer; + gap: 12px; } -.custom-checkbox input { display: none; } + +.custom-checkbox input { + display: none; +} + .custom-checkbox .checkmark { - width: 20px; height: 20px; border: 2px solid var(--border-color); - border-radius: 6px; transition: all 0.2s; + width: 20px; + height: 20px; + border: 2px solid var(--border-color); + border-radius: 6px; + transition: all 0.2s; } -.custom-checkbox input:checked + .checkmark { background-color: var(--primary-color); border-color: var(--primary-color); } + +.custom-checkbox input:checked+.checkmark { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + .custom-checkbox .checkmark::after { - content: "\f00c"; font-family: "Font Awesome 6 Free"; font-weight: 900; - font-size: 12px; color: white; position: absolute; top: 3px; left: 3px; - transform: scale(0); transition: transform 0.2s; + content: "\f00c"; + font-family: "Font Awesome 6 Free"; + font-weight: 900; + font-size: 12px; + color: white; + position: absolute; + top: 3px; + left: 3px; + transform: scale(0); + transition: transform 0.2s; } -.custom-checkbox input:checked + .checkmark::after { transform: scale(1); } + +.custom-checkbox input:checked+.checkmark::after { + transform: scale(1); +} + .header-entry { - display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; margin-bottom: 12px; + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 12px; + margin-bottom: 12px; + align-items: center; + /* 修改: 确保所有元素垂直居中对齐 */ } + .header-entry input { - width: 100%; padding: 10px; background-color: var(--bg-color); border: 1px solid var(--border-color); - border-radius: var(--border-radius-small); color: var(--text-color); + width: 100%; + padding: 10px; + background-color: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-small); + color: var(--text-color); } + .segmented-control { position: relative; display: flex; @@ -440,6 +880,7 @@ fieldset { border: none; padding: 0; margin: 0 0 24px 0; } padding: 4px; transition: background-color var(--transition-speed), border-color var(--transition-speed); } + .segmented-control button { flex: 1; padding: 8px 12px; @@ -451,40 +892,150 @@ fieldset { border: none; padding: 0; margin: 0 0 24px 0; } transition: color 0.2s ease; z-index: 2; } + .segmented-control button:hover { color: var(--text-color); } + .segmented-control button.active { color: white; } + [data-theme="dark"] .segmented-control button.active { color: var(--text-color); } + #segmented-control-slider { position: absolute; top: 4px; bottom: 4px; background-color: var(--primary-color); border-radius: 6px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1; } -#rendered-output-panel pre { - background-color: var(--bg-color); padding: 16px; border-radius: var(--border-radius-small); - overflow-x: auto; white-space: pre-wrap; word-break: break-all; - border: 1px solid var(--border-color); transition: background-color var(--transition-speed); + +.custom-select { + position: relative; } + +/* 修改: 统一 .select-selected 的外观, 使其与 input 一致 */ +.select-selected { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 12px; + background-color: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-small); + color: var(--text-color); + font-size: 1rem; + font-family: inherit; + cursor: pointer; + user-select: none; + transition: border-color 0.2s, box-shadow 0.2s, background-color var(--transition-speed); + line-height: 1.5; + /* 与input的字体和内边距计算出的行高一致 */ + height: calc(1.5em + 24px + 2px); + /* font-size * line-height + padding * 2 + border * 2 */ +} + +.select-selected.select-arrow-active { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); +} + +.select-selected::after { + content: '\f078'; + font-family: 'Font Awesome 6 Free'; + font-weight: 900; + transition: transform var(--transition-speed) ease; +} + +.select-selected.select-arrow-active::after { + transform: rotate(180deg); +} + +.select-items { + position: absolute; + background-color: var(--surface-color); + top: 100%; + left: 0; + right: 0; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-small); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + z-index: 99; + margin-top: 8px; + max-height: 200px; + overflow-y: auto; + opacity: 0; + transform: translateY(-10px); + visibility: hidden; + transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s; +} + +.select-items.select-show { + opacity: 1; + transform: translateY(0); + visibility: visible; +} + +.select-items div { + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.2s; +} + +.select-items div:hover, +.same-as-selected { + background-color: var(--bg-color); +} + +#rendered-output-panel pre { + background-color: var(--bg-color); + padding: 16px; + border-radius: var(--border-radius-small); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; + border: 1px solid var(--border-color); + transition: background-color var(--transition-speed); +} + @media (max-width: 992px) { .sidebar { - position: fixed; z-index: 200; height: 100%; + position: fixed; + z-index: 200; + height: 100%; transform: translateX(-100%); - box-shadow: 0 0 20px rgba(0,0,0,0.1); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); + } + + .sidebar.is-open { + transform: translateX(0); + } + + #menu-toggle-btn { + display: inline-flex; + } + + .main-header .btn-text { + display: none; + } + + .sidebar-nav span, + .logout-section span { + display: inline; + } + + .caddy-control-panel .btn span { + display: inline; + } + + .main-content { + padding: 16px; } - .sidebar.is-open { transform: translateX(0); } - #menu-toggle-btn { display: inline-flex; } - .main-header .btn-text { display: none; } - .sidebar-nav span, .logout-section span { display: inline; } - .caddy-control-panel .btn span { display: inline; } - .main-content { padding: 16px; } } \ No newline at end of file diff --git a/frontend/global.html b/frontend/global.html new file mode 100644 index 0000000..222ba36 --- /dev/null +++ b/frontend/global.html @@ -0,0 +1,141 @@ + + + +
+ + +修改这些配置将会重写您的主 Caddyfile 并触发 + Caddy 重载。
+ + +