From 4ef6f8f9b9223c741f0a1881d237239431de95df Mon Sep 17 00:00:00 2001 From: spaxie <311188777+spaxie@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:04:07 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=AE=9A=E4=B9=89=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=BA=A7=E5=88=AB=E7=AD=9B=E9=80=89=E4=B8=8B=E6=8B=89?= =?UTF-8?q?=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 +- .../static/assets/app.js | 72 ++++++++++++++- .../static/assets/styles.css | 91 ++++++++++++++++++- .../static/index.html | 35 ++++--- tests/test_package.py | 13 +++ uv.lock | 2 +- 6 files changed, 200 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc3b67e..b5b4b2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nonebot-plugin-mimo-console" -version = "0.1.10" +version = "0.1.11" description = "A lightweight WebUI management console for NoneBot2" readme = "README.md" requires-python = ">=3.10" @@ -58,7 +58,7 @@ requires = ["uv_build>=0.10.0,<0.12.0"] build-backend = "uv_build" [tool.bumpversion] -current_version = "0.1.10" +current_version = "0.1.11" commit = true message = "chore: 发布 v{new_version}" tag = true diff --git a/src/nonebot_plugin_mimo_console/static/assets/app.js b/src/nonebot_plugin_mimo_console/static/assets/app.js index b1db684..f135723 100644 --- a/src/nonebot_plugin_mimo_console/static/assets/app.js +++ b/src/nonebot_plugin_mimo_console/static/assets/app.js @@ -25,6 +25,7 @@ const state = { logs: [], logAfter: 0, logFollow: true, + logLevel: "ALL", timers: [], detailPlugin: null, detailSource: null, @@ -177,7 +178,7 @@ function bindEvents() { $("#dependency-install-form").addEventListener("submit", installDependency); $("#config-search").addEventListener("input", renderConfig); $("#log-search").addEventListener("input", renderLogs); - $("#log-level").addEventListener("change", renderLogs); + bindLogLevelSelect(); $("#log-follow").addEventListener("click", () => { state.logFollow = !state.logFollow; $("#log-follow").classList.toggle("active", state.logFollow); @@ -1453,9 +1454,76 @@ function renderRecentLogs() { : '
正在等待日志…
'; } +function setLogLevelSelectOpen(open, focusOption = false) { + const root = $("#log-level-select"); + const trigger = $("#log-level-trigger"); + const menu = $("#log-level-menu"); + root.classList.toggle("open", open); + menu.classList.toggle("hidden", !open); + trigger.setAttribute("aria-expanded", String(open)); + if (open && focusOption) { + const selected = $('.custom-select-option[aria-selected="true"]', menu); + (selected || $(".custom-select-option", menu))?.focus(); + } +} + +function selectLogLevel(value, { focusTrigger = true } = {}) { + const option = $(`.custom-select-option[data-value="${CSS.escape(value)}"]`, $("#log-level-menu")); + if (!option) return; + state.logLevel = value; + $("#log-level-label").textContent = option.textContent.trim(); + $$(".custom-select-option", $("#log-level-menu")).forEach((item) => { + const active = item === option; + item.classList.toggle("active", active); + item.setAttribute("aria-selected", String(active)); + }); + setLogLevelSelectOpen(false); + if (focusTrigger) $("#log-level-trigger").focus(); + renderLogs(); +} + +function bindLogLevelSelect() { + const root = $("#log-level-select"); + const trigger = $("#log-level-trigger"); + const menu = $("#log-level-menu"); + const options = $$(".custom-select-option", menu); + + trigger.addEventListener("click", () => { + setLogLevelSelectOpen(!root.classList.contains("open"), true); + }); + trigger.addEventListener("keydown", (event) => { + if (!["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) return; + event.preventDefault(); + setLogLevelSelectOpen(true, true); + }); + options.forEach((option) => { + option.addEventListener("click", () => selectLogLevel(option.dataset.value)); + option.addEventListener("keydown", (event) => { + const index = options.indexOf(option); + if (event.key === "ArrowDown" || event.key === "ArrowUp") { + event.preventDefault(); + const offset = event.key === "ArrowDown" ? 1 : -1; + options[(index + offset + options.length) % options.length].focus(); + } else if (event.key === "Home" || event.key === "End") { + event.preventDefault(); + options[event.key === "Home" ? 0 : options.length - 1].focus(); + } else if (event.key === "Escape" || event.key === "Tab") { + setLogLevelSelectOpen(false); + if (event.key === "Escape") { + event.preventDefault(); + trigger.focus(); + } + } + }); + }); + document.addEventListener("click", (event) => { + if (!root.contains(event.target)) setLogLevelSelectOpen(false); + }); +} + function renderLogs() { const query = $("#log-search").value.trim().toLowerCase(); - const level = $("#log-level").value; + const level = state.logLevel; const items = state.logs.filter((item) => (level === "ALL" || item.level === level) && [item.message, item.module].join(" ").toLowerCase().includes(query), diff --git a/src/nonebot_plugin_mimo_console/static/assets/styles.css b/src/nonebot_plugin_mimo_console/static/assets/styles.css index 3b4f2c1..d051b03 100644 --- a/src/nonebot_plugin_mimo_console/static/assets/styles.css +++ b/src/nonebot_plugin_mimo_console/static/assets/styles.css @@ -1931,12 +1931,97 @@ a { color: inherit; text-decoration: none; } .log-toolbar .search-box { max-width: none; } -.select { +.custom-select { + position: relative; width: 140px; + flex: 0 0 140px; +} + +.custom-select-trigger { + width: 100%; height: 40px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 0 12px; + border: 1px solid var(--line-strong); + border-radius: var(--radius-sm); + outline: 0; + background: var(--surface); color: var(--text-soft); font-size: 13px; + transition: border-color .18s var(--ease), box-shadow .18s var(--ease), background .18s var(--ease); +} +.custom-select-trigger:hover { background: var(--surface-hover); } +.custom-select.open .custom-select-trigger, +.custom-select-trigger:focus-visible { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} +.custom-select-trigger svg { + flex: 0 0 auto; + color: var(--muted); + transition: transform .18s var(--ease); +} +.custom-select.open .custom-select-trigger svg { transform: rotate(180deg); } + +.custom-select-menu { + position: absolute; + z-index: 85; + top: calc(100% + 7px); + right: 0; + width: 172px; + display: grid; + gap: 3px; + padding: 6px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + box-shadow: var(--shadow-md); + animation: select-menu-in .15s var(--ease); +} +@keyframes select-menu-in { + from { opacity: 0; transform: translateY(-4px) scale(.98); } +} + +.custom-select-option { + width: 100%; + height: 34px; + display: flex; + align-items: center; + gap: 9px; + padding: 0 9px; + border-radius: 7px; + outline: 0; + color: var(--text-soft); + font-family: var(--mono); + font-size: 12px; + text-align: left; + transition: color .12s, background .12s; +} +.custom-select-option:hover, +.custom-select-option:focus-visible { background: var(--surface-hover); color: var(--text); } +.custom-select-option.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; } +.custom-select-option.active::after { + content: "✓"; + margin-left: auto; + font-family: var(--font); +} + +.level-dot { + width: 7px; + height: 7px; + flex: 0 0 7px; + border-radius: 50%; + background: var(--muted-2); } +.level-dot.INFO { background: var(--color-blue); } +.level-dot.SUCCESS { background: var(--color-green); } +.level-dot.WARNING { background: var(--color-yellow); } +.level-dot.ERROR, +.level-dot.CRITICAL { background: var(--color-red); } +.level-dot.DEBUG { background: var(--muted); } .terminal { overflow: hidden; @@ -2492,6 +2577,8 @@ a { color: inherit; text-decoration: none; } .field input, .config-input, .select, + .custom-select-trigger, + .custom-select-menu, .view-toggle, .net-item, .radio-option, @@ -2518,6 +2605,8 @@ a { color: inherit; text-decoration: none; } .field input, .config-input, .select, + .custom-select-trigger, + .custom-select-menu, .net-item, .radio-option, .file-upload-btn, diff --git a/src/nonebot_plugin_mimo_console/static/index.html b/src/nonebot_plugin_mimo_console/static/index.html index 2543a56..a03f1a9 100644 --- a/src/nonebot_plugin_mimo_console/static/index.html +++ b/src/nonebot_plugin_mimo_console/static/index.html @@ -19,7 +19,7 @@ - +
@@ -407,15 +407,28 @@

