feat(R2): 插件新增原生「平台管理」只读分区(仅 admin)
档案 82 / 81 §9.2b:形态=原生弹窗(非 iframe),只读优先,写操作回跳平台域。
- client.js 895→1083 行:新增 PaCard + PlatformAdminSection + settings.section 注册(id=platform-admin, order=102)
- 数据源全部为平台既有只读路由(/api/auth/me、/api/dsh/status、/api/admin/{users,storage,runtime}、/api/plugins/business)
- i18n:zh/en 各 +29 个 pa.* 键
- 版本 0.2.8 → 0.2.9;tgz 已出(结构校验通过)
- 未验证:渲染/交互/权限门禁(需投放 + 启用,卡在需 admin 登录态)
This commit is contained in:
1 parent
43976fea6a
commit
8d5597b3f9
2 files changed
+191
-3
No files matched your search
@@ -162,7 +162,36 @@ window.__ModuleLoader__.load({
|
||||
"mem.tight": "接近上限",
|
||||
"mem.safe": "余量充足",
|
||||
"mem.overWarn": "勾选后将超出单实例内存上限,实例可能起不来。建议先停用部分插件再应用。",
|
||||
"mem.perCard": "预估内存"
|
||||
"mem.perCard": "预估内存",
|
||||
"pa.label": "平台管理",
|
||||
"pa.readonly": "只读",
|
||||
"pa.loading": "加载中…",
|
||||
"pa.loadingDesc": "正在读取平台状态…",
|
||||
"pa.notAdminTitle": "平台管理仅对管理员显示",
|
||||
"pa.notAdminDesc": "当前账号不是管理员,此处不展示平台信息。",
|
||||
"pa.loadFailed": "平台状态读取失败",
|
||||
"pa.retry": "重试",
|
||||
"pa.openPortal": "打开完整管理台",
|
||||
"pa.readonlyHint": "本分区为只读;写操作请在平台管理台完成。",
|
||||
"pa.inst": "当前实例",
|
||||
"pa.running": "运行中",
|
||||
"pa.stopped": "未运行",
|
||||
"pa.port": "端口",
|
||||
"pa.restarts": "自动重启",
|
||||
"pa.breaker": "熔断",
|
||||
"pa.breakerOpen": "已熔断(冷却中)",
|
||||
"pa.breakerOk": "正常",
|
||||
"pa.users": "用户",
|
||||
"pa.total": "合计",
|
||||
"pa.pending": "待审核",
|
||||
"pa.disabled": "已禁用",
|
||||
"pa.storage": "存储",
|
||||
"pa.storageUsers": "有用量的用户",
|
||||
"pa.storageNote": "报告每小时刷新",
|
||||
"pa.pool": "候选池",
|
||||
"pa.poolCount": "已投放插件",
|
||||
"pa.runtime": "运行时",
|
||||
"pa.items": "条目"
|
||||
};
|
||||
/** English dictionary, key-set complete against zh. */
|
||||
var en = {
|
||||
@@ -208,7 +237,36 @@ window.__ModuleLoader__.load({
|
||||
"mem.tight": "close to the limit",
|
||||
"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"
|
||||
"mem.perCard": "est. memory",
|
||||
"pa.label": "Platform admin",
|
||||
"pa.readonly": "read-only",
|
||||
"pa.loading": "Loading…",
|
||||
"pa.loadingDesc": "Reading platform status…",
|
||||
"pa.notAdminTitle": "Platform admin 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.inst": "Current instance",
|
||||
"pa.running": "Running",
|
||||
"pa.stopped": "Stopped",
|
||||
"pa.port": "Port",
|
||||
"pa.restarts": "Auto-restarts",
|
||||
"pa.breaker": "Circuit breaker",
|
||||
"pa.breakerOpen": "Open (cooling down)",
|
||||
"pa.breakerOk": "OK",
|
||||
"pa.users": "Users",
|
||||
"pa.total": "Total",
|
||||
"pa.pending": "Pending",
|
||||
"pa.disabled": "Disabled",
|
||||
"pa.storage": "Storage",
|
||||
"pa.storageUsers": "Users with usage",
|
||||
"pa.storageNote": "report refreshes hourly",
|
||||
"pa.pool": "Candidate pool",
|
||||
"pa.poolCount": "Published plugins",
|
||||
"pa.runtime": "Runtime",
|
||||
"pa.items": "items"
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -886,6 +944,136 @@ window.__ModuleLoader__.load({
|
||||
BusinessPluginsSection
|
||||
);
|
||||
});
|
||||
|
||||
// ── R2 · 平台管理(只读就地化;档案 81 §9.2b「原生弹窗」形态)────────────────
|
||||
// 形态:**原生渲染**(非 iframe)——复用本插件的 token 与 06-工作台UI规范 字号阶梯。
|
||||
// 数据:直接调平台只读 API(跨子域 fetch + credentials,门户已加 CORS 白名单)。
|
||||
// 范围:**只读子集**(实例 / 用户 / 存储 / 候选池 / 运行时);
|
||||
// **写操作一律回跳平台管理台** —— 不复制门户的写 UI(否则双源维护 + 扩大执行面)。
|
||||
// 可见性:**仅 admin**(先 /api/auth/me 判角色;非 admin 只渲染一行说明)。
|
||||
function PaCard(props) {
|
||||
return jsxRuntime.jsx("div", {
|
||||
style: {
|
||||
border: "1px solid " + T.border, borderRadius: "10px", background: T.field,
|
||||
padding: "12px 14px", display: "flex", flexDirection: "column", gap: "4px",
|
||||
minWidth: "0", flex: "1 1 150px"
|
||||
},
|
||||
children: [
|
||||
jsxRuntime.jsx("div", { key: "t", style: { fontSize: "12px", color: T.dim }, children: props.title }),
|
||||
jsxRuntime.jsx("div", { key: "v", style: { fontSize: "20px", fontWeight: 500, color: props.color || T.text, fontVariantNumeric: "tabular-nums" }, children: props.value }),
|
||||
jsxRuntime.jsx("div", { key: "d", style: { fontSize: "12px", color: T.dim, lineHeight: 1.6 }, children: props.desc })
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function PlatformAdminSection() {
|
||||
var useState = React.useState;
|
||||
var useEffect = React.useEffect;
|
||||
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];
|
||||
|
||||
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) {
|
||||
var role = me && me.user ? me.user.role : null;
|
||||
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")
|
||||
]).then(function (r) {
|
||||
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") }); });
|
||||
});
|
||||
}
|
||||
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") })
|
||||
] });
|
||||
}
|
||||
|
||||
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 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")
|
||||
})
|
||||
] });
|
||||
}
|
||||
|
||||
ctx.slots.inject("settings.section", function () {
|
||||
return ctx.slots.register(
|
||||
{
|
||||
name: "settings.section",
|
||||
id: "platform-admin",
|
||||
order: 102,
|
||||
label: function () { return t("pa.label"); }
|
||||
},
|
||||
PlatformAdminSection
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
exports.apply = apply;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dsh-local/business-plugins",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.9",
|
||||
"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",
|
||||
|
||||
Reference in new issue
Block a user