dsh-db-tool 1.5.4 → 1.5.6

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.
package/client/client.js CHANGED
@@ -92,8 +92,6 @@ window.__ModuleLoader__.load({
92
92
  emptyStructure: "暂无字段信息",
93
93
  emptyData: "暂无数据",
94
94
  retry: "重试",
95
- browsePanelHint: "数据浏览已在浮动窗口打开,可继续操作其他功能。",
96
- reopenBrowse: "打开数据浏览",
97
95
  selectTableHint: "在左侧选择表后查看数据",
98
96
  // 单元格查看/编辑(Navicat 式就地写回)
99
97
  editCell: "编辑",
@@ -107,7 +105,6 @@ window.__ModuleLoader__.load({
107
105
  truncatedNoEdit: "值已截断显示,就地编辑可能覆盖数据,请改用 SQL 控制台",
108
106
  blobNoEdit: "二进制值不支持就地编辑",
109
107
  redisTypeRo: "Redis 该类型不支持就地编辑",
110
- cellSaved: "已保存",
111
108
  // SQL 控制台
112
109
  modeQuery: "只读查询",
113
110
  modeExecute: "写入执行",
@@ -202,8 +199,6 @@ window.__ModuleLoader__.load({
202
199
  emptyStructure: "No columns",
203
200
  emptyData: "No rows",
204
201
  retry: "Retry",
205
- browsePanelHint: "Browse is open in a floating window; you can keep working here.",
206
- reopenBrowse: "Open Data Browser",
207
202
  selectTableHint: "Select a table on the left to view data",
208
203
  // Cell view/edit (Navicat-style in-place write-back)
209
204
  editCell: "Edit",
@@ -217,7 +212,6 @@ window.__ModuleLoader__.load({
217
212
  truncatedNoEdit: "Value shown truncated; edit via SQL Console instead",
218
213
  blobNoEdit: "Binary values cannot be edited here",
219
214
  redisTypeRo: "This Redis type cannot be edited here",
220
- cellSaved: "Saved",
221
215
  modeQuery: "query (read-only)",
222
216
  modeExecute: "execute (write)",
223
217
  modeScript: "script",
@@ -324,22 +318,25 @@ window.__ModuleLoader__.load({
324
318
  if (styleEl) return;
325
319
  styleEl = document.createElement("style");
326
320
  styleEl.textContent = [
327
- "/* ===== Apple-style Design Tokens(dark 基线,light 经 media query 反转;.dbt-browse-root 并入以承载 portal 弹窗) ===== */",
321
+ "/* ===== Apple-style Design Tokens(dark 基线,light 经 media query 反转;.dbt-browse-root 并入以承载内嵌浏览面板与单元格浮层) ===== */",
328
322
  ".dbt-panel,.dbt-browse-root{--dbt-accent:var(--dsh-accent,#0a84ff);--dbt-bg:transparent;--dbt-surface:rgba(120,120,128,.12);--dbt-surface-strong:rgba(120,120,128,.18);--dbt-separator:rgba(120,120,128,.24);--dbt-text:rgba(235,235,245,.92);--dbt-text-secondary:rgba(235,235,245,.6);--dbt-danger:#ff453a;--dbt-success:#30d158;--dbt-warning:#ffd60a;--dbt-dialog-bg:rgba(40,40,44,.85);--dbt-th-bg:rgba(30,30,32,.72);--dbt-seg-active:rgba(255,255,255,.14);--dbt-shadow:0 8px 32px rgba(0,0,0,.28);--dbt-radius-card:12px;--dbt-radius-ctrl:8px;--dbt-radius-pill:6px;--dbt-ease:cubic-bezier(.25,.1,.25,1);--dbt-mono:ui-monospace,'SF Mono',Menlo,Consolas,monospace;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,sans-serif;}",
329
323
  "@media (prefers-color-scheme: light){.dbt-panel,.dbt-browse-root{--dbt-accent:var(--dsh-accent,#007aff);--dbt-surface:rgba(120,120,128,.08);--dbt-surface-strong:rgba(120,120,128,.14);--dbt-separator:rgba(60,60,67,.18);--dbt-text:rgba(30,30,32,.92);--dbt-text-secondary:rgba(60,60,67,.6);--dbt-danger:#ff3b30;--dbt-success:#34c759;--dbt-warning:#ff9f0a;--dbt-dialog-bg:rgba(252,252,252,.9);--dbt-th-bg:rgba(255,255,255,.72);--dbt-seg-active:rgba(255,255,255,.9);--dbt-shadow:0 8px 32px rgba(0,0,0,.12);}}",
330
- // 弹窗根自持文字色:portal 挂 document.body 后不再依赖侧栏祖先的 color 继承(ocr medium)
324
+ // 浏览面板根自持文字色:PiP 独立文档挂 body 后无侧栏祖先 color 可继承(内嵌/浮窗两态共用)
331
325
  ".dbt-browse-root{color:var(--dbt-text);}",
332
326
  "/* ===== 根容器 ===== */",
333
- ".dbt-panel{font-size:13px;line-height:1.45;display:flex;flex-direction:column;gap:12px;padding:16px;height:100%;box-sizing:border-box;overflow-y:auto;}",
327
+ ".dbt-panel{font-size:13px;line-height:1.45;display:flex;flex-direction:column;gap:10px;padding:16px;height:100%;box-sizing:border-box;overflow-y:auto;}",
334
328
  "/* ===== iOS segmented control(顶部 tab) ===== */",
335
329
  ".dbt-tabs{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
336
330
  ".dbt-tabs button{flex:1;border:none;background:transparent;color:inherit;border-radius:var(--dbt-radius-pill);padding:5px 8px;font-size:12px;font-weight:500;cursor:pointer;transition:all .18s var(--dbt-ease);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
337
331
  ".dbt-tabs button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
338
332
  "/* ===== inset grouped 卡片 ===== */",
339
- ".dbt-card{background:var(--dbt-surface);border:none;border-radius:var(--dbt-radius-card);padding:12px;display:flex;flex-direction:column;gap:10px;}",
333
+ ".dbt-card{background:var(--dbt-surface);border:none;border-radius:var(--dbt-radius-card);padding:12px;display:flex;flex-direction:column;gap:8px;}",
340
334
  ".dbt-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
335
+ "/* ===== 共享列布局(T1 逐步替换各视图内联 flex-column) ===== */",
336
+ ".dbt-col{display:flex;flex-direction:column;gap:8px;}",
337
+ ".dbt-col.dbt-col-tight{gap:2px;}",
341
338
  "/* ===== 填充式无边框输入 ===== */",
342
- ".dbt-row input,.dbt-row select,.dbt-card input,.dbt-card select,.dbt-card textarea,.dbt-group input,.dbt-group select{flex:1;min-width:60px;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:6px 10px;font-size:13px;font-family:inherit;transition:all .18s var(--dbt-ease);}",
339
+ ".dbt-row input,.dbt-row select,.dbt-card input,.dbt-card select,.dbt-card textarea,.dbt-group input,.dbt-group select{flex:1;min-width:60px;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:5px 12px;font-size:13px;font-family:inherit;transition:all .18s var(--dbt-ease);}",
343
340
  ".dbt-row input:focus,.dbt-row select:focus,.dbt-card input:focus,.dbt-card select:focus,.dbt-card textarea:focus,.dbt-group input:focus,.dbt-group select:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
344
341
  ".dbt-card textarea{font-family:var(--dbt-mono);min-height:96px;resize:vertical;}",
345
342
  "/* ===== 按钮 ===== */",
@@ -352,24 +349,25 @@ window.__ModuleLoader__.load({
352
349
  ".dbt-btn.danger:hover{background:rgba(255,69,58,.12);}",
353
350
  ".dbt-btn:disabled{opacity:.4;cursor:default;transform:none;}",
354
351
  "/* ===== 文本反馈 ===== */",
355
- ".dbt-muted{color:var(--dbt-text-secondary);font-size:11px;}",
352
+ ".dbt-panel strong{font-size:13px;font-weight:600;}",
353
+ ".dbt-muted,.dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
356
354
  ".dbt-err{color:var(--dbt-danger);font-size:12px;white-space:pre-wrap;}",
357
355
  ".dbt-msg{color:var(--dbt-success);font-size:12px;white-space:pre-wrap;}",
358
356
  "/* ===== 行 hairline 表格 + 毛玻璃 sticky 表头 ===== */",
359
357
  ".dbt-table{width:100%;border-collapse:collapse;font-size:12px;}",
360
- ".dbt-table th,.dbt-table td{border:none;border-bottom:1px solid var(--dbt-separator);padding:6px 8px;text-align:left;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
361
- ".dbt-table th{position:sticky;top:0;z-index:1;background:var(--dbt-th-bg);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);font-weight:600;font-size:11px;}",
358
+ ".dbt-table th,.dbt-table td{border:none;border-bottom:1px solid var(--dbt-separator);padding:7px 10px;text-align:left;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
359
+ ".dbt-table th{position:sticky;top:0;z-index:1;background:var(--dbt-th-bg);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);font-weight:600;font-size:12px;}",
362
360
  ".dbt-table td{font-family:var(--dbt-mono);}",
363
361
  ".dbt-tablewrap{border:none;border-radius:var(--dbt-radius-card);overflow:auto;max-height:320px;background:var(--dbt-surface);}",
364
- "/* ===== 浏览弹窗表格:列分明(列间 hairline + 表头加深一档 + 行 hover) ===== */",
362
+ "/* ===== 浏览面板表格:列分明(列间 hairline + 表头加深一档 + 行 hover) ===== */",
365
363
  ".dbt-browse-root .dbt-table th + th,.dbt-browse-root .dbt-table td + td{border-left:1px solid var(--dbt-separator);}",
366
- ".dbt-browse-root .dbt-table th{background:rgba(30,30,32,.85);font-size:12px;}",
367
- ".dbt-browse-root .dbt-table td{font-size:13px;padding:7px 10px;}",
364
+ ".dbt-browse-root .dbt-table th{background:rgba(30,30,32,.85);}",
365
+ ".dbt-browse-root .dbt-table td{font-size:12px;}",
368
366
  ".dbt-browse-root .dbt-table tbody tr:hover td{background:var(--dbt-surface-strong);}",
369
367
  "@media (prefers-color-scheme: light){.dbt-browse-root .dbt-table th{background:rgba(242,242,247,.9);}}",
370
368
  "/* ===== 对话框(macOS alert 材质) ===== */",
371
369
  ".dbt-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:9999;}",
372
- ".dbt-dialog{background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:14px;padding:16px;max-width:460px;width:90%;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;font-size:13px;-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);box-shadow:var(--dbt-shadow);}",
370
+ ".dbt-dialog{background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:var(--dbt-radius-card);padding:16px;max-width:460px;width:90%;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;font-size:13px;-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);box-shadow:var(--dbt-shadow);}",
373
371
  ".dbt-dialog pre{background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);padding:8px;font-family:var(--dbt-mono);font-size:12px;white-space:pre-wrap;word-break:break-all;max-height:160px;overflow:auto;}",
374
372
  ".dbt-dialog .dbt-row{justify-content:flex-end;}",
375
373
  "/* ===== 授权/连接列表:inset grouped(一张卡多行 hairline) ===== */",
@@ -379,7 +377,7 @@ window.__ModuleLoader__.load({
379
377
  ".dbt-listrow:hover{background:var(--dbt-surface-strong);}",
380
378
  ".dbt-tree{display:flex;flex-direction:column;}",
381
379
  "/* ===== Navicat 式对象树(数据浏览) ===== */",
382
- ".dbt-treerow{display:flex;gap:6px;align-items:center;padding:8px 14px;font-size:14px;cursor:pointer;user-select:none;border-bottom:1px solid var(--dbt-separator);transition:background .15s var(--dbt-ease);}",
380
+ ".dbt-treerow{display:flex;gap:6px;align-items:center;padding:10px 12px;font-size:13px;cursor:pointer;user-select:none;border-bottom:1px solid var(--dbt-separator);transition:background .15s var(--dbt-ease);}",
383
381
  ".dbt-treerow:last-child{border-bottom:none;}",
384
382
  ".dbt-treerow:hover{background:var(--dbt-surface-strong);}",
385
383
  ".dbt-treerow.active{color:var(--dbt-accent);font-weight:600;}",
@@ -387,48 +385,39 @@ window.__ModuleLoader__.load({
387
385
  ".dbt-chev.open{transform:rotate(90deg);}",
388
386
  ".dbt-chev.leaf{visibility:hidden;}",
389
387
  ".dbt-treename{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
390
- /* ===== Navicat 式浏览弹窗 ===== */
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
- ".dbt-browse-dialog.dragging{user-select:none;}",
393
- ".dbt-browse-header{flex:none;display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--dbt-separator);}",
394
- ".dbt-browse-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;font-size:17px;}",
395
- ".dbt-browse-close{flex:none;width:26px;height:26px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:var(--dbt-text-secondary);border-radius:var(--dbt-radius-pill);cursor:pointer;transition:all .18s var(--dbt-ease);}",
396
- ".dbt-browse-close:hover{background:var(--dbt-surface-strong);color:inherit;}",
397
- ".dbt-browse-close:active{transform:scale(.92);}",
398
- ".dbt-browse-body{flex:1;min-height:0;display:flex;}",
399
- ".dbt-browse-tree{flex:none;width:260px;overflow-y:auto;border-right:1px solid var(--dbt-separator);}",
400
- ".dbt-browse-main{flex:1;min-width:0;display:flex;flex-direction:column;}",
401
- ".dbt-browse-tabs{flex:none;padding:10px 14px 0;}",
402
- ".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;gap:10px;padding:12px 14px;overflow:hidden;}",
403
- ".dbt-browse-tablewrap{flex:1;min-height:0;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
404
- ".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid var(--dbt-separator);}",
405
- ".dbt-browse-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;}",
406
- ".dbt-browse-errorbox{flex:1;display:flex;flex-direction:column;gap:10px;align-items:flex-start;justify-content:center;}",
388
+ /* ===== Navicat 式浏览(内嵌面板,纵向流式;浮窗壳类已随 DOM 回收) ===== */
389
+ ".dbt-browse-body{display:flex;flex-direction:column;gap:10px;min-height:0;}",
390
+ ".dbt-browse-tree{flex:none;max-height:280px;overflow-y:auto;border-bottom:1px solid var(--dbt-separator);}",
391
+ ".dbt-browse-main{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;}",
392
+ ".dbt-browse-tabs{flex:none;}",
393
+ ".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;gap:10px;padding:12px;}",
394
+ ".dbt-browse-tablewrap{flex:none;max-height:320px;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
395
+ ".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid var(--dbt-separator);}",
396
+ ".dbt-browse-empty{display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;padding:24px 12px;}",
397
+ ".dbt-browse-errorbox{display:flex;flex-direction:column;gap:8px;align-items:flex-start;}",
407
398
  ".dbt-browse-skel{display:flex;flex-direction:column;gap:8px;}",
408
399
  ".dbt-browse-skelrow{display:flex;gap:8px;}",
409
400
  ".dbt-browse-skelcell{flex:1;height:24px;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);}",
410
- "/* ===== 浏览弹窗:单元格查看/编辑浮层(定位锚点为 .dbt-browse-content) ===== */",
401
+ "/* ===== 浏览面板:单元格查看/编辑浮层(定位锚点为 .dbt-browse-content) ===== */",
411
402
  ".dbt-cellbtn{border:none;background:transparent;color:inherit;font:inherit;padding:0;margin:0;cursor:pointer;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;text-align:left;}",
412
403
  ".dbt-cellbtn:focus-visible{outline:2px solid var(--dbt-accent);outline-offset:-1px;border-radius:2px;}",
413
404
  ".dbt-cellpop{position:absolute;z-index:20;width:min(460px,100%);background:var(--dbt-dialog-bg);border:1px solid var(--dbt-separator);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);padding:12px;display:flex;flex-direction:column;gap:8px;box-sizing:border-box;font-size:13px;}",
414
- ".dbt-cellpop-value{font-family:var(--dbt-mono);font-size:13px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow:auto;background:var(--dbt-surface-strong);border-radius:8px;padding:8px;}",
405
+ ".dbt-cellpop-value{font-family:var(--dbt-mono);font-size:12px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow:auto;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-ctrl);padding:8px;}",
415
406
  ".dbt-cellpop-actions{display:flex;gap:8px;align-items:center;}",
416
407
  ".dbt-celledit{display:flex;flex-direction:column;gap:8px;}",
417
- ".dbt-celledit textarea{font-family:var(--dbt-mono);font-size:13px;min-height:96px;resize:vertical;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:8px;padding:8px;}",
408
+ ".dbt-celledit textarea{font-family:var(--dbt-mono);font-size:13px;min-height:96px;resize:vertical;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:5px 12px;}",
418
409
  ".dbt-celledit textarea:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
419
410
  ".dbt-nullchip{display:inline-flex;align-items:center;border:1px solid var(--dbt-warning);color:var(--dbt-warning);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;}",
420
- ".dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
421
411
  "/* ===== 视图切换入场(仅 transform/opacity;reduced-motion 全局已禁) ===== */",
422
412
  "@keyframes dbt-in{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}",
423
413
  ".dbt-view{display:flex;flex-direction:column;gap:10px;animation:dbt-in .22s var(--dbt-ease);}",
424
414
  "/* ===== mini segmented(ro/rw 等切换) ===== */",
425
415
  ".dbt-seg{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
426
- ".dbt-seg button{flex:none;border:none;background:transparent;color:inherit;font-size:11px;padding:3px 10px;border-radius:var(--dbt-radius-pill);cursor:pointer;transition:all .18s var(--dbt-ease);}",
416
+ ".dbt-seg button{flex:none;border:none;background:transparent;color:inherit;font-size:12px;padding:5px 12px;border-radius:var(--dbt-radius-pill);cursor:pointer;transition:all .18s var(--dbt-ease);}",
427
417
  ".dbt-seg button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
428
- ".dbt-browse-root .dbt-seg button{font-size:12px;padding:4px 12px;}",
429
418
  "/* ===== 降级:透明度减弱 / 动效减弱 ===== */",
430
- "@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-browse-dialog,.dbt-cellpop{backdrop-filter:none;-webkit-backdrop-filter:none;}.dbt-dialog,.dbt-browse-dialog{background:#2c2c2e;}.dbt-cellpop{background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
431
- "@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-browse-dialog,.dbt-cellpop{background:#f5f5f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
419
+ "@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-cellpop{backdrop-filter:none;-webkit-backdrop-filter:none;background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
420
+ "@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-cellpop{background:#f5f5f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
432
421
  "@media (prefers-reduced-motion: reduce){.dbt-panel *,.dbt-browse-root *{transition:none!important;animation:none!important;}.dbt-btn:active{transform:none;}}",
433
422
  ].join("\n");
434
423
  document.head.appendChild(styleEl);
@@ -454,7 +443,8 @@ window.__ModuleLoader__.load({
454
443
  React.createElement("div", { className: "dbt-muted" }, t("confirmHint")),
455
444
  React.createElement(
456
445
  "div",
457
- { className: "dbt-row", style: { justifyContent: "flex-end" } },
446
+ // 末对齐由样式层 .dbt-dialog .dbt-row 规则承担,无需内联 style
447
+ { className: "dbt-row" },
458
448
  React.createElement("button", { className: "dbt-btn danger", onClick: () => done(true) }, t("confirmRun")),
459
449
  React.createElement("button", { className: "dbt-btn", onClick: () => done(false) }, t("confirmCancel")),
460
450
  ),
@@ -462,51 +452,6 @@ window.__ModuleLoader__.load({
462
452
  );
463
453
  }
464
454
 
465
- // --- 弹窗挂载:优先 createPortal 挂 document.body(避免侧栏容器裁剪);宿主未提供 react-dom 时回退 Panel 内 fixed overlay(DangerDialog 同模式) ---
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
- }
495
- function mountDialog(children) {
496
- return (portalImpl && !portalDisabled) ? portalImpl(children, document.body) : children;
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 浮窗降级 */ }
509
-
510
455
  /* ---------------- 连接管理 ---------------- */
511
456
  const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
512
457
 
@@ -840,7 +785,7 @@ window.__ModuleLoader__.load({
840
785
  draftTest && draftTest.ok === null ? t("testing") : t("test")),
841
786
  React.createElement("button", { className: "dbt-btn", onClick: props.onCancel }, t("cancel")),
842
787
  draftTest && draftTest.ok !== null
843
- ? React.createElement("span", { style: { color: draftTest.ok ? "var(--dbt-success, #30d158)" : "var(--dbt-danger, #ff453a)", fontSize: 12, alignSelf: "center" } }, draftTest.msg)
788
+ ? React.createElement("span", { className: draftTest.ok ? "dbt-msg" : "dbt-err" }, draftTest.msg)
844
789
  : null,
845
790
  ),
846
791
  );
@@ -932,7 +877,7 @@ window.__ModuleLoader__.load({
932
877
  { className: "dbt-muted", style: { fontFamily: MONO_FONT, fontSize: 11, wordBreak: "break-all" } },
933
878
  c.safeUrl || (c.host + ":" + (c.port || "")),
934
879
  ) : null,
935
- testInfo[c.id] ? React.createElement("div", { style: { fontSize: 12, color: testInfo[c.id].startsWith("✓") ? "var(--dbt-success, #30d158)" : "var(--dbt-danger, #ff453a)" } }, testInfo[c.id]) : null,
880
+ testInfo[c.id] ? React.createElement("div", { className: testInfo[c.id].startsWith("✓") ? "dbt-msg" : "dbt-err" }, testInfo[c.id]) : null,
936
881
  ),
937
882
  // 右列:动作按钮(次要语义,danger 仅删除)
938
883
  React.createElement(
@@ -1012,7 +957,7 @@ window.__ModuleLoader__.load({
1012
957
  }
1013
958
 
1014
959
  /* ---------------- 数据浏览 ---------------- */
1015
- // Navicat 式对象树:连接 ▸ 库/[schema] ▸ 表,懒加载展开(弹窗唯一实例,展开状态随弹窗生命周期)
960
+ // Navicat 式对象树:连接 ▸ 库/[schema] ▸ 表,懒加载展开(浏览面板唯一树实例,展开状态随面板生命周期)
1016
961
  function BrowseTree(props) {
1017
962
  const { conns, projectPath, sel, onSelect, autoExpand } = props;
1018
963
  // sel 由父组件持有(弹窗内 active 高亮与初始选中都依赖);onSelect(选中记录, 触发元素) 上报
@@ -1160,7 +1105,7 @@ window.__ModuleLoader__.load({
1160
1105
  return React.createElement(
1161
1106
  "div",
1162
1107
  { style: { display: "flex", flexDirection: "column", gap: 8 } },
1163
- // Navicat 式对象树:点击展开连接/库,点击表上报选中(由父组件决定打开弹窗)
1108
+ // Navicat 式对象树:点击展开连接/库,点击表上报选中(由父组件就地预览)
1164
1109
  conns.length === 0 ? React.createElement("div", { className: "dbt-muted" }, t("noConns")) :
1165
1110
  React.createElement("div", { className: "dbt-group" },
1166
1111
  React.createElement("div", { className: "dbt-tree" }, treeRows)),
@@ -1343,30 +1288,18 @@ window.__ModuleLoader__.load({
1343
1288
  );
1344
1289
  }
1345
1290
 
1346
- function BrowseDialog(props) {
1347
- const { conns, projectPath, grants, askConfirm, onClose, ctx } = props;
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
- }, []);
1361
- const [sel, setSel] = React.useState(null); // 弹窗内当前选中(null=未选表,右栏显示引导空态)
1291
+ // Navicat 式浏览面板(内嵌侧边栏):上半对象树选库-表,下半结构/数据预览,点表即看,不再弹窗
1292
+ function BrowsePane(props) {
1293
+ const { conns, projectPath, grants, askConfirm } = props;
1294
+ // grants(editable 判定)与 askConfirm(单元格写回确认)下传 PreviewGrid
1295
+ const [sel, setSel] = React.useState(null); // 面板内当前选中(null=未选表,内容区显示引导空态)
1362
1296
  const [view, setView] = React.useState("structure"); // structure | preview(纯视图切换,不影响数据加载)
1363
1297
  const [schema, setSchema] = React.useState([]);
1364
1298
  const [preview, setPreview] = React.useState(null); // QueryResult
1365
1299
  const [page, setPage] = React.useState(1);
1366
1300
  const [busy, setBusy] = React.useState("");
1367
- const [loadErr, setLoadErr] = React.useState(""); // 右栏加载失败(就地显示 + 重试)
1301
+ const [loadErr, setLoadErr] = React.useState(""); // 内容区加载失败(就地显示 + 重试)
1368
1302
  const openSeq = React.useRef(0); // 请求序号守卫
1369
- const closeRef = React.useRef(null);
1370
1303
 
1371
1304
  // 请求序号守卫(沿用面板):快速切表/翻页时丢弃晚到旧响应,防止旧数据覆盖新选中项
1372
1305
  const openTable = React.useCallback((s, pg) => {
@@ -1383,59 +1316,16 @@ window.__ModuleLoader__.load({
1383
1316
  if (seq !== openSeq.current) return; // 旧请求晚到,丢弃
1384
1317
  setSchema(sch || []); setPreview(prev); setPage(pg || 1);
1385
1318
  })
1386
- // 弹窗内错误就地显示(不走面板顶部错误区)
1319
+ // 面板内错误就地显示(不走面板顶部错误区)
1387
1320
  .catch((e) => { if (seq === openSeq.current) setLoadErr(String(e && e.message ? e.message : e)); })
1388
1321
  .finally(() => { if (seq === openSeq.current) setBusy(""); });
1389
1322
  }, [projectPath]);
1390
1323
  React.useEffect(() => { if (sel) openTable(sel, 1); }, [sel]); // eslint-disable-line
1391
1324
 
1392
- // 非模态浮窗:无焦点陷阱(焦点可自由离开,主页交互不受阻);Esc 仅在焦点位于弹窗内时
1393
- // 经容器 onKeyDown 触发(stopPropagation 防止宿主级快捷键同时响应)。初始焦点=关闭按钮
1394
- React.useEffect(() => {
1395
- if (closeRef.current) closeRef.current.focus();
1396
- }, []);
1397
-
1398
- // 拖动定位:首帧给个保守值,mount 后按浮窗实测尺寸居中(坐标基准取浮窗所在 document 的视口——DOM 浮窗为主视口,PiP 独立窗口为 PiP 视口)
1399
- const dialogRef2 = React.useRef(null);
1400
- const [pos, setPos] = React.useState(() => ({ top: 72, left: 12 }));
1401
- React.useEffect(() => {
1402
- const el = dialogRef2.current;
1403
- if (!el) return;
1404
- const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
1405
- const w = view.innerWidth || 1200, h = view.innerHeight || 800;
1406
- setPos({
1407
- top: Math.max(0, Math.round((h - el.offsetHeight) / 2)),
1408
- left: Math.max(0, Math.round((w - el.offsetWidth) / 2)),
1409
- });
1410
- }, []);
1411
- const draggingRef = React.useRef(null); // {dx, dy}
1412
- const [dragging, setDragging] = React.useState(false);
1413
- function stopDrag() { draggingRef.current = null; setDragging(false); }
1414
- function onHeaderPointerDown(e) {
1415
- // header 内可点元素(关闭按钮)不启动拖动,否则 setPointerCapture 会把 click 重定向到 header 使按钮失效
1416
- if (e.target.closest && e.target.closest("button")) return;
1417
- draggingRef.current = { dx: e.clientX - pos.left, dy: e.clientY - pos.top };
1418
- e.currentTarget.setPointerCapture(e.pointerId);
1419
- setDragging(true);
1420
- }
1421
- function onHeaderPointerMove(e) {
1422
- if (!draggingRef.current) return;
1423
- const view = (e.currentTarget.ownerDocument && e.currentTarget.ownerDocument.defaultView) || window;
1424
- const h = view.innerHeight || 800, w = view.innerWidth || 1200;
1425
- // clamp 全部用浮窗实测尺寸:整体保持在视口内(窄视口下常量估宽会把窗拖丢)
1426
- const dlg = e.currentTarget.parentElement;
1427
- const dlgH = (dlg && dlg.offsetHeight) || 720;
1428
- const dlgW = (dlg && dlg.offsetWidth) || 1100;
1429
- setPos({
1430
- top: Math.min(Math.max(e.clientY - draggingRef.current.dy, 0), Math.max(0, h - dlgH)),
1431
- left: Math.min(Math.max(e.clientX - draggingRef.current.dx, 0), Math.max(0, w - dlgW)),
1432
- });
1433
- }
1434
-
1435
- // 右栏互斥状态:未选中引导 / 加载骨架 / 错误重试 / 空态 / 表格
1325
+ // 内容区互斥状态:未选中引导 / 加载骨架 / 错误重试 / 空态 / 表格
1436
1326
  function rightPane() {
1437
1327
  if (!sel && busy !== "open" && !loadErr) {
1438
- // 未选表:显示引导空态(浏览窗口可直接打开,不强制先选表)
1328
+ // 未选表:显示引导空态(内嵌面板可直接浏览,不强制先选表)
1439
1329
  return React.createElement("div", { className: "dbt-browse-empty" }, t("selectTableHint"));
1440
1330
  }
1441
1331
  if (busy === "open") {
@@ -1445,7 +1335,7 @@ window.__ModuleLoader__.load({
1445
1335
  [0, 1, 2, 3, 4, 5].map((c) => React.createElement("div", { className: "dbt-browse-skelcell", key: c })))));
1446
1336
  }
1447
1337
  if (loadErr) {
1448
- // 错误就地显示 + 重试(弹窗遮罩下面板顶部错误区不可见,不走 props.onError)
1338
+ // 错误就地显示 + 重试(内嵌面板保持就地呈现,不走 props.onError)
1449
1339
  return React.createElement("div", { className: "dbt-browse-errorbox" },
1450
1340
  React.createElement("div", { className: "dbt-err" }, t("error") + ": " + loadErr),
1451
1341
  React.createElement("button", { className: "dbt-btn", onClick: () => openTable(sel, 1) }, t("retry")));
@@ -1477,143 +1367,49 @@ window.__ModuleLoader__.load({
1477
1367
  });
1478
1368
  }
1479
1369
 
1480
- // 非模态浮窗:无遮罩、fixed 定位、可拖动;portal 到 body 不受侧栏视图切换影响
1481
- return mountDialog(React.createElement(
1370
+ // 内嵌面板纵向布局:上半对象树(限高滚动,容器类名留给样式层),下半预览主区 + 分页
1371
+ return React.createElement(
1482
1372
  "div",
1483
- {
1484
- className: "dbt-browse-root dbt-browse-dialog" + (dragging ? " dragging" : ""),
1485
- ref: dialogRef2,
1486
- style: { position: "fixed", top: pos.top, left: pos.left },
1487
- role: "dialog", "aria-labelledby": "dbt-browse-title",
1488
- onKeyDown: (e) => {
1489
- if (e.key === "Escape") { e.stopPropagation(); onClose(); }
1490
- },
1491
- },
1492
- // ---- header:面包屑 + 关闭(整条 header 可拖动)----
1373
+ { className: "dbt-view" },
1493
1374
  React.createElement(
1494
1375
  "div",
1495
- {
1496
- className: "dbt-browse-header",
1497
- style: { cursor: dragging ? "grabbing" : "grab" },
1498
- onPointerDown: onHeaderPointerDown,
1499
- onPointerMove: onHeaderPointerMove,
1500
- onPointerUp: stopDrag,
1501
- onPointerCancel: stopDrag,
1502
- },
1503
- React.createElement("div", { className: "dbt-browse-title", id: "dbt-browse-title" },
1504
- !sel ? t("viewBrowse") : t("browseDialogTitle", {
1505
- conn: (conns.find((c) => c.id === sel.connId) || {}).name || sel.connId,
1506
- db: sel.schemaName ? sel.db + "." + sel.schemaName : sel.db,
1507
- table: sel.table.name,
1508
- })),
1509
- React.createElement(
1510
- "button",
1511
- { className: "dbt-browse-close", ref: closeRef, "aria-label": t("close"), onClick: onClose },
1512
- React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
1513
- React.createElement("path", { d: "M2.5 2.5 L9.5 9.5 M9.5 2.5 L2.5 9.5", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round" })),
1514
- ),
1515
- ),
1516
- // ---- body:左树 + 右主区 ----
1376
+ { style: { maxHeight: 280, overflowY: "auto" } },
1377
+ React.createElement(BrowseTree, {
1378
+ conns, projectPath, sel, autoExpand: true, // 内嵌后此树是唯一树实例,挂载即展开第一层级
1379
+ onSelect: (s) => setSel(s), // 树内切表
1380
+ }),
1381
+ ),
1382
+ React.createElement(
1383
+ "div",
1384
+ { className: "dbt-browse-main" },
1385
+ // 结构/数据:mini segmented(复用面板同款与 i18n key)
1517
1386
  React.createElement(
1518
1387
  "div",
1519
- { className: "dbt-browse-body" },
1520
- React.createElement(
1521
- "div",
1522
- { className: "dbt-browse-tree" },
1523
- React.createElement(BrowseTree, {
1524
- conns, projectPath, sel, autoExpand: true, // 本任务后弹窗树是唯一树实例,挂载即展开第一层级
1525
- onSelect: (s) => setSel(s), // 弹窗内树切表
1526
- }),
1527
- ),
1388
+ { className: "dbt-browse-tabs" },
1528
1389
  React.createElement(
1529
1390
  "div",
1530
- { className: "dbt-browse-main" },
1531
- // 结构/数据:mini segmented(复用面板同款与 i18n key)
1532
- React.createElement(
1533
- "div",
1534
- { className: "dbt-browse-tabs" },
1535
- React.createElement(
1536
- "div",
1537
- { className: "dbt-seg" },
1538
- ["structure", "preview"].map((v) =>
1539
- React.createElement("button", { key: v, className: view === v ? "active" : "", onClick: () => setView(v) },
1540
- v === "structure" ? t("structure") : t("preview"))),
1541
- ),
1542
- ),
1543
- React.createElement("div", { className: "dbt-browse-content" }, rightPane()),
1391
+ { className: "dbt-seg" },
1392
+ ["structure", "preview"].map((v) =>
1393
+ React.createElement("button", { key: v, className: view === v ? "active" : "", onClick: () => setView(v) },
1394
+ v === "structure" ? t("structure") : t("preview"))),
1544
1395
  ),
1545
1396
  ),
1546
- // ---- footer:分页条(仅已选表的数据预览视图;空选中态不显示) ----
1547
- sel && view === "preview" && !loadErr ? React.createElement(
1548
- "div",
1549
- { className: "dbt-browse-footer" },
1550
- React.createElement("button", { className: "dbt-btn", disabled: page <= 1 || busy === "open", onClick: () => openTable(sel, page - 1) }, "‹ " + t("prevPage")),
1551
- React.createElement("span", { className: "dbt-muted" }, t("pageInfo", { page })),
1552
- React.createElement("span", { style: { flex: 1 } }),
1553
- preview && preview.truncated ? React.createElement("span", { className: "dbt-muted" }, t("previewTruncated")) : null,
1554
- React.createElement("button", { className: "dbt-btn", disabled: (preview && preview.truncated) === false || busy === "open", onClick: () => openTable(sel, page + 1) }, t("nextPage") + " ›"),
1555
- ) : null,
1397
+ // 限高兜底:无固定高度弹窗壳后给内容区可视上限,预览表在容器内滚动、单元格浮层贴底可见
1398
+ React.createElement("div", { className: "dbt-browse-content", style: { maxHeight: 420 } }, rightPane()),
1556
1399
  ),
1400
+ // 分页条(仅已选表的数据预览视图;空选中态不显示)
1401
+ sel && view === "preview" && !loadErr ? React.createElement(
1402
+ "div",
1403
+ { className: "dbt-browse-footer" },
1404
+ React.createElement("button", { className: "dbt-btn", disabled: page <= 1 || busy === "open", onClick: () => openTable(sel, page - 1) }, "‹ " + t("prevPage")),
1405
+ React.createElement("span", { className: "dbt-muted" }, t("pageInfo", { page })),
1406
+ React.createElement("span", { style: { flex: 1 } }),
1407
+ preview && preview.truncated ? React.createElement("span", { className: "dbt-muted" }, t("previewTruncated")) : null,
1408
+ React.createElement("button", { className: "dbt-btn", disabled: (preview && preview.truncated) === false || busy === "open", onClick: () => openTable(sel, page + 1) }, t("nextPage") + " ›"),
1409
+ ) : null,
1557
1410
  );
1558
1411
  }
1559
1412
 
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
-
1617
1413
  /* ---------------- SQL 控制台 ---------------- */
1618
1414
  // Apple 等宽字体栈(规范 §1):优先样式层 --dbt-mono token,fallback 内联栈
1619
1415
  const MONO_FONT = "var(--dbt-mono, ui-monospace, \"SF Mono\", Menlo, Consolas, monospace)";
@@ -1740,23 +1536,6 @@ window.__ModuleLoader__.load({
1740
1536
  const [error, setError] = React.useState("");
1741
1537
  const [message, setMessage] = React.useState("");
1742
1538
  const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
1743
- // 数据浏览浮窗开关:由 Panel 层持有(不随 view 切换卸载),跨 tab 常驻
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
- }
1760
1539
 
1761
1540
  const askConfirm = React.useCallback((info) => new Promise((resolve) => {
1762
1541
  setConfirmReq(Object.assign({}, info, { resolve }));
@@ -1811,10 +1590,9 @@ window.__ModuleLoader__.load({
1811
1590
  "div",
1812
1591
  { className: "dbt-tabs" },
1813
1592
  [["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")]].map(([v, label]) =>
1814
- // browse tab:切视图的同时直接打开浮窗(单击即用,不必再进树点表)
1815
1593
  React.createElement("button", {
1816
1594
  key: v, className: view === v ? "active" : "",
1817
- onClick: () => { setView(v); if (v === "browse") openBrowse(); },
1595
+ onClick: () => setView(v),
1818
1596
  }, label)),
1819
1597
  ),
1820
1598
  (projectEdited || !projectPath)
@@ -1850,25 +1628,14 @@ window.__ModuleLoader__.load({
1850
1628
  React.createElement("div", { className: "dbt-view", key: view },
1851
1629
  view === "manage" ? React.createElement(ManageView, shared) : null,
1852
1630
  view === "grants" ? React.createElement(GrantsView, shared) : null,
1853
- // browse:提示卡(浮窗由 Panel 层在下方常驻渲染,不随视图切换卸载)
1854
- view === "browse" ? React.createElement(
1855
- "div",
1856
- { className: "dbt-card" },
1857
- // 主提示用正常文字色(dbt-muted 11px 次要色压深底对比不足,视觉审查命中)
1858
- React.createElement("div", { style: { fontSize: 13 } }, t("browsePanelHint")),
1859
- React.createElement("div", { className: "dbt-row" },
1860
- React.createElement("button", { className: "dbt-btn primary", onClick: openBrowse }, t("reopenBrowse"))),
1861
- ) : null,
1631
+ // browse:内嵌浏览面板(对象树 + 结构/数据预览,点表即看)
1632
+ view === "browse" ? React.createElement(BrowsePane, {
1633
+ ctx: props.ctx, conns, projectPath, grants, askConfirm,
1634
+ }) : null,
1862
1635
  view === "console" ? React.createElement(ConsoleView, Object.assign({}, shared, { askConfirm })) : null,
1863
1636
  ),
1864
1637
  React.createElement("div", { className: "dbt-muted" }, t("footerHint")),
1865
1638
  confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
1866
- // 浮窗常驻:portal 到 body 且在 dbt-view 外,切 tab 时不消失
1867
- browseOpen ? React.createElement(BrowseDialog, {
1868
- conns, projectPath, grants, askConfirm,
1869
- ctx: props.ctx, // 宿主 ctx 透传给浮窗,供 portal 异步解析(ctx.modules.import)
1870
- onClose: () => setBrowseOpen(false),
1871
- }) : null,
1872
1639
  );
1873
1640
  }
1874
1641
 
@@ -1891,7 +1658,7 @@ window.__ModuleLoader__.load({
1891
1658
  key: "db-tool-" + String(localeKey || "zh"),
1892
1659
  visible: props.visible,
1893
1660
  scope: props.scope,
1894
- ctx: ctx, // 宿主 ctx 下传,Panel 透传给 BrowseDialog 供 portal 解析
1661
+ ctx: ctx, // 宿主 ctx 下传,Panel 透传给 BrowsePane
1895
1662
  });
1896
1663
  }
1897
1664
 
@@ -1914,7 +1681,7 @@ window.__ModuleLoader__.load({
1914
1681
  return ctx.betterSidebar.registerTab({
1915
1682
  id: TAB_ID,
1916
1683
  title: function () { return t("tabTitle"); },
1917
- icon: function (size) { return icon(size); },
1684
+ icon,
1918
1685
  order: 45,
1919
1686
  single: true,
1920
1687
  component: function (p) {
@@ -1,12 +1,13 @@
1
1
  import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent } from './common.js';
2
- function poolConfig(conn) {
2
+ function poolConfig(conn, overrideUrl) {
3
3
  const base = {
4
4
  max: 10,
5
5
  idleTimeoutMillis: 30000,
6
6
  connectionTimeoutMillis: 5000,
7
7
  };
8
8
  if (conn.url) {
9
- base.connectionString = conn.url;
9
+ // overrideUrl:多主机探测选定台 / 剥 jdbc: 后的最终 connectionString
10
+ base.connectionString = overrideUrl ?? conn.url;
10
11
  }
11
12
  else if (conn.fields) {
12
13
  const f = conn.fields;
@@ -29,8 +30,122 @@ function poolConfig(conn) {
29
30
  base.ssl = { rejectUnauthorized: false };
30
31
  return base;
31
32
  }
33
+ /**
34
+ * 规范化 JDBC / 多主机风格连接 URL。
35
+ *
36
+ * 背景:`jdbc:gaussdb://...` 的 `jdbc:` 是 WHATWG 非特殊 scheme,驱动内 new URL()
37
+ * 把 `gaussdb://...` 当不透明路径 → hostname/port 为空 → 兜底连本机;多主机
38
+ * authority(host:port,host:port)端口段含逗号非数字 → new URL 直接抛 Invalid URL。
39
+ * 故自行按文本解析 authority,不经 WHATWG 解析。
40
+ */
41
+ export function normalizeUrl(url) {
42
+ const stripped = url.replace(/^jdbc:/i, '');
43
+ const schemeEnd = stripped.indexOf('://');
44
+ // 无 authority(非 URL 形态):整串原样保留,交由驱动按既有逻辑处理
45
+ if (schemeEnd < 0)
46
+ return { prefix: stripped, suffix: '' };
47
+ const rest = stripped.slice(schemeEnd + 3);
48
+ const authEnd = /[/?#]/.exec(rest)?.index ?? rest.length;
49
+ const authority = rest.slice(0, authEnd);
50
+ const suffix = rest.slice(authEnd);
51
+ const at = authority.lastIndexOf('@');
52
+ const userinfo = at >= 0 ? authority.slice(0, at + 1) : '';
53
+ const hostPart = authority.slice(at + 1);
54
+ const hosts = hostPart.split(',').map((seg) => {
55
+ // ponytail: 按最后一个冒号拆 host/port,仅支持 IPv4/主机名(IPv6 需方括号感知,遇到再加)
56
+ const ci = seg.lastIndexOf(':');
57
+ if (ci < 0)
58
+ return { host: seg };
59
+ const port = Number.parseInt(seg.slice(ci + 1), 10);
60
+ return Number.isFinite(port) ? { host: seg.slice(0, ci), port } : { host: seg };
61
+ });
62
+ const prefix = `${stripped.slice(0, schemeEnd)}://${userinfo}`;
63
+ // 单台不设 hosts(调用方只在多台时探测)
64
+ if (hosts.length <= 1)
65
+ return { prefix: prefix + hostPart, suffix };
66
+ return { prefix, hosts, suffix };
67
+ }
68
+ /**
69
+ * 逐台探测可用节点(移植自用户项目 GaussDB-MCP 的 probeHosts):返回第一台可连
70
+ * (masterOnly 时须为主节点,pg_is_in_recovery() 为 true 的备节点跳过)的主机;
71
+ * 全部失败返回 undefined(调用方回退首台,让 testConnect 报真实连接错误)。
72
+ */
73
+ // ponytail: 仅适配器创建时探测一次;运行中主备切换不自动 failover,需要时在 pool error 事件里重建池再探测
74
+ export async function pickHost(driver, base, hosts, masterOnly) {
75
+ if (!driver.Client)
76
+ return undefined;
77
+ for (const h of hosts) {
78
+ // urlUser/urlPassword 是调用方传入的内部凭据约定键,转成驱动的 user/password 后剔除
79
+ const { urlUser, urlPassword, ...rest } = base;
80
+ const cfg = { ...rest, connectionString: undefined, host: h.host };
81
+ if (h.port != null)
82
+ cfg.port = h.port;
83
+ // 凭据自 base.urlUser/base.urlPassword 解出;无则不设
84
+ if (typeof urlUser === 'string' && urlUser !== '')
85
+ cfg.user = urlUser;
86
+ if (typeof urlPassword === 'string')
87
+ cfg.password = urlPassword;
88
+ const client = new driver.Client(cfg);
89
+ try {
90
+ await client.connect();
91
+ if (masterOnly) {
92
+ const res = await client.query('SELECT pg_is_in_recovery() AS in_recovery');
93
+ if (res.rows[0]?.in_recovery === true) {
94
+ await client.end().catch(() => { });
95
+ continue; // 备节点跳过
96
+ }
97
+ }
98
+ await client.end().catch(() => { });
99
+ return h.port != null ? { host: h.host, port: h.port } : { host: h.host };
100
+ }
101
+ catch {
102
+ await client.end().catch(() => { });
103
+ }
104
+ }
105
+ return undefined;
106
+ }
32
107
  export async function createPgLikeAdapter(kind, Driver, conn, opts) {
33
- const pool = new Driver.Pool(poolConfig(conn));
108
+ // GaussDB JDBC 风格 URL 驱动自带的 WHATWG 解析处理不了(jdbc: 非特殊 scheme 使
109
+ // hostname/port 为空 → 兜底连本机;多主机 authority 端口段含逗号 → 抛 Invalid URL),
110
+ // 故建池前先规范化,多主机时逐台探测选可用节点;postgresql 多主机列表剥 jdbc: 后
111
+ // 原样透传(pg 驱动原生支持 libpq 多主机 failover),不 probe。
112
+ const norm = conn.url ? normalizeUrl(conn.url) : null;
113
+ const joinSeg = (h) => `${h.host}${h.port != null ? ':' + h.port : ''}`;
114
+ const cleanUrl = norm ? norm.prefix + (norm.hosts ?? []).map(joinSeg).join(',') + norm.suffix : undefined;
115
+ let finalUrl = cleanUrl;
116
+ if (kind === 'gaussdb' && norm?.hosts && norm.hosts.length > 1) {
117
+ const { prefix, suffix } = norm;
118
+ const joinHost = (h) => prefix + joinSeg(h) + suffix;
119
+ const firstUrl = joinHost(norm.hosts[0]);
120
+ // targetServerType=master(键值大小写不敏感)→ 只接受主节点
121
+ const qi = suffix.indexOf('?');
122
+ let masterOnly = false;
123
+ for (const [k, v] of new URLSearchParams(qi >= 0 ? suffix.slice(qi + 1) : '')) {
124
+ if (k.toLowerCase() === 'targetservertype' && v.toLowerCase() === 'master')
125
+ masterOnly = true;
126
+ }
127
+ const base = poolConfig(conn, firstUrl);
128
+ // 探测凭据自 prefix userinfo 解出(按最后一个 @ / 最后一个 : 分界,decode 与
129
+ // percent-encode 注入互逆);仅密码无用户名时 user 不设
130
+ if (prefix.endsWith('@')) {
131
+ const body = prefix.slice(prefix.indexOf('://') + 3, -1);
132
+ const ci = body.lastIndexOf(':');
133
+ if (ci >= 0) {
134
+ const user = decodeURIComponent(body.slice(0, ci));
135
+ if (user !== '')
136
+ base.urlUser = user;
137
+ base.urlPassword = decodeURIComponent(body.slice(ci + 1));
138
+ }
139
+ else if (body !== '') {
140
+ base.urlUser = decodeURIComponent(body);
141
+ }
142
+ }
143
+ const picked = await pickHost(Driver, base, norm.hosts, masterOnly);
144
+ // 全败回退首台重组值:让 testConnect 报真实连接错误
145
+ finalUrl = picked ? joinHost(picked) : firstUrl;
146
+ }
147
+ const cfg = poolConfig(conn, finalUrl);
148
+ const pool = new Driver.Pool(cfg);
34
149
  const readOnly = opts?.mode === 'ro';
35
150
  // 所有池(主池 + 跨库池)统一走此初始化。
36
151
  // pg 约定:release(err) 若无等待者会 emit pool 'error',无监听器会崩进程——
@@ -58,10 +173,11 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
58
173
  const mainDb = () => {
59
174
  if (conn.fields)
60
175
  return conn.fields.database ? String(conn.fields.database) : null;
61
- if (!conn.url)
176
+ if (!finalUrl)
62
177
  return null;
63
178
  try {
64
- return decodeURIComponent(new URL(conn.url).pathname.replace(/^\//, '')) || null;
179
+ // 规范化后的最终 URL(单台 authority)才可被 new URL 正确解析
180
+ return decodeURIComponent(new URL(finalUrl).pathname.replace(/^\//, '')) || null;
65
181
  }
66
182
  catch {
67
183
  return null;
@@ -73,7 +189,8 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
73
189
  const cached = dbPools.get(db);
74
190
  if (cached)
75
191
  return cached;
76
- const created = new Driver.Pool({ ...poolConfig(conn), database: db });
192
+ // 与主池同一份最终 cfg(多主机探测选定台后的 connectionString),仅覆盖库名
193
+ const created = new Driver.Pool({ ...cfg, database: db });
77
194
  setupPool(created); // 与主池一致:error 兜底 + ro 会话只读
78
195
  dbPools.set(db, created);
79
196
  return created;
@@ -10,11 +10,16 @@ import * as path from 'node:path';
10
10
  import { readJson, writeJsonAtomic } from './io.js';
11
11
  /** fields 中被视为机密的键,落盘前拆出到 secrets.json */
12
12
  const PASSWORD_KEY = 'password';
13
+ /** authority 段定位:scheme:// 到首个 /?#(兼容可选 jdbc: 前缀,如 jdbc:gaussdb://),
14
+ * 捕获组 1 为 scheme:// 部分、组 2 为 authority(多主机逗号串整体落在该段内)。
15
+ * redactUrl 与 mergeUrlCredentials 的字符串回退注入共用。 */
16
+ const AUTHORITY_RE = /^((?:jdbc:)?[a-z][a-z0-9+.-]*:\/\/)([^/?#]*)/i;
13
17
  /** 将 URL 中的密码段替换为 ***:scheme://user:pass@host → scheme://user:***@host。
14
18
  * 密码可能包含 / ? # @ 等字符:先取 scheme:// 到首个 /?# 的 authority 段,
15
- * 在 authority 内以最后一个 @ 分界取 userinfo,再以最后一个 : 分界取密码。 */
19
+ * 在 authority 内以最后一个 @ 分界取 userinfo,再以最后一个 : 分界取密码。
20
+ * scheme 兼容可选 jdbc: 前缀(jdbc:gaussdb://…),无前缀行为不变。 */
16
21
  export function redactUrl(url) {
17
- const m = url.match(/^([a-z][a-z0-9+.-]*:\/\/)([^/?#]*)/i);
22
+ const m = url.match(AUTHORITY_RE);
18
23
  if (!m)
19
24
  return url;
20
25
  const authority = m[2] ?? '';
@@ -34,7 +39,12 @@ export function redactUrl(url) {
34
39
  * - url 含脱敏标记 ':***@'(编辑回填值)→ 有 secretsUrl 以其为基底整体替换,否则报错;
35
40
  * - 注入用 WHATWG URL 的 username/password setter(自动 percent-encode '@:/?# ' 等特殊字符),
36
41
  * 与适配器侧 decodeURIComponent / 驱动 RFC 3986 解码严格互逆;只填一项只覆盖一项
37
- * (redis 'redis://:pass@host' 无用户名形态、oracle 必填用户名校验均兼容)。 */
42
+ * (redis 'redis://:pass@host' 无用户名形态、oracle 必填用户名校验均兼容)。
43
+ * - WHATWG 解析失败(如多主机逗号端口 gaussdb://h1:8000,h2:8000/…)或 setter 静默无效
44
+ * (jdbc:gaussdb://… 为 opaque path、host 为空)时回退字符串注入:正则定位 authority 段
45
+ * (兼容可选 jdbc: 前缀),段内以最后一个 @ 为界替换/插入 userinfo,其余部分逐字保留;
46
+ * 凭据按 encodeURIComponent 编码,只填一项只覆盖一项的语义与 setter 对齐;
47
+ * 无 scheme://authority 形态的残缺串仍报「URL 无效」。 */
38
48
  export function mergeUrlCredentials(opts) {
39
49
  const { url, urlUser, urlPassword, secretsUrl } = opts;
40
50
  if (urlUser === undefined && urlPassword === undefined)
@@ -59,7 +69,15 @@ export function mergeUrlCredentials(opts) {
59
69
  u = new URL(base);
60
70
  }
61
71
  catch {
62
- throw new Error('连接 URL 无效,无法注入用户名/密码');
72
+ u = undefined; // WHATWG 解析失败:多主机逗号端口、残缺串等
73
+ }
74
+ const m = base.match(AUTHORITY_RE);
75
+ // 回退条件:解析失败;或 opaque path(host 为空、setter no-op)且 authority 非空。
76
+ // authority 为空(sqlite:// 等无主机形态)不回退,维持 setter no-op 的现状语义。
77
+ if (u === undefined || (u.host === '' && (m?.[2] ?? '') !== '')) {
78
+ if (m === null)
79
+ throw new Error('连接 URL 无效,无法注入用户名/密码');
80
+ return injectUserInfo(base, m, urlUser, urlPassword);
63
81
  }
64
82
  if (urlUser !== undefined && urlUser !== '')
65
83
  u.username = urlUser;
@@ -67,6 +85,35 @@ export function mergeUrlCredentials(opts) {
67
85
  u.password = urlPassword;
68
86
  return u.toString();
69
87
  }
88
+ /** 字符串回退注入:在 authority(m[2])内以最后一个 @ 为界替换/插入 userinfo。
89
+ * 与 WHATWG setter 语义对齐:只填一项只覆盖一项(另一项保留基底值);
90
+ * 两者均无效时不改动原样返回。凭据按 encodeURIComponent 编码(与适配器侧
91
+ * decodeURIComponent 互逆),authority 之外(path/query/fragment)逐字保留。 */
92
+ function injectUserInfo(base, m, urlUser, urlPassword) {
93
+ const whole = m[0] ?? '';
94
+ const authority = m[2] ?? '';
95
+ const user = urlUser !== undefined && urlUser !== '' ? encodeURIComponent(urlUser) : undefined;
96
+ const pass = urlPassword !== undefined && urlPassword !== '' ? encodeURIComponent(urlPassword) : undefined;
97
+ if (user === undefined && pass === undefined)
98
+ return base;
99
+ const at = authority.lastIndexOf('@');
100
+ const oldUserinfo = at >= 0 ? authority.slice(0, at) : '';
101
+ const colon = oldUserinfo.lastIndexOf(':');
102
+ const oldUser = colon >= 0 ? oldUserinfo.slice(0, colon) : oldUserinfo;
103
+ const oldPass = colon >= 0 ? oldUserinfo.slice(colon + 1) : undefined;
104
+ let userinfo;
105
+ if (user !== undefined && pass !== undefined) {
106
+ userinfo = `${user}:${pass}`;
107
+ }
108
+ else if (user !== undefined) {
109
+ userinfo = oldPass !== undefined ? `${user}:${oldPass}` : user;
110
+ }
111
+ else {
112
+ userinfo = `${oldUser}:${pass}`;
113
+ }
114
+ const hostPart = at >= 0 ? authority.slice(at + 1) : authority;
115
+ return whole.slice(0, whole.length - authority.length) + userinfo + '@' + hostPart + base.slice(whole.length);
116
+ }
70
117
  function splitPassword(fields) {
71
118
  if (!fields || typeof fields[PASSWORD_KEY] !== 'string') {
72
119
  return { clean: fields, password: undefined };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
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,8 +38,7 @@
38
38
  },
39
39
  "client": {
40
40
  "inject": [
41
- "@deepseek-ai/dsh-client-locale",
42
- "react-dom"
41
+ "@deepseek-ai/dsh-client-locale"
43
42
  ],
44
43
  "platform": "web"
45
44
  }