// @dsh-local/business-plugins — client half (dsh 0.1.2-rc.1, v0.2.4) // // dsh client bundles 以普通脚本形式执行,向 window.__ModuleLoader__ 注册 factory; // 首次 import 时惰性物化 factory(require) -> exports。require 解析平台 seed 词 // (react/jsx-runtime)与图行(其他 dsh.client 包)。**严格镜像官方 client bundle // 的 CJS-style factory 形态,只导出 apply + inject,绝不 exports.default**(R3: // exports.default 会让 loader 的 ESM/CJS interop 把 bare apply FUNCTION 当作插件体, // 无 inject 声明点 → ctx.slots 抛 cannot get property "slots" without inject)。 // // v0.2.0(档案 37 · 批次 3): // · 配色全部改用官方 design token `--dsw-*`(原来硬编码深色)→ **自动跟随 dsh 主题** // (亮/暗/第三方主题都适配),不再需要自己实现主题切换。 // · 文案走官方 locale:ctx.locale.register(NS,{zh,en}) + ctx.locale.bind(NS) → 中英双语。 // · 交互:搜索过滤 + 每行状态徽章(已启用/未启用)+ **被自动禁用插件的行内提示与重试** // + 空态/加载态。 // · 应用成功且实例重启后,**自动探活并刷新页面**(重启会换端口,刷新即可接上新实例)。 // // v0.2.4(档案 67 · 对齐 06-工作台UI规范.md): // · **信息层次反转**:主视觉改为插件的**用途说明**(候选池入库时已优先取官方目录的中文), // 插件名 / 版本退为副行小字 —— 与门户 plugins 页同一决策(「一眼知道这插件干什么」)。 // · 字号对齐规范 §2.2 阶梯(section 语境取 14 / 13 / 12,不照搬页面级 16px 基准); // 徽章改 `2px 8px` r10、按钮改 `.btn-sm` 量级、行距与内边距按规范 §2.4 序列。 // · 空态/加载态按规范 §4.9 改为「标题 + 说明」层次,不再是一行灰字。 // · 新增注入式 CSS 提供行 hover 反馈与按钮 hover(规范 §4.3 / §5);disposer 随 fiber 移除。 // // v0.2.5(档案 67 补 · 按 `impeccable` 的 craft-floor 复核后修正;检测器未随技能包提供,人工过检): // · **去掉 rejected 提示的彩色 `border-left`** —— craft-floor 明令:卡片/列表项/提示上 // 不得出现 >1px 的彩色 border-left/right。改为整块浅底 + 圆角,危险语义由标题文字色承担。 // · **对比度**:所有必读文字从 `--dsw-alias-label-tertiary` 提到 `secondary` // (tertiary 在亮色下约 3.5:1,低于 craft-floor 的 4.5:1 底线)。 // · **层次**:主视觉加 `fontWeight: 500`,与 12px 副行拉开 weight 台阶(原先只差 2px 字号, // 属于 craft-floor 点名的「scale and weight steps 不明显」)。 // · **动效**:transition 由 `.15s ease` 改为 `.18s cubic-bezier(.16,1,.3,1)`(exponential ease-out)。 // // v0.2.7(档案 75 维度 B 落地 · 2026-09-13 用户要求): // · **卡片加内存预估**:每张卡片多一行「预估内存 ≈ N MiB」小徽章,卡片加高(minHeight 112) // 让「用途说明 / 包名 / 预估内存 / 状态徽章」四层排得开。 // · **列表上方新增「内存预估」状态条**:实心段 = 当前已启用插件的预估占用, // 半透明段 = 本次勾选带来的增量;超过单实例上限(384 MiB)时整条转红并提示。 // · **口径**:`MEM_BASE_MIB`(-dsh 本体+官方插件基线) + `MEM_TABLE`(逐插件实测加载成本); // **是预估值,不是实时读数**(客户端拿不到 cgroup 实测值,需平台加只读路由才能做实时 —— 见档案 75)。 // // 功能:在 dsh 设置面板注册「功能管理」section —— 列出候选池 + 每个插件启用状态, // 批量勾选后点「应用更改」,确认弹窗(提示重启会中断会话)→ POST 门户 // /api/plugins/mine/apply(跨子域 fetch,credentials include;门户已加 CORS)。 // 门户端启用=复制 bundle 到该用户 profile 并加入 bundles,禁用=从 bundles 移除, // 然后重启该用户实例生效;**启用会让实例起不来的插件会被门户单独摘掉并标记**。 window.__ModuleLoader__.load({ id: "@dsh-local/business-plugins", factory: (require) => { var module = { exports: {} }; var exports = module.exports; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); var jsxRuntime = require("react/jsx-runtime"); var React = require("react"); // 门户主域:去掉实例子域 label(admin.alotbuy.com -> alotbuy.com)。 function portalHost() { try { var labels = window.location.hostname.split("."); if (labels.length > 2) { return window.location.protocol + "//" + labels.slice(1).join("."); } } catch (e) {} return window.location.origin; } // 官方 design token(dsh 主题变量)。刻意**不硬编码颜色**:dsw-* 由主题提供, // 亮/暗/第三方主题自动适配;fallback 仅在该 token 缺失时兜底。 // 注:06-工作台UI规范 §2.1 给的是亮色基线色值,本 section 嵌在 dsh 面板内, // 跟随宿主主题优先,故取 token + 规范色值作 fallback。 var T = { text: "var(--dsw-alias-label-primary, #24292f)", sub: "var(--dsw-alias-label-secondary, #4e5969)", dim: "var(--dsw-alias-label-tertiary, #6b7280)", border: "var(--dsw-alias-border-l2, #e3e6ea)", field: "var(--dsw-alias-bg-layer-3, #f5f6f8)", primary: "var(--dsw-alias-brand-primary, #2f6fed)", danger: "var(--dsw-alias-state-error-primary, #d93026)", warn: "var(--dsw-alias-state-warning-primary, #bf8700)", success: "var(--dsw-alias-state-success-primary, #1a7f37)" }; // ── 内存预估模型(档案 75「维度 B 资源成本」的前端落地)──────────────────── // 口径:**预估值** = 空载基线 + Σ(已启用插件的加载成本)。客户端拿不到 cgroup 实测值, // 因此这里是估算而非实时读数;数值来源 = 2026-09-13 隔离 cgroup 实测 //(`node --expose-gc` 逐项 import 的 rss 增量,见 `.workbuddy/memory/2026-09-13.md`)。 /** 单实例 cgroup 上限(MiB)—— 档案 58 定档;改它必须同时改编排器的 `MemoryMax`。 */ var MEM_LIMIT_MIB = 384; /** 空载基线(MiB):dsh 本体 + 148 个官方插件 + 平台自研 ×3(smaps 实测 ≈ 285)。 */ var MEM_BASE_MIB = 285; /** 逐插件加载成本(MiB,实测 rss 增量)。不在此表的按 MEM_FALLBACK_MIB 计。 */ var MEM_TABLE = { "dsh-univer-office": 64, "dsh-plugin-mcn-suite": 39 }; /** 未实测插件的兜底值 —— 平台自研轻量插件实测合计仅 1.7 MiB。 */ var MEM_FALLBACK_MIB = 2; /** 越过该比例即提前警示,给页缓存与用户自己的任务留余量。 */ var MEM_TIGHT_RATIO = 0.85; function memMiB(p) { if (p && p.name && Object.prototype.hasOwnProperty.call(MEM_TABLE, p.name)) { return MEM_TABLE[p.name]; } return MEM_FALLBACK_MIB; } /** 把一组插件里 `key` 为真的那些的预估内存求和。 */ function sumMiB(list, key) { var total = 0; for (var i = 0; i < list.length; i++) { if (list[i][key]) total += memMiB(list[i]); } return total; } /** 本插件的 locale 命名空间(官方 i18n)。 */ var NS = "business-plugins"; /** 简体中文字典(key 集的事实来源)。 */ var zh = { "section.label": "功能管理", "loading": "加载中…", "loadingDesc": "正在从门户读取候选池…", "emptyTitle": "暂无功能插件", "empty": "需管理员先在门户「插件管理」里投放,之后可在此启用", "noMatch": "没有匹配的插件", "search": "搜索插件名或用途", "selectAll": "全选", "clear": "清空", "enabled": "已启用", "disabled": "未启用", "selected": "已选", "apply": "应用更改", "applying": "应用中…", "confirm.title": "确认应用更改?", "confirm": "将重启当前 dsh 实例以生效。重启会中断当前会话(正在进行的对话会断开)。", "confirm.cancel": "取消", "confirm.ok": "确认应用", "confirm.overTitle": "⚠ 本次勾选将超出单实例内存上限", "nochange": "没有需要更改的插件", "queued": "排队中…", "installing": "正在安装", "configuring": "正在配置", "restarting": "正在重启实例", "probing": "正在检查实例", "isolating": "正在定位问题插件", "applied": "✓ 已应用", "reloading": "实例重启中,页面将自动刷新…", "rejected.title": "以下插件与实例不兼容,已自动禁用", "retry": "重试", "requestFailed": "请求失败", "loadFailed": "加载失败", "mem.title": "内存预估", "mem.est": "预估(实测基准,非实时读数)", "mem.now": "当前", "mem.planned": "勾选后", "mem.limit": "上限", "mem.left": "剩余", "mem.over": "将超出上限", "mem.tight": "接近上限", "mem.safe": "余量充足", "mem.overWarn": "勾选后将超出单实例内存上限,实例可能起不来。建议先停用部分插件再应用。", "mem.perCard": "预估内存", "pa.label": "系统管理", "pa.readonly": "只读", "pa.loading": "加载中…", "pa.loadingDesc": "正在读取平台状态…", "pa.notAdminTitle": "系统管理仅对管理员显示", "pa.notAdminDesc": "当前账号不是管理员,此处不展示平台信息。", "pa.loadFailed": "平台状态读取失败", "pa.retry": "重试", "pa.openPortal": "打开完整管理台", "pa.readonlyHint": "点开每一项,在弹窗内直接管理(无需跳转门户)。", "pa.inst": "当前实例", "pa.running": "运行中", "pa.stopped": "未运行", "pa.port": "端口", "pa.restarts": "自动重启", "pa.breaker": "熔断", "pa.breakerOpen": "已熔断(冷却中)", "pa.breakerOk": "正常", "pa.users": "用户", "pa.total": "合计", "pa.pending": "待审核", "pa.disabled": "已禁用", "pa.storage": "存储", "pa.storageUsers": "有用量的用户", "pa.storageNote": "报告每小时刷新", "pa.pool": "候选池", "pa.poolCount": "已投放插件", "pa.runtime": "运行时", "pa.items": "条目", "pa.usersTitle": "用户管理", "pa.colUser": "用户名", "pa.colRole": "角色", "pa.colOp": "操作", "pa.roleAdmin": "管理员", "pa.roleActive": "已启用", "pa.rolePending": "待审核", "pa.roleDisabled": "已禁用", "pa.opApprove": "通过", "pa.opDisable": "禁用", "pa.opEnable": "启用", "pa.opRemove": "删除", "pa.confirmRemove": "确认删除该用户?此操作不可撤销。", "pa.done": "已更新", "pa.failed": "操作失败", "pa.noUsers": "暂无用户", "pa.working": "处理中…" }; /** English dictionary, key-set complete against zh. */ var en = { "section.label": "Feature management", "loading": "Loading…", "loadingDesc": "Reading the candidate pool from the portal…", "emptyTitle": "No feature plugins yet", "empty": "An admin must publish them in the portal's plugin management first", "noMatch": "No matching plugins", "search": "Search by name or purpose", "selectAll": "Select all", "clear": "Clear", "enabled": "Enabled", "disabled": "Disabled", "selected": "selected", "apply": "Apply changes", "applying": "Applying…", "confirm.title": "Apply the changes?", "confirm": "The dsh instance will restart, which interrupts the current session (any running conversation is disconnected).", "confirm.cancel": "Cancel", "confirm.ok": "Apply", "confirm.overTitle": "⚠ This selection exceeds the per-instance memory limit", "nochange": "No changes to apply", "queued": "Queued…", "installing": "Installing", "configuring": "Configuring", "restarting": "Restarting instance", "probing": "Checking instance health", "isolating": "Locating the failing plugin", "applied": "✓ Applied", "reloading": "Instance is restarting — the page will refresh automatically…", "rejected.title": "These plugins are incompatible with the instance and were disabled automatically", "retry": "Retry", "requestFailed": "Request failed", "loadFailed": "Failed to load", "mem.title": "Memory estimate", "mem.est": "estimate from measured baselines, not a live reading", "mem.now": "now", "mem.planned": "after selection", "mem.limit": "limit", "mem.left": "left", "mem.over": "over the limit", "mem.tight": "close to the limit", "mem.safe": "headroom is fine", "mem.overWarn": "The selection exceeds the per-instance memory limit and the instance may fail to start. Disable some plugins first.", "mem.perCard": "est. memory", "pa.label": "System management", "pa.readonly": "read-only", "pa.loading": "Loading…", "pa.loadingDesc": "Reading platform status…", "pa.notAdminTitle": "System management is admin-only", "pa.notAdminDesc": "This account is not an admin, so platform info is hidden here.", "pa.loadFailed": "Failed to read platform status", "pa.retry": "Retry", "pa.openPortal": "Open full admin console", "pa.readonlyHint": "Open any item to manage it in a popup — no portal jump.", "pa.inst": "Current instance", "pa.running": "Running", "pa.stopped": "Stopped", "pa.port": "Port", "pa.restarts": "Auto-restarts", "pa.breaker": "Circuit breaker", "pa.breakerOpen": "Open (cooling down)", "pa.breakerOk": "OK", "pa.users": "Users", "pa.total": "Total", "pa.pending": "Pending", "pa.disabled": "Disabled", "pa.storage": "Storage", "pa.storageUsers": "Users with usage", "pa.storageNote": "report refreshes hourly", "pa.pool": "Candidate pool", "pa.poolCount": "Published plugins", "pa.runtime": "Runtime", "pa.items": "items", "pa.usersTitle": "User management", "pa.colUser": "Username", "pa.colRole": "Role", "pa.colOp": "Actions", "pa.roleAdmin": "Admin", "pa.roleActive": "Active", "pa.rolePending": "Pending", "pa.roleDisabled": "Disabled", "pa.opApprove": "Approve", "pa.opDisable": "Disable", "pa.opEnable": "Enable", "pa.opRemove": "Delete", "pa.confirmRemove": "Delete this user? This cannot be undone.", "pa.done": "Updated", "pa.failed": "Action failed", "pa.noUsers": "No users", "pa.working": "Working…" }; /** * Required services (cordis fiber inject). `locale` is required for the * dictionaries; the slot registry for the settings section. */ var inject = ["slots", "locale"]; /** 应用成功后探活并刷新(重启会换端口;探通了再 reload,避免白屏)。 */ function waitAndReload(attempts) { if (attempts <= 0) { window.location.reload(); return; } fetch(window.location.origin + "/", { cache: "no-store" }) .then(function (r) { if (r.ok) window.location.reload(); else setTimeout(function () { waitAndReload(attempts - 1); }, 1500); }) .catch(function () { setTimeout(function () { waitAndReload(attempts - 1); }, 1500); }); } function apply(ctx) { ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "business-plugins: dictionaries"); // 内联样式表达不了 `:hover`,而 06-工作台UI规范要求「列表行 hover 反馈」(§4.3) // 与「hover 才提示可点」(§5)。这里注入一小段 CSS,disposer 随 fiber 移除 —— // 不引外部样式表,也不污染宿主全局(选择器统一带 bp- 前缀)。 ctx.effect(function () { var el = document.createElement("style"); el.setAttribute("data-bp-style", "1"); el.textContent = [ ".bp-row{transition:background .18s cubic-bezier(.16,1,.3,1),border-color .18s cubic-bezier(.16,1,.3,1),box-shadow .18s cubic-bezier(.16,1,.3,1)}", ".bp-row:hover{border-color:var(--dsw-alias-brand-primary, #2f6fed);box-shadow:0 4px 12px rgba(47,111,237,.15)}", ".bp-btn{transition:border-color .18s cubic-bezier(.16,1,.3,1),color .18s cubic-bezier(.16,1,.3,1)}", ".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}}" ].join(""); document.head.appendChild(el); return function () { el.remove(); }; }, "business-plugins: styles"); var t = ctx.locale.bind(NS); function BusinessPluginsSection() { var useState = React.useState; var useEffect = React.useEffect; var pluginsState = useState([]); var plugins = pluginsState[0]; var setPlugins = pluginsState[1]; var loadingState = useState(true); var loading = loadingState[0]; var setLoading = loadingState[1]; var busyState = useState(false); var busy = busyState[0]; var setBusy = busyState[1]; var msgState = useState(""); var msg = msgState[0]; var setMsg = msgState[1]; var queryState = useState(""); var query = queryState[0]; var setQuery = queryState[1]; var rejectedState = useState([]); var rejected = rejectedState[0]; var setRejected = rejectedState[1]; // 「应用更改」的页内确认弹窗开关(替代 window.confirm)。 var confirmState = useState(false); var confirmOpen = confirmState[0]; var setConfirmOpen = confirmState[1]; // 「服务端当前已启用」的 id 快照 —— 用于把「当前占用」与「勾选后会有多少」分开算。 // 没有它就没法在用户勾选时显示「增量」(p.enabled 会被 toggle 就地改掉)。 var savedState = useState([]); var savedIds = savedState[0]; var setSavedIds = savedState[1]; function load() { setLoading(true); fetch(portalHost() + "/api/plugins/mine", { credentials: "include" }) .then(function (r) { return r.json(); }) .then(function (d) { var list = d.plugins || []; setPlugins(list); // 记下「服务端认为已启用」的那批 → 内存条据此区分「当前」与「勾选后」。 setSavedIds(list.filter(function (p) { return p.enabled; }).map(function (p) { return p.id; })); setLoading(false); }) .catch(function () { setLoading(false); setMsg(t("loadFailed")); }); } useEffect(function () { load(); }, []); function toggle(id) { setPlugins(plugins.map(function (p) { if (p.id === id) return Object.assign({}, p, { enabled: !p.enabled }); return p; })); } /** * 打开**页内确认弹窗**(不再用 `window.confirm`)。 * * 2026-09-13 用户要求:所有弹窗改成页面弹窗,视觉参考 MCN 工作台 * (`dsh-plugin-mcn` 的 `fixed inset:0` 遮罩 + 居中面板 + 点遮罩关闭 + * `stopPropagation` + ✕,见其 `modalTitle/modalText/modalBtns` 三段结构)。 */ function askApply() { setConfirmOpen(true); } /** 用户已在弹窗里点「确认应用」:真正的提交(原 `applyChanges` 主体)。 */ function doApply() { setConfirmOpen(false); setBusy(true); setMsg(t("queued")); setRejected([]); fetch(portalHost() + "/api/plugins/mine/apply", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ plugins: plugins.map(function (p) { return { id: p.id, enabled: p.enabled }; }) }) }) .then(function (r) { return r.json().catch(function () { return {}; }); }) .then(function (d) { if (d.error) { setBusy(false); setMsg(t("requestFailed") + ":" + d.error); return; } if (d.noop) { setBusy(false); setMsg(t("nochange")); return; } pollTask(d.taskId); }) .catch(function () { setBusy(false); setMsg(t("requestFailed")); }); } // 异步任务:轮询「阶段 + 已用秒数」(B 方案,不滚日志)。 function pollTask(taskId) { var start = Date.now(); var timer = setInterval(function () { fetch(portalHost() + "/api/plugins/mine/task/" + taskId, { credentials: "include" }) .then(function (r) { return r.json().catch(function () { return {}; }); }) .then(function (task) { var secs = Math.floor((Date.now() - start) / 1000); var stage = task.stage || t("configuring"); if (task.status === "pending" || task.status === "running") { setMsg("⏳ " + stage + "(" + secs + "s)"); } else if (task.status === "success") { clearInterval(timer); var bad = task.rejected || []; setRejected(bad); setBusy(false); if (bad.length > 0) { setMsg(t("applied") + " — " + t("rejected.title")); load(); } else { setMsg(t("applied") + " · " + t("reloading")); waitAndReload(3); } } else { clearInterval(timer); setBusy(false); setMsg("✗ " + (task.error || t("requestFailed")) + "(已回滚到改动前的状态)"); // 2026-09-12 修复:失败后必须重新拉取真实状态。否则本地 plugins 仍停留在 // 用户勾选后的样子 → 徽章显示「已启用」,与「安装失败」自相矛盾(用户实测报障)。 // 后端失败分支已 restoreProfile() 回滚,load() 即可取回正确状态。 load(); } }) .catch(function () { /* 网络抖动忽略,继续下一轮 */ }); }, 1500); } // 06-工作台UI规范 §2.2 字号阶梯:section 语境取「正文 14 / 次要 13 / 辅助 12」三档 //(不照搬页面级 16px 基准 —— 本 section 嵌在 dsh 设置面板内,非独立页)。 var wrap = { display: "flex", flexDirection: "column", gap: "10px", padding: "4px 2px", color: T.text, fontSize: "14px" }; /** 规范 §4.9 空态/加载态:应有「标题 + 说明」层次,而非一行灰字。 */ function StateBlock(props) { return jsxRuntime.jsxs("div", { style: { padding: "28px 20px", display: "flex", flexDirection: "column", alignItems: "center", gap: "6px", textAlign: "center" }, children: [ jsxRuntime.jsx("div", { key: "t", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: props.title }), jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 }, children: props.desc }) ] }); } /** * 内存预估状态条(置于列表上方 —— 2026-09-13 用户要求)。 * * 三段视觉:**实心** = 「服务端当前已启用」那批的预估占用;**半透明** = 本次勾选 * 带来的增量;超过单实例上限时整条转红并给出文字警告。全程只读本地数据、不发请求。 */ function MemBar(props) { var limit = props.limit; var nowMiB = props.nowMiB; var plannedMiB = props.plannedMiB; var over = plannedMiB > limit; var tight = !over && plannedMiB > limit * MEM_TIGHT_RATIO; var accent = over ? T.danger : (tight ? T.warn : T.success); var nowPct = Math.max(0, Math.min(100, (nowMiB / limit) * 100)); var planPct = Math.max(0, Math.min(100, (plannedMiB / limit) * 100)); var delta = plannedMiB - nowMiB; var stateText = over ? t("mem.over") : (tight ? t("mem.tight") : t("mem.safe")); var line = t("mem.now") + " " + nowMiB + " MiB"; if (delta !== 0) { line += " → " + t("mem.planned") + " " + plannedMiB + " MiB"; } else { line += " / " + t("mem.limit") + " " + limit + " MiB"; } return jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px", padding: "10px 12px", borderRadius: "10px", background: T.field, border: "1px solid " + (over ? T.danger : T.border) }, children: [ jsxRuntime.jsxs("div", { key: "h", style: { display: "flex", alignItems: "baseline", gap: "8px", flexWrap: "wrap" }, children: [ jsxRuntime.jsx("span", { key: "t", style: { fontSize: "13px", fontWeight: 500, color: T.text }, children: t("mem.title") }), jsxRuntime.jsx("span", { key: "n", style: { fontSize: "13px", color: T.sub, fontVariantNumeric: "tabular-nums" }, children: line }), jsxRuntime.jsx("span", { key: "s", style: { fontSize: "12px", color: accent, marginLeft: "auto", flexShrink: 0 }, children: (over ? "⚠ " : "") + stateText }), jsxRuntime.jsx("span", { key: "e", style: { fontSize: "12px", color: T.dim, flexBasis: "100%" }, children: t("mem.est") }) ] }), jsxRuntime.jsxs("div", { key: "bar", style: { position: "relative", height: "8px", borderRadius: "4px", background: T.border, overflow: "hidden" }, children: [ jsxRuntime.jsx("div", { key: "plan", style: { position: "absolute", left: 0, top: 0, bottom: 0, width: planPct + "%", background: accent, opacity: 0.28 } }), jsxRuntime.jsx("div", { key: "now", style: { position: "absolute", left: 0, top: 0, bottom: 0, width: nowPct + "%", background: accent, opacity: 0.85 } }) ] }), over ? jsxRuntime.jsx("div", { key: "w", style: { fontSize: "12px", color: T.danger, lineHeight: 1.6 }, children: t("mem.overWarn") }) : null ] }); } if (loading) { return jsxRuntime.jsx("div", { style: wrap, children: jsxRuntime.jsx(StateBlock, { title: t("loading"), desc: t("loadingDesc") }) }); } if (plugins.length === 0) { return jsxRuntime.jsx("div", { style: wrap, children: jsxRuntime.jsx(StateBlock, { title: t("emptyTitle"), desc: t("empty") }) }); } var kw = query.trim().toLowerCase(); var shown = kw === "" ? plugins : plugins.filter(function (p) { return (p.name || "").toLowerCase().indexOf(kw) >= 0 || (p.description || "").toLowerCase().indexOf(kw) >= 0; }); var selected = plugins.filter(function (p) { return p.enabled; }).length; // 内存预估:基线 + Σ 插件成本。 // · nowMiB = 服务端快照里「已启用」的那批(savedIds)→ 代表**当前**占用; // · plannedMiB = 当前勾选状态(用户点一下就变)→ 代表**应用后会是多少**。 var nowMiB = MEM_BASE_MIB + plugins.reduce(function (sum, p) { return sum + (savedIds.indexOf(p.id) >= 0 ? memMiB(p) : 0); }, 0); var plannedMiB = MEM_BASE_MIB + sumMiB(plugins, "enabled"); var memOver = plannedMiB > MEM_LIMIT_MIB; var inputStyle = { flex: "1", minWidth: "180px", padding: "6px 10px", borderRadius: "8px", border: "1px solid " + T.border, background: T.field, color: T.text, fontSize: "14px", outline: "none" }; // 规范 §4.1:次要动作用白底 + border(`.btn` / `.btn-sm` 量级),非主色实心。 var ghostBtnStyle = { padding: "5px 12px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.border, background: "transparent", color: T.sub, fontSize: "13px" }; var rows = []; var cards = []; // 内存预估状态条:**卡片列表上方**(2026-09-13 用户要求)—— // 当前占用 / 勾选后预估 / 是否超上限,让用户在点「应用」之前就知道后果。 rows.push(jsxRuntime.jsx(MemBar, { key: "__membar", limit: MEM_LIMIT_MIB, nowMiB: nowMiB, plannedMiB: plannedMiB })); // 工具行:搜索 + 全选/清空 + 计数(规范 §2.4:工具条内部 gap 8~10px) rows.push(jsxRuntime.jsxs("div", { key: "__toolbar", style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }, children: [ jsxRuntime.jsx("input", { key: "q", value: query, placeholder: t("search"), onChange: function (e) { setQuery(e.target.value); }, style: inputStyle }), jsxRuntime.jsx("button", { key: "all", type: "button", className: "bp-btn", onClick: function () { setPlugins(plugins.map(function (p) { return Object.assign({}, p, { enabled: true }); })); }, style: ghostBtnStyle, children: t("selectAll") }), jsxRuntime.jsx("button", { key: "none", type: "button", className: "bp-btn", onClick: function () { setPlugins(plugins.map(function (p) { return Object.assign({}, p, { enabled: false }); })); }, style: ghostBtnStyle, children: t("clear") }), jsxRuntime.jsx("span", { key: "cnt", style: { color: T.sub, fontSize: "13px", marginLeft: "auto" }, children: selected + " / " + plugins.length + " " + t("selected") }) ] })); if (shown.length === 0) { rows.push(jsxRuntime.jsx("div", { key: "__nomatch", style: { padding: "20px 0", textAlign: "center", color: T.sub, fontSize: "13px" }, children: t("noMatch") })); } for (var i = 0; i < shown.length; i++) { (function (p) { var bad = rejected.filter(function (r) { return r.id === p.id; })[0]; var desc = p.description && String(p.description).trim() !== "" ? String(p.description).trim() : ""; // 主视觉:用途说明(候选池入库时已优先取官方目录中文);无说明时回退为包名。 var primary = desc !== "" ? desc : p.name + (p.version ? " v" + p.version : ""); // 副行:包名 + 版本(有说明时才渲染,避免与主视觉重复)。 var meta = p.name + (p.version ? " v" + p.version : ""); cards.push(jsxRuntime.jsxs("label", { key: p.id, className: "bp-row", style: { display: "flex", alignItems: "flex-start", gap: "10px", cursor: "pointer", padding: "14px 16px", borderRadius: "12px", minWidth: 0, minHeight: "112px", background: "var(--dsw-alias-bg-layer-1, #ffffff)", border: "1px solid " + (bad ? T.danger : T.border) }, children: [ jsxRuntime.jsx("input", { key: "cb", type: "checkbox", checked: !!p.enabled, onChange: function () { toggle(p.id); } }), jsxRuntime.jsxs("span", { key: "nm", style: { flex: "1", minWidth: "0", display: "flex", flexDirection: "column", gap: "2px" }, children: [ // 主视觉 = 用途说明(规范 §5「信息层次」;与门户 plugins 页同一决策) jsxRuntime.jsx("span", { key: "p1", style: { fontSize: "14px", fontWeight: 500, color: T.text, lineHeight: 1.45, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, title: primary, children: primary }), // 副行 = 包名 + 版本(长文本截断三件套 + title,规范 §5) desc !== "" ? jsxRuntime.jsx("span", { key: "p2", style: { fontSize: "12px", color: T.sub, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, title: meta, children: meta }) : null, // 预估内存(2026-09-13 用户要求):数值越大越"贵",用文字色分级 // (≥60 MiB 红 / ≥30 MiB 黄 / 其余次要色),一眼能看出谁是大头。 jsxRuntime.jsx("span", { key: "mem", style: { marginTop: "4px" }, children: jsxRuntime.jsx("span", { style: { padding: "1px 8px", borderRadius: "10px", border: "1px solid " + T.border, background: T.field, color: memMiB(p) >= 60 ? T.danger : (memMiB(p) >= 30 ? T.warn : T.sub) }, children: t("mem.perCard") + " ≈ " + memMiB(p) + " MiB" }) }) ] }), jsxRuntime.jsx("span", { key: "bd", style: { fontSize: "12px", padding: "2px 8px", borderRadius: "10px", flexShrink: 0, color: p.enabled ? T.success : T.dim, border: "1px solid " + (p.enabled ? T.success : T.border) }, children: p.enabled ? t("enabled") : t("disabled") }), bad ? jsxRuntime.jsx("span", { key: "bad", style: { fontSize: "12px", color: T.danger, flexShrink: 0 }, title: String(bad.reason || ""), children: "⚠" }) : null ] })); })(shown[i]); } // 卡片网格:一行 2 个(用户 2026-09-12 要求「插件列表改为卡片形式,一行放 2 个卡片」) // 单独包一层 grid —— 不能直接改外层 wrap(它同时装标题/搜索/按钮/提示等纵向块)。 rows.push(jsxRuntime.jsx("div", { key: "__grid", style: { display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: "10px" }, children: cards })); rows.push(jsxRuntime.jsxs("div", { key: "__actions", style: { display: "flex", alignItems: "center", gap: "10px", marginTop: "4px" }, children: [ // 规范 §4.1:主行动用 primary 实心(`.btn` 量级,section 内 14px) jsxRuntime.jsx("button", { key: "apply", type: "button", onClick: askApply, disabled: busy, style: { display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "7px 16px", borderRadius: "8px", border: "1px solid " + T.primary, background: T.primary, color: "#ffffff", cursor: busy ? "default" : "pointer", opacity: busy ? 0.5 : 1, fontSize: "14px" }, children: busy ? t("applying") : t("apply") }), rejected.length > 0 ? jsxRuntime.jsx("button", { key: "retry", type: "button", className: "bp-btn", onClick: askApply, disabled: busy, style: Object.assign({}, ghostBtnStyle, { opacity: busy ? 0.5 : 1 }), children: t("retry") }) : null ] })); if (rejected.length > 0) { // impeccable craft-floor 明令禁止「卡片/列表项/提示上 >1px 的彩色 border-left/right」 // —— 改为整块浅底 + 圆角,危险语义由标题文字色承担(不靠侧边色条)。 rows.push(jsxRuntime.jsxs("div", { key: "__rejected", style: { padding: "10px 12px", borderRadius: "8px", color: T.sub, fontSize: "13px", lineHeight: "1.6", background: T.field }, children: [ jsxRuntime.jsxs("div", { key: "h", style: { fontWeight: 500, color: T.danger, marginBottom: "2px" }, children: ["⚠ ", t("rejected.title")] }), jsxRuntime.jsx("div", { key: "b", children: rejected.map(function (r) { return r.id; }).join("、") }) ] })); } if (msg) { rows.push(jsxRuntime.jsx("div", { key: "__msg", style: { color: T.sub, fontSize: "13px" }, children: msg })); } // ── 页内确认弹窗(替代 window.confirm;视觉对齐 MCN 工作台弹窗)────────── // 结构照 MCN:遮罩(fixed inset:0 + rgba(0,0,0,.35) + zIndex 2000 + 居中)→ 面板 // (radius 12 / padding 18·22 / maxHeight 82vh / 阴影)→ 标题行(✕ 关闭)→ 正文 → // 内存块(**超限时红底 + 警告**,用户 2026-09-13 明确要求)→ 按钮行(取消 / 确认)。 // 点遮罩关闭、点面板 stopPropagation(与 MCN 同款交互)。 if (confirmOpen) { var overNow = plannedMiB > MEM_LIMIT_MIB; rows.push(jsxRuntime.jsxs("div", { key: "__confirm", style: { position: "fixed", inset: 0, background: "rgba(0,0,0,.35)", zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center", padding: "24px" }, onClick: function () { setConfirmOpen(false); }, children: jsxRuntime.jsxs("div", { style: { background: "var(--dsw-alias-bg-layer-1, #ffffff)", borderRadius: "12px", maxWidth: "480px", width: "100%", maxHeight: "82vh", overflow: "auto", padding: "18px 22px", boxShadow: "0 8px 30px rgba(0,0,0,.18)", display: "flex", flexDirection: "column", gap: "10px" }, onClick: function (e) { e.stopPropagation(); }, children: [ jsxRuntime.jsxs("div", { key: "t", style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [ jsxRuntime.jsx("span", { key: "s", style: { fontSize: "15px", fontWeight: 500, color: T.text }, children: t("confirm.title") }), jsxRuntime.jsx("button", { key: "x", type: "button", title: t("confirm.cancel"), onClick: function () { setConfirmOpen(false); }, style: { border: "none", background: "none", fontSize: "18px", lineHeight: 1, cursor: "pointer", color: T.dim, padding: "2px 4px" }, children: "✕" }) ] }), jsxRuntime.jsx("div", { key: "b", style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 }, children: t("confirm") }), jsxRuntime.jsxs("div", { key: "m", style: { padding: "10px 12px", borderRadius: "10px", background: overNow ? "rgba(217,48,38,.06)" : T.field, border: "1px solid " + (overNow ? T.danger : T.border), display: "flex", flexDirection: "column", gap: "4px" }, children: [ jsxRuntime.jsx("div", { key: "h", style: { fontSize: "13px", fontWeight: 500, color: overNow ? T.danger : T.text }, children: overNow ? t("confirm.overTitle") : t("mem.title") }), jsxRuntime.jsx("div", { key: "n", style: { fontSize: "13px", color: T.sub, fontVariantNumeric: "tabular-nums" }, children: t("mem.now") + " " + nowMiB + " MiB → " + t("mem.planned") + " " + plannedMiB + " MiB / " + t("mem.limit") + " " + MEM_LIMIT_MIB + " MiB" }), overNow ? jsxRuntime.jsx("div", { key: "w", style: { fontSize: "12px", color: T.danger, lineHeight: 1.6 }, children: t("mem.overWarn") }) : null, jsxRuntime.jsx("div", { key: "e", style: { fontSize: "12px", color: T.dim }, children: t("mem.est") }) ] }), jsxRuntime.jsxs("div", { key: "f", style: { display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "8px", paddingTop: "10px", borderTop: "1px solid " + T.border }, children: [ jsxRuntime.jsx("button", { key: "c", type: "button", className: "bp-btn", onClick: function () { setConfirmOpen(false); }, style: ghostBtnStyle, children: t("confirm.cancel") }), jsxRuntime.jsx("button", { key: "k", type: "button", onClick: doApply, disabled: busy, style: { padding: "7px 16px", borderRadius: "8px", cursor: busy ? "default" : "pointer", border: "1px solid " + (overNow ? T.danger : T.primary), background: overNow ? T.danger : T.primary, color: "#ffffff", fontSize: "14px", opacity: busy ? 0.5 : 1 }, children: busy ? t("applying") : t("confirm.ok") }) ] }) ] }) })); } return jsxRuntime.jsx("div", { style: wrap, children: rows }); } ctx.slots.inject("settings.section", function () { return ctx.slots.register( { name: "settings.section", id: "business-plugins", order: 101, label: function () { return t("section.label"); } }, BusinessPluginsSection ); }); // ── R2 · 平台管理(只读就地化;档案 81 §9.2b「原生弹窗」形态)──────────────── // 形态:**原生渲染**(非 iframe)——复用本插件的 token 与 06-工作台UI规范 字号阶梯。 // 数据:直接调平台只读 API(跨子域 fetch + credentials,门户已加 CORS 白名单)。 // 范围:**只读子集**(实例 / 用户 / 存储 / 候选池 / 运行时); // **写操作一律回跳平台管理台** —— 不复制门户的写 UI(否则双源维护 + 扩大执行面)。 // 可见性:**仅 admin**(先 /api/auth/me 判角色;非 admin 只渲染一行说明)。 function PaCard(props) { return jsxRuntime.jsx("div", { style: { border: "1px solid " + T.border, borderRadius: "10px", background: T.field, padding: "12px 14px", display: "flex", flexDirection: "column", gap: "4px", minWidth: "0", flex: "1 1 150px" }, children: [ jsxRuntime.jsx("div", { key: "t", style: { fontSize: "12px", color: T.dim }, children: props.title }), jsxRuntime.jsx("div", { key: "v", style: { fontSize: "20px", fontWeight: 500, color: props.color || T.text, fontVariantNumeric: "tabular-nums" }, children: props.value }), jsxRuntime.jsx("div", { key: "d", style: { fontSize: "12px", color: T.dim, lineHeight: 1.6 }, children: props.desc }) ] }); } function PlatformAdminSection() { var useState = React.useState; var useEffect = React.useEffect; var st = useState({ loading: true, err: "", me: null, status: null, users: null, storage: null, pool: null, runtime: null }); var s = st[0]; var setS = st[1]; var ul = useState([]); var userList = ul[0]; var setUserList = ul[1]; var mv = useState(null); var modal = mv[0]; var setModal = mv[1]; var bs = useState(false); var busy = bs[0]; var setBusy = bs[1]; var nt = useState(""); var note = nt[0]; var setNote = nt[1]; var ne = useState(false); var noteErr = ne[0]; var setNoteErr = ne[1]; function req(path, method) { return fetch(portalHost() + path, { credentials: "include", method: method || "GET" }).then(function (r) { return r.ok ? r.json().catch(function () { return {}; }) : null; }).catch(function () { return null; }); } function loadUsers() { return req("/api/admin/users").then(function (d) { setUserList((d && d.users) || []); }); } function load() { setS(function (old) { return Object.assign({}, old, { loading: true, err: "" }); }); req("/api/auth/me").then(function (me) { var role = me && me.user ? me.user.role : null; if (role !== "admin") { setS(function (old) { return Object.assign({}, old, { loading: false, me: me }); }); return; } Promise.all([ req("/api/dsh/status"), req("/api/admin/users"), req("/api/admin/storage"), req("/api/plugins/business"), req("/api/admin/runtime") ]).then(function (r) { setUserList((r[1] && r[1].users) || []); setS({ loading: false, err: "", me: me, status: r[0], users: r[1], storage: r[2], pool: r[3], runtime: r[4] }); }); }).catch(function () { setS(function (old) { return Object.assign({}, old, { loading: false, err: t("pa.loadFailed") }); }); }); } useEffect(function () { load(); }, []); // 管理动作:就地调用平台 admin API(不跳门户) function doUserAction(u, url, method, label) { if (method === "DELETE" && !window.confirm(t("pa.confirmRemove"))) return; setBusy(true); setNote(""); req(url, method).then(function (r) { setBusy(false); setNoteErr(!r); setNote(r ? t("pa.done") : t("pa.failed")); return loadUsers(); }).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" }; if (s.loading) return jsxRuntime.jsx("div", { style: wrap, children: [ jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.loading") }), jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub }, children: t("pa.loadingDesc") }) ] }); if (s.err) return jsxRuntime.jsx("div", { style: wrap, children: [ jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500, color: T.danger }, children: s.err }), jsxRuntime.jsx("button", { key: "b", type: "button", onClick: load, style: { alignSelf: "flex-start", padding: "6px 14px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.border, background: "transparent", color: T.text, fontSize: "13px" }, children: t("pa.retry") }) ] }); if (!s.me || !s.me.user || s.me.user.role !== "admin") return jsxRuntime.jsx("div", { style: wrap, children: [ jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.notAdminTitle") }), jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 }, children: t("pa.notAdminDesc") }) ] }); var inst = s.status && s.status.running ? t("pa.running") : t("pa.stopped"); var brk = s.status && s.status.breaker; var nPending = userList.filter(function (u) { return u.role === "pending"; }).length; var nDisabled = userList.filter(function (u) { return u.role === "disabled"; }).length; var stUsers = (s.storage && s.storage.users) || []; var pool = (s.pool && (s.pool.plugins || s.pool.items)) || []; var rt = (s.runtime && (s.runtime.items || s.runtime.groups)) || []; // ── 管理项清单:每项点开都是「弹窗内管理」(用户 2026-09-13 要求) 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 : "—" }, ]; // 卡片(06-工作台UI规范 §4.5 .data-card:r12 / 16px 18px / shadow / hover 上浮 + 蓝边 + 蓝影) function itemCard(it) { return jsxRuntime.jsx("div", { key: it.id, role: "button", tabIndex: 0, className: "pa-card", onClick: function () { setNote(""); 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: "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 }) ] }); } 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 ] }); } 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 : "") }); }) }); } 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 : "") }); }) }); } 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" : "") }); }) }); } // 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")) }) ] }); } 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) }), ]; if (modal) { var it = ITEMS.filter(function (x) { return x.id === modal; })[0] || { title: modal }; rows.push(jsxRuntime.jsx("div", { key: "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", { 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" }, 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() ] }) })); } return jsxRuntime.jsx("div", { style: wrap, children: rows }); } ctx.slots.inject("settings.section", function () { return ctx.slots.register( { name: "settings.section", id: "platform-admin", order: 102, label: function () { return t("pa.label"); } }, PlatformAdminSection ); }); } exports.apply = apply; exports.inject = inject; return module.exports; } });