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 @@ 插件详情
-
+