style(ui): 「系统管理」改按门户 portal.html 实际组件照抄(5 个弹窗全部改表格)

用户第 2 次反馈「点击卡片后的弹窗样式还是没变,要用之前门户的里面的对应样式」。

查明两个真因:
1. 上一步改动没出厂 —— client.js mtime 20:33 > 最新产物 tgz 20:17;本次已升到 0.3.3 并铺发。
2. 更要命:上一版只有「用户管理」1 个弹窗是门户表格,另外 4 个
   (候选池/运行时/存储/当前实例)仍是裸 div + 内联样式。

本轮改法:读 web/portal.html 源码,逐值对应(不再照抄 06-规范 的抽象条目)
- .nav-grid / .nav-card(r10 / 18px 16px / hover 上浮-2px + 蓝边 + 0 4px 12px)
- .pg-tab .pg-cnt 计数胶囊;.table-wrap + table.tbl(5 个弹窗全部)
- .badge 四色 / .btn-sm + .danger / .card-h / .page-title / .page-sub
- 弹窗外壳 = .card(r10/18px) + design.css --shadow(门户无弹窗)
仅颜色走 dsh token --dsw-*(须跟随 dsh 主题),字号/间距/圆角/动效与门户逐值一致。

校验:verify 脚本新增结构层断言(8 项全绿)+ 服务器三方 md5 一致
     + 端到端取实例下发 bundle(rev=fd084d6aa33a)新标记命中、旧标记为 0。
顺带修校验脚本自身缺陷:树遍历 depth>12 会截断表格行文本 → 放宽到 20。
This commit is contained in:
admin committed 2026-09-13 20:45:36 +08:00
1 parent ac67db7702
commit b9540a8536
3 files changed
+230 -96

No files matched your search

