dsh-db-tool 1.6.0 → 1.7.1

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
@@ -26,6 +26,72 @@ window.__ModuleLoader__.load({
26
26
  viewGrants: "项目授权",
27
27
  viewBrowse: "数据浏览",
28
28
  viewConsole: "SQL 控制台",
29
+ viewTransfer: "数据传输",
30
+ viewTransferLog: "传输日志",
31
+ tlogEmpty: "暂无传输日志",
32
+ tlogAuto: "自动刷新",
33
+ tlogClear: "清除日志",
34
+ tlogClearConfirm: "确定清除全部传输日志?",
35
+ tlogTypeStart: "开始",
36
+ tlogTypeTable: "表",
37
+ tlogTypeProgress: "进度",
38
+ tlogTypeFinish: "完成",
39
+ trSource: "源连接",
40
+ trTarget: "目标连接",
41
+ trTables: "数据库对象",
42
+ trGroupTable: "表",
43
+ trGroupCollection: "集合",
44
+ trGroupView: "视图",
45
+ trGroupOther: "其他",
46
+ trSelectAll: "全选",
47
+ trClear: "清空",
48
+ trWriteMode: "写入模式",
49
+ trModeInsert: "插入(冲突报错)",
50
+ trModeIgnore: "跳过重复",
51
+ trModeReplace: "覆盖(按主键)",
52
+ trModeTruncate: "清空后写入(危险)",
53
+ trBatchSize: "每批行数",
54
+ trTableConcurrency: "表级并行",
55
+ trShardConcurrency: "分片并行",
56
+ trStart: "开始传输",
57
+ trCancel: "取消任务",
58
+ trProgress: "传输进度",
59
+ trStatus: "状态",
60
+ trRows: "已写入行数",
61
+ trShards: "分片",
62
+ trFailures: "失败清单",
63
+ trStatusRunning: "进行中",
64
+ trStatusDone: "已完成",
65
+ trStatusFailed: "有失败",
66
+ trStatusCancelled: "已取消",
67
+ trStatusPending: "排队中",
68
+ trxOverwrite: "覆盖已有表结构(删除重建,危险)",
69
+ trxOverwriteHint: "已存在的目标表将被删除重建,其中数据全部丢失",
70
+ trHistory: "传输历史",
71
+ trHistoryClear: "清除历史",
72
+ trHistoryClearConfirm: "确定清除当前项目的全部传输历史?",
73
+ trHistoryEmpty: "暂无历史记录",
74
+ trSameConn: "源与目标上下文相同,请更改库或模式",
75
+ trLoadingTables: "加载表清单…",
76
+ // 数据传输(Navicat 式双栏)
77
+ trxSource: "源",
78
+ trxTarget: "目标",
79
+ trxConn: "连接:",
80
+ trxDatabase: "数据库:",
81
+ trxSchema: "模式:",
82
+ trxInfo: "信息",
83
+ trxConnType: "连接类型",
84
+ trxConnName: "连接名称",
85
+ trxHost: "主机",
86
+ trxPort: "端口",
87
+ trxServerVer: "服务器版本",
88
+ trxNoDbLevel: "默认上下文",
89
+ trxPickSource: "请选择源连接以加载表清单",
90
+ trxSelectedCount: "已选 {n} 项",
91
+ trxOptions: "传输选项",
92
+ trxArrowLabel: "传输方向:源到目标",
93
+ trxPickTable: "请至少勾选一张表",
94
+ trxLoadDbsFail: "库清单加载失败",
29
95
  footerHint: "管理在此侧栏;业务操作要求当前项目已获得对应连接的授权。",
30
96
  // 连接管理
31
97
  newConn: "新建连接",
@@ -50,6 +116,7 @@ window.__ModuleLoader__.load({
50
116
  testOk: "连接成功",
51
117
  testFail: "连接失败",
52
118
  edit: "编辑",
119
+ trRefresh: "刷新(重新拉取已展开的库/模式/表)",
53
120
  delete: "删除",
54
121
  deleteConnConfirm: "确定删除连接「{name}」?将级联删除其密钥与所有项目的授权。",
55
122
  fieldKind: "数据库类型",
@@ -178,6 +245,72 @@ window.__ModuleLoader__.load({
178
245
  viewGrants: "Project Access",
179
246
  viewBrowse: "Browse",
180
247
  viewConsole: "SQL Console",
248
+ viewTransfer: "Data Transfer",
249
+ viewTransferLog: "Transfer Log",
250
+ tlogEmpty: "No transfer log yet",
251
+ tlogAuto: "Auto refresh",
252
+ tlogClear: "Clear log",
253
+ tlogClearConfirm: "Clear all transfer log?",
254
+ tlogTypeStart: "Start",
255
+ tlogTypeTable: "Table",
256
+ tlogTypeProgress: "Progress",
257
+ tlogTypeFinish: "Finish",
258
+ trSource: "Source connection",
259
+ trTarget: "Target connection",
260
+ trTables: "Database objects",
261
+ trGroupTable: "Tables",
262
+ trGroupCollection: "Collections",
263
+ trGroupView: "Views",
264
+ trGroupOther: "Other",
265
+ trSelectAll: "Select all",
266
+ trClear: "Clear",
267
+ trWriteMode: "Write mode",
268
+ trModeInsert: "Insert (error on conflict)",
269
+ trModeIgnore: "Skip duplicates",
270
+ trModeReplace: "Replace (by primary key)",
271
+ trModeTruncate: "Clear then write (dangerous)",
272
+ trBatchSize: "Batch size",
273
+ trTableConcurrency: "Table parallelism",
274
+ trShardConcurrency: "Shard parallelism",
275
+ trStart: "Start transfer",
276
+ trCancel: "Cancel task",
277
+ trProgress: "Transfer progress",
278
+ trStatus: "Status",
279
+ trRows: "Rows written",
280
+ trShards: "Shards",
281
+ trFailures: "Failures",
282
+ trStatusRunning: "Running",
283
+ trStatusDone: "Done",
284
+ trStatusFailed: "Failed",
285
+ trStatusCancelled: "Cancelled",
286
+ trStatusPending: "Pending",
287
+ trxOverwrite: "Overwrite existing table structure (drop & recreate, dangerous)",
288
+ trxOverwriteHint: "Existing target tables will be dropped and recreated; their data will be lost",
289
+ trHistory: "Transfer History",
290
+ trHistoryClear: "Clear history",
291
+ trHistoryClearConfirm: "Clear all transfer history for this project?",
292
+ trHistoryEmpty: "No history yet",
293
+ trSameConn: "Source and target share the same context; pick a different database or schema",
294
+ trLoadingTables: "Loading tables…",
295
+ // Data transfer (Navicat-style two columns)
296
+ trxSource: "Source",
297
+ trxTarget: "Target",
298
+ trxConn: "Connection:",
299
+ trxDatabase: "Database:",
300
+ trxSchema: "Schema:",
301
+ trxInfo: "Information",
302
+ trxConnType: "Type",
303
+ trxConnName: "Name",
304
+ trxHost: "Host",
305
+ trxPort: "Port",
306
+ trxServerVer: "Server version",
307
+ trxNoDbLevel: "Default context",
308
+ trxPickSource: "Pick a source connection to load tables",
309
+ trxSelectedCount: "{n} selected",
310
+ trxOptions: "Transfer options",
311
+ trxArrowLabel: "Transfer direction: source to target",
312
+ trxPickTable: "Select at least one table",
313
+ trxLoadDbsFail: "Failed to load databases",
181
314
  footerHint: "Manage here in the sidebar; business operations require a project grant on the connection.",
182
315
  newConn: "New Connection",
183
316
  editConn: "Edit Connection",
@@ -201,6 +334,7 @@ window.__ModuleLoader__.load({
201
334
  testOk: "Connection OK",
202
335
  testFail: "Connection failed",
203
336
  edit: "Edit",
337
+ trRefresh: "Refresh (reload expanded databases/schemas/tables)",
204
338
  delete: "Delete",
205
339
  deleteConnConfirm: "Delete connection \"{name}\"? Its secrets and all project grants will be removed.",
206
340
  fieldKind: "Database type",
@@ -409,6 +543,7 @@ window.__ModuleLoader__.load({
409
543
  ".dbt-browse-root{color:var(--dbt-text);}",
410
544
  "/* ===== 根容器 ===== */",
411
545
  ".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;}",
546
+ ".dbt-panel input[type=\"checkbox\"]{width:16px;height:16px;flex:none;margin:0;accent-color:var(--dbt-accent);}",
412
547
  "/* ===== iOS segmented control(顶部 tab) ===== */",
413
548
  ".dbt-tabs{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
414
549
  ".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:background .18s var(--dbt-ease),box-shadow .18s var(--dbt-ease);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
@@ -489,6 +624,31 @@ window.__ModuleLoader__.load({
489
624
  ".dbt-browse-skel{display:flex;flex-direction:column;gap:8px;}",
490
625
  ".dbt-browse-skelrow{display:flex;gap:8px;}",
491
626
  ".dbt-browse-skelcell{flex:1;height:24px;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);}",
627
+ "/* ===== 数据传输(Navicat 式双栏:摘要条 + 源/目标栏 + 信息区) ===== */",
628
+ ".dbt-transfer-summary{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;}",
629
+ ".dbt-transfer-endpoint{display:flex;align-items:baseline;gap:4px;min-width:0;}",
630
+ ".dbt-transfer-endpoint strong,.dbt-transfer-endpoint span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px;}",
631
+ ".dbt-transfer-cols{display:flex;gap:8px;align-items:stretch;}",
632
+ ".dbt-transfer-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:6px;}",
633
+ ".dbt-transfer-coltitle,.dbt-transfer-infotitle{font-size:var(--dbt-caption,11px);font-weight:600;color:var(--dbt-accent);}",
634
+ ".dbt-transfer-field{display:flex;flex-direction:column;gap:2px;min-width:0;}",
635
+ ".dbt-transfer-fieldlabel{font-size:var(--dbt-caption-xs,10px);color:var(--dbt-text-secondary);}",
636
+ ".dbt-transfer-divider{flex:none;align-self:center;color:var(--dbt-text-secondary);}",
637
+ ".dbt-transfer-info{border-top:1px solid var(--dbt-separator);padding-top:6px;display:flex;flex-direction:column;gap:3px;}",
638
+ ".dbt-transfer-kv{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;margin:0;font-size:var(--dbt-caption-xs,10px);}",
639
+ ".dbt-transfer-kv dt{color:var(--dbt-text-secondary);}",
640
+ ".dbt-transfer-kv dd{margin:0;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
641
+ ".dbt-transfer-tablelist{max-height:220px;overflow:auto;border:1px solid var(--dbt-separator);border-radius:var(--dbt-radius-ctrl,6px);padding:3px 0;font-size:12px;background:var(--dbt-surface-strong);}",
642
+ ".dbt-tr-group-head{display:flex;align-items:center;gap:6px;padding:3px 8px;min-height:26px;font-weight:600;cursor:pointer;user-select:none;}",
643
+ ".dbt-tr-group-head:hover{background:var(--dbt-surface);}",
644
+ ".dbt-tr-caret{display:inline-block;width:0;height:0;border-left:4px solid currentColor;border-top:3px solid transparent;border-bottom:3px solid transparent;opacity:.55;transform:rotate(0deg);transition:transform .12s var(--dbt-ease);flex:none;}",
645
+ ".dbt-tr-caret.open{transform:rotate(90deg);}",
646
+ ".dbt-tr-item{display:flex;align-items:center;padding:1px 8px 1px 26px;min-height:24px;}",
647
+ ".dbt-tr-item:hover{background:var(--dbt-surface);}",
648
+ ".dbt-tr-pair{display:flex;align-items:center;gap:6px;cursor:pointer;min-width:0;}",
649
+ ".dbt-tr-pair span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
650
+ ".dbt-tr-comment{color:var(--dbt-text-secondary);font-size:11px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-width:0;}",
651
+ ".dbt-transfer-options>summary{cursor:pointer;font-size:var(--dbt-caption,11px);color:var(--dbt-text-secondary);padding:4px 0;}",
492
652
  "/* ===== 浏览面板:单元格按钮与详情底栏(Apple 分区卡) ===== */",
493
653
  ".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;}",
494
654
  ".dbt-cellbtn:focus-visible{outline:2px solid var(--dbt-accent);outline-offset:-1px;border-radius:2px;}",
@@ -1485,7 +1645,7 @@ window.__ModuleLoader__.load({
1485
1645
  /* ---------------- 数据浏览 ---------------- */
1486
1646
  // Navicat 式对象树:连接 ▸ 库/[schema] ▸ 表,懒加载展开(浏览面板唯一树实例,展开状态随面板生命周期)
1487
1647
  function BrowseTree(props) {
1488
- const { conns, projectPath, sel, onSelect, autoExpand } = props;
1648
+ const { conns, projectPath, sel, onSelect, autoExpand, refreshTick } = props;
1489
1649
  // sel 由父组件持有(弹窗内 active 高亮与初始选中都依赖);onSelect(选中记录, 触发元素) 上报
1490
1650
  const [open, setOpen] = React.useState({}); // "c:<id>" | "d:<id>/<db>" -> bool
1491
1651
  const [loading, setLoading] = React.useState({});
@@ -1499,6 +1659,50 @@ window.__ModuleLoader__.load({
1499
1659
  setOpen({}); setLoading({}); setError({}); setDbs({}); setSchemasMap({}); setTablesMap({});
1500
1660
  }, [projectPath]);
1501
1661
 
1662
+ // 三层加载统一入口(toggle 展开与手动/自动刷新共用;key: c:<id> | d:<id>/<db> | s:<id>/<db>/<schema>)
1663
+ function loadByKey(key) {
1664
+ const cm = /^c:(.+)$/.exec(key);
1665
+ const dm = /^d:(.+?)\/(.+)$/.exec(key);
1666
+ const sm = /^s:(.+?)\/(.+?)\/(.+)$/.exec(key);
1667
+ const connOf = (id) => conns.find((x) => x.id === id);
1668
+ if (cm) {
1669
+ const c = connOf(cm[1]);
1670
+ if (!c) return Promise.resolve();
1671
+ return api("databases" + qs({ project: projectPath, connId: c.id })).then((list) => {
1672
+ setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
1673
+ });
1674
+ }
1675
+ if (dm) {
1676
+ const c = connOf(dm[1]);
1677
+ if (!c) return Promise.resolve();
1678
+ const useSchemas = !!HAS_SCHEMAS[c.kind];
1679
+ const what = useSchemas ? "schemas" : "tables";
1680
+ return api(what + qs({ project: projectPath, connId: c.id, database: dm[2] })).then((list) => {
1681
+ const setter = useSchemas ? setSchemasMap : setTablesMap;
1682
+ setter((m) => Object.assign({}, m, { [c.id + "/" + dm[2]]: list || [] }));
1683
+ });
1684
+ }
1685
+ if (sm) {
1686
+ return api("tables" + qs({ project: projectPath, connId: sm[1], database: sm[2] + "." + sm[3] })).then((list) => {
1687
+ setTablesMap((m) => Object.assign({}, m, { [sm[1] + "/" + sm[2] + "/" + sm[3]]: list || [] }));
1688
+ });
1689
+ }
1690
+ return Promise.resolve();
1691
+ }
1692
+
1693
+ // 手动刷新按钮 / 侧边栏切回可见:对所有已展开节点重新拉取(新库/新表即时可见),展开态保持
1694
+ React.useEffect(() => {
1695
+ if (!refreshTick) return;
1696
+ for (const [key, isOpen] of Object.entries(open)) {
1697
+ if (!isOpen || loading[key]) continue;
1698
+ setError((s) => Object.assign({}, s, { [key]: undefined }));
1699
+ setLoading((s) => Object.assign({}, s, { [key]: true }));
1700
+ loadByKey(key)
1701
+ .catch((e) => setError((s) => Object.assign({}, s, { [key]: e && e.message ? e.message : String(e) })))
1702
+ .finally(() => setLoading((s) => Object.assign({}, s, { [key]: false })));
1703
+ }
1704
+ }, [refreshTick]); // eslint-disable-line
1705
+
1502
1706
  function toggle(key, load) {
1503
1707
  const isOpen = !!open[key];
1504
1708
  setOpen((o) => Object.assign({}, o, { [key]: !isOpen }));
@@ -1517,10 +1721,7 @@ window.__ModuleLoader__.load({
1517
1721
  }
1518
1722
  function toggleConn(c) {
1519
1723
  if (!projectPath) return;
1520
- toggle("c:" + c.id, () =>
1521
- api("databases" + qs({ project: projectPath, connId: c.id })).then((list) => {
1522
- setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
1523
- }));
1724
+ toggle("c:" + c.id, () => loadByKey("c:" + c.id));
1524
1725
  }
1525
1726
  // autoExpand:挂载后自动展开第一层级(连接节点),复用 toggleConn 的展开+懒加载;
1526
1727
  // 加载失败仍走既有就地错误行机制。声明在清缓存 effect 之后:项目切换时先清 open 再重新展开。
@@ -1535,21 +1736,12 @@ window.__ModuleLoader__.load({
1535
1736
  const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
1536
1737
  function toggleDb(c, d) {
1537
1738
  if (!projectPath) return;
1538
- const useSchemas = !!HAS_SCHEMAS[c.kind];
1539
- const what = useSchemas ? "schemas" : "tables";
1540
- toggle("d:" + c.id + "/" + d, () =>
1541
- api(what + qs({ project: projectPath, connId: c.id, database: d })).then((list) => {
1542
- const setter = useSchemas ? setSchemasMap : setTablesMap;
1543
- setter((m) => Object.assign({}, m, { [c.id + "/" + d]: list || [] }));
1544
- }));
1739
+ toggle("d:" + c.id + "/" + d, () => loadByKey("d:" + c.id + "/" + d));
1545
1740
  }
1546
1741
  function toggleSchema(c, d, s) {
1547
1742
  if (!projectPath) return;
1548
1743
  // PG 系跨库浏览:tables 的 database 传 "库名.schema"(Navicat 官方行为,服务端按库开连接)
1549
- toggle("s:" + c.id + "/" + d + "/" + s, () =>
1550
- api("tables" + qs({ project: projectPath, connId: c.id, database: d + "." + s })).then((list) => {
1551
- setTablesMap((m) => Object.assign({}, m, { [c.id + "/" + d + "/" + s]: list || [] }));
1552
- }));
1744
+ toggle("s:" + c.id + "/" + d + "/" + s, () => loadByKey("s:" + c.id + "/" + d + "/" + s));
1553
1745
  }
1554
1746
  // 树键盘导航(WAI-ARIA treeview / roving tabindex):Tab 仅进停靠行,方向键在可见行间移动;
1555
1747
  // 右箭头展开、左箭头收起或回到父级(父级 = DOM 序向上第一个更浅层级行),Home/End 跳首尾
@@ -1868,7 +2060,7 @@ window.__ModuleLoader__.load({
1868
2060
  "label",
1869
2061
  // 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
1870
2062
  { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
1871
- React.createElement("input", { type: "checkbox", style: { flex: "none", minWidth: 0, width: "auto" }, checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
2063
+ React.createElement("input", { type: "checkbox", checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
1872
2064
  t("setNull"),
1873
2065
  ) : null,
1874
2066
  truncHint && truncHint !== selReason ? React.createElement("span", { className: "dbt-readhint" }, t(truncHint)) : null,
@@ -1994,6 +2186,7 @@ window.__ModuleLoader__.load({
1994
2186
  React.createElement(BrowseTree, {
1995
2187
  conns, projectPath, sel,
1996
2188
  autoExpand: !!active, // 首次激活才自动展开连接层级(常驻挂载下隐藏不发请求)
2189
+ refreshTick: props.refreshTick, // 手动刷新按钮 / 侧边栏切回可见时重拉已展开层
1997
2190
  onSelect: (s) => setSel(s), // 树内切表
1998
2191
  }),
1999
2192
  ),
@@ -2616,6 +2809,579 @@ window.__ModuleLoader__.load({
2616
2809
  );
2617
2810
  }
2618
2811
 
2812
+ /* ---------------- 数据传输(Navicat 式双栏:摘要条 + 源/目标层级联动 + 信息区 + 表清单 + 进度轮询) ---------------- */
2813
+ // 双向箭头(摘要条与栏间分隔共用;stroke currentColor 随容器变色)
2814
+ function transferArrow() {
2815
+ return React.createElement("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.8", "aria-hidden": true },
2816
+ React.createElement("path", { d: "M4 8.5h15M15.5 4.5L19 8.5l-3.5 4" }),
2817
+ React.createElement("path", { d: "M20 15.5H5M8.5 11.5L5 15.5l3.5 4" }));
2818
+ }
2819
+ // 信息区服务器版本探测:300ms 防抖 + 按 connId 缓存(组件卸载丢弃)+ 失败静默为空串 + 切走后只写缓存不改显示
2820
+ function useServerVer(connId, cacheRef) {
2821
+ const [ver, setVer] = React.useState("");
2822
+ const liveRef = React.useRef(connId); // 乱序护栏:慢响应回来时只服务当前选中连接
2823
+ React.useEffect(() => {
2824
+ liveRef.current = connId;
2825
+ if (!connId) { setVer(""); return undefined; }
2826
+ const cached = cacheRef.current[connId];
2827
+ if (cached !== undefined) { setVer(cached); return undefined; }
2828
+ const asked = connId;
2829
+ const timer = window.setTimeout(() => {
2830
+ api("connections/" + encodeURIComponent(asked) + "/test", { method: "POST", body: {} })
2831
+ .then((r) => {
2832
+ const info = r && r.ok && r.serverInfo ? String(r.serverInfo) : "";
2833
+ cacheRef.current[asked] = info; // 失败也缓存空串,会话内不反复探测
2834
+ if (liveRef.current === asked) setVer(info);
2835
+ })
2836
+ .catch(() => { cacheRef.current[asked] = ""; });
2837
+ }, 300);
2838
+ return () => window.clearTimeout(timer);
2839
+ }, [connId]);
2840
+ return ver;
2841
+ }
2842
+ function TransferView(props) {
2843
+ const conns = props.conns || [];
2844
+ const projectPath = props.projectPath;
2845
+ const askConfirm = props.askConfirm;
2846
+ const visible = props.visible; // Panel 传入:视图隐藏时停轮询
2847
+ const [srcId, setSrcId] = React.useState("");
2848
+ const [dstId, setDstId] = React.useState("");
2849
+ // 层级状态(BrowsePane 同语义:database=对象树第一层,schema=第二层仅 pg 系;""=连接默认上下文)
2850
+ const [srcDb, setSrcDb] = React.useState("");
2851
+ const [srcSchema, setSrcSchema] = React.useState("");
2852
+ const [dstDb, setDstDb] = React.useState("");
2853
+ const [dstSchema, setDstSchema] = React.useState("");
2854
+ const [srcDbs, setSrcDbs] = React.useState(null); // null=未加载
2855
+ const [srcSchemas, setSrcSchemas] = React.useState(null);
2856
+ const [dstDbs, setDstDbs] = React.useState(null);
2857
+ const [dstSchemas, setDstSchemas] = React.useState(null);
2858
+ const [dbsErr, setDbsErr] = React.useState({ src: "", dst: "" }); // 库清单加载失败就地显示(不弹全局)
2859
+ const [srcTables, setSrcTables] = React.useState([]); // {name,type}[] 可传输对象(表/集合)
2860
+ const [loadingTables, setLoadingTables] = React.useState(false);
2861
+ const [tablesErr, setTablesErr] = React.useState("");
2862
+ const [tablesTick, setTablesTick] = React.useState(0); // 表清单就地重试
2863
+ const [selected, setSelected] = React.useState([]); // 已勾选表名
2864
+ const [writeMode, setWriteMode] = React.useState("insert");
2865
+ const [batchSize, setBatchSize] = React.useState(1000);
2866
+ const [tableConcurrency, setTableConcurrency] = React.useState(4);
2867
+ const [shardConcurrency, setShardConcurrency] = React.useState(4);
2868
+ const [overwriteStructure, setOverwriteStructure] = React.useState(false);
2869
+ const [subTab, setSubTab] = React.useState("history"); // history=传输历史 | tlog=传输日志(同级子标签)
2870
+ const [liveTasks, setLiveTasks] = React.useState(null); // 运行中任务快照(2s 轮询)
2871
+ // 实时进度轮询:标签可见即轮(不依赖本视图发起的任务,重启/外部发起的传输也可见)
2872
+ React.useEffect(() => {
2873
+ const loadLive = () => {
2874
+ api("transfer/live" + qs({ project: projectPath }))
2875
+ .then((list) => setLiveTasks(Array.isArray(list) ? list : []))
2876
+ .catch(() => setLiveTasks([]));
2877
+ };
2878
+ loadLive();
2879
+ if (!visible) return undefined;
2880
+ const timer = setInterval(loadLive, 2000);
2881
+ return () => clearInterval(timer);
2882
+ }, [visible, projectPath]);
2883
+ const pct = (rows, total) => (total != null && total > 0 ? Math.min(100, Math.round((rows / total) * 100)) + "%" : "—");
2884
+ const [task, setTask] = React.useState(null); // {id, snap}
2885
+ const [busy, setBusy] = React.useState(false);
2886
+ const [error, setError] = React.useState("");
2887
+ const pollRef = React.useRef(null);
2888
+ const mountedRef = React.useRef(true);
2889
+ const dbsCacheRef = React.useRef({}); // connId -> string[]
2890
+ const schemasCacheRef = React.useRef({}); // connId + "/" + db -> string[]
2891
+ const verCacheRef = React.useRef({}); // connId -> serverInfo(含失败空串)
2892
+ React.useEffect(() => () => { mountedRef.current = false; }, []);
2893
+ const srcVer = useServerVer(srcId, verCacheRef);
2894
+ const dstVer = useServerVer(dstId, verCacheRef);
2895
+
2896
+ const srcConn = conns.find((c) => c.id === srcId);
2897
+ const dstConn = conns.find((c) => c.id === dstId);
2898
+ const dstGrant = (props.grants || []).find((x) => x.connId === dstId);
2899
+ // PG/GaussDB 三层(库→模式→表),与 BrowsePane 同一张表;sqlite 无库层(连接默认上下文)
2900
+ const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
2901
+ const NO_DB_LEVEL = { sqlite: true };
2902
+
2903
+ // 项目切换清层级缓存(对齐 BrowsePane:避免陈旧授权下的旧数据)
2904
+ React.useEffect(() => {
2905
+ dbsCacheRef.current = {}; schemasCacheRef.current = {};
2906
+ setSrcDb(""); setSrcSchema(""); setDstDb(""); setDstSchema(""); setSrcDbs(null); setSrcSchemas(null); setDstDbs(null); setDstSchemas(null);
2907
+ setDbsErr({ src: "", dst: "" });
2908
+ }, [projectPath]);
2909
+
2910
+ // 库清单加载(BrowsePane 同端点;缓存防重复拉取,失败就地记 dbsErr 可重试)
2911
+ function loadDbs(slot, connId) {
2912
+ const conn = conns.find((c) => c.id === connId);
2913
+ if (!connId || !projectPath || !conn || NO_DB_LEVEL[conn.kind]) return;
2914
+ const cached = dbsCacheRef.current[connId];
2915
+ if (cached) {
2916
+ if (slot === "src") setSrcDbs(cached); else setDstDbs(cached);
2917
+ return;
2918
+ }
2919
+ api("databases" + qs({ project: projectPath, connId }))
2920
+ .then((list) => {
2921
+ const v = Array.isArray(list) ? list : [];
2922
+ dbsCacheRef.current[connId] = v;
2923
+ if (slot === "src") setSrcDbs(v); else setDstDbs(v);
2924
+ })
2925
+ .catch((e) => setDbsErr((s) => Object.assign({}, s, { [slot]: String(e && e.message ? e.message : e) })));
2926
+ }
2927
+ // 源连接变化 → 清层级与表选中,拉库清单(目标侧连接变化只联动自身)
2928
+ React.useEffect(() => {
2929
+ setSrcDb(""); setSrcSchema(""); setSrcSchemas(null); setTablesErr("");
2930
+ setDbsErr((s) => Object.assign({}, s, { src: "" }));
2931
+ loadDbs("src", srcId);
2932
+ }, [srcId, projectPath]); // eslint-disable-line
2933
+ React.useEffect(() => {
2934
+ setDstDb(""); setDstSchema(""); setDstSchemas(null);
2935
+ setDbsErr((s) => Object.assign({}, s, { dst: "" }));
2936
+ loadDbs("dst", dstId);
2937
+ }, [dstId, projectPath]); // eslint-disable-line
2938
+ // 库变化(pg 系)→ 清模式并拉库内 schema 清单,完成后自动补选第一项(Navicat 式:进库即见表);源/目标共用
2939
+ function loadSchemas(slot, connId, db) {
2940
+ const conn = conns.find((c) => c.id === connId);
2941
+ if (!connId || !db || !HAS_SCHEMAS[conn && conn.kind]) return;
2942
+ const setList = slot === "src" ? setSrcSchemas : setDstSchemas;
2943
+ const setPick = slot === "src" ? setSrcSchema : setDstSchema;
2944
+ setPick(""); setList(null);
2945
+ const ck = connId + "/" + db;
2946
+ const cached = schemasCacheRef.current[ck];
2947
+ if (cached) { setList(cached); setPick(cached[0] || ""); return; }
2948
+ api("schemas" + qs({ project: projectPath, connId, database: db }))
2949
+ .then((list) => {
2950
+ const v = Array.isArray(list) ? list : [];
2951
+ schemasCacheRef.current[ck] = v;
2952
+ setList(v);
2953
+ setPick(v[0] || "");
2954
+ })
2955
+ .catch(() => { setList([]); }); // 失败置空清单:模式/表区按「无模式」呈现,不弹全局错误
2956
+ }
2957
+ React.useEffect(() => { loadSchemas("src", srcId, srcDb); }, [srcId, srcDb, projectPath]); // eslint-disable-line
2958
+ React.useEffect(() => { loadSchemas("dst", dstId, dstDb); }, [dstId, dstDb, projectPath]); // eslint-disable-line
2959
+ // 表清单按源层级加载:pg 系需库+模式齐备(服务端 tables.database="库名.模式" 复合形态,与 BrowsePane 一致);
2960
+ // 其余库层直传库名(""=默认库),sqlite/mongo 按连接默认上下文。只列 BASE TABLE/COLLECTION。乱序护栏 stopped。
2961
+ React.useEffect(() => {
2962
+ setSrcTables([]); setSelected([]); setTablesErr("");
2963
+ if (!srcId || !projectPath) return undefined;
2964
+ const kind = srcConn && srcConn.kind;
2965
+ let database;
2966
+ if (NO_DB_LEVEL[kind]) database = undefined;
2967
+ else if (HAS_SCHEMAS[kind]) {
2968
+ if (!srcDb || !srcSchema) return undefined; // 等模式下拉就位(schema effect 自动补选)
2969
+ database = srcDb + "." + srcSchema;
2970
+ } else database = srcDb || undefined;
2971
+ let stopped = false;
2972
+ setLoadingTables(true);
2973
+ api("tables" + qs({ project: projectPath, connId: srcId, database }))
2974
+ .then((list) => {
2975
+ if (stopped) return;
2976
+ const rows = (Array.isArray(list) ? list : [])
2977
+ .filter((x) => !x || !x.type || String(x.type).toUpperCase() === "TABLE" || String(x.type).toUpperCase() === "COLLECTION" || String(x.type).toUpperCase() === "BASE TABLE")
2978
+ .map((x) => ({ name: String(x.name), type: String((x && x.type) || "").toUpperCase(), comment: x && x.comment ? String(x.comment) : "" }))
2979
+ .filter((x) => x.name);
2980
+ setSrcTables(rows);
2981
+ })
2982
+ .catch((e) => { if (!stopped) { setSrcTables([]); setTablesErr(String(e && e.message ? e.message : e)); } })
2983
+ .finally(() => { if (!stopped) setLoadingTables(false); });
2984
+ return () => { stopped = true; };
2985
+ }, [srcId, srcDb, srcSchema, projectPath, tablesTick]); // eslint-disable-line
2986
+
2987
+ // 轮询:running → 每 800ms;终态/NOT_FOUND 停止(NOT_FOUND = 终态即清除)
2988
+ const stopPoll = React.useCallback(() => {
2989
+ if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
2990
+ }, []);
2991
+ React.useEffect(() => stopPoll, [stopPoll]);
2992
+ function poll(id) {
2993
+ stopPoll();
2994
+ // 乱序护栏:慢响应(>800ms)晚到时本轮已被置失效,不得覆盖已到达的终态快照
2995
+ let stopped = false;
2996
+ pollRef.current = setInterval(async () => {
2997
+ if (stopped) return;
2998
+ try {
2999
+ const snap = await api("transfer/" + encodeURIComponent(id) + qs({ project: projectPath }));
3000
+ if (stopped) return; // 终态已先行(另一轮已 stopPoll):丢弃本轮旧快照
3001
+ setTask({ id, snap });
3002
+ if (!snap || snap.status !== "running") { stopped = true; stopPoll(); }
3003
+ } catch (e) {
3004
+ stopped = true;
3005
+ stopPoll();
3006
+ // 任务条目已被清理(终态保留窗口过后/被新任务 sweep):拉一次失败不报错,保持既有快照
3007
+ setTask((prev) => (prev && prev.snap && prev.snap.status !== "running" ? prev : null));
3008
+ }
3009
+ }, 800);
3010
+ }
3011
+
3012
+ // 同连接且库/模式都相同才视为同上下文:允许同连接跨库(或同库跨模式)传输
3013
+ // (后端同目标互斥仍按 targetConnId,statement 携带定位入审计)
3014
+ const sameCtx = !!srcId && srcId === dstId && (srcDb || "") === (dstDb || "") && (srcSchema || "") === (dstSchema || "");
3015
+ const canStart = !!srcId && !!dstId && selected.length > 0 && !sameCtx;
3016
+ // 禁用提示按实际缺失条件给出(sameCtx 优先:结构性约束先于勾表提醒),不得与禁因不符
3017
+ const startHint = sameCtx ? t("trSameConn") : (!srcId || !dstId) ? t("selectConn") : t("trxPickTable");
3018
+
3019
+ async function start() {
3020
+ if (busy || !canStart) return;
3021
+ setBusy(true); setError("");
3022
+ try {
3023
+ const data = await runGuarded(
3024
+ (challengeId) => api("transfer/start", {
3025
+ method: "POST",
3026
+ body: {
3027
+ projectPath, sourceConnId: srcId, targetConnId: dstId, tables: selected, writeMode, batchSize, tableConcurrency, shardConcurrency, overwriteStructure, challengeId,
3028
+ // 定位字段(undefined 序列化即省略 = 连接默认上下文,后端 optStr 语义)
3029
+ sourceDatabase: srcDb || undefined, sourceSchema: srcSchema || undefined,
3030
+ targetDatabase: dstDb || undefined, targetSchema: dstSchema || undefined,
3031
+ },
3032
+ }),
3033
+ (info) => askConfirm(Object.assign({}, info, {
3034
+ target: { conn: dstConn ? (dstConn.name || dstId) : dstId, kind: dstConn ? dstConn.kind : "", db: dstDb ? (dstDb + (dstSchema ? "." + dstSchema : "")) : t("defaultDb"), mode: dstGrant ? dstGrant.mode : "" },
3035
+ })),
3036
+ );
3037
+ const id = data && data.taskId;
3038
+ // 确认往返期间组件可能已卸载(locale 切换重建 Panel):此时不再建立轮询
3039
+ if (id && mountedRef.current) {
3040
+ setTask({ id, snap: { status: "running", tables: [], failures: [] } });
3041
+ poll(id);
3042
+ }
3043
+ } catch (e) {
3044
+ if (!e.cancelled) setError(String(e && e.message ? e.message : e));
3045
+ } finally {
3046
+ setBusy(false);
3047
+ }
3048
+ }
3049
+
3050
+ function cancel() {
3051
+ if (!task) return;
3052
+ api("transfer/" + encodeURIComponent(task.id) + "/cancel", { method: "POST", body: { projectPath } })
3053
+ .catch((e) => setError(String(e && e.message ? e.message : e)));
3054
+ }
3055
+
3056
+ const toggle = (name) => setSelected((prev) => (prev.includes(name) ? prev.filter((n) => n !== name) : prev.concat(name)));
3057
+ const [collapsed, setCollapsed] = React.useState({}); // 分组树折叠状态({ [type]: true })
3058
+ const [history, setHistory] = React.useState(null); // 终态历史条目数组(子标签激活时懒加载)
3059
+ const loadHistory = React.useCallback(() => {
3060
+ if (!projectPath) return;
3061
+ api("transfer-history" + qs({ limit: 30 }))
3062
+ .then((list) => setHistory(Array.isArray(list) ? list : []))
3063
+ .catch((e) => setError(String(e && e.message ? e.message : e)));
3064
+ }, [projectPath]);
3065
+ // 传输历史懒加载:子标签激活且顶级可见时拉取(默认激活子标签首次进入即加载,onClick 保持纯切换)
3066
+ React.useEffect(() => {
3067
+ if (visible && subTab === "history" && history === null) loadHistory();
3068
+ }, [visible, subTab, history, loadHistory]);
3069
+ // 清除确认走 askConfirm 通道(原生 confirm 已在库内移除:无 aria/焦点管理)
3070
+ async function clearHistory() {
3071
+ const yes = await props.askConfirm({
3072
+ title: t("trHistoryClear"),
3073
+ statement: t("trHistory"),
3074
+ hint: t("trHistoryClearConfirm"),
3075
+ confirmLabel: t("trHistoryClear"),
3076
+ target: { conn: "", kind: "", db: "", mode: "" },
3077
+ });
3078
+ if (!yes) return;
3079
+ api("transfer-history/clear", { method: "POST", body: {} })
3080
+ .then((r) => { setMessage(t("ok") + " (" + (r && r.removed != null ? r.removed : 0) + ")"); setHistory([]); })
3081
+ .catch((e) => setError(String(e && e.message ? e.message : e)));
3082
+ }
3083
+ const historyStatus = (s2) => s2 === "done" ? t("trStatusDone") : s2 === "failed" ? t("trStatusFailed") : s2 === "cancelled" ? t("trStatusCancelled") : s2;
3084
+ // 状态着色:failed 红 / done 绿 / 其余默认
3085
+ const statusColor = (s) => s === "failed" ? { color: "var(--dbt-danger,#ff453a)", fontWeight: 600 } : s === "done" ? { color: "var(--dbt-success,#30d158)" } : undefined;
3086
+ const statusLabel = (s) => s === "pending" ? t("trStatusPending") : s === "running" ? t("trStatusRunning") : s === "done" ? t("trStatusDone") : s === "failed" ? t("trStatusFailed") : s === "cancelled" ? t("trStatusCancelled") : s;
3087
+ const connOptions = (value, onPick, disabled, label) => React.createElement("select", { value, disabled: !!disabled || busy, "aria-label": label, onChange: (e) => onPick(e.target.value) },
3088
+ React.createElement("option", { value: "" }, t("selectConn")),
3089
+ conns.filter((c) => c.kind !== "redis").map((c) => {
3090
+ const g = (props.grants || []).find((x) => x.connId === c.id);
3091
+ const suffix = g && g.mode ? " · " + g.mode : "";
3092
+ return React.createElement("option", { key: c.id, value: c.id }, (c.name || c.id) + " (" + c.kind + suffix + ")");
3093
+ }));
3094
+ // 摘要条层级描述:pg 系「库名.模式名」,其余库层显库名,无层级/未选库显示默认库
3095
+ const scopeText = (conn, db, schema) => {
3096
+ if (!conn) return "—";
3097
+ const k = conn.kind;
3098
+ if (NO_DB_LEVEL[k] || !db) return t("defaultDb");
3099
+ return HAS_SCHEMAS[k] && schema ? db + "." + schema : db;
3100
+ };
3101
+ // 信息区键值:kind/name/host/port 直读连接元数据(URL 方式可能无 host/port,显示「—」不编造)
3102
+ function infoKv(conn, ver) {
3103
+ return React.createElement("dl", { className: "dbt-transfer-kv" },
3104
+ React.createElement("dt", null, t("trxConnType")), React.createElement("dd", null, conn.kind || "—"),
3105
+ React.createElement("dt", null, t("trxConnName")), React.createElement("dd", null, conn.name || conn.id),
3106
+ React.createElement("dt", null, t("trxHost")), React.createElement("dd", null, conn.host || "—"),
3107
+ React.createElement("dt", null, t("trxPort")), React.createElement("dd", null, conn.port != null ? String(conn.port) : "—"),
3108
+ React.createElement("dt", null, t("trxServerVer")), React.createElement("dd", null, ver || "—"));
3109
+ }
3110
+ // 单栏:栏标题(蓝)+ 连接/库(/模式)纵向下拉 + 底部信息区
3111
+ function transferCol(side) {
3112
+ const isSrc = side === "src";
3113
+ const connId = isSrc ? srcId : dstId;
3114
+ const conn = isSrc ? srcConn : dstConn;
3115
+ const kind = conn && conn.kind;
3116
+ const db = isSrc ? srcDb : dstDb;
3117
+ const onDb = isSrc ? setSrcDb : setDstDb;
3118
+ const dbs = isSrc ? srcDbs : dstDbs;
3119
+ const schemas = isSrc ? srcSchemas : dstSchemas;
3120
+ const schema = isSrc ? srcSchema : dstSchema;
3121
+ const onSchema = isSrc ? setSrcSchema : setDstSchema;
3122
+ const errText = isSrc ? dbsErr.src : dbsErr.dst;
3123
+ const titleId = "dbt-transfer-" + side + "-title";
3124
+ // mongo 源/目标栏均可选库:服务端 tables/preview 按 database 定位(mongodb/index.ts dbOf),
3125
+ // 表清单与 sourceDatabase/targetDatabase 同库对齐,无错位
3126
+ const noDbLevel = NO_DB_LEVEL[kind];
3127
+ return React.createElement("section", { className: "dbt-transfer-col", "aria-labelledby": titleId },
3128
+ React.createElement("header", { id: titleId, className: "dbt-transfer-coltitle" }, t(isSrc ? "trxSource" : "trxTarget")),
3129
+ React.createElement("label", { className: "dbt-transfer-field" },
3130
+ React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxConn")),
3131
+ connOptions(connId, isSrc ? setSrcId : setDstId, false, t("trxConn"))),
3132
+ noDbLevel
3133
+ ? React.createElement("label", { className: "dbt-transfer-field" },
3134
+ React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxDatabase")),
3135
+ React.createElement("span", { className: "dbt-muted" }, t("trxNoDbLevel")))
3136
+ : React.createElement("label", { className: "dbt-transfer-field" },
3137
+ React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxDatabase")),
3138
+ React.createElement("select", { value: db, disabled: busy || !connId, "aria-label": t("trxDatabase"), onChange: (e) => onDb(e.target.value) },
3139
+ React.createElement("option", { value: "" }, t("defaultDb")),
3140
+ (dbs || []).map((d) => React.createElement("option", { key: d, value: d }, d)))),
3141
+ errText ? React.createElement("span", { className: "dbt-err" },
3142
+ t("trxLoadDbsFail") + ":" + errText + " ",
3143
+ React.createElement("button", { className: "dbt-btn", onClick: () => { setDbsErr((s) => Object.assign({}, s, { [side]: "" })); loadDbs(side, connId); } }, t("retry"))) : null,
3144
+ HAS_SCHEMAS[kind] ? React.createElement("label", { className: "dbt-transfer-field" },
3145
+ React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxSchema")),
3146
+ React.createElement("select", { value: schema, disabled: busy || !db || !schemas, "aria-label": t("trxSchema"), onChange: (e) => onSchema(e.target.value) },
3147
+ (schemas || []).map((s) => React.createElement("option", { key: s, value: s }, s)))) : null,
3148
+ conn ? React.createElement("footer", { className: "dbt-transfer-info" },
3149
+ React.createElement("div", { className: "dbt-transfer-infotitle" }, t("trxInfo")),
3150
+ infoKv(conn, isSrc ? srcVer : dstVer)) : null,
3151
+ !isSrc && sameCtx ? React.createElement("span", { className: "dbt-err", role: "alert" }, t("trSameConn")) : null,
3152
+ );
3153
+ }
3154
+ // 表清单三态:加载中 / 失败+就地重试 / 空态(未选连接或 pg 系未选库、库内无模式、无表)/ 分组树
3155
+ function tablesBody() {
3156
+ if (loadingTables) return React.createElement("div", { className: "dbt-muted" }, t("trLoadingTables"));
3157
+ if (tablesErr) return React.createElement("div", { className: "dbt-browse-errorbox" },
3158
+ React.createElement("span", { className: "dbt-err", role: "alert" }, tablesErr),
3159
+ React.createElement("button", { className: "dbt-btn", onClick: () => setTablesTick((x) => x + 1) }, t("retry")));
3160
+ const kind = srcConn && srcConn.kind;
3161
+ if (!srcId || (HAS_SCHEMAS[kind] && !srcDb)) return React.createElement("div", { className: "dbt-muted" }, t("trxPickSource"));
3162
+ if (HAS_SCHEMAS[kind] && srcDb && srcSchemas && srcSchemas.length === 0) return React.createElement("div", { className: "dbt-muted" }, t("noSchemas"));
3163
+ if (HAS_SCHEMAS[kind] && srcDb && !srcSchema) return React.createElement("div", { className: "dbt-muted" }, t("trLoadingTables"));
3164
+ if (srcTables.length === 0) return React.createElement("div", { className: "dbt-muted" }, t("noTables"));
3165
+ // 按类型分组(TABLE/BASE TABLE→表、COLLECTION→集合、其余→其他),对齐 Navicat 数据库对象树
3166
+ const byType = new Map();
3167
+ for (const row of srcTables) {
3168
+ const key = row.type === "COLLECTION" ? "COLLECTION" : row.type === "VIEW" ? "VIEW" : row.type === "" ? "OTHER" : "TABLE";
3169
+ if (!byType.has(key)) byType.set(key, []);
3170
+ byType.get(key).push(row);
3171
+ }
3172
+ const groupLabel = (key) => key === "COLLECTION" ? t("trGroupCollection") : key === "VIEW" ? t("trGroupView") : key === "OTHER" ? t("trGroupOther") : t("trGroupTable");
3173
+ const toggleGroup = (rows, allSelected) =>
3174
+ setSelected((prev) => allSelected
3175
+ ? prev.filter((n) => !rows.some((r) => r.name === n))
3176
+ : prev.concat(rows.map((r) => r.name).filter((n) => !prev.includes(n))));
3177
+ const triRef = (all, some) => (el) => { if (el) el.indeterminate = some && !all; };
3178
+ return React.createElement("div", { className: "dbt-transfer-tablelist", role: "group", "aria-label": t("trTables") },
3179
+ [...byType.entries()].map(([key, rows]) => {
3180
+ const selCount = rows.filter((r) => selected.includes(r.name)).length;
3181
+ const all = selCount === rows.length;
3182
+ const some = selCount > 0;
3183
+ const open = !collapsed[key];
3184
+ const flip = () => setCollapsed((prev) => Object.assign({}, prev, { [key]: open }));
3185
+ return React.createElement("div", { key },
3186
+ React.createElement("div", { className: "dbt-tr-group-head", role: "button", tabIndex: 0, "aria-expanded": open,
3187
+ onClick: flip, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); flip(); } } },
3188
+ React.createElement("span", { className: "dbt-tr-caret" + (open ? " open" : ""), "aria-hidden": "true" }),
3189
+ React.createElement("input", { type: "checkbox", ref: triRef(all, some), checked: all, onChange: () => toggleGroup(rows, all), onClick: (e) => e.stopPropagation(), "aria-label": groupLabel(key) }),
3190
+ React.createElement("span", null, groupLabel(key) + " (" + selCount + "/" + rows.length + ")")),
3191
+ open ? rows.map((row) => React.createElement("div", { key: row.name, className: "dbt-tr-item", title: row.comment || undefined },
3192
+ React.createElement("label", { className: "dbt-tr-pair" },
3193
+ React.createElement("input", { type: "checkbox", checked: selected.includes(row.name), onChange: () => toggle(row.name) }),
3194
+ React.createElement("span", null, row.name),
3195
+ row.comment ? React.createElement("span", { className: "dbt-tr-comment" }, row.comment) : null))) : null);
3196
+ }));
3197
+ }
3198
+
3199
+ return React.createElement(
3200
+ "div",
3201
+ { className: "dbt-card" },
3202
+ // 顶部摘要条:源连接/层级 ⇄ 目标连接/层级(源=目标同上下文时箭头转 danger 色)
3203
+ React.createElement("div", { className: "dbt-transfer-summary", "aria-label": t("trxArrowLabel") },
3204
+ React.createElement("span", { className: "dbt-transfer-endpoint" },
3205
+ React.createElement("strong", null, srcConn ? (srcConn.name || srcId) : t("selectConn")),
3206
+ React.createElement("span", { className: "dbt-muted" }, scopeText(srcConn, srcDb, srcSchema))),
3207
+ React.createElement("span", { className: "dbt-transfer-divider", style: sameCtx ? { color: "var(--dbt-danger)" } : undefined }, transferArrow()),
3208
+ React.createElement("span", { className: "dbt-transfer-endpoint" },
3209
+ React.createElement("strong", null, dstConn ? (dstConn.name || dstId) : t("selectConn")),
3210
+ React.createElement("span", { className: "dbt-muted" }, scopeText(dstConn, dstDb, dstSchema)))),
3211
+ // 双栏:源(连接/数据库/模式)⇄ 目标(连接/数据库)
3212
+ React.createElement("div", { className: "dbt-transfer-cols" },
3213
+ transferCol("src"),
3214
+ React.createElement("div", { className: "dbt-transfer-divider" }, transferArrow()),
3215
+ transferCol("dst")),
3216
+ // 传输对象:表清单按源层级联动加载
3217
+ React.createElement("div", { className: "dbt-group" },
3218
+ React.createElement("div", { className: "dbt-listrow" },
3219
+ React.createElement("span", { className: "dbt-muted" }, t("trTables")),
3220
+ React.createElement("span", { className: "dbt-muted" }, t("trxSelectedCount", { n: selected.length })),
3221
+ React.createElement("span", { style: { flex: 1 } }),
3222
+ srcTables.length > 0 ? React.createElement("button", { className: "dbt-btn", onClick: () => setSelected(srcTables.map((x) => x.name)) }, t("trSelectAll")) : null,
3223
+ selected.length > 0 ? React.createElement("button", { className: "dbt-btn", onClick: () => setSelected([]) }, t("trClear")) : null),
3224
+ tablesBody()),
3225
+ // 传输选项(原生 details 渐进披露;四控件沿用原逻辑)
3226
+ React.createElement("details", { className: "dbt-transfer-options" },
3227
+ React.createElement("summary", null, t("trxOptions")),
3228
+ React.createElement("div", { className: "dbt-group" },
3229
+ React.createElement("div", { className: "dbt-listrow" },
3230
+ React.createElement("span", { className: "dbt-muted" }, t("trWriteMode")),
3231
+ React.createElement("select", { value: writeMode, disabled: busy, "aria-label": t("trWriteMode"), onChange: (e) => setWriteMode(e.target.value) },
3232
+ React.createElement("option", { value: "insert" }, t("trModeInsert")),
3233
+ React.createElement("option", { value: "ignore" }, t("trModeIgnore")),
3234
+ React.createElement("option", { value: "replace" }, t("trModeReplace")),
3235
+ React.createElement("option", { value: "truncate" }, t("trModeTruncate")))),
3236
+ React.createElement("div", { className: "dbt-listrow" },
3237
+ React.createElement("span", { className: "dbt-muted" }, t("trBatchSize")),
3238
+ React.createElement("input", { type: "number", min: 1, max: 5000, "aria-label": t("trBatchSize"), value: batchSize, disabled: busy, onChange: (e) => setBatchSize(Math.min(5000, Math.max(1, Math.floor(Number(e.target.value) || 1)))) })),
3239
+ React.createElement("div", { className: "dbt-listrow" },
3240
+ React.createElement("span", { className: "dbt-muted" }, t("trTableConcurrency")),
3241
+ React.createElement("input", { type: "number", min: 1, max: 16, "aria-label": t("trTableConcurrency"), value: tableConcurrency, disabled: busy, onChange: (e) => setTableConcurrency(Math.min(16, Math.max(1, Math.floor(Number(e.target.value) || 1)))) })),
3242
+ React.createElement("div", { className: "dbt-listrow" },
3243
+ React.createElement("span", { className: "dbt-muted" }, t("trShardConcurrency")),
3244
+ React.createElement("input", { type: "number", min: 1, max: 32, "aria-label": t("trShardConcurrency"), value: shardConcurrency, disabled: busy, onChange: (e) => setShardConcurrency(Math.min(32, Math.max(1, Math.floor(Number(e.target.value) || 1)))) })),
3245
+ React.createElement("label", { className: "dbt-listrow", title: t("trxOverwriteHint") },
3246
+ React.createElement("input", { type: "checkbox", checked: overwriteStructure, disabled: busy, onChange: (e) => setOverwriteStructure(e.target.checked) }),
3247
+ React.createElement("span", { className: "dbt-muted" }, t("trxOverwrite"))))),
3248
+
3249
+ // 动作行(禁用时 title 说明原因)+ 进度(仅任务存在时渲染)
3250
+ React.createElement("div", { className: "dbt-listrow" },
3251
+ React.createElement("button", { className: "dbt-btn primary", disabled: busy || !canStart, title: canStart ? undefined : startHint, onClick: () => { start(); } }, t("trStart")),
3252
+ task && task.snap && task.snap.status === "running"
3253
+ ? React.createElement("button", { className: "dbt-btn", onClick: cancel }, t("trCancel"))
3254
+ : null,
3255
+ error ? React.createElement("span", { className: "dbt-err", role: "alert" }, error) : null),
3256
+ task && task.snap
3257
+ ? React.createElement(
3258
+ "div",
3259
+ { className: "dbt-group" },
3260
+ React.createElement("div", { className: "dbt-listrow" },
3261
+ React.createElement("span", { className: "dbt-muted" }, t("trProgress")),
3262
+ React.createElement("strong", null, React.createElement("span", { "aria-live": "polite", style: statusColor(task.snap.status) }, statusLabel(task.snap.status)))),
3263
+ React.createElement("table", { className: "dbt-table" },
3264
+ React.createElement("thead", null, React.createElement("tr", null,
3265
+ React.createElement("th", null, t("trTables")),
3266
+ React.createElement("th", null, t("trStatus")),
3267
+ React.createElement("th", null, t("trRows")),
3268
+ React.createElement("th", null, t("trShards")))),
3269
+ React.createElement("tbody", null, (task.snap.tables || []).map((tb) =>
3270
+ React.createElement("tr", { key: tb.name },
3271
+ React.createElement("td", null, tb.name),
3272
+ React.createElement("td", { style: statusColor(tb.status) }, statusLabel(tb.status)),
3273
+ React.createElement("td", null, String(tb.rows) + (tb.totalRows != null ? " / " + tb.totalRows + "(" + pct(tb.rows, tb.totalRows) + ")" : "")),
3274
+ React.createElement("td", null, tb.shardsDone + "/" + tb.shardsTotal + (tb.shardsFailed > 0 ? " (" + tb.shardsFailed + "×)" : "")))))),
3275
+ (task.snap.failures || []).length > 0
3276
+ ? React.createElement("div", { className: "dbt-group" },
3277
+ React.createElement("div", { className: "dbt-muted" }, t("trFailures")),
3278
+ task.snap.failures.map((f, i) => React.createElement("div", { key: i, className: "dbt-err" },
3279
+ f.table + (f.shard >= 0 ? " #" + f.shard : "") + ": " + f.error)))
3280
+ : null)
3281
+ : null,
3282
+ // 传输历史/传输日志:同级子标签(分段控件复用 .dbt-tabs),内容常驻挂载 + hidden 切换(切换不丢状态)
3283
+ React.createElement("div", { className: "dbt-tabs", role: "tablist" },
3284
+ [["history", t("trHistory")], ["tlog", t("viewTransferLog")]].map(([v, label]) =>
3285
+ React.createElement("button", {
3286
+ key: v, className: subTab === v ? "active" : "",
3287
+ role: "tab", "aria-selected": subTab === v,
3288
+ onClick: () => setSubTab(v),
3289
+ }, label)),
3290
+ ),
3291
+ React.createElement("div", { hidden: subTab !== "history" },
3292
+ React.createElement("div", { className: "dbt-group" },
3293
+ React.createElement("div", { className: "dbt-listrow" },
3294
+ React.createElement("span", { style: { flex: 1 } }),
3295
+ history !== null && history.length > 0
3296
+ ? React.createElement("button", { className: "dbt-btn", onClick: () => { clearHistory(); } }, t("trHistoryClear"))
3297
+ : null),
3298
+ history === null
3299
+ ? React.createElement("div", { className: "dbt-muted" }, "…")
3300
+ : history.length === 0
3301
+ ? React.createElement("div", { className: "dbt-muted" }, t("trHistoryEmpty"))
3302
+ : history.map((h) => React.createElement("div", { key: h.taskId + h.ts, className: "dbt-tr-item", title: h.statement },
3303
+ React.createElement("span", { className: "dbt-muted", style: { flex: "none" } }, new Date(h.ts).toLocaleString()),
3304
+ React.createElement("strong", { style: statusColor(h.status) }, historyStatus(h.status)),
3305
+ React.createElement("span", { className: "dbt-tr-comment" },
3306
+ ((h.statement.match(/表\[[^\]]*\]/) || [""])[0].slice(0, 80))
3307
+ + " · " + (h.tables || []).filter((t2) => t2.status === "done").length + "✓/"
3308
+ + (h.tables || []).filter((t2) => t2.status === "failed").length + "✗"
3309
+ + ((h.failures || []).length > 0 ? " · " + t("trFailures") + " " + h.failures.length : ""))))),
3310
+ ),
3311
+ // 传输日志:props 直取原始入参别名(2843-2846 行绑定),不经过本地遮蔽的 busy/error(那是传输任务自己的状态);
3312
+ // visible=顶级标签激活且当前在日志子标签(切走即停 5s 轮询)
3313
+ React.createElement("div", { hidden: subTab !== "tlog" },
3314
+ React.createElement(TransferLogView, { projectPath, visible: visible && subTab === "tlog", askConfirm })),
3315
+ );
3316
+ }
3317
+ /* ---------------- 传输日志(「数据传输」内子标签:实时事件流,5s 自动刷新) ---------------- */
3318
+ function TransferLogView(props) {
3319
+ const projectPath = props.projectPath;
3320
+ const [rows, setRows] = React.useState(null); // 日志条目数组
3321
+ const [auto, setAuto] = React.useState(true);
3322
+ const [error, setError] = React.useState("");
3323
+
3324
+ const load = React.useCallback(() => {
3325
+ api("transfer-log" + qs({ project: projectPath, limit: 300 }))
3326
+ .then((list) => setRows(Array.isArray(list) ? list : []))
3327
+ .catch((e) => setError(String(e && e.message ? e.message : e)));
3328
+ }, [projectPath]);
3329
+
3330
+ // 子标签可见时 5s 轮询;隐藏即真正清除定时器(切走即停),重进/进入时立即拉取一次
3331
+ // (auto 关闭时这次拉取是唯一加载路径,等价替代已删除的手动刷新按钮)
3332
+ React.useEffect(() => {
3333
+ if (!props.visible) return undefined;
3334
+ load();
3335
+ if (!auto) return undefined;
3336
+ const timer = setInterval(load, 5000);
3337
+ return () => clearInterval(timer);
3338
+ }, [props.visible, auto, load]);
3339
+
3340
+ async function clearLog() {
3341
+ const yes = await props.askConfirm({
3342
+ title: t("tlogClear"),
3343
+ statement: t("viewTransferLog"),
3344
+ hint: t("tlogClearConfirm"),
3345
+ confirmLabel: t("tlogClear"),
3346
+ target: { conn: "", kind: "", db: "", mode: "" },
3347
+ });
3348
+ if (!yes) return;
3349
+ api("transfer-history/clear", { method: "POST", body: {} })
3350
+ .then(() => load())
3351
+ .catch((e) => setError(String(e && e.message ? e.message : e)));
3352
+ }
3353
+
3354
+ const typeLabel = (ty) => ty === "start" ? t("tlogTypeStart") : ty === "table" ? t("tlogTypeTable") : ty === "progress" ? t("tlogTypeProgress") : ty === "finish" ? t("tlogTypeFinish") : ty;
3355
+
3356
+ return React.createElement(
3357
+ "div",
3358
+ { className: "dbt-card" },
3359
+ React.createElement("div", { className: "dbt-listrow" },
3360
+ React.createElement("label", { className: "dbt-listrow", style: { flex: "none" } },
3361
+ React.createElement("input", { type: "checkbox", checked: auto, onChange: (e) => setAuto(e.target.checked) }),
3362
+ React.createElement("span", { className: "dbt-muted" }, t("tlogAuto"))),
3363
+ React.createElement("span", { style: { flex: 1 } }),
3364
+ React.createElement("button", { className: "dbt-btn", onClick: () => { clearLog(); } }, t("tlogClear"))),
3365
+ error ? React.createElement("div", { className: "dbt-err", role: "alert" }, error) : null,
3366
+ rows === null
3367
+ ? React.createElement("div", { className: "dbt-muted" }, "…")
3368
+ : rows.length === 0
3369
+ ? React.createElement("div", { className: "dbt-muted" }, t("tlogEmpty"))
3370
+ : React.createElement("div", { className: "dbt-transfer-tablelist", style: { maxHeight: 320 } },
3371
+ rows.map((r, i) => {
3372
+ const brief = r.type === "start"
3373
+ ? (r.statement || "").replace(/表\[[^\]]*\]/, "表[" + ((r.statement.match(/表\[([^\]]*)\]/) || ["", ""])[1].split(",").length) + "张]")
3374
+ : r.type === "table" || r.type === "progress"
3375
+ ? (r.table || "") + " " + (r.rows != null ? r.rows : 0) + (r.totalRows != null ? " / " + r.totalRows : "") + " 行" + (r.status === "failed" ? " " + (r.statusLabel || "") : "")
3376
+ : (r.statement || "").slice(0, 60) + " " + (r.status || "");
3377
+ return React.createElement("div", { key: r.ts + i, className: "dbt-tr-item" },
3378
+ React.createElement("span", { className: "dbt-muted", style: { flex: "none", minWidth: 84 } }, new Date(r.ts).toLocaleTimeString()),
3379
+ React.createElement("span", { className: "dbt-tr-chip", style: { flex: "none" } }, typeLabel(r.type)),
3380
+ React.createElement("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, brief));
3381
+ })),
3382
+ );
3383
+ }
3384
+
2619
3385
  /* ---------------- 根面板 ---------------- */
2620
3386
  function Panel(props) {
2621
3387
  const visible = props.visible;
@@ -2629,6 +3395,11 @@ window.__ModuleLoader__.load({
2629
3395
  const [error, setError] = React.useState("");
2630
3396
  const [message, setMessage] = React.useState("");
2631
3397
  const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
3398
+ const [treeRefresh, setTreeRefresh] = React.useState(0); // 浏览树刷新信号(手动按钮 / 切回可见自动)
3399
+ // 侧边栏切回可见时自动重拉已展开层:agents 等外部建库/建表后切回来即可见,无需手点刷新
3400
+ React.useEffect(() => {
3401
+ if (visible) setTreeRefresh((x) => x + 1);
3402
+ }, [visible]);
2632
3403
 
2633
3404
  const askConfirm = React.useCallback((info) => new Promise((resolve) => {
2634
3405
  setConfirmReq(Object.assign({}, info, { resolve }));
@@ -2681,6 +3452,7 @@ window.__ModuleLoader__.load({
2681
3452
  manage: React.createElement(ManageView, shared),
2682
3453
  grants: React.createElement(GrantsView, shared),
2683
3454
  console: React.createElement(ConsoleView, shared),
3455
+ transfer: React.createElement(TransferView, Object.assign({}, shared, { visible })),
2684
3456
  };
2685
3457
  return React.createElement(
2686
3458
  "div",
@@ -2688,7 +3460,7 @@ window.__ModuleLoader__.load({
2688
3460
  React.createElement(
2689
3461
  "div",
2690
3462
  { className: "dbt-tabs", role: "tablist" },
2691
- [["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")]].map(([v, label]) =>
3463
+ [["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")], ["transfer", t("viewTransfer")]].map(([v, label]) =>
2692
3464
  React.createElement("button", {
2693
3465
  key: v, className: view === v ? "active" : "",
2694
3466
  role: "tab", "aria-selected": view === v,
@@ -2714,21 +3486,31 @@ window.__ModuleLoader__.load({
2714
3486
  React.createElement("span", { className: "dbt-muted" }, t("projectLabel", { path: projectPath })),
2715
3487
  // 编辑图标:极简铅笔 SVG(验收要求无 emoji;svg aria-hidden,按钮 aria-label 提供语义)
2716
3488
  React.createElement("button", {
2717
- className: "dbt-btn", onClick: () => setProjectEdited(true), "aria-label": t("edit"),
2718
- },
2719
- React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
2720
- React.createElement("path", {
2721
- d: "M8.6 1.4 L10.6 3.4 L4.2 9.8 L1.6 10.4 L2.2 7.8 Z",
2722
- stroke: "currentColor", strokeWidth: 1.2, strokeLinejoin: "round",
2723
- })),
2724
- )),
3489
+ className: "dbt-btn", onClick: () => setProjectEdited(true), "aria-label": t("edit"),
3490
+ },
3491
+ React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
3492
+ React.createElement("path", {
3493
+ d: "M8.6 1.4 L10.6 3.4 L4.2 9.8 L1.6 10.4 L2.2 7.8 Z",
3494
+ stroke: "currentColor", strokeWidth: 1.2, strokeLinejoin: "round",
3495
+ })),
3496
+ ),
3497
+ // 刷新:重拉所有已展开的库/模式/表(agents 等外部建库后点一下即可见)
3498
+ React.createElement("button", {
3499
+ className: "dbt-btn", onClick: () => setTreeRefresh((x) => x + 1), "aria-label": t("trRefresh"), title: t("trRefresh"),
3500
+ },
3501
+ React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
3502
+ React.createElement("path", {
3503
+ d: "M10.5 6a4.5 4.5 0 1 1-1.32-3.18M10.5 1v2.5H8",
3504
+ stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round", strokeLinejoin: "round",
3505
+ })),
3506
+ )),
2725
3507
  error ? React.createElement("div", { className: "dbt-err", "aria-live": "polite" }, t("error") + ": " + error) : null,
2726
3508
  message ? React.createElement("div", { className: "dbt-msg", "aria-live": "polite" }, message) : null,
2727
3509
  // 视图常驻挂载 + hidden 切换:切 tab 保留各视图内部状态(控制台 SQL、浏览选中不丢);
2728
3510
  // dbt-in 入场动画只在面板首次打开播一次(.dbt-view 类自带,常驻后不随 tab 重播);
2729
3511
  // browse 撑满面板剩余高度(BrowsePane 内详情栏才能钉在面板最底部),active 控制首次激活才拉树。
2730
3512
  // .dbt-view 类的 display:flex 会盖过 hidden 的 UA 样式,故内联 display 同步切换
2731
- ["manage", "grants", "browse", "console"].map((v) =>
3513
+ ["manage", "grants", "browse", "console", "transfer"].map((v) =>
2732
3514
  React.createElement("div", {
2733
3515
  key: v, className: "dbt-view", hidden: view !== v,
2734
3516
  style: v === "browse"
@@ -2736,7 +3518,7 @@ window.__ModuleLoader__.load({
2736
3518
  : (view === v ? undefined : { display: "none" }),
2737
3519
  },
2738
3520
  v === "browse"
2739
- ? React.createElement(BrowsePane, { ctx: props.ctx, conns, projectPath, grants, askConfirm, active: view === "browse" })
3521
+ ? React.createElement(BrowsePane, { ctx: props.ctx, conns, projectPath, grants, askConfirm, active: view === "browse", refreshTick: treeRefresh })
2740
3522
  : viewEls[v])),
2741
3523
  React.createElement("div", { className: "dbt-muted" }, t("footerHint")),
2742
3524
  confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
@@ -2800,13 +3582,14 @@ window.__ModuleLoader__.load({
2800
3582
  }
2801
3583
  exports.apply = apply;
2802
3584
  exports.inject = inject;
2803
- // 测试面:单元格写回 + 控制台工具纯函数 + ConsoleView(集成测试浅渲染元素树)
3585
+ // 测试面:单元格写回 + 控制台工具纯函数 + ConsoleView(集成测试浅渲染元素树)+ Panel/TransferView/TransferLogView(传输日志降级结构断言)
2804
3586
  exports.__testables = {
2805
3587
  dialectOf, quoteIdent, parseCellText, isTruncatedCell, isBlobCell,
2806
3588
  buildUpdate, buildRedisOp, buildMongoOp, renderStatementWithParams,
2807
3589
  splitSqlStatements, fmtDialectOf, toCsv, toJson, fmtMs,
2808
3590
  sortRows, slicePage, ResultSetGrid,
2809
3591
  classifyHead, ConsoleView,
3592
+ Panel, TransferView, TransferLogView,
2810
3593
  };
2811
3594
  return module.exports;
2812
3595
  },