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

+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);