diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5f6c07a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "LiteCron Web", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/webapp.py", + "cwd": "${workspaceFolder}/src", + "console": "integratedTerminal", + "env": { + "WEBUI_PORT": "5000" + } + } + ] +} \ No newline at end of file diff --git a/compose.example.yml b/compose.example.yml index b3a579d..5d8f677 100644 --- a/compose.example.yml +++ b/compose.example.yml @@ -17,8 +17,8 @@ services: ports: - "5000:5000" volumes: - - ./config.yml:/app/config.yml:ro - - ./tasks:/app/tasks:ro + - ./config.yml:/app/config.yml + - ./tasks:/app/tasks - ./logs:/app/logs - ./data:/app/data networks: diff --git a/requirements.txt b/requirements.txt index 3c88880..cfa3970 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # 定时任务容器依赖 PyYAML==6.0.1 +ruamel.yaml>=0.18.0 requests>=2.28.0 curl_cffi>=0.5.0 diff --git a/src/static/app.js b/src/static/app.js index 31df9e2..e196c16 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -16,8 +16,7 @@ const elements = { taskList: document.getElementById('task-list'), taskCount: document.getElementById('task-count'), enabledCount: document.getElementById('enabled-count'), - updateTime: document.getElementById('update-time'), - + // 按钮 btnLogs: document.getElementById('btn-logs'), btnClean: document.getElementById('btn-clean'), @@ -173,16 +172,11 @@ async function loadTasks() { `; - // 更新时间 - elements.updateTime.textContent = new Date().toLocaleString(); return; } renderTasks(); - - // 更新时间 - elements.updateTime.textContent = new Date().toLocaleString(); - + } catch (error) { console.error('加载任务失败:', error); elements.taskList.innerHTML = ` @@ -250,16 +244,16 @@ function renderTasks() {
- -
diff --git a/src/static/style.css b/src/static/style.css index 59fc84f..7a21196 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -127,18 +127,30 @@ body { gap: 12px; } -.logo-icon { - font-size: 2rem; - filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); -} - .logo h1 { font-size: 1.75rem; font-weight: 700; +} + +.logo h1 a { + text-decoration: none; background: linear-gradient(135deg, var(--accent-primary), var(--accent-info)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + transition: opacity var(--transition-fast); +} + +.logo h1 a:hover { + opacity: 0.8; +} + +.version-badge { + display: inline-flex; + align-items: center; + color: var(--text-muted); + font-size: inherit; + font-weight: inherit; } .header-stats { @@ -449,44 +461,97 @@ body { /* 操作按钮 */ .task-actions { display: flex; - gap: 8px; + gap: 6px; } .btn-action { - padding: 8px 14px; + padding: 6px 12px; font-size: 0.8125rem; border-radius: var(--radius-sm); border: none; cursor: pointer; transition: all var(--transition-fast); font-weight: 500; + display: inline-flex; + align-items: center; + gap: 4px; } .btn-run { - background: var(--accent-primary); + background: linear-gradient(135deg, var(--accent-primary), #2563eb); color: var(--text-inverse); + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); + position: relative; + overflow: hidden; +} + +.btn-run::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + transition: left 0.5s ease; +} + +.btn-run:hover::before { + left: 100%; } .btn-run:hover { - background: var(--accent-primary-hover); - transform: scale(1.05); + background: linear-gradient(135deg, #2563eb, #1d4ed8); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3); +} + +.btn-run:disabled { + background: var(--text-muted); + box-shadow: none; } .btn-toggle { - background: var(--bg-primary); + background: var(--bg-card); color: var(--text-secondary); border: 1px solid var(--border-color); + box-shadow: var(--shadow-sm); } .btn-toggle:hover { background: var(--bg-hover); color: var(--text-primary); + border-color: var(--accent-primary); + transform: translateY(-1px); +} + +.btn-toggle.enable { + color: var(--accent-success); + border-color: rgba(16, 185, 129, 0.3); +} + +.btn-toggle.enable:hover { + background: rgba(16, 185, 129, 0.1); + border-color: var(--accent-success); + color: var(--accent-success); +} + +.btn-toggle.disable { + color: var(--accent-warning); + border-color: rgba(245, 158, 11, 0.3); +} + +.btn-toggle.disable:hover { + background: rgba(245, 158, 11, 0.1); + border-color: var(--accent-warning); + color: var(--accent-warning); } .btn-action:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; + box-shadow: none !important; } /* 加载状态 */ @@ -525,6 +590,16 @@ body { font-size: 0.875rem; } +.footer a { + color: var(--text-muted); + text-decoration: none; +} + +.footer a:hover { + color: var(--text-secondary); + text-decoration: underline; +} + /* 模态框 */ .modal { display: none; diff --git a/src/template/index.html b/src/template/index.html index 2f87833..5639921 100644 --- a/src/template/index.html +++ b/src/template/index.html @@ -13,8 +13,7 @@
@@ -80,7 +79,7 @@

diff --git a/src/webapp.py b/src/webapp.py index 4f90977..55bc384 100644 --- a/src/webapp.py +++ b/src/webapp.py @@ -64,20 +64,44 @@ def load_config() -> Optional[Dict[str, Any]]: def save_config(config: Dict[str, Any]) -> bool: - """保存 YAML 配置文件""" + """保存 YAML 配置文件,保留原始格式和注释""" try: - import yaml + from ruamel.yaml import YAML + ry = YAML() + ry.preserve_quotes = True + + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + doc = ry.load(f) + + _deep_merge(doc, config) with open(CONFIG_FILE, "w", encoding="utf-8") as f: - yaml.dump( - config, f, allow_unicode=True, sort_keys=False, default_flow_style=False - ) + ry.dump(doc, f) + return True except Exception as e: log_error(f"保存配置失败: {e}") return False +def _deep_merge(base, update): + """深度合并 update 到 base,保留 base 的 ruamel 格式对象""" + for key, value in update.items(): + if key in base and isinstance(base[key], dict) and isinstance(value, dict): + _deep_merge(base[key], value) + elif key in base and isinstance(base[key], list) and isinstance(value, list): + for i, item in enumerate(value): + if i < len(base[key]): + if isinstance(base[key][i], dict) and isinstance(item, dict): + _deep_merge(base[key][i], item) + else: + base[key][i] = item + else: + base[key].append(item) + else: + base[key] = value + + def get_container_status() -> Dict[str, Any]: """获取容器状态""" try: