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:
1 parent
ac67db7702
commit
b9540a8536
3 files changed
+230
-96
No files matched your search
@@ -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
|
||||
]
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user