diff --git a/poc/business-plugins/lib/client.js b/poc/business-plugins/lib/client.js index 4b78f68..965ff85 100644 --- a/poc/business-plugins/lib/client.js +++ b/poc/business-plugins/lib/client.js @@ -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 ] }) })); diff --git a/poc/business-plugins/package.json b/poc/business-plugins/package.json index 018ae73..b9d9d8e 100644 --- a/poc/business-plugins/package.json +++ b/poc/business-plugins/package.json @@ -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": { diff --git a/scripts/verify-platform-admin-section.mjs b/scripts/verify-platform-admin-section.mjs index af9d859..fe61fcb 100644 --- a/scripts/verify-platform-admin-section.mjs +++ b/scripts/verify-platform-admin-section.mjs @@ -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);