运行日志

- +
+ + +
@@ -626,6 +639,6 @@

插件详情

- + diff --git a/tests/test_package.py b/tests/test_package.py index 155b92b..ee39b91 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -45,6 +45,19 @@ def test_github_proxy_tests_render_per_source_latency(self) -> None: self.assertIn('.proxy-latency[data-status="timeout"]', styles) self.assertIn('"status": "timeout"', api) + def test_log_level_filter_uses_custom_select(self) -> None: + static = ROOT / "src" / "nonebot_plugin_mimo_console" / "static" + index = (static / "index.html").read_text(encoding="utf-8") + script = (static / "assets" / "app.js").read_text(encoding="utf-8") + styles = (static / "assets" / "styles.css").read_text(encoding="utf-8") + self.assertNotIn('id="log-level" class="select"', index) + self.assertIn('id="log-level-trigger"', index) + self.assertIn('role="listbox"', index) + self.assertIn("function bindLogLevelSelect()", script) + self.assertIn("const level = state.logLevel;", script) + self.assertIn(".custom-select-menu", styles) + self.assertIn(".custom-select-option.active", styles) + def test_header_version_is_populated_from_runtime_metadata(self) -> None: static = ROOT / "src" / "nonebot_plugin_mimo_console" / "static" index = (static / "index.html").read_text(encoding="utf-8") diff --git a/uv.lock b/uv.lock index 5a9a758..6022c4c 100644 --- a/uv.lock +++ b/uv.lock @@ -866,7 +866,7 @@ wheels = [ [[package]] name = "nonebot-plugin-mimo-console" -version = "0.1.10" +version = "0.1.11" source = { editable = "." } dependencies = [ { name = "httpx" },