dsh-db-tool 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/client/client.js +136 -10
  2. package/package.json +3 -2
package/client/client.js CHANGED
@@ -388,7 +388,7 @@ window.__ModuleLoader__.load({
388
388
  ".dbt-chev.leaf{visibility:hidden;}",
389
389
  ".dbt-treename{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
390
390
  /* ===== Navicat 式浏览弹窗 ===== */
391
- ".dbt-browse-dialog{position:fixed;width:min(1100px,calc(100vw - 48px));height:min(720px,calc(100vh - 48px));min-width:min(680px,calc(100vw - 16px));min-height:min(420px,calc(100vh - 16px));background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,.32),0 4px 16px rgba(0,0,0,.18);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;font-size:14px;animation:dbt-in .22s var(--dbt-ease);}",
391
+ ".dbt-browse-dialog{position:fixed;z-index:2147483000;width:min(1100px,calc(100vw - 48px));height:min(720px,calc(100vh - 48px));min-width:min(680px,calc(100vw - 16px));min-height:min(420px,calc(100vh - 16px));background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,.32),0 4px 16px rgba(0,0,0,.18);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;font-size:14px;animation:dbt-in .22s var(--dbt-ease);}",
392
392
  ".dbt-browse-dialog.dragging{user-select:none;}",
393
393
  ".dbt-browse-header{flex:none;display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--dbt-separator);}",
394
394
  ".dbt-browse-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;font-size:17px;}",
@@ -463,11 +463,49 @@ window.__ModuleLoader__.load({
463
463
  }
464
464
 
465
465
  // --- 弹窗挂载:优先 createPortal 挂 document.body(避免侧栏容器裁剪);宿主未提供 react-dom 时回退 Panel 内 fixed overlay(DangerDialog 同模式) ---
466
- let createPortal = null;
467
- try { createPortal = require("react-dom").createPortal; } catch (e) { /* 留 null,走回退 */ }
466
+ // 三重降级解析(模块级缓存,全模块一次):①模块加载时同步 require(dsh.client.inject 声明 react-dom 后应成功)
467
+ // ②宿主模块系统异步 import(ctx.modules,宿主 rc.8+ 提供)③均不可用则保持 Panel 内回退渲染。
468
+ let portalImpl = null; // 解析成功的 createPortal(缓存,全模块一次)
469
+ let portalTried = false; // 是否已尝试解析(异步 import 只发起一次)
470
+ let portalPending = null; // 订阅解析成功的回调列表
471
+ try {
472
+ var rd = require("react-dom");
473
+ if (rd && typeof rd.createPortal === "function") { portalImpl = rd.createPortal.bind(rd); portalTried = true; }
474
+ } catch (e) { /* 留 null,等异步解析 */ }
475
+ function resolvePortalAsync(ctx) {
476
+ if (portalTried) return;
477
+ portalTried = true;
478
+ if (portalImpl) return;
479
+ var mods = ctx && ctx.modules;
480
+ var p = mods && typeof mods.import === "function" ? mods.import("react-dom") : null;
481
+ if (p && typeof p.then === "function") {
482
+ p.then(function (m) {
483
+ if (m && typeof m.createPortal === "function") {
484
+ portalImpl = m.createPortal.bind(m);
485
+ var waiters = portalPending || []; portalPending = null;
486
+ for (var i = 0; i < waiters.length; i++) waiters[i]();
487
+ }
488
+ }).catch(function () { /* 解析失败保持回退渲染 */ });
489
+ }
490
+ }
491
+ function onPortalReady(cb) {
492
+ if (portalImpl) { cb(); return; }
493
+ (portalPending = portalPending || []).push(cb);
494
+ }
468
495
  function mountDialog(children) {
469
- return createPortal ? createPortal(children, document.body) : children;
496
+ return (portalImpl && !portalDisabled) ? portalImpl(children, document.body) : children;
470
497
  }
498
+ // PiP 独立窗口渲染时禁 portal:PiP root 本身就挂在 pipDoc.body 上,再 portal 回主 document 会把 DOM 拉出独立窗口
499
+ let portalDisabled = false;
500
+ // createRoot 多级探测(PiP 独立窗口需要独立 React root):react-dom → react-dom/client
501
+ let reactCreateRoot = null;
502
+ try {
503
+ var rd2 = require("react-dom");
504
+ reactCreateRoot = rd2 && typeof rd2.createRoot === "function" ? rd2.createRoot.bind(rd2) : null;
505
+ if (!reactCreateRoot) {
506
+ try { var rdc = require("react-dom/client"); reactCreateRoot = rdc && typeof rdc.createRoot === "function" ? rdc.createRoot.bind(rdc) : null; } catch (e2) { /* 保持 null */ }
507
+ }
508
+ } catch (e) { /* 保持 null,PiP 不可用走 DOM 浮窗降级 */ }
471
509
 
472
510
  /* ---------------- 连接管理 ---------------- */
473
511
  const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
@@ -1306,8 +1344,20 @@ window.__ModuleLoader__.load({
1306
1344
  }
1307
1345
 
1308
1346
  function BrowseDialog(props) {
1309
- const { conns, projectPath, grants, askConfirm, onClose } = props;
1347
+ const { conns, projectPath, grants, askConfirm, onClose, ctx } = props;
1310
1348
  // grants / askConfirm:预留给后续单元格编辑的危险操作确认通道,本任务先接住不使用
1349
+ // portal 异步解析接入:portalReady 仅用于触发重渲染(mountDialog 内部读 portalImpl)。
1350
+ // createPortal 不换组件实例——portalReady 切换前后是同一 DOM 节点,pos state 保持,居中定位不受 portal 重挂影响。
1351
+ const [portalReady, setPortalReady] = React.useState(!!portalImpl);
1352
+ React.useEffect(() => {
1353
+ resolvePortalAsync(ctx);
1354
+ onPortalReady(function () { setPortalReady(true); });
1355
+ // 诊断:一次性输出(帮助真机排查 iframe 隔离与 portal 解析结果)
1356
+ if (!window.__dbtPortalDiag) {
1357
+ window.__dbtPortalDiag = true;
1358
+ console.info("[dbt] portal:", portalImpl ? "sync/async ok" : "unavailable", "sameDoc:", window.top === window.self);
1359
+ }
1360
+ }, []);
1311
1361
  const [sel, setSel] = React.useState(null); // 弹窗内当前选中(null=未选表,右栏显示引导空态)
1312
1362
  const [view, setView] = React.useState("structure"); // structure | preview(纯视图切换,不影响数据加载)
1313
1363
  const [schema, setSchema] = React.useState([]);
@@ -1345,13 +1395,14 @@ window.__ModuleLoader__.load({
1345
1395
  if (closeRef.current) closeRef.current.focus();
1346
1396
  }, []);
1347
1397
 
1348
- // 拖动定位:首帧给个保守值,mount 后按浮窗实测尺寸居中(侧栏 iframe 视口窄,常量估宽会错位)
1398
+ // 拖动定位:首帧给个保守值,mount 后按浮窗实测尺寸居中(坐标基准取浮窗所在 document 的视口——DOM 浮窗为主视口,PiP 独立窗口为 PiP 视口)
1349
1399
  const dialogRef2 = React.useRef(null);
1350
1400
  const [pos, setPos] = React.useState(() => ({ top: 72, left: 12 }));
1351
1401
  React.useEffect(() => {
1352
1402
  const el = dialogRef2.current;
1353
1403
  if (!el) return;
1354
- const w = window.innerWidth || 1200, h = window.innerHeight || 800;
1404
+ const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
1405
+ const w = view.innerWidth || 1200, h = view.innerHeight || 800;
1355
1406
  setPos({
1356
1407
  top: Math.max(0, Math.round((h - el.offsetHeight) / 2)),
1357
1408
  left: Math.max(0, Math.round((w - el.offsetWidth) / 2)),
@@ -1369,7 +1420,8 @@ window.__ModuleLoader__.load({
1369
1420
  }
1370
1421
  function onHeaderPointerMove(e) {
1371
1422
  if (!draggingRef.current) return;
1372
- const h = window.innerHeight || 800, w = window.innerWidth || 1200;
1423
+ const view = (e.currentTarget.ownerDocument && e.currentTarget.ownerDocument.defaultView) || window;
1424
+ const h = view.innerHeight || 800, w = view.innerWidth || 1200;
1373
1425
  // clamp 全部用浮窗实测尺寸:整体保持在视口内(窄视口下常量估宽会把窗拖丢)
1374
1426
  const dlg = e.currentTarget.parentElement;
1375
1427
  const dlgH = (dlg && dlg.offsetHeight) || 720;
@@ -1505,6 +1557,63 @@ window.__ModuleLoader__.load({
1505
1557
  );
1506
1558
  }
1507
1559
 
1560
+ /* ---------------- PiP 独立窗口(Document Picture-in-Picture) ---------------- */
1561
+ // OS 级独立窗口:可拖出桌面端主窗口、系统级置顶。内容为完整数据浏览 UI(同 JS 环境复用
1562
+ // BrowseDialog/DangerDialog 与 mock 之外的宿主 fetch 同源);DOM 浮窗作为不可用时的降级。
1563
+ // 打开成功返回 pipWindow,失败返回 null(调用方降级为 DOM 浮窗)。
1564
+ function PipBrowseApp(props) { // {ctx, conns, projectPath, grants, onClose}
1565
+ // 危险操作确认渲染在 PiP 窗口内(Panel 的确认对话框在主窗口,PiP 用户看不见)
1566
+ const [confirmReq, setConfirmReq] = React.useState(null);
1567
+ const askConfirm = React.useCallback((info) => new Promise((resolve) => {
1568
+ setConfirmReq(Object.assign({}, info, { resolve }));
1569
+ }), []);
1570
+ return React.createElement(
1571
+ React.Fragment,
1572
+ null,
1573
+ React.createElement(BrowseDialog, {
1574
+ ctx: props.ctx, conns: props.conns, projectPath: props.projectPath,
1575
+ grants: props.grants, askConfirm: askConfirm, onClose: props.onClose,
1576
+ }),
1577
+ confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
1578
+ );
1579
+ }
1580
+ function openBrowsePip(ctx, deps) { // deps: {conns, projectPath, grants, onClosed} → 返回 pipWindow | null
1581
+ try {
1582
+ const dpip = window.documentPictureInPicture;
1583
+ if (!dpip || typeof dpip.requestWindow !== "function" || !reactCreateRoot || !portalImpl) return null;
1584
+ // PiP 只能在用户手势内同步发起 requestWindow,Promise then 里再做 DOM 装配
1585
+ return dpip.requestWindow({ width: 1100, height: 720 }).then((w) => {
1586
+ const doc = w.document;
1587
+ // 相对地址基准:PiP 文档是 about:blank,fetch 的相对 API 路径须按主 origin 解析
1588
+ const base = doc.createElement("base");
1589
+ base.href = window.location.origin + "/";
1590
+ doc.head.appendChild(base);
1591
+ // 样式:整套 --dbt-* token 与规则注入 PiP 文档
1592
+ if (styleEl) doc.head.appendChild(doc.importNode(styleEl, true));
1593
+ doc.body.style.margin = "0";
1594
+ portalDisabled = true; // PiP root 已在 pipDoc.body 上,禁 portal 防 DOM 被拉回主窗口
1595
+ const root = reactCreateRoot(doc.body);
1596
+ const closePip = () => {
1597
+ try { root.unmount(); } catch (e) { /* 已卸载 */ }
1598
+ portalDisabled = false;
1599
+ try { w.close(); } catch (e2) { /* 已关闭 */ }
1600
+ };
1601
+ root.render(React.createElement(PipBrowseApp, {
1602
+ ctx, conns: deps.conns, projectPath: deps.projectPath, grants: deps.grants,
1603
+ onClose: () => { closePip(); deps.onClosed(); },
1604
+ }));
1605
+ w.addEventListener("pagehide", () => { // 用户点 PiP 窗口系统关闭钮
1606
+ portalDisabled = false;
1607
+ try { root.unmount(); } catch (e) { /* 已卸载 */ }
1608
+ deps.onClosed();
1609
+ });
1610
+ return w;
1611
+ }).catch(() => null);
1612
+ } catch (e) {
1613
+ return Promise.resolve(null);
1614
+ }
1615
+ }
1616
+
1508
1617
  /* ---------------- SQL 控制台 ---------------- */
1509
1618
  // Apple 等宽字体栈(规范 §1):优先样式层 --dbt-mono token,fallback 内联栈
1510
1619
  const MONO_FONT = "var(--dbt-mono, ui-monospace, \"SF Mono\", Menlo, Consolas, monospace)";
@@ -1633,6 +1742,21 @@ window.__ModuleLoader__.load({
1633
1742
  const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
1634
1743
  // 数据浏览浮窗开关:由 Panel 层持有(不随 view 切换卸载),跨 tab 常驻
1635
1744
  const [browseOpen, setBrowseOpen] = React.useState(false);
1745
+ // PiP 独立窗口句柄:优先于 DOM 浮窗(OS 级窗口,可拖出桌面端主窗口);存在时 DOM 浮窗不渲染
1746
+ const [pipWin, setPipWin] = React.useState(null);
1747
+ function openBrowse() {
1748
+ if (pipWin) { try { pipWin.focus(); } catch (e) { /* 已关闭 */ } return; }
1749
+ // requestWindow 必须在用户手势内同步发起(本函数仅由 onClick 直调)
1750
+ const p = openBrowsePip(props.ctx, {
1751
+ conns, projectPath, grants,
1752
+ onClosed: () => setPipWin(null),
1753
+ });
1754
+ if (p && typeof p.then === "function") {
1755
+ p.then((w) => { if (w) setPipWin(w); else setBrowseOpen(true); });
1756
+ } else {
1757
+ setBrowseOpen(true);
1758
+ }
1759
+ }
1636
1760
 
1637
1761
  const askConfirm = React.useCallback((info) => new Promise((resolve) => {
1638
1762
  setConfirmReq(Object.assign({}, info, { resolve }));
@@ -1690,7 +1814,7 @@ window.__ModuleLoader__.load({
1690
1814
  // browse tab:切视图的同时直接打开浮窗(单击即用,不必再进树点表)
1691
1815
  React.createElement("button", {
1692
1816
  key: v, className: view === v ? "active" : "",
1693
- onClick: () => { setView(v); if (v === "browse") setBrowseOpen(true); },
1817
+ onClick: () => { setView(v); if (v === "browse") openBrowse(); },
1694
1818
  }, label)),
1695
1819
  ),
1696
1820
  (projectEdited || !projectPath)
@@ -1733,7 +1857,7 @@ window.__ModuleLoader__.load({
1733
1857
  // 主提示用正常文字色(dbt-muted 11px 次要色压深底对比不足,视觉审查命中)
1734
1858
  React.createElement("div", { style: { fontSize: 13 } }, t("browsePanelHint")),
1735
1859
  React.createElement("div", { className: "dbt-row" },
1736
- React.createElement("button", { className: "dbt-btn primary", onClick: () => setBrowseOpen(true) }, t("reopenBrowse"))),
1860
+ React.createElement("button", { className: "dbt-btn primary", onClick: openBrowse }, t("reopenBrowse"))),
1737
1861
  ) : null,
1738
1862
  view === "console" ? React.createElement(ConsoleView, Object.assign({}, shared, { askConfirm })) : null,
1739
1863
  ),
@@ -1742,6 +1866,7 @@ window.__ModuleLoader__.load({
1742
1866
  // 浮窗常驻:portal 到 body 且在 dbt-view 外,切 tab 时不消失
1743
1867
  browseOpen ? React.createElement(BrowseDialog, {
1744
1868
  conns, projectPath, grants, askConfirm,
1869
+ ctx: props.ctx, // 宿主 ctx 透传给浮窗,供 portal 异步解析(ctx.modules.import)
1745
1870
  onClose: () => setBrowseOpen(false),
1746
1871
  }) : null,
1747
1872
  );
@@ -1766,6 +1891,7 @@ window.__ModuleLoader__.load({
1766
1891
  key: "db-tool-" + String(localeKey || "zh"),
1767
1892
  visible: props.visible,
1768
1893
  scope: props.scope,
1894
+ ctx: ctx, // 宿主 ctx 下传,Panel 透传给 BrowseDialog 供 portal 解析
1769
1895
  });
1770
1896
  }
1771
1897
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,8 @@
38
38
  },
39
39
  "client": {
40
40
  "inject": [
41
- "@deepseek-ai/dsh-client-locale"
41
+ "@deepseek-ai/dsh-client-locale",
42
+ "react-dom"
42
43
  ],
43
44
  "platform": "web"
44
45
  }