From 58e53c8aec2cb425542396cb27b9a4d6c0300acc Mon Sep 17 00:00:00 2001 From: zhilv Date: Sun, 3 May 2026 21:56:48 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(command):=20=E9=BB=91=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E5=BC=80=E5=85=B3=E3=80=81=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E4=B8=8E=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 list_enabled 开关控制是否启用名单过滤 - 表单变更后 800ms 自动保存,去掉手动保存按钮 - Header 显示"未保存"指示器,保存中 toast 提示 - 内容区限制最大宽度 900px,优化宽屏显示 - 侧边栏增加圆角选中态,运行状态带脉冲动画 - 白名单模式灰掉黑名单输入,关闭名单时显示遮罩 - 命令测试结果增加成功/失败颜色反馈 - 回调格式改用等宽字体代码块 --- config.py | 14 + handlers/admin.py | 804 +++++++++++++++++++++++++--------------------- plugin.py | 28 +- 3 files changed, 470 insertions(+), 376 deletions(-) diff --git a/config.py b/config.py index 6d846cd..4c77428 100644 --- a/config.py +++ b/config.py @@ -43,6 +43,7 @@ class CommandConfig: length_min: int = 2 length_max: int = 4 scope: str = "all" # all / group / private + list_enabled: bool = False # 是否启用黑白名单过滤 list_mode: str = "allow" # allow / deny allowed_groups: frozenset[str] = frozenset() denied_groups: frozenset[str] = frozenset() @@ -62,6 +63,7 @@ _ENV_DEFAULTS: dict = { "length_min": int(os.environ.get("COMMAND_LENGTH_MIN", "2")), "length_max": int(os.environ.get("COMMAND_LENGTH_MAX", "4")), "scope": os.environ.get("COMMAND_SCOPE", "all"), + "list_enabled": os.environ.get("COMMAND_LIST_ENABLED", "false").lower() in ("true", "1", "yes"), "list_mode": os.environ.get("COMMAND_LIST_MODE", "allow"), "allowed_groups": os.environ.get("COMMAND_ALLOWED_GROUPS", ""), "denied_groups": os.environ.get("COMMAND_DENIED_GROUPS", ""), @@ -87,6 +89,7 @@ def _yaml_defaults() -> dict: "length_min": 2, "length_max": 4, "scope": "all", + "list_enabled": False, "list_mode": "allow", "allowed_groups": [], "denied_groups": [], @@ -127,6 +130,7 @@ def ensure_settings_yaml() -> None: "length_max": ("COMMAND_LENGTH_MAX", int), "scope": ("COMMAND_SCOPE", str), "list_mode": ("COMMAND_LIST_MODE", str), + "list_enabled": ("COMMAND_LIST_ENABLED", "bool"), "allowed_groups": ("COMMAND_ALLOWED_GROUPS", "csv_list"), "denied_groups": ("COMMAND_DENIED_GROUPS", "csv_list"), "allowed_users": ("COMMAND_ALLOWED_USERS", "csv_list"), @@ -158,6 +162,14 @@ def _apply_yaml_to_command(data: dict) -> None: command.length_max = int(cmd_data.get("length_max", _ENV_DEFAULTS["length_max"])) command.scope = str(cmd_data.get("scope", _ENV_DEFAULTS["scope"])) command.list_mode = str(cmd_data.get("list_mode", _ENV_DEFAULTS["list_mode"])) + + # list_enabled: YAML bool → Python bool + list_enabled_val = cmd_data.get("list_enabled", _ENV_DEFAULTS["list_enabled"]) + if isinstance(list_enabled_val, str): + command.list_enabled = list_enabled_val.lower() in ("true", "1", "yes") + else: + command.list_enabled = bool(list_enabled_val) + command.callback_url = str(cmd_data.get("callback_url", _ENV_DEFAULTS["callback_url"])) command.callback_timeout = int(cmd_data.get("callback_timeout", _ENV_DEFAULTS["callback_timeout"])) @@ -210,6 +222,7 @@ def get_settings_flat() -> dict[str, str]: "command_length_max": str(command.length_max), "command_scope": command.scope, "command_list_mode": command.list_mode, + "command_list_enabled": str(command.list_enabled).lower(), "command_allowed_groups": ",".join(sorted(command.allowed_groups)), "command_denied_groups": ",".join(sorted(command.denied_groups)), "command_allowed_users": ",".join(sorted(command.allowed_users)), @@ -227,6 +240,7 @@ _API_KEY_MAP: dict[str, tuple[str, ...]] = { "command_length_max": ("length_max", int), "command_scope": ("scope", str), "command_list_mode": ("list_mode", str), + "command_list_enabled": ("list_enabled", "bool"), "command_at_sender": ("at_sender", "bool"), "command_callback_url": ("callback_url", str), "command_callback_timeout": ("callback_timeout", int), diff --git a/handlers/admin.py b/handlers/admin.py index 90311a6..de3396b 100644 --- a/handlers/admin.py +++ b/handlers/admin.py @@ -59,10 +59,11 @@ ADMIN_HTML = r""" --success: #52C41A; --warning: #FAAD14; --danger: #FF4D4F; - --sidebar-width: 220px; - --header-height: 56px; + --sidebar-width: 200px; + --header-height: 52px; --radius: 6px; --shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + --content-max-width: 900px; } * { margin: 0; padding: 0; box-sizing: border-box; } @@ -85,66 +86,63 @@ body { display: flex; flex-direction: column; z-index: 100; - transition: width 0.2s; } .sidebar-logo { height: var(--header-height); display: flex; align-items: center; - padding: 0 20px; + padding: 0 16px; color: #fff; - font-size: 16px; + font-size: 15px; font-weight: 600; border-bottom: 1px solid rgba(255,255,255,0.08); - white-space: nowrap; - overflow: hidden; } .sidebar-logo .logo-icon { - width: 28px; height: 28px; + width: 26px; height: 26px; background: var(--primary-color); border-radius: 6px; display: flex; align-items: center; justify-content: center; margin-right: 10px; - font-size: 14px; + font-size: 13px; flex-shrink: 0; } -.sidebar-nav { flex: 1; padding: 8px 0; overflow-y: auto; } +.sidebar-nav { flex: 1; padding: 12px 0; overflow-y: auto; } .nav-group-title { - padding: 16px 20px 6px; + padding: 12px 16px 4px; font-size: 11px; color: rgba(255,255,255,0.35); text-transform: uppercase; - letter-spacing: 1px; + letter-spacing: 0.5px; } .nav-item { display: flex; align-items: center; - padding: 10px 20px; + padding: 12px 16px; + margin: 2px 8px; + border-radius: 6px; color: rgba(255,255,255,0.65); cursor: pointer; transition: all 0.15s; - font-size: 14px; - border-left: 3px solid transparent; + font-size: 13px; } .nav-item:hover { color: #fff; - background: rgba(255,255,255,0.06); + background: rgba(255,255,255,0.08); } .nav-item.active { color: #fff; - background: rgba(22,119,255,0.15); - border-left-color: var(--primary-color); + background: var(--primary-color); } .nav-item .nav-icon { width: 18px; margin-right: 10px; - text-align: center; font-size: 15px; + text-align: center; font-size: 14px; flex-shrink: 0; } @@ -157,7 +155,7 @@ body { min-height: 100vh; } -/* Header */ +/* Header - sticky with unsaved indicator */ .header { height: var(--header-height); background: var(--card-bg); @@ -170,9 +168,12 @@ body { } .header-title { - font-size: 16px; + font-size: 15px; font-weight: 500; color: var(--text-main); + display: flex; + align-items: center; + gap: 8px; } .header-right { @@ -182,18 +183,52 @@ body { gap: 12px; } -.header-badge { +/* Status badge with pulse animation */ +.status-badge { + display: flex; + align-items: center; + gap: 6px; font-size: 12px; - padding: 2px 8px; - border-radius: 10px; - background: rgba(22,119,255,0.1); - color: var(--primary-color); + padding: 4px 10px; + border-radius: 12px; + background: rgba(82,196,26,0.1); + color: var(--success); } -/* Content */ +.status-dot { + width: 6px; height: 6px; + border-radius: 50%; + background: var(--success); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(1.2); } +} + +/* Unsaved indicator */ +.unsaved-indicator { + font-size: 12px; + color: var(--warning); + display: none; + align-items: center; + gap: 4px; +} + +.unsaved-indicator.show { display: flex; } + +/* Content - with max width */ .content { flex: 1; padding: 24px; + display: flex; + justify-content: center; +} + +.content-inner { + width: 100%; + max-width: var(--content-max-width); } /* Card */ @@ -201,11 +236,11 @@ body { background: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow); - margin-bottom: 20px; + margin-bottom: 16px; } .card-header { - padding: 16px 20px; + padding: 14px 18px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; @@ -213,99 +248,110 @@ body { } .card-header h3 { - font-size: 15px; + font-size: 14px; font-weight: 500; } -.card-body { padding: 20px; } +.card-body { padding: 18px; } /* Form */ .form-group { - margin-bottom: 18px; + margin-bottom: 16px; } .form-group:last-child { margin-bottom: 0; } .form-label { - display: flex; - align-items: center; + display: block; margin-bottom: 6px; font-size: 13px; font-weight: 500; color: var(--text-main); } -.form-label .label-hint { - margin-left: 6px; +.form-hint { font-weight: 400; color: var(--text-light); font-size: 12px; + margin-left: 4px; } -.form-input, .form-select, .form-textarea { +.form-input, .form-select { width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius); - font-size: 14px; + font-size: 13px; color: var(--text-main); background: #fff; - transition: border-color 0.15s, box-shadow 0.15s; + transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s; outline: none; } -.form-input:focus, .form-select:focus, .form-textarea:focus { +.form-input:focus, .form-select:focus { border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(22,119,255,0.15); + box-shadow: 0 0 0 2px rgba(22,119,255,0.12); } -.form-textarea { - min-height: 80px; - resize: vertical; - font-family: inherit; +.form-input::placeholder { color: var(--text-light); } + +.form-input:disabled, .form-select:disabled { + background: #f5f5f5; + color: var(--text-light); + cursor: not-allowed; + opacity: 0.6; } .form-row { - display: flex; + display: grid; + grid-template-columns: 1fr 1fr; gap: 16px; } -.form-row .form-group { flex: 1; } +@media (max-width: 600px) { + .form-row { grid-template-columns: 1fr; } +} /* Tag input */ .tag-container { display: flex; flex-wrap: wrap; gap: 6px; - padding: 6px 10px; + padding: 8px 10px; border: 1px solid var(--border-color); border-radius: var(--radius); - min-height: 38px; + min-height: 42px; cursor: text; - transition: border-color 0.15s, box-shadow 0.15s; + transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s, background 0.2s; background: #fff; } .tag-container:focus-within { border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(22,119,255,0.15); + box-shadow: 0 0 0 2px rgba(22,119,255,0.12); +} + +.tag-container.disabled { + background: #f5f5f5; + opacity: 0.6; + pointer-events: none; } .tag-item { display: inline-flex; align-items: center; - padding: 2px 8px; - background: rgba(22,119,255,0.08); + padding: 3px 8px; + background: rgba(22,119,255,0.1); color: var(--primary-color); border-radius: 4px; - font-size: 13px; + font-size: 12px; gap: 4px; } .tag-item .tag-close { cursor: pointer; font-size: 14px; - opacity: 0.6; + opacity: 0.5; line-height: 1; } @@ -315,60 +361,27 @@ body { border: none; outline: none; flex: 1; - min-width: 80px; - font-size: 14px; + min-width: 100px; + font-size: 13px; padding: 2px 0; color: var(--text-main); + background: transparent; } -/* Buttons */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 7px 16px; - border-radius: var(--radius); - font-size: 14px; - cursor: pointer; - border: 1px solid transparent; - transition: all 0.15s; - gap: 6px; - line-height: 1.4; -} - -.btn-primary { - background: var(--primary-color); - color: #fff; -} -.btn-primary:hover { background: var(--primary-hover); } - -.btn-default { - background: #fff; - border-color: var(--border-color); - color: var(--text-main); -} -.btn-default:hover { border-color: var(--primary-color); color: var(--primary-color); } - -.btn-danger { - background: #fff; - border-color: var(--danger); - color: var(--danger); -} -.btn-danger:hover { background: var(--danger); color: #fff; } - -.btn-sm { padding: 4px 10px; font-size: 12px; } +.tag-input::placeholder { color: var(--text-light); } /* Switch */ .switch-wrapper { display: flex; align-items: center; - gap: 8px; + gap: 10px; } .switch { position: relative; - width: 40px; height: 22px; + width: 38px; height: 22px; cursor: pointer; + flex-shrink: 0; } .switch input { display: none; } @@ -391,34 +404,63 @@ body { transition: 0.2s; } -.switch input:checked + .switch-slider { - background: var(--primary-color); -} - -.switch input:checked + .switch-slider::before { - transform: translateX(18px); -} +.switch input:checked + .switch-slider { background: var(--primary-color); } +.switch input:checked + .switch-slider::before { transform: translateX(16px); } .switch-text { font-size: 13px; color: var(--text-secondary); } -/* Status dot */ -.status-dot { - width: 8px; height: 8px; - border-radius: 50%; - display: inline-block; - margin-right: 6px; +/* Code block */ +.code-block { + font-family: "SF Mono", Monaco, "Cascadia Code", Consolas, monospace; + font-size: 12px; + background: #f6f8fa; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 10px 12px; + overflow-x: auto; + line-height: 1.5; + color: #24292f; +} + +/* Info block */ +.info-block { + padding: 12px 14px; + background: #f0f5ff; + border-radius: var(--radius); + border-left: 3px solid var(--primary-color); + margin-bottom: 16px; + font-size: 13px; + color: var(--text-secondary); + line-height: 1.6; +} + +/* Section disabled overlay */ +.section-disabled { + position: relative; +} + +.section-disabled::after { + content: '名单功能已关闭'; + position: absolute; + inset: 0; + background: rgba(255,255,255,0.7); + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + color: var(--text-light); + border-radius: var(--radius); + pointer-events: none; } -.status-dot.on { background: var(--success); } -.status-dot.off { background: var(--text-light); } /* Toast */ .toast-container { position: fixed; - top: 16px; - right: 16px; + top: 12px; + right: 12px; z-index: 9999; display: flex; flex-direction: column; @@ -426,20 +468,20 @@ body { } .toast { - padding: 10px 16px; + padding: 10px 14px; border-radius: var(--radius); - font-size: 14px; + font-size: 13px; box-shadow: 0 4px 12px rgba(0,0,0,0.12); display: flex; align-items: center; gap: 8px; animation: slideIn 0.2s ease; - min-width: 240px; } .toast-success { background: #f6ffed; color: #389e0d; border: 1px solid #b7eb8f; } .toast-error { background: #fff2f0; color: #cf1322; border: 1px solid #ffccc7; } .toast-info { background: #e6f7ff; color: #0958d9; border: 1px solid #91caff; } +.toast-saving { background: #fffbe6; color: #d48806; border: 1px solid #ffe58f; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } @@ -449,61 +491,20 @@ body { /* Responsive */ @media (max-width: 768px) { .sidebar { width: 0; overflow: hidden; } - .sidebar.open { width: var(--sidebar-width); } .main { margin-left: 0; } - .form-row { flex-direction: column; gap: 0; } -} - -/* Tabs */ -.tabs { - display: flex; - border-bottom: 1px solid var(--border-color); - margin-bottom: 0; -} - -.tab-item { - padding: 12px 20px; - font-size: 14px; - color: var(--text-secondary); - cursor: pointer; - border-bottom: 2px solid transparent; - transition: all 0.15s; -} - -.tab-item:hover { color: var(--primary-color); } - -.tab-item.active { - color: var(--primary-color); - border-bottom-color: var(--primary-color); - font-weight: 500; -} - -.tab-panel { display: none; } -.tab-panel.active { display: block; } - -/* Info block */ -.info-block { - padding: 12px 16px; - background: #f0f5ff; - border-radius: var(--radius); - border-left: 3px solid var(--primary-color); - margin-bottom: 16px; - font-size: 13px; - color: var(--text-secondary); - line-height: 1.6; + .content { padding: 16px; } } - -