+175 -93
View File
@@ -208,7 +208,15 @@ window.__ModuleLoader__.load({
"pa.done": "已更新",
"pa.failed": "操作失败",
"pa.noUsers": "暂无用户",
"pa.working": "处理中…"
"pa.working": "处理中…",
"pa.colName": "插件",
"pa.colVer": "版本",
"pa.colDesc": "说明",
"pa.colItem": "项目",
"pa.colValue": "值",
"pa.colUsed": "已用",
"pa.detail": "详情",
"pa.close": "关闭"
};
/** English dictionary, key-set complete against zh. */
var en = {
@@ -300,7 +308,15 @@ window.__ModuleLoader__.load({
"pa.done": "Updated",
"pa.failed": "Action failed",
"pa.noUsers": "No users",
"pa.working": "Working…"
"pa.working": "Working…",
"pa.colName": "Plugin",
"pa.colVer": "Version",
"pa.colDesc": "Description",
"pa.colItem": "Item",
"pa.colValue": "Value",
"pa.colUsed": "Used",
"pa.detail": "Details",
"pa.close": "Close"
};
/**
@@ -340,19 +356,59 @@ window.__ModuleLoader__.load({
".bp-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary, #2f6fed);color:var(--dsw-alias-brand-primary, #2f6fed)}",
".bp-row input[type=checkbox]{cursor:pointer;margin:0}",
// 2026-09-13 ·「系统管理」视觉层(对齐 06-工作台UI规范:§4.5 卡片 hover 上浮 / §4.7 弹窗 / §4.1 按钮)
".pa-card{transition:border-color .15s cubic-bezier(.16,1,.3,1),box-shadow .15s cubic-bezier(.16,1,.3,1),transform .15s cubic-bezier(.16,1,.3,1)}",
".pa-card:hover{border-color:var(--dsw-alias-brand-primary,#2f6fed);box-shadow:0 4px 12px rgba(47,111,237,.15);transform:translateY(-2px)}",
".pa-card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f6fed);outline-offset:2px}",
".pa-card:hover .pa-go{color:var(--dsw-alias-brand-primary,#2f6fed);opacity:1}",
".pa-go{opacity:.45;transition:color .15s,opacity .15s}",
".pa-btn{transition:border-color .15s cubic-bezier(.16,1,.3,1),color .15s cubic-bezier(.16,1,.3,1),background .15s cubic-bezier(.16,1,.3,1)}",
".pa-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed);background:rgba(47,111,237,.06)}",
".pa-row{transition:background .15s}",
".pa-row:hover{background:rgba(47,111,237,.06)}",
".pa-modal{box-shadow:0 8px 30px rgba(0,0,0,.28);animation:paIn .18s cubic-bezier(.16,1,.3,1)}",
"@keyframes paIn{from{opacity:0;transform:translateY(6px) scale(.985)}to{opacity:1;transform:none}}",
"@media (prefers-reduced-motion: reduce){.pa-card,.pa-modal{animation:none;transition:none}.pa-card:hover{transform:none}}"
// 2026-09-13 第 2 轮 ·「系统管理」视觉层 —— **照抄门户 `web/portal.html` 的对应组件**
// (用户要求:「说了要用之前门户的里面的对应样式」)。逐条对应关系,便于核对:
// .pa-hd / .pa-sub ← .page-title(20-600) / .page-sub(13, dim)
// .pa-sec ← .home-section-title(13, dim, 600)
// .pa-grid ← .nav-grid(repeat(auto-fill,minmax(200px,1fr)) / gap 14px)
// .pa-card ← .nav-card(r10 / 18px 16px / hover: 蓝边 + 0 4px 12px rgba(47,111,237,.12) + 上浮 -2px)
// .pa-card .pa-ic/.pa-t/.pa-d ← .nav-card .ic(主色) / .t(15-600) / .d(12, dim)
// .pa-cnt ← .pg-tab .pg-cnt(12-600 胶囊 / 主色底)
// .pa-wrap ← .table-wrap(r8 / 白底 / 1px 边框)
// .pa-tbl th/td ← table.tbl th(灰底 8px 10px 600) / td(7px 10px) / 行 hover 浅蓝
// .pa-badge ← .badge(2px 8px / r10 / 13px / 四色)
// .pa-sm ← .btn-sm(3px 10px / r6 / 14px;.danger 红字红边)
// .pa-empty-cell ← 门户 `emptyRow()`(表格内空态:居中 + 40px 上下留白)
// .pa-card-h ← .card-h(15-600 + .sub 13 dim)
// .pa-modal ← 门户无弹窗 ⇒ 面板取 .card(r10 / 18px) + 门户家族阴影(design.css `--shadow`)
// ⚠️ 唯一差异:颜色走 dsh 官方 token `--dsw-*`(本 section 嵌在 dsh 面板内、须随 dsh 主题;
// 门户那套 `--bp-*` 是给亮色静态页的硬编码)。字号/间距/圆角/动效与门户**逐值一致**。
".pa-hd{font-size:20px;font-weight:600;margin:0 0 4px}",
".pa-sub{font-size:13px;color:var(--dsw-alias-label-tertiary,#6b7280);margin:0 0 20px}",
".pa-sec{font-size:13px;font-weight:600;color:var(--dsw-alias-label-tertiary,#6b7280);margin:0 0 10px}",
".pa-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px}",
".pa-card{display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:10px;padding:18px 16px;cursor:pointer;transition:all .15s}",
".pa-card:hover{border-color:var(--dsw-alias-brand-primary,#2f6fed);box-shadow:0 4px 12px rgba(47,111,237,.12);transform:translateY(-2px)}",
".pa-card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f6fed);outline-offset:2px}",
".pa-card .pa-ic{color:var(--dsw-alias-brand-primary,#2f6fed);font-size:18px;line-height:1}",
".pa-card .pa-t{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary,#24292f)}",
".pa-card .pa-d{font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-cnt{display:inline-block;min-width:18px;padding:1px 6px;margin-left:6px;border-radius:9px;background:rgba(47,111,237,.10);color:var(--dsw-alias-brand-primary,#2f6fed);font-size:12px;font-weight:600;line-height:16px;text-align:center}",
".pa-wrap{overflow:auto;border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff)}",
".pa-tbl{width:100%;border-collapse:collapse;font-size:15px}",
".pa-tbl th{background:var(--dsw-alias-bg-layer-2,#f6f8fa);padding:8px 10px;text-align:left;font-weight:600;white-space:nowrap;border-bottom:1px solid var(--dsw-alias-border-l2,#e3e6ea)}",
".pa-tbl td{padding:7px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,#e3e6ea);color:var(--dsw-alias-label-primary,#24292f)}",
".pa-tbl tbody tr:last-child td{border-bottom:none}",
".pa-tbl tbody tr:hover{background:rgba(47,111,237,.06)}",
".pa-tbl td.pa-num{text-align:right;font-size:13px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-tbl td.pa-dim{color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:13px}",
".pa-badge.admin{background:rgba(47,111,237,.10);color:var(--dsw-alias-brand-primary,#2f6fed)}",
".pa-badge.active{background:rgba(26,127,55,.10);color:#1a7f37}",
".pa-badge.pending{background:rgba(232,89,12,.10);color:#e8590c}",
".pa-badge.disabled{background:rgba(107,114,128,.10);color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-sm{display:inline-flex;align-items:center;padding:3px 10px;font-size:14px;border-radius:6px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,#e3e6ea);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#24292f);transition:all .15s}",
".pa-sm:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-brand-primary,#2f6fed)}",
".pa-sm.danger{color:#d93026}",
".pa-sm.danger:hover:not(:disabled){border-color:#d93026;color:#d93026}",
".pa-sm:disabled{opacity:.5;cursor:default}",
".pa-tbl td.pa-empty-cell{text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280);padding:40px 10px}",
".pa-card-h{display:flex;align-items:baseline;gap:10px;font-size:15px;font-weight:600;margin:0 0 12px}",
".pa-card-h .sub{font-size:13px;font-weight:400;color:var(--dsw-alias-label-tertiary,#6b7280)}",
".pa-overlay{position:fixed;inset:0;z-index:9999;background:rgba(15,28,51,.35);display:flex;align-items:center;justify-content:center;padding:24px}",
".pa-modal{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e3e6ea);border-radius:10px;box-shadow:0 18px 50px -18px rgba(9,24,58,.35);animation:paIn .18s cubic-bezier(.16,1,.3,1)}",
"@keyframes paIn{from{opacity:0;transform:translateY(6px) scale(.985)}to{opacity:1;transform:none}}",
"@media (prefers-reduced-motion: reduce){.pa-card,.pa-modal{animation:none;transition:none}.pa-card:hover{transform:none}}"
].join("");
document.head.appendChild(el);
return function () { el.remove(); };
@@ -1072,16 +1128,17 @@ window.__ModuleLoader__.load({
}).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" };
var wrap = { padding: "4px 2px", display: "flex", flexDirection: "column", gap: "12px" };
// 加载 / 失败 / 非管理员三种态也走门户排版(.page-title → .pa-hd、.page-sub → .pa-sub、.btn-sm → .pa-sm)
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") }) ] });
jsxRuntime.jsx("h1", { key: "h", className: "pa-hd", children: t("pa.loading") }),
jsxRuntime.jsx("p", { key: "d", className: "pa-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") }) ] });
jsxRuntime.jsx("h1", { key: "h", className: "pa-hd", style: { color: T.danger }, children: s.err }),
jsxRuntime.jsx("div", { key: "b", children: jsxRuntime.jsx("button", { type: "button", onClick: load, className: "pa-sm", 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") }) ] });
jsxRuntime.jsx("h1", { key: "h", className: "pa-hd", children: t("pa.notAdminTitle") }),
jsxRuntime.jsx("p", { key: "d", className: "pa-sub", children: t("pa.notAdminDesc") }) ] });
var inst = s.status && s.status.running ? t("pa.running") : t("pa.stopped");
var brk = s.status && s.status.breaker;
@@ -1092,111 +1149,136 @@ window.__ModuleLoader__.load({
var rt = (s.runtime && (s.runtime.items || s.runtime.groups)) || [];
// ── 管理项清单:每项点开都是「弹窗内管理」(用户 2026-09-13 要求)
// 卡片取值口径与门户 `renderHome()` 的 sec2 一致:图标 + 标题 + 一句说明(+ 计数胶囊)
var ITEMS = [
{ id: "users", icon: "👥", title: t("pa.usersTitle"), desc: t("pa.pending") + " " + nPending + " · " + t("pa.disabled") + " " + nDisabled, badge: userList.length },
{ id: "plugins", icon: "🧩", title: t("pa.pool"), desc: t("pa.poolCount"), badge: pool.length },
{ id: "runtime", icon: "📦", title: t("pa.runtime"), desc: t("pa.items"), badge: rt.length },
{ id: "storage", icon: "💾", title: t("pa.storage"), desc: t("pa.storageUsers"), badge: stUsers.length },
{ id: "instance", icon: "🖥️", title: t("pa.inst"), desc: inst + (brk ? " · " + t("pa.breakerOpen") : ""), badge: s.status && s.status.instance ? s.status.instance.port : "—" },
{ id: "users", icon: "👥", title: t("pa.usersTitle"), count: userList.length, desc: t("pa.pending") + " " + nPending + " · " + t("pa.disabled") + " " + nDisabled },
{ id: "plugins", icon: "🧩", title: t("pa.pool"), count: pool.length, desc: t("pa.poolCount") },
{ id: "runtime", icon: "📦", title: t("pa.runtime"), count: rt.length, desc: t("pa.items") },
{ id: "storage", icon: "💾", title: t("pa.storage"), count: stUsers.length, desc: t("pa.storageUsers") },
{ id: "instance", icon: "🖥️", title: t("pa.inst"), count: null, desc: inst + (brk ? " · " + t("pa.breakerOpen") : "") + " · " + t("pa.port") + " " + ((s.status && s.status.instance && s.status.instance.port) || "—") },
];
// 卡片(06-工作台UI规范 §4.5 .data-card:r12 / 16px 18px / shadow / hover 上浮 + 蓝边 + 蓝影)
// 卡片 = 门户 `.nav-card`(ic 一行 / t 一行 / d 一行;计数借门户 `.pg-tab .pg-cnt` 的胶囊)
function itemCard(it) {
return jsxRuntime.jsx("div", {
key: it.id, role: "button", tabIndex: 0, className: "pa-card",
onClick: function () { setNote(""); setModal(it.id); },
onClick: function () { setNote(""); setNoteErr(false); setModal(it.id); },
onKeyDown: function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setModal(it.id); } },
style: {
border: "1px solid " + T.border, borderRadius: "12px", background: T.field,
padding: "16px 18px", cursor: "pointer", display: "flex", flexDirection: "column",
gap: "6px", minHeight: "104px", boxShadow: "0 1px 3px rgba(0,0,0,.08)"
},
children: [
jsxRuntime.jsx("div", { key: "top", style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
jsxRuntime.jsx("span", { key: "i", style: { fontSize: "16px", lineHeight: 1 }, children: it.icon }),
jsxRuntime.jsx("span", { key: "t", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: it.title }),
jsxRuntime.jsx("span", { key: "g", className: "pa-go", style: { marginLeft: "auto", fontSize: "14px", color: T.dim }, children: "›" })
jsxRuntime.jsx("div", { key: "i", className: "pa-ic", children: it.icon }),
jsxRuntime.jsx("div", { key: "t", className: "pa-t", children: [
jsxRuntime.jsx("span", { key: "l", children: it.title }),
it.count == null ? null : jsxRuntime.jsx("span", { key: "c", className: "pa-cnt", children: it.count })
] }),
jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.dim, lineHeight: 1.6 }, children: it.desc }),
jsxRuntime.jsx("div", { key: "n", style: { marginTop: "auto", fontSize: "17px", fontWeight: 500, color: T.text, textAlign: "right", fontVariantNumeric: "tabular-nums" }, children: it.badge })
jsxRuntime.jsx("div", { key: "d", className: "pa-d", children: it.desc })
]
});
}
// 弹窗正文统一 = 门户 `.table-wrap` + `table.tbl`
// (表头灰底 8px 10px/600、单元格 7px 10px、行 hover 浅蓝、末行去下边框;
// 空态对齐门户 `emptyRow()`:居中 + 40px 上下留白)
function tbl(head, bodyRows, emptyText) {
return jsxRuntime.jsx("div", { className: "pa-wrap", children:
jsxRuntime.jsx("table", { className: "pa-tbl", children: [
jsxRuntime.jsx("thead", { key: "h", children: jsxRuntime.jsx("tr", { children: head.map(function (h, i) {
return jsxRuntime.jsx("th", { key: i, children: h });
}) }) }),
jsxRuntime.jsx("tbody", { key: "b", children: bodyRows.length === 0
? jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { colSpan: head.length, className: "pa-empty-cell", children: emptyText || t("pa.empty") }) })
: bodyRows })
] }) });
}
// 两列「项目 / 值」表:当前实例这类键值信息用(仍走门户表格样式)
function kvTbl(pairs) {
return tbl([t("pa.colItem"), t("pa.colValue")], pairs.map(function (r, i) {
return jsxRuntime.jsx("tr", { key: i, children: [
jsxRuntime.jsx("td", { key: "k", className: "pa-dim", children: r[0] }),
jsxRuntime.jsx("td", { key: "v", children: r[1] })
] });
}));
}
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
] });
return tbl([t("pa.colUser"), t("pa.colRole"), t("pa.colOp")], userList.map(function (u) {
var roleCls = u.role === "admin" ? "admin" : u.role === "pending" ? "pending" : u.role === "disabled" ? "disabled" : "active";
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, "danger"]);
return jsxRuntime.jsx("tr", { key: u.id, children: [
jsxRuntime.jsx("td", { key: "n", children: u.username }),
jsxRuntime.jsx("td", { key: "r", children: jsxRuntime.jsx("span", { className: "pa-badge " + roleCls, children: roleText }) }),
jsxRuntime.jsx("td", { key: "o", children: ops.map(function (o) {
return jsxRuntime.jsx("button", { key: o[0], type: "button", disabled: busy, className: "pa-sm" + (o[4] ? " " + o[4] : ""),
style: { marginRight: "6px" },
onClick: function () { doUserAction(u, o[3], o[2], o[1]); },
children: o[1] });
}) })
] });
}), t("pa.noUsers"));
}
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 : "") });
}) });
return tbl([t("pa.colName"), t("pa.colVer")], pool.map(function (p, i) {
return jsxRuntime.jsx("tr", { key: (p.id || i), children: [
jsxRuntime.jsx("td", { key: "n", children: p.name || p.id || "—" }),
jsxRuntime.jsx("td", { key: "v", className: "pa-dim", children: 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 : "") });
}) });
return tbl([t("pa.colItem"), t("pa.colVer")], rt.map(function (x, i) {
return jsxRuntime.jsx("tr", { key: i, children: [
jsxRuntime.jsx("td", { key: "n", children: x.name || x.title || "—" }),
jsxRuntime.jsx("td", { key: "v", className: "pa-dim", children: 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" : "") }); }) });
return tbl([t("pa.colUser"), t("pa.colUsed")], stUsers.map(function (u, i) {
return jsxRuntime.jsx("tr", { key: i, children: [
jsxRuntime.jsx("td", { key: "n", children: u.username || u.id || "—" }),
jsxRuntime.jsx("td", { key: "u", className: "pa-num", children: u.usedMb != null ? u.usedMb + " MiB" : "—" })
] });
}), (s.storage && s.storage.note) || t("pa.empty"));
}
// 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")) }) ] });
return kvTbl([
[t("pa.inst"), inst],
[t("pa.port"), (i2.instance && i2.instance.port) || "—"],
[t("pa.restarts"), (i2.instance && i2.instance.restarts) || 0],
[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: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(158px, 1fr))", gap: "12px" }, children: ITEMS.map(itemCard) }),
jsxRuntime.jsx("h1", { key: "hd", className: "pa-hd", children: t("pa.label") }),
jsxRuntime.jsx("p", { key: "sub", className: "pa-sub", children: t("pa.readonlyHint") }),
jsxRuntime.jsx("div", { key: "sec", className: "pa-sec", children: t("pa.detail") }),
jsxRuntime.jsx("div", { key: "items", className: "pa-grid", children: ITEMS.map(itemCard) }),
];
if (modal) {
var it = ITEMS.filter(function (x) { return x.id === modal; })[0] || { title: modal };
rows.push(jsxRuntime.jsx("div", {
key: "overlay",
key: "overlay", className: "pa-overlay",
onClick: function () { setModal(null); },
style: { position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,.35)", display: "flex", alignItems: "center", justifyContent: "center", padding: "24px" },
className: "pa-modal",
children: jsxRuntime.jsx("div", {
children: jsxRuntime.jsx("div", {
className: "pa-modal",
onClick: function (e) { e.stopPropagation(); },
style: { background: T.text === "" ? "#fff" : ("var(--dsw-alias-bg-layer-1, #ffffff)"), border: "1px solid " + T.border, borderRadius: "14px", width: "520px", maxWidth: "92vw", maxHeight: "80vh", overflow: "auto", padding: "22px 26px" },
style: { width: "560px", maxWidth: "92vw", maxHeight: "80vh", overflow: "auto", padding: "18px" },
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()
// 弹窗头 = 门户 `.card-h`(标题 15-600;右上角关闭按钮用门户 `.btn-sm`)
jsxRuntime.jsx("div", { key: "mh", className: "pa-card-h", children: [
jsxRuntime.jsx("span", { key: "t", children: it.title }),
jsxRuntime.jsx("span", { key: "g", style: { marginLeft: "auto" }, children:
jsxRuntime.jsx("button", { type: "button", className: "pa-sm", onClick: function () { setModal(null); }, children: t("pa.close") }) })
] }),
modalBody(),
note ? jsxRuntime.jsx("p", { key: "n", className: "pa-sub", style: { margin: "12px 0 0", color: noteErr ? T.danger : T.success }, children: note }) : null
]
})
}));
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@dsh-local/business-plugins",
"version": "0.3.2",
"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)。",
"version": "0.3.3",
"description": "功能管理(原「功能插件」)section for dsh web profile — v0.3.3(2026-09-13):「系统管理」全面改为**照抄门户 web/portal.html 的实际组件**(用户反馈「点击卡片后的弹窗样式还是没变,要用之前门户的里面的对应样式」)—— 卡片=.nav-card(r10 / 18px 16px / hover 上浮 -2px + 蓝边 + 0 4px 12px rgba(47,111,237,.12))、计数=.pg-tab .pg-cnt 胶囊、**5 个弹窗全部**改为 .table-wrap + table.tbl(th 灰底 8px 10px/600、td 7px 10px、行 hover 浅蓝、空态居中 40px)、徽章=.badge 四色、按钮=.btn-sm(danger 红字红边)、弹窗外壳=r10 白底 + 门户家族阴影、标题/副标题=.page-title/.page-sub。⚠️ 上一版只有「用户管理」一个弹窗是表格,另外 4 个仍是裸 div 行 —— 这是「看着没变」的真因。|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",
"exports": {
+53 -1
View File
@@ -72,7 +72,7 @@ await new Promise((r) => setTimeout(r, 80));
console.log(" 已注册 section:", regs.map(r => r.meta.id + " / order=" + r.meta.order + " / label=" + r.meta.label()).join(" | "));
function text(n, out = [], depth = 0) {
if (n == null || depth > 12) return out;
if (n == null || depth > 20) return out; // 表格是「容器>table>tbody>tr>td>文本」6 层,旧上限 12 会把行文本截掉
if (typeof n === "string" || typeof n === "number") { out.push(String(n)); return out; }
if (Array.isArray(n)) { n.forEach(x => text(x, out, depth + 1)); return out; }
if (typeof n === "object") {
@@ -87,6 +87,28 @@ async function render(Comp) {
for (let i = 0; i < 6; i++) { await new Promise(r => setTimeout(r, 30)); if (!dirty) break; dirty = false; cursor = 0; tree = Comp(); }
return tree;
}
/** 首帧渲染完成后改一个 state(patch 直接写 slots),再渲染一次 —— 用来把弹窗「点开」。 */
async function renderWith(Comp, patch) {
let tree = await render(Comp);
patch();
cursor = 0; dirty = false;
tree = Comp();
for (let i = 0; i < 6; i++) { await new Promise(r => setTimeout(r, 30)); if (!dirty) break; dirty = false; cursor = 0; tree = Comp(); }
return tree;
}
/** 收集整棵树上的 className(用来断言「用的是门户那套 class」) */
function classes(n, out = [], depth = 0) {
if (n == null || depth > 14) return out;
if (Array.isArray(n)) { n.forEach(x => classes(x, out, depth + 1)); return out; }
if (typeof n === "object") {
if (typeof n.type === "function") { classes(n.type(n.props || {}), out, depth + 1); return out; }
if (n.props) {
if (typeof n.props.className === "string") n.props.className.split(/\s+/).forEach(c => { if (c) out.push(c); });
classes(n.props.children, out, depth + 1);
}
}
return out;
}
for (const { meta, Comp } of regs.filter(r => r.meta.id === "platform-admin")) {
for (const role of ["admin", "active"]) {
ROLE = role;
@@ -125,5 +147,35 @@ ok("非 admin:不出现任何管理项", !["候选池","运行时"].some(k =>
"-> " + regs.map(r => r.meta.id).join(", "));
regs = []; ROLE = "admin";
}
// ── 视觉层(2026-09-13 第 2 轮):卡片网格 + **5 个弹窗全部**走门户 `portal.html` 的表格样式 ──
{
ROLE = "admin";
const clsHome = classes(await render(pa.Comp));
ok("首页:卡片网格取门户 .nav-grid 值", clsHome.includes("pa-grid"));
ok("首页:卡片取门户 .nav-card 值(.pa-card)", clsHome.includes("pa-card"));
ok("首页:计数胶囊取门户 .pg-cnt 值(.pa-cnt)", clsHome.includes("pa-cnt"));
ok("首页:标题/副标题取门户 .page-title/.page-sub 值", clsHome.includes("pa-hd") && clsHome.includes("pa-sub"));
// 逐个点开 5 个管理项,**每一个**都必须是「门户 .table-wrap + table.tbl」而不是裸 div 列表
const ITEM_IDS = ["users", "plugins", "runtime", "storage", "instance"];
const bad = [];
for (const which of ITEM_IDS) {
const cls = classes(await renderWith(pa.Comp, () => { slots[2] = which; }));
const good = cls.includes("pa-overlay") && cls.includes("pa-modal") && cls.includes("pa-wrap") && cls.includes("pa-tbl")
&& cls.includes("pa-card-h") && cls.includes("pa-sm");
if (!good) bad.push(which + "(" + cls.filter(c => c.startsWith("pa-")).join("/") + ")");
}
ok("5 个弹窗全部 = 门户弹窗外壳 + portal 表格 + .btn-sm", bad.length === 0, bad.length ? "-> " + bad.join(" ; ") : "-> users/plugins/runtime/storage/instance 均通过");
// 用户弹窗:角色徽章 + 危险操作按钮(门户 .badge / .btn-sm.danger)
const clsUsers = classes(await renderWith(pa.Comp, () => { slots[2] = "users"; }));
ok("用户弹窗:角色用门户 .badge 四色", clsUsers.filter(c => c === "pa-badge").length >= 4, "-> " + clsUsers.filter(c => c === "pa-badge").length + " 枚");
ok("用户弹窗:删除按钮用门户 .btn-sm.danger", clsUsers.includes("danger"));
// 实例弹窗:键值对表格,值列不是裸文本
const tInst = text(await renderWith(pa.Comp, () => { slots[2] = "instance"; })).join(" | ");
ok("实例弹窗:键值齐全", ["端口", "自动重启", "熔断"].every(k => tInst.includes(k)));
slots[2] = null; cursor = 0; dirty = false;
}
console.log(failed === 0 ? "\n结论:全绿 ✅" : "\n结论:有 " + failed + " 项失败 ❌");
process.exit(failed === 0 ? 0 : 1);