From 281c0816fa6080019d74389588d151a9f86977b3 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Sun, 13 Sep 2026 19:46:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20dsh=E8=AE=BE=E7=BD=AE=E3=80=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E3=80=8D=E2=86=92=E3=80=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=AE=BE=E7=BD=AE=E3=80=8D=EF=BC=9B=E3=80=8C?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E7=AE=A1=E7=90=86=E3=80=8D=E2=86=92=E3=80=8C?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86=20+=20=E6=AF=8F=E9=A1=B9?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E7=AE=A1=E7=90=86=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户要求(2026-09-13): - portal-entry:「用户管理」改名「用户设置」+ 去掉「打开管理台」入口(order 让位 102→103) - business-plugins:删掉跳转 portal 的按钮;分区改为管理项列表,5 项每项点开是原生弹窗; 「用户管理」弹窗内可直接 通过/禁用/启用/删除(调平台 admin API,不跳门户) - 版本:portal-entry 0.5.2 / business-plugins 0.3.0(均已铺发生效) - 验收脚本同步更新,npm run verify 全绿 --- poc/business-plugins/lib/client.js | 271 ++++++++++++++++------ poc/business-plugins/package.json | 4 +- scripts/verify-platform-admin-section.mjs | 8 +- 3 files changed, 201 insertions(+), 82 deletions(-) diff --git a/poc/business-plugins/lib/client.js b/poc/business-plugins/lib/client.js index f02f770..7285a7b 100644 --- a/poc/business-plugins/lib/client.js +++ b/poc/business-plugins/lib/client.js @@ -163,16 +163,16 @@ window.__ModuleLoader__.load({ "mem.safe": "余量充足", "mem.overWarn": "勾选后将超出单实例内存上限,实例可能起不来。建议先停用部分插件再应用。", "mem.perCard": "预估内存", - "pa.label": "平台管理", + "pa.label": "系统管理", "pa.readonly": "只读", "pa.loading": "加载中…", "pa.loadingDesc": "正在读取平台状态…", - "pa.notAdminTitle": "平台管理仅对管理员显示", + "pa.notAdminTitle": "系统管理仅对管理员显示", "pa.notAdminDesc": "当前账号不是管理员,此处不展示平台信息。", "pa.loadFailed": "平台状态读取失败", "pa.retry": "重试", "pa.openPortal": "打开完整管理台", - "pa.readonlyHint": "本分区为只读;写操作请在平台管理台完成。", + "pa.readonlyHint": "点开每一项,在弹窗内直接管理(无需跳转门户)。", "pa.inst": "当前实例", "pa.running": "运行中", "pa.stopped": "未运行", @@ -191,7 +191,24 @@ window.__ModuleLoader__.load({ "pa.pool": "候选池", "pa.poolCount": "已投放插件", "pa.runtime": "运行时", - "pa.items": "条目" + "pa.items": "条目", + "pa.usersTitle": "用户管理", + "pa.colUser": "用户名", + "pa.colRole": "角色", + "pa.colOp": "操作", + "pa.roleAdmin": "管理员", + "pa.roleActive": "已启用", + "pa.rolePending": "待审核", + "pa.roleDisabled": "已禁用", + "pa.opApprove": "通过", + "pa.opDisable": "禁用", + "pa.opEnable": "启用", + "pa.opRemove": "删除", + "pa.confirmRemove": "确认删除该用户?此操作不可撤销。", + "pa.done": "已更新", + "pa.failed": "操作失败", + "pa.noUsers": "暂无用户", + "pa.working": "处理中…" }; /** English dictionary, key-set complete against zh. */ var en = { @@ -238,16 +255,16 @@ window.__ModuleLoader__.load({ "mem.safe": "headroom is fine", "mem.overWarn": "The selection exceeds the per-instance memory limit and the instance may fail to start. Disable some plugins first.", "mem.perCard": "est. memory", - "pa.label": "Platform admin", + "pa.label": "System management", "pa.readonly": "read-only", "pa.loading": "Loading…", "pa.loadingDesc": "Reading platform status…", - "pa.notAdminTitle": "Platform admin is admin-only", + "pa.notAdminTitle": "System management is admin-only", "pa.notAdminDesc": "This account is not an admin, so platform info is hidden here.", "pa.loadFailed": "Failed to read platform status", "pa.retry": "Retry", "pa.openPortal": "Open full admin console", - "pa.readonlyHint": "This panel is read-only; do write actions in the admin console.", + "pa.readonlyHint": "Open any item to manage it in a popup — no portal jump.", "pa.inst": "Current instance", "pa.running": "Running", "pa.stopped": "Stopped", @@ -266,7 +283,24 @@ window.__ModuleLoader__.load({ "pa.pool": "Candidate pool", "pa.poolCount": "Published plugins", "pa.runtime": "Runtime", - "pa.items": "items" + "pa.items": "items", + "pa.usersTitle": "User management", + "pa.colUser": "Username", + "pa.colRole": "Role", + "pa.colOp": "Actions", + "pa.roleAdmin": "Admin", + "pa.roleActive": "Active", + "pa.rolePending": "Pending", + "pa.roleDisabled": "Disabled", + "pa.opApprove": "Approve", + "pa.opDisable": "Disable", + "pa.opEnable": "Enable", + "pa.opRemove": "Delete", + "pa.confirmRemove": "Delete this user? This cannot be undone.", + "pa.done": "Updated", + "pa.failed": "Action failed", + "pa.noUsers": "No users", + "pa.working": "Working…" }; /** @@ -972,29 +1006,39 @@ window.__ModuleLoader__.load({ var st = useState({ loading: true, err: "", me: null, status: null, users: null, storage: null, pool: null, runtime: null }); var s = st[0]; var setS = st[1]; + var ul = useState([]); + var userList = ul[0]; + var setUserList = ul[1]; + var mv = useState(null); + var modal = mv[0]; + var setModal = mv[1]; + var bs = useState(false); + var busy = bs[0]; + var setBusy = bs[1]; + var nt = useState(""); + var note = nt[0]; + var setNote = nt[1]; + var ne = useState(false); + var noteErr = ne[0]; + var setNoteErr = ne[1]; + function req(path, method) { + return fetch(portalHost() + path, { credentials: "include", method: method || "GET" }).then(function (r) { + return r.ok ? r.json().catch(function () { return {}; }) : null; + }).catch(function () { return null; }); + } + function loadUsers() { return req("/api/admin/users").then(function (d) { setUserList((d && d.users) || []); }); } function load() { setS(function (old) { return Object.assign({}, old, { loading: true, err: "" }); }); - var host = portalHost(); - function get(path) { - return fetch(host + path, { credentials: "include" }).then(function (r) { - return r.ok ? r.json() : null; - }).catch(function () { return null; }); - } - get("/api/auth/me").then(function (me) { + req("/api/auth/me").then(function (me) { var role = me && me.user ? me.user.role : null; - if (role !== "admin") { - setS(function (old) { return Object.assign({}, old, { loading: false, me: me }); }); - return; - } + if (role !== "admin") { setS(function (old) { return Object.assign({}, old, { loading: false, me: me }); }); return; } Promise.all([ - get("/api/dsh/status"), get("/api/admin/users"), - get("/api/admin/storage"), get("/api/plugins/business"), get("/api/admin/runtime") + req("/api/dsh/status"), req("/api/admin/users"), + req("/api/admin/storage"), req("/api/plugins/business"), req("/api/admin/runtime") ]).then(function (r) { - setS({ - loading: false, err: "", me: me, - status: r[0], users: r[1], storage: r[2], pool: r[3], runtime: r[4] - }); + setUserList((r[1] && r[1].users) || []); + setS({ loading: false, err: "", me: me, status: r[0], users: r[1], storage: r[2], pool: r[3], runtime: r[4] }); }); }).catch(function () { setS(function (old) { return Object.assign({}, old, { loading: false, err: t("pa.loadFailed") }); }); @@ -1002,65 +1046,140 @@ window.__ModuleLoader__.load({ } useEffect(function () { load(); }, []); - var wrap = { padding: "4px 2px", fontSize: "13px", color: T.text, display: "flex", flexDirection: "column", gap: "12px" }; - if (s.loading) { - return jsxRuntime.jsx("div", { style: wrap, children: [ - jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.loading") }), - jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub }, children: t("pa.loadingDesc") }) - ] }); - } - if (s.err) { - return jsxRuntime.jsx("div", { style: wrap, children: [ - jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500, color: T.danger }, children: s.err }), - jsxRuntime.jsx("button", { - key: "b", type: "button", onClick: load, - style: { alignSelf: "flex-start", padding: "6px 14px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.border, background: "transparent", color: T.text, fontSize: "13px" }, - children: t("pa.retry") - }) - ] }); - } - if (!s.me || !s.me.user || s.me.user.role !== "admin") { - return jsxRuntime.jsx("div", { style: wrap, children: [ - jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.notAdminTitle") }), - jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 }, children: t("pa.notAdminDesc") }) - ] }); + // 管理动作:就地调用平台 admin API(不跳门户) + function doUserAction(u, url, method, label) { + if (method === "DELETE" && !window.confirm(t("pa.confirmRemove"))) return; + setBusy(true); setNote(""); + req(url, method).then(function (r) { + setBusy(false); + setNoteErr(!r); + setNote(r ? t("pa.done") : t("pa.failed")); + return loadUsers(); + }).then(function () { return req("/api/admin/users"); }).then(function (d) { if (d && d.users) setUserList(d.users); }); } + var wrap = { padding: "4px 2px", fontSize: "13px", color: T.text, display: "flex", flexDirection: "column", gap: "12px" }; + if (s.loading) return jsxRuntime.jsx("div", { style: wrap, children: [ + jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.loading") }), + jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub }, children: t("pa.loadingDesc") }) ] }); + if (s.err) return jsxRuntime.jsx("div", { style: wrap, children: [ + jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500, color: T.danger }, children: s.err }), + jsxRuntime.jsx("button", { key: "b", type: "button", onClick: load, style: { alignSelf: "flex-start", padding: "6px 14px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.border, background: "transparent", color: T.text, fontSize: "13px" }, children: t("pa.retry") }) ] }); + if (!s.me || !s.me.user || s.me.user.role !== "admin") return jsxRuntime.jsx("div", { style: wrap, children: [ + jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.notAdminTitle") }), + jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 }, children: t("pa.notAdminDesc") }) ] }); + var inst = s.status && s.status.running ? t("pa.running") : t("pa.stopped"); - var instDesc = s.status && s.status.instance - ? t("pa.port") + " " + s.status.instance.port + " · " + t("pa.restarts") + " " + (s.status.instance.restarts || 0) - : "—"; var brk = s.status && s.status.breaker; - var users = (s.users && s.users.users) || []; - var nTotal = users.length; - var nPending = users.filter(function (u) { return u.role === "pending"; }).length; - var nDisabled = users.filter(function (u) { return u.role === "disabled"; }).length; + var nPending = userList.filter(function (u) { return u.role === "pending"; }).length; + var nDisabled = userList.filter(function (u) { return u.role === "disabled"; }).length; var stUsers = (s.storage && s.storage.users) || []; - var stDesc = s.storage && s.storage.generatedAt ? t("pa.storageNote") : (s.storage && s.storage.note) || "—"; var pool = (s.pool && (s.pool.plugins || s.pool.items)) || []; var rt = (s.runtime && (s.runtime.items || s.runtime.groups)) || []; - return jsxRuntime.jsx("div", { style: wrap, children: [ - jsxRuntime.jsx("div", { key: "hd", style: { display: "flex", alignItems: "baseline", gap: "8px", flexWrap: "wrap" }, children: [ - jsxRuntime.jsx("span", { key: "a", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.label") }), - jsxRuntime.jsx("span", { key: "b", style: { fontSize: "12px", color: T.dim }, children: t("pa.readonly") }), - jsxRuntime.jsx("span", { key: "c", style: { fontSize: "12px", color: T.dim, flexBasis: "100%" }, children: t("pa.readonlyHint") }) - ] }), - jsxRuntime.jsx("div", { key: "cards", style: { display: "flex", gap: "10px", flexWrap: "wrap" }, children: [ - jsxRuntime.jsx(PaCard, { key: "i", title: t("pa.inst"), value: inst, color: s.status && s.status.running ? T.success : T.sub, desc: instDesc }), - jsxRuntime.jsx(PaCard, { key: "b", title: t("pa.breaker"), value: brk ? t("pa.breakerOpen") : t("pa.breakerOk"), color: brk ? T.warn : T.success, desc: brk && brk.until ? String(brk.until) : "—" }), - jsxRuntime.jsx(PaCard, { key: "u", title: t("pa.users"), value: nTotal, desc: t("pa.pending") + " " + nPending + " · " + t("pa.disabled") + " " + nDisabled }), - jsxRuntime.jsx(PaCard, { key: "s", title: t("pa.storage"), value: stUsers.length, desc: stDesc }), - jsxRuntime.jsx(PaCard, { key: "p", title: t("pa.pool"), value: pool.length, desc: t("pa.poolCount") }), - jsxRuntime.jsx(PaCard, { key: "r", title: t("pa.runtime"), value: rt.length, desc: t("pa.items") }) - ] }), - jsxRuntime.jsx("button", { - key: "open", type: "button", - onClick: function () { window.open(portalHost() + "/portal.html", "_blank", "noopener"); }, - style: { alignSelf: "flex-start", padding: "7px 16px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.primary, background: T.primary, color: "#ffffff", fontSize: "14px" }, - children: t("pa.openPortal") - }) - ] }); + // ── 管理项清单:每项点开都是「弹窗内管理」(用户 2026-09-13 要求) + var ITEMS = [ + { id: "users", title: t("pa.usersTitle"), desc: t("pa.pending") + " " + nPending + " · " + t("pa.disabled") + " " + nDisabled, badge: userList.length }, + { id: "plugins", title: t("pa.pool"), desc: t("pa.poolCount"), badge: pool.length }, + { id: "runtime", title: t("pa.runtime"), desc: t("pa.items"), badge: rt.length }, + { id: "storage", title: t("pa.storage"), desc: t("pa.storageUsers"), badge: stUsers.length }, + { id: "instance", title: t("pa.inst"), desc: inst + (brk ? " · " + t("pa.breakerOpen") : ""), badge: s.status && s.status.instance ? s.status.instance.port : "—" }, + ]; + + function itemRow(it) { + return jsxRuntime.jsx("div", { + key: it.id, role: "button", tabIndex: 0, + onClick: function () { setNote(""); setModal(it.id); }, + onKeyDown: function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setModal(it.id); } }, + style: { display: "flex", alignItems: "center", gap: "10px", padding: "10px 12px", border: "1px solid " + T.border, borderRadius: "10px", background: T.field, cursor: "pointer" }, + children: [ + jsxRuntime.jsx("div", { key: "t", style: { flex: "1 1 auto", minWidth: 0 }, children: [ + jsxRuntime.jsx("div", { key: "a", style: { fontSize: "14px", fontWeight: 500, color: T.text }, children: it.title }), + jsxRuntime.jsx("div", { key: "b", style: { fontSize: "12px", color: T.dim, marginTop: "2px" }, children: it.desc }) ] }), + jsxRuntime.jsx("div", { key: "n", style: { fontSize: "13px", color: T.sub, fontVariantNumeric: "tabular-nums" }, children: it.badge }), + jsxRuntime.jsx("div", { key: "c", style: { color: T.dim, fontSize: "14px" }, children: "›" }) + ] + }); + } + + function modalBody() { + if (modal === "users") { + return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [ + jsxRuntime.jsx("div", { key: "hd", style: { display: "flex", background: T.field, fontSize: "12px", color: T.dim, borderRadius: "8px 8px 0 0" }, children: [ + jsxRuntime.jsx("div", { key: "a", style: { flex: "1 1 auto", padding: "6px 10px" }, children: t("pa.colUser") }), + jsxRuntime.jsx("div", { key: "b", style: { flex: "0 0 90px", padding: "6px 10px" }, children: t("pa.colRole") }), + jsxRuntime.jsx("div", { key: "c", style: { flex: "0 0 190px", padding: "6px 10px" }, children: t("pa.colOp") }) ] }), + (userList.length === 0 + ? jsxRuntime.jsx("div", { key: "e", style: { padding: "12px", fontSize: "13px", color: T.dim }, children: t("pa.noUsers") }) + : userList.map(function (u) { + var roleText = u.role === "admin" ? t("pa.roleAdmin") : u.role === "pending" ? t("pa.rolePending") : u.role === "disabled" ? t("pa.roleDisabled") : t("pa.roleActive"); + var ops = []; + if (u.role === "pending") ops.push(["a", t("pa.opApprove"), "POST", "/api/admin/users/" + u.id + "/approve"]); + if (u.role === "disabled") ops.push(["e", t("pa.opEnable"), "POST", "/api/admin/users/" + u.id + "/enable"]); + if (u.role === "active") ops.push(["d", t("pa.opDisable"), "POST", "/api/admin/users/" + u.id + "/disable"]); + ops.push(["r", t("pa.opRemove"), "DELETE", "/api/admin/users/" + u.id]); + return jsxRuntime.jsx("div", { key: u.id, style: { display: "flex", alignItems: "center", borderTop: "1px solid " + T.border, fontSize: "13px", color: T.text }, children: [ + jsxRuntime.jsx("div", { key: "n", style: { flex: "1 1 auto", padding: "7px 10px", wordBreak: "break-all" }, children: u.username }), + jsxRuntime.jsx("div", { key: "r", style: { flex: "0 0 90px", padding: "7px 10px", color: u.role === "pending" ? T.warn : u.role === "disabled" ? T.dim : T.sub }, children: roleText }), + jsxRuntime.jsx("div", { key: "o", style: { flex: "0 0 190px", padding: "7px 10px", display: "flex", gap: "6px", flexWrap: "wrap" }, children: ops.map(function (o) { + return jsxRuntime.jsx("button", { key: o[0], type: "button", disabled: busy, onClick: function () { doUserAction(u, o[3], o[2], o[1]); }, + style: { padding: "3px 10px", borderRadius: "6px", cursor: busy ? "default" : "pointer", fontSize: "12px", background: "transparent", border: "1px solid " + (o[0] === "r" ? T.danger : T.border), color: o[0] === "r" ? T.danger : T.text, opacity: busy ? 0.5 : 1 }, + children: o[1] }); + }) }), + ] }); + })), + note ? jsxRuntime.jsx("div", { key: "n", style: { fontSize: "12px", color: noteErr ? T.danger : T.success }, children: note }) : null + ] }); + } + if (modal === "plugins") { + return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: + pool.length === 0 ? jsxRuntime.jsx("div", { style: { fontSize: "13px", color: T.dim }, children: t("pa.empty") }) : pool.map(function (p, i) { + return jsxRuntime.jsx("div", { key: (p.id || i), style: { padding: "8px 10px", border: "1px solid " + T.border, borderRadius: "8px", fontSize: "13px", color: T.text }, children: (p.name || p.id || "插件") + (p.version ? " v" + p.version : "") }); + }) }); + } + if (modal === "runtime") { + return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: rt.map(function (x, i) { + return jsxRuntime.jsx("div", { key: i, style: { padding: "8px 10px", border: "1px solid " + T.border, borderRadius: "8px", fontSize: "13px", color: T.text }, children: (x.name || x.title || "条目") + (x.version ? " " + x.version : "") }); + }) }); + } + if (modal === "storage") { + return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: stUsers.length === 0 + ? jsxRuntime.jsx("div", { style: { fontSize: "13px", color: T.dim }, children: (s.storage && s.storage.note) || "—" }) + : stUsers.map(function (u, i) { return jsxRuntime.jsx("div", { key: i, style: { padding: "8px 10px", border: "1px solid " + T.border, borderRadius: "8px", fontSize: "13px", color: T.text }, children: (u.username || u.id || "—") + (u.usedMb != null ? " " + u.usedMb + " MiB" : "") }); }) }); + } + // instance + var i2 = s.status || {}; + return jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px", fontSize: "13px", color: T.text }, children: [ + jsxRuntime.jsx("div", { key: "a", children: t("pa.inst") + ":" + inst }), + jsxRuntime.jsx("div", { key: "b", children: t("pa.port") + ":" + ((i2.instance && i2.instance.port) || "—") }), + jsxRuntime.jsx("div", { key: "c", children: t("pa.restarts") + ":" + ((i2.instance && i2.instance.restarts) || 0) }), + jsxRuntime.jsx("div", { key: "d", children: t("pa.breaker") + ":" + (brk ? t("pa.breakerOpen") : t("pa.breakerOk")) }) ] }); + } + + var rows = [ + jsxRuntime.jsx("div", { key: "hd", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: t("pa.label") }), + jsxRuntime.jsx("div", { key: "sub", style: { fontSize: "12px", color: T.dim }, children: t("pa.readonlyHint") }), + jsxRuntime.jsx("div", { key: "items", style: { display: "flex", flexDirection: "column", gap: "8px" }, children: ITEMS.map(itemRow) }), + ]; + if (modal) { + var it = ITEMS.filter(function (x) { return x.id === modal; })[0] || { title: modal }; + rows.push(jsxRuntime.jsx("div", { + key: "overlay", + onClick: function () { setModal(null); }, + style: { position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,.4)", display: "flex", alignItems: "center", justifyContent: "center", padding: "24px" }, + children: jsxRuntime.jsx("div", { + onClick: function (e) { e.stopPropagation(); }, + style: { background: T.text === "" ? "#fff" : ("var(--dsw-alias-bg-layer-1, #ffffff)"), border: "1px solid " + T.border, borderRadius: "12px", width: "100%", maxWidth: "760px", maxHeight: "78vh", overflow: "auto", padding: "16px", boxShadow: "0 8px 32px rgba(0,0,0,.2)" }, + children: [ + jsxRuntime.jsx("div", { key: "mh", style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px" }, children: [ + jsxRuntime.jsx("div", { key: "t", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: it.title }), + jsxRuntime.jsx("button", { key: "x", type: "button", onClick: function () { setModal(null); }, style: { border: 0, background: "transparent", cursor: "pointer", fontSize: "16px", color: T.dim, padding: "2px 6px" }, children: "✕" }) ] }), + modalBody() + ] + }) + })); + } + return jsxRuntime.jsx("div", { style: wrap, children: rows }); } ctx.slots.inject("settings.section", function () { diff --git a/poc/business-plugins/package.json b/poc/business-plugins/package.json index beb1270..1f6e71a 100644 --- a/poc/business-plugins/package.json +++ b/poc/business-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@dsh-local/business-plugins", - "version": "0.2.9", + "version": "0.3.0", "description": "功能管理(原「功能插件」)section for dsh web profile — v0.2.8(2026-09-13):**所有弹窗改页内弹窗** —— 弃用 window.confirm,改为 fixed 遮罩 + 居中面板 + 点遮罩/✕ 关闭(视觉与交互对齐 MCN 工作台 dsh-plugin-mcn 的 modalTitle/modalText/modalBtns 三段结构);**超限时弹窗内红底警告块**(标题 + 说明 + 内存数字),确认按钮转危险色,未点确认不发请求。v0.2.7(档案 75 维度 B 落地 · 2026-09-13):卡片新增「预估内存 ≈ N MiB」徽章(按实测成本分色:≥60 红 / ≥30 黄)、卡片加高(minHeight 112 + padding 14/16)便于四层信息排布;**列表上方新增「内存预估」状态条** —— 实心段=当前已启用插件预估占用、半透明段=本次勾选增量、超单实例上限(384 MiB)整条转红并给文字警告,确认弹窗也带上内存预估。口径 = 空载基线 285 MiB + 逐插件实测加载成本(隔离 cgroup 的 rss 增量),**是预估值不是实时读数**。候选池列表 + 搜索/状态徽章 + 批量启用/禁用 + 确认弹窗 + 重启后自动刷新。v0.2.4(档案 67):对齐 06-工作台UI规范 —— **信息层次反转**(主视觉改为插件用途说明,插件名/版本退为副行小字;中文说明由候选池入库时优先取官方目录)、字号阶梯 14/13/12、徽章与按钮改规范量级、空态改「标题 + 说明」、新增列表行 hover 反馈。v0.2.2:分区名由「功能插件」改为「功能管理」。v0.2.0:配色改用官方 --dsw-* design token(跟随 dsh 主题);文案走官方 locale(zh/en)。", "type": "module", "main": "lib/index.js", @@ -21,4 +21,4 @@ }, "dependencies": {}, "license": "MIT" -} \ No newline at end of file +} diff --git a/scripts/verify-platform-admin-section.mjs b/scripts/verify-platform-admin-section.mjs index 9a97dce..2657e02 100644 --- a/scripts/verify-platform-admin-section.mjs +++ b/scripts/verify-platform-admin-section.mjs @@ -104,12 +104,12 @@ ok("存在 platform-admin 分区", !!pa); if (pa) ok("其 order = 102", pa.meta.order === 102); ROLE = "admin"; delete globalThis.__OPENED; const tAdmin = text(await render(pa.Comp)).join(" | "); -ok("admin:含分区标题「平台管理」", tAdmin.includes("平台管理")); -ok("admin:6 张卡齐全", ["当前实例","熔断","用户","存储","候选池","运行时"].every(k => tAdmin.includes(k))); -ok("admin:含只读声明", tAdmin.includes("只读")); +ok("admin:含分区标题「系统管理」", tAdmin.includes("系统管理")); +ok("admin:5 个管理项齐全", ["用户管理","候选池","运行时","存储","当前实例"].every(k => tAdmin.includes(k))); +ok("admin:不再是只读(提示改为弹窗内管理)", tAdmin.includes("弹窗")); ROLE = "active"; const tUser = text(await render(pa.Comp)).join(" | "); ok("非 admin:被门禁挡住", tUser.includes("仅对管理员显示")); -ok("非 admin:不出现任何指标卡", !["候选池","运行时"].some(k => tUser.includes(k))); +ok("非 admin:不出现任何管理项", !["候选池","运行时"].some(k => tUser.includes(k))); console.log(failed === 0 ? "\n结论:全绿 ✅" : "\n结论:有 " + failed + " 项失败 ❌"); process.exit(failed === 0 ? 0 : 1);