feat(ui): dsh设置「用户管理」→「用户设置」;「平台管理」→「系统管理 + 每项弹窗管理」

用户要求(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 全绿
This commit is contained in:
admin committed 2026-09-13 19:46:23 +08:00
1 parent 5869a4a979
commit 281c0816fa
3 files changed
+200 -81

No files matched your search

+195 -76
View File
@@ -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 () {
+1 -1
View File
@@ -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",
+4 -4
View File
@@ -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);