|
|
@@ -99,7 +99,8 @@
|
|
|
console: { title: "控制台", eyebrow: "Console" },
|
|
|
software: { title: "软件管理", eyebrow: "Software" },
|
|
|
sites: { title: "网站管理", eyebrow: "Websites" },
|
|
|
- mysql: { title: "数据库", eyebrow: "MySQL" },
|
|
|
+ mysql: { title: "MySQL", eyebrow: "数据库" },
|
|
|
+ redis: { title: "Redis", eyebrow: "数据库" },
|
|
|
files: { title: "文件管理", eyebrow: "Files" },
|
|
|
processes: { title: "进程管理", eyebrow: "Processes" },
|
|
|
shell: { title: "终端", eyebrow: "Shell" },
|
|
|
@@ -107,6 +108,10 @@
|
|
|
settings: { title: "设置", eyebrow: "Settings" },
|
|
|
};
|
|
|
|
|
|
+ const navGroups = {
|
|
|
+ database: ["mysql", "redis"],
|
|
|
+ };
|
|
|
+
|
|
|
function toast(msg, type = "ok") {
|
|
|
const el = document.createElement("div");
|
|
|
el.className = `toast-item ${type === "err" ? "err" : "ok"}`;
|
|
|
@@ -161,10 +166,23 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ function syncNavGroups(activeView) {
|
|
|
+ $$(".nav-group").forEach((g) => {
|
|
|
+ const key = g.dataset.navGroup;
|
|
|
+ const children = navGroups[key] || [];
|
|
|
+ const hasActive = children.includes(activeView);
|
|
|
+ g.classList.toggle("has-active", hasActive);
|
|
|
+ if (hasActive) g.classList.add("is-open");
|
|
|
+ const toggle = g.querySelector("[data-nav-toggle]");
|
|
|
+ if (toggle) toggle.setAttribute("aria-expanded", g.classList.contains("is-open") ? "true" : "false");
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
function setView(name) {
|
|
|
if (name === "overview" || name === "monitor") name = "console";
|
|
|
$$(".nav-item[data-view]").forEach((b) => b.classList.toggle("is-active", b.dataset.view === name));
|
|
|
$$(".view").forEach((v) => v.classList.toggle("is-active", v.dataset.viewPanel === name));
|
|
|
+ syncNavGroups(name);
|
|
|
const t = titles[name] || titles.console;
|
|
|
$("#page-title").textContent = t.title;
|
|
|
$("#page-eyebrow").textContent = t.eyebrow;
|
|
|
@@ -193,6 +211,9 @@
|
|
|
stopScheduleAutoRefresh();
|
|
|
}
|
|
|
if (name === "settings") fillSettingsForm();
|
|
|
+ if (name === "redis") {
|
|
|
+ loadRedisPage().catch((err) => toast(err.message || String(err), "err"));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function applyPanelName(name) {
|
|
|
@@ -1545,6 +1566,7 @@
|
|
|
function renderDatabases() {
|
|
|
fillSqlDbSelect();
|
|
|
const tbody = $("#db-table tbody");
|
|
|
+ if (!tbody) return;
|
|
|
if (!state.databases.length) {
|
|
|
tbody.innerHTML = `<tr><td colspan="6" class="text-secondary text-center py-4">暂无数据库记录</td></tr>`;
|
|
|
return;
|
|
|
@@ -1567,6 +1589,87 @@
|
|
|
.join("");
|
|
|
}
|
|
|
|
|
|
+ function redisInfoCell(label, value) {
|
|
|
+ return `<div class="col-sm-6 col-lg-3">
|
|
|
+ <div class="small text-secondary">${escapeHtml(label)}</div>
|
|
|
+ <div class="fw-semibold text-break">${value}</div>
|
|
|
+ </div>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderRedis() {
|
|
|
+ const grid = $("#redis-info-grid");
|
|
|
+ const actions = $("#redis-actions");
|
|
|
+ const summary = $("#redis-status-summary");
|
|
|
+ if (!grid || !actions) return;
|
|
|
+ const info = (state.status && state.status.redis) || {};
|
|
|
+ const installed = !!info.installed;
|
|
|
+ const running = info.running;
|
|
|
+ if (summary) {
|
|
|
+ summary.textContent = !installed
|
|
|
+ ? "未安装"
|
|
|
+ : running
|
|
|
+ ? "运行中"
|
|
|
+ : "已停止";
|
|
|
+ }
|
|
|
+ const btns = [];
|
|
|
+ if (!installed) {
|
|
|
+ btns.push(`<button type="button" class="btn btn-primary" data-act="redis-install">安装</button>`);
|
|
|
+ } else {
|
|
|
+ if (running) {
|
|
|
+ btns.push(`<button type="button" class="btn btn-outline-secondary" data-act="redis-stop">停止</button>`);
|
|
|
+ } else {
|
|
|
+ btns.push(`<button type="button" class="btn btn-primary" data-act="redis-start">启动</button>`);
|
|
|
+ }
|
|
|
+ btns.push(`<button type="button" class="btn btn-outline-primary" data-act="redis-config" data-conf="${escapeHtml(info.conf || "")}">配置</button>`);
|
|
|
+ btns.push(`<button type="button" class="btn btn-outline-secondary" data-act="redis-refresh">刷新</button>`);
|
|
|
+ btns.push(`<button type="button" class="btn btn-outline-danger" data-act="redis-uninstall">卸载</button>`);
|
|
|
+ }
|
|
|
+ actions.innerHTML = btns.join("");
|
|
|
+ grid.innerHTML = [
|
|
|
+ redisInfoCell("状态", statusPill(installed, running)),
|
|
|
+ redisInfoCell("版本", escapeHtml(info.version || "—")),
|
|
|
+ redisInfoCell("端口", escapeHtml(info.port != null ? String(info.port) : "—")),
|
|
|
+ redisInfoCell("配置文件", `<code class="small">${escapeHtml(info.conf || "—")}</code>`),
|
|
|
+ redisInfoCell("安装目录", `<code class="small">${escapeHtml(info.install_dir || "—")}</code>`),
|
|
|
+ redisInfoCell("可执行文件", `<code class="small">${escapeHtml(info.bin || "—")}</code>`),
|
|
|
+ ].join("");
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadRedisPage() {
|
|
|
+ try {
|
|
|
+ const data = await api("/api/redis/status");
|
|
|
+ if (!state.status) state.status = {};
|
|
|
+ state.status.redis = { ...(state.status.redis || {}), ...(data || {}) };
|
|
|
+ } catch (_) {
|
|
|
+ /* keep cached status */
|
|
|
+ }
|
|
|
+ renderRedis();
|
|
|
+ }
|
|
|
+
|
|
|
+ async function execRedisCmd(btn) {
|
|
|
+ const input = $("#redis-cmd-input");
|
|
|
+ const meta = $("#redis-cmd-meta");
|
|
|
+ const out = $("#redis-cmd-output");
|
|
|
+ if (!input) return;
|
|
|
+ const command = String(input.value || "").trim();
|
|
|
+ if (!command) {
|
|
|
+ toast("请输入 Redis 命令", "err");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await withBusy(async () => {
|
|
|
+ const data = await api("/api/redis/exec", {
|
|
|
+ method: "POST",
|
|
|
+ body: { command },
|
|
|
+ });
|
|
|
+ const elapsed = data && data.elapsed_ms != null ? `${data.elapsed_ms} ms` : "";
|
|
|
+ if (meta) meta.textContent = [elapsed, data && data.truncated ? "输出已截断" : ""].filter(Boolean).join(" · ");
|
|
|
+ if (out) {
|
|
|
+ out.hidden = false;
|
|
|
+ out.textContent = (data && data.output) || "(无输出)";
|
|
|
+ }
|
|
|
+ }, btn);
|
|
|
+ }
|
|
|
+
|
|
|
function stopProcAutoRefresh() {
|
|
|
if (state.procTimer) {
|
|
|
clearInterval(state.procTimer);
|
|
|
@@ -2164,6 +2267,7 @@
|
|
|
renderSoftware();
|
|
|
renderSites();
|
|
|
renderDatabases();
|
|
|
+ renderRedis();
|
|
|
const hash = (location.hash || "#console").slice(1);
|
|
|
if (hash === "files") await loadFiles(state.filesPath);
|
|
|
} catch (err) {
|
|
|
@@ -2174,6 +2278,13 @@
|
|
|
}
|
|
|
|
|
|
async function softAction(act, btn) {
|
|
|
+ if (act === "redis-refresh") {
|
|
|
+ await withBusy(async () => {
|
|
|
+ await loadRedisPage();
|
|
|
+ toast("已刷新");
|
|
|
+ }, btn);
|
|
|
+ return;
|
|
|
+ }
|
|
|
if (act === "redis-config") {
|
|
|
await withBusy(async () => openRedisConfigModal(), btn);
|
|
|
return;
|
|
|
@@ -4753,6 +4864,30 @@
|
|
|
$$(".nav-item[data-view]").forEach((btn) => {
|
|
|
btn.addEventListener("click", () => setView(btn.dataset.view));
|
|
|
});
|
|
|
+ $$("[data-nav-toggle]").forEach((btn) => {
|
|
|
+ btn.addEventListener("click", () => {
|
|
|
+ const key = btn.dataset.navToggle;
|
|
|
+ const group = $(`.nav-group[data-nav-group="${key}"]`);
|
|
|
+ if (!group) return;
|
|
|
+ group.classList.toggle("is-open");
|
|
|
+ btn.setAttribute("aria-expanded", group.classList.contains("is-open") ? "true" : "false");
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ $("#redis-actions")?.addEventListener("click", (e) => {
|
|
|
+ const btn = e.target.closest("button[data-act]");
|
|
|
+ if (!btn) return;
|
|
|
+ softAction(btn.dataset.act, btn);
|
|
|
+ });
|
|
|
+ $("#btn-redis-exec")?.addEventListener("click", (e) => {
|
|
|
+ execRedisCmd(e.currentTarget);
|
|
|
+ });
|
|
|
+ $("#redis-cmd-input")?.addEventListener("keydown", (e) => {
|
|
|
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
|
+ e.preventDefault();
|
|
|
+ execRedisCmd($("#btn-redis-exec"));
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
$("#btn-schedule-refresh")?.addEventListener("click", (e) => {
|
|
|
withBusy(async () => {